Browse Source

feat(46-03): update 4 node files for full-bleed content (Annotation, Array, AudioInput, ConditionalSwitch)

- Remove header props (title, customTitle, comment, etc.) from BaseNode calls
- Apply full-bleed content pattern with contentClassName
- Replace dashed border empty states with subtle fills (bg-neutral-900/40)
- Move floating UI elements to absolute positioning

Part of batch update for all 21 remaining node files.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
handoff-20260429-1057
shrimbly 4 months ago
parent
commit
b02656e51e
  1. 27
      src/components/nodes/AnnotationNode.tsx
  2. 34
      src/components/nodes/ArrayNode.tsx
  3. 14
      src/components/nodes/AudioInputNode.tsx
  4. 19
      src/components/nodes/ConditionalSwitchNode.tsx

27
src/components/nodes/AnnotationNode.tsx

@ -3,7 +3,6 @@
import { useCallback, useRef } from "react"; import { useCallback, useRef } from "react";
import { Handle, Position, NodeProps, Node } from "@xyflow/react"; import { Handle, Position, NodeProps, Node } from "@xyflow/react";
import { BaseNode } from "./BaseNode"; import { BaseNode } from "./BaseNode";
import { useCommentNavigation } from "@/hooks/useCommentNavigation";
import { useAnnotationStore } from "@/store/annotationStore"; import { useAnnotationStore } from "@/store/annotationStore";
import { useWorkflowStore } from "@/store/workflowStore"; import { useWorkflowStore } from "@/store/workflowStore";
import { AnnotationNodeData } from "@/types"; import { AnnotationNodeData } from "@/types";
@ -12,7 +11,6 @@ type AnnotationNodeType = Node<AnnotationNodeData, "annotation">;
export function AnnotationNode({ id, data, selected }: NodeProps<AnnotationNodeType>) { export function AnnotationNode({ id, data, selected }: NodeProps<AnnotationNodeType>) {
const nodeData = data; const nodeData = data;
const commentNavigation = useCommentNavigation(id);
const openModal = useAnnotationStore((state) => state.openModal); const openModal = useAnnotationStore((state) => state.openModal);
const updateNodeData = useWorkflowStore((state) => state.updateNodeData); const updateNodeData = useWorkflowStore((state) => state.updateNodeData);
const fileInputRef = useRef<HTMLInputElement>(null); const fileInputRef = useRef<HTMLInputElement>(null);
@ -95,13 +93,8 @@ export function AnnotationNode({ id, data, selected }: NodeProps<AnnotationNodeT
return ( return (
<BaseNode <BaseNode
id={id} id={id}
title="Annotate"
customTitle={nodeData.customTitle}
comment={nodeData.comment}
onCustomTitleChange={(title) => updateNodeData(id, { customTitle: title || undefined })}
onCommentChange={(comment) => updateNodeData(id, { comment: comment || undefined })}
selected={selected} selected={selected}
commentNavigation={commentNavigation ?? undefined} contentClassName="flex-1 min-h-0 overflow-clip"
> >
<input <input
ref={fileInputRef} ref={fileInputRef}
@ -126,27 +119,27 @@ export function AnnotationNode({ id, data, selected }: NodeProps<AnnotationNodeT
{displayImage ? ( {displayImage ? (
<div <div
className="relative group cursor-pointer flex-1 flex flex-col min-h-0" className="relative group cursor-pointer w-full h-full"
onClick={handleEdit} onClick={handleEdit}
> >
<img <img
src={displayImage} src={displayImage}
alt="Annotated" alt="Annotated"
className="w-full flex-1 min-h-0 object-contain rounded" className="w-full h-full object-cover"
/> />
<button <button
onClick={(e) => { onClick={(e) => {
e.stopPropagation(); e.stopPropagation();
handleRemove(); handleRemove();
}} }}
className="absolute top-1 right-1 w-5 h-5 bg-black/60 text-white rounded text-xs opacity-0 group-hover:opacity-100 transition-opacity flex items-center justify-center" className="absolute top-2 right-2 w-6 h-6 bg-black/60 hover:bg-black/80 text-white rounded text-xs opacity-0 group-hover:opacity-100 transition-opacity flex items-center justify-center"
> >
<svg className="w-3 h-3" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}> <svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<path strokeLinecap="round" strokeLinejoin="round" d="M6 18L18 6M6 6l12 12" /> <path strokeLinecap="round" strokeLinejoin="round" d="M6 18L18 6M6 6l12 12" />
</svg> </svg>
</button> </button>
<div className="absolute inset-0 bg-black/0 group-hover:bg-black/10 transition-colors rounded flex items-center justify-center pointer-events-none"> <div className="absolute inset-0 bg-black/0 group-hover:bg-black/10 transition-colors flex items-center justify-center pointer-events-none">
<span className="text-[10px] font-medium text-white opacity-0 group-hover:opacity-100 transition-opacity bg-black/50 px-2 py-1 rounded"> <span className="text-xs font-medium text-white opacity-0 group-hover:opacity-100 transition-opacity bg-black/60 px-3 py-1.5 rounded">
{nodeData.annotations.length > 0 ? `Edit (${nodeData.annotations.length})` : "Add annotations"} {nodeData.annotations.length > 0 ? `Edit (${nodeData.annotations.length})` : "Add annotations"}
</span> </span>
</div> </div>
@ -156,12 +149,12 @@ export function AnnotationNode({ id, data, selected }: NodeProps<AnnotationNodeT
onClick={() => fileInputRef.current?.click()} onClick={() => fileInputRef.current?.click()}
onDrop={handleDrop} onDrop={handleDrop}
onDragOver={handleDragOver} onDragOver={handleDragOver}
className="w-full flex-1 min-h-[112px] border border-dashed border-neutral-600 rounded flex flex-col items-center justify-center cursor-pointer hover:border-neutral-500 hover:bg-neutral-700/50 transition-colors" className="w-full h-full bg-neutral-900/40 flex flex-col items-center justify-center cursor-pointer hover:bg-neutral-800/60 transition-colors"
> >
<svg className="w-5 h-5 text-neutral-500" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.5}> <svg className="w-8 h-8 text-neutral-600" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.5}>
<path strokeLinecap="round" strokeLinejoin="round" d="M12 4.5v15m7.5-7.5h-15" /> <path strokeLinecap="round" strokeLinejoin="round" d="M12 4.5v15m7.5-7.5h-15" />
</svg> </svg>
<span className="text-[10px] text-neutral-400 mt-1"> <span className="text-xs text-neutral-500 mt-2">
Drop, click, or connect Drop, click, or connect
</span> </span>
</div> </div>

34
src/components/nodes/ArrayNode.tsx

@ -3,7 +3,6 @@
import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { Handle, Node, NodeProps, Position, useReactFlow } from "@xyflow/react"; import { Handle, Node, NodeProps, Position, useReactFlow } from "@xyflow/react";
import { BaseNode } from "./BaseNode"; import { BaseNode } from "./BaseNode";
import { useCommentNavigation } from "@/hooks/useCommentNavigation";
import { useWorkflowStore } from "@/store/workflowStore"; import { useWorkflowStore } from "@/store/workflowStore";
import { ArrayNodeData } from "@/types"; import { ArrayNodeData } from "@/types";
import { getConnectedInputsPure } from "@/store/utils/connectedInputs"; import { getConnectedInputsPure } from "@/store/utils/connectedInputs";
@ -21,7 +20,6 @@ function arraysEqual(a: string[], b: string[]): boolean {
export function ArrayNode({ id, data, selected }: NodeProps<ArrayNodeType>) { export function ArrayNode({ id, data, selected }: NodeProps<ArrayNodeType>) {
const nodeData = data; const nodeData = data;
const commentNavigation = useCommentNavigation(id);
const updateNodeData = useWorkflowStore((state) => state.updateNodeData); const updateNodeData = useWorkflowStore((state) => state.updateNodeData);
const addNode = useWorkflowStore((state) => state.addNode); const addNode = useWorkflowStore((state) => state.addNode);
const onConnect = useWorkflowStore((state) => state.onConnect); const onConnect = useWorkflowStore((state) => state.onConnect);
@ -174,35 +172,29 @@ export function ArrayNode({ id, data, selected }: NodeProps<ArrayNodeType>) {
return ( return (
<BaseNode <BaseNode
id={id} id={id}
title="Array"
customTitle={nodeData.customTitle}
comment={nodeData.comment}
onCustomTitleChange={(title) => updateNodeData(id, { customTitle: title || undefined })}
onCommentChange={(comment) => updateNodeData(id, { comment: comment || undefined })}
selected={selected} selected={selected}
commentNavigation={commentNavigation ?? undefined}
hasError={!!nodeData.error} hasError={!!nodeData.error}
minWidth={320} minWidth={320}
minHeight={300} minHeight={300}
headerButtons={
<button
type="button"
onClick={handleAutoRouteToPrompts}
disabled={previewItems.length === 0}
className="nodrag nopan ml-2 shrink-0 p-1 rounded border border-neutral-600 text-neutral-300 hover:bg-neutral-700 hover:text-neutral-100 disabled:opacity-50 disabled:cursor-not-allowed"
title="Auto-route to Prompts"
>
<svg className="w-3.5 h-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<path strokeLinecap="round" strokeLinejoin="round" d="M6 3v6m0 0a2 2 0 104 0 2 2 0 00-4 0zm0 0v7a3 3 0 003 3h6m0 0a2 2 0 100 4 2 2 0 000-4zm0 0v-6a3 3 0 013-3h0" />
</svg>
</button>
}
> >
<Handle type="target" position={Position.Left} id="text" data-handletype="text" /> <Handle type="target" position={Position.Left} id="text" data-handletype="text" />
{/* Single text output point (each outgoing edge receives a separate item) */} {/* Single text output point (each outgoing edge receives a separate item) */}
<Handle type="source" position={Position.Right} id="text" data-handletype="text" style={{ top: 48 }} /> <Handle type="source" position={Position.Right} id="text" data-handletype="text" style={{ top: 48 }} />
{/* Auto-route button - floating absolute positioned */}
<button
type="button"
onClick={handleAutoRouteToPrompts}
disabled={previewItems.length === 0}
className="nodrag nopan absolute top-2 right-2 z-10 shrink-0 p-1 rounded border border-neutral-600 bg-neutral-800/90 text-neutral-300 hover:bg-neutral-700 hover:text-neutral-100 disabled:opacity-50 disabled:cursor-not-allowed"
title="Auto-route to Prompts"
>
<svg className="w-3.5 h-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<path strokeLinecap="round" strokeLinejoin="round" d="M6 3v6m0 0a2 2 0 104 0 2 2 0 00-4 0zm0 0v7a3 3 0 003 3h6m0 0a2 2 0 100 4 2 2 0 000-4zm0 0v-6a3 3 0 013-3h0" />
</svg>
</button>
<div className="flex flex-col gap-2 flex-1 min-h-0"> <div className="flex flex-col gap-2 flex-1 min-h-0">
<div className="grid grid-cols-[auto_1fr] gap-2 items-center"> <div className="grid grid-cols-[auto_1fr] gap-2 items-center">
<label className="text-[11px] text-neutral-400">Split</label> <label className="text-[11px] text-neutral-400">Split</label>

14
src/components/nodes/AudioInputNode.tsx

@ -3,7 +3,6 @@
import { useCallback, useRef, useState, useEffect } from "react"; import { useCallback, useRef, useState, useEffect } from "react";
import { Handle, Position, NodeProps, Node } from "@xyflow/react"; import { Handle, Position, NodeProps, Node } from "@xyflow/react";
import { BaseNode } from "./BaseNode"; import { BaseNode } from "./BaseNode";
import { useCommentNavigation } from "@/hooks/useCommentNavigation";
import { useWorkflowStore } from "@/store/workflowStore"; import { useWorkflowStore } from "@/store/workflowStore";
import { AudioInputNodeData } from "@/types"; import { AudioInputNodeData } from "@/types";
import { useAudioVisualization } from "@/hooks/useAudioVisualization"; import { useAudioVisualization } from "@/hooks/useAudioVisualization";
@ -13,7 +12,6 @@ type AudioInputNodeType = Node<AudioInputNodeData, "audioInput">;
export function AudioInputNode({ id, data, selected }: NodeProps<AudioInputNodeType>) { export function AudioInputNode({ id, data, selected }: NodeProps<AudioInputNodeType>) {
const nodeData = data; const nodeData = data;
const commentNavigation = useCommentNavigation(id);
const updateNodeData = useWorkflowStore((state) => state.updateNodeData); const updateNodeData = useWorkflowStore((state) => state.updateNodeData);
const fileInputRef = useRef<HTMLInputElement>(null); const fileInputRef = useRef<HTMLInputElement>(null);
@ -131,13 +129,7 @@ export function AudioInputNode({ id, data, selected }: NodeProps<AudioInputNodeT
return ( return (
<BaseNode <BaseNode
id={id} id={id}
title="Audio"
customTitle={nodeData.customTitle}
comment={nodeData.comment}
onCustomTitleChange={(title) => updateNodeData(id, { customTitle: title || undefined })}
onCommentChange={(comment) => updateNodeData(id, { comment: comment || undefined })}
selected={selected} selected={selected}
commentNavigation={commentNavigation ?? undefined}
minWidth={250} minWidth={250}
minHeight={150} minHeight={150}
> >
@ -231,12 +223,12 @@ export function AudioInputNode({ id, data, selected }: NodeProps<AudioInputNodeT
onClick={() => fileInputRef.current?.click()} onClick={() => fileInputRef.current?.click()}
onDrop={handleDrop} onDrop={handleDrop}
onDragOver={handleDragOver} onDragOver={handleDragOver}
className="w-full flex-1 min-h-[112px] border border-dashed border-neutral-600 rounded flex flex-col items-center justify-center cursor-pointer hover:border-neutral-500 hover:bg-neutral-700/50 transition-colors" className="w-full h-full bg-neutral-900/40 flex flex-col items-center justify-center cursor-pointer hover:bg-neutral-800/60 transition-colors"
> >
<svg className="w-6 h-6 text-neutral-500" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.5}> <svg className="w-8 h-8 text-neutral-600" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.5}>
<path strokeLinecap="round" strokeLinejoin="round" d="M9 9l10.5-3m0 6.553v3.75a2.25 2.25 0 01-1.632 2.163l-1.32.377a1.803 1.803 0 11-.99-3.467l2.31-.66a2.25 2.25 0 001.632-2.163zm0 0V2.25L9 5.25v10.303m0 0v3.75a2.25 2.25 0 01-1.632 2.163l-1.32.377a1.803 1.803 0 01-.99-3.467l2.31-.66A2.25 2.25 0 009 15.553z" /> <path strokeLinecap="round" strokeLinejoin="round" d="M9 9l10.5-3m0 6.553v3.75a2.25 2.25 0 01-1.632 2.163l-1.32.377a1.803 1.803 0 11-.99-3.467l2.31-.66a2.25 2.25 0 001.632-2.163zm0 0V2.25L9 5.25v10.303m0 0v3.75a2.25 2.25 0 01-1.632 2.163l-1.32.377a1.803 1.803 0 01-.99-3.467l2.31-.66A2.25 2.25 0 009 15.553z" />
</svg> </svg>
<span className="text-[10px] text-neutral-400 mt-1"> <span className="text-xs text-neutral-500 mt-2">
Drop audio or click Drop audio or click
</span> </span>
</div> </div>

19
src/components/nodes/ConditionalSwitchNode.tsx

@ -206,32 +206,29 @@ export const ConditionalSwitchNode = memo(({ id, data, selected }: NodeProps<Wor
return ( return (
<BaseNode <BaseNode
id={id} id={id}
title="Conditional Switch"
customTitle={nodeData.customTitle}
comment={nodeData.comment}
onCustomTitleChange={(customTitle) => updateNodeData(id, { customTitle })}
onCommentChange={(comment) => updateNodeData(id, { comment })}
selected={selected} selected={selected}
minWidth={260} minWidth={260}
minHeight={minHeight} minHeight={minHeight}
className="bg-teal-950/80 border-teal-600" className="bg-teal-950/80 border-teal-600"
headerButtons={showClearButton ? ( >
<div className="relative ml-2 shrink-0 group"> {/* Clear button - floating absolute positioned */}
{showClearButton && (
<div className="absolute top-2 right-2 z-10">
<button <button
onClick={handleClear} onClick={handleClear}
className="nodrag nopan p-0.5 rounded transition-all duration-200 ease-in-out flex items-center overflow-hidden group-hover:pr-2 text-neutral-500 group-hover:text-neutral-200 border border-neutral-600" className="nodrag nopan p-0.5 rounded transition-all duration-200 ease-in-out flex items-center overflow-hidden group pr-2 text-neutral-500 hover:text-neutral-200 border border-neutral-600 bg-neutral-800/90"
title="Clear evaluation" title="Clear evaluation"
> >
<svg className="w-3.5 h-3.5 flex-shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}> <svg className="w-3.5 h-3.5 flex-shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<path strokeLinecap="round" strokeLinejoin="round" d="M6 18L18 6M6 6l12 12" /> <path strokeLinecap="round" strokeLinejoin="round" d="M6 18L18 6M6 6l12 12" />
</svg> </svg>
<span className="text-[9px] whitespace-nowrap max-w-0 group-hover:max-w-[60px] transition-all duration-200 ease-in-out overflow-hidden opacity-0 group-hover:opacity-100"> <span className="text-[9px] whitespace-nowrap ml-1">
Clear Clear
</span> </span>
</button> </button>
</div> </div>
) : undefined} )}
>
{/* Input handle (left) - text only, aligned with header */} {/* Input handle (left) - text only, aligned with header */}
<Handle <Handle
type="target" type="target"

Loading…
Cancel
Save