MarkdownTokens.svelte 4.9 KB

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