Files
infinite-canvas/web/src/components/model-picker.tsx
T
HouYunFei 44c5825cb0 feat(canvas): 添加视频节点支持功能
- 在后端添加 AI 视频相关接口处理函数
- 扩展前端画布组件支持视频节点类型
- 实现视频上传、生成和播放功能
- 更新画布数据结构文档以包含视频节点
- 添加视频节点的操作工具栏和配置面板
- 集成视频存储和检索服务
- 优化视频节点的渲染和交互体验
2026-05-23 16:53:21 +08:00

98 lines
3.9 KiB
TypeScript

"use client";
import { useMemo, useState } from "react";
import { Cpu } from "lucide-react";
import { Select, SelectContent, SelectItem, SelectTrigger } from "@/components/ui/select";
import { cn } from "@/lib/utils";
import type { AiConfig } from "@/stores/use-config-store";
type ModelPickerProps = {
config: AiConfig;
value?: string;
onChange: (model: string) => void;
className?: string;
fullWidth?: boolean;
placeholder?: string;
onMissingConfig?: () => void;
};
export function ModelPicker({ config, value, onChange, className, fullWidth = false, placeholder = "选择模型", onMissingConfig }: ModelPickerProps) {
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
open={open}
value={current}
onOpenChange={(nextOpen) => {
if (nextOpen && !options.length) {
onMissingConfig?.();
return;
}
setOpen(nextOpen);
}}
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
data-canvas-no-zoom
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"
onPointerDown={(event) => event.stopPropagation()}
onMouseDown={(event) => event.stopPropagation()}
>
{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 }) {
return (
<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 dark:invert" /> : <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";
if (name.includes("gemini") || name.includes("google")) return "/icons/gemini.svg";
if (name.includes("gpt") || name.includes("openai")) return "/icons/openai.svg";
if (name.includes("grok") || name.includes("grok")) return "/icons/grok.svg";
return "";
}