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

323 lines
9.2 KiB
Svelte
Raw Normal View History

<script lang="ts">
import DOMPurify from 'dompurify';
2025-04-18 09:38:53 +00:00
import { 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';
import AlertRenderer, { alertComponent } from './AlertRenderer.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';
2025-03-15 01:56:04 +00:00
import { settings } from '$lib/stores';
2025-04-20 06:38:08 +00:00
import HtmlToken from './HTMLToken.svelte';
2024-08-08 22:01:38 +00:00
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-05-16 17:47:43 +00:00
export let preview = false;
2025-02-14 06:37:01 +00:00
2025-05-16 23:14:26 +00:00
export let onSave: Function = () => {};
2025-04-18 09:38:53 +00:00
export let onUpdate: Function = () => {};
2025-05-16 17:47:43 +00:00
export let onPreview: Function = () => {};
2025-04-18 09:38:53 +00:00
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}`}
2025-03-15 02:01:59 +00:00
collapsed={$settings?.collapseCodeBlocks ?? false}
2024-10-06 02:58:27 +00:00
{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-05-16 17:47:43 +00:00
{preview}
2025-02-28 15:36:56 +00:00
onSave={(value) => {
2025-05-16 23:14:26 +00:00
onSave({
2024-10-06 02:58:27 +00:00
raw: token.raw,
oldContent: token.text,
2025-02-24 05:39:34 +00:00
newContent: value
2024-10-06 02:58:27 +00:00
});
}}
2025-05-16 23:14:26 +00:00
{onUpdate}
{onPreview}
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]}`}
>
2025-03-19 00:53:11 +00:00
<div class="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]}`}
>
2025-03-19 00:53:11 +00:00
<div class="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'}
{@const alert = alertComponent(token)}
{#if alert}
2025-03-18 13:39:37 +00:00
<AlertRenderer {token} {alert} />
{:else}
<blockquote dir="auto">
<svelte:self id={`${id}-${tokenIdx}`} tokens={token.tokens} {onTaskClick} {onSourceClick} />
</blockquote>
{/if}
2024-08-08 18:46:39 +00:00
{:else if token.type === 'list'}
{#if token.ordered}
2025-04-06 23:53:30 +00:00
<ol start={token.start || 1} dir="auto">
2024-08-08 18:46:39 +00:00
{#each token.items as item, itemIdx}
2025-04-06 23:53:30 +00:00
<li 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}
2025-04-06 23:53:30 +00:00
<ul dir="auto">
2024-08-08 18:46:39 +00:00
{#each token.items as item, itemIdx}
2025-04-06 23:53:30 +00:00
<li 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}
2025-03-15 01:56:04 +00:00
open={$settings?.expandDetails ?? false}
2025-02-20 09:01:29 +00:00
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'}
2025-04-20 06:38:08 +00:00
<HtmlToken {id} {token} {onSourceClick} />
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}
2025-04-07 00:10:09 +00:00
<p>
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}