feat(canvas): 优化模型选择器和画布助手设置面板
- 模型选择器添加模糊匹配和图标显示支持OpenAI、Claude、Gemini - 模型选择器下拉滚动条改为更窄的轻量样式 - 画布助手生图设置弹层调整为紧凑图像设置面板避免裁切问题 - 画布节点设置面板打开时隐藏悬浮工具条避免遮挡
This commit is contained in:
@@ -1,6 +1,7 @@
|
|||||||
# 待测试
|
# 待测试
|
||||||
|
|
||||||
- 画布右侧助手和节点下方对话框的生图设置弹层改为紧凑图像设置面板,面板只包含质量、尺寸、宽高比和生成数量选择,模型选择保留在对话框底部工具栏;需要确认节点内质量可正常选择并参与生成、尺寸可编辑、宽高比预览显示正常、auto 不重复显示、张数每行 4 个且自定义输入框能显示当前值、实际生图参数正常;节点设置面板打开时会隐藏节点悬浮工具条,避免遮挡质量选择。
|
- 模型选择器会按模型名称模糊匹配并显示 OpenAI、Claude、Gemini 图标,且下拉滚动条改为更窄更轻的样式,需要确认生图页、画布助手、节点配置和配置弹窗中的模型下拉项及选中态显示正常,搜索仍可按模型名称过滤。
|
||||||
|
- 画布右侧助手和节点下方对话框的生图设置弹层改为紧凑图像设置面板,面板只包含质量、尺寸、宽高比和生成数量选择,模型选择保留在对话框底部工具栏;需要确认助手内设置面板不会被右侧面板裁切,节点内质量可正常选择并参与生成、尺寸可编辑、宽高比预览显示正常且为直角矩形、auto 不重复显示、张数每行 4 个且自定义输入框能显示当前值、实际生图参数正常;节点设置面板打开时会隐藏节点悬浮工具条,避免遮挡质量选择。
|
||||||
- 管理后台 `/admin/prompts` 新增提示词批量删除,需要确认多选、确认弹窗、删除后列表刷新和筛选条件下删除行为。
|
- 管理后台 `/admin/prompts` 新增提示词批量删除,需要确认多选、确认弹窗、删除后列表刷新和筛选条件下删除行为。
|
||||||
- 管理后台 `/admin/settings` 的私有配置新增提示词定时同步开关和 Cron 表达式;开启后后端会按配置同步内置 GitHub 远程提示词源,需要确认保存配置和到点同步行为。
|
- 管理后台 `/admin/settings` 的私有配置新增提示词定时同步开关和 Cron 表达式;开启后后端会按配置同步内置 GitHub 远程提示词源,需要确认保存配置和到点同步行为。
|
||||||
- 管理后台 `/admin/settings` 中的渠道模型列表获取和模型测试已改为走后端接口,前端不再直接读取或转发 API Key,需要确认保存后、编辑中和已有渠道三种场景都能正常拉取模型和测试连通性。
|
- 管理后台 `/admin/settings` 中的渠道模型列表获取和模型测试已改为走后端接口,前端不再直接读取或转发 API Key,需要确认保存后、编辑中和已有渠道三种场景都能正常拉取模型和测试连通性。
|
||||||
|
|||||||
@@ -404,7 +404,7 @@ function AssistantComposer({
|
|||||||
{mode === "image" ? (
|
{mode === "image" ? (
|
||||||
<>
|
<>
|
||||||
<ModelPicker config={config} value={config.imageModel || config.model} onChange={(model) => onConfigChange("imageModel", model)} onMissingConfig={onMissingConfig} />
|
<ModelPicker config={config} value={config.imageModel || config.model} onChange={(model) => onConfigChange("imageModel", model)} onMissingConfig={onMissingConfig} />
|
||||||
<CanvasImageSettingsPopover config={config} onConfigChange={onConfigChange} onMissingConfig={onMissingConfig} />
|
<CanvasImageSettingsPopover config={config} placement="topRight" getPopupContainer={() => document.body} onConfigChange={onConfigChange} onMissingConfig={onMissingConfig} />
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<ModelPicker config={config} value={config.textModel || config.model} onChange={(model) => onConfigChange("textModel", model)} onMissingConfig={onMissingConfig} />
|
<ModelPicker config={config} value={config.textModel || config.model} onChange={(model) => onConfigChange("textModel", model)} onMissingConfig={onMissingConfig} />
|
||||||
|
|||||||
@@ -152,7 +152,7 @@ function AspectIcon({ type, width, height, color }: { type: string; width: numbe
|
|||||||
const boxHeight = ratio >= 1 ? Math.max(12, 28 / ratio) : 28;
|
const boxHeight = ratio >= 1 ? Math.max(12, 28 / ratio) : 28;
|
||||||
return (
|
return (
|
||||||
<span className="grid h-8 w-10 place-items-center">
|
<span className="grid h-8 w-10 place-items-center">
|
||||||
<span className="rounded-md border-2" style={{ width: boxWidth, height: boxHeight, borderColor: color }} />
|
<span className="border-2" style={{ width: boxWidth, height: boxHeight, borderColor: color }} />
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,29 @@
|
|||||||
|
.dropdown :global(.ant-select-item) {
|
||||||
|
font-weight: 400;
|
||||||
|
min-height: 34px;
|
||||||
|
padding-inline: 10px 28px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown :global(.ant-select-item-option-selected) {
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown :global(.rc-virtual-list-holder) {
|
||||||
|
scrollbar-width: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown :global(.rc-virtual-list-holder)::-webkit-scrollbar,
|
||||||
|
.dropdown :global(.rc-virtual-list-holder)::-webkit-scrollbar-button {
|
||||||
|
height: 0;
|
||||||
|
width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown :global(.rc-virtual-list-scrollbar) {
|
||||||
|
right: 4px !important;
|
||||||
|
width: 2px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown :global(.rc-virtual-list-scrollbar-thumb) {
|
||||||
|
background: rgba(120, 113, 108, 0.42) !important;
|
||||||
|
border-radius: 999px !important;
|
||||||
|
}
|
||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
import { Select } from "antd";
|
import { Select } from "antd";
|
||||||
|
|
||||||
|
import styles from "./model-picker.module.css";
|
||||||
import type { AiConfig } from "@/stores/use-config-store";
|
import type { AiConfig } from "@/stores/use-config-store";
|
||||||
|
|
||||||
type ModelPickerProps = {
|
type ModelPickerProps = {
|
||||||
@@ -15,14 +16,15 @@ type ModelPickerProps = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export function ModelPicker({ config, value, onChange, className, fullWidth = false, placeholder = "选择模型", onMissingConfig }: ModelPickerProps) {
|
export function ModelPicker({ config, value, onChange, className, fullWidth = false, placeholder = "选择模型", onMissingConfig }: ModelPickerProps) {
|
||||||
const options = Array.from(new Set([value, ...config.models].filter(Boolean))).map((model) => ({ value: model, label: model }));
|
const options = Array.from(new Set([value, ...config.models].filter(Boolean))).map((model) => ({ value: model, label: <ModelLabel model={model} /> }));
|
||||||
const width = fullWidth ? "100%" : `min(${Math.max(156, (value || placeholder).length * 8 + 64)}px, 100%)`;
|
const width = fullWidth ? "100%" : `min(${Math.max(156, (value || placeholder).length * 8 + 64)}px, 100%)`;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Select
|
<Select
|
||||||
showSearch
|
showSearch
|
||||||
className={`canvas-control-select ${className || ""}`}
|
className={`canvas-control-select ${className || ""}`}
|
||||||
popupMatchSelectWidth={false}
|
classNames={{ popup: { root: styles.dropdown } }}
|
||||||
|
popupMatchSelectWidth
|
||||||
popupRender={(menu) => <div onMouseDown={(event) => event.stopPropagation()} onPointerDown={(event) => event.stopPropagation()}>{menu}</div>}
|
popupRender={(menu) => <div onMouseDown={(event) => event.stopPropagation()} onPointerDown={(event) => event.stopPropagation()}>{menu}</div>}
|
||||||
style={{ width, maxWidth: "100%", minWidth: 0, flexShrink: 1 }}
|
style={{ width, maxWidth: "100%", minWidth: 0, flexShrink: 1 }}
|
||||||
value={value || undefined}
|
value={value || undefined}
|
||||||
@@ -35,7 +37,20 @@ export function ModelPicker({ config, value, onChange, className, fullWidth = fa
|
|||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (!options.length) onMissingConfig?.();
|
if (!options.length) onMissingConfig?.();
|
||||||
}}
|
}}
|
||||||
filterOption={(input, option) => String(option?.label || "").toLowerCase().includes(input.toLowerCase())}
|
filterOption={(input, option) => String(option?.value || "").toLowerCase().includes(input.toLowerCase())}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function ModelLabel({ model }: { model: string }) {
|
||||||
|
const icon = resolveModelIcon(model);
|
||||||
|
return <span className="flex min-w-0 items-center gap-2">{icon && <img src={icon} alt="" className="size-4 shrink-0" />}<span className="truncate">{model}</span></span>;
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveModelIcon(model: string) {
|
||||||
|
const name = model.toLowerCase();
|
||||||
|
if (name.includes("claude") || name.includes("anthropic")) return "/icons/claude.svg";
|
||||||
|
if (name.includes("gemini") || name.includes("google")) return "/icons/gemini.svg";
|
||||||
|
if (name.includes("gpt") || name.includes("openai")) return "/icons/openai.svg";
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user