From 79e1daff5a6545c018fcd63911fe6fda81215be4 Mon Sep 17 00:00:00 2001 From: RedwindA Date: Tue, 24 Feb 2026 21:44:21 +0800 Subject: [PATCH] feat(web): add custom-model create hint and i18n translations --- .../channels/modals/EditChannelModal.jsx | 31 ++++++++++++++++++ .../table/channels/modals/EditTagModal.jsx | 32 ++++++++++++++++++- web/src/i18n/locales/en.json | 3 +- web/src/i18n/locales/fr.json | 3 +- web/src/i18n/locales/ja.json | 3 +- web/src/i18n/locales/ru.json | 3 +- web/src/i18n/locales/vi.json | 3 +- web/src/i18n/locales/zh-CN.json | 3 +- web/src/i18n/locales/zh-TW.json | 3 +- 9 files changed, 76 insertions(+), 8 deletions(-) diff --git a/web/src/components/table/channels/modals/EditChannelModal.jsx b/web/src/components/table/channels/modals/EditChannelModal.jsx index 8d30a5a34..3d3afcc38 100644 --- a/web/src/components/table/channels/modals/EditChannelModal.jsx +++ b/web/src/components/table/channels/modals/EditChannelModal.jsx @@ -191,6 +191,7 @@ const EditChannelModal = (props) => { const [fullModels, setFullModels] = useState([]); const [modelGroups, setModelGroups] = useState([]); const [customModel, setCustomModel] = useState(''); + const [modelSearchValue, setModelSearchValue] = useState(''); const [modalImageUrl, setModalImageUrl] = useState(''); const [isModalOpenurl, setIsModalOpenurl] = useState(false); const [modelModalVisible, setModelModalVisible] = useState(false); @@ -231,6 +232,25 @@ const EditChannelModal = (props) => { return []; } }, [inputs.model_mapping]); + const modelSearchMatchedCount = useMemo(() => { + const keyword = modelSearchValue.trim(); + if (!keyword) { + return modelOptions.length; + } + return modelOptions.reduce( + (count, option) => count + (selectFilter(keyword, option) ? 1 : 0), + 0, + ); + }, [modelOptions, modelSearchValue]); + const modelSearchHintText = useMemo(() => { + const keyword = modelSearchValue.trim(); + if (!keyword || modelSearchMatchedCount !== 0) { + return ''; + } + return t('未匹配到模型,按回车键可将「{{name}}」作为自定义模型名添加', { + name: keyword, + }); + }, [modelSearchMatchedCount, modelSearchValue, t]); const [isIonetChannel, setIsIonetChannel] = useState(false); const [ionetMetadata, setIonetMetadata] = useState(null); const [codexOAuthModalVisible, setCodexOAuthModalVisible] = useState(false); @@ -1019,6 +1039,7 @@ const EditChannelModal = (props) => { }, [inputs]); useEffect(() => { + setModelSearchValue(''); if (props.visible) { if (isEdit) { loadChannel(); @@ -1073,6 +1094,7 @@ const EditChannelModal = (props) => { // 重置豆包隐藏入口状态 setDoubaoApiEditUnlocked(false); doubaoApiClickCountRef.current = 0; + setModelSearchValue(''); // 清空表单中的key_mode字段 if (formApiRef.current) { formApiRef.current.setValue('key_mode', undefined); @@ -2815,9 +2837,18 @@ const EditChannelModal = (props) => { rules={[{ required: true, message: t('请选择模型') }]} multiple filter={selectFilter} + allowCreate autoClearSearchValue={false} searchPosition='dropdown' optionList={modelOptions} + onSearch={(value) => setModelSearchValue(value)} + innerBottomSlot={ + modelSearchHintText ? ( + + {modelSearchHintText} + + ) : null + } style={{ width: '100%' }} onChange={(value) => handleInputChange('models', value)} renderSelectedItem={(optionNode) => { diff --git a/web/src/components/table/channels/modals/EditTagModal.jsx b/web/src/components/table/channels/modals/EditTagModal.jsx index d4d060690..fbb00be58 100644 --- a/web/src/components/table/channels/modals/EditTagModal.jsx +++ b/web/src/components/table/channels/modals/EditTagModal.jsx @@ -17,7 +17,7 @@ along with this program. If not, see . For commercial licensing, please contact support@quantumnous.com */ -import React, { useState, useEffect, useRef } from 'react'; +import React, { useState, useEffect, useRef, useMemo } from 'react'; import { API, showError, @@ -64,6 +64,7 @@ const EditTagModal = (props) => { const [modelOptions, setModelOptions] = useState([]); const [groupOptions, setGroupOptions] = useState([]); const [customModel, setCustomModel] = useState(''); + const [modelSearchValue, setModelSearchValue] = useState(''); const originInputs = { tag: '', new_tag: null, @@ -74,6 +75,25 @@ const EditTagModal = (props) => { header_override: null, }; const [inputs, setInputs] = useState(originInputs); + const modelSearchMatchedCount = useMemo(() => { + const keyword = modelSearchValue.trim(); + if (!keyword) { + return modelOptions.length; + } + return modelOptions.reduce( + (count, option) => count + (selectFilter(keyword, option) ? 1 : 0), + 0, + ); + }, [modelOptions, modelSearchValue]); + const modelSearchHintText = useMemo(() => { + const keyword = modelSearchValue.trim(); + if (!keyword || modelSearchMatchedCount !== 0) { + return ''; + } + return t('未匹配到模型,按回车键可将「{{name}}」作为自定义模型名添加', { + name: keyword, + }); + }, [modelSearchMatchedCount, modelSearchValue, t]); const formApiRef = useRef(null); const getInitValues = () => ({ ...originInputs }); @@ -292,6 +312,7 @@ const EditTagModal = (props) => { fetchModels().then(); fetchGroups().then(); fetchTagModels().then(); + setModelSearchValue(''); if (formApiRef.current) { formApiRef.current.setValues({ ...getInitValues(), @@ -461,9 +482,18 @@ const EditTagModal = (props) => { placeholder={t('请选择该渠道所支持的模型,留空则不更改')} multiple filter={selectFilter} + allowCreate autoClearSearchValue={false} searchPosition='dropdown' optionList={modelOptions} + onSearch={(value) => setModelSearchValue(value)} + innerBottomSlot={ + modelSearchHintText ? ( + + {modelSearchHintText} + + ) : null + } style={{ width: '100%' }} onChange={(value) => handleInputChange('models', value)} /> diff --git a/web/src/i18n/locales/en.json b/web/src/i18n/locales/en.json index e06c68362..f6c13e7d8 100644 --- a/web/src/i18n/locales/en.json +++ b/web/src/i18n/locales/en.json @@ -2835,6 +2835,7 @@ "缓存写": "Cache Write", "写": "Write", "根据 Anthropic 协定,/v1/messages 的输入 tokens 仅统计非缓存输入,不包含缓存读取与缓存写入 tokens。": "Per Anthropic conventions, /v1/messages input tokens count only non-cached input and exclude cache read/write tokens.", - "设计版本": "b80c3466cb6feafeb3990c7820e10e50" + "设计版本": "b80c3466cb6feafeb3990c7820e10e50", + "未匹配到模型,按回车键可将「{{name}}」作为自定义模型名添加": "No matching models. Press Enter to add \"{{name}}\" as a custom model name." } } diff --git a/web/src/i18n/locales/fr.json b/web/src/i18n/locales/fr.json index 2843728b8..c36b969dd 100644 --- a/web/src/i18n/locales/fr.json +++ b/web/src/i18n/locales/fr.json @@ -2737,6 +2737,7 @@ "缓存写": "Écriture cache", "写": "Écriture", "根据 Anthropic 协定,/v1/messages 的输入 tokens 仅统计非缓存输入,不包含缓存读取与缓存写入 tokens。": "Selon la convention Anthropic, les tokens d'entrée de /v1/messages ne comptent que les entrées non mises en cache et excluent les tokens de lecture/écriture du cache.", - "设计版本": "b80c3466cb6feafeb3990c7820e10e50" + "设计版本": "b80c3466cb6feafeb3990c7820e10e50", + "未匹配到模型,按回车键可将「{{name}}」作为自定义模型名添加": "Aucun modèle correspondant. Appuyez sur Entrée pour ajouter «{{name}}» comme nom de modèle personnalisé." } } diff --git a/web/src/i18n/locales/ja.json b/web/src/i18n/locales/ja.json index d18a62923..2951e9ea3 100644 --- a/web/src/i18n/locales/ja.json +++ b/web/src/i18n/locales/ja.json @@ -2720,6 +2720,7 @@ "缓存写": "キャッシュ書込", "写": "書込", "根据 Anthropic 协定,/v1/messages 的输入 tokens 仅统计非缓存输入,不包含缓存读取与缓存写入 tokens。": "Anthropic の仕様により、/v1/messages の入力 tokens は非キャッシュ入力のみを集計し、キャッシュ読み取り/書き込み tokens は含みません。", - "设计版本": "b80c3466cb6feafeb3990c7820e10e50" + "设计版本": "b80c3466cb6feafeb3990c7820e10e50", + "未匹配到模型,按回车键可将「{{name}}」作为自定义模型名添加": "一致するモデルが見つかりません。Enterキーで「{{name}}」をカスタムモデル名として追加できます。" } } diff --git a/web/src/i18n/locales/ru.json b/web/src/i18n/locales/ru.json index 099f405c9..82ccb0edf 100644 --- a/web/src/i18n/locales/ru.json +++ b/web/src/i18n/locales/ru.json @@ -2750,6 +2750,7 @@ "缓存写": "Запись в кэш", "写": "Запись", "根据 Anthropic 协定,/v1/messages 的输入 tokens 仅统计非缓存输入,不包含缓存读取与缓存写入 tokens。": "Согласно соглашению Anthropic, входные токены /v1/messages учитывают только некэшированный ввод и не включают токены чтения/записи кэша.", - "设计版本": "b80c3466cb6feafeb3990c7820e10e50" + "设计版本": "b80c3466cb6feafeb3990c7820e10e50", + "未匹配到模型,按回车键可将「{{name}}」作为自定义模型名添加": "Совпадающих моделей не найдено. Нажмите Enter, чтобы добавить «{{name}}» как пользовательское имя модели." } } diff --git a/web/src/i18n/locales/vi.json b/web/src/i18n/locales/vi.json index d2602efdf..f78620cff 100644 --- a/web/src/i18n/locales/vi.json +++ b/web/src/i18n/locales/vi.json @@ -3296,6 +3296,7 @@ "缓存写": "Ghi bộ nhớ đệm", "写": "Ghi", "根据 Anthropic 协定,/v1/messages 的输入 tokens 仅统计非缓存输入,不包含缓存读取与缓存写入 tokens。": "Theo quy ước của Anthropic, input tokens của /v1/messages chỉ tính phần đầu vào không dùng cache và không bao gồm tokens đọc/ghi cache.", - "设计版本": "b80c3466cb6feafeb3990c7820e10e50" + "设计版本": "b80c3466cb6feafeb3990c7820e10e50", + "未匹配到模型,按回车键可将「{{name}}」作为自定义模型名添加": "Không tìm thấy mô hình khớp. Nhấn Enter để thêm \"{{name}}\" làm tên mô hình tùy chỉnh." } } diff --git a/web/src/i18n/locales/zh-CN.json b/web/src/i18n/locales/zh-CN.json index d067ad569..fb135f6fb 100644 --- a/web/src/i18n/locales/zh-CN.json +++ b/web/src/i18n/locales/zh-CN.json @@ -2812,6 +2812,7 @@ "缓存读": "缓存读", "缓存写": "缓存写", "写": "写", - "根据 Anthropic 协定,/v1/messages 的输入 tokens 仅统计非缓存输入,不包含缓存读取与缓存写入 tokens。": "根据 Anthropic 协定,/v1/messages 的输入 tokens 仅统计非缓存输入,不包含缓存读取与缓存写入 tokens。" + "根据 Anthropic 协定,/v1/messages 的输入 tokens 仅统计非缓存输入,不包含缓存读取与缓存写入 tokens。": "根据 Anthropic 协定,/v1/messages 的输入 tokens 仅统计非缓存输入,不包含缓存读取与缓存写入 tokens。", + "未匹配到模型,按回车键可将「{{name}}」作为自定义模型名添加": "未匹配到模型,按回车键可将「{{name}}」作为自定义模型名添加" } } diff --git a/web/src/i18n/locales/zh-TW.json b/web/src/i18n/locales/zh-TW.json index 26f7092b7..85be3f9f7 100644 --- a/web/src/i18n/locales/zh-TW.json +++ b/web/src/i18n/locales/zh-TW.json @@ -2805,6 +2805,7 @@ "填写服务器地址后自动生成:": "填寫伺服器位址後自動生成:", "自动生成:": "自動生成:", "请先填写服务器地址,以自动生成完整的端点 URL": "請先填寫伺服器位址,以自動生成完整的端點 URL", - "端点 URL 必须是完整地址(以 http:// 或 https:// 开头)": "端點 URL 必須是完整位址(以 http:// 或 https:// 開頭)" + "端点 URL 必须是完整地址(以 http:// 或 https:// 开头)": "端點 URL 必須是完整位址(以 http:// 或 https:// 開頭)", + "未匹配到模型,按回车键可将「{{name}}」作为自定义模型名添加": "未匹配到模型,按下 Enter 鍵可將「{{name}}」作為自訂模型名稱新增" } }