mirror of
https://github.com/open-webui/open-webui.git
synced 2025-12-15 13:55:19 +00:00
refac/enh: mention token rendering
This commit is contained in:
parent
22e11760a1
commit
098f34f400
7 changed files with 108 additions and 6 deletions
|
|
@ -409,14 +409,14 @@ input[type='number'] {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.tiptap .mention {
|
.mention {
|
||||||
border-radius: 0.4rem;
|
border-radius: 0.4rem;
|
||||||
box-decoration-break: clone;
|
box-decoration-break: clone;
|
||||||
padding: 0.1rem 0.3rem;
|
padding: 0.1rem 0.3rem;
|
||||||
@apply text-blue-900 dark:text-blue-100 bg-blue-300/20 dark:bg-blue-500/20;
|
@apply text-blue-900 dark:text-blue-100 bg-blue-300/20 dark:bg-blue-500/20;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tiptap .mention::after {
|
.mention::after {
|
||||||
content: '\200B';
|
content: '\200B';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,8 @@
|
||||||
|
|
||||||
const select = (index: number) => {
|
const select = (index: number) => {
|
||||||
const item = filteredItems[index];
|
const item = filteredItems[index];
|
||||||
if (item) command({ id: item.id, label: item.name });
|
// Add the "A:" prefix to the id to indicate it's an assistant model
|
||||||
|
if (item) command({ id: `A:${item.id}|${item.name}`, label: item.name });
|
||||||
};
|
};
|
||||||
|
|
||||||
const onKeyDown = (event: KeyboardEvent) => {
|
const onKeyDown = (event: KeyboardEvent) => {
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@
|
||||||
|
|
||||||
import markedExtension from '$lib/utils/marked/extension';
|
import markedExtension from '$lib/utils/marked/extension';
|
||||||
import markedKatexExtension from '$lib/utils/marked/katex-extension';
|
import markedKatexExtension from '$lib/utils/marked/katex-extension';
|
||||||
|
import { mentionExtension } from '$lib/utils/marked/mention-extension';
|
||||||
|
|
||||||
import MarkdownTokens from './Markdown/MarkdownTokens.svelte';
|
import MarkdownTokens from './Markdown/MarkdownTokens.svelte';
|
||||||
|
|
||||||
|
|
@ -37,6 +38,7 @@
|
||||||
|
|
||||||
marked.use(markedKatexExtension(options));
|
marked.use(markedKatexExtension(options));
|
||||||
marked.use(markedExtension(options));
|
marked.use(markedExtension(options));
|
||||||
|
marked.use({ extensions: [mentionExtension({ triggerChar: '@' })] });
|
||||||
|
|
||||||
$: (async () => {
|
$: (async () => {
|
||||||
if (content) {
|
if (content) {
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,7 @@
|
||||||
import HtmlToken from './HTMLToken.svelte';
|
import HtmlToken from './HTMLToken.svelte';
|
||||||
import TextToken from './MarkdownInlineTokens/TextToken.svelte';
|
import TextToken from './MarkdownInlineTokens/TextToken.svelte';
|
||||||
import CodespanToken from './MarkdownInlineTokens/CodespanToken.svelte';
|
import CodespanToken from './MarkdownInlineTokens/CodespanToken.svelte';
|
||||||
|
import MentionToken from './MarkdownInlineTokens/MentionToken.svelte';
|
||||||
|
|
||||||
export let id: string;
|
export let id: string;
|
||||||
export let done = true;
|
export let done = true;
|
||||||
|
|
@ -60,6 +61,8 @@
|
||||||
frameborder="0"
|
frameborder="0"
|
||||||
onload="this.style.height=(this.contentWindow.document.body.scrollHeight+20)+'px';"
|
onload="this.style.height=(this.contentWindow.document.body.scrollHeight+20)+'px';"
|
||||||
></iframe>
|
></iframe>
|
||||||
|
{:else if token.type === 'mention'}
|
||||||
|
<MentionToken {token} />
|
||||||
{:else if token.type === 'text'}
|
{:else if token.type === 'text'}
|
||||||
<TextToken {token} {done} />
|
<TextToken {token} {done} />
|
||||||
{/if}
|
{/if}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,10 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import Tooltip from '$lib/components/common/Tooltip.svelte';
|
||||||
|
import type { Token } from 'marked';
|
||||||
|
|
||||||
|
export let token: Token;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Tooltip as="span" className="mention" content={token.id} placement="top">
|
||||||
|
{token.triggerChar}{token.label}
|
||||||
|
</Tooltip>
|
||||||
|
|
@ -7,10 +7,12 @@
|
||||||
|
|
||||||
export let elementId = '';
|
export let elementId = '';
|
||||||
|
|
||||||
|
export let as = 'div';
|
||||||
|
export let className = 'flex';
|
||||||
|
|
||||||
export let placement = 'top';
|
export let placement = 'top';
|
||||||
export let content = `I'm a tooltip!`;
|
export let content = `I'm a tooltip!`;
|
||||||
export let touch = true;
|
export let touch = true;
|
||||||
export let className = 'flex';
|
|
||||||
export let theme = '';
|
export let theme = '';
|
||||||
export let offset = [0, 4];
|
export let offset = [0, 4];
|
||||||
export let allowHTML = true;
|
export let allowHTML = true;
|
||||||
|
|
@ -59,8 +61,8 @@
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div bind:this={tooltipElement} class={className}>
|
<svelte:element this={as} bind:this={tooltipElement} class={className}>
|
||||||
<slot />
|
<slot />
|
||||||
</div>
|
</svelte:element>
|
||||||
|
|
||||||
<slot name="tooltip"></slot>
|
<slot name="tooltip"></slot>
|
||||||
|
|
|
||||||
84
src/lib/utils/marked/mention-extension.ts
Normal file
84
src/lib/utils/marked/mention-extension.ts
Normal file
|
|
@ -0,0 +1,84 @@
|
||||||
|
// mention-extension.ts
|
||||||
|
type MentionOptions = {
|
||||||
|
triggerChar?: string; // default "@"
|
||||||
|
className?: string; // default "mention"
|
||||||
|
extraAttrs?: Record<string, string>; // additional HTML attrs
|
||||||
|
};
|
||||||
|
|
||||||
|
function escapeHtml(s: string) {
|
||||||
|
return s.replace(
|
||||||
|
/[&<>"']/g,
|
||||||
|
(c) => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' })[c]!
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function mentionStart(src: string) {
|
||||||
|
// Find the first "<" followed by trigger char
|
||||||
|
// We'll refine inside tokenizer
|
||||||
|
return src.indexOf('<');
|
||||||
|
}
|
||||||
|
|
||||||
|
function mentionTokenizer(this: any, src: string, options: MentionOptions = {}) {
|
||||||
|
const trigger = options.triggerChar ?? '@';
|
||||||
|
|
||||||
|
// Build dynamic regex for `<@id>`, `<@id|label>`, `<@id|>`
|
||||||
|
const re = new RegExp(`^<\\${trigger}([\\w.\\-:]+)(?:\\|([^>]*))?>`);
|
||||||
|
const m = re.exec(src);
|
||||||
|
if (!m) return;
|
||||||
|
|
||||||
|
const [, id, label] = m;
|
||||||
|
return {
|
||||||
|
type: 'mention',
|
||||||
|
raw: m[0],
|
||||||
|
triggerChar: trigger,
|
||||||
|
id,
|
||||||
|
label: label && label.length > 0 ? label : id
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function mentionRenderer(token: any, options: MentionOptions = {}) {
|
||||||
|
const trigger = options.triggerChar ?? '@';
|
||||||
|
const cls = options.className ?? 'mention';
|
||||||
|
const extra = options.extraAttrs ?? {};
|
||||||
|
|
||||||
|
const attrs = Object.entries({
|
||||||
|
class: cls,
|
||||||
|
'data-type': 'mention',
|
||||||
|
'data-id': token.id,
|
||||||
|
'data-mention-suggestion-char': trigger,
|
||||||
|
...extra
|
||||||
|
})
|
||||||
|
.map(([k, v]) => `${k}="${escapeHtml(String(v))}"`)
|
||||||
|
.join(' ');
|
||||||
|
|
||||||
|
return `<span ${attrs}>${escapeHtml(trigger + token.label)}</span>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function mentionExtension(opts: MentionOptions = {}) {
|
||||||
|
return {
|
||||||
|
name: 'mention',
|
||||||
|
level: 'inline' as const,
|
||||||
|
start: mentionStart,
|
||||||
|
tokenizer(src: string) {
|
||||||
|
return mentionTokenizer.call(this, src, opts);
|
||||||
|
},
|
||||||
|
renderer(token: any) {
|
||||||
|
return mentionRenderer(token, opts);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Usage:
|
||||||
|
// import { marked } from 'marked';
|
||||||
|
// marked.use({ extensions: [mentionExtension({ triggerChar: '@' })] });
|
||||||
|
//
|
||||||
|
// "<@llama3.2:latest>" →
|
||||||
|
// <span class="mention" data-type="mention" data-id="llama3.2:latest" data-mention-suggestion-char="@">@llama3.2:latest</span>
|
||||||
|
//
|
||||||
|
// "<@llama3.2:latest|friendly>" →
|
||||||
|
// <span class="mention" ...>@friendly</span>
|
||||||
|
//
|
||||||
|
// "<@llama3.2:latest|>" →
|
||||||
|
// <span class="mention" ...>@llama3.2:latest</span>
|
||||||
|
//
|
||||||
|
// If triggerChar = "#"
|
||||||
Loading…
Reference in a new issue