MarkdownTokens.svelte 8.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314
  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 { 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 attributes = {};
  21. export let save = false;
  22. export let onTaskClick: Function = () => {};
  23. export let onSourceClick: Function = () => {};
  24. const headerComponent = (depth: number) => {
  25. return 'h' + depth;
  26. };
  27. const exportTableToCSVHandler = (token, tokenIdx = 0) => {
  28. console.log('Exporting table to CSV');
  29. // Extract header row text and escape for CSV.
  30. const header = token.header.map((headerCell) => `"${headerCell.text.replace(/"/g, '""')}"`);
  31. // Create an array for rows that will hold the mapped cell text.
  32. const rows = token.rows.map((row) =>
  33. row.map((cell) => {
  34. // Map tokens into a single text
  35. const cellContent = cell.tokens.map((token) => token.text).join('');
  36. // Escape double quotes and wrap the content in double quotes
  37. return `"${cellContent.replace(/"/g, '""')}"`;
  38. })
  39. );
  40. // Combine header and rows
  41. const csvData = [header, ...rows];
  42. // Join the rows using commas (,) as the separator and rows using newline (\n).
  43. const csvContent = csvData.map((row) => row.join(',')).join('\n');
  44. // Log rows and CSV content to ensure everything is correct.
  45. console.log(csvData);
  46. console.log(csvContent);
  47. // To handle Unicode characters, you need to prefix the data with a BOM:
  48. const bom = '\uFEFF'; // BOM for UTF-8
  49. // Create a new Blob prefixed with the BOM to ensure proper Unicode encoding.
  50. const blob = new Blob([bom + csvContent], { type: 'text/csv;charset=UTF-8' });
  51. // Use FileSaver.js's saveAs function to save the generated CSV file.
  52. saveAs(blob, `table-${id}-${tokenIdx}.csv`);
  53. };
  54. </script>
  55. <!-- {JSON.stringify(tokens)} -->
  56. {#each tokens as token, tokenIdx (tokenIdx)}
  57. {#if token.type === 'hr'}
  58. <hr class=" border-gray-100 dark:border-gray-850" />
  59. {:else if token.type === 'heading'}
  60. <svelte:element this={headerComponent(token.depth)} dir="auto">
  61. <MarkdownInlineTokens id={`${id}-${tokenIdx}-h`} tokens={token.tokens} {onSourceClick} />
  62. </svelte:element>
  63. {:else if token.type === 'code'}
  64. {#if token.raw.includes('```')}
  65. <CodeBlock
  66. id={`${id}-${tokenIdx}`}
  67. {token}
  68. lang={token?.lang ?? ''}
  69. code={token?.text ?? ''}
  70. {attributes}
  71. {save}
  72. onCode={(value) => {
  73. dispatch('code', value);
  74. }}
  75. onSave={(e) => {
  76. dispatch('update', {
  77. raw: token.raw,
  78. oldContent: token.text,
  79. newContent: value
  80. });
  81. }}
  82. />
  83. {:else}
  84. {token.text}
  85. {/if}
  86. {:else if token.type === 'table'}
  87. <div class="relative w-full group">
  88. <div class="scrollbar-hidden relative overflow-x-auto max-w-full rounded-lg">
  89. <table
  90. class=" w-full text-sm text-left text-gray-500 dark:text-gray-400 max-w-full rounded-xl"
  91. >
  92. <thead
  93. class="text-xs text-gray-700 uppercase bg-gray-50 dark:bg-gray-850 dark:text-gray-400 border-none"
  94. >
  95. <tr class="">
  96. {#each token.header as header, headerIdx}
  97. <th
  98. scope="col"
  99. class="px-3! py-1.5! cursor-pointer border border-gray-100 dark:border-gray-850"
  100. style={token.align[headerIdx] ? '' : `text-align: ${token.align[headerIdx]}`}
  101. >
  102. <div class="flex flex-col gap-1.5 text-left">
  103. <div class="shrink-0 break-normal">
  104. <MarkdownInlineTokens
  105. id={`${id}-${tokenIdx}-header-${headerIdx}`}
  106. tokens={header.tokens}
  107. {onSourceClick}
  108. />
  109. </div>
  110. </div>
  111. </th>
  112. {/each}
  113. </tr>
  114. </thead>
  115. <tbody>
  116. {#each token.rows as row, rowIdx}
  117. <tr class="bg-white dark:bg-gray-900 dark:border-gray-850 text-xs">
  118. {#each row ?? [] as cell, cellIdx}
  119. <td
  120. class="px-3! py-1.5! text-gray-900 dark:text-white w-max border border-gray-100 dark:border-gray-850"
  121. style={token.align[cellIdx] ? '' : `text-align: ${token.align[cellIdx]}`}
  122. >
  123. <div class="flex flex-col break-normal">
  124. <MarkdownInlineTokens
  125. id={`${id}-${tokenIdx}-row-${rowIdx}-${cellIdx}`}
  126. tokens={cell.tokens}
  127. {onSourceClick}
  128. />
  129. </div>
  130. </td>
  131. {/each}
  132. </tr>
  133. {/each}
  134. </tbody>
  135. </table>
  136. </div>
  137. <div class=" absolute top-1 right-1.5 z-20 invisible group-hover:visible">
  138. <Tooltip content={$i18n.t('Export to CSV')}>
  139. <button
  140. class="p-1 rounded-lg bg-transparent transition"
  141. on:click={(e) => {
  142. e.stopPropagation();
  143. exportTableToCSVHandler(token, tokenIdx);
  144. }}
  145. >
  146. <ArrowDownTray className=" size-3.5" strokeWidth="1.5" />
  147. </button>
  148. </Tooltip>
  149. </div>
  150. </div>
  151. {:else if token.type === 'blockquote'}
  152. <blockquote dir="auto">
  153. <svelte:self id={`${id}-${tokenIdx}`} tokens={token.tokens} {onTaskClick} {onSourceClick} />
  154. </blockquote>
  155. {:else if token.type === 'list'}
  156. {#if token.ordered}
  157. <ol start={token.start || 1}>
  158. {#each token.items as item, itemIdx}
  159. <li dir="auto" class="text-start">
  160. {#if item?.task}
  161. <input
  162. class=" translate-y-[1px] -translate-x-1"
  163. type="checkbox"
  164. checked={item.checked}
  165. on:change={(e) => {
  166. onTaskClick({
  167. id: id,
  168. token: token,
  169. tokenIdx: tokenIdx,
  170. item: item,
  171. itemIdx: itemIdx,
  172. checked: e.target.checked
  173. });
  174. }}
  175. />
  176. {/if}
  177. <svelte:self
  178. id={`${id}-${tokenIdx}-${itemIdx}`}
  179. tokens={item.tokens}
  180. top={token.loose}
  181. {onTaskClick}
  182. {onSourceClick}
  183. />
  184. </li>
  185. {/each}
  186. </ol>
  187. {:else}
  188. <ul>
  189. {#each token.items as item, itemIdx}
  190. <li dir="auto" class="text-start">
  191. {#if item?.task}
  192. <input
  193. class=" translate-y-[1px] -translate-x-1"
  194. type="checkbox"
  195. checked={item.checked}
  196. on:change={(e) => {
  197. onTaskClick({
  198. id: id,
  199. token: token,
  200. tokenIdx: tokenIdx,
  201. item: item,
  202. itemIdx: itemIdx,
  203. checked: e.target.checked
  204. });
  205. }}
  206. />
  207. {/if}
  208. <svelte:self
  209. id={`${id}-${tokenIdx}-${itemIdx}`}
  210. tokens={item.tokens}
  211. top={token.loose}
  212. {onTaskClick}
  213. {onSourceClick}
  214. />
  215. </li>
  216. {/each}
  217. </ul>
  218. {/if}
  219. {:else if token.type === 'details'}
  220. <Collapsible
  221. title={token.summary}
  222. attributes={token?.attributes}
  223. className="w-full space-y-1"
  224. dir="auto"
  225. >
  226. <div class=" mb-1.5" slot="content">
  227. <svelte:self
  228. id={`${id}-${tokenIdx}-d`}
  229. tokens={marked.lexer(token.text)}
  230. attributes={token?.attributes}
  231. {onTaskClick}
  232. {onSourceClick}
  233. />
  234. </div>
  235. </Collapsible>
  236. {:else if token.type === 'html'}
  237. {@const html = DOMPurify.sanitize(token.text)}
  238. {#if html && html.includes('<video')}
  239. {@html html}
  240. {:else if token.text.includes(`<iframe src="${WEBUI_BASE_URL}/api/v1/files/`)}
  241. {@html `${token.text}`}
  242. {:else}
  243. {token.text}
  244. {/if}
  245. {:else if token.type === 'iframe'}
  246. <iframe
  247. src="{WEBUI_BASE_URL}/api/v1/files/{token.fileId}/content"
  248. title={token.fileId}
  249. width="100%"
  250. frameborder="0"
  251. onload="this.style.height=(this.contentWindow.document.body.scrollHeight+20)+'px';"
  252. ></iframe>
  253. {:else if token.type === 'paragraph'}
  254. <p dir="auto">
  255. <MarkdownInlineTokens
  256. id={`${id}-${tokenIdx}-p`}
  257. tokens={token.tokens ?? []}
  258. {onSourceClick}
  259. />
  260. </p>
  261. {:else if token.type === 'text'}
  262. {#if top}
  263. <p dir="auto">
  264. {#if token.tokens}
  265. <MarkdownInlineTokens id={`${id}-${tokenIdx}-t`} tokens={token.tokens} {onSourceClick} />
  266. {:else}
  267. {unescapeHtml(token.text)}
  268. {/if}
  269. </p>
  270. {:else if token.tokens}
  271. <MarkdownInlineTokens
  272. id={`${id}-${tokenIdx}-p`}
  273. tokens={token.tokens ?? []}
  274. {onSourceClick}
  275. />
  276. {:else}
  277. {unescapeHtml(token.text)}
  278. {/if}
  279. {:else if token.type === 'inlineKatex'}
  280. {#if token.text}
  281. <KatexRenderer content={token.text} displayMode={token?.displayMode ?? false} />
  282. {/if}
  283. {:else if token.type === 'blockKatex'}
  284. {#if token.text}
  285. <KatexRenderer content={token.text} displayMode={token?.displayMode ?? false} />
  286. {/if}
  287. {:else if token.type === 'space'}
  288. <div class="my-2" />
  289. {:else}
  290. {console.log('Unknown token', token)}
  291. {/if}
  292. {/each}