"use client"; import { useEffect, useMemo, useState } from "react"; import useSWR from "swr"; import { toast } from "sonner"; import { Button } from "@/components/ui/button"; import { Card, CardContent, CardDescription, CardHeader, CardTitle, } from "@/components/ui/card"; import { Label } from "@/components/ui/label"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@/components/ui/select"; import { Switch } from "@/components/ui/switch"; import { Textarea } from "@/components/ui/textarea"; import { isImageGenerationModelDescriptor, isVideoModelDescriptor, } from "@/lib/ai/model-classification"; import { fetcher } from "@/lib/utils"; import { getVideoCanvasFunctionBillingCopy, type VideoCanvasFunctionConfig, type VideoCanvasFunctionDefinition, VIDEO_CANVAS_FUNCTION_CONFIG_KEY, defaultVideoCanvasFunctionConfig, normalizeVideoCanvasFunctionConfig, } from "@/lib/video-canvas/config"; type SettingsResponse = { setting?: { value?: unknown; }; }; type AiModelOption = { id: string; modelId: string; displayName: string; providerName?: string; providerType?: string; supportsVision?: boolean; supportsImageGen?: boolean; isEnabled?: boolean; isHidden?: boolean; isMultimodal?: boolean; capabilities?: unknown; }; type AiModelsResponse = { models?: AiModelOption[]; }; const INHERIT_MODEL_VALUE = "__inherit__"; const functionKindLabels: Record = { audio: "音频", image: "图片生成", "image-edit": "图片编辑", multimodal: "多模态", utility: "工具", video: "视频生成", workflow: "工作流", }; function getModelOptionValue(model: AiModelOption) { return String(model.modelId || model.id || "").trim(); } function getModelOptionLabel(model: AiModelOption) { const name = model.displayName || model.modelId || model.id; const id = model.modelId && model.modelId !== name ? ` · ${model.modelId}` : ""; return `${name}${id}`; } function isImageModelOption(model: AiModelOption) { return isImageGenerationModelDescriptor({ providerType: model.providerType, modelId: model.modelId, capabilities: model.capabilities, supportsImageGen: model.supportsImageGen, }); } function isVideoModelOption(model: AiModelOption) { return isVideoModelDescriptor({ providerType: model.providerType, modelId: model.modelId, capabilities: model.capabilities, supportsImageGen: model.supportsImageGen, }); } function filterModelsForFunctionKind( models: AiModelOption[], kind: VideoCanvasFunctionDefinition["kind"], ) { const enabled = models.filter((model) => model.isEnabled !== false && getModelOptionValue(model)); let filtered = enabled; if (kind === "image" || kind === "image-edit") { filtered = enabled.filter(isImageModelOption); } else if (kind === "video") { filtered = enabled.filter(isVideoModelOption); } else if (kind === "multimodal") { filtered = enabled.filter( (model) => model.isMultimodal || model.supportsVision || (!isImageModelOption(model) && !isVideoModelOption(model)), ); } return filtered.length ? filtered : enabled; } function ModelSelect({ allowInherit = false, kind, models, onChange, value, }: { allowInherit?: boolean; kind: VideoCanvasFunctionDefinition["kind"]; models: AiModelOption[]; onChange: (value: string) => void; value: string; }) { const filteredModels = filterModelsForFunctionKind(models, kind); const normalizedValue = value?.trim() || (allowInherit ? INHERIT_MODEL_VALUE : "auto"); const hasCurrent = normalizedValue === "auto" || normalizedValue === INHERIT_MODEL_VALUE || filteredModels.some((model) => getModelOptionValue(model) === normalizedValue); return ( ); } export function VideoCanvasSettingsClient() { const { data, isLoading, mutate } = useSWR( `/api/admin/settings?key=${VIDEO_CANVAS_FUNCTION_CONFIG_KEY}`, fetcher, ); const { data: aiModelsData } = useSWR("/api/admin/ai-models", fetcher); const [config, setConfig] = useState(defaultVideoCanvasFunctionConfig); const [saving, setSaving] = useState(false); const modelOptions = useMemo( () => (aiModelsData?.models || []).filter((model) => model.isEnabled !== false), [aiModelsData?.models], ); useEffect(() => { if (!data) return; setConfig(normalizeVideoCanvasFunctionConfig(data.setting?.value)); }, [data]); const functions = useMemo( () => Object.values(config.functions).sort((a, b) => { const kindCompare = a.kind.localeCompare(b.kind); return kindCompare || a.label.localeCompare(b.label); }), [config.functions], ); const updateRoot = (updates: Partial) => { setConfig((current) => ({ ...current, ...updates })); }; const updateFunction = ( key: string, updates: Partial, ) => { setConfig((current) => ({ ...current, functions: { ...current.functions, [key]: { ...current.functions[key], ...updates, }, }, })); }; const save = async () => { setSaving(true); try { const response = await fetch("/api/admin/settings", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ key: VIDEO_CANVAS_FUNCTION_CONFIG_KEY, value: config, }), }); const payload = await response.json().catch(() => ({})); if (!response.ok) { throw new Error(payload?.message || payload?.error || "保存失败"); } toast.success("视频画布功能配置已保存"); await mutate(); } catch (error) { toast.error(error instanceof Error ? error.message : "保存失败"); } finally { setSaving(false); } }; const resetToDefault = () => { setConfig(defaultVideoCanvasFunctionConfig); toast.info("已恢复默认配置,保存后生效"); }; return (

视频画布功能设置

这里集中配置复刻画布里的工具能力、预设提示词和默认模型。积分与扣费统一跟随全站模型/功能配置,不再在视频画布里单独维护一套价格。

全局模型与开关 默认模型从后台已启用模型中选择;保持自动时由各接口按当前可用模型选择。
updateRoot({ enabled: checked })} />
updateRoot({ defaultImageModel: value || "auto" })} value={config.defaultImageModel} />
updateRoot({ defaultVideoModel: value || "auto" })} value={config.defaultVideoModel} />
updateRoot({ defaultMultimodalModel: value || "auto" })} value={config.defaultMultimodalModel} />

功能预设提示词

视频画布运行时会通过 /api/video-canvas/config 读取这里保存的提示词。比如“多机位九宫格”的分镜规则、黑色分割线要求和镜头说明,都由管理员在这里维护,前端不再写死覆盖。

计费说明只展示来源,不在这里改价;需要调整积分请回到全站积分设置、模型设置或对应功能设置统一修改。

{functions.map((item) => (
{item.label} {functionKindLabels[item.kind]} {item.route} {item.description}
updateFunction(item.key, { enabled: checked })} />
updateFunction(item.key, { model: value })} value={item.model || ""} />
积分来源:{getVideoCanvasFunctionBillingCopy(item).shortLabel}
{getVideoCanvasFunctionBillingCopy(item).description}