Replace static pixel math with useLayoutEffect + offsetTop measurement to
position output handles at the actual vertical center of their corresponding
rule/default rows. This is robust against header height, padding, and text
preview variations that caused the previous static offset to misalign.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Resolve three issues with the Conditional Switch node:
- Add fromHandleType resolution for conditionalSwitch in handleConnectEnd so
the connection drop menu appears when dragging from rule/default outputs
- Align output handles with visual rule rows by accounting for the text preview
height in baseOffset and using fixed h-8 rows matching handleSpacing=32
- Move "Add Rule" button after the Default row so the default handle aligns
with its visual row instead of being displaced
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add geminiApiKey to useProviderApiKeys hook. Update GenerateVideoNode to
include Gemini as a provider when API key is configured, and pass the
X-Gemini-API-Key header when fetching models.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add Anthropic as a third LLM provider with Claude Opus 4.6, Sonnet 4.5,
and Haiku 4.5 models. Extend Google provider with Gemini 3.1 Pro.
Includes full API route support with multimodal content blocks,
provider settings UI, temperature clamping (Anthropic max 1.0),
header builder plumbing, and comprehensive test coverage.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Nano Banana Pro supports 1K/2K/4K, while Nano Banana 2 also supports 512.
Split the shared RESOLUTIONS constant into RESOLUTIONS_PRO and RESOLUTIONS_NB2,
and use the correct list based on the selected model in both GenerateImageNode
and SplitGridSettingsModal. Also normalize aspect ratio and resolution to valid
defaults when switching models in the SplitGrid modal.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Nano Banana 2 (gemini-3.1-flash-image) supports a new searchTypes API
that allows enabling web search and image search independently. This adds
a useImageSearch toggle visible only for Nano Banana 2, while keeping the
existing Google Search toggle using the legacy format for Nano Banana Pro.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Make aspect ratio dropdowns model-aware: nano-banana-2 shows 14 ratios
while other models keep showing the base 10. Also add nano-banana-2 to
the SplitGrid settings model list.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add Nano Banana 2 entry to GEMINI_IMAGE_MODELS in model registry
- Add model to GenerateImageNode dropdown with 512px resolution option
- Extend resolution/Google Search gates to support nano-banana-2
- Replace hardcoded model name ternaries with MODEL_DISPLAY_NAMES lookup
in workflowStore, nodeDefaults, and GenerateImageNode migration code
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Text input handle on left (blue, text type)
- Text preview section showing truncated incoming text (50 chars)
- Rule rows with:
- Match status indicator (green checkmark when matched, gray dot when not)
- Up/down arrow buttons for reordering rules
- Editable label (double-click to edit, inline editing pattern from SwitchNode)
- Mode dropdown (exact, contains, starts-with, ends-with)
- Value text field (comma-separated match values)
- Delete button (hidden if only one rule to prevent empty state)
- Add rule button (creates new rule with auto-generated label)
- Default output row (always at bottom, not editable/deletable)
- Active (green) when NO rules match, inactive (gray) otherwise
- Dynamic output handles per rule + default handle (all text type)
- Non-matching rule handles at 30% opacity (same as Switch disabled pattern)
- evaluateRule function: case-insensitive, multi-value OR logic, all 4 modes
- Match status updates reactively via useEffect watching incoming text
- Dynamic handle sync via useUpdateNodeInternals on rule count change
- Auto-resize height based on rule count
- Teal/cyan theme (bg-teal-950/50 border-teal-600) to differentiate from Router/Switch
- Add MatchMode type (exact, contains, starts-with, ends-with)
- Add ConditionalSwitchRule interface with id, value, mode, label, isMatched
- Add ConditionalSwitchNodeData interface with incomingText and rules array
- Add conditionalSwitch to NodeType union (after switch)
- Add ConditionalSwitchNodeData to WorkflowNodeData union
- Add default data with single rule (mode: contains, empty value)
- Add default dimensions (260x180, wider than Switch for mode dropdown)
- Add executeConditionalSwitch passthrough executor
- Add conditionalSwitch to VALID_NODE_TYPES in validation
- Export ConditionalSwitchNode from components index (component created in next task)
The menu path calls onConnect() directly, bypassing handleConnect's
resolveSwitchHandle. This left edges with targetHandle "generic-input"
instead of the actual type, causing inputType to be invalid, handles
to render without color, and downstream connections to fail validation.
- Set targetHandleId to actual type in handleMenuSelect Switch case
- Update Switch inputType immediately when created from menu
- Filter "generic-input" in derivedInputType as safety net
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Create SwitchNode.tsx with violet theme (bg-violet-950/50, border-violet-600)
- Implement toggle controls per switch (sliding toggle with peer-checked styling)
- Add/remove switch management with + button and delete X button on hover
- Double-click to rename switch labels with inline editing
- Dynamic input handle: generic gray dashed when no connection, typed colored when connected
- Dynamic output handles: only shown when inputType is set (input connected)
- Output handles use 30% opacity when switch is disabled
- Auto-resize height based on switch count using useEffect and setNodes
- Handle color mapping matches RouterNode's HANDLE_COLORS
- useUpdateNodeInternals called when switch count or inputType changes
- Remove edges when switch is deleted via setEdges filter
- Component is 277 lines with full toggle, add, delete, rename functionality
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add "switch" to NodeType union in types/nodes.ts
- Create SwitchNodeData interface with inputType and switches array
- Add SwitchNodeData to WorkflowNodeData union
- Register switch dimensions (220x120) in nodeDefaults.ts and validation.ts
- Add createDefaultNodeData case with single default switch
- Implement executeSwitch passthrough executor in simpleNodeExecutors.ts
- Add SwitchNode export to components/nodes/index.ts
- Add switch dimensions to WorkflowCanvas.tsx default dimensions map
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Raise baseOffset from 20 to 38 so handles clear the header bar
- Calculate minHeight from total handle slots (active + placeholder)
- Programmatically grow node height via setNodes when handles are added
- Respects user-resized heights (only grows, never shrinks)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Make input placeholder grey with solid border matching other handles,
and remove the unnecessary output-side placeholder connector.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Image and text colors were swapped, 3d was cyan instead of orange,
video/audio/easeCurve used incorrect colors. Now matches globals.css
and inline styles used by other node components.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
ConnectionDropMenu.tsx:
- Add Router entry to all 8 connection drop menu arrays
- Router appears as last item (utility node) in each menu
- Uses git-branch SVG icon for routing/branching concept
workflowStore.ts:
- Import executeRouter from execution module
- Add "router" case to executeWorkflow switch (after easeCurve)
- Add "router" case to regenerateNode switch (after easeCurve)
execution/index.ts:
- Export executeRouter from simpleNodeExecutors
connectedInputs.ts:
- Add optional visited parameter to getConnectedInputsPure for circular protection
- Add Router passthrough logic before getSourceOutput call
- Router nodes recursively fetch upstream data and route by handle type
- Prevents infinite loops with visited Set tracking
- Handle all 6 data types: image, text, video, audio, 3d, easeCurve
RouterNode.tsx (bugfix):
- Add missing id prop to BaseNode (from 43-01)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Create RouterNode.tsx with dynamic handle arrays derived from edges
- Implement color-coded typed handles (blue, emerald, purple, amber, cyan, red)
- Add gray dashed generic handles when not all 6 types are connected
- Use useUpdateNodeInternals to sync handle positions on type changes
- Implement compact strip BaseNode layout with dynamic height adjustment
- Add activeInputTypes useMemo watching edges for handle derivation
- Display type count in node body (or 'Drop connections here' hint)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add 'router' to NodeType union in types/nodes.ts
- Add RouterNodeData interface (extends BaseNodeData, no internal state)
- Add RouterNodeData to WorkflowNodeData union
- Add router dimensions (200x80) to nodeDefaults.ts and validation.ts
- Add router default data (empty object) to both files
- Add executeRouter pure passthrough function to simpleNodeExecutors.ts
- Add RouterNode export to components/nodes/index.ts
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add early return in setNodes when height unchanged to prevent unnecessary updates
- Rename shadowed 'selected' variable to 'currentSelection' in validation useEffect
- Reduces redundant React Flow re-renders when array items don't change height
- Replace broad `state.edges` subscription with count-only selector scoped to this
node's target edges, preventing unnecessary re-renders from unrelated edge changes
- Merge two useEffect hooks into one with a null sentinel ref to prevent false
regeneration on mount when loading saved workflows with existing connections
- Pass isExecuting={isRunning} to BaseNode so the Run button grays out during execution
- Update test mock to include edges, regenerateNode, and isRunning
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Output Node now automatically triggers execution when a new connection is made
- Added Run button to Output Node header for manual triggering
- Updated regenerateNode to support output node type
- Prevents re-triggering on initial mount or disconnections using edge count tracking
Resolves issue where Output Node would not display data from already-generated upstream nodes unless connected before generation.
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
The thumbnail extraction effect had three issues compounding on weak
hardware:
1. The onseeked promise had no timeout — if seek hangs on systems
without hardware video decoding, the entire effect blocks
indefinitely. Added 10-second Promise.race timeout.
2. Created video elements were never cleaned up after use, leaving
phantom decoders running. Added cleanupVideo() that nulls handlers,
clears src, and calls load().
3. The thumbnail cache was cleared wholesale on every run
(thumbnailsRef.current = new Map()), making the cache check on
the next line always miss. Replaced with fingerprint-based
invalidation: only clips whose video data actually changed get
re-extracted, reusing cached thumbnails for unchanged clips.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Data URLs force Chrome to re-parse base64 on each access. With autoPlay
loop, this happens continuously and can freeze the main thread on systems
with weak GPUs that fall back to software video decoding.
Add useVideoBlobUrl hook that converts data URLs to blob URLs via
fetch->blob->createObjectURL. Returns the data URL immediately as
fallback to avoid flicker, then swaps to blob URL once ready (~50ms).
Properly revokes blob URLs on input change and unmount.
Applied to all 6 <video> elements across 5 components:
- GenerateVideoNode (outputVideo)
- VideoStitchNode (outputVideo)
- OutputNode (contentSrc, inline + lightbox)
- EaseCurveNode (outputVideo)
- VideoTrimNode (previewUrl)
Store data is unaffected — getConnectedInputs, executors, and
imageStorage all read outputVideo from the Zustand store, not from
the rendering layer.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Instead of mutating selectedOutputIndex N times in a loop (which races
with React renders), pass { arrayItemIndex: index } as an edge data
override through onConnect. Adds optional edgeDataOverrides parameter
to onConnect and addEdgeWithType.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The clipKey dependency used a presence-only check ("1"/"0"), so when a
video re-generated (truthy→truthy), the effect never re-ran. Additionally,
the ref-based cache keyed by edgeId returned stale thumbnails even if the
effect did fire. Fix by fingerprinting video data via its trailing 20 chars
and clearing the thumbnail cache when clipKey changes.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The end slider's full-width invisible body (higher z-index) was intercepting
all pointer events, preventing the start slider from receiving input. Fix by
setting pointer-events: none on both range inputs and pointer-events: all on
only the thumb pseudo-elements. Swap z-index so the start handle is on top
when both thumbs overlap at position 0.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add path traversal guard to workflow-images GET handler (matching POST handler)
- Preserve stored prompt in generateAudioExecutor when connected text is null
- Reset conflicting isVideo/isAudio flags in kie.ts content-type detection
- Fix Infinity aspect ratio causing zero-height canvas in VideoStitchNode
- Check response content-type before trusting isAudioModel in fal.ts
- Move audio feeding inside inner try block in useStitchVideos for proper cleanup
- Add 30s seek timeout to frame extraction in videoProcessingExecutors
- Surface file-open errors via toast in Generate3DNode instead of failing silently
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Extract shared video-probing module (constants, helpers, probeVideoMetadata)
from useTrimVideo and useStitchVideos into src/lib/video-probing.ts
- Fix audio interleaving in useTrimVideo to feed audio before video frames,
matching useStitchVideos fix for Discord playback compatibility
- Fix blob URL leaks in VideoTrimNode and videoProcessingExecutors
- Fix nanoBananaExecutor: null coercion for non-string prompts, redact PII
in console.warn, remove duplicate dynamicInputs.prompt
- Fix generateAudioExecutor double updateNodeData on error paths
- Fix fal.ts media fetch ordering for 3D model detection
- Fix kie.ts and replicate.ts content-type detection for audio models
- Fix WorkflowCanvas connection drop menu wiring for generateAudio nodes
- Fix open-file route Windows path quoting
- Add path traversal protection to workflow-images route
- Add AudioInputNode onerror handler for metadata loading failures
- Fix useAudioPlayback uncaught play() promise rejection
- Add generateAudio case to regenerateNode in workflowStore
- Update placeholder Discord links to real invite URL
- Extract ProviderBadge into shared component in Generate3DNode
- Fix VideoTrimNode duration detection with proper cleanup and cancellation
- Fix GenerateAudioNode test fetch mock cleanup
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add generateAudio and videoFrameGrab cases to executeNode dispatcher
- Add selectedModel null guards in route.ts for all non-Gemini providers
- Fix ConnectionDropMenu wrap-around test (5->6 items after generateAudio)
- Fix audio duration edge cases (NaN guard in formatTime, truthiness check)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Register VideoFrameGrabNode in node exports, nodeTypes map, and minimap colors
- Add to ConnectionDropMenu as video target (Frame Grab) and image source
- Add executeVideoFrameGrab executor with canvas-based frame extraction
- Wire into all three execution paths: executeWorkflow, regenerateNode, executeSelectedNodes
- Add video connection validation for videoFrameGrab target type
- Add handle mapping for video->videoFrameGrab and videoFrameGrab->image connections
- Add videoFrameGrab case to quickstart validation defaults
- Video input handle (left, purple) and image output handle (right, blue)
- First/Last frame position toggle with output clearing on toggle change
- Extract Frame button triggers regenerateNode
- Image preview area with clear button overlay
- Processing spinner overlay and error display
- BaseNode wrapper with title, comment, and keyboard run support
1. Add "videoTrim" to source type check in orderedClips so VideoTrim
node outputs are recognized and thumbnails are extracted
2. Generate thumbnails at source video's native aspect ratio instead
of fixed 160x120 (4:3) to prevent distortion
3. Change thumbnail display from object-cover to object-contain to
prevent cropping/distortion when the node is resized
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Export VideoTrimNode from src/components/nodes/index.ts
- Register videoTrim in nodeTypes map in WorkflowCanvas.tsx
- Add videoTrim to getNodeHandles (video in, video out)
- Add videoTrim to isValidConnection video target list
- Add videoTrim connection drop handling (video input and output)
- Add videoTrim minimap color: blue-400 (#60a5fa)
- Add VideoTrim to VIDEO_TARGET_OPTIONS in ConnectionDropMenu (scissors icon)
- Add VideoTrim to VIDEO_SOURCE_OPTIONS in ConnectionDropMenu (scissors icon)
- Dual range sliders with visual timeline scrubber for start/end times
- Inline video preview with play/pause controls (shows source or trimmed)
- Source/Trimmed toggle when output is available
- Time labels: start, duration, end (M:SS.s format)
- Trim button (disabled when no source, loading, or invalid range)
- Processing overlay with progress percentage
- Error display for processing failures
- Encoder check states (not supported / checking)
- Auto-detects video duration on connect and sets endTime
Deduplicate 6 patterns (drawWaveform, ResizeObserver, redraw effect,
animation loop, handlePlayPause, handleSeek) that were 100% identical
between AudioInputNode and GenerateAudioNode into a shared hook.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Use local state during editing and sync to store on blur, preventing
React Flow re-renders from resetting cursor position on every keystroke.
Also add click-to-seek on GenerateAudioNode waveform.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The waveform container was missing an onClick handler, preventing
scrubbing. Added handleSeek using audioRef.current.duration directly.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Both GenerateAudioNode and AudioInputNode relied on nodeData.duration
for the waveform progress calculation, but this was often null (never
set by the audio executor). Using audioRef.current.duration directly
avoids the store roundtrip and works as soon as audio metadata loads.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>