MarkdownTokens.svelte 7.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254
  1. <script lang="ts">
  2. import DOMPurify from 'dompurify';
  3. import { createEventDispatcher, onMount, getContext } from 'svelte';
  4. const i18n = getContext('i18n');
  5. import fileSaver from 'file-saver';
  6. const { saveAs } = fileSaver;
  7. import { marked, type Token } from 'marked';
  8. import { revertSanitizedResponseContent, unescapeHtml } from '$lib/utils';
  9. import { WEBUI_BASE_URL } from '$lib/constants';
  10. import CodeBlock from '$lib/components/chat/Messages/CodeBlock.svelte';
  11. import MarkdownInlineTokens from '$lib/components/chat/Messages/Markdown/MarkdownInlineTokens.svelte';
  12. import KatexRenderer from './KatexRenderer.svelte';
  13. import Collapsible from '$lib/components/common/Collapsible.svelte';
  14. import Tooltip from '$lib/components/common/Tooltip.svelte';
  15. import ArrowDownTray from '$lib/components/icons/ArrowDownTray.svelte';
  16. const dispatch = createEventDispatcher();
  17. export let id: string;
  18. export let tokens: Token[];
  19. export let top = true;
  20. export let save = false;
  21. export let onSourceClick: Function = () => {};
  22. const headerComponent = (depth: number) => {
  23. return 'h' + depth;
  24. };
  25. const exportTableToCSVHandler = (token, tokenIdx = 0) => {
  26. console.log('Exporting table to CSV');
  27. // Create an array for rows that will hold the mapped cell text.
  28. const rows = token.rows.map((row) =>
  29. row.map((cell) => cell.tokens.map((token) => token.text).join(''))
  30. );
  31. // Join the rows using commas (,) as the separator and rows using newline (\n).
  32. const csvContent = rows.map((row) => row.join(',')).join('\n');
  33. // Log rows and CSV content to ensure everything is correct.
  34. console.log(rows);
  35. console.log(csvContent);
  36. // To handle Unicode characters, you need to prefix the data with a BOM:
  37. const bom = '\uFEFF'; // BOM for UTF-8
  38. // Create a new Blob prefixed with the BOM to ensure proper Unicode encoding.
  39. const blob = new Blob([bom + csvContent], { type: 'text/csv;charset=UTF-8' });
  40. // Use FileSaver.js's saveAs function to save the generated CSV file.
  41. saveAs(blob, `table-${id}-${tokenIdx}.csv`);
  42. };
  43. </script>
  44. <!-- {JSON.stringify(tokens)} -->
  45. {#each tokens as token, tokenIdx (tokenIdx)}
  46. {#if token.type === 'hr'}
  47. <hr />
  48. {:else if token.type === 'heading'}
  49. <svelte:element this={headerComponent(token.depth)}>
  50. <MarkdownInlineTokens id={`${id}-${tokenIdx}-h`} tokens={token.tokens} {onSourceClick} />
  51. </svelte:element>
  52. {:else if token.type === 'code'}
  53. {#if token.raw.includes('```')}
  54. <CodeBlock
  55. id={`${id}-${tokenIdx}`}
  56. {token}
  57. lang={token?.lang ?? ''}
  58. code={revertSanitizedResponseContent(token?.text ?? '')}
  59. {save}
  60. on:code={(e) => {
  61. dispatch('code', e.detail);
  62. }}
  63. on:save={(e) => {
  64. dispatch('update', {
  65. raw: token.raw,
  66. oldContent: token.text,
  67. newContent: e.detail
  68. });
  69. }}
  70. />
  71. {:else}
  72. {token.text}
  73. {/if}
  74. {:else if token.type === 'table'}
  75. <div class="relative w-full group">
  76. <div class="scrollbar-hidden relative overflow-x-auto max-w-full rounded-lg">
  77. <table
  78. class=" w-full text-sm text-left text-gray-500 dark:text-gray-400 max-w-full rounded-xl"
  79. >
  80. <thead
  81. class="text-xs text-gray-700 uppercase bg-gray-50 dark:bg-gray-850 dark:text-gray-400 border-none"
  82. >
  83. <tr class="">
  84. {#each token.header as header, headerIdx}
  85. <th
  86. scope="col"
  87. class="!px-3 !py-1.5 whitespace-nowrap break-words cursor-pointer select-none border border-gray-50 dark:border-gray-850"
  88. style={token.align[headerIdx] ? '' : `text-align: ${token.align[headerIdx]}`}
  89. >
  90. <div class="flex flex-col gap-1.5 text-left">
  91. <div class="flex-shrink-0">
  92. <MarkdownInlineTokens
  93. id={`${id}-${tokenIdx}-header-${headerIdx}`}
  94. tokens={header.tokens}
  95. {onSourceClick}
  96. />
  97. </div>
  98. </div>
  99. </th>
  100. {/each}
  101. </tr>
  102. </thead>
  103. <tbody>
  104. {#each token.rows as row, rowIdx}
  105. <tr class="bg-white dark:bg-gray-900 dark:border-gray-850 text-xs">
  106. {#each row ?? [] as cell, cellIdx}
  107. <td
  108. class="!px-3 !py-1.5 text-gray-900 dark:text-white w-max border border-gray-50 dark:border-gray-850"
  109. style={token.align[cellIdx] ? '' : `text-align: ${token.align[cellIdx]}`}
  110. >
  111. <div class="flex flex-col break-normal">
  112. <MarkdownInlineTokens
  113. id={`${id}-${tokenIdx}-row-${rowIdx}-${cellIdx}`}
  114. tokens={cell.tokens}
  115. {onSourceClick}
  116. />
  117. </div>
  118. </td>
  119. {/each}
  120. </tr>
  121. {/each}
  122. </tbody>
  123. </table>
  124. </div>
  125. <div class=" absolute top-1 right-1.5 z-20 invisible group-hover:visible">
  126. <Tooltip content={$i18n.t('Export to CSV')}>
  127. <button
  128. class="p-1 rounded-lg bg-transparent transition"
  129. on:click={(e) => {
  130. e.stopPropagation();
  131. exportTableToCSVHandler(token, tokenIdx);
  132. }}
  133. >
  134. <ArrowDownTray className=" size-3.5" strokeWidth="1.5" />
  135. </button>
  136. </Tooltip>
  137. </div>
  138. </div>
  139. {:else if token.type === 'blockquote'}
  140. <blockquote>
  141. <svelte:self id={`${id}-${tokenIdx}`} tokens={token.tokens} />
  142. </blockquote>
  143. {:else if token.type === 'list'}
  144. {#if token.ordered}
  145. <ol start={token.start || 1}>
  146. {#each token.items as item, itemIdx}
  147. <li>
  148. <svelte:self
  149. id={`${id}-${tokenIdx}-${itemIdx}`}
  150. tokens={item.tokens}
  151. top={token.loose}
  152. />
  153. </li>
  154. {/each}
  155. </ol>
  156. {:else}
  157. <ul>
  158. {#each token.items as item, itemIdx}
  159. <li>
  160. <svelte:self
  161. id={`${id}-${tokenIdx}-${itemIdx}`}
  162. tokens={item.tokens}
  163. top={token.loose}
  164. />
  165. </li>
  166. {/each}
  167. </ul>
  168. {/if}
  169. {:else if token.type === 'details'}
  170. <Collapsible title={token.summary} className="w-fit space-y-1">
  171. <div class=" mb-1.5" slot="content">
  172. <svelte:self id={`${id}-${tokenIdx}-d`} tokens={marked.lexer(token.text)} />
  173. </div>
  174. </Collapsible>
  175. {:else if token.type === 'html'}
  176. {@const html = DOMPurify.sanitize(token.text)}
  177. {#if html && html.includes('<video')}
  178. {@html html}
  179. {:else if token.text.includes(`<iframe src="${WEBUI_BASE_URL}/api/v1/files/`)}
  180. {@html `${token.text}`}
  181. {:else}
  182. {token.text}
  183. {/if}
  184. {:else if token.type === 'iframe'}
  185. <iframe
  186. src="{WEBUI_BASE_URL}/api/v1/files/{token.fileId}/content"
  187. title={token.fileId}
  188. width="100%"
  189. frameborder="0"
  190. onload="this.style.height=(this.contentWindow.document.body.scrollHeight+20)+'px';"
  191. ></iframe>
  192. {:else if token.type === 'paragraph'}
  193. <p>
  194. <MarkdownInlineTokens
  195. id={`${id}-${tokenIdx}-p`}
  196. tokens={token.tokens ?? []}
  197. {onSourceClick}
  198. />
  199. </p>
  200. {:else if token.type === 'text'}
  201. {#if top}
  202. <p>
  203. {#if token.tokens}
  204. <MarkdownInlineTokens id={`${id}-${tokenIdx}-t`} tokens={token.tokens} {onSourceClick} />
  205. {:else}
  206. {unescapeHtml(token.text)}
  207. {/if}
  208. </p>
  209. {:else if token.tokens}
  210. <MarkdownInlineTokens
  211. id={`${id}-${tokenIdx}-p`}
  212. tokens={token.tokens ?? []}
  213. {onSourceClick}
  214. />
  215. {:else}
  216. {unescapeHtml(token.text)}
  217. {/if}
  218. {:else if token.type === 'inlineKatex'}
  219. {#if token.text}
  220. <KatexRenderer
  221. content={revertSanitizedResponseContent(token.text)}
  222. displayMode={token?.displayMode ?? false}
  223. />
  224. {/if}
  225. {:else if token.type === 'blockKatex'}
  226. {#if token.text}
  227. <KatexRenderer
  228. content={revertSanitizedResponseContent(token.text)}
  229. displayMode={token?.displayMode ?? false}
  230. />
  231. {/if}
  232. {:else if token.type === 'space'}
  233. <div class="my-2" />
  234. {:else}
  235. {console.log('Unknown token', token)}
  236. {/if}
  237. {/each}