123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314 |
- <script lang="ts">
- import DOMPurify from 'dompurify';
- import { createEventDispatcher, onMount, getContext } from 'svelte';
- const i18n = getContext('i18n');
- import fileSaver from 'file-saver';
- const { saveAs } = fileSaver;
- import { marked, type Token } from 'marked';
- import { unescapeHtml } from '$lib/utils';
- import { WEBUI_BASE_URL } from '$lib/constants';
- import CodeBlock from '$lib/components/chat/Messages/CodeBlock.svelte';
- import MarkdownInlineTokens from '$lib/components/chat/Messages/Markdown/MarkdownInlineTokens.svelte';
- import KatexRenderer from './KatexRenderer.svelte';
- import Collapsible from '$lib/components/common/Collapsible.svelte';
- import Tooltip from '$lib/components/common/Tooltip.svelte';
- import ArrowDownTray from '$lib/components/icons/ArrowDownTray.svelte';
- const dispatch = createEventDispatcher();
- export let id: string;
- export let tokens: Token[];
- export let top = true;
- export let attributes = {};
- export let save = false;
- export let onTaskClick: Function = () => {};
- export let onSourceClick: Function = () => {};
- const headerComponent = (depth: number) => {
- return 'h' + depth;
- };
- const exportTableToCSVHandler = (token, tokenIdx = 0) => {
- console.log('Exporting table to CSV');
- // Extract header row text and escape for CSV.
- const header = token.header.map((headerCell) => `"${headerCell.text.replace(/"/g, '""')}"`);
- // Create an array for rows that will hold the mapped cell text.
- const rows = token.rows.map((row) =>
- row.map((cell) => {
- // Map tokens into a single text
- const cellContent = cell.tokens.map((token) => token.text).join('');
- // Escape double quotes and wrap the content in double quotes
- return `"${cellContent.replace(/"/g, '""')}"`;
- })
- );
- // Combine header and rows
- const csvData = [header, ...rows];
- // Join the rows using commas (,) as the separator and rows using newline (\n).
- const csvContent = csvData.map((row) => row.join(',')).join('\n');
- // Log rows and CSV content to ensure everything is correct.
- console.log(csvData);
- console.log(csvContent);
- // To handle Unicode characters, you need to prefix the data with a BOM:
- const bom = '\uFEFF'; // BOM for UTF-8
- // Create a new Blob prefixed with the BOM to ensure proper Unicode encoding.
- const blob = new Blob([bom + csvContent], { type: 'text/csv;charset=UTF-8' });
- // Use FileSaver.js's saveAs function to save the generated CSV file.
- saveAs(blob, `table-${id}-${tokenIdx}.csv`);
- };
- </script>
- <!-- {JSON.stringify(tokens)} -->
- {#each tokens as token, tokenIdx (tokenIdx)}
- {#if token.type === 'hr'}
- <hr class=" border-gray-100 dark:border-gray-850" />
- {:else if token.type === 'heading'}
- <svelte:element this={headerComponent(token.depth)} dir="auto">
- <MarkdownInlineTokens id={`${id}-${tokenIdx}-h`} tokens={token.tokens} {onSourceClick} />
- </svelte:element>
- {:else if token.type === 'code'}
- {#if token.raw.includes('```')}
- <CodeBlock
- id={`${id}-${tokenIdx}`}
- {token}
- lang={token?.lang ?? ''}
- code={token?.text ?? ''}
- {attributes}
- {save}
- onCode={(value) => {
- dispatch('code', value);
- }}
- onSave={(e) => {
- dispatch('update', {
- raw: token.raw,
- oldContent: token.text,
- newContent: value
- });
- }}
- />
- {:else}
- {token.text}
- {/if}
- {:else if token.type === 'table'}
- <div class="relative w-full group">
- <div class="scrollbar-hidden relative overflow-x-auto max-w-full rounded-lg">
- <table
- class=" w-full text-sm text-left text-gray-500 dark:text-gray-400 max-w-full rounded-xl"
- >
- <thead
- class="text-xs text-gray-700 uppercase bg-gray-50 dark:bg-gray-850 dark:text-gray-400 border-none"
- >
- <tr class="">
- {#each token.header as header, headerIdx}
- <th
- scope="col"
- class="px-3! py-1.5! cursor-pointer border border-gray-100 dark:border-gray-850"
- style={token.align[headerIdx] ? '' : `text-align: ${token.align[headerIdx]}`}
- >
- <div class="flex flex-col gap-1.5 text-left">
- <div class="shrink-0 break-normal">
- <MarkdownInlineTokens
- id={`${id}-${tokenIdx}-header-${headerIdx}`}
- tokens={header.tokens}
- {onSourceClick}
- />
- </div>
- </div>
- </th>
- {/each}
- </tr>
- </thead>
- <tbody>
- {#each token.rows as row, rowIdx}
- <tr class="bg-white dark:bg-gray-900 dark:border-gray-850 text-xs">
- {#each row ?? [] as cell, cellIdx}
- <td
- class="px-3! py-1.5! text-gray-900 dark:text-white w-max border border-gray-100 dark:border-gray-850"
- style={token.align[cellIdx] ? '' : `text-align: ${token.align[cellIdx]}`}
- >
- <div class="flex flex-col break-normal">
- <MarkdownInlineTokens
- id={`${id}-${tokenIdx}-row-${rowIdx}-${cellIdx}`}
- tokens={cell.tokens}
- {onSourceClick}
- />
- </div>
- </td>
- {/each}
- </tr>
- {/each}
- </tbody>
- </table>
- </div>
- <div class=" absolute top-1 right-1.5 z-20 invisible group-hover:visible">
- <Tooltip content={$i18n.t('Export to CSV')}>
- <button
- class="p-1 rounded-lg bg-transparent transition"
- on:click={(e) => {
- e.stopPropagation();
- exportTableToCSVHandler(token, tokenIdx);
- }}
- >
- <ArrowDownTray className=" size-3.5" strokeWidth="1.5" />
- </button>
- </Tooltip>
- </div>
- </div>
- {:else if token.type === 'blockquote'}
- <blockquote dir="auto">
- <svelte:self id={`${id}-${tokenIdx}`} tokens={token.tokens} {onTaskClick} {onSourceClick} />
- </blockquote>
- {:else if token.type === 'list'}
- {#if token.ordered}
- <ol start={token.start || 1}>
- {#each token.items as item, itemIdx}
- <li dir="auto" class="text-start">
- {#if item?.task}
- <input
- class=" translate-y-[1px] -translate-x-1"
- type="checkbox"
- checked={item.checked}
- on:change={(e) => {
- onTaskClick({
- id: id,
- token: token,
- tokenIdx: tokenIdx,
- item: item,
- itemIdx: itemIdx,
- checked: e.target.checked
- });
- }}
- />
- {/if}
- <svelte:self
- id={`${id}-${tokenIdx}-${itemIdx}`}
- tokens={item.tokens}
- top={token.loose}
- {onTaskClick}
- {onSourceClick}
- />
- </li>
- {/each}
- </ol>
- {:else}
- <ul>
- {#each token.items as item, itemIdx}
- <li dir="auto" class="text-start">
- {#if item?.task}
- <input
- class=" translate-y-[1px] -translate-x-1"
- type="checkbox"
- checked={item.checked}
- on:change={(e) => {
- onTaskClick({
- id: id,
- token: token,
- tokenIdx: tokenIdx,
- item: item,
- itemIdx: itemIdx,
- checked: e.target.checked
- });
- }}
- />
- {/if}
- <svelte:self
- id={`${id}-${tokenIdx}-${itemIdx}`}
- tokens={item.tokens}
- top={token.loose}
- {onTaskClick}
- {onSourceClick}
- />
- </li>
- {/each}
- </ul>
- {/if}
- {:else if token.type === 'details'}
- <Collapsible
- title={token.summary}
- attributes={token?.attributes}
- className="w-full space-y-1"
- dir="auto"
- >
- <div class=" mb-1.5" slot="content">
- <svelte:self
- id={`${id}-${tokenIdx}-d`}
- tokens={marked.lexer(token.text)}
- attributes={token?.attributes}
- {onTaskClick}
- {onSourceClick}
- />
- </div>
- </Collapsible>
- {:else if token.type === 'html'}
- {@const html = DOMPurify.sanitize(token.text)}
- {#if html && html.includes('<video')}
- {@html html}
- {:else if token.text.includes(`<iframe src="${WEBUI_BASE_URL}/api/v1/files/`)}
- {@html `${token.text}`}
- {:else}
- {token.text}
- {/if}
- {:else if token.type === 'iframe'}
- <iframe
- src="{WEBUI_BASE_URL}/api/v1/files/{token.fileId}/content"
- title={token.fileId}
- width="100%"
- frameborder="0"
- onload="this.style.height=(this.contentWindow.document.body.scrollHeight+20)+'px';"
- ></iframe>
- {:else if token.type === 'paragraph'}
- <p dir="auto">
- <MarkdownInlineTokens
- id={`${id}-${tokenIdx}-p`}
- tokens={token.tokens ?? []}
- {onSourceClick}
- />
- </p>
- {:else if token.type === 'text'}
- {#if top}
- <p dir="auto">
- {#if token.tokens}
- <MarkdownInlineTokens id={`${id}-${tokenIdx}-t`} tokens={token.tokens} {onSourceClick} />
- {:else}
- {unescapeHtml(token.text)}
- {/if}
- </p>
- {:else if token.tokens}
- <MarkdownInlineTokens
- id={`${id}-${tokenIdx}-p`}
- tokens={token.tokens ?? []}
- {onSourceClick}
- />
- {:else}
- {unescapeHtml(token.text)}
- {/if}
- {:else if token.type === 'inlineKatex'}
- {#if token.text}
- <KatexRenderer content={token.text} displayMode={token?.displayMode ?? false} />
- {/if}
- {:else if token.type === 'blockKatex'}
- {#if token.text}
- <KatexRenderer content={token.text} displayMode={token?.displayMode ?? false} />
- {/if}
- {:else if token.type === 'space'}
- <div class="my-2" />
- {:else}
- {console.log('Unknown token', token)}
- {/if}
- {/each}
|