44c5825cb0
- 在后端添加 AI 视频相关接口处理函数 - 扩展前端画布组件支持视频节点类型 - 实现视频上传、生成和播放功能 - 更新画布数据结构文档以包含视频节点 - 添加视频节点的操作工具栏和配置面板 - 集成视频存储和检索服务 - 优化视频节点的渲染和交互体验
98 lines
3.9 KiB
TypeScript
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 "";
|
|
}
|