From 721d7354403e631fd03c0b3f125c70a8320591f8 Mon Sep 17 00:00:00 2001 From: shrimbly Date: Tue, 3 Mar 2026 22:09:41 +1300 Subject: [PATCH] feat(quick-16): convert LLM node to full-bleed, add max tokens, add panel shadow - LLMGenerateNode: Remove inline provider/model/parameter controls - LLMGenerateNode: Convert to full-bleed layout with fullBleed prop - LLMGenerateNode: Show only text output area in node body - LLMGenerateNode: Keep floating action buttons (copy, regenerate, clear) - ControlPanel: Add Max Tokens slider to LLMControls (256-16384 range) - ControlPanel: Add shadow/gradient overlay on left edge of panel - WorkflowCanvas: Fix type errors with node.data union types (Rule 1 bug fix) --- src/components/WorkflowCanvas.tsx | 18 +- src/components/nodes/ControlPanel.tsx | 24 ++ src/components/nodes/LLMGenerateNode.tsx | 296 ++++++----------------- 3 files changed, 112 insertions(+), 226 deletions(-) diff --git a/src/components/WorkflowCanvas.tsx b/src/components/WorkflowCanvas.tsx index 77a4daae..1cc1706e 100644 --- a/src/components/WorkflowCanvas.tsx +++ b/src/components/WorkflowCanvas.tsx @@ -352,13 +352,13 @@ export function WorkflowCanvas() { const getNodeTitle = useCallback((node: Node) => { // For generate nodes, check for selectedModel display name if (node.type === "nanoBanana" || node.type === "generateVideo" || node.type === "generate3d" || node.type === "generateAudio") { - const model = node.data?.selectedModel; + const model = (node.data as any)?.selectedModel; if (model?.displayName) return model.displayName; } // For LLM nodes, check for selectedLLMModel or selectedModel if (node.type === "llmGenerate") { - const model = node.data?.selectedLLMModel || node.data?.selectedModel; + const model = (node.data as any)?.selectedLLMModel || (node.data as any)?.selectedModel; if (model?.displayName) return model.displayName; if (model?.name) return model.name; } @@ -368,7 +368,7 @@ export function WorkflowCanvas() { // Helper to get title prefix (provider badge for generate/LLM nodes) const getNodeTitlePrefix = useCallback((node: Node): React.ReactNode => { - const provider = node.data?.selectedModel?.provider; + const provider = (node.data as any)?.selectedModel?.provider; if (provider) { return ; } @@ -400,9 +400,9 @@ export function WorkflowCanvas() { return () => { const node = getNodeById(nodeId); if (!node) return; - const imageToEdit = node.data?.outputImage || node.data?.image; + const imageToEdit = (node.data as any)?.outputImage || (node.data as any)?.image; if (!imageToEdit) return; - openAnnotationModal(nodeId, imageToEdit, node.data?.annotations); + openAnnotationModal(nodeId, imageToEdit, (node.data as any)?.annotations); }; } @@ -2058,7 +2058,7 @@ export function WorkflowCanvas() { {allNodes.map((node) => { // Groups don't get floating headers - if (node.type === "group") return null; + if (node.type === "group" as any) return null; const defaultWidth = defaultNodeDimensions[node.type as NodeType]?.width ?? 250; const headerWidth = node.measured?.width || (node.style?.width as number) || defaultWidth; @@ -2129,7 +2129,7 @@ export function WorkflowCanvas() { return createPortal( { updateNodeData(expandingNode.id, { prompt }); setExpandingNode(null); @@ -2146,7 +2146,7 @@ export function WorkflowCanvas() { return createPortal( { updateNodeData(expandingNode.id, { template }); setExpandingNode(null); @@ -2163,7 +2163,7 @@ export function WorkflowCanvas() { return ( setExpandingNode(null)} /> ); diff --git a/src/components/nodes/ControlPanel.tsx b/src/components/nodes/ControlPanel.tsx index e481b028..a39fa752 100644 --- a/src/components/nodes/ControlPanel.tsx +++ b/src/components/nodes/ControlPanel.tsx @@ -117,6 +117,8 @@ export function ControlPanel() { return (
+ {/* Shadow/gradient overlay on left edge */} +
{/* Header */} @@ -852,6 +854,13 @@ function LLMControls({ node }: { node: Node }) { [node.id, updateNodeData] ); + const handleMaxTokensChange = useCallback( + (e: React.ChangeEvent) => { + updateNodeData(node.id, { maxTokens: parseInt(e.target.value, 10) }); + }, + [node.id, updateNodeData] + ); + const provider = nodeData.provider || "google"; const availableModels = LLM_MODELS[provider] || LLM_MODELS.google; @@ -897,6 +906,21 @@ function LLMControls({ node }: { node: Node }) { className="nodrag nopan w-full h-1 bg-neutral-700 rounded-lg appearance-none cursor-pointer accent-blue-500" />
+ +
+ + +
); } diff --git a/src/components/nodes/LLMGenerateNode.tsx b/src/components/nodes/LLMGenerateNode.tsx index 77758752..224a44e6 100644 --- a/src/components/nodes/LLMGenerateNode.tsx +++ b/src/components/nodes/LLMGenerateNode.tsx @@ -4,31 +4,7 @@ import { useCallback, useState } from "react"; import { Handle, Position, NodeProps, Node } from "@xyflow/react"; import { BaseNode } from "./BaseNode"; import { useWorkflowStore } from "@/store/workflowStore"; -import { LLMGenerateNodeData, LLMProvider, LLMModelType } from "@/types"; - -const PROVIDERS: { value: LLMProvider; label: string }[] = [ - { value: "google", label: "Google" }, - { value: "openai", label: "OpenAI" }, - { value: "anthropic", label: "Anthropic" }, -]; - -const 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" }, - ], -}; +import { LLMGenerateNodeData } from "@/types"; type LLMGenerateNodeType = Node; @@ -36,46 +12,6 @@ export function LLMGenerateNode({ id, data, selected }: NodeProps state.updateNodeData); - const handleProviderChange = useCallback( - (e: React.ChangeEvent) => { - const newProvider = e.target.value as LLMProvider; - const firstModelForProvider = MODELS[newProvider][0].value; - const updates: Partial = { - provider: newProvider, - model: firstModelForProvider, - }; - // Anthropic limits temperature to 0-1 - if (newProvider === "anthropic" && nodeData.temperature > 1) { - updates.temperature = 1; - } - updateNodeData(id, updates); - }, - [id, updateNodeData, nodeData.temperature] - ); - - 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 [showParams, setShowParams] = useState(false); - const regenerateNode = useWorkflowStore((state) => state.regenerateNode); const isRunning = useWorkflowStore((state) => state.isRunning); @@ -101,18 +37,13 @@ export function LLMGenerateNode({ id, data, selected }: NodeProps m.value === nodeData.model) - ? nodeData.model - : availableModels[0].value; - return ( {/* Image input - optional */} -
- {/* Output preview area */} -
- {nodeData.status === "loading" ? ( -
- - - - -
- ) : nodeData.status === "error" ? ( - - {nodeData.error || "Failed"} - - ) : nodeData.outputText ? ( - <> -

- {nodeData.outputText} -

-
- - - -
- - ) : ( -
- - Run to generate - -
- )} -
- - {/* Provider selector */} - - - {/* Model selector */} - - - {/* Collapsible parameters section */} -
- - {showParams && ( -
- {/* Temperature slider */} -
- - -
- {/* Max tokens slider */} -
- - -
+ Generation failed +
+ ) : nodeData.outputText ? ( +
+

+ {nodeData.outputText} +

+
+ + +
- )} -
+
+ ) : ( +
+ + Run to generate + +
+ )}
);