diff --git a/src/app/globals.css b/src/app/globals.css index d50c940a..b9d6da5f 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -65,14 +65,23 @@ body { right: -5px; } -/* Image handles - soft green */ +/* Image handles - cyan/teal */ .react-flow__handle[data-handletype="image"] { - background: #10b981; + background: #22d3ee; + border-color: #22d3ee; } -/* Prompt/text handles - soft blue */ +/* Text/prompt handles - magenta/pink */ +.react-flow__handle[data-handletype="text"], .react-flow__handle[data-handletype="prompt"] { - background: #3b82f6; + background: #d946ef; + border-color: #d946ef; +} + +/* CSS variables for handle colors (used by labels) */ +:root { + --handle-color-image: #22d3ee; + --handle-color-text: #d946ef; } .react-flow__edge-path { diff --git a/src/components/nodes/GenerateImageNode.tsx b/src/components/nodes/GenerateImageNode.tsx index 24ade0f3..847a6189 100644 --- a/src/components/nodes/GenerateImageNode.tsx +++ b/src/components/nodes/GenerateImageNode.tsx @@ -337,32 +337,49 @@ export function GenerateImageNode({ id, data, selected }: NodeProps {/* Dynamic input handles based on model schema (external providers only) */} {!isGeminiProvider && nodeData.inputSchema && nodeData.inputSchema.length > 0 ? ( - // Render handles from schema - nodeData.inputSchema.map((input, index) => { - const total = nodeData.inputSchema!.length; - const topPercent = ((index + 1) / (total + 1)) * 100; - return ( -
- - {/* Handle label */} -
- {input.label} - {input.required && *} + // Render handles from schema, sorted by type (images first, text second) + (() => { + const imageInputs = nodeData.inputSchema!.filter(i => i.type === "image"); + const textInputs = nodeData.inputSchema!.filter(i => i.type === "text"); + const sortedInputs = [...imageInputs, ...textInputs]; + + // Calculate positions with gap between image and text groups + const imageCount = imageInputs.length; + const textCount = textInputs.length; + const totalSlots = imageCount + textCount + (imageCount > 0 && textCount > 0 ? 1 : 0); // +1 for gap + + return sortedInputs.map((input, index) => { + // Add 1 to index for text inputs if there are image inputs (to account for gap) + const adjustedIndex = input.type === "text" && imageCount > 0 ? index + 1 : index; + const topPercent = ((adjustedIndex + 1) / (totalSlots + 1)) * 100; + const isImage = input.type === "image"; + + return ( +
+ + {/* Handle label - positioned outside node, colored to match handle */} +
+ {input.label} +
-
- ); - }) + ); + }); + })() ) : ( // Default handles for Gemini or when no schema <> @@ -374,6 +391,17 @@ export function GenerateImageNode({ id, data, selected }: NodeProps + {/* Default image label */} +
+ Image +
+ {/* Default text label */} +
+ Prompt +
)} {/* Image output */} @@ -388,8 +427,20 @@ export function GenerateImageNode({ id, data, selected }: NodeProps + {/* Output label */} +
+ Image +
{/* Preview area */} diff --git a/src/components/nodes/GenerateVideoNode.tsx b/src/components/nodes/GenerateVideoNode.tsx index 240bd5f5..4033dc6b 100644 --- a/src/components/nodes/GenerateVideoNode.tsx +++ b/src/components/nodes/GenerateVideoNode.tsx @@ -172,32 +172,49 @@ export function GenerateVideoNode({ id, data, selected }: NodeProps {/* Dynamic input handles based on model schema */} {nodeData.inputSchema && nodeData.inputSchema.length > 0 ? ( - // Render handles from schema - nodeData.inputSchema.map((input, index) => { - const total = nodeData.inputSchema!.length; - const topPercent = ((index + 1) / (total + 1)) * 100; - return ( -
- - {/* Handle label */} -
- {input.label} - {input.required && *} + // Render handles from schema, sorted by type (images first, text second) + (() => { + const imageInputs = nodeData.inputSchema!.filter(i => i.type === "image"); + const textInputs = nodeData.inputSchema!.filter(i => i.type === "text"); + const sortedInputs = [...imageInputs, ...textInputs]; + + // Calculate positions with gap between image and text groups + const imageCount = imageInputs.length; + const textCount = textInputs.length; + const totalSlots = imageCount + textCount + (imageCount > 0 && textCount > 0 ? 1 : 0); // +1 for gap + + return sortedInputs.map((input, index) => { + // Add 1 to index for text inputs if there are image inputs (to account for gap) + const adjustedIndex = input.type === "text" && imageCount > 0 ? index + 1 : index; + const topPercent = ((adjustedIndex + 1) / (totalSlots + 1)) * 100; + const isImage = input.type === "image"; + + return ( +
+ + {/* Handle label - positioned outside node, colored to match handle */} +
+ {input.label} +
-
- ); - }) + ); + }); + })() ) : ( // Default handles when no schema <> @@ -209,6 +226,17 @@ export function GenerateVideoNode({ id, data, selected }: NodeProps + {/* Default image label */} +
+ Image +
+ {/* Default text label */} +
+ Prompt +
)} {/* Video output */} @@ -223,8 +262,20 @@ export function GenerateVideoNode({ id, data, selected }: NodeProps + {/* Output label */} +
+ Video +
{/* Preview area */}