diff --git a/docs/content/docs/progress/pending-test.mdx b/docs/content/docs/progress/pending-test.mdx index 2a56905..a527394 100644 --- a/docs/content/docs/progress/pending-test.mdx +++ b/docs/content/docs/progress/pending-test.mdx @@ -5,7 +5,7 @@ description: 当前版本已实现但仍需人工验证的变更项 # 待测试 -- 配置弹窗新增“画布默认生图张数”,新建画布生图和配置节点会读取该值;模型下拉会按生图、视频、文本能力过滤可选模型,需要验证远程渠道和本地直连模型列表都能正确显示。 +- 配置弹窗改为“配置与用户偏好”并放大为可滚动弹窗;本地直连支持配置生图、视频、文本、音频四类可选模型列表和默认模型,新建画布生图和配置节点会读取“画布默认生图张数”,需要验证远程渠道和本地直连模型列表都能正确显示。 - 画布左上角菜单和右上角状态栏新增“文档”入口,会使用 `NEXT_PUBLIC_DOC_URL` 配置的地址并在新标签打开文档站;需要验证登录和未登录状态下顶部入口都可见。 - 文档站搜索改为中英文混合 tokenizer,中文正文、标题和短语会按中文词、单字、二元和三元片段建立索引;需要验证 `/api/search` 和搜索弹窗能命中文档中的中文关键词。 - 文档站改为 Next.js standalone server 输出,新增 `docs/Dockerfile`、`docs/docker-compose.yml` 和 `docs/docker-compose.local.yml` 独立运行入口;需要验证文档页、搜索接口和 LLM 文本接口在文档站容器中可访问。 diff --git a/web/src/components/layout/app-config-modal.tsx b/web/src/components/layout/app-config-modal.tsx index 882c35d..d6d8a76 100644 --- a/web/src/components/layout/app-config-modal.tsx +++ b/web/src/components/layout/app-config-modal.tsx @@ -1,11 +1,26 @@ "use client"; -import { App, Button, Form, Input, Modal, Segmented } from "antd"; +import { App, Button, Form, Input, Modal, Segmented, Select } from "antd"; import { useState } from "react"; import { ModelPicker } from "@/components/model-picker"; import { fetchImageModels } from "@/services/api/image"; -import { filterModelsByCapability, useConfigStore, useEffectiveConfig, type AiConfig } from "@/stores/use-config-store"; +import { filterModelsByCapability, useConfigStore, useEffectiveConfig, type AiConfig, type ModelCapability } from "@/stores/use-config-store"; + +type ModelGroup = { + capability: ModelCapability; + modelKey: "imageModel" | "videoModel" | "textModel" | "audioModel"; + modelsKey: "imageModels" | "videoModels" | "textModels" | "audioModels"; + defaultLabel: string; + optionsLabel: string; +}; + +const modelGroups: ModelGroup[] = [ + { capability: "image", modelKey: "imageModel", modelsKey: "imageModels", defaultLabel: "默认生图模型", optionsLabel: "生图模型可选项" }, + { capability: "video", modelKey: "videoModel", modelsKey: "videoModels", defaultLabel: "默认视频模型", optionsLabel: "视频模型可选项" }, + { capability: "text", modelKey: "textModel", modelsKey: "textModels", defaultLabel: "默认文本模型", optionsLabel: "文本模型可选项" }, + { capability: "audio", modelKey: "audioModel", modelsKey: "audioModels", defaultLabel: "默认音频模型", optionsLabel: "音频模型可选项" }, +]; export function AppConfigModal() { const { message } = App.useApp(); @@ -22,6 +37,7 @@ export function AppConfigModal() { const allowCustomChannel = modelChannel?.allowCustomChannel === true; const effectiveMode = allowCustomChannel ? config.channelMode : "remote"; const modelConfig = effectiveMode === "remote" ? effectiveConfig : config; + const modelOptions = config.models.map((model) => ({ label: model, value: model })); const finishConfig = () => { setConfigDialogOpen(false); @@ -44,10 +60,20 @@ export function AppConfigModal() { const imageModels = filterModelsByCapability(models, "image"); const videoModels = filterModelsByCapability(models, "video"); const textModels = filterModelsByCapability(models, "text"); + const audioModels = filterModelsByCapability(models, "audio"); + const nextImageModels = resolveNextCapabilityModels(config.imageModels, imageModels, models); + const nextVideoModels = resolveNextCapabilityModels(config.videoModels, videoModels, models); + const nextTextModels = resolveNextCapabilityModels(config.textModels, textModels, models); + const nextAudioModels = resolveNextCapabilityModels(config.audioModels, audioModels, models); updateConfig("models", models); - if (imageModels.length && !imageModels.includes(config.imageModel)) updateConfig("imageModel", imageModels[0]); - if (videoModels.length && !videoModels.includes(config.videoModel)) updateConfig("videoModel", videoModels[0]); - if (textModels.length && !textModels.includes(config.textModel)) updateConfig("textModel", textModels[0]); + updateConfig("imageModels", nextImageModels); + updateConfig("videoModels", nextVideoModels); + updateConfig("textModels", nextTextModels); + updateConfig("audioModels", nextAudioModels); + if (nextImageModels.length && !nextImageModels.includes(config.imageModel)) updateConfig("imageModel", nextImageModels[0]); + if (nextVideoModels.length && !nextVideoModels.includes(config.videoModel)) updateConfig("videoModel", nextVideoModels[0]); + if (nextTextModels.length && !nextTextModels.includes(config.textModel)) updateConfig("textModel", nextTextModels[0]); + if (nextAudioModels.length && !nextAudioModels.includes(config.audioModel)) updateConfig("audioModel", nextAudioModels[0]); message.success("模型列表已更新"); } catch (error) { message.error(error instanceof Error ? error.message : "读取模型失败"); @@ -56,18 +82,25 @@ export function AppConfigModal() { } }; + const updateCapabilityModels = (group: ModelGroup, models: string[]) => { + const next = uniqueModels(models); + updateConfig(group.modelsKey, next); + if (!next.includes(config[group.modelKey])) updateConfig(group.modelKey, next[0] || ""); + }; + return ( -
配置
-
模型和密钥
+
配置与用户偏好
+
模型、渠道和画布默认行为
} open={isConfigOpen} - width={760} + width={960} centered onCancel={() => setConfigDialogOpen(false)} + styles={{ body: { maxHeight: "72vh", overflowY: "auto", paddingRight: 18 } }} footer={