模型选择始终回退默认模型,不再出现请选择模型
ConversationModelSelect 在已保存选择失效时回退到后台默认模型,只要默认模型可用就不会出现「选择模型」空态 首页模型选择器改为挂载即加载目录,触发按钮直接落在默认模型上(移除懒加载) 首页不再需要 lazy 路径,清理组件中已无调用方的懒加载逻辑 同步更新模型选择相关回归测试与首页/运行时配置用例
This commit is contained in:
+15
-16
@@ -11,21 +11,18 @@ import {
|
||||
export function ConversationModelSelect({
|
||||
className,
|
||||
disabled,
|
||||
lazy = false,
|
||||
onReady,
|
||||
}: {
|
||||
className?: string;
|
||||
disabled: boolean;
|
||||
lazy?: boolean;
|
||||
onReady?: (ready: boolean) => void;
|
||||
}) {
|
||||
const [models, setModels] = useState<ClientLlmModel[]>([]);
|
||||
const [selected, setSelected] = useState('');
|
||||
const [defaultModelId, setDefaultModelId] = useState('');
|
||||
const [busy, setBusy] = useState(!lazy);
|
||||
const [busy, setBusy] = useState(true);
|
||||
const [error, setError] = useState('');
|
||||
const [open, setOpen] = useState(false);
|
||||
const initializedRef = useRef(false);
|
||||
const containerRef = useRef<HTMLDivElement | null>(null);
|
||||
const refresh = useCallback(async () => {
|
||||
setBusy(true);
|
||||
@@ -40,16 +37,25 @@ export function ConversationModelSelect({
|
||||
]);
|
||||
setModels(catalog.models);
|
||||
setDefaultModelId(catalog.defaultModelId);
|
||||
const id = config.config.selectedModelId || catalog.defaultModelId;
|
||||
const savedSelection = config.config.selectedModelId;
|
||||
// 优先沿用用户已选且仍可用的模型;已选模型被停用/移除时回退到默认模型,
|
||||
// 避免下拉出现「请选择模型」的空态。默认模型由后台强制配置,缺失时才走错误提示。
|
||||
const savedAvailable = savedSelection
|
||||
? catalog.models.some((model) => model.id === savedSelection)
|
||||
: false;
|
||||
const id =
|
||||
savedAvailable && savedSelection
|
||||
? savedSelection
|
||||
: catalog.defaultModelId;
|
||||
setSelected(id);
|
||||
const available = catalog.models.some((model) => model.id === id);
|
||||
if (available && !config.config.selectedModelId) {
|
||||
if (available && savedSelection !== id) {
|
||||
const saved = await invoke<GameCreatorAppConfigView>(
|
||||
'select_game_creator_model',
|
||||
{ modelId: id },
|
||||
);
|
||||
if (saved.config.selectedModelId !== id)
|
||||
throw new Error('Default selection was not saved');
|
||||
throw new Error('Model selection was not saved');
|
||||
}
|
||||
onReady?.(available);
|
||||
if (!available) setError('请选择可用模型');
|
||||
@@ -62,9 +68,8 @@ export function ConversationModelSelect({
|
||||
}, [onReady]);
|
||||
|
||||
useEffect(() => {
|
||||
if (lazy) return;
|
||||
void refresh();
|
||||
}, [refresh, lazy]);
|
||||
}, [refresh]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!open) return;
|
||||
@@ -126,13 +131,7 @@ export function ConversationModelSelect({
|
||||
aria-haspopup="listbox"
|
||||
aria-expanded={open}
|
||||
disabled={disabled}
|
||||
onClick={() => {
|
||||
if (lazy && !initializedRef.current) {
|
||||
initializedRef.current = true;
|
||||
void refresh();
|
||||
}
|
||||
setOpen((current) => !current);
|
||||
}}
|
||||
onClick={() => setOpen((current) => !current)}
|
||||
>
|
||||
<span
|
||||
className="conversation-model-trigger-status"
|
||||
|
||||
@@ -248,7 +248,6 @@ export default function HomeView({
|
||||
<ConversationModelSelect
|
||||
className="home-input-model-select"
|
||||
disabled={homeCreationBusy}
|
||||
lazy
|
||||
/>
|
||||
<button
|
||||
className="grid size-7 cursor-pointer place-items-center rounded-full border-0 bg-(image:--platform-button-primary-fill) p-0 text-(--platform-button-primary-text) shadow-(--platform-profile-action-shadow) transition-transform hover:scale-105 disabled:cursor-not-allowed disabled:opacity-55"
|
||||
|
||||
@@ -62,7 +62,7 @@ function ApprovedGddStartHarness() {
|
||||
}
|
||||
|
||||
export function registerClientHomeTests() {
|
||||
it('adds a model selector to the home composer and lazily loads the catalog on open', async () => {
|
||||
it('adds a model selector to the home composer and shows the default model', async () => {
|
||||
const invoke = vi.fn(async (command: string, args?: unknown) => {
|
||||
if (command === 'read_game_creator_app_config') {
|
||||
return { config: { selectedModelId: 'quality' } };
|
||||
@@ -77,14 +77,14 @@ export function registerClientHomeTests() {
|
||||
window.__TAURI__ = { core: { invoke } };
|
||||
renderLauncherAt('/?launcher');
|
||||
|
||||
// Lazy: the home composer must not read config or load models on mount.
|
||||
expect(invoke).not.toHaveBeenCalledWith(
|
||||
'read_game_creator_app_config',
|
||||
expect.anything(),
|
||||
// 挂载即加载目录,触发按钮直接落在默认模型上,不出现「选择模型」空态。
|
||||
await waitFor(() =>
|
||||
expect(invoke).toHaveBeenCalledWith('read_game_creator_app_config'),
|
||||
);
|
||||
const modelTrigger = await screen.findByRole('button', {
|
||||
name: '对话模型',
|
||||
});
|
||||
await waitFor(() => expect(modelTrigger.textContent).toContain('高质量'));
|
||||
const createButton = screen.getByRole('button', { name: '开启创作' });
|
||||
expect(createButton).toHaveProperty('disabled', false);
|
||||
|
||||
@@ -1260,7 +1260,17 @@ export function registerHomeProjectCreationTests() {
|
||||
});
|
||||
|
||||
it('keeps only open and create project actions without exposing a Linux fallback', () => {
|
||||
const invoke = vi.fn();
|
||||
const invoke = vi.fn(async (command: string, args?: unknown) => {
|
||||
if (command === 'read_game_creator_app_config') {
|
||||
return { config: { selectedModelId: 'quality' } };
|
||||
}
|
||||
if (command === 'select_game_creator_model') {
|
||||
return {
|
||||
config: { selectedModelId: (args as { modelId: string }).modelId },
|
||||
};
|
||||
}
|
||||
throw new Error(`unexpected invoke ${command}`);
|
||||
});
|
||||
window.__TAURI__ = { core: { invoke } };
|
||||
|
||||
renderLauncherProjectsAt('/?launcher');
|
||||
@@ -1282,7 +1292,12 @@ export function registerHomeProjectCreationTests() {
|
||||
screen.queryByRole('button', { name: '在文件管理器中显示' }),
|
||||
).toBeNull();
|
||||
expect(screen.queryByRole('button', { name: /Godot 项目/ })).toBeNull();
|
||||
expect(invoke).not.toHaveBeenCalled();
|
||||
// 首页模型选择器会在挂载时读取模型目录(read_game_creator_app_config),
|
||||
// 这里只校验没有打开工作区窗口或其它项目操作被触发。
|
||||
expect(invoke).not.toHaveBeenCalledWith(
|
||||
'open_game_creator_workspace_window',
|
||||
expect.anything(),
|
||||
);
|
||||
});
|
||||
|
||||
it('opens the directory selected by the native picker', async () => {
|
||||
|
||||
@@ -692,9 +692,16 @@ export function registerRuntimeSettingsTests() {
|
||||
}) => void)
|
||||
| undefined;
|
||||
let readCount = 0;
|
||||
let modelReadResolved = false;
|
||||
const invoke = vi.fn((command: string) => {
|
||||
if (command === 'read_game_creator_app_config') {
|
||||
readCount += 1;
|
||||
// 首页模型选择器会在挂载时读取一次配置;让首次读取立即完成,
|
||||
// 以免一直处于 pending 干扰运行时配置对话框的读取计数。
|
||||
if (!modelReadResolved) {
|
||||
modelReadResolved = true;
|
||||
return Promise.resolve({ config: { selectedModelId: 'quality' } });
|
||||
}
|
||||
return new Promise((resolve) => {
|
||||
resolveRead = resolve as typeof resolveRead;
|
||||
});
|
||||
@@ -723,10 +730,13 @@ export function registerRuntimeSettingsTests() {
|
||||
true,
|
||||
);
|
||||
|
||||
// 首页模型选择器挂载时会读取一次配置(上面已让首次读取立即完成),
|
||||
// 这里只校验:对话框处于「正在读取」时点击读取/保存不会新增读取请求。
|
||||
const readsWhileReading = readCount;
|
||||
fireEvent.click(screen.getByRole('button', { name: '读取' }));
|
||||
fireEvent.click(screen.getByRole('button', { name: '保存' }));
|
||||
|
||||
expect(readCount).toBe(1);
|
||||
expect(readCount).toBe(readsWhileReading);
|
||||
await act(async () => {
|
||||
resolveRead?.({
|
||||
path: '/home/test/AppData/game-creator.config.json',
|
||||
|
||||
@@ -54,15 +54,25 @@ test('only displays aliases and persists selection through the native command',
|
||||
).toContain('快速');
|
||||
});
|
||||
|
||||
test('does not mark a removed selection ready or expose the old identifier', async () => {
|
||||
invoke.mockResolvedValue({
|
||||
config: { selectedModelId: 'private-old-model' },
|
||||
});
|
||||
test('falls back to the default model when the saved selection was removed', async () => {
|
||||
invoke.mockImplementation(async (command, input) => ({
|
||||
config: {
|
||||
selectedModelId:
|
||||
command === 'select_game_creator_model'
|
||||
? (input as { modelId: string }).modelId
|
||||
: 'private-old-model',
|
||||
},
|
||||
}));
|
||||
const onReady = vi.fn();
|
||||
render(<ConversationModelSelect disabled={false} onReady={onReady} />);
|
||||
await screen.findByText('请选择可用模型');
|
||||
expect(onReady).toHaveBeenLastCalledWith(false);
|
||||
await waitFor(() => expect(onReady).toHaveBeenLastCalledWith(true));
|
||||
expect(screen.queryByText('private-old-model')).toBeNull();
|
||||
expect(invoke).toHaveBeenCalledWith('select_game_creator_model', {
|
||||
modelId: 'quality',
|
||||
});
|
||||
expect(
|
||||
screen.getByRole('button', { name: '对话模型' }).textContent,
|
||||
).toContain('高质量');
|
||||
});
|
||||
|
||||
test('failed catalog can be refreshed without enabling submission', async () => {
|
||||
|
||||
Reference in New Issue
Block a user