diff --git a/src/app.css b/src/app.css
index 756b0d9a08..6f40acab32 100644
--- a/src/app.css
+++ b/src/app.css
@@ -409,14 +409,14 @@ input[type='number'] {
}
}
-.tiptap .mention {
+.mention {
border-radius: 0.4rem;
box-decoration-break: clone;
padding: 0.1rem 0.3rem;
@apply text-blue-900 dark:text-blue-100 bg-blue-300/20 dark:bg-blue-500/20;
}
-.tiptap .mention::after {
+.mention::after {
content: '\200B';
}
diff --git a/src/lib/components/channel/MessageInput/MentionList.svelte b/src/lib/components/channel/MessageInput/MentionList.svelte
index 4bc2b5c8a1..330ea01fdb 100644
--- a/src/lib/components/channel/MessageInput/MentionList.svelte
+++ b/src/lib/components/channel/MessageInput/MentionList.svelte
@@ -15,7 +15,8 @@
const select = (index: number) => {
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) => {
diff --git a/src/lib/components/chat/Messages/Markdown.svelte b/src/lib/components/chat/Messages/Markdown.svelte
index 736c93cb0d..c2ef2b923e 100644
--- a/src/lib/components/chat/Messages/Markdown.svelte
+++ b/src/lib/components/chat/Messages/Markdown.svelte
@@ -5,6 +5,7 @@
import markedExtension from '$lib/utils/marked/extension';
import markedKatexExtension from '$lib/utils/marked/katex-extension';
+ import { mentionExtension } from '$lib/utils/marked/mention-extension';
import MarkdownTokens from './Markdown/MarkdownTokens.svelte';
@@ -37,6 +38,7 @@
marked.use(markedKatexExtension(options));
marked.use(markedExtension(options));
+ marked.use({ extensions: [mentionExtension({ triggerChar: '@' })] });
$: (async () => {
if (content) {
diff --git a/src/lib/components/chat/Messages/Markdown/MarkdownInlineTokens.svelte b/src/lib/components/chat/Messages/Markdown/MarkdownInlineTokens.svelte
index c49d60df69..8a0358a752 100644
--- a/src/lib/components/chat/Messages/Markdown/MarkdownInlineTokens.svelte
+++ b/src/lib/components/chat/Messages/Markdown/MarkdownInlineTokens.svelte
@@ -16,6 +16,7 @@
import HtmlToken from './HTMLToken.svelte';
import TextToken from './MarkdownInlineTokens/TextToken.svelte';
import CodespanToken from './MarkdownInlineTokens/CodespanToken.svelte';
+ import MentionToken from './MarkdownInlineTokens/MentionToken.svelte';
export let id: string;
export let done = true;
@@ -60,6 +61,8 @@
frameborder="0"
onload="this.style.height=(this.contentWindow.document.body.scrollHeight+20)+'px';"
>
+ {:else if token.type === 'mention'}
+
{:else if token.type === 'text'}
{/if}
diff --git a/src/lib/components/chat/Messages/Markdown/MarkdownInlineTokens/MentionToken.svelte b/src/lib/components/chat/Messages/Markdown/MarkdownInlineTokens/MentionToken.svelte
new file mode 100644
index 0000000000..c23307e515
--- /dev/null
+++ b/src/lib/components/chat/Messages/Markdown/MarkdownInlineTokens/MentionToken.svelte
@@ -0,0 +1,10 @@
+
+
+
+ {token.triggerChar}{token.label}
+
diff --git a/src/lib/components/common/Tooltip.svelte b/src/lib/components/common/Tooltip.svelte
index 3d1566e650..59575520e6 100644
--- a/src/lib/components/common/Tooltip.svelte
+++ b/src/lib/components/common/Tooltip.svelte
@@ -7,10 +7,12 @@
export let elementId = '';
+ export let as = 'div';
+ export let className = 'flex';
+
export let placement = 'top';
export let content = `I'm a tooltip!`;
export let touch = true;
- export let className = 'flex';
export let theme = '';
export let offset = [0, 4];
export let allowHTML = true;
@@ -59,8 +61,8 @@
});
-
+
-
+
diff --git a/src/lib/utils/marked/mention-extension.ts b/src/lib/utils/marked/mention-extension.ts
new file mode 100644
index 0000000000..21aa6bc687
--- /dev/null
+++ b/src/lib/utils/marked/mention-extension.ts
@@ -0,0 +1,84 @@
+// mention-extension.ts
+type MentionOptions = {
+ triggerChar?: string; // default "@"
+ className?: string; // default "mention"
+ extraAttrs?: Record; // 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 `${escapeHtml(trigger + token.label)}`;
+}
+
+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>" →
+// @llama3.2:latest
+//
+// "<@llama3.2:latest|friendly>" →
+// @friendly
+//
+// "<@llama3.2:latest|>" →
+// @llama3.2:latest
+//
+// If triggerChar = "#"