From f9c524d31d7737239aa990751cca4705e3872cb8 Mon Sep 17 00:00:00 2001 From: Timothy Jaeryang Baek Date: Wed, 14 Oct 2026 15:54:56 +0400 Subject: [PATCH] refac --- src/lib/apis/channels/index.ts | 7 +- src/lib/components/channel/Channel.svelte | 3 +- .../components/channel/MessageInput.svelte | 74 +++++++++- .../channel/MessageInput/MentionList.svelte | 31 ++-- src/lib/components/channel/Messages.svelte | 3 + .../channel/Messages/Message.svelte | 14 +- src/lib/components/channel/Thread.svelte | 3 +- .../MarkdownInlineTokens/MentionToken.svelte | 66 +++++---- .../components/common/RichTextInput.svelte | 50 ++++++- src/lib/utils/channelMentions.ts | 133 ++++++++++++++++++ src/lib/utils/marked/mention-extension.ts | 3 +- 11 files changed, 327 insertions(+), 60 deletions(-) create mode 100644 src/lib/utils/channelMentions.ts diff --git a/src/lib/apis/channels/index.ts b/src/lib/apis/channels/index.ts index 01a036ce8c..0d040e4395 100644 --- a/src/lib/apis/channels/index.ts +++ b/src/lib/apis/channels/index.ts @@ -1,5 +1,5 @@ import { WEBUI_API_BASE_URL } from '$lib/constants'; -import type { App } from '$lib/apis/apps'; +import { getApps, type App } from '$lib/apis/apps'; type ChannelForm = { type?: string; @@ -970,6 +970,11 @@ export const getChannelAppsById = async ( return res; }; +export const getChannelMentionApps = async (token: string, channelId: string): Promise => { + const [apps, joined] = await Promise.all([getApps(token), getChannelAppsById(token, channelId)]); + return apps.filter((app) => app.is_active && joined.some((member) => member.id === app.id)); +}; + export const inviteAppToChannel = async ( token: string = '', channel_id: string, diff --git a/src/lib/components/channel/Channel.svelte b/src/lib/components/channel/Channel.svelte index d1e51d658c..23095a5577 100644 --- a/src/lib/components/channel/Channel.svelte +++ b/src/lib/components/channel/Channel.svelte @@ -374,7 +374,7 @@ { replyToMessage = message; @@ -407,6 +407,7 @@
{ + const targetQueueKey = queueKey; + const editor = inputEditor; + if (editor && /[@#]/.test(content)) { + const state = editor.state; + try { + const candidates = await getChannelMentionCandidates( + localStorage.token, + channel.id, + getMentionUserQueries(state.doc), + { + users: userSuggestions, + apps: $config?.features?.enable_apps ?? false, + models: $models, + channels: channelSuggestions ? $channels : [] + } + ); + if (isDestroyed || targetQueueKey !== queueKey || editor.isDestroyed) return false; + const tr = resolveMentions(state, candidates); + // A paste lookup may have finished while Send was waiting for the same result. + if (editor.state.doc.eq(tr.doc)) return true; + if (editor.state.doc !== state.doc) return false; + if (tr.docChanged) editor.view.dispatch(tr); + } catch (error) { + toast.error(`${error}`); + return false; + } + } + return true; + }; + const submitHandler = async () => { if (disabled || !$user?.id || (content === '' && files.length === 0)) return; const targetQueueKey = queueKey; + if (inputEditor && /[@#]/.test(content) && !(await resolveInputMentions())) return; + if (content === '' && files.length === 0) return; const entry = { id: uuidv4(), prompt: content, @@ -999,6 +1045,7 @@ id="chat-input" className="input-prose min-h-fit h-full text-sm!" bind:this={chatInputElement} + bind:editor={inputEditor} json={true} messageInput={true} editable={!disabled} @@ -1015,6 +1062,7 @@ largeTextAsFile={$settings?.largeTextAsFile ?? false} floatingMenuPlacement={'top-start'} {suggestions} + on:pasted={resolveInputMentions} onChange={(e) => { const { md } = e; content = md; @@ -1028,6 +1076,30 @@ document.getElementById('suggestions-container'); if (!suggestionsContainerElement) { + if ( + e.key === 'ArrowUp' && + !e.isComposing && + !isCtrlPressed && + !e.shiftKey && + !e.altKey && + !disabled && + content === '' && + files.length === 0 + ) { + const message = messages?.find( + (message) => message.user_id === $user?.id && !message.temp_id + ); + if (message) { + const messageId = parentId ? `${parentId}-${message.id}` : message.id; + const editButton = document.getElementById( + `edit-message-${messageId}` + ); + if (editButton) { + e.preventDefault(); + editButton.click(); + } + } + } if ( !$mobile || !( diff --git a/src/lib/components/channel/MessageInput/MentionList.svelte b/src/lib/components/channel/MessageInput/MentionList.svelte index ed89361d9e..47d810497b 100644 --- a/src/lib/components/channel/MessageInput/MentionList.svelte +++ b/src/lib/components/channel/MessageInput/MentionList.svelte @@ -3,8 +3,8 @@ import { getAbortSignal, getContext, onMount } from 'svelte'; const i18n = getContext('i18n'); - import { getChannelAppsById } from '$lib/apis/channels'; - import { getApps } from '$lib/apis/apps'; + import { getChannelMentionApps } from '$lib/apis/channels'; + import { mentionAttributes } from '$lib/utils/channelMentions'; import { channels, models, config } from '$lib/stores'; import Tooltip from '$lib/components/common/Tooltip.svelte'; import Grid from '$lib/components/icons/Grid.svelte'; @@ -90,14 +90,7 @@ const item = filteredItems[index]; if (!item) return; - // Add the "U:", "M:" or "C:" prefix to the id - // and also append the label after a pipe | - // so that the mention renderer can show the label - if (item) - command({ - id: `${item.type === 'user' ? 'U' : item.type === 'model' ? 'M' : item.type === 'app' ? 'A' : 'C'}:${item.id}|${item.label}`, - label: item.label - }); + command(mentionAttributes(item)); }; const onKeyDown = (event: KeyboardEvent) => { @@ -138,16 +131,14 @@ onMount(() => { if ($config?.features?.enable_apps && channelId && triggerChar === '@') { - Promise.all([getApps(localStorage.token), getChannelAppsById(localStorage.token, channelId)]) - .then(([apps, joined]) => { - _apps = apps - .filter((a) => a.is_active && joined.some((member) => member.id === a.id)) - .map((a) => ({ - type: 'app', - id: a.id, - label: a.name, - profile_image_url: a.data.manifest.profile_image_url - })); + getChannelMentionApps(localStorage.token, channelId) + .then((apps) => { + _apps = apps.map((a) => ({ + type: 'app', + id: a.id, + label: a.name, + profile_image_url: a.data.manifest.profile_image_url + })); }) .catch(() => {}); } diff --git a/src/lib/components/channel/Messages.svelte b/src/lib/components/channel/Messages.svelte index 8624adba01..5e002d0ca3 100644 --- a/src/lib/components/channel/Messages.svelte +++ b/src/lib/components/channel/Messages.svelte @@ -160,6 +160,9 @@ disabled={!channel?.write_access || message?.temp_id} pending={!!message?.temp_id} showUserProfile={messageIdx === 0 || + Object.values(appReplies).some( + (event) => event.message_id === messageList.at(messageIdx - 1)?.id + ) || messageList.at(messageIdx - 1)?.user_id !== message.user_id || messageList.at(messageIdx - 1)?.user?.id !== message.user?.id || messageList.at(messageIdx - 1)?.meta?.model_id !== message?.meta?.model_id || diff --git a/src/lib/components/channel/Messages/Message.svelte b/src/lib/components/channel/Messages/Message.svelte index f2cba13bb7..06c42bf0cb 100644 --- a/src/lib/components/channel/Messages/Message.svelte +++ b/src/lib/components/channel/Messages/Message.svelte @@ -14,7 +14,7 @@ dayjs.extend(isYesterday); dayjs.extend(localizedFormat); - import { getContext } from 'svelte'; + import { getContext, tick } from 'svelte'; const i18n = getContext>('i18n'); import { formatDate } from '$lib/utils'; @@ -286,10 +286,18 @@ {#if onEdit}