From 68e345708311a42a48b508f44b1ba1a24c3b8d4b Mon Sep 17 00:00:00 2001 From: Suzumiya Date: Sun, 6 Sep 2026 22:01:42 +0800 Subject: [PATCH 1/6] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=20AGC=20=E9=A6=96?= =?UTF-8?q?=E9=A1=B5=E4=B8=8E=E9=A1=B9=E7=9B=AE=E5=AF=B9=E8=AF=9D=E7=9A=84?= =?UTF-8?q?=E6=A8=A1=E5=9E=8B=E9=80=89=E6=8B=A9=E5=85=A5=E5=8F=A3=E5=92=8C?= =?UTF-8?q?=E4=BA=A4=E4=BA=92?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 首页聊天框架右下角新增模型选择入口,复用项目右侧对话的模型选择器,目录按需加载且不阻塞开启创作 修复首次进入项目时右侧模型选择器点不动的问题:选择器不再因目录加载或对话进行中被禁用,切换模型只影响后续轮次 为模型选择器菜单增加加载占位与统一弹层定位,首页组件使用相对定位容器 同步更新 AGC 后台模型别名与对话选择技术方案文档 补充首页模型选择与首次进入项目可交互的回归测试 --- .../ConversationModelSelect.tsx | 45 ++++++++++++++----- .../ProjectSupervisorView.tsx | 4 +- apps/ai-game-creator-shell/src/styles.css | 18 ++++++-- .../src/view/home/index.tsx | 10 ++++- .../tests/appSurface/home.suite.ts | 40 +++++++++++++++++ .../appSurface/project-development.suite.ts | 44 ++++++++++++++++++ ...方案】AGC后台模型别名与对话选择-2026-09-05.md | 2 + 7 files changed, 147 insertions(+), 16 deletions(-) diff --git a/apps/ai-game-creator-shell/src/features/project-workspace/ConversationModelSelect.tsx b/apps/ai-game-creator-shell/src/features/project-workspace/ConversationModelSelect.tsx index 92d10d2a2..b4acba71b 100644 --- a/apps/ai-game-creator-shell/src/features/project-workspace/ConversationModelSelect.tsx +++ b/apps/ai-game-creator-shell/src/features/project-workspace/ConversationModelSelect.tsx @@ -1,5 +1,5 @@ import { Check, ChevronDown, RefreshCcw } from 'lucide-react'; -import { useCallback, useEffect, useState } from 'react'; +import { useCallback, useEffect, useRef, useState } from 'react'; import { resolveTauriInvoke } from '../../app/tauri'; import type { GameCreatorAppConfigView } from '../../app/types'; @@ -9,21 +9,26 @@ import { } from '../../services/clientApi'; export function ConversationModelSelect({ + className, disabled, + lazy = false, onReady, }: { + className?: string; disabled: boolean; - onReady: (ready: boolean) => void; + lazy?: boolean; + onReady?: (ready: boolean) => void; }) { const [models, setModels] = useState([]); const [selected, setSelected] = useState(''); - const [busy, setBusy] = useState(true); + const [busy, setBusy] = useState(!lazy); const [error, setError] = useState(''); const [open, setOpen] = useState(false); + const initializedRef = useRef(false); const refresh = useCallback(async () => { setBusy(true); setError(''); - onReady(false); + onReady?.(false); try { const invoke = resolveTauriInvoke(); if (!invoke) throw new Error('Native host unavailable'); @@ -43,7 +48,7 @@ export function ConversationModelSelect({ if (saved.config.selectedModelId !== id) throw new Error('Default selection was not saved'); } - onReady(available); + onReady?.(available); if (!available) setError('请选择可用模型'); } catch { setModels([]); @@ -54,11 +59,12 @@ export function ConversationModelSelect({ }, [onReady]); useEffect(() => { + if (lazy) return; void refresh(); - }, [refresh]); + }, [refresh, lazy]); async function select(id: string) { - onReady(false); + onReady?.(false); setBusy(true); setError(''); try { @@ -71,7 +77,7 @@ export function ConversationModelSelect({ if (result.config.selectedModelId !== id) throw new Error('Selection was not saved'); setSelected(id); - onReady(true); + onReady?.(true); } catch { setError('模型选择保存失败'); } finally { @@ -80,7 +86,13 @@ export function ConversationModelSelect({ } return ( -
+
{error ? {error} : null}
-
-- 2.52.0 From dab6a5c59d683b18a8247d72af04444db30d7775 Mon Sep 17 00:00:00 2001 From: Suzumiya Date: Mon, 7 Sep 2026 14:21:38 +0800 Subject: [PATCH 3/6] =?UTF-8?q?=E6=A8=A1=E5=9E=8B=E9=80=89=E6=8B=A9?= =?UTF-8?q?=E4=B8=8B=E6=8B=89=E6=94=AF=E6=8C=81=E7=82=B9=E5=87=BB=E5=A4=96?= =?UTF-8?q?=E9=83=A8=E4=B8=8E=20Esc=20=E8=87=AA=E5=8A=A8=E6=94=B6=E8=B5=B7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 为 ConversationModelSelect 增加点击外部关闭和 Esc 关闭,首页与项目右侧对话共用生效 补充点击外部与 Esc 收起的回归测试 --- .../ConversationModelSelect.tsx | 23 ++++++++++++++++ .../tests/conversationModelSelect.test.tsx | 26 +++++++++++++++++++ 2 files changed, 49 insertions(+) diff --git a/apps/ai-game-creator-shell/src/features/project-workspace/ConversationModelSelect.tsx b/apps/ai-game-creator-shell/src/features/project-workspace/ConversationModelSelect.tsx index b4acba71b..6869ede23 100644 --- a/apps/ai-game-creator-shell/src/features/project-workspace/ConversationModelSelect.tsx +++ b/apps/ai-game-creator-shell/src/features/project-workspace/ConversationModelSelect.tsx @@ -25,6 +25,7 @@ export function ConversationModelSelect({ const [error, setError] = useState(''); const [open, setOpen] = useState(false); const initializedRef = useRef(false); + const containerRef = useRef(null); const refresh = useCallback(async () => { setBusy(true); setError(''); @@ -63,6 +64,27 @@ export function ConversationModelSelect({ void refresh(); }, [refresh, lazy]); + useEffect(() => { + if (!open) return; + function handleOutsidePointerDown(event: MouseEvent) { + const target = event.target as Node | null; + if (containerRef.current && !containerRef.current.contains(target)) { + setOpen(false); + } + } + function handleEscape(event: KeyboardEvent) { + if (event.key === 'Escape') { + setOpen(false); + } + } + document.addEventListener('mousedown', handleOutsidePointerDown); + document.addEventListener('keydown', handleEscape); + return () => { + document.removeEventListener('mousedown', handleOutsidePointerDown); + document.removeEventListener('keydown', handleEscape); + }; + }, [open]); + async function select(id: string) { onReady?.(false); setBusy(true); @@ -87,6 +109,7 @@ export function ConversationModelSelect({ return (
{ await screen.findByText('模型选择保存失败'); expect(onReady).toHaveBeenLastCalledWith(false); }); + +test('closes the menu when clicking outside', async () => { + const onReady = vi.fn(); + render(); + await screen.findByRole('button', { name: '对话模型' }); + fireEvent.click(screen.getByRole('button', { name: '对话模型' })); + expect(screen.getByRole('option', { name: '快速' })).not.toBeNull(); + + fireEvent.mouseDown(document.body); + await waitFor(() => + expect(screen.queryByRole('option', { name: '快速' })).toBeNull(), + ); +}); + +test('closes the menu on Escape', async () => { + const onReady = vi.fn(); + render(); + await screen.findByRole('button', { name: '对话模型' }); + fireEvent.click(screen.getByRole('button', { name: '对话模型' })); + expect(screen.getByRole('option', { name: '快速' })).not.toBeNull(); + + fireEvent.keyDown(document, { key: 'Escape' }); + await waitFor(() => + expect(screen.queryByRole('option', { name: '快速' })).toBeNull(), + ); +}); -- 2.52.0 From 70fdd186cfdd2b32792cebbaab1310a3411abcee Mon Sep 17 00:00:00 2001 From: Suzumiya Date: Mon, 7 Sep 2026 14:26:34 +0800 Subject: [PATCH 4/6] =?UTF-8?q?=E4=B8=BA=E9=BB=98=E8=AE=A4=E6=A8=A1?= =?UTF-8?q?=E5=9E=8B=E5=A2=9E=E5=8A=A0=E6=A0=87=E8=AF=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ConversationModelSelect 记录 defaultModelId,在下拉选项中对默认模型显示「默认」小标签 首页与项目右侧对话共用同一组件,默认模型标识两处同时生效 补充默认模型标识的回归测试 --- .../ConversationModelSelect.tsx | 9 ++++++++- apps/ai-game-creator-shell/src/styles.css | 18 ++++++++++++++++++ .../tests/conversationModelSelect.test.tsx | 13 +++++++++++++ 3 files changed, 39 insertions(+), 1 deletion(-) diff --git a/apps/ai-game-creator-shell/src/features/project-workspace/ConversationModelSelect.tsx b/apps/ai-game-creator-shell/src/features/project-workspace/ConversationModelSelect.tsx index 6869ede23..88ba4836b 100644 --- a/apps/ai-game-creator-shell/src/features/project-workspace/ConversationModelSelect.tsx +++ b/apps/ai-game-creator-shell/src/features/project-workspace/ConversationModelSelect.tsx @@ -21,6 +21,7 @@ export function ConversationModelSelect({ }) { const [models, setModels] = useState([]); const [selected, setSelected] = useState(''); + const [defaultModelId, setDefaultModelId] = useState(''); const [busy, setBusy] = useState(!lazy); const [error, setError] = useState(''); const [open, setOpen] = useState(false); @@ -38,6 +39,7 @@ export function ConversationModelSelect({ invoke('read_game_creator_app_config'), ]); setModels(catalog.models); + setDefaultModelId(catalog.defaultModelId); const id = config.config.selectedModelId || catalog.defaultModelId; setSelected(id); const available = catalog.models.some((model) => model.id === id); @@ -164,7 +166,12 @@ export function ConversationModelSelect({ void select(model.id); }} > - {model.displayName} + + {model.displayName} + {model.id === defaultModelId ? ( + 默认 + ) : null} + {model.id === selected ? (