Browse Source

feat: add visual indicators for optional input nodes

- ImageInputNode: dashed border + "Optional" text when empty, badge when loaded
- AudioInputNode: same pattern as ImageInputNode
- PromptNode: updated placeholder text when isOptional is set

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
handoff-20260429-1057
shrimbly 4 months ago
parent
commit
5271c1559b
  1. 9
      src/components/nodes/AudioInputNode.tsx
  2. 9
      src/components/nodes/ImageInputNode.tsx
  3. 2
      src/components/nodes/PromptNode.tsx

9
src/components/nodes/AudioInputNode.tsx

@ -143,6 +143,11 @@ export function AudioInputNode({ id, data, selected }: NodeProps<AudioInputNodeT
{nodeData.audioFile ? ( {nodeData.audioFile ? (
<div className="relative group flex-1 flex flex-col min-h-0 gap-2"> <div className="relative group flex-1 flex flex-col min-h-0 gap-2">
{nodeData.isOptional && (
<span className="absolute top-1 left-1 z-10 text-[9px] font-medium text-neutral-300 bg-black/50 px-1.5 py-0.5 rounded">
Optional
</span>
)}
{/* Filename and duration */} {/* Filename and duration */}
<div className="flex items-center justify-between shrink-0"> <div className="flex items-center justify-between shrink-0">
<span className="text-[10px] text-neutral-400 truncate max-w-[150px]" title={nodeData.filename || ""}> <span className="text-[10px] text-neutral-400 truncate max-w-[150px]" title={nodeData.filename || ""}>
@ -223,13 +228,13 @@ 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 h-full bg-neutral-900/40 flex flex-col items-center justify-center cursor-pointer hover:bg-neutral-800/60 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 ${nodeData.isOptional ? "border-2 border-dashed border-neutral-600" : ""}`}
> >
<svg className="w-8 h-8 text-neutral-600" 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-xs text-neutral-500 mt-2"> <span className="text-xs text-neutral-500 mt-2">
Drop audio or click {nodeData.isOptional ? "Optional" : "Drop audio or click"}
</span> </span>
</div> </div>
)} )}

9
src/components/nodes/ImageInputNode.tsx

@ -106,6 +106,11 @@ export function ImageInputNode({ id, data, selected }: NodeProps<ImageInputNodeT
alt={nodeData.filename || "Uploaded image"} alt={nodeData.filename || "Uploaded image"}
className="w-full h-full object-cover rounded-lg" className="w-full h-full object-cover rounded-lg"
/> />
{nodeData.isOptional && (
<span className="absolute bottom-2 left-2 text-[9px] font-medium text-neutral-300 bg-black/50 px-1.5 py-0.5 rounded">
Optional
</span>
)}
<button <button
onClick={handleRemove} onClick={handleRemove}
aria-label="Remove image" aria-label="Remove image"
@ -130,12 +135,12 @@ export function ImageInputNode({ id, data, selected }: NodeProps<ImageInputNodeT
}} }}
onDrop={handleDrop} onDrop={handleDrop}
onDragOver={handleDragOver} onDragOver={handleDragOver}
className="w-full h-full bg-neutral-900/40 flex flex-col items-center justify-center cursor-pointer hover:bg-neutral-900/60 transition-colors" className={`w-full h-full bg-neutral-900/40 flex flex-col items-center justify-center cursor-pointer hover:bg-neutral-900/60 transition-colors ${nodeData.isOptional ? "border-2 border-dashed border-neutral-600" : ""}`}
> >
<svg className="w-8 h-8 text-neutral-600" 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="M3 16.5v2.25A2.25 2.25 0 005.25 21h13.5A2.25 2.25 0 0021 18.75V16.5m-13.5-9L12 3m0 0l4.5 4.5M12 3v13.5" /> <path strokeLinecap="round" strokeLinejoin="round" d="M3 16.5v2.25A2.25 2.25 0 005.25 21h13.5A2.25 2.25 0 0021 18.75V16.5m-13.5-9L12 3m0 0l4.5 4.5M12 3v13.5" />
</svg> </svg>
<span className="text-xs text-neutral-500 mt-2">Drop image</span> <span className="text-xs text-neutral-500 mt-2">{nodeData.isOptional ? "Optional" : "Drop image"}</span>
</div> </div>
)} )}

2
src/components/nodes/PromptNode.tsx

@ -109,7 +109,7 @@ export function PromptNode({ id, data, selected }: NodeProps<PromptNodeType>) {
onChange={handleChange} onChange={handleChange}
onFocus={handleFocus} onFocus={handleFocus}
onBlur={handleBlur} onBlur={handleBlur}
placeholder={hasIncomingTextConnection ? "Text from connected node (editable)..." : "Describe what to generate..."} placeholder={hasIncomingTextConnection ? "Text from connected node (editable)..." : nodeData.isOptional ? "Optional prompt (leave empty to skip)..." : "Describe what to generate..."}
className="nodrag nopan nowheel w-full h-full p-3 pb-7 text-xs leading-relaxed text-neutral-100 bg-neutral-800 rounded-t-lg resize-none focus:outline-none placeholder:text-neutral-500" className="nodrag nopan nowheel w-full h-full p-3 pb-7 text-xs leading-relaxed text-neutral-100 bg-neutral-800 rounded-t-lg resize-none focus:outline-none placeholder:text-neutral-500"
/> />
<div className="absolute bottom-0 left-0 right-0 z-10 px-3 py-1.5 bg-neutral-900/90 rounded-b-lg"> <div className="absolute bottom-0 left-0 right-0 z-10 px-3 py-1.5 bg-neutral-900/90 rounded-b-lg">

Loading…
Cancel
Save