diff --git a/src/components/nodes/Generate3DNode.tsx b/src/components/nodes/Generate3DNode.tsx index dca8c4c8..a4d3f517 100644 --- a/src/components/nodes/Generate3DNode.tsx +++ b/src/components/nodes/Generate3DNode.tsx @@ -11,6 +11,8 @@ import { ModelSearchDialog } from "@/components/modals/ModelSearchDialog"; import { useToast } from "@/components/Toast"; import { ProviderBadge } from "./ProviderBadge"; import { getModelPageUrl, getProviderDisplayName } from "@/utils/providerUrls"; +import { useInlineParameters } from "@/hooks/useInlineParameters"; +import { InlineParameterPanel } from "./InlineParameterPanel"; // 3D generation capabilities const THREE_D_CAPABILITIES: ModelCapability[] = ["text-to-3d", "image-to-3d"]; @@ -23,6 +25,9 @@ export function Generate3DNode({ id, data, selected }: NodeProps { + const nodes = useWorkflowStore.getState().nodes; + const node = nodes.find(n => n.id === id); + const currentExpanded = (node?.data as Generate3DNodeData)?.parametersExpanded ?? true; + updateNodeData(id, { parametersExpanded: !currentExpanded }); + }, [id, updateNodeData]); + // Track previous status to detect error transitions const prevStatusRef = useRef(nodeData.status); @@ -397,8 +412,8 @@ export function Generate3DNode({ id, data, selected }: NodeProps )} - {/* Model-specific parameters */} - {nodeData.selectedModel?.modelId && ( + {/* Model-specific parameters (hidden when inline enabled - shown in panel below) */} + {!inlineParametersEnabled && nodeData.selectedModel?.modelId && ( )} + + {/* Inline parameter panel */} + {inlineParametersEnabled && ( + + {/* Model selector: Browse button + current model display */} +
+
+
+ {displayTitle} +
+
+ {currentProvider} +
+
+ +
+ + {/* External provider parameters - reuse ModelParameters component */} + {nodeData.selectedModel?.modelId && ( + + )} +
+ )} diff --git a/src/components/nodes/GenerateAudioNode.tsx b/src/components/nodes/GenerateAudioNode.tsx index 17197d1c..632518af 100644 --- a/src/components/nodes/GenerateAudioNode.tsx +++ b/src/components/nodes/GenerateAudioNode.tsx @@ -11,6 +11,8 @@ import { ProviderModel } from "@/lib/providers/types"; import { ModelSearchDialog } from "@/components/modals/ModelSearchDialog"; import { useAudioVisualization } from "@/hooks/useAudioVisualization"; import { useAudioPlayback } from "@/hooks/useAudioPlayback"; +import { useInlineParameters } from "@/hooks/useInlineParameters"; +import { InlineParameterPanel } from "./InlineParameterPanel"; type GenerateAudioNodeType = Node; @@ -21,6 +23,9 @@ export function GenerateAudioNode({ id, data, selected }: NodeProps { + const nodes = useWorkflowStore.getState().nodes; + const node = nodes.find(n => n.id === id); + const currentExpanded = (node?.data as GenerateAudioNodeData)?.parametersExpanded ?? true; + updateNodeData(id, { parametersExpanded: !currentExpanded }); + }, [id, updateNodeData]); + // Dynamic handles based on inputSchema const dynamicHandles = useMemo(() => { if (!nodeData.inputSchema || nodeData.inputSchema.length === 0) return null; @@ -222,8 +237,8 @@ export function GenerateAudioNode({ id, data, selected }: NodeProps - {/* Model parameters */} - {nodeData.selectedModel?.modelId && ( + {/* Model parameters (hidden when inline enabled - shown in panel below) */} + {!inlineParametersEnabled && nodeData.selectedModel?.modelId && ( + + {/* Inline parameter panel */} + {inlineParametersEnabled && ( + + {/* Model selector: Browse button + current model display */} +
+
+
+ {displayTitle} +
+
+ {currentProvider} +
+
+ +
+ + {/* External provider parameters - reuse ModelParameters component */} + {nodeData.selectedModel?.modelId && ( + + )} +
+ )} {/* Browse dialog */} diff --git a/src/components/nodes/LLMGenerateNode.tsx b/src/components/nodes/LLMGenerateNode.tsx index 2b4cf9a4..e9bb0b80 100644 --- a/src/components/nodes/LLMGenerateNode.tsx +++ b/src/components/nodes/LLMGenerateNode.tsx @@ -4,7 +4,34 @@ import { useCallback, useState } from "react"; import { Handle, Position, NodeProps, Node } from "@xyflow/react"; import { BaseNode } from "./BaseNode"; import { useWorkflowStore } from "@/store/workflowStore"; -import { LLMGenerateNodeData } from "@/types"; +import { LLMGenerateNodeData, LLMProvider, LLMModelType } from "@/types"; +import { useInlineParameters } from "@/hooks/useInlineParameters"; +import { InlineParameterPanel } from "./InlineParameterPanel"; + +// LLM providers and models +const LLM_PROVIDERS: { value: LLMProvider; label: string }[] = [ + { value: "google", label: "Google" }, + { value: "openai", label: "OpenAI" }, + { value: "anthropic", label: "Anthropic" }, +]; + +const LLM_MODELS: Record = { + google: [ + { value: "gemini-3-flash-preview", label: "Gemini 3 Flash" }, + { value: "gemini-2.5-flash", label: "Gemini 2.5 Flash" }, + { value: "gemini-3-pro-preview", label: "Gemini 3.0 Pro" }, + { value: "gemini-3.1-pro-preview", label: "Gemini 3.1 Pro" }, + ], + openai: [ + { value: "gpt-4.1-mini", label: "GPT-4.1 Mini" }, + { value: "gpt-4.1-nano", label: "GPT-4.1 Nano" }, + ], + anthropic: [ + { value: "claude-sonnet-4.5", label: "Claude Sonnet 4.5" }, + { value: "claude-haiku-4.5", label: "Claude Haiku 4.5" }, + { value: "claude-opus-4.6", label: "Claude Opus 4.6" }, + ], +}; type LLMGenerateNodeType = Node; @@ -15,6 +42,9 @@ export function LLMGenerateNode({ id, data, selected }: NodeProps state.regenerateNode); const isRunning = useWorkflowStore((state) => state.isRunning); + // Inline parameters infrastructure + const { inlineParametersEnabled } = useInlineParameters(); + const handleRegenerate = useCallback(() => { regenerateNode(id); }, [id, regenerateNode]); @@ -37,6 +67,57 @@ export function LLMGenerateNode({ id, data, selected }: NodeProps { + const nodes = useWorkflowStore.getState().nodes; + const node = nodes.find(n => n.id === id); + const currentExpanded = (node?.data as LLMGenerateNodeData)?.parametersExpanded ?? true; + updateNodeData(id, { parametersExpanded: !currentExpanded }); + }, [id, updateNodeData]); + + // LLM parameter handlers + const handleProviderChange = useCallback( + (e: React.ChangeEvent) => { + const newProvider = e.target.value as LLMProvider; + const firstModelForProvider = LLM_MODELS[newProvider][0].value; + const updates: Partial = { + provider: newProvider, + model: firstModelForProvider, + }; + if (newProvider === "anthropic" && (nodeData.temperature || 0.7) > 1) { + updates.temperature = 1; + } + updateNodeData(id, updates); + }, + [id, nodeData.temperature, updateNodeData] + ); + + const handleModelChange = useCallback( + (e: React.ChangeEvent) => { + updateNodeData(id, { model: e.target.value as LLMModelType }); + }, + [id, updateNodeData] + ); + + const handleTemperatureChange = useCallback( + (e: React.ChangeEvent) => { + updateNodeData(id, { temperature: parseFloat(e.target.value) }); + }, + [id, updateNodeData] + ); + + const handleMaxTokensChange = useCallback( + (e: React.ChangeEvent) => { + updateNodeData(id, { maxTokens: parseInt(e.target.value, 10) }); + }, + [id, updateNodeData] + ); + + const provider = nodeData.provider || "google"; + const availableModels = LLM_MODELS[provider] || LLM_MODELS.google; + return ( )} + + {/* Inline parameter panel */} + {inlineParametersEnabled && ( + + {/* LLM-specific controls with compact styling */} +
+ {/* Provider */} +
+ + +
+ + {/* Model */} +
+ + +
+ + {/* Temperature */} +
+ + +
+ + {/* Max Tokens */} +
+ + +
+
+
+ )}
); }