import type { CSSProperties } from "react"; import { Image as ImageIcon, LoaderCircle, MessageSquare, Music2, Play, Settings2, Square, Video } from "lucide-react"; import { Button, Segmented, Select } from "antd"; import { ModelPicker } from "@/components/model-picker"; import { configuredModelMatchesCapability, defaultConfig, modelOptionName, resolveModelChannel, useEffectiveConfig, type AiConfig } from "@/stores/use-config-store"; import { CreditSymbol, requestCreditCost } from "@/constant/credits"; import { canvasThemes } from "@/lib/canvas-theme"; import { normalizeVideoDuration, normalizeVideoResolution } from "@/lib/video-generation-options"; import { modelCapabilityConfigFor, normalizeImageValue, normalizeVideoValue } from "@/lib/model-capabilities"; import { modelCompatibilityError, resolveCompatibleModel, type ModelRequirements } from "@/lib/model-selection"; import { navigateToSettings } from "@/lib/settings-navigation"; import { useThemeStore } from "@/stores/use-theme-store"; import { useUserStore } from "@/stores/use-user-store"; import { CanvasImageSettingsPopover } from "./canvas-image-settings-popover"; import { CanvasAudioSettingsPopover, type CanvasAudioSettingKey } from "./canvas-audio-settings-popover"; import { CanvasVideoSettingsPopover } from "./canvas-video-settings-popover"; import type { CanvasGenerationMode, CanvasNodeData, CanvasNodeMetadata, CanvasVideoEditOperation, CanvasWorkspaceMode } from "@/types/canvas"; type CanvasConfigNodePanelProps = { node: CanvasNodeData; isRunning: boolean; inputSummary: { textCount: number; imageCount: number; videoCount: number; audioCount: number; characterCount: number }; onConfigChange: (nodeId: string, patch: Partial) => void; onGenerate: (nodeId: string) => void; onStop: (nodeId: string) => void; onComposerToggle: () => void; workspaceMode?: CanvasWorkspaceMode; }; const videoOperationOptions: Array<{ label: string; value: CanvasVideoEditOperation }> = [ { label: "文生视频", value: "text_to_video" }, { label: "图生视频", value: "image_to_video" }, { label: "视频续写", value: "extend" }, { label: "局部修改", value: "inpaint" }, { label: "元素替换", value: "replace_element" }, { label: "运镜调整", value: "camera_motion" }, { label: "风格迁移", value: "style_transfer" }, { label: "音频生视频", value: "audio_to_video" }, { label: "版本对比", value: "compare_versions" }, ]; export function CanvasConfigNodePanel({ node, isRunning, inputSummary, onConfigChange, onGenerate, onStop, onComposerToggle, workspaceMode = "professional" }: CanvasConfigNodePanelProps) { const globalConfig = useEffectiveConfig(); const theme = canvasThemes[useThemeStore((state) => state.theme)]; const creditsEnabled = useUserStore((state) => state.features.creditsEnabled); const mode = node.metadata?.generationMode || "image"; const simpleMode = workspaceMode === "simple"; const requirements: ModelRequirements = { capability: mode, input: inputSummary, videoOperation: node.metadata?.videoEditOperation, videoSeconds: node.metadata?.seconds || globalConfig.videoSeconds, }; const config = buildNodeConfig(globalConfig, node, mode, requirements); const videoProfile = mode === "video" ? modelCapabilityConfigFor(config, config.model).video! : undefined; const operationOptions = videoProfile ? videoOperationOptions.filter((item) => videoProfile.operations.includes(item.value) || item.value === "concat") : videoOperationOptions; const count = Math.max(1, Math.min(15, Math.floor(Math.abs(Number(config.count)) || 1))); const priceChannel = resolveModelChannel(config, config.model); const credits = requestCreditCost({ channelMode: priceChannel.scope === "system" ? "remote" : "local", modelCosts: priceChannel.modelCosts, model: modelOptionName(config.model), count: mode === "image" ? count : 1, seconds: mode === "video" ? config.videoSeconds : 1 }); const hasPrice = creditsEnabled && credits !== null; const chipStyle = { background: theme.node.fill, borderColor: theme.node.stroke, color: theme.node.text }; const hasAnyInput = Boolean(inputSummary.textCount || inputSummary.imageCount || inputSummary.videoCount || inputSummary.audioCount || inputSummary.characterCount); const hasComposerContent = Boolean((node.metadata?.composerContent ?? node.metadata?.prompt ?? "").trim()); const capabilityError = modelCompatibilityError(config, config.model, requirements); const canGenerate = (hasComposerContent || (mode === "audio" ? inputSummary.textCount > 0 : hasAnyInput)) && !capabilityError; return (
event.stopPropagation()}>
{simpleMode ? "快速生成" : "生成配置"}
{simpleMode ? 自动配置 :
event.stopPropagation()}> onConfigChange(node.id, { generationMode: value as CanvasGenerationMode })} options={[ { value: "image", label: ( 生图 ), }, { value: "text", label: ( 文本 ), }, { value: "video", label: ( ), }, { value: "audio", label: ( 音频 ), }, ]} />
}
{inputSummary.characterCount ? : null}
{mode === "video" && !simpleMode ? (
event.stopPropagation()} onPointerDown={(event) => event.stopPropagation()}>