import { useEffect, useState } from "react"; import { ArrowUp, LoaderCircle, Maximize2, Square } from "lucide-react"; import { Button, Modal, Tooltip } from "antd"; import { useTranslation } from "react-i18next"; import { ModelPicker } from "@/components/model-picker"; import { defaultConfig, resolveModelForCapability, useConfigStore, useEffectiveConfig, type AiConfig } from "@/stores/use-config-store"; import { canvasThemes } from "@/lib/canvas-theme"; import { useThemeStore } from "@/stores/use-theme-store"; import { CanvasImageSettingsPopover } from "./canvas-image-settings-popover"; import { CanvasPromptLibrary } from "./canvas-prompt-library"; import { CanvasAudioSettingsPopover, type CanvasAudioSettingKey } from "./canvas-audio-settings-popover"; import { CanvasPromptChipInput } from "./canvas-prompt-chip-input"; import { CanvasVideoSettingsPopover } from "./canvas-video-settings-popover"; import { CanvasTextSettingsPopover } from "./canvas-text-settings-popover"; import { CanvasNodeType, type CanvasGenerationMode, type CanvasNodeData } from "@/types/canvas"; import type { CanvasResourceReference } from "@/lib/canvas/canvas-resource-references"; export type CanvasNodeGenerationMode = CanvasGenerationMode; type CanvasNodePromptPanelProps = { node: CanvasNodeData; isRunning: boolean; onPromptChange: (nodeId: string, prompt: string) => void; onConfigChange: (nodeId: string, patch: Partial) => void; onGenerate: (nodeId: string, mode: CanvasNodeGenerationMode, prompt: string) => void; onStop: (nodeId: string) => void; mentionReferences?: CanvasResourceReference[]; onImageSettingsOpenChange?: (open: boolean) => void; modeOverride?: CanvasNodeGenerationMode; // Plugin nodes set their generation type through useBuiltinPanel.mode. }; export function CanvasNodePromptPanel({ node, isRunning, onPromptChange, onConfigChange, onGenerate, onStop, mentionReferences = [], onImageSettingsOpenChange, modeOverride }: CanvasNodePromptPanelProps) { const { t } = useTranslation(); const globalConfig = useEffectiveConfig(); const openConfigDialog = useConfigStore((state) => state.openConfigDialog); const theme = canvasThemes[useThemeStore((state) => state.theme)]; const mode = modeOverride ?? defaultMode(node.type); const config = buildNodeConfig(globalConfig, node, mode); const hasTextContent = node.type === CanvasNodeType.Text && Boolean(node.metadata?.content?.trim()); const hasImageContent = node.type === CanvasNodeType.Image && Boolean(node.metadata?.content); const isEditingExistingContent = hasTextContent || hasImageContent; const [prompt, setPrompt] = useState(node.metadata?.composerContent ?? node.metadata?.prompt ?? ""); const [expandedPrompt, setExpandedPrompt] = useState(""); const [expanded, setExpanded] = useState(false); // Restore prompts only when switching nodes; preserve the current input after generation on the same node. useEffect(() => { setPrompt(node.metadata?.composerContent ?? node.metadata?.prompt ?? ""); // eslint-disable-next-line react-hooks/exhaustive-deps }, [node.id]); const updatePrompt = (value: string) => { setPrompt(value); if (isEditingExistingContent) onConfigChange(node.id, { composerContent: value }); else onPromptChange(node.id, value); }; const submit = () => { const text = prompt.trim(); if (!text || isRunning) return; onGenerate(node.id, mode, text); }; const openExpandedEditor = () => { setExpandedPrompt(prompt); setExpanded(true); }; const saveExpandedPrompt = () => { updatePrompt(expandedPrompt); setExpanded(false); }; return (
event.stopPropagation()} onPointerDown={(event) => event.stopPropagation()} onWheel={(event) => event.stopPropagation()} >
setExpanded(false)} destroyOnHidden>
event.stopPropagation()}>
); } function defaultMode(type: CanvasNodeData["type"]): CanvasNodeGenerationMode { return type === CanvasNodeType.Text ? "text" : type === CanvasNodeType.Video ? "video" : type === CanvasNodeType.Audio ? "audio" : "image"; } function buildNodeConfig(globalConfig: AiConfig, node: CanvasNodeData, mode: CanvasNodeGenerationMode): AiConfig { return { ...globalConfig, model: resolveModelForCapability(globalConfig, node.metadata?.model, mode), reasoningEffort: node.metadata?.reasoningEffort || globalConfig.reasoningEffort || defaultConfig.reasoningEffort, quality: node.metadata?.quality || globalConfig.quality || defaultConfig.quality, size: node.metadata?.size || globalConfig.size || defaultConfig.size, background: node.metadata?.background ?? globalConfig.background ?? defaultConfig.background, videoSeconds: node.metadata?.seconds || globalConfig.videoSeconds || defaultConfig.videoSeconds, vquality: node.metadata?.vquality || globalConfig.vquality || defaultConfig.vquality, videoGenerateAudio: node.metadata?.generateAudio || globalConfig.videoGenerateAudio || defaultConfig.videoGenerateAudio, videoWatermark: node.metadata?.watermark || globalConfig.videoWatermark || defaultConfig.videoWatermark, audioVoice: node.metadata?.audioVoice || globalConfig.audioVoice || defaultConfig.audioVoice, audioFormat: node.metadata?.audioFormat || globalConfig.audioFormat || defaultConfig.audioFormat, audioSpeed: node.metadata?.audioSpeed || globalConfig.audioSpeed || defaultConfig.audioSpeed, audioInstructions: node.metadata?.audioInstructions || globalConfig.audioInstructions || defaultConfig.audioInstructions, count: String(node.metadata?.count || (mode === "image" ? globalConfig.canvasImageCount || globalConfig.count : globalConfig.count) || defaultConfig.count), }; } function videoConfigPatch(key: keyof AiConfig, value: string) { if (key === "videoSeconds") return { seconds: value }; if (key === "videoGenerateAudio") return { generateAudio: value }; if (key === "videoWatermark") return { watermark: value }; return { [key]: value }; } function audioConfigPatch(key: CanvasAudioSettingKey, value: string) { if (key === "audioVoice") return { audioVoice: value }; if (key === "audioFormat") return { audioFormat: value }; if (key === "audioSpeed") return { audioSpeed: value }; return { audioInstructions: value }; }