mirror of
https://github.com/basketikun/infinite-canvas.git
synced 2026-08-05 00:34:22 +08:00
refactor(canvas): 优化模型选择器和定时同步配置
- 将模型选择器替换为统一的 shadcn 风格下拉组件 - 移除原生数字输入框的步进箭头样式 - 更新默认定时同步配置为每5分钟执行 - 修复画布助手和节点配置中的模型选择逻辑 - 统一各页面中的模型选择器样式和交互行为
This commit is contained in:
@@ -1,9 +1,10 @@
|
||||
"use client";
|
||||
|
||||
import { Select } from "antd";
|
||||
import { useMemo, useState } from "react";
|
||||
import { Cpu } from "lucide-react";
|
||||
|
||||
import styles from "./model-picker.module.css";
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger } from "@/components/ui/select";
|
||||
import { cn } from "@/lib/utils";
|
||||
import type { AiConfig } from "@/stores/use-config-store";
|
||||
|
||||
type ModelPickerProps = {
|
||||
@@ -17,50 +18,68 @@ type 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: <ModelLabel model={model} /> }));
|
||||
const width = fullWidth ? "100%" : `min(${Math.max(156, (value || placeholder).length * 8 + 64)}px, 100%)`;
|
||||
const [open, setOpen] = useState(false);
|
||||
const options = useMemo(() => Array.from(new Set([value, ...config.models].filter(Boolean))), [config.models, value]);
|
||||
const current = value || "";
|
||||
|
||||
return (
|
||||
<Select
|
||||
showSearch
|
||||
className={`canvas-control-select ${className || ""}`}
|
||||
classNames={{ popup: { root: styles.dropdown } }}
|
||||
popupMatchSelectWidth
|
||||
popupRender={(menu) => (
|
||||
<div onMouseDown={(event) => event.stopPropagation()} onPointerDown={(event) => event.stopPropagation()}>
|
||||
{menu}
|
||||
</div>
|
||||
)}
|
||||
style={{ width, maxWidth: "100%", minWidth: 0, flexShrink: 1 }}
|
||||
value={value || undefined}
|
||||
placeholder={placeholder}
|
||||
options={options}
|
||||
notFoundContent="请先到配置里拉取模型列表"
|
||||
onChange={onChange}
|
||||
onMouseDown={(event) => event.stopPropagation()}
|
||||
onPointerDown={(event) => event.stopPropagation()}
|
||||
onClick={() => {
|
||||
if (!options.length) onMissingConfig?.();
|
||||
open={open}
|
||||
value={current}
|
||||
onOpenChange={(nextOpen) => {
|
||||
if (nextOpen && !options.length) {
|
||||
onMissingConfig?.();
|
||||
return;
|
||||
}
|
||||
setOpen(nextOpen);
|
||||
}}
|
||||
filterOption={(input, option) =>
|
||||
String(option?.value || "")
|
||||
.toLowerCase()
|
||||
.includes(input.toLowerCase())
|
||||
}
|
||||
/>
|
||||
onValueChange={onChange}
|
||||
>
|
||||
<SelectTrigger
|
||||
className={cn(
|
||||
"canvas-composer-model-picker h-8 w-fit max-w-full gap-2 rounded-full border border-input bg-transparent px-3 text-sm font-normal shadow-sm transition-colors",
|
||||
fullWidth ? "w-full min-w-0 justify-start" : "min-w-[9rem] justify-start",
|
||||
"data-[state=open]:border-ring data-[state=open]:ring-2 data-[state=open]:ring-ring/20",
|
||||
className,
|
||||
)}
|
||||
onMouseDown={(event) => event.stopPropagation()}
|
||||
onPointerDown={(event) => event.stopPropagation()}
|
||||
title={current || placeholder}
|
||||
>
|
||||
<ModelIcon model={current} />
|
||||
<span className="canvas-model-picker-text min-w-0 flex-1 truncate text-left">{current || placeholder}</span>
|
||||
</SelectTrigger>
|
||||
<SelectContent className="z-50 w-80 max-w-[calc(100vw-24px)] rounded-xl border border-border/70 bg-popover p-1 shadow-xl" position="popper" align="start">
|
||||
{options.length ? (
|
||||
options.map((model) => (
|
||||
<SelectItem key={model} value={model} textValue={model}>
|
||||
<ModelLabel model={model} />
|
||||
</SelectItem>
|
||||
))
|
||||
) : (
|
||||
<SelectItem value="__empty__" disabled>
|
||||
请先到配置里拉取模型列表
|
||||
</SelectItem>
|
||||
)}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
);
|
||||
}
|
||||
|
||||
function ModelLabel({ model }: { model: string }) {
|
||||
const icon = resolveModelIcon(model);
|
||||
return (
|
||||
<span className="model-picker-label flex min-w-0 items-center gap-2">
|
||||
{icon ? <img src={icon} alt="" className="size-4 shrink-0" /> : <Cpu className="size-4 shrink-0 opacity-70" />}
|
||||
<span className="model-picker-label-text truncate">{model}</span>
|
||||
<span className="flex min-w-0 items-center gap-2">
|
||||
<ModelIcon model={model} />
|
||||
<span className="truncate">{model}</span>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
function ModelIcon({ model }: { model: string }) {
|
||||
const icon = resolveModelIcon(model);
|
||||
return icon ? <img src={icon} alt="" className="size-4 shrink-0" /> : <Cpu className="size-4 shrink-0 opacity-70" />;
|
||||
}
|
||||
|
||||
function resolveModelIcon(model: string) {
|
||||
const name = model.toLowerCase();
|
||||
if (name.includes("claude") || name.includes("anthropic")) return "/icons/claude.svg";
|
||||
|
||||
Reference in New Issue
Block a user