fix: restore small handle labels and make Format node dynamic
- TypedHandle now shows tiny 8px labels next to handles (color-matched)
- Labels positioned outside the handle, no overlap with node content
- Format node dynamically generates input handles from template {0}, {1}, etc.
- NodeHandles accepts dynamicInputs/dynamicOutputs overrides
- isValidConnection treats dynamic handles (arg-*, option-*) as any-type
This commit is contained in:
parent
8ec0c194ff
commit
707562faaa
4 changed files with 68 additions and 24 deletions
|
|
@ -3,13 +3,30 @@
|
|||
import { type NodeProps } from "@xyflow/react";
|
||||
import { AlignLeftIcon } from "lucide-react";
|
||||
import { NodeHandles } from "./NodeHandles";
|
||||
import { NODE_SOCKETS, type SocketDef } from "./types";
|
||||
|
||||
function parseTemplateArgs(template: string): SocketDef[] {
|
||||
const matches = template.match(/\{(\d+)\}/g);
|
||||
if (!matches) return [];
|
||||
const unique = [...new Set(matches)].sort();
|
||||
return unique.map((m) => ({
|
||||
id: `arg-${m.slice(1, -1)}`,
|
||||
type: "any" as const,
|
||||
label: m,
|
||||
}));
|
||||
}
|
||||
|
||||
export function FormatNode({ data }: NodeProps) {
|
||||
const d = data as { template?: string };
|
||||
const template = d.template ?? "{0}";
|
||||
const dynamicInputs: SocketDef[] = [
|
||||
...(NODE_SOCKETS.format?.inputs.filter((s) => s.id === "template") ?? []),
|
||||
...parseTemplateArgs(template),
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="bg-sky-950/80 border border-sky-500/50 rounded-lg shadow-md min-w-40">
|
||||
<NodeHandles nodeType="format" />
|
||||
<NodeHandles nodeType="format" dynamicInputs={dynamicInputs} />
|
||||
<div className="flex items-center gap-2 px-3 py-2 border-b border-sky-500/30">
|
||||
<AlignLeftIcon className="size-4 text-sky-400 shrink-0" />
|
||||
<span className="text-sm font-medium text-sky-200 flex-1">
|
||||
|
|
@ -17,7 +34,7 @@ export function FormatNode({ data }: NodeProps) {
|
|||
</span>
|
||||
</div>
|
||||
<div className="px-3 py-2 text-xs text-sky-300/80 font-mono truncate">
|
||||
{d.template || "{0} {1}"}
|
||||
{template}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -119,10 +119,14 @@ export function NarrativeFlowEditor() {
|
|||
|
||||
const sourceSocket = sourceSockets.outputs.find((s) => s.id === connection.sourceHandle);
|
||||
const targetSocket = targetSockets.inputs.find((s) => s.id === connection.targetHandle);
|
||||
if (!sourceSocket || !targetSocket) return false;
|
||||
|
||||
if (sourceSocket.type === "any" || targetSocket.type === "any") return true;
|
||||
return sourceSocket.type === targetSocket.type;
|
||||
// Dynamic handles (arg-*, option-*) are treated as "any"
|
||||
const sourceType = sourceSocket?.type ?? (connection.sourceHandle.startsWith("arg-") || connection.sourceHandle.startsWith("option-") ? "any" : null);
|
||||
const targetType = targetSocket?.type ?? (connection.targetHandle.startsWith("arg-") || connection.targetHandle.startsWith("option-") ? "any" : null);
|
||||
|
||||
if (sourceType === null || targetType === null) return false;
|
||||
if (sourceType === "any" || targetType === "any") return true;
|
||||
return sourceType === targetType;
|
||||
},
|
||||
[nodes],
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,21 +1,20 @@
|
|||
"use client";
|
||||
|
||||
import { TypedHandle } from "./TypedHandle";
|
||||
import { NODE_SOCKETS } from "./types";
|
||||
import { NODE_SOCKETS, type SocketDef } from "./types";
|
||||
|
||||
interface NodeHandlesProps {
|
||||
nodeType: string;
|
||||
dynamicOutputs?: Array<{ id: string }>;
|
||||
dynamicInputs?: SocketDef[];
|
||||
dynamicOutputs?: SocketDef[];
|
||||
}
|
||||
|
||||
export function NodeHandles({ nodeType, dynamicOutputs }: NodeHandlesProps) {
|
||||
export function NodeHandles({ nodeType, dynamicInputs, dynamicOutputs }: NodeHandlesProps) {
|
||||
const config = NODE_SOCKETS[nodeType];
|
||||
if (!config) return null;
|
||||
|
||||
const inputs = config.inputs;
|
||||
const outputs = dynamicOutputs
|
||||
? dynamicOutputs.map((o) => ({ ...o, type: "flow" as const }))
|
||||
: config.outputs;
|
||||
const inputs = dynamicInputs ?? config.inputs;
|
||||
const outputs = dynamicOutputs ?? config.outputs;
|
||||
|
||||
const maxSlots = Math.max(inputs.length, outputs.length, 1);
|
||||
|
||||
|
|
@ -28,6 +27,7 @@ export function NodeHandles({ nodeType, dynamicOutputs }: NodeHandlesProps) {
|
|||
key={socket.id}
|
||||
id={socket.id}
|
||||
socketType={socket.type}
|
||||
label={socket.label}
|
||||
isInput={true}
|
||||
style={{ top: `${topPercent}%` }}
|
||||
/>
|
||||
|
|
@ -40,6 +40,7 @@ export function NodeHandles({ nodeType, dynamicOutputs }: NodeHandlesProps) {
|
|||
key={socket.id}
|
||||
id={socket.id}
|
||||
socketType={socket.type}
|
||||
label={socket.label}
|
||||
isInput={false}
|
||||
style={{ top: `${topPercent}%` }}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -7,26 +7,48 @@ interface TypedHandleProps {
|
|||
id: string;
|
||||
socketType: SocketType;
|
||||
isInput: boolean;
|
||||
label?: string;
|
||||
style?: React.CSSProperties;
|
||||
}
|
||||
|
||||
export function TypedHandle({ id, socketType, isInput, style }: TypedHandleProps) {
|
||||
export function TypedHandle({ id, socketType, isInput, label, style }: TypedHandleProps) {
|
||||
const color = SOCKET_COLORS[socketType];
|
||||
const isFlow = socketType === "flow";
|
||||
|
||||
return (
|
||||
<Handle
|
||||
type={isInput ? "target" : "source"}
|
||||
position={isInput ? Position.Left : Position.Right}
|
||||
id={id}
|
||||
<div
|
||||
className="absolute flex items-center gap-1"
|
||||
style={{
|
||||
background: color,
|
||||
width: isFlow ? 8 : 6,
|
||||
height: isFlow ? 8 : 6,
|
||||
borderRadius: isFlow ? 2 : "50%",
|
||||
border: `1.5px solid ${color}60`,
|
||||
...style,
|
||||
[isInput ? "left" : "right"]: -4,
|
||||
top: style?.top ?? "50%",
|
||||
transform: "translateY(-50%)",
|
||||
flexDirection: isInput ? "row" : "row-reverse",
|
||||
}}
|
||||
/>
|
||||
>
|
||||
<Handle
|
||||
type={isInput ? "target" : "source"}
|
||||
position={isInput ? Position.Left : Position.Right}
|
||||
id={id}
|
||||
style={{
|
||||
background: color,
|
||||
width: isFlow ? 8 : 6,
|
||||
height: isFlow ? 8 : 6,
|
||||
borderRadius: isFlow ? 2 : "50%",
|
||||
border: `1.5px solid ${color}60`,
|
||||
position: "relative",
|
||||
left: "auto",
|
||||
right: "auto",
|
||||
top: "auto",
|
||||
}}
|
||||
/>
|
||||
{label && (
|
||||
<span
|
||||
className="text-[8px] font-mono leading-none whitespace-nowrap pointer-events-none opacity-70"
|
||||
style={{ color }}
|
||||
>
|
||||
{label}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue