feat: 设置页新增模型卡片,支持为7种能力配置默认模型
This commit is contained in:
@@ -26,6 +26,7 @@ export function getSettings(raw: Database): SettingsData {
|
|||||||
const parsed = JSON.parse(row.data) as Partial<SettingsData>;
|
const parsed = JSON.parse(row.data) as Partial<SettingsData>;
|
||||||
return {
|
return {
|
||||||
compact: typeof parsed.compact === "boolean" ? parsed.compact : false,
|
compact: typeof parsed.compact === "boolean" ? parsed.compact : false,
|
||||||
|
defaultModels: parsed.defaultModels,
|
||||||
theme: parsed.theme === "dark" || parsed.theme === "light" || parsed.theme === "system" ? parsed.theme : "system",
|
theme: parsed.theme === "dark" || parsed.theme === "light" || parsed.theme === "system" ? parsed.theme : "system",
|
||||||
};
|
};
|
||||||
} catch {
|
} catch {
|
||||||
|
|||||||
@@ -237,8 +237,21 @@ export type ProviderType = "anthropic" | "openai" | "openai-compatible";
|
|||||||
|
|
||||||
export type RuntimeMode = "development" | "production" | "test";
|
export type RuntimeMode = "development" | "production" | "test";
|
||||||
|
|
||||||
|
/** 模型能力到默认模型 ID 的映射,用于后台自动流程 */
|
||||||
|
export interface DefaultModelSettings {
|
||||||
|
/** 文本能力,覆盖 text + reasoning */
|
||||||
|
text?: string | null;
|
||||||
|
imageRecognition?: string | null;
|
||||||
|
audioRecognition?: string | null;
|
||||||
|
videoRecognition?: string | null;
|
||||||
|
imageGeneration?: string | null;
|
||||||
|
audioGeneration?: string | null;
|
||||||
|
videoGeneration?: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
export interface SettingsData {
|
export interface SettingsData {
|
||||||
compact?: boolean;
|
compact?: boolean;
|
||||||
|
defaultModels?: DefaultModelSettings;
|
||||||
theme: ThemePreference;
|
theme: ThemePreference;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
119
src/web/features/settings/components/ModelSettingsCard.tsx
Normal file
119
src/web/features/settings/components/ModelSettingsCard.tsx
Normal file
@@ -0,0 +1,119 @@
|
|||||||
|
import { useQueries } from "@tanstack/react-query";
|
||||||
|
import { App as AntApp, Card, Form, Select } from "antd";
|
||||||
|
import { useCallback, useMemo } from "react";
|
||||||
|
|
||||||
|
import type { DefaultModelSettings, ModelCapability } from "../../../../shared/api";
|
||||||
|
import { fetchModelList } from "../../../shared/hooks/use-models";
|
||||||
|
import { useSettings } from "../../../shared/hooks/use-settings";
|
||||||
|
|
||||||
|
interface CapabilityItem {
|
||||||
|
field: keyof DefaultModelSettings;
|
||||||
|
filterCapabilities: ModelCapability[];
|
||||||
|
help?: string;
|
||||||
|
label: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const CAPABILITY_ITEMS: CapabilityItem[] = [
|
||||||
|
{ field: "text", filterCapabilities: ["text", "reasoning"], help: "同时用于推理场景", label: "文本" },
|
||||||
|
{ field: "imageRecognition", filterCapabilities: ["image-recognition"], label: "图片识别" },
|
||||||
|
{ field: "audioRecognition", filterCapabilities: ["audio-recognition"], label: "音频识别" },
|
||||||
|
{ field: "videoRecognition", filterCapabilities: ["video-recognition"], label: "视频识别" },
|
||||||
|
{ field: "imageGeneration", filterCapabilities: ["image-generation"], label: "图片生成" },
|
||||||
|
{ field: "audioGeneration", filterCapabilities: ["audio-generation"], label: "音频生成" },
|
||||||
|
{ field: "videoGeneration", filterCapabilities: ["video-generation"], label: "视频生成" },
|
||||||
|
];
|
||||||
|
|
||||||
|
const SAVE_MESSAGE_KEY = "model-save";
|
||||||
|
|
||||||
|
interface CapabilityModelSelectProps {
|
||||||
|
disabled: boolean;
|
||||||
|
item: CapabilityItem;
|
||||||
|
onChange: (field: keyof DefaultModelSettings, modelId: string | undefined) => void;
|
||||||
|
value: string | undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
function CapabilityModelSelect({ disabled, item, onChange, value }: CapabilityModelSelectProps) {
|
||||||
|
const queries = useQueries({
|
||||||
|
queries: item.filterCapabilities.map((cap) => ({
|
||||||
|
queryFn: () => fetchModelList({ capabilities: cap, pageSize: 200 }),
|
||||||
|
queryKey: ["models", "list-cap", cap],
|
||||||
|
})),
|
||||||
|
});
|
||||||
|
|
||||||
|
const isLoading = queries.some((q) => q.isLoading);
|
||||||
|
|
||||||
|
const options = useMemo(() => {
|
||||||
|
const seen = new Set<string>();
|
||||||
|
const result: Array<{ label: string; value: string }> = [];
|
||||||
|
for (const q of queries) {
|
||||||
|
for (const model of q.data?.items ?? []) {
|
||||||
|
if (!seen.has(model.id)) {
|
||||||
|
seen.add(model.id);
|
||||||
|
result.push({ label: model.name, value: model.id });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}, [queries]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Form.Item colon={false} help={item.help} label={item.label}>
|
||||||
|
<Select
|
||||||
|
allowClear
|
||||||
|
disabled={disabled}
|
||||||
|
loading={isLoading}
|
||||||
|
onChange={(val) => onChange(item.field, val)}
|
||||||
|
options={options}
|
||||||
|
placeholder={isLoading ? "加载中..." : "请选择"}
|
||||||
|
value={value}
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ModelSettingsCard() {
|
||||||
|
const { message } = AntApp.useApp();
|
||||||
|
const { data: settings, isUpdating, updateSettings } = useSettings();
|
||||||
|
|
||||||
|
const handleChange = useCallback(
|
||||||
|
(field: keyof DefaultModelSettings, modelId: string | undefined) => {
|
||||||
|
const next: DefaultModelSettings = { ...settings?.defaultModels, [field]: modelId ?? null };
|
||||||
|
message.open({ content: "保存中...", duration: 0, key: SAVE_MESSAGE_KEY, type: "loading" });
|
||||||
|
updateSettings(
|
||||||
|
{ defaultModels: next },
|
||||||
|
{
|
||||||
|
onError: () => {
|
||||||
|
void message.open({ content: "保存失败", duration: 2, key: SAVE_MESSAGE_KEY, type: "error" });
|
||||||
|
},
|
||||||
|
onSuccess: () => {
|
||||||
|
void message.open({ content: "已保存", duration: 1.5, key: SAVE_MESSAGE_KEY, type: "success" });
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
[settings?.defaultModels, updateSettings, message],
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card title="模型" type="inner">
|
||||||
|
<Form
|
||||||
|
className="settings-form"
|
||||||
|
colon={false}
|
||||||
|
disabled={isUpdating}
|
||||||
|
labelAlign="left"
|
||||||
|
labelCol={{ flex: "120px" }}
|
||||||
|
layout="horizontal"
|
||||||
|
>
|
||||||
|
{CAPABILITY_ITEMS.map((item) => (
|
||||||
|
<CapabilityModelSelect
|
||||||
|
key={item.field}
|
||||||
|
disabled={isUpdating}
|
||||||
|
item={item}
|
||||||
|
onChange={handleChange}
|
||||||
|
value={settings?.defaultModels?.[item.field] ?? undefined}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</Form>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,8 +1,9 @@
|
|||||||
import { App as AntApp, Card, Form, Radio, Switch } from "antd";
|
import { App as AntApp, Card, Form, Radio, Space, Switch } from "antd";
|
||||||
|
|
||||||
import type { ThemePreference } from "../../../shared/api";
|
import type { ThemePreference } from "../../../shared/api";
|
||||||
import { useSettings } from "../../shared/hooks/use-settings";
|
import { useSettings } from "../../shared/hooks/use-settings";
|
||||||
import { parseThemePreference, useThemePreference } from "../../shared/hooks/use-theme-preference";
|
import { parseThemePreference, useThemePreference } from "../../shared/hooks/use-theme-preference";
|
||||||
|
import { ModelSettingsCard } from "./components/ModelSettingsCard";
|
||||||
|
|
||||||
const THEME_OPTIONS = [
|
const THEME_OPTIONS = [
|
||||||
{ label: "系统", value: "system" },
|
{ label: "系统", value: "system" },
|
||||||
@@ -50,6 +51,8 @@ export function SettingsPage() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<>
|
||||||
|
<Space orientation="vertical" size="middle" style={{ width: "100%" }}>
|
||||||
<Card title="主题" type="inner">
|
<Card title="主题" type="inner">
|
||||||
<Form
|
<Form
|
||||||
className="settings-form"
|
className="settings-form"
|
||||||
@@ -73,5 +76,8 @@ export function SettingsPage() {
|
|||||||
</Form.Item>
|
</Form.Item>
|
||||||
</Form>
|
</Form>
|
||||||
</Card>
|
</Card>
|
||||||
|
<ModelSettingsCard />
|
||||||
|
</Space>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -113,4 +113,45 @@ describe("设置数据访问层", () => {
|
|||||||
expect(result).toEqual({ compact: false, theme: "dark" });
|
expect(result).toEqual({ compact: false, theme: "dark" });
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("updateSettings 写入 defaultModels 并读取", () => {
|
||||||
|
withSettingsDb((db) => {
|
||||||
|
const updated = updateSettings(
|
||||||
|
db,
|
||||||
|
{ defaultModels: { text: "model-1", imageRecognition: null } },
|
||||||
|
createNoopLogger(),
|
||||||
|
);
|
||||||
|
expect(updated.defaultModels).toEqual({ text: "model-1", imageRecognition: null });
|
||||||
|
expect(updated.compact).toBe(false);
|
||||||
|
expect(updated.theme).toBe("system");
|
||||||
|
|
||||||
|
const read = getSettings(db);
|
||||||
|
expect(read.defaultModels).toEqual({ text: "model-1", imageRecognition: null });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test("defaultModels 与 theme/compact 合并持久化", () => {
|
||||||
|
withSettingsDb((db) => {
|
||||||
|
updateSettings(db, { theme: "dark", compact: true }, createNoopLogger());
|
||||||
|
const result = updateSettings(db, { defaultModels: { imageGeneration: "model-2" } }, createNoopLogger());
|
||||||
|
expect(result).toEqual({
|
||||||
|
compact: true,
|
||||||
|
defaultModels: { imageGeneration: "model-2" },
|
||||||
|
theme: "dark",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test("defaultModels 全量替换——前端负责深度合并", () => {
|
||||||
|
withSettingsDb((db) => {
|
||||||
|
updateSettings(db, { defaultModels: { text: "model-1", imageRecognition: "model-2" } }, createNoopLogger());
|
||||||
|
// 前端在 onChange 中负责合并 old + newField,提交完整对象
|
||||||
|
const result = updateSettings(
|
||||||
|
db,
|
||||||
|
{ defaultModels: { text: "model-3", imageRecognition: "model-2" } },
|
||||||
|
createNoopLogger(),
|
||||||
|
);
|
||||||
|
expect(result.defaultModels).toEqual({ text: "model-3", imageRecognition: "model-2" });
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import type Database from "bun:sqlite";
|
|||||||
|
|
||||||
import { describe, expect, test } from "bun:test";
|
import { describe, expect, test } from "bun:test";
|
||||||
|
|
||||||
import type { RuntimeMode } from "../../../src/shared/api";
|
import type { RuntimeMode, SettingsData } from "../../../src/shared/api";
|
||||||
|
|
||||||
import { createNoopLogger } from "../../../src/server/logger";
|
import { createNoopLogger } from "../../../src/server/logger";
|
||||||
import { createMigratedMemoryTestDatabase } from "../../helpers";
|
import { createMigratedMemoryTestDatabase } from "../../helpers";
|
||||||
@@ -191,4 +191,48 @@ describe("设置 API 路由", () => {
|
|||||||
expect(body).toEqual({ compact: true, theme: "dark" });
|
expect(body).toEqual({ compact: true, theme: "dark" });
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("PUT /api/settings 写入 defaultModels 后 GET 返回包含该字段", async () => {
|
||||||
|
await withRouteDb(async (db) => {
|
||||||
|
const putReq = new Request("http://localhost/api/settings", {
|
||||||
|
body: JSON.stringify({ defaultModels: { text: "model-1", imageRecognition: null } }),
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
method: "PUT",
|
||||||
|
});
|
||||||
|
const putRes = await updateSettingsViaHandler(putReq, db);
|
||||||
|
expect(putRes.status).toBe(200);
|
||||||
|
const putBody = (await putRes.json()) as SettingsData;
|
||||||
|
expect(putBody.defaultModels).toEqual({ text: "model-1", imageRecognition: null });
|
||||||
|
expect(putBody.compact).toBe(false);
|
||||||
|
expect(putBody.theme).toBe("system");
|
||||||
|
|
||||||
|
const getReq = new Request("http://localhost/api/settings");
|
||||||
|
const getRes = await getSettingsViaHandler(getReq, db);
|
||||||
|
expect(getRes.status).toBe(200);
|
||||||
|
const getBody = (await getRes.json()) as SettingsData;
|
||||||
|
expect(getBody.defaultModels).toEqual({ text: "model-1", imageRecognition: null });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test("PUT /api/settings defaultModels 与 theme 合并持久化不丢失", async () => {
|
||||||
|
await withRouteDb(async (db) => {
|
||||||
|
const req1 = new Request("http://localhost/api/settings", {
|
||||||
|
body: JSON.stringify({ theme: "dark" }),
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
method: "PUT",
|
||||||
|
});
|
||||||
|
await updateSettingsViaHandler(req1, db);
|
||||||
|
|
||||||
|
const req2 = new Request("http://localhost/api/settings", {
|
||||||
|
body: JSON.stringify({ defaultModels: { videoGeneration: "model-v" } }),
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
method: "PUT",
|
||||||
|
});
|
||||||
|
const res2 = await updateSettingsViaHandler(req2, db);
|
||||||
|
expect(res2.status).toBe(200);
|
||||||
|
const body = (await res2.json()) as SettingsData;
|
||||||
|
expect(body.theme).toBe("dark");
|
||||||
|
expect(body.defaultModels).toEqual({ videoGeneration: "model-v" });
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -9,10 +9,15 @@ function mockSettingsResponse(theme = "system", compact = false): Response {
|
|||||||
return jsonResponse({ compact, theme });
|
return jsonResponse({ compact, theme });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function mockEmptyModelsResponse(): Response {
|
||||||
|
return jsonResponse({ items: [], page: 1, pageSize: 200, total: 0 });
|
||||||
|
}
|
||||||
|
|
||||||
describe("SettingsPage", () => {
|
describe("SettingsPage", () => {
|
||||||
test("渲染主题卡片", () => {
|
test("渲染主题卡片", () => {
|
||||||
installFetchMock((call) => {
|
installFetchMock((call) => {
|
||||||
if (call.url.includes("/api/settings")) return mockSettingsResponse();
|
if (call.url.includes("/api/settings")) return mockSettingsResponse();
|
||||||
|
if (call.url.includes("/api/models")) return mockEmptyModelsResponse();
|
||||||
return jsonResponse({});
|
return jsonResponse({});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -24,6 +29,7 @@ describe("SettingsPage", () => {
|
|||||||
test("渲染主题模式 Radio.Group 选项", () => {
|
test("渲染主题模式 Radio.Group 选项", () => {
|
||||||
installFetchMock((call) => {
|
installFetchMock((call) => {
|
||||||
if (call.url.includes("/api/settings")) return mockSettingsResponse();
|
if (call.url.includes("/api/settings")) return mockSettingsResponse();
|
||||||
|
if (call.url.includes("/api/models")) return mockEmptyModelsResponse();
|
||||||
return jsonResponse({});
|
return jsonResponse({});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -37,6 +43,7 @@ describe("SettingsPage", () => {
|
|||||||
test("渲染紧凑模式标签和开关", () => {
|
test("渲染紧凑模式标签和开关", () => {
|
||||||
installFetchMock((call) => {
|
installFetchMock((call) => {
|
||||||
if (call.url.includes("/api/settings")) return mockSettingsResponse();
|
if (call.url.includes("/api/settings")) return mockSettingsResponse();
|
||||||
|
if (call.url.includes("/api/models")) return mockEmptyModelsResponse();
|
||||||
return jsonResponse({});
|
return jsonResponse({});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -48,6 +55,7 @@ describe("SettingsPage", () => {
|
|||||||
test("渲染水平表单结构", () => {
|
test("渲染水平表单结构", () => {
|
||||||
installFetchMock((call) => {
|
installFetchMock((call) => {
|
||||||
if (call.url.includes("/api/settings")) return mockSettingsResponse();
|
if (call.url.includes("/api/settings")) return mockSettingsResponse();
|
||||||
|
if (call.url.includes("/api/models")) return mockEmptyModelsResponse();
|
||||||
return jsonResponse({});
|
return jsonResponse({});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -60,6 +68,7 @@ describe("SettingsPage", () => {
|
|||||||
test("不再使用 Segmented", () => {
|
test("不再使用 Segmented", () => {
|
||||||
installFetchMock((call) => {
|
installFetchMock((call) => {
|
||||||
if (call.url.includes("/api/settings")) return mockSettingsResponse();
|
if (call.url.includes("/api/settings")) return mockSettingsResponse();
|
||||||
|
if (call.url.includes("/api/models")) return mockEmptyModelsResponse();
|
||||||
return jsonResponse({});
|
return jsonResponse({});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -71,6 +80,7 @@ describe("SettingsPage", () => {
|
|||||||
test("不显示保存状态文本(已迁移到 toast)", () => {
|
test("不显示保存状态文本(已迁移到 toast)", () => {
|
||||||
installFetchMock((call) => {
|
installFetchMock((call) => {
|
||||||
if (call.url.includes("/api/settings")) return mockSettingsResponse();
|
if (call.url.includes("/api/settings")) return mockSettingsResponse();
|
||||||
|
if (call.url.includes("/api/models")) return mockEmptyModelsResponse();
|
||||||
return jsonResponse({});
|
return jsonResponse({});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -92,4 +102,16 @@ describe("SettingsPage", () => {
|
|||||||
expect(radioGroup).not.toBeNull();
|
expect(radioGroup).not.toBeNull();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("渲染模型卡片标题", () => {
|
||||||
|
installFetchMock((call) => {
|
||||||
|
if (call.url.includes("/api/settings")) return mockSettingsResponse();
|
||||||
|
if (call.url.includes("/api/models")) return mockEmptyModelsResponse();
|
||||||
|
return jsonResponse({});
|
||||||
|
});
|
||||||
|
|
||||||
|
renderWithProviders(createElement(SettingsPage));
|
||||||
|
|
||||||
|
expect(screen.getByText("模型")).not.toBeNull();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -0,0 +1,67 @@
|
|||||||
|
import { describe, expect, test } from "bun:test";
|
||||||
|
import { screen, waitFor } from "@testing-library/react";
|
||||||
|
import { createElement } from "react";
|
||||||
|
|
||||||
|
import { ModelSettingsCard } from "../../../../../src/web/features/settings/components/ModelSettingsCard";
|
||||||
|
import { installFetchMock, jsonResponse, renderWithProviders } from "../../../test-utils";
|
||||||
|
|
||||||
|
function mockSettingsResponse(defaultModels?: Record<string, string | null>): Response {
|
||||||
|
return jsonResponse({ compact: false, defaultModels, theme: "system" });
|
||||||
|
}
|
||||||
|
|
||||||
|
function mockModelsResponse(items: Array<{ id: string; name: string }>): Response {
|
||||||
|
return jsonResponse({
|
||||||
|
items: items.map((m) => ({ ...m, capabilities: [], createdAt: "", externalId: "", providerId: "", updatedAt: "" })),
|
||||||
|
page: 1,
|
||||||
|
pageSize: 200,
|
||||||
|
total: items.length,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function mockEmptyModelsResponse(): Response {
|
||||||
|
return mockModelsResponse([]);
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("ModelSettingsCard", () => {
|
||||||
|
test("渲染 7 个模型能力配置项", async () => {
|
||||||
|
installFetchMock((call) => {
|
||||||
|
if (call.url.includes("/api/settings")) return mockSettingsResponse();
|
||||||
|
if (call.url.includes("/api/models")) return mockEmptyModelsResponse();
|
||||||
|
return jsonResponse({});
|
||||||
|
});
|
||||||
|
|
||||||
|
renderWithProviders(createElement(ModelSettingsCard));
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByText("文本")).not.toBeNull();
|
||||||
|
expect(screen.getByText("图片识别")).not.toBeNull();
|
||||||
|
expect(screen.getByText("音频识别")).not.toBeNull();
|
||||||
|
expect(screen.getByText("视频识别")).not.toBeNull();
|
||||||
|
expect(screen.getByText("图片生成")).not.toBeNull();
|
||||||
|
expect(screen.getByText("音频生成")).not.toBeNull();
|
||||||
|
expect(screen.getByText("视频生成")).not.toBeNull();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test("回显已保存的默认模型值", async () => {
|
||||||
|
installFetchMock((call) => {
|
||||||
|
if (call.url.includes("/api/settings")) {
|
||||||
|
return mockSettingsResponse({ imageRecognition: "model-b", text: "model-a" });
|
||||||
|
}
|
||||||
|
if (call.url.includes("/api/models")) {
|
||||||
|
return mockModelsResponse([
|
||||||
|
{ id: "model-a", name: "GPT-4" },
|
||||||
|
{ id: "model-b", name: "Claude Vision" },
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
return jsonResponse({});
|
||||||
|
});
|
||||||
|
|
||||||
|
renderWithProviders(createElement(ModelSettingsCard));
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByText("GPT-4")).not.toBeNull();
|
||||||
|
expect(screen.getByText("Claude Vision")).not.toBeNull();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user