MarkdownTokens.svelte 4.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180
  1. <script lang="ts">
  2. import DOMPurify from 'dompurify';
  3. import { createEventDispatcher, onMount } from 'svelte';
  4. import { marked, type Token } from 'marked';
  5. import { revertSanitizedResponseContent, unescapeHtml } from '$lib/utils';
  6. import { WEBUI_BASE_URL } from '$lib/constants';
  7. import CodeBlock from '$lib/components/chat/Messages/CodeBlock.svelte';
  8. import MarkdownInlineTokens from '$lib/components/chat/Messages/Markdown/MarkdownInlineTokens.svelte';
  9. import KatexRenderer from './KatexRenderer.svelte';
  10. import Collapsible from '$lib/components/common/Collapsible.svelte';
  11. const dispatch = createEventDispatcher();
  12. export let id: string;
  13. export let tokens: Token[];
  14. export let top = true;
  15. export let save = false;
  16. const headerComponent = (depth: number) => {
  17. return 'h' + depth;
  18. };
  19. </script>
  20. <!-- {JSON.stringify(tokens)} -->
  21. {#each tokens as token, tokenIdx (tokenIdx)}
  22. {#if token.type === 'hr'}
  23. <hr />
  24. {:else if token.type === 'heading'}
  25. <svelte:element this={headerComponent(token.depth)}>
  26. <MarkdownInlineTokens id={`${id}-${tokenIdx}-h`} tokens={token.tokens} />
  27. </svelte:element>
  28. {:else if token.type === 'code'}
  29. {#if token.lang === 'html'}
  30. {dispatch('code', {
  31. lang: token.lang
  32. })}
  33. {/if}
  34. {#if token.raw.includes('```')}
  35. <CodeBlock
  36. id={`${id}-${tokenIdx}`}
  37. {token}
  38. lang={token?.lang ?? ''}
  39. code={revertSanitizedResponseContent(token?.text ?? '')}
  40. {save}
  41. on:save={(e) => {
  42. dispatch('update', {
  43. raw: token.raw,
  44. oldContent: token.text,
  45. newContent: e.detail
  46. });
  47. }}
  48. />
  49. {:else}
  50. {token.text}
  51. {/if}
  52. {:else if token.type === 'table'}
  53. <div class="scrollbar-hidden relative whitespace-nowrap overflow-x-auto max-w-full">
  54. <table class="w-full">
  55. <thead>
  56. <tr>
  57. {#each token.header as header, headerIdx}
  58. <th style={token.align[headerIdx] ? '' : `text-align: ${token.align[headerIdx]}`}>
  59. <MarkdownInlineTokens
  60. id={`${id}-${tokenIdx}-header-${headerIdx}`}
  61. tokens={header.tokens}
  62. />
  63. </th>
  64. {/each}
  65. </tr>
  66. </thead>
  67. <tbody>
  68. {#each token.rows as row, rowIdx}
  69. <tr>
  70. {#each row ?? [] as cell, cellIdx}
  71. <td style={token.align[cellIdx] ? '' : `text-align: ${token.align[cellIdx]}`}>
  72. <MarkdownInlineTokens
  73. id={`${id}-${tokenIdx}-row-${rowIdx}-${cellIdx}`}
  74. tokens={cell.tokens}
  75. />
  76. </td>
  77. {/each}
  78. </tr>
  79. {/each}
  80. </tbody>
  81. </table>
  82. </div>
  83. {:else if token.type === 'blockquote'}
  84. <blockquote>
  85. <svelte:self id={`${id}-${tokenIdx}`} tokens={token.tokens} />
  86. </blockquote>
  87. {:else if token.type === 'list'}
  88. {#if token.ordered}
  89. <ol start={token.start || 1}>
  90. {#each token.items as item, itemIdx}
  91. <li>
  92. <svelte:self
  93. id={`${id}-${tokenIdx}-${itemIdx}`}
  94. tokens={item.tokens}
  95. top={token.loose}
  96. />
  97. </li>
  98. {/each}
  99. </ol>
  100. {:else}
  101. <ul>
  102. {#each token.items as item, itemIdx}
  103. <li>
  104. <svelte:self
  105. id={`${id}-${tokenIdx}-${itemIdx}`}
  106. tokens={item.tokens}
  107. top={token.loose}
  108. />
  109. </li>
  110. {/each}
  111. </ul>
  112. {/if}
  113. {:else if token.type === 'details'}
  114. <Collapsible title={token.summary} className="w-fit space-y-1">
  115. <div class=" mb-1.5" slot="content">
  116. <svelte:self id={`${id}-${tokenIdx}-d`} tokens={marked.lexer(token.text)} />
  117. </div>
  118. </Collapsible>
  119. {:else if token.type === 'html'}
  120. {@const html = DOMPurify.sanitize(token.text)}
  121. {#if html && html.includes('<video')}
  122. {@html html}
  123. {:else if token.text.includes(`<iframe src="${WEBUI_BASE_URL}/api/v1/files/`)}
  124. {@html `${token.text}`}
  125. {:else}
  126. {token.text}
  127. {/if}
  128. {:else if token.type === 'iframe'}
  129. <iframe
  130. src="{WEBUI_BASE_URL}/api/v1/files/{token.fileId}/content"
  131. title={token.fileId}
  132. width="100%"
  133. frameborder="0"
  134. onload="this.style.height=(this.contentWindow.document.body.scrollHeight+20)+'px';"
  135. ></iframe>
  136. {:else if token.type === 'paragraph'}
  137. <p>
  138. <MarkdownInlineTokens id={`${id}-${tokenIdx}-p`} tokens={token.tokens ?? []} />
  139. </p>
  140. {:else if token.type === 'text'}
  141. {#if top}
  142. <p>
  143. {#if token.tokens}
  144. <MarkdownInlineTokens id={`${id}-${tokenIdx}-t`} tokens={token.tokens} />
  145. {:else}
  146. {unescapeHtml(token.text)}
  147. {/if}
  148. </p>
  149. {:else if token.tokens}
  150. <MarkdownInlineTokens id={`${id}-${tokenIdx}-p`} tokens={token.tokens ?? []} />
  151. {:else}
  152. {unescapeHtml(token.text)}
  153. {/if}
  154. {:else if token.type === 'inlineKatex'}
  155. {#if token.text}
  156. <KatexRenderer
  157. content={revertSanitizedResponseContent(token.text)}
  158. displayMode={token?.displayMode ?? false}
  159. />
  160. {/if}
  161. {:else if token.type === 'blockKatex'}
  162. {#if token.text}
  163. <KatexRenderer
  164. content={revertSanitizedResponseContent(token.text)}
  165. displayMode={token?.displayMode ?? false}
  166. />
  167. {/if}
  168. {:else if token.type === 'space'}
  169. <div class="my-2" />
  170. {:else}
  171. {console.log('Unknown token', token)}
  172. {/if}
  173. {/each}