From cba2e44de4b067a56d2451dca78e8ddd43cf0696 Mon Sep 17 00:00:00 2001 From: Shirasawa <764798966@qq.com> Date: Fri, 12 Sep 2025 17:01:08 +0800 Subject: [PATCH 01/74] i18n: improve zh-TW translation --- src/lib/i18n/locales/zh-TW/translation.json | 32 ++++++++++----------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/src/lib/i18n/locales/zh-TW/translation.json b/src/lib/i18n/locales/zh-TW/translation.json index 220cedfb18..0a5b4c39bc 100644 --- a/src/lib/i18n/locales/zh-TW/translation.json +++ b/src/lib/i18n/locales/zh-TW/translation.json @@ -14,13 +14,13 @@ "{{COUNT}} extracted lines": "已擷取 {{COUNT}} 行", "{{COUNT}} hidden lines": "已隱藏 {{COUNT}} 行", "{{COUNT}} Replies": "{{COUNT}} 回覆", - "{{COUNT}} Sources": "", + "{{COUNT}} Sources": "{{COUNT}} 個來源", "{{COUNT}} words": "{{COUNT}} 個詞", "{{model}} download has been canceled": "已取消模型 {{model}} 的下載", "{{user}}'s Chats": "{{user}} 的對話", "{{webUIName}} Backend Required": "需要提供 {{webUIName}} 後端", "*Prompt node ID(s) are required for image generation": "* 圖片生成需要提示詞節點 ID", - "1 Source": "", + "1 Source": "1 個來源", "A new version (v{{LATEST_VERSION}}) is now available.": "新版本 (v{{LATEST_VERSION}}) 已釋出。", "A task model is used when performing tasks such as generating titles for chats and web search queries": "執行「產生對話標題」和「網頁搜尋查詢生成」等任務時使用的任務模型", "a user": "使用者", @@ -31,7 +31,7 @@ "Accessible to all users": "所有使用者皆可存取", "Account": "帳號", "Account Activation Pending": "帳號待啟用", - "accurate": "", + "accurate": "準確", "Accurate information": "準確資訊", "Action": "操作", "Action not found": "找不到對應的操作項目", @@ -428,9 +428,9 @@ "Displays citations in the response": "在回應中顯示引用", "Displays status updates (e.g., web search progress) in the response": "在回應中顯示進度狀態(例如:網路搜尋進度)", "Dive into knowledge": "挖掘知識", - "dlparse_v1": "", - "dlparse_v2": "", - "dlparse_v4": "", + "dlparse_v1": "dlparse_v1", + "dlparse_v2": "dlparse_v2", + "dlparse_v4": "dlparse_v4", "Do not install functions from sources you do not fully trust.": "請勿從您無法完全信任的來源安裝函式。", "Do not install tools from sources you do not fully trust.": "請勿從您無法完全信任的來源安裝工具。", "Docling": "Docling", @@ -689,7 +689,7 @@ "Failed to save models configuration": "儲存模型設定失敗", "Failed to update settings": "更新設定失敗", "Failed to upload file.": "上傳檔案失敗。", - "fast": "", + "fast": "快速", "Features": "功能", "Features Permissions": "功能權限", "February": "2 月", @@ -831,7 +831,7 @@ "Import Prompts": "匯入提示詞", "Import Tools": "匯入工具", "Important Update": "重要更新", - "In order to force OCR, performing OCR must be enabled.": "", + "In order to force OCR, performing OCR must be enabled.": "要啟用「強制執行 OCR」,必須先啟用「執行 OCR」。", "Include": "包含", "Include `--api-auth` flag when running stable-diffusion-webui": "執行 stable-diffusion-webui 時包含 `--api-auth` 參數", "Include `--api` flag when running stable-diffusion-webui": "執行 stable-diffusion-webui 時包含 `--api` 參數", @@ -1112,14 +1112,14 @@ "Password": "密碼", "Passwords do not match.": "兩次輸入的密碼不一致。", "Paste Large Text as File": "將大型文字以檔案貼上", - "PDF Backend": "", + "PDF Backend": "PDF 解析器後端", "PDF document (.pdf)": "PDF 檔案 (.pdf)", "PDF Extract Images (OCR)": "PDF 影像擷取(OCR 光學文字辨識)", "pending": "待處理", "Pending": "待處理", "Pending User Overlay Content": "待處理的使用者訊息覆蓋層內容", "Pending User Overlay Title": "待處理的使用者訊息覆蓋層標題", - "Perform OCR": "", + "Perform OCR": "執行 OCR", "Permission denied when accessing media devices": "存取媒體裝置時權限遭拒", "Permission denied when accessing microphone": "存取麥克風時權限遭拒", "Permission denied when accessing microphone: {{error}}": "存取麥克風時權限遭拒:{{error}}", @@ -1135,7 +1135,7 @@ "Pinned": "已釘選", "Pioneer insights": "先驅見解", "Pipe": "Pipe", - "Pipeline": "", + "Pipeline": "管線", "Pipeline deleted successfully": "成功刪除管線", "Pipeline downloaded successfully": "成功下載管線", "Pipelines": "管線", @@ -1184,7 +1184,7 @@ "Public": "公開", "Pull \"{{searchValue}}\" from Ollama.com": "從 Ollama.com 下載「{{searchValue}}」", "Pull a model from Ollama.com": "從 Ollama.com 下載模型", - "pypdfium2": "", + "pypdfium2": "pypdfium2", "Query Generation Prompt": "查詢生成提示詞", "Querying": "查詢中", "Quick Actions": "快速操作", @@ -1237,7 +1237,7 @@ "RESULT": "結果", "Retrieval": "檢索", "Retrieval Query Generation": "檢索查詢生成", - "Retrieved {{count}} sources": "", + "Retrieved {{count}} sources": "搜尋到 {{count}} 個來源", "Retrieved {{count}} sources_other": "搜索到 {{count}} 個來源", "Retrieved 1 source": "搜索到 1 個來源", "Rich Text Input for Chat": "使用富文字輸入對話", @@ -1397,7 +1397,7 @@ "Speech recognition error: {{error}}": "語音辨識錯誤:{{error}}", "Speech-to-Text": "語音轉文字 (STT) ", "Speech-to-Text Engine": "語音轉文字 (STT) 引擎", - "standard": "", + "standard": "標準", "Start of the channel": "頻道起點", "Start Tag": "起始標籤", "Status Updates": "顯示實時回答狀態", @@ -1426,7 +1426,7 @@ "System": "系統", "System Instructions": "系統指令", "System Prompt": "系統提示詞", - "Table Mode": "", + "Table Mode": "表格模式", "Tags": "標籤", "Tags Generation": "標籤生成", "Tags Generation Prompt": "標籤生成提示詞", @@ -1609,7 +1609,7 @@ "View Result from **{{NAME}}**": "檢視來自 **{{NAME}}** 的結果", "Visibility": "可見度", "Vision": "視覺", - "vlm": "", + "vlm": "視覺語言模型(VLM)", "Voice": "語音", "Voice Input": "語音輸入", "Voice mode": "語音模式", From a68342d5a887e36695e21f8c2aec593b159654ff Mon Sep 17 00:00:00 2001 From: Timothy Jaeryang Baek Date: Fri, 12 Sep 2025 15:05:37 +0400 Subject: [PATCH 02/74] refac: input menu --- src/lib/components/chat/MessageInput.svelte | 245 ++++++++------ .../chat/MessageInput/InputMenu.svelte | 194 +++++------ .../chat/MessageInput/OptionsMenu.svelte | 309 ++++++++++++++++++ src/lib/components/icons/Camera.svelte | 22 ++ src/lib/components/icons/Clip.svelte | 18 + src/lib/components/icons/Component.svelte | 22 ++ src/lib/components/icons/Grid.svelte | 22 ++ src/lib/components/icons/PlusAlt.svelte | 15 + src/lib/components/icons/Union.svelte | 22 ++ 9 files changed, 646 insertions(+), 223 deletions(-) create mode 100644 src/lib/components/chat/MessageInput/OptionsMenu.svelte create mode 100644 src/lib/components/icons/Camera.svelte create mode 100644 src/lib/components/icons/Clip.svelte create mode 100644 src/lib/components/icons/Component.svelte create mode 100644 src/lib/components/icons/Grid.svelte create mode 100644 src/lib/components/icons/PlusAlt.svelte create mode 100644 src/lib/components/icons/Union.svelte diff --git a/src/lib/components/chat/MessageInput.svelte b/src/lib/components/chat/MessageInput.svelte index e2fd03d8a5..7e1a765750 100644 --- a/src/lib/components/chat/MessageInput.svelte +++ b/src/lib/components/chat/MessageInput.svelte @@ -71,6 +71,9 @@ import Voice from '../icons/Voice.svelte'; import { getSessionUser } from '$lib/apis/auths'; import Terminal from '../icons/Terminal.svelte'; + import OptionsMenu from './MessageInput/OptionsMenu.svelte'; + import Component from '../icons/Component.svelte'; + import PlusAlt from '../icons/PlusAlt.svelte'; const i18n = getContext('i18n'); export let onChange: Function = () => {}; @@ -1657,7 +1660,6 @@
- +
- {#if $_user && (showToolsButton || (toggleFilters && toggleFilters.length > 0) || showWebSearchButton || showImageGenerationButton || showCodeInterpreterButton)} +
+ + { + await tick(); + + const chatInput = document.getElementById('chat-input'); + chatInput?.focus(); + }} + >
+ class="bg-transparent hover:bg-gray-100 text-gray-700 dark:text-white dark:hover:bg-gray-800 rounded-full size-8 flex justify-center items-center outline-hidden focus:outline-hidden" + > + +
+
-
- {#if showToolsButton} - + {#if showToolsButton} + + - - {/if} + + {toolServers.length + selectedToolIds.length} + + + + {/if} - {#each toggleFilters as filter, filterIdx (filter.id)} + {#each selectedFilterIds as filterId} + {@const filter = toggleFilters.find((f) => f.id === filterId)} + {#if filter} - - {/each} - {#if showWebSearchButton} - - {/if} + {/each} - {#if showImageGenerationButton} - - - - {/if} - {#if showCodeInterpreterButton} - - + + {/if} + + {#if imageGenerationEnabled} + + - - {/if} -
- {/if} + + + + + {/if} + + {#if codeInterpreterEnabled} + + + + {/if} +
diff --git a/src/lib/components/chat/MessageInput/InputMenu.svelte b/src/lib/components/chat/MessageInput/InputMenu.svelte index 351c882388..6192e22180 100644 --- a/src/lib/components/chat/MessageInput/InputMenu.svelte +++ b/src/lib/components/chat/MessageInput/InputMenu.svelte @@ -10,14 +10,10 @@ import Dropdown from '$lib/components/common/Dropdown.svelte'; import Tooltip from '$lib/components/common/Tooltip.svelte'; - import DocumentArrowUpSolid from '$lib/components/icons/DocumentArrowUpSolid.svelte'; - import Switch from '$lib/components/common/Switch.svelte'; - import GlobeAltSolid from '$lib/components/icons/GlobeAltSolid.svelte'; - import WrenchSolid from '$lib/components/icons/WrenchSolid.svelte'; - import CameraSolid from '$lib/components/icons/CameraSolid.svelte'; - import PhotoSolid from '$lib/components/icons/PhotoSolid.svelte'; - import CommandLineSolid from '$lib/components/icons/CommandLineSolid.svelte'; - import Spinner from '$lib/components/common/Spinner.svelte'; + import DocumentArrowUp from '$lib/components/icons/DocumentArrowUp.svelte'; + import Camera from '$lib/components/icons/Camera.svelte'; + import Note from '$lib/components/icons/Note.svelte'; + import Clip from '$lib/components/icons/Clip.svelte'; const i18n = getContext('i18n'); @@ -35,34 +31,13 @@ export let onClose: Function; - let tools = null; let show = false; - let showAllTools = false; - - $: if (show) { - init(); - } let fileUploadEnabled = true; $: fileUploadEnabled = fileUploadCapableModels.length === selectedModels.length && ($user?.role === 'admin' || $user?.permissions?.chat?.file_upload); - const init = async () => { - await _tools.set(await getTools(localStorage.token)); - if ($_tools) { - tools = $_tools.reduce((a, tool, i, arr) => { - a[tool.id] = { - name: tool.name, - description: tool.meta.description, - enabled: selectedToolIds.includes(tool.id) - }; - return a; - }, {}); - selectedToolIds = selectedToolIds.filter((id) => $_tools?.some((tool) => tool.id === id)); - } - }; - const detectMobile = () => { const userAgent = navigator.userAgent || navigator.vendor || window.opera; return /android|iphone|ipad|ipod|windows phone/i.test(userAgent); @@ -101,86 +76,36 @@
- {#if tools} - {#if Object.keys(tools).length > 0} -
- {#each Object.keys(tools) as toolId} - - {/each} -
- {#if Object.keys(tools).length > 3} - - {/if} -
- {/if} - {:else} -
- -
- -
- {/if} +
{$i18n.t('Upload Files')}
+ + { @@ -208,7 +133,7 @@ } }} > - +
{$i18n.t('Capture')}
@@ -222,24 +147,63 @@ className="w-full" > { if (fileUploadEnabled) { - uploadFilesHandler(); + if (!detectMobile()) { + screenCaptureHandler(); + } else { + const cameraInputElement = document.getElementById('camera-input'); + + if (cameraInputElement) { + cameraInputElement.click(); + } + } } }} > - -
{$i18n.t('Upload Files')}
+ +
{$i18n.t('Attach Notes')}
+
+ + + + { + if (fileUploadEnabled) { + if (!detectMobile()) { + screenCaptureHandler(); + } else { + const cameraInputElement = document.getElementById('camera-input'); + + if (cameraInputElement) { + cameraInputElement.click(); + } + } + } + }} + > + +
{$i18n.t('Attach Knowledge')}
{#if fileUploadEnabled} {#if $config?.features?.enable_google_drive_integration} { uploadGoogleDriveHandler(); }} @@ -277,7 +241,7 @@ {#if $config?.features?.enable_onedrive_integration} { uploadOneDriveHandler('personal'); }} @@ -381,7 +345,7 @@
{$i18n.t('Microsoft OneDrive (personal)')}
{ uploadOneDriveHandler('organizations'); }} diff --git a/src/lib/components/chat/MessageInput/OptionsMenu.svelte b/src/lib/components/chat/MessageInput/OptionsMenu.svelte new file mode 100644 index 0000000000..d1928c913c --- /dev/null +++ b/src/lib/components/chat/MessageInput/OptionsMenu.svelte @@ -0,0 +1,309 @@ + + + { + if (e.detail === false) { + onClose(); + } + }} +> + + + + +
+ + {#if toggleFilters && toggleFilters.length > 0} + {#each toggleFilters as filter, filterIdx (filter.id)} + + + + {/each} + {/if} + + {#if showWebSearchButton} + + + + {/if} + + {#if showImageGenerationButton} + + + + {/if} + + {#if showCodeInterpreterButton} + + + + {/if} + + {#if tools} +
+ + {#if Object.keys(tools).length > 0} +
+ {#each Object.keys(tools) as toolId} + + {/each} +
+ {#if Object.keys(tools).length > 3} + + {/if} + {/if} + {:else} +
+ +
+ {/if} +
+
+
diff --git a/src/lib/components/icons/Camera.svelte b/src/lib/components/icons/Camera.svelte new file mode 100644 index 0000000000..a553475fa1 --- /dev/null +++ b/src/lib/components/icons/Camera.svelte @@ -0,0 +1,22 @@ + + + diff --git a/src/lib/components/icons/Clip.svelte b/src/lib/components/icons/Clip.svelte new file mode 100644 index 0000000000..d3c89be163 --- /dev/null +++ b/src/lib/components/icons/Clip.svelte @@ -0,0 +1,18 @@ + + + diff --git a/src/lib/components/icons/Component.svelte b/src/lib/components/icons/Component.svelte new file mode 100644 index 0000000000..cafc7058ec --- /dev/null +++ b/src/lib/components/icons/Component.svelte @@ -0,0 +1,22 @@ + + + diff --git a/src/lib/components/icons/Grid.svelte b/src/lib/components/icons/Grid.svelte new file mode 100644 index 0000000000..8846f03107 --- /dev/null +++ b/src/lib/components/icons/Grid.svelte @@ -0,0 +1,22 @@ + + + diff --git a/src/lib/components/icons/PlusAlt.svelte b/src/lib/components/icons/PlusAlt.svelte new file mode 100644 index 0000000000..895fe0b763 --- /dev/null +++ b/src/lib/components/icons/PlusAlt.svelte @@ -0,0 +1,15 @@ + + + diff --git a/src/lib/components/icons/Union.svelte b/src/lib/components/icons/Union.svelte new file mode 100644 index 0000000000..71953329f4 --- /dev/null +++ b/src/lib/components/icons/Union.svelte @@ -0,0 +1,22 @@ + + + From ca853ca4656180487afcd84230d214f91db52533 Mon Sep 17 00:00:00 2001 From: Timothy Jaeryang Baek Date: Fri, 12 Sep 2025 15:06:11 +0400 Subject: [PATCH 03/74] refac/enh: sort toggle filter by default --- src/lib/components/chat/MessageInput/OptionsMenu.svelte | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib/components/chat/MessageInput/OptionsMenu.svelte b/src/lib/components/chat/MessageInput/OptionsMenu.svelte index d1928c913c..cd9587a9ee 100644 --- a/src/lib/components/chat/MessageInput/OptionsMenu.svelte +++ b/src/lib/components/chat/MessageInput/OptionsMenu.svelte @@ -88,7 +88,7 @@ transition={flyAndScale} > {#if toggleFilters && toggleFilters.length > 0} - {#each toggleFilters as filter, filterIdx (filter.id)} + {#each toggleFilters.sort( (a, b) => a.name.localeCompare( b.name, undefined, { sensitivity: 'base' } ) ) as filter, filterIdx (filter.id)} @@ -1770,7 +1770,7 @@ {#each selectedFilterIds as filterId} {@const filter = toggleFilters.find((f) => f.id === filterId)} {#if filter} - + + {:else} +
+ +
+ {/if} + + {#if showAllTools} + {#each Object.keys(tools) as toolId} + + {/each} + {:else} + {#if toggleFilters && toggleFilters.length > 0} + {#each toggleFilters.sort( (a, b) => a.name.localeCompare( b.name, undefined, { sensitivity: 'base' } ) ) as filter, filterIdx (filter.id)} + + + + {/each} + {/if} + + {#if showWebSearchButton} + + + {/if} + + {#if showImageGenerationButton} + + + + {/if} + + {#if showCodeInterpreterButton} + + - {/each} - {/if} - - {#if showWebSearchButton} - - - - {/if} - - {#if showImageGenerationButton} - - - - {/if} - - {#if showCodeInterpreterButton} - - - - {/if} - - {#if tools} -
- - {#if Object.keys(tools).length > 0} -
- {#each Object.keys(tools) as toolId} - - {/each} -
- {#if Object.keys(tools).length > 3} - - {/if} {/if} - {:else} -
- -
{/if}
From 136972ccf077998ab33e9b8d734a4ebf3c15fcdc Mon Sep 17 00:00:00 2001 From: Timothy Jaeryang Baek Date: Fri, 12 Sep 2025 15:54:42 +0400 Subject: [PATCH 06/74] refac: styling --- .../chat/MessageInput/InputMenu.svelte | 16 ++++++------- .../chat/MessageInput/OptionsMenu.svelte | 12 +++++----- src/lib/components/common/Switch.svelte | 4 ++-- src/lib/components/icons/Keyboard.svelte | 12 ++++------ .../components/layout/Sidebar/ChatMenu.svelte | 24 +++++++++---------- .../components/layout/Sidebar/UserMenu.svelte | 20 ++++++++-------- 6 files changed, 43 insertions(+), 45 deletions(-) diff --git a/src/lib/components/chat/MessageInput/InputMenu.svelte b/src/lib/components/chat/MessageInput/InputMenu.svelte index 6192e22180..2a58fffa9d 100644 --- a/src/lib/components/chat/MessageInput/InputMenu.svelte +++ b/src/lib/components/chat/MessageInput/InputMenu.svelte @@ -92,7 +92,7 @@ className="w-full" > { @@ -116,7 +116,7 @@ className="w-full" > { @@ -147,7 +147,7 @@ className="w-full" > { @@ -178,7 +178,7 @@ className="w-full" > { @@ -203,7 +203,7 @@ {#if fileUploadEnabled} {#if $config?.features?.enable_google_drive_integration} { uploadGoogleDriveHandler(); }} @@ -241,7 +241,7 @@ {#if $config?.features?.enable_onedrive_integration} { uploadOneDriveHandler('personal'); }} @@ -345,7 +345,7 @@
{$i18n.t('Microsoft OneDrive (personal)')}
{ uploadOneDriveHandler('organizations'); }} diff --git a/src/lib/components/chat/MessageInput/OptionsMenu.svelte b/src/lib/components/chat/MessageInput/OptionsMenu.svelte index f650dddf9a..3345e96687 100644 --- a/src/lib/components/chat/MessageInput/OptionsMenu.svelte +++ b/src/lib/components/chat/MessageInput/OptionsMenu.svelte @@ -93,7 +93,7 @@ > {#if tools} + {/if} + + {/if} {:else}
From 6b69c4da0fb9329ccf7024483960e070cf52ccab Mon Sep 17 00:00:00 2001 From: Timothy Jaeryang Baek Date: Fri, 12 Sep 2025 20:31:57 +0400 Subject: [PATCH 09/74] refac/enh: commands ui --- package-lock.json | 12 +- package.json | 1 + .../components/channel/MessageInput.svelte | 49 +- src/lib/components/chat/Chat.svelte | 1 - src/lib/components/chat/MessageInput.svelte | 699 +++++++++--------- .../MessageInput/CommandSuggestionList.svelte | 163 ++++ .../MessageInput/Commands/Knowledge.svelte | 341 ++++----- .../chat/MessageInput/Commands/Models.svelte | 117 +-- .../chat/MessageInput/Commands/Prompts.svelte | 153 ++-- src/lib/components/chat/Navbar.svelte | 45 +- src/lib/components/common/FileItem.svelte | 34 +- .../components/common/RichTextInput.svelte | 44 +- .../RichTextInput/FormattingButtons.svelte | 2 +- .../common/RichTextInput/MentionList.svelte | 85 +++ .../common/RichTextInput/commands.ts | 26 + .../common/RichTextInput/suggestions.ts | 69 ++ src/lib/components/icons/Database.svelte | 16 + src/lib/components/icons/DocumentPage.svelte | 26 + src/lib/components/icons/Youtube.svelte | 16 + 19 files changed, 1052 insertions(+), 847 deletions(-) create mode 100644 src/lib/components/chat/MessageInput/CommandSuggestionList.svelte create mode 100644 src/lib/components/common/RichTextInput/MentionList.svelte create mode 100644 src/lib/components/common/RichTextInput/commands.ts create mode 100644 src/lib/components/common/RichTextInput/suggestions.ts create mode 100644 src/lib/components/icons/Database.svelte create mode 100644 src/lib/components/icons/DocumentPage.svelte create mode 100644 src/lib/components/icons/Youtube.svelte diff --git a/package-lock.json b/package-lock.json index 48122812b8..d2fd62c282 100644 --- a/package-lock.json +++ b/package-lock.json @@ -37,6 +37,7 @@ "@tiptap/extensions": "^3.0.7", "@tiptap/pm": "^3.0.7", "@tiptap/starter-kit": "^3.0.7", + "@tiptap/suggestion": "^3.4.2", "@xyflow/svelte": "^0.1.19", "async": "^3.2.5", "bits-ui": "^0.21.15", @@ -3856,18 +3857,17 @@ } }, "node_modules/@tiptap/suggestion": { - "version": "3.0.9", - "resolved": "https://registry.npmjs.org/@tiptap/suggestion/-/suggestion-3.0.9.tgz", - "integrity": "sha512-irthqfUybezo3IwR6AXvyyTOtkzwfvvst58VXZtTnR1nN6NEcrs3TQoY3bGKGbN83bdiquKh6aU2nLnZfAhoXg==", + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/@tiptap/suggestion/-/suggestion-3.4.2.tgz", + "integrity": "sha512-sljtfiDtdAsbPOwrXrFGf64D6sXUjeU3Iz5v3TvN7TVJKozkZ/gaMkPRl+WC1CGwC6BnzQVDBEEa1e+aApV0mA==", "license": "MIT", - "peer": true, "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" }, "peerDependencies": { - "@tiptap/core": "^3.0.9", - "@tiptap/pm": "^3.0.9" + "@tiptap/core": "^3.4.2", + "@tiptap/pm": "^3.4.2" } }, "node_modules/@tiptap/y-tiptap": { diff --git a/package.json b/package.json index 054baf39c6..19c897cb48 100644 --- a/package.json +++ b/package.json @@ -81,6 +81,7 @@ "@tiptap/extensions": "^3.0.7", "@tiptap/pm": "^3.0.7", "@tiptap/starter-kit": "^3.0.7", + "@tiptap/suggestion": "^3.4.2", "@xyflow/svelte": "^0.1.19", "async": "^3.2.5", "bits-ui": "^0.21.15", diff --git a/src/lib/components/channel/MessageInput.svelte b/src/lib/components/channel/MessageInput.svelte index fc85ea2aa1..c7ca93b902 100644 --- a/src/lib/components/channel/MessageInput.svelte +++ b/src/lib/components/channel/MessageInput.svelte @@ -753,53 +753,10 @@ e = e.detail.event; const isCtrlPressed = e.ctrlKey || e.metaKey; // metaKey is for Cmd key on Mac - const commandsContainerElement = document.getElementById('commands-container'); + const suggestionsContainerElement = + document.getElementById('suggestions-container'); - if (commandsContainerElement) { - if (commandsContainerElement && e.key === 'ArrowUp') { - e.preventDefault(); - commandsElement.selectUp(); - - const commandOptionButton = [ - ...document.getElementsByClassName('selected-command-option-button') - ]?.at(-1); - commandOptionButton.scrollIntoView({ block: 'center' }); - } - - if (commandsContainerElement && e.key === 'ArrowDown') { - e.preventDefault(); - commandsElement.selectDown(); - - const commandOptionButton = [ - ...document.getElementsByClassName('selected-command-option-button') - ]?.at(-1); - commandOptionButton.scrollIntoView({ block: 'center' }); - } - - if (commandsContainerElement && e.key === 'Tab') { - e.preventDefault(); - - const commandOptionButton = [ - ...document.getElementsByClassName('selected-command-option-button') - ]?.at(-1); - - commandOptionButton?.click(); - } - - if (commandsContainerElement && e.key === 'Enter') { - e.preventDefault(); - - const commandOptionButton = [ - ...document.getElementsByClassName('selected-command-option-button') - ]?.at(-1); - - if (commandOptionButton) { - commandOptionButton?.click(); - } else { - document.getElementById('send-message-button')?.click(); - } - } - } else { + if (!suggestionsContainerElement) { if ( !$mobile || !( diff --git a/src/lib/components/chat/Chat.svelte b/src/lib/components/chat/Chat.svelte index faf4b30861..fd64c0226a 100644 --- a/src/lib/components/chat/Chat.svelte +++ b/src/lib/components/chat/Chat.svelte @@ -2259,7 +2259,6 @@ bind:selectedModels shareEnabled={!!history.currentId} {initNewChat} - showBanners={!showCommands} archiveChatHandler={() => {}} {moveChatHandler} onSaveTempChat={async () => { diff --git a/src/lib/components/chat/MessageInput.svelte b/src/lib/components/chat/MessageInput.svelte index 28128d5433..d24c5a5d2b 100644 --- a/src/lib/components/chat/MessageInput.svelte +++ b/src/lib/components/chat/MessageInput.svelte @@ -76,6 +76,10 @@ import { KokoroWorker } from '$lib/workers/KokoroWorker'; + import { getSuggestionRenderer } from '../common/RichTextInput/suggestions'; + import MentionList from '../common/RichTextInput/MentionList.svelte'; + import CommandSuggestionList from './MessageInput/CommandSuggestionList.svelte'; + const i18n = getContext('i18n'); export let onChange: Function = () => {}; @@ -428,9 +432,9 @@ }; let command = ''; - export let showCommands = false; $: showCommands = ['/', '#', '@'].includes(command?.charAt(0)) || '\\#' === command?.slice(0, 2); + let suggestions = null; let showTools = false; @@ -845,6 +849,115 @@ }; onMount(async () => { + suggestions = [ + { + char: '@', + render: getSuggestionRenderer(CommandSuggestionList, { + i18n, + onSelect: (e) => { + const { type, data } = e; + + if (type === 'model') { + atSelectedModel = data; + } + + document.getElementById('chat-input')?.focus(); + }, + + insertTextHandler: insertTextAtCursor, + onUpload: (e) => { + const { type, data } = e; + + if (type === 'file') { + if (files.find((f) => f.id === data.id)) { + return; + } + files = [ + ...files, + { + ...data, + status: 'processed' + } + ]; + } else { + dispatch('upload', e); + } + } + }) + }, + { + char: '/', + render: getSuggestionRenderer(CommandSuggestionList, { + i18n, + onSelect: (e) => { + const { type, data } = e; + + if (type === 'model') { + atSelectedModel = data; + } + + document.getElementById('chat-input')?.focus(); + }, + + insertTextHandler: insertTextAtCursor, + onUpload: (e) => { + const { type, data } = e; + + if (type === 'file') { + if (files.find((f) => f.id === data.id)) { + return; + } + files = [ + ...files, + { + ...data, + status: 'processed' + } + ]; + } else { + dispatch('upload', e); + } + } + }) + }, + { + char: '#', + render: getSuggestionRenderer(CommandSuggestionList, { + i18n, + onSelect: (e) => { + const { type, data } = e; + + if (type === 'model') { + atSelectedModel = data; + } + + document.getElementById('chat-input')?.focus(); + }, + + insertTextHandler: insertTextAtCursor, + onUpload: (e) => { + const { type, data } = e; + + if (type === 'file') { + if (files.find((f) => f.id === data.id)) { + return; + } + files = [ + ...files, + { + ...data, + status: 'processed' + } + ]; + } else { + dispatch('upload', e); + } + } + }) + } + ]; + + console.log(suggestions); loaded = true; window.setTimeout(() => { @@ -929,78 +1042,6 @@
{/if}
- -
- {#if atSelectedModel !== undefined} -
-
-
- model profile model.id === atSelectedModel.id)?.info?.meta - ?.profile_image_url ?? - ($i18n.language === 'dg-DG' - ? `${WEBUI_BASE_URL}/doge.png` - : `${WEBUI_BASE_URL}/static/favicon.png`)} - /> -
- {$i18n.t('Talk to model')}: - {atSelectedModel.name} -
-
-
- -
-
-
- {/if} - - { - const { type, data } = e; - - if (type === 'file') { - if (files.find((f) => f.id === data.id)) { - return; - } - files = [ - ...files, - { - ...data, - status: 'processed' - } - ]; - } else { - dispatch('upload', e); - } - }} - onSelect={(e) => { - const { type, data } = e; - - if (type === 'model') { - atSelectedModel = data; - } - - document.getElementById('chat-input')?.focus(); - }} - /> -
@@ -1066,6 +1107,38 @@ class="flex-1 flex flex-col relative w-full shadow-lg rounded-3xl border border-gray-50 dark:border-gray-850 hover:border-gray-100 focus-within:border-gray-100 hover:dark:border-gray-800 focus-within:dark:border-gray-800 transition px-1 bg-white/90 dark:bg-gray-400/5 dark:text-gray-100" dir={$settings?.chatDirection ?? 'auto'} > + {#if atSelectedModel !== undefined} +
+
+
+ model profile model.id === atSelectedModel.id)?.info?.meta + ?.profile_image_url ?? + ($i18n.language === 'dg-DG' + ? `${WEBUI_BASE_URL}/doge.png` + : `${WEBUI_BASE_URL}/static/favicon.png`)} + /> +
+ {atSelectedModel.name} +
+
+
+ +
+
+
+ {/if} + {#if files.length > 0}
{#each files as file, fileIdx} @@ -1075,7 +1148,7 @@ {#if atSelectedModel ? visionCapableModels.length === 0 : selectedModels.length !== visionCapableModels.length} { // Remove from UI state @@ -1161,250 +1235,201 @@ class="scrollbar-hidden rtl:text-right ltr:text-left bg-transparent dark:text-gray-100 outline-hidden w-full pt-2.5 pb-[5px] px-1 resize-none h-fit max-h-80 overflow-auto" id="chat-input-container" > - {#key $settings?.showFormattingToolbar ?? false} - { - prompt = e.md; - command = getCommand(); - }} - json={true} - messageInput={true} - showFormattingToolbar={$settings?.showFormattingToolbar ?? false} - floatingMenuPlacement={'top-start'} - insertPromptAsRichText={$settings?.insertPromptAsRichText ?? false} - shiftEnter={!($settings?.ctrlEnterToSend ?? false) && - (!$mobile || - !( - 'ontouchstart' in window || - navigator.maxTouchPoints > 0 || - navigator.msMaxTouchPoints > 0 - ))} - placeholder={placeholder ? placeholder : $i18n.t('Send a Message')} - largeTextAsFile={($settings?.largeTextAsFile ?? false) && !shiftKey} - autocomplete={$config?.features?.enable_autocomplete_generation && - ($settings?.promptAutocomplete ?? false)} - generateAutoCompletion={async (text) => { - if (selectedModelIds.length === 0 || !selectedModelIds.at(0)) { - toast.error($i18n.t('Please select a model first.')); - } - - const res = await generateAutoCompletion( - localStorage.token, - selectedModelIds.at(0), - text, - history?.currentId - ? createMessagesList(history, history.currentId) - : null - ).catch((error) => { - console.log(error); - - return null; - }); - - console.log(res); - return res; - }} - oncompositionstart={() => (isComposing = true)} - oncompositionend={(e) => { - compositionEndedAt = e.timeStamp; - isComposing = false; - }} - on:keydown={async (e) => { - e = e.detail.event; - - const isCtrlPressed = e.ctrlKey || e.metaKey; // metaKey is for Cmd key on Mac - const commandsContainerElement = - document.getElementById('commands-container'); - - if (e.key === 'Escape') { - stopResponse(); - } - - // Command/Ctrl + Shift + Enter to submit a message pair - if (isCtrlPressed && e.key === 'Enter' && e.shiftKey) { - e.preventDefault(); - createMessagePair(prompt); - } - - // Check if Ctrl + R is pressed - if (prompt === '' && isCtrlPressed && e.key.toLowerCase() === 'r') { - e.preventDefault(); - console.log('regenerate'); - - const regenerateButton = [ - ...document.getElementsByClassName('regenerate-response-button') - ]?.at(-1); - - regenerateButton?.click(); - } - - if (prompt === '' && e.key == 'ArrowUp') { - e.preventDefault(); - - const userMessageElement = [ - ...document.getElementsByClassName('user-message') - ]?.at(-1); - - if (userMessageElement) { - userMessageElement.scrollIntoView({ block: 'center' }); - const editButton = [ - ...document.getElementsByClassName('edit-user-message-button') - ]?.at(-1); - - editButton?.click(); - } - } - - if (commandsContainerElement) { - if (commandsContainerElement && e.key === 'ArrowUp') { - e.preventDefault(); - commandsElement.selectUp(); - - const commandOptionButton = [ - ...document.getElementsByClassName( - 'selected-command-option-button' - ) - ]?.at(-1); - commandOptionButton.scrollIntoView({ block: 'center' }); - } - - if (commandsContainerElement && e.key === 'ArrowDown') { - e.preventDefault(); - commandsElement.selectDown(); - - const commandOptionButton = [ - ...document.getElementsByClassName( - 'selected-command-option-button' - ) - ]?.at(-1); - commandOptionButton.scrollIntoView({ block: 'center' }); - } - - if (commandsContainerElement && e.key === 'Tab') { - e.preventDefault(); - - const commandOptionButton = [ - ...document.getElementsByClassName( - 'selected-command-option-button' - ) - ]?.at(-1); - - commandOptionButton?.click(); - } - - if (commandsContainerElement && e.key === 'Enter') { - e.preventDefault(); - - const commandOptionButton = [ - ...document.getElementsByClassName( - 'selected-command-option-button' - ) - ]?.at(-1); - - if (commandOptionButton) { - commandOptionButton?.click(); - } else { - document.getElementById('send-message-button')?.click(); - } - } - } else { - if ( - !$mobile || + {#if suggestions} + {#key $settings?.showFormattingToolbar ?? false} + { + prompt = e.md; + command = getCommand(); + }} + json={true} + messageInput={true} + showFormattingToolbar={$settings?.showFormattingToolbar ?? false} + floatingMenuPlacement={'top-start'} + insertPromptAsRichText={$settings?.insertPromptAsRichText ?? false} + shiftEnter={!($settings?.ctrlEnterToSend ?? false) && + (!$mobile || !( 'ontouchstart' in window || navigator.maxTouchPoints > 0 || navigator.msMaxTouchPoints > 0 - ) - ) { - if (inOrNearComposition(e)) { - return; - } + ))} + placeholder={placeholder ? placeholder : $i18n.t('Send a Message')} + largeTextAsFile={($settings?.largeTextAsFile ?? false) && !shiftKey} + autocomplete={$config?.features?.enable_autocomplete_generation && + ($settings?.promptAutocomplete ?? false)} + generateAutoCompletion={async (text) => { + if (selectedModelIds.length === 0 || !selectedModelIds.at(0)) { + toast.error($i18n.t('Please select a model first.')); + } - // Uses keyCode '13' for Enter key for chinese/japanese keyboards. - // - // Depending on the user's settings, it will send the message - // either when Enter is pressed or when Ctrl+Enter is pressed. - const enterPressed = - ($settings?.ctrlEnterToSend ?? false) - ? (e.key === 'Enter' || e.keyCode === 13) && isCtrlPressed - : (e.key === 'Enter' || e.keyCode === 13) && !e.shiftKey; + const res = await generateAutoCompletion( + localStorage.token, + selectedModelIds.at(0), + text, + history?.currentId + ? createMessagesList(history, history.currentId) + : null + ).catch((error) => { + console.log(error); - if (enterPressed) { - e.preventDefault(); - if (prompt !== '' || files.length > 0) { - dispatch('submit', prompt); - } + return null; + }); + + console.log(res); + return res; + }} + {suggestions} + oncompositionstart={() => (isComposing = true)} + oncompositionend={(e) => { + compositionEndedAt = e.timeStamp; + isComposing = false; + }} + on:keydown={async (e) => { + e = e.detail.event; + + const isCtrlPressed = e.ctrlKey || e.metaKey; // metaKey is for Cmd key on Mac + const suggestionsContainerElement = + document.getElementById('suggestions-container'); + + if (e.key === 'Escape') { + stopResponse(); + } + + // Command/Ctrl + Shift + Enter to submit a message pair + if (isCtrlPressed && e.key === 'Enter' && e.shiftKey) { + e.preventDefault(); + createMessagePair(prompt); + } + + // Check if Ctrl + R is pressed + if (prompt === '' && isCtrlPressed && e.key.toLowerCase() === 'r') { + e.preventDefault(); + console.log('regenerate'); + + const regenerateButton = [ + ...document.getElementsByClassName('regenerate-response-button') + ]?.at(-1); + + regenerateButton?.click(); + } + + if (prompt === '' && e.key == 'ArrowUp') { + e.preventDefault(); + + const userMessageElement = [ + ...document.getElementsByClassName('user-message') + ]?.at(-1); + + if (userMessageElement) { + userMessageElement.scrollIntoView({ block: 'center' }); + const editButton = [ + ...document.getElementsByClassName('edit-user-message-button') + ]?.at(-1); + + editButton?.click(); } } - } - if (e.key === 'Escape') { - console.log('Escape'); - atSelectedModel = undefined; - selectedToolIds = []; - selectedFilterIds = []; - - webSearchEnabled = false; - imageGenerationEnabled = false; - codeInterpreterEnabled = false; - } - }} - on:paste={async (e) => { - e = e.detail.event; - console.log(e); - - const clipboardData = e.clipboardData || window.clipboardData; - - if (clipboardData && clipboardData.items) { - for (const item of clipboardData.items) { - if (item.type.indexOf('image') !== -1) { - const blob = item.getAsFile(); - const reader = new FileReader(); - - reader.onload = function (e) { - files = [ - ...files, - { - type: 'image', - url: `${e.target.result}` - } - ]; - }; - - reader.readAsDataURL(blob); - } else if (item?.kind === 'file') { - const file = item.getAsFile(); - if (file) { - const _files = [file]; - await inputFilesHandler(_files); - e.preventDefault(); + if (!suggestionsContainerElement) { + if ( + !$mobile || + !( + 'ontouchstart' in window || + navigator.maxTouchPoints > 0 || + navigator.msMaxTouchPoints > 0 + ) + ) { + if (inOrNearComposition(e)) { + return; } - } else if (item.type === 'text/plain') { - if (($settings?.largeTextAsFile ?? false) && !shiftKey) { - const text = clipboardData.getData('text/plain'); - if (text.length > PASTED_TEXT_CHARACTER_LIMIT) { - e.preventDefault(); - const blob = new Blob([text], { type: 'text/plain' }); - const file = new File( - [blob], - `Pasted_Text_${Date.now()}.txt`, - { - type: 'text/plain' - } - ); + // Uses keyCode '13' for Enter key for chinese/japanese keyboards. + // + // Depending on the user's settings, it will send the message + // either when Enter is pressed or when Ctrl+Enter is pressed. + const enterPressed = + ($settings?.ctrlEnterToSend ?? false) + ? (e.key === 'Enter' || e.keyCode === 13) && isCtrlPressed + : (e.key === 'Enter' || e.keyCode === 13) && !e.shiftKey; - await uploadFileHandler(file, true); + if (enterPressed) { + e.preventDefault(); + if (prompt !== '' || files.length > 0) { + dispatch('submit', prompt); } } } } - } - }} - /> - {/key} + + if (e.key === 'Escape') { + console.log('Escape'); + atSelectedModel = undefined; + selectedToolIds = []; + selectedFilterIds = []; + + webSearchEnabled = false; + imageGenerationEnabled = false; + codeInterpreterEnabled = false; + } + }} + on:paste={async (e) => { + e = e.detail.event; + console.log(e); + + const clipboardData = e.clipboardData || window.clipboardData; + + if (clipboardData && clipboardData.items) { + for (const item of clipboardData.items) { + if (item.type.indexOf('image') !== -1) { + const blob = item.getAsFile(); + const reader = new FileReader(); + + reader.onload = function (e) { + files = [ + ...files, + { + type: 'image', + url: `${e.target.result}` + } + ]; + }; + + reader.readAsDataURL(blob); + } else if (item?.kind === 'file') { + const file = item.getAsFile(); + if (file) { + const _files = [file]; + await inputFilesHandler(_files); + e.preventDefault(); + } + } else if (item.type === 'text/plain') { + if (($settings?.largeTextAsFile ?? false) && !shiftKey) { + const text = clipboardData.getData('text/plain'); + + if (text.length > PASTED_TEXT_CHARACTER_LIMIT) { + e.preventDefault(); + const blob = new Blob([text], { type: 'text/plain' }); + const file = new File( + [blob], + `Pasted_Text_${Date.now()}.txt`, + { + type: 'text/plain' + } + ); + + await uploadFileHandler(file, true); + } + } + } + } + } + }} + /> + {/key} + {/if}
{:else}