From d77bb386b335b8d0e5bc0f70aa7739ee7ae891a8 Mon Sep 17 00:00:00 2001 From: su-fen <715041@qq.com> Date: Tue, 21 Jul 2026 15:56:18 +0800 Subject: [PATCH] feat(chat): align model picker controls across frontends --- crates/agent-gateway/web/src/i18n/config.ts | 2 - .../web/src/pages/chat/ChatHeader.tsx | 54 ++++++++++++++++--- crates/agent-gui/src/i18n/config.ts | 2 - .../src/pages/chat/components/ChatHeader.tsx | 3 -- .../chat/execution-mode-model-picker.test.mjs | 10 ++++ 5 files changed, 56 insertions(+), 15 deletions(-) diff --git a/crates/agent-gateway/web/src/i18n/config.ts b/crates/agent-gateway/web/src/i18n/config.ts index 1e14097f1..fc8fbdefd 100644 --- a/crates/agent-gateway/web/src/i18n/config.ts +++ b/crates/agent-gateway/web/src/i18n/config.ts @@ -170,7 +170,6 @@ export const translations: Record> = { "chat.expandProvider": "展开该提供商的模型", "chat.collapseProvider": "收起该提供商的模型", "chat.searchModel": "搜索模型...", - "chat.executionModeHint": "切换对下一条消息生效,当前对话继续", "chat.noModelFound": "未找到匹配的模型", "chat.aborted": "已取消", "chat.contextWindow": "上下文窗口", @@ -2036,7 +2035,6 @@ export const translations: Record> = { "chat.expandProvider": "Expand provider models", "chat.collapseProvider": "Collapse provider models", "chat.searchModel": "Search models...", - "chat.executionModeHint": "Applies from the next message; the conversation continues", "chat.noModelFound": "No matching models found", "chat.aborted": "Cancelled", "chat.contextWindow": "Context Window", diff --git a/crates/agent-gateway/web/src/pages/chat/ChatHeader.tsx b/crates/agent-gateway/web/src/pages/chat/ChatHeader.tsx index 6ccfbef30..cdc35a3a6 100644 --- a/crates/agent-gateway/web/src/pages/chat/ChatHeader.tsx +++ b/crates/agent-gateway/web/src/pages/chat/ChatHeader.tsx @@ -1,5 +1,5 @@ import { Popover } from "@base-ui/react"; -import { memo, type ReactNode, useEffect, useId, useMemo, useState } from "react"; +import { memo, type ReactNode, useEffect, useId, useMemo, useRef, useState } from "react"; import { Check, ChevronDown, @@ -9,6 +9,7 @@ import { Moon, OpenaiChatgptIcon, PanelLeft, + Search, Settings, Sun, } from "../../components/icons"; @@ -83,20 +84,25 @@ export const ChatHeader = memo(function ChatHeader(props: { ? t("tooltip.switchToDark") : t("tooltip.switchToAuto"); const [isModelPickerOpen, setIsModelPickerOpen] = useState(false); + const [modelSearch, setModelSearch] = useState(""); const [expandedGroups, setExpandedGroups] = useState>({}); + const searchInputRef = useRef(null); const executionModeRadioName = useId(); useEffect(() => { if (isModelPickerOpen) { + setModelSearch(""); setExpandedGroups({}); } }, [isModelPickerOpen]); + const normalizedSearch = modelSearch.trim().toLowerCase(); const groups = useMemo(() => groupModelOptionsByProvider(modelOptions), [modelOptions]); const selectedOption = modelOptions.find((option) => option.value === selectedValue); const selectedGroupId = selectedOption?.providerId; - // 默认全部折叠,仅当前选中模型所在分组展开 - const isGroupExpanded = (id: string) => expandedGroups[id] ?? id === selectedGroupId; + // 默认全部折叠,仅当前选中模型所在分组展开;搜索时强制展开所有匹配分组 + const isGroupExpanded = (id: string) => + normalizedSearch.length > 0 || (expandedGroups[id] ?? id === selectedGroupId); const toggleGroup = (id: string) => setExpandedGroups((prev) => ({ ...prev, @@ -153,6 +159,7 @@ export const ChatHeader = memo(function ChatHeader(props: { className="z-50" > @@ -208,16 +215,47 @@ export const ChatHeader = memo(function ChatHeader(props: { -
- {t("chat.executionModeHint")} -
); })()} -
+
+
+ + setModelSearch(event.target.value)} + placeholder={t("chat.searchModel")} + className="min-w-0 flex-1 bg-transparent text-xs text-foreground outline-none placeholder:text-muted-foreground/60" + onKeyDown={(event) => event.stopPropagation()} + /> +
+
+
{(() => { let animationIndex = 0; - return groups.map((group, groupIndex) => { + const filteredGroups = normalizedSearch + ? groups + .map((group) => ({ + ...group, + opts: group.opts.filter( + (option) => + option.model.toLowerCase().includes(normalizedSearch) || + option.providerName.toLowerCase().includes(normalizedSearch), + ), + })) + .filter((group) => group.opts.length > 0) + : groups; + + if (filteredGroups.length === 0) { + return ( +
+ {t("chat.noModelFound")} +
+ ); + } + + return filteredGroups.map((group, groupIndex) => { const expanded = isGroupExpanded(group.id); return (
diff --git a/crates/agent-gui/src/i18n/config.ts b/crates/agent-gui/src/i18n/config.ts index 5faa75e80..fa2904b92 100644 --- a/crates/agent-gui/src/i18n/config.ts +++ b/crates/agent-gui/src/i18n/config.ts @@ -190,7 +190,6 @@ export const translations: Record> = { "chat.branchUnavailable": "旧历史缺少稳定消息标识,无法创建分支", "chat.selectModel": "选择模型", "chat.searchModel": "搜索模型...", - "chat.executionModeHint": "切换对下一条消息生效,当前对话继续", "chat.noModelFound": "未找到匹配的模型", "chat.expandProvider": "展开该提供商的模型", "chat.collapseProvider": "收起该提供商的模型", @@ -2137,7 +2136,6 @@ export const translations: Record> = { "chat.branchUnavailable": "Legacy history lacks stable message ids; cannot branch", "chat.selectModel": "Select Model", "chat.searchModel": "Search models...", - "chat.executionModeHint": "Applies from the next message; the conversation continues", "chat.noModelFound": "No matching models found", "chat.expandProvider": "Expand provider models", "chat.collapseProvider": "Collapse provider models", diff --git a/crates/agent-gui/src/pages/chat/components/ChatHeader.tsx b/crates/agent-gui/src/pages/chat/components/ChatHeader.tsx index 9449dcdd4..10136ebdd 100644 --- a/crates/agent-gui/src/pages/chat/components/ChatHeader.tsx +++ b/crates/agent-gui/src/pages/chat/components/ChatHeader.tsx @@ -226,9 +226,6 @@ export const ChatHeader = memo(function ChatHeader(props: {
-
- {t("chat.executionModeHint")} -
); })()} diff --git a/crates/agent-gui/test/chat/execution-mode-model-picker.test.mjs b/crates/agent-gui/test/chat/execution-mode-model-picker.test.mjs index 87bbb23a8..8e928956c 100644 --- a/crates/agent-gui/test/chat/execution-mode-model-picker.test.mjs +++ b/crates/agent-gui/test/chat/execution-mode-model-picker.test.mjs @@ -43,3 +43,13 @@ test("popover interactions preserve mode changes and close after model selection assert.match(source, /onSelectModel\(parsed\);\s+setIsModelPickerOpen\(false\);/); } }); + +test("model pickers search models and providers", () => { + for (const source of headerSources) { + assert.match(source, /initialFocus=\{searchInputRef\}/); + assert.match(source, /placeholder=\{t\("chat\.searchModel"\)\}/); + assert.match(source, /\w+\.model\.toLowerCase\(\)\.includes\(normalizedSearch\)/); + assert.match(source, /\w+\.providerName\.toLowerCase\(\)\.includes\(normalizedSearch\)/); + assert.match(source, /t\("chat\.noModelFound"\)/); + } +});