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>;
|
||||
return {
|
||||
compact: typeof parsed.compact === "boolean" ? parsed.compact : false,
|
||||
defaultModels: parsed.defaultModels,
|
||||
theme: parsed.theme === "dark" || parsed.theme === "light" || parsed.theme === "system" ? parsed.theme : "system",
|
||||
};
|
||||
} catch {
|
||||
|
||||
@@ -237,8 +237,21 @@ export type ProviderType = "anthropic" | "openai" | "openai-compatible";
|
||||
|
||||
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 {
|
||||
compact?: boolean;
|
||||
defaultModels?: DefaultModelSettings;
|
||||
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 { useSettings } from "../../shared/hooks/use-settings";
|
||||
import { parseThemePreference, useThemePreference } from "../../shared/hooks/use-theme-preference";
|
||||
import { ModelSettingsCard } from "./components/ModelSettingsCard";
|
||||
|
||||
const THEME_OPTIONS = [
|
||||
{ label: "系统", value: "system" },
|
||||
@@ -50,28 +51,33 @@ export function SettingsPage() {
|
||||
};
|
||||
|
||||
return (
|
||||
<Card title="主题" type="inner">
|
||||
<Form
|
||||
className="settings-form"
|
||||
colon={false}
|
||||
disabled={isUpdating}
|
||||
labelAlign="left"
|
||||
labelCol={{ flex: "120px" }}
|
||||
layout="horizontal"
|
||||
>
|
||||
<Form.Item colon={false} help="选择跟随系统将自动适配操作系统的深浅色偏好" label="主题模式">
|
||||
<Radio.Group
|
||||
buttonStyle="solid"
|
||||
onChange={(e) => handleThemeChange(parseThemePreference(e.target.value))}
|
||||
optionType="button"
|
||||
options={THEME_OPTIONS.map((o) => ({ label: o.label, value: o.value }))}
|
||||
value={preference}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item colon={false} help="开启后控件间距和高度变小,显示更多内容" label="紧凑模式">
|
||||
<Switch checked={compact} onChange={handleCompactChange} />
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Card>
|
||||
<>
|
||||
<Space orientation="vertical" size="middle" style={{ width: "100%" }}>
|
||||
<Card title="主题" type="inner">
|
||||
<Form
|
||||
className="settings-form"
|
||||
colon={false}
|
||||
disabled={isUpdating}
|
||||
labelAlign="left"
|
||||
labelCol={{ flex: "120px" }}
|
||||
layout="horizontal"
|
||||
>
|
||||
<Form.Item colon={false} help="选择跟随系统将自动适配操作系统的深浅色偏好" label="主题模式">
|
||||
<Radio.Group
|
||||
buttonStyle="solid"
|
||||
onChange={(e) => handleThemeChange(parseThemePreference(e.target.value))}
|
||||
optionType="button"
|
||||
options={THEME_OPTIONS.map((o) => ({ label: o.label, value: o.value }))}
|
||||
value={preference}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item colon={false} help="开启后控件间距和高度变小,显示更多内容" label="紧凑模式">
|
||||
<Switch checked={compact} onChange={handleCompactChange} />
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Card>
|
||||
<ModelSettingsCard />
|
||||
</Space>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user