open-webui/src/lib/components/chat/Messages/Markdown/MarkdownTokens.svelte

318 lines
9.1 KiB
Svelte
Raw Normal View History

<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;
2024-08-16 15:51:50 +00:00
import { marked, type Token } from 'marked';
2025-01-08 08:10:38 +00:00
import { unescapeHtml } from '$lib/utils';
2024-08-08 22:01:38 +00:00
2024-10-05 19:04:36 +00:00
import { WEBUI_BASE_URL } from '$lib/constants';
2024-08-06 23:55:37 +00:00
import CodeBlock from '$lib/components/chat/Messages/CodeBlock.svelte';
2024-08-18 16:28:58 +00:00
import MarkdownInlineTokens from '$lib/components/chat/Messages/Markdown/MarkdownInlineTokens.svelte';
2024-08-08 22:01:38 +00:00
import KatexRenderer from './KatexRenderer.svelte';
2024-09-30 11:03:47 +00:00
import Collapsible from '$lib/components/common/Collapsible.svelte';
import Tooltip from '$lib/components/common/Tooltip.svelte';
import ArrowDownTray from '$lib/components/icons/ArrowDownTray.svelte';
2025-03-04 03:48:00 +00:00
import Source from './Source.svelte';
2024-08-08 22:01:38 +00:00
2024-10-05 19:04:36 +00:00
const dispatch = createEventDispatcher();
export let id: string;
export let tokens: Token[];
export let top = true;
2025-02-03 08:03:41 +00:00
export let attributes = {};
2024-08-08 22:01:38 +00:00
2024-10-05 19:07:45 +00:00
export let save = false;
2025-02-14 06:37:01 +00:00
export let onTaskClick: Function = () => {};
2024-11-22 01:58:29 +00:00
export let onSourceClick: Function = () => {};
2024-10-05 19:07:45 +00:00
const headerComponent = (depth: number) => {
return 'h' + depth;
};
const exportTableToCSVHandler = (token, tokenIdx = 0) => {
console.log('Exporting table to CSV');
2024-12-19 02:11:01 +00:00
// 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) =>
2024-12-19 02:11:01 +00:00
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, '""')}"`;
})
);
2024-12-19 02:11:01 +00:00
// Combine header and rows
const csvData = [header, ...rows];
// Join the rows using commas (,) as the separator and rows using newline (\n).
2024-12-19 02:11:01 +00:00
const csvContent = csvData.map((row) => row.join(',')).join('\n');
// Log rows and CSV content to ensure everything is correct.
2024-12-19 02:11:01 +00:00
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>
2024-08-08 22:01:38 +00:00
<!-- {JSON.stringify(tokens)} -->
2024-09-23 12:24:50 +00:00
{#each tokens as token, tokenIdx (tokenIdx)}
2024-08-08 18:46:39 +00:00
{#if token.type === 'hr'}
2025-02-16 03:50:40 +00:00
<hr class=" border-gray-100 dark:border-gray-850" />
2024-08-08 18:46:39 +00:00
{:else if token.type === 'heading'}
<svelte:element this={headerComponent(token.depth)} dir="auto">
2024-11-22 01:58:29 +00:00
<MarkdownInlineTokens id={`${id}-${tokenIdx}-h`} tokens={token.tokens} {onSourceClick} />
2024-08-08 18:46:39 +00:00
</svelte:element>
{:else if token.type === 'code'}
2024-10-06 02:58:27 +00:00
{#if token.raw.includes('```')}
<CodeBlock
id={`${id}-${tokenIdx}`}
{token}
lang={token?.lang ?? ''}
2025-01-08 07:59:58 +00:00
code={token?.text ?? ''}
2025-02-03 08:03:41 +00:00
{attributes}
2024-10-06 02:58:27 +00:00
{save}
2025-02-24 05:39:34 +00:00
onCode={(value) => {
dispatch('code', value);
2024-10-06 07:28:33 +00:00
}}
2025-02-28 15:36:56 +00:00
onSave={(value) => {
2024-10-06 02:58:27 +00:00
dispatch('update', {
raw: token.raw,
oldContent: token.text,
2025-02-24 05:39:34 +00:00
newContent: value
2024-10-06 02:58:27 +00:00
});
}}
/>
{:else}
2024-10-06 06:55:44 +00:00
{token.text}
2024-10-06 02:58:27 +00:00
{/if}
2024-08-08 18:46:39 +00:00
{:else if token.type === 'table'}
<div class="relative w-full group">
2024-11-30 20:22:07 +00:00
<div class="scrollbar-hidden relative overflow-x-auto max-w-full rounded-lg">
<table
2024-11-30 20:22:07 +00:00
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"
2025-02-16 03:50:40 +00:00
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">
2025-02-16 03:27:25 +00:00
<div class="shrink-0 break-normal">
2024-11-30 20:22:07 +00:00
<MarkdownInlineTokens
id={`${id}-${tokenIdx}-header-${headerIdx}`}
tokens={header.tokens}
{onSourceClick}
/>
</div>
</div>
</th>
2024-09-14 22:23:52 +00:00
{/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
2025-02-16 03:50:40 +00:00
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]}`}
>
2024-11-30 20:22:07 +00:00
<div class="flex flex-col break-normal">
<MarkdownInlineTokens
id={`${id}-${tokenIdx}-row-${rowIdx}-${cellIdx}`}
tokens={cell.tokens}
2024-11-22 01:58:29 +00:00
{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>
2024-09-14 22:23:52 +00:00
</div>
2024-08-08 18:46:39 +00:00
{:else if token.type === 'blockquote'}
<blockquote dir="auto">
2025-02-14 06:37:01 +00:00
<svelte:self id={`${id}-${tokenIdx}`} tokens={token.tokens} {onTaskClick} {onSourceClick} />
2024-08-08 18:46:39 +00:00
</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">
2025-02-14 06:37:01 +00:00
{#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}
2024-08-08 18:46:39 +00:00
<svelte:self
id={`${id}-${tokenIdx}-${itemIdx}`}
tokens={item.tokens}
top={token.loose}
2025-02-14 06:37:01 +00:00
{onTaskClick}
{onSourceClick}
2024-08-08 18:46:39 +00:00
/>
</li>
{/each}
</ol>
{:else}
<ul>
{#each token.items as item, itemIdx}
<li dir="auto" class="text-start">
2025-02-14 06:37:01 +00:00
{#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}
2024-08-08 18:46:39 +00:00
<svelte:self
id={`${id}-${tokenIdx}-${itemIdx}`}
tokens={item.tokens}
top={token.loose}
2025-02-14 06:37:01 +00:00
{onTaskClick}
{onSourceClick}
2024-08-08 18:46:39 +00:00
/>
</li>
{/each}
</ul>
{/if}
2024-09-30 10:50:53 +00:00
{:else if token.type === 'details'}
2025-02-20 09:01:29 +00:00
<Collapsible
title={token.summary}
attributes={token?.attributes}
className="w-full space-y-1"
dir="auto"
>
2024-09-30 11:03:47 +00:00
<div class=" mb-1.5" slot="content">
2025-02-03 08:03:41 +00:00
<svelte:self
id={`${id}-${tokenIdx}-d`}
tokens={marked.lexer(token.text)}
attributes={token?.attributes}
2025-02-14 06:37:01 +00:00
{onTaskClick}
{onSourceClick}
2025-02-03 08:03:41 +00:00
/>
2024-09-30 11:03:47 +00:00
</div>
</Collapsible>
2024-08-08 18:46:39 +00:00
{:else if token.type === 'html'}
{@const html = DOMPurify.sanitize(token.text)}
2024-08-16 13:44:18 +00:00
{#if html && html.includes('<video')}
{@html html}
2024-08-16 15:51:50 +00:00
{:else if token.text.includes(`<iframe src="${WEBUI_BASE_URL}/api/v1/files/`)}
{@html `${token.text}`}
2025-03-04 03:48:00 +00:00
{:else if token.text.includes(`<source_id`)}
<Source {id} {token} onClick={onSourceClick} />
{:else}
{token.text}
{/if}
2024-08-16 13:33:14 +00:00
{: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>
2024-08-08 18:46:39 +00:00
{:else if token.type === 'paragraph'}
<p dir="auto">
2024-11-22 01:58:29 +00:00
<MarkdownInlineTokens
id={`${id}-${tokenIdx}-p`}
tokens={token.tokens ?? []}
{onSourceClick}
/>
2024-08-08 18:46:39 +00:00
</p>
{:else if token.type === 'text'}
{#if top}
<p dir="auto">
2024-08-08 18:46:39 +00:00
{#if token.tokens}
2024-11-22 01:58:29 +00:00
<MarkdownInlineTokens id={`${id}-${tokenIdx}-t`} tokens={token.tokens} {onSourceClick} />
2024-08-08 18:46:39 +00:00
{:else}
{unescapeHtml(token.text)}
{/if}
</p>
{:else if token.tokens}
2024-11-22 01:58:29 +00:00
<MarkdownInlineTokens
id={`${id}-${tokenIdx}-p`}
tokens={token.tokens ?? []}
{onSourceClick}
/>
2024-08-06 23:55:37 +00:00
{:else}
2024-08-08 18:46:39 +00:00
{unescapeHtml(token.text)}
2024-08-06 23:55:37 +00:00
{/if}
2024-08-08 22:01:38 +00:00
{:else if token.type === 'inlineKatex'}
{#if token.text}
2025-01-08 07:59:58 +00:00
<KatexRenderer content={token.text} displayMode={token?.displayMode ?? false} />
2024-08-08 22:01:38 +00:00
{/if}
2024-08-14 14:07:39 +00:00
{:else if token.type === 'blockKatex'}
{#if token.text}
2025-01-08 07:59:58 +00:00
<KatexRenderer content={token.text} displayMode={token?.displayMode ?? false} />
2024-08-14 14:07:39 +00:00
{/if}
2024-08-08 18:46:39 +00:00
{:else if token.type === 'space'}
2024-09-07 01:56:58 +00:00
<div class="my-2" />
2024-08-08 18:46:39 +00:00
{:else}
{console.log('Unknown token', token)}
{/if}
{/each}