"use client"; import type { CSSProperties } from "react"; import { useState } from "react"; import { ArrowDown, ArrowLeft, ArrowRight, ArrowUp, Edit3, Eye, Image as ImageIcon, LoaderCircle, MessageSquare, Play, Video } from "lucide-react"; import { App, Button, Empty, Input, InputNumber, Modal, Segmented } from "antd"; import { ModelPicker } from "@/components/model-picker"; import { defaultConfig, useConfigStore, useEffectiveConfig, type AiConfig } from "@/stores/use-config-store"; import { CreditSymbol, requestCreditCost } from "@/constant/credits"; import { canvasThemes } from "@/lib/canvas-theme"; import { useThemeStore } from "@/stores/use-theme-store"; import { CanvasSizePicker } from "./canvas-size-picker"; import { CanvasVideoSettingsPopover } from "./canvas-video-settings-popover"; import type { NodeGenerationInput } from "./canvas-node-generation"; import type { CanvasGenerationMode, CanvasNodeData, CanvasNodeMetadata } from "../types"; type CanvasConfigNodePanelProps = { node: CanvasNodeData; isRunning: boolean; inputSummary: { textCount: number; imageCount: number }; inputs: NodeGenerationInput[]; onConfigChange: (nodeId: string, patch: Partial) => void; onTextInputChange: (nodeId: string, content: string) => void; onGenerate: (nodeId: string) => void; }; export function CanvasConfigNodePanel({ node, isRunning, inputSummary, inputs, onConfigChange, onTextInputChange, onGenerate }: CanvasConfigNodePanelProps) { const { message } = App.useApp(); const [previewOpen, setPreviewOpen] = useState(false); const [editingTextId, setEditingTextId] = useState(null); const [editingText, setEditingText] = useState(""); const globalConfig = useEffectiveConfig(); const modelCosts = useConfigStore((state) => state.publicSettings?.modelChannel.modelCosts); const openConfigDialog = useConfigStore((state) => state.openConfigDialog); const theme = canvasThemes[useThemeStore((state) => state.theme)]; const mode = node.metadata?.generationMode || "image"; const config = buildNodeConfig(globalConfig, node, mode); const count = Math.max(1, Math.min(15, Math.floor(Math.abs(Number(node.metadata?.count || 3)) || 1))); const credits = requestCreditCost({ channelMode: config.channelMode, modelCosts, model: config.model, count: mode === "image" ? count : 1 }); const chipStyle = { background: theme.node.fill, borderColor: theme.node.stroke, color: theme.node.text }; const textInputs = inputs.filter((input) => input.type === "text"); const imageInputs = inputs.filter((input) => input.type === "image"); const moveInput = (input: NodeGenerationInput, offset: number) => { const sameTypeInputs = inputs.filter((item) => item.type === input.type); const sameTypeIndex = sameTypeInputs.findIndex((item) => item.nodeId === input.nodeId); const targetInput = sameTypeInputs[sameTypeIndex + offset]; if (!targetInput) return; const index = inputs.findIndex((item) => item.nodeId === input.nodeId); const targetIndex = inputs.findIndex((item) => item.nodeId === targetInput.nodeId); const next = [...inputs]; [next[index], next[targetIndex]] = [next[targetIndex], next[index]]; onConfigChange(node.id, { inputOrder: next.map((input) => input.nodeId) }); message.success("已调整输入顺序"); }; const startTextEdit = (input: NodeGenerationInput) => { setEditingTextId(input.nodeId); setEditingText(input.text || ""); }; const saveTextEdit = () => { if (!editingTextId) return; onTextInputChange(editingTextId, editingText); setEditingText(""); setEditingTextId(null); message.success("已保存文本提示词"); }; return (
event.stopPropagation()}>
生成配置
event.stopPropagation()}> onConfigChange(node.id, { generationMode: value as CanvasGenerationMode })} options={[ { value: "image", label: ( 生图 ), }, { value: "text", label: ( 文本 ), }, { value: "video", label: ( ), }, ]} />
event.stopPropagation()}>
event.stopPropagation()}> onConfigChange(node.id, { model })} onMissingConfig={() => openConfigDialog(true)} fullWidth /> {mode === "video" ? onConfigChange(node.id, key === "videoSeconds" ? { seconds: value } : { [key]: value })} /> : mode === "image" ? onConfigChange(node.id, { size: value })} /> : null} {mode === "video" ? null : onConfigChange(node.id, { count: Number(value) || 1 })} />}
setPreviewOpen(false)} footer={null} centered width={860} mask={{ closable: true }} keyboard destroyOnHidden modalRender={(modal) => (
event.stopPropagation()} onMouseDown={(event) => event.stopPropagation()} onPointerDown={(event) => event.stopPropagation()}> {modal}
)} >
event.stopPropagation()} onPointerDown={(event) => event.stopPropagation()} onWheelCapture={(event) => event.stopPropagation()}> {inputs.length ? (
{imageInputs.map((input, index) => ( ))}
{textInputs.map((input, index) => ( ))}
{editingTextId ? ( <>
编辑文本提示词
setEditingText(event.target.value)} />
) : (
选择一条文本后在这里编辑
)}
) : ( )}
); } function PreviewSection({ title, count, empty, children }: { title: string; count: number; empty: string; children: React.ReactNode }) { return (
{title}
{count} 个
{count ? children :
{empty}
}
); } function TextSortCard({ input, textIndex, textTotal, inputs, theme, onMove, onEdit, }: { input: NodeGenerationInput; textIndex: number; textTotal: number; inputs: NodeGenerationInput[]; theme: (typeof canvasThemes)[keyof typeof canvasThemes]; onMove: (input: NodeGenerationInput, offset: number) => void; onEdit: (input: NodeGenerationInput) => void; }) { return (
文本 {textIndex + 1}
{input.text}
); } function ImageSortCard({ input, imageIndex, imageTotal, inputs, theme, onMove, }: { input: NodeGenerationInput; imageIndex: number; imageTotal: number; inputs: NodeGenerationInput[]; theme: (typeof canvasThemes)[keyof typeof canvasThemes]; onMove: (input: NodeGenerationInput, offset: number) => void; }) { if (!input.image) return null; return (
{input.title} {imageIndex + 1} onMove(input, offset)} />
); } function VerticalOrderButtons({ index, total, onMove }: { index: number; total: number; onMove: (offset: number) => void }) { return ( <>