refactor(recipe-studio): simplify aux node graph logic and remove dead handle/sync code
This commit is contained in:
parent
48f7d40e87
commit
dd3e1e7293
14 changed files with 426 additions and 459 deletions
|
|
@ -4,19 +4,15 @@ import { Input } from "@/components/ui/input";
|
|||
import { Textarea } from "@/components/ui/textarea";
|
||||
import {
|
||||
Handle,
|
||||
NodeResizer,
|
||||
Position,
|
||||
type Node,
|
||||
type NodeProps,
|
||||
useUpdateNodeInternals,
|
||||
} from "@xyflow/react";
|
||||
import { memo, type ReactElement, useEffect } from "react";
|
||||
import { MAX_NODE_WIDTH, MIN_NODE_WIDTH } from "../constants";
|
||||
import { useRecipeStudioStore } from "../stores/recipe-studio";
|
||||
import type { LayoutDirection, LlmConfig, Score, ScoreOption } from "../types";
|
||||
import {
|
||||
AUX_HANDLE_CLASS,
|
||||
getAuxSourceHandlePosition,
|
||||
} from "../utils/handle-layout";
|
||||
import type { LlmConfig, Score, ScoreOption } from "../types";
|
||||
import { AUX_HANDLE_CLASS } from "../utils/handle-layout";
|
||||
import { HANDLE_IDS } from "../utils/handles";
|
||||
import { getAvailableVariableEntries } from "../utils/variables";
|
||||
import { BaseNode, BaseNodeContent, BaseNodeHeader, BaseNodeHeaderTitle } from "./rf-ui/base-node";
|
||||
|
|
@ -28,14 +24,12 @@ type PromptInputNodeData = {
|
|||
llmId: string;
|
||||
field: PromptField;
|
||||
title: string;
|
||||
layoutDirection: LayoutDirection;
|
||||
};
|
||||
|
||||
type JudgeScoreNodeData = {
|
||||
kind: "llm-judge-score";
|
||||
llmId: string;
|
||||
scoreIndex: number;
|
||||
layoutDirection: LayoutDirection;
|
||||
};
|
||||
|
||||
export type RecipeGraphAuxNodeData = PromptInputNodeData | JudgeScoreNodeData;
|
||||
|
|
@ -104,24 +98,47 @@ function AuxNodeBase({
|
|||
return null;
|
||||
}
|
||||
|
||||
const sourcePosition = getAuxSourceHandlePosition(data.layoutDirection);
|
||||
const sourceHandles = (
|
||||
<>
|
||||
<Handle
|
||||
id={HANDLE_IDS.llmInputOutLeft}
|
||||
type="source"
|
||||
position={Position.Left}
|
||||
isConnectable={false}
|
||||
isConnectableStart={false}
|
||||
className={AUX_HANDLE_CLASS}
|
||||
/>
|
||||
<Handle
|
||||
id={HANDLE_IDS.llmInputOutRight}
|
||||
type="source"
|
||||
position={Position.Right}
|
||||
isConnectable={false}
|
||||
isConnectableStart={false}
|
||||
className={AUX_HANDLE_CLASS}
|
||||
/>
|
||||
<Handle
|
||||
id={HANDLE_IDS.llmInputOutTop}
|
||||
type="source"
|
||||
position={Position.Top}
|
||||
isConnectable={false}
|
||||
isConnectableStart={false}
|
||||
className={AUX_HANDLE_CLASS}
|
||||
/>
|
||||
<Handle
|
||||
id={HANDLE_IDS.llmInputOutBottom}
|
||||
type="source"
|
||||
position={Position.Bottom}
|
||||
isConnectable={false}
|
||||
isConnectableStart={false}
|
||||
className={AUX_HANDLE_CLASS}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
||||
if (data.kind === "llm-prompt-input") {
|
||||
const value = data.field === "prompt" ? config.prompt : config.system_prompt;
|
||||
return (
|
||||
<BaseNode className="corner-squircle w-full min-w-0 rounded-lg border-border/60 bg-card shadow-sm">
|
||||
<NodeResizer
|
||||
isVisible={true}
|
||||
minWidth={MIN_NODE_WIDTH}
|
||||
minHeight={120}
|
||||
maxWidth={MAX_NODE_WIDTH}
|
||||
maxHeight={520}
|
||||
color="var(--primary)"
|
||||
lineClassName="!border-transparent !shadow-none"
|
||||
lineStyle={{ opacity: 0 }}
|
||||
handleClassName="!h-3 !w-3 !border-transparent !bg-transparent"
|
||||
handleStyle={{ opacity: 0 }}
|
||||
/>
|
||||
<BaseNodeHeader className="border-b border-border/50 px-3 py-2">
|
||||
<BaseNodeHeaderTitle className="text-xs">{data.title}</BaseNodeHeaderTitle>
|
||||
</BaseNodeHeader>
|
||||
|
|
@ -137,14 +154,7 @@ function AuxNodeBase({
|
|||
/>
|
||||
<AuxVariableBadges llmId={data.llmId} />
|
||||
</BaseNodeContent>
|
||||
<Handle
|
||||
id={HANDLE_IDS.llmInputOut}
|
||||
type="source"
|
||||
position={sourcePosition}
|
||||
isConnectable={false}
|
||||
isConnectableStart={false}
|
||||
className={AUX_HANDLE_CLASS}
|
||||
/>
|
||||
{sourceHandles}
|
||||
</BaseNode>
|
||||
);
|
||||
}
|
||||
|
|
@ -190,18 +200,6 @@ function AuxNodeBase({
|
|||
|
||||
return (
|
||||
<BaseNode className="corner-squircle w-full min-w-0 rounded-lg border-border/60 bg-card shadow-sm">
|
||||
<NodeResizer
|
||||
isVisible={true}
|
||||
minWidth={MIN_NODE_WIDTH}
|
||||
minHeight={120}
|
||||
maxWidth={MAX_NODE_WIDTH}
|
||||
maxHeight={640}
|
||||
color="var(--primary)"
|
||||
lineClassName="!border-transparent !shadow-none"
|
||||
lineStyle={{ opacity: 0 }}
|
||||
handleClassName="!h-3 !w-3 !border-transparent !bg-transparent"
|
||||
handleStyle={{ opacity: 0 }}
|
||||
/>
|
||||
<BaseNodeHeader className="border-b border-border/50 px-3 py-2">
|
||||
<BaseNodeHeaderTitle className="text-xs">
|
||||
{score.name.trim() || `Scorer ${data.scoreIndex + 1}`}
|
||||
|
|
@ -260,14 +258,7 @@ function AuxNodeBase({
|
|||
</Button>
|
||||
</div>
|
||||
</BaseNodeContent>
|
||||
<Handle
|
||||
id={HANDLE_IDS.llmInputOut}
|
||||
type="source"
|
||||
position={sourcePosition}
|
||||
isConnectable={false}
|
||||
isConnectableStart={false}
|
||||
className={AUX_HANDLE_CLASS}
|
||||
/>
|
||||
{sourceHandles}
|
||||
</BaseNode>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -20,7 +20,6 @@ import {
|
|||
} from "@hugeicons/core-free-icons";
|
||||
import { HugeiconsIcon } from "@hugeicons/react";
|
||||
import {
|
||||
Handle,
|
||||
NodeResizer,
|
||||
Position,
|
||||
useUpdateNodeInternals,
|
||||
|
|
@ -32,12 +31,11 @@ import { useRecipeStudioStore } from "../stores/recipe-studio";
|
|||
import type {
|
||||
RecipeNode as RecipeGraphNodeType,
|
||||
LlmType,
|
||||
LayoutDirection,
|
||||
NodeConfig,
|
||||
SamplerType,
|
||||
} from "../types";
|
||||
import { NODE_HANDLE_CLASS } from "../utils/handle-layout";
|
||||
import { getLlmJudgeScoreHandleId, HANDLE_IDS } from "../utils/handles";
|
||||
import { HANDLE_IDS } from "../utils/handles";
|
||||
import { InlineCategoryBadges } from "./inline/inline-category-badges";
|
||||
import { InlineExpression } from "./inline/inline-expression";
|
||||
import { InlineLlm } from "./inline/inline-llm";
|
||||
|
|
@ -268,89 +266,6 @@ function renderNodeBody(
|
|||
return <p className="text-xs text-muted-foreground">{summary}</p>;
|
||||
}
|
||||
|
||||
type LlmInputHandleItem = {
|
||||
id: string;
|
||||
label: string;
|
||||
};
|
||||
|
||||
function getLlmInputHandleItems(config: NodeConfig | undefined): LlmInputHandleItem[] {
|
||||
if (!(config && config.kind === "llm")) {
|
||||
return [];
|
||||
}
|
||||
const items: LlmInputHandleItem[] = [];
|
||||
if (config.system_prompt.trim()) {
|
||||
items.push({ id: HANDLE_IDS.llmSystemIn, label: "System" });
|
||||
}
|
||||
if (config.prompt.trim()) {
|
||||
items.push({ id: HANDLE_IDS.llmPromptIn, label: "Prompt" });
|
||||
}
|
||||
if (config.llm_type === "judge") {
|
||||
(config.scores ?? []).forEach((score, index) => {
|
||||
items.push({
|
||||
id: getLlmJudgeScoreHandleId(index),
|
||||
label: score.name.trim() || `Score ${index + 1}`,
|
||||
});
|
||||
});
|
||||
}
|
||||
return items;
|
||||
}
|
||||
|
||||
type LlmInputHandlesProps = {
|
||||
items: LlmInputHandleItem[];
|
||||
layoutDirection: LayoutDirection;
|
||||
};
|
||||
|
||||
function LlmInputHandles({
|
||||
items,
|
||||
layoutDirection,
|
||||
}: LlmInputHandlesProps): ReactElement | null {
|
||||
if (items.length === 0) {
|
||||
return null;
|
||||
}
|
||||
const isTopBottom = layoutDirection === "TB";
|
||||
|
||||
if (isTopBottom) {
|
||||
return (
|
||||
<div className="flex flex-wrap gap-2 pb-1">
|
||||
{items.map((item) => (
|
||||
<div
|
||||
key={item.id}
|
||||
className="pointer-events-none relative flex min-w-[80px] flex-1 justify-center pt-2"
|
||||
>
|
||||
<Handle
|
||||
id={item.id}
|
||||
type="target"
|
||||
position={Position.Top}
|
||||
className={NODE_HANDLE_CLASS}
|
||||
style={{ left: "50%", top: 0, transform: "translate(-50%, -50%)" }}
|
||||
/>
|
||||
<span className="text-[10px] text-muted-foreground">{item.label}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-1 pb-1">
|
||||
{items.map((item) => (
|
||||
<div key={item.id} className="pointer-events-none relative min-w-0 pl-3">
|
||||
<Handle
|
||||
id={item.id}
|
||||
type="target"
|
||||
position={Position.Left}
|
||||
className={NODE_HANDLE_CLASS}
|
||||
style={{ left: -3, top: "50%", transform: "translate(-50%, -50%)" }}
|
||||
/>
|
||||
<span className="block truncate text-[10px] text-muted-foreground">
|
||||
{item.label}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function RecipeGraphNodeBase({
|
||||
id,
|
||||
data,
|
||||
|
|
@ -418,7 +333,6 @@ function RecipeGraphNodeBase({
|
|||
data.kind === "model_config" || data.kind === "model_provider";
|
||||
const summary = getConfigSummary(config);
|
||||
const nodeBody = renderNodeBody(config, summary, updateConfig);
|
||||
const llmInputHandles = llmAuxVisible ? getLlmInputHandleItems(config) : [];
|
||||
const canShowLlmAux =
|
||||
config?.kind === "llm" &&
|
||||
(Boolean(config.prompt.trim()) ||
|
||||
|
|
@ -491,7 +405,6 @@ function RecipeGraphNodeBase({
|
|||
</BaseNodeHeader>
|
||||
|
||||
<BaseNodeContent className="gap-2 px-3 py-2">
|
||||
<LlmInputHandles items={llmInputHandles} layoutDirection={layoutDirection} />
|
||||
{nodeBody}
|
||||
</BaseNodeContent>
|
||||
|
||||
|
|
@ -506,6 +419,15 @@ function RecipeGraphNodeBase({
|
|||
labelClassName="sr-only"
|
||||
handleClassName={NODE_HANDLE_CLASS}
|
||||
/>
|
||||
<LabeledHandle
|
||||
id={HANDLE_IDS.dataOutLeft}
|
||||
title="Data output"
|
||||
type="source"
|
||||
position={Position.Left}
|
||||
className="absolute inset-0 pointer-events-none opacity-0"
|
||||
labelClassName="sr-only"
|
||||
handleClassName={NODE_HANDLE_CLASS}
|
||||
/>
|
||||
<LabeledHandle
|
||||
id={HANDLE_IDS.dataInTop}
|
||||
title="Data input"
|
||||
|
|
@ -515,6 +437,15 @@ function RecipeGraphNodeBase({
|
|||
labelClassName="sr-only"
|
||||
handleClassName={NODE_HANDLE_CLASS}
|
||||
/>
|
||||
<LabeledHandle
|
||||
id={HANDLE_IDS.dataOutTop}
|
||||
title="Data output"
|
||||
type="source"
|
||||
position={Position.Top}
|
||||
className="absolute inset-0 pointer-events-none opacity-0"
|
||||
labelClassName="sr-only"
|
||||
handleClassName={NODE_HANDLE_CLASS}
|
||||
/>
|
||||
<LabeledHandle
|
||||
id={HANDLE_IDS.dataOut}
|
||||
title="Data output"
|
||||
|
|
@ -524,6 +455,15 @@ function RecipeGraphNodeBase({
|
|||
labelClassName="sr-only"
|
||||
handleClassName={NODE_HANDLE_CLASS}
|
||||
/>
|
||||
<LabeledHandle
|
||||
id={HANDLE_IDS.dataInRight}
|
||||
title="Data input"
|
||||
type="target"
|
||||
position={Position.Right}
|
||||
className="absolute inset-0 pointer-events-none opacity-0"
|
||||
labelClassName="sr-only"
|
||||
handleClassName={NODE_HANDLE_CLASS}
|
||||
/>
|
||||
<LabeledHandle
|
||||
id={HANDLE_IDS.dataOutBottom}
|
||||
title="Data output"
|
||||
|
|
@ -533,6 +473,15 @@ function RecipeGraphNodeBase({
|
|||
labelClassName="sr-only"
|
||||
handleClassName={NODE_HANDLE_CLASS}
|
||||
/>
|
||||
<LabeledHandle
|
||||
id={HANDLE_IDS.dataInBottom}
|
||||
title="Data input"
|
||||
type="target"
|
||||
position={Position.Bottom}
|
||||
className="absolute inset-0 pointer-events-none opacity-0"
|
||||
labelClassName="sr-only"
|
||||
handleClassName={NODE_HANDLE_CLASS}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
|
|
|
|||
|
|
@ -94,7 +94,6 @@ export function RecipeStudioPage({
|
|||
nodes,
|
||||
edges,
|
||||
auxNodePositions,
|
||||
auxNodeSizes,
|
||||
llmAuxVisibility,
|
||||
configs,
|
||||
processors,
|
||||
|
|
@ -125,15 +124,11 @@ export function RecipeStudioPage({
|
|||
setLayoutDirection,
|
||||
applyLayout,
|
||||
setAuxNodePosition,
|
||||
setAuxNodeSize,
|
||||
syncAuxNodePositions,
|
||||
syncAuxNodeSizes,
|
||||
} = useRecipeStudioStore(
|
||||
useShallow((state) => ({
|
||||
nodes: state.nodes,
|
||||
edges: state.edges,
|
||||
auxNodePositions: state.auxNodePositions,
|
||||
auxNodeSizes: state.auxNodeSizes,
|
||||
llmAuxVisibility: state.llmAuxVisibility,
|
||||
configs: state.configs,
|
||||
processors: state.processors,
|
||||
|
|
@ -164,9 +159,6 @@ export function RecipeStudioPage({
|
|||
setLayoutDirection: state.setLayoutDirection,
|
||||
applyLayout: state.applyLayout,
|
||||
setAuxNodePosition: state.setAuxNodePosition,
|
||||
setAuxNodeSize: state.setAuxNodeSize,
|
||||
syncAuxNodePositions: state.syncAuxNodePositions,
|
||||
syncAuxNodeSizes: state.syncAuxNodeSizes,
|
||||
})),
|
||||
);
|
||||
const [sheetContainer, setSheetContainer] = useState<HTMLDivElement | null>(
|
||||
|
|
@ -202,12 +194,10 @@ export function RecipeStudioPage({
|
|||
configs,
|
||||
layoutDirection,
|
||||
auxNodePositions,
|
||||
auxNodeSizes,
|
||||
llmAuxVisibility,
|
||||
});
|
||||
}, [
|
||||
auxNodePositions,
|
||||
auxNodeSizes,
|
||||
configs,
|
||||
edges,
|
||||
layoutDirection,
|
||||
|
|
@ -218,12 +208,6 @@ export function RecipeStudioPage({
|
|||
() => displayGraph.nodes.map((node) => node.id),
|
||||
[displayGraph.nodes],
|
||||
);
|
||||
useEffect(() => {
|
||||
syncAuxNodePositions(displayGraph.auxNodeIds, displayGraph.auxDefaults);
|
||||
}, [displayGraph.auxDefaults, displayGraph.auxNodeIds, syncAuxNodePositions]);
|
||||
useEffect(() => {
|
||||
syncAuxNodeSizes(displayGraph.auxNodeIds);
|
||||
}, [displayGraph.auxNodeIds, syncAuxNodeSizes]);
|
||||
|
||||
const handleNodeClick = useCallback(
|
||||
(_: unknown, node: Node<RecipeNodeData | RecipeGraphAuxNodeData>) => {
|
||||
|
|
@ -250,7 +234,7 @@ export function RecipeStudioPage({
|
|||
|
||||
const handleNodesChange = useCallback(
|
||||
(changes: NodeChange<Node<RecipeNodeData | RecipeGraphAuxNodeData>>[]) => {
|
||||
applyAuxNodeChanges(changes, { setAuxNodePosition, setAuxNodeSize });
|
||||
applyAuxNodeChanges(changes, { setAuxNodePosition });
|
||||
const next = filterNodeChangesByIds(
|
||||
changes as NodeChange<RecipeBuilderNode>[],
|
||||
baseNodeIds,
|
||||
|
|
@ -259,7 +243,7 @@ export function RecipeStudioPage({
|
|||
onNodesChange(next);
|
||||
}
|
||||
},
|
||||
[baseNodeIds, onNodesChange, setAuxNodePosition, setAuxNodeSize],
|
||||
[baseNodeIds, onNodesChange, setAuxNodePosition],
|
||||
);
|
||||
|
||||
const handleEdgesChange = useCallback(
|
||||
|
|
@ -288,8 +272,16 @@ export function RecipeStudioPage({
|
|||
}, []);
|
||||
|
||||
const payloadResult = useMemo(
|
||||
() => buildRecipePayload(configs, nodes, edges, processors, layoutDirection),
|
||||
[configs, edges, layoutDirection, nodes, processors],
|
||||
() =>
|
||||
buildRecipePayload(
|
||||
configs,
|
||||
nodes,
|
||||
edges,
|
||||
processors,
|
||||
layoutDirection,
|
||||
auxNodePositions,
|
||||
),
|
||||
[auxNodePositions, configs, edges, layoutDirection, nodes, processors],
|
||||
);
|
||||
const getCurrentPayloadFromStore = useCallback((): RecipePayload => {
|
||||
const state = useRecipeStudioStore.getState();
|
||||
|
|
@ -299,6 +291,7 @@ export function RecipeStudioPage({
|
|||
state.edges,
|
||||
state.processors,
|
||||
state.layoutDirection,
|
||||
state.auxNodePositions,
|
||||
).payload;
|
||||
}, []);
|
||||
const {
|
||||
|
|
|
|||
|
|
@ -1,61 +0,0 @@
|
|||
import type { XYPosition } from "@xyflow/react";
|
||||
|
||||
export function syncPositionsRecord(
|
||||
prev: Record<string, XYPosition>,
|
||||
activeIds: string[],
|
||||
defaults: Record<string, XYPosition>,
|
||||
): Record<string, XYPosition> {
|
||||
const next: Record<string, XYPosition> = {};
|
||||
for (const id of activeIds) {
|
||||
const existing = prev[id];
|
||||
if (existing) {
|
||||
next[id] = existing;
|
||||
continue;
|
||||
}
|
||||
const fallback = defaults[id];
|
||||
if (fallback) {
|
||||
next[id] = fallback;
|
||||
}
|
||||
}
|
||||
|
||||
const prevIds = Object.keys(prev);
|
||||
const nextIds = Object.keys(next);
|
||||
if (prevIds.length !== nextIds.length) {
|
||||
return next;
|
||||
}
|
||||
for (const id of nextIds) {
|
||||
const a = prev[id];
|
||||
const b = next[id];
|
||||
if (!(a && b && a.x === b.x && a.y === b.y)) {
|
||||
return next;
|
||||
}
|
||||
}
|
||||
return prev;
|
||||
}
|
||||
|
||||
export function syncSizesRecord(
|
||||
prev: Record<string, { width: number; height: number }>,
|
||||
activeIds: string[],
|
||||
): Record<string, { width: number; height: number }> {
|
||||
const active = new Set(activeIds);
|
||||
const next: Record<string, { width: number; height: number }> = {};
|
||||
for (const [id, size] of Object.entries(prev)) {
|
||||
if (active.has(id)) {
|
||||
next[id] = size;
|
||||
}
|
||||
}
|
||||
|
||||
const prevIds = Object.keys(prev);
|
||||
const nextIds = Object.keys(next);
|
||||
if (prevIds.length !== nextIds.length) {
|
||||
return next;
|
||||
}
|
||||
for (const id of nextIds) {
|
||||
const a = prev[id];
|
||||
const b = next[id];
|
||||
if (!(a && b && a.width === b.width && a.height === b.height)) {
|
||||
return next;
|
||||
}
|
||||
}
|
||||
return prev;
|
||||
}
|
||||
|
|
@ -29,7 +29,6 @@ import { applyRecipeConnection, isValidRecipeConnection } from "../utils/graph";
|
|||
import { HANDLE_IDS, remapRecipeEdgeHandlesForLayout } from "../utils/handles";
|
||||
import type { RecipeSnapshot } from "../utils/import";
|
||||
import { getLayoutedElements } from "../utils/layout";
|
||||
import { syncPositionsRecord, syncSizesRecord } from "./helpers/aux-sync";
|
||||
import { applyEdgeRemovals, applyNodeRemovals } from "./helpers/removals";
|
||||
import {
|
||||
applyRenameToConfigs,
|
||||
|
|
@ -53,7 +52,6 @@ type RecipeStudioState = {
|
|||
nodes: RecipeNode[];
|
||||
edges: Edge[];
|
||||
auxNodePositions: Record<string, XYPosition>;
|
||||
auxNodeSizes: Record<string, { width: number; height: number }>;
|
||||
llmAuxVisibility: Record<string, boolean>;
|
||||
configs: Record<string, NodeConfig>;
|
||||
processors: RecipeProcessorConfig[];
|
||||
|
|
@ -83,15 +81,6 @@ type RecipeStudioState = {
|
|||
updateConfig: (id: string, patch: Partial<NodeConfig>) => void;
|
||||
loadRecipe: (snapshot: RecipeSnapshot) => void;
|
||||
setAuxNodePosition: (id: string, position: XYPosition) => void;
|
||||
setAuxNodeSize: (
|
||||
id: string,
|
||||
size: { width: number; height: number },
|
||||
) => void;
|
||||
syncAuxNodePositions: (
|
||||
activeIds: string[],
|
||||
defaults: Record<string, XYPosition>,
|
||||
) => void;
|
||||
syncAuxNodeSizes: (activeIds: string[]) => void;
|
||||
onNodesChange: (changes: NodeChange<RecipeNode>[]) => void;
|
||||
onEdgesChange: (changes: EdgeChange<Edge>[]) => void;
|
||||
onConnect: (connection: Connection) => void;
|
||||
|
|
@ -102,7 +91,6 @@ const INITIAL_STATE = {
|
|||
nodes: [],
|
||||
edges: [],
|
||||
auxNodePositions: {},
|
||||
auxNodeSizes: {},
|
||||
llmAuxVisibility: {},
|
||||
configs: {},
|
||||
processors: [],
|
||||
|
|
@ -118,7 +106,6 @@ const INITIAL_STATE = {
|
|||
| "nodes"
|
||||
| "edges"
|
||||
| "auxNodePositions"
|
||||
| "auxNodeSizes"
|
||||
| "llmAuxVisibility"
|
||||
| "configs"
|
||||
| "processors"
|
||||
|
|
@ -248,8 +235,7 @@ export const useRecipeStudioStore = create<RecipeStudioState>((set, get) => ({
|
|||
edges: state.edges,
|
||||
configs: state.configs,
|
||||
layoutDirection: state.layoutDirection,
|
||||
auxNodePositions: state.auxNodePositions,
|
||||
auxNodeSizes: state.auxNodeSizes,
|
||||
auxNodePositions: {},
|
||||
llmAuxVisibility: state.llmAuxVisibility,
|
||||
});
|
||||
const { nodes } = getLayoutedElements(displayGraph.nodes, displayGraph.edges, {
|
||||
|
|
@ -267,25 +253,8 @@ export const useRecipeStudioStore = create<RecipeStudioState>((set, get) => ({
|
|||
}
|
||||
return { ...node, position };
|
||||
});
|
||||
const nextAuxNodePositions: Record<string, XYPosition> = {};
|
||||
for (const auxId of displayGraph.auxNodeIds) {
|
||||
const existing = state.auxNodePositions[auxId];
|
||||
const layouted = layoutedPositions.get(auxId);
|
||||
if (layouted) {
|
||||
nextAuxNodePositions[auxId] = layouted;
|
||||
continue;
|
||||
}
|
||||
if (existing) {
|
||||
nextAuxNodePositions[auxId] = existing;
|
||||
continue;
|
||||
}
|
||||
const fallback = displayGraph.auxDefaults[auxId];
|
||||
if (fallback) {
|
||||
nextAuxNodePositions[auxId] = fallback;
|
||||
}
|
||||
}
|
||||
return {
|
||||
auxNodePositions: nextAuxNodePositions,
|
||||
auxNodePositions: {},
|
||||
nodes: applyLayoutDirectionToNodes(
|
||||
nextNodes,
|
||||
state.configs,
|
||||
|
|
@ -461,8 +430,7 @@ export const useRecipeStudioStore = create<RecipeStudioState>((set, get) => ({
|
|||
layoutDirection: snapshot.layoutDirection,
|
||||
nextId: snapshot.nextId,
|
||||
nextY: snapshot.nextY,
|
||||
auxNodePositions: {},
|
||||
auxNodeSizes: {},
|
||||
auxNodePositions: snapshot.auxNodePositions ?? {},
|
||||
llmAuxVisibility: {},
|
||||
activeConfigId: null,
|
||||
dialogOpen: false,
|
||||
|
|
@ -482,36 +450,6 @@ export const useRecipeStudioStore = create<RecipeStudioState>((set, get) => ({
|
|||
},
|
||||
};
|
||||
}),
|
||||
setAuxNodeSize: (id, size) =>
|
||||
set((state) => {
|
||||
const width = Math.max(1, size.width);
|
||||
const height = Math.max(1, size.height);
|
||||
const current = state.auxNodeSizes[id];
|
||||
if (current && current.width === width && current.height === height) {
|
||||
return state;
|
||||
}
|
||||
return {
|
||||
auxNodeSizes: {
|
||||
...state.auxNodeSizes,
|
||||
[id]: { width, height },
|
||||
},
|
||||
};
|
||||
}),
|
||||
syncAuxNodePositions: (activeIds, defaults) =>
|
||||
set((state) => {
|
||||
const next = syncPositionsRecord(state.auxNodePositions, activeIds, defaults);
|
||||
if (next === state.auxNodePositions) {
|
||||
return state;
|
||||
}
|
||||
return {
|
||||
auxNodePositions: next,
|
||||
};
|
||||
}),
|
||||
syncAuxNodeSizes: (activeIds) =>
|
||||
set((state) => {
|
||||
const next = syncSizesRecord(state.auxNodeSizes, activeIds);
|
||||
return next === state.auxNodeSizes ? state : { auxNodeSizes: next };
|
||||
}),
|
||||
updateConfig: (id, patch) => {
|
||||
const applyUpdate = (state: RecipeStudioState) => {
|
||||
const current = state.configs[id];
|
||||
|
|
|
|||
|
|
@ -7,7 +7,6 @@ import {
|
|||
getDefaultDataTargetHandle,
|
||||
getDefaultSemanticSourceHandle,
|
||||
getDefaultSemanticTargetHandle,
|
||||
getLlmJudgeScoreHandleId,
|
||||
HANDLE_IDS,
|
||||
isDataSourceHandle,
|
||||
isDataTargetHandle,
|
||||
|
|
@ -24,15 +23,12 @@ type DisplayGraphInput = {
|
|||
configs: Record<string, NodeConfig>;
|
||||
layoutDirection: LayoutDirection;
|
||||
auxNodePositions: Record<string, XYPosition>;
|
||||
auxNodeSizes: Record<string, { width: number; height: number }>;
|
||||
llmAuxVisibility: Record<string, boolean>;
|
||||
};
|
||||
|
||||
export type DisplayGraph = {
|
||||
nodes: Array<Node<RecipeNode["data"] | RecipeGraphAuxNodeData>>;
|
||||
edges: Edge[];
|
||||
auxNodeIds: string[];
|
||||
auxDefaults: Record<string, XYPosition>;
|
||||
};
|
||||
|
||||
function normalizeEdge(
|
||||
|
|
@ -99,7 +95,6 @@ function normalizeEdge(
|
|||
|
||||
type AuxNodeItem = {
|
||||
key: string;
|
||||
targetHandle: string;
|
||||
data: RecipeGraphAuxNodeData;
|
||||
};
|
||||
|
||||
|
|
@ -136,42 +131,235 @@ function findNonOverlappingPosition(
|
|||
preferred: XYPosition,
|
||||
width: number,
|
||||
height: number,
|
||||
direction: LayoutDirection,
|
||||
occupied: Rect[],
|
||||
): XYPosition {
|
||||
const primaryStep =
|
||||
direction === "TB"
|
||||
? { x: 0, y: -(height + 24) }
|
||||
: { x: -(width + 24), y: 0 };
|
||||
const lateralUnit =
|
||||
direction === "TB"
|
||||
? { x: Math.max(48, Math.round(width * 0.3)), y: 0 }
|
||||
: { x: 0, y: Math.max(40, Math.round(height * 0.35)) };
|
||||
const lateralPattern = [0, 1, -1, 2, -2];
|
||||
|
||||
for (let ring = 0; ring <= 8; ring += 1) {
|
||||
for (const lateral of lateralPattern) {
|
||||
const candidate = {
|
||||
x: preferred.x + primaryStep.x * ring + lateralUnit.x * lateral,
|
||||
y: preferred.y + primaryStep.y * ring + lateralUnit.y * lateral,
|
||||
};
|
||||
const rect = toRect(candidate, width, height);
|
||||
if (!occupied.some((other) => intersects(rect, other))) {
|
||||
return candidate;
|
||||
const step = 24;
|
||||
for (let ring = 0; ring <= 10; ring += 1) {
|
||||
for (let dx = -ring; dx <= ring; dx += 1) {
|
||||
for (let dy = -ring; dy <= ring; dy += 1) {
|
||||
if (ring > 0 && Math.max(Math.abs(dx), Math.abs(dy)) !== ring) {
|
||||
continue;
|
||||
}
|
||||
const candidate = {
|
||||
x: preferred.x + dx * step,
|
||||
y: preferred.y + dy * step,
|
||||
};
|
||||
const rect = toRect(candidate, width, height);
|
||||
if (!occupied.some((other) => intersects(rect, other))) {
|
||||
return candidate;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return preferred;
|
||||
}
|
||||
|
||||
type HandleSide = "left" | "right" | "top" | "bottom";
|
||||
|
||||
const SIDE_TO_TARGET_HANDLE: Record<HandleSide, string> = {
|
||||
left: HANDLE_IDS.dataIn,
|
||||
right: HANDLE_IDS.dataInRight,
|
||||
top: HANDLE_IDS.dataInTop,
|
||||
bottom: HANDLE_IDS.dataInBottom,
|
||||
};
|
||||
|
||||
function getTargetSide(
|
||||
handleId: string | null | undefined,
|
||||
direction: LayoutDirection,
|
||||
): HandleSide {
|
||||
const normalized = normalizeRecipeHandleId(handleId);
|
||||
if (!normalized) {
|
||||
return direction === "TB" ? "top" : "left";
|
||||
}
|
||||
if (
|
||||
normalized === HANDLE_IDS.dataInRight ||
|
||||
normalized === HANDLE_IDS.semanticInRight
|
||||
) {
|
||||
return "right";
|
||||
}
|
||||
if (
|
||||
normalized === HANDLE_IDS.dataInBottom ||
|
||||
normalized === HANDLE_IDS.semanticInBottom
|
||||
) {
|
||||
return "bottom";
|
||||
}
|
||||
if (
|
||||
normalized === HANDLE_IDS.dataInTop ||
|
||||
normalized === HANDLE_IDS.semanticInTop
|
||||
) {
|
||||
return "top";
|
||||
}
|
||||
return "left";
|
||||
}
|
||||
|
||||
function getSourceSide(
|
||||
handleId: string | null | undefined,
|
||||
direction: LayoutDirection,
|
||||
): HandleSide {
|
||||
const normalized = normalizeRecipeHandleId(handleId);
|
||||
if (!normalized) {
|
||||
return direction === "TB" ? "bottom" : "right";
|
||||
}
|
||||
if (
|
||||
normalized === HANDLE_IDS.dataOutLeft ||
|
||||
normalized === HANDLE_IDS.semanticOutLeft
|
||||
) {
|
||||
return "left";
|
||||
}
|
||||
if (
|
||||
normalized === HANDLE_IDS.dataOutTop ||
|
||||
normalized === HANDLE_IDS.semanticOutTop
|
||||
) {
|
||||
return "top";
|
||||
}
|
||||
if (
|
||||
normalized === HANDLE_IDS.dataOutBottom ||
|
||||
normalized === HANDLE_IDS.semanticOutBottom
|
||||
) {
|
||||
return "bottom";
|
||||
}
|
||||
return "right";
|
||||
}
|
||||
|
||||
function pickAuxTargetHandle(
|
||||
llmId: string,
|
||||
direction: LayoutDirection,
|
||||
edges: Edge[],
|
||||
): string {
|
||||
const occupied = new Set<HandleSide>();
|
||||
for (const edge of edges) {
|
||||
if (edge.source.startsWith("aux-") || edge.target.startsWith("aux-")) {
|
||||
continue;
|
||||
}
|
||||
if (edge.target === llmId) {
|
||||
occupied.add(getTargetSide(edge.targetHandle, direction));
|
||||
}
|
||||
if (edge.source === llmId) {
|
||||
occupied.add(getSourceSide(edge.sourceHandle, direction));
|
||||
}
|
||||
}
|
||||
|
||||
const priority: HandleSide[] =
|
||||
direction === "LR"
|
||||
? ["left", "right", "bottom", "top"]
|
||||
: ["top", "bottom", "right", "left"];
|
||||
for (const side of priority) {
|
||||
if (!occupied.has(side)) {
|
||||
return SIDE_TO_TARGET_HANDLE[side];
|
||||
}
|
||||
}
|
||||
|
||||
const fallback: HandleSide = direction === "LR" ? "bottom" : "right";
|
||||
return SIDE_TO_TARGET_HANDLE[fallback];
|
||||
}
|
||||
|
||||
function getHandleSideFromTargetHandle(targetHandle: string): HandleSide {
|
||||
if (targetHandle === HANDLE_IDS.dataInRight) {
|
||||
return "right";
|
||||
}
|
||||
if (targetHandle === HANDLE_IDS.dataInTop) {
|
||||
return "top";
|
||||
}
|
||||
if (targetHandle === HANDLE_IDS.dataInBottom) {
|
||||
return "bottom";
|
||||
}
|
||||
return "left";
|
||||
}
|
||||
|
||||
function pickAuxSourceHandle(
|
||||
auxPosition: XYPosition,
|
||||
auxWidth: number,
|
||||
auxHeight: number,
|
||||
llmPosition: XYPosition,
|
||||
llmWidth: number,
|
||||
llmHeight: number,
|
||||
): string {
|
||||
const auxCenter = {
|
||||
x: auxPosition.x + auxWidth / 2,
|
||||
y: auxPosition.y + auxHeight / 2,
|
||||
};
|
||||
const llmCenter = {
|
||||
x: llmPosition.x + llmWidth / 2,
|
||||
y: llmPosition.y + llmHeight / 2,
|
||||
};
|
||||
const dx = llmCenter.x - auxCenter.x;
|
||||
const dy = llmCenter.y - auxCenter.y;
|
||||
|
||||
if (Math.abs(dx) >= Math.abs(dy)) {
|
||||
return dx >= 0 ? HANDLE_IDS.llmInputOutRight : HANDLE_IDS.llmInputOutLeft;
|
||||
}
|
||||
return dy >= 0 ? HANDLE_IDS.llmInputOutBottom : HANDLE_IDS.llmInputOutTop;
|
||||
}
|
||||
|
||||
type AppendAuxNodeAndEdgeInput = {
|
||||
auxNodes: Node<RecipeGraphAuxNodeData>[];
|
||||
auxEdges: Edge[];
|
||||
entry: {
|
||||
item: AuxNodeItem;
|
||||
auxId: string;
|
||||
width: number;
|
||||
height: number;
|
||||
};
|
||||
position: XYPosition;
|
||||
parentNode: Node<RecipeNode["data"] | RecipeGraphAuxNodeData>;
|
||||
parentWidth: number;
|
||||
parentHeight: number;
|
||||
auxTargetHandle: string;
|
||||
};
|
||||
|
||||
function appendAuxNodeAndEdge({
|
||||
auxNodes,
|
||||
auxEdges,
|
||||
entry,
|
||||
position,
|
||||
parentNode,
|
||||
parentWidth,
|
||||
parentHeight,
|
||||
auxTargetHandle,
|
||||
}: AppendAuxNodeAndEdgeInput): void {
|
||||
auxNodes.push({
|
||||
id: entry.auxId,
|
||||
type: "aux",
|
||||
data: entry.item.data,
|
||||
position,
|
||||
width: entry.width,
|
||||
height: entry.height,
|
||||
style: {
|
||||
width: entry.width,
|
||||
height: entry.height,
|
||||
},
|
||||
draggable: true,
|
||||
selectable: true,
|
||||
focusable: true,
|
||||
connectable: false,
|
||||
});
|
||||
|
||||
auxEdges.push({
|
||||
id: `e-${entry.auxId}-${parentNode.id}`,
|
||||
source: entry.auxId,
|
||||
sourceHandle: pickAuxSourceHandle(
|
||||
position,
|
||||
entry.width,
|
||||
entry.height,
|
||||
parentNode.position,
|
||||
parentWidth,
|
||||
parentHeight,
|
||||
),
|
||||
target: parentNode.id,
|
||||
targetHandle: auxTargetHandle,
|
||||
type: "canvas",
|
||||
data: { path: "auto" },
|
||||
selectable: false,
|
||||
focusable: false,
|
||||
});
|
||||
}
|
||||
|
||||
export function deriveDisplayGraph({
|
||||
nodes,
|
||||
edges,
|
||||
configs,
|
||||
layoutDirection,
|
||||
auxNodePositions,
|
||||
auxNodeSizes,
|
||||
llmAuxVisibility,
|
||||
}: DisplayGraphInput): DisplayGraph {
|
||||
const displayNodes = nodes.map((node) => {
|
||||
|
|
@ -190,8 +378,6 @@ export function deriveDisplayGraph({
|
|||
});
|
||||
const auxNodes: Node<RecipeGraphAuxNodeData>[] = [];
|
||||
const auxEdges: Edge[] = [];
|
||||
const auxDefaults: Record<string, XYPosition> = {};
|
||||
const auxNodeIds: string[] = [];
|
||||
const occupiedRects: Rect[] = displayNodes.map((node) =>
|
||||
toRect(
|
||||
node.position,
|
||||
|
|
@ -209,18 +395,18 @@ export function deriveDisplayGraph({
|
|||
continue;
|
||||
}
|
||||
const llmDirection = node.data.layoutDirection ?? layoutDirection;
|
||||
const auxTargetHandle = pickAuxTargetHandle(node.id, llmDirection, edges);
|
||||
const auxTargetSide = getHandleSideFromTargetHandle(auxTargetHandle);
|
||||
const items: AuxNodeItem[] = [];
|
||||
|
||||
if (config.system_prompt.trim()) {
|
||||
items.push({
|
||||
key: "system",
|
||||
targetHandle: HANDLE_IDS.llmSystemIn,
|
||||
data: {
|
||||
kind: "llm-prompt-input",
|
||||
llmId: config.id,
|
||||
field: "system_prompt",
|
||||
title: "System Prompt",
|
||||
layoutDirection: llmDirection,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
|
@ -228,13 +414,11 @@ export function deriveDisplayGraph({
|
|||
if (config.prompt.trim()) {
|
||||
items.push({
|
||||
key: "prompt",
|
||||
targetHandle: HANDLE_IDS.llmPromptIn,
|
||||
data: {
|
||||
kind: "llm-prompt-input",
|
||||
llmId: config.id,
|
||||
field: "prompt",
|
||||
title: "Prompt",
|
||||
layoutDirection: llmDirection,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
|
@ -243,12 +427,10 @@ export function deriveDisplayGraph({
|
|||
(config.scores ?? []).forEach((_score, scoreIndex) => {
|
||||
items.push({
|
||||
key: `score-${scoreIndex}`,
|
||||
targetHandle: getLlmJudgeScoreHandleId(scoreIndex),
|
||||
data: {
|
||||
kind: "llm-judge-score",
|
||||
llmId: config.id,
|
||||
scoreIndex,
|
||||
layoutDirection: llmDirection,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
|
@ -262,19 +444,20 @@ export function deriveDisplayGraph({
|
|||
const parentHeight = readNodeHeight(node) ?? DEFAULT_NODE_HEIGHT;
|
||||
const itemsWithLayout = items.map((item) => {
|
||||
const auxId = `aux-${node.id}-${item.key}`;
|
||||
const savedSize = auxNodeSizes[auxId];
|
||||
return {
|
||||
item,
|
||||
auxId,
|
||||
width: savedSize?.width ?? DEFAULT_NODE_WIDTH,
|
||||
height: savedSize?.height ?? DEFAULT_NODE_HEIGHT,
|
||||
width: DEFAULT_NODE_WIDTH,
|
||||
height: DEFAULT_NODE_HEIGHT,
|
||||
};
|
||||
});
|
||||
|
||||
const gap = 24;
|
||||
const sideOffset = 48;
|
||||
const stackHorizontal =
|
||||
auxTargetSide === "top" || auxTargetSide === "bottom";
|
||||
|
||||
if (llmDirection === "TB") {
|
||||
if (stackHorizontal) {
|
||||
const totalWidth =
|
||||
itemsWithLayout.reduce((sum, entry) => sum + entry.width, 0) +
|
||||
(itemsWithLayout.length - 1) * gap;
|
||||
|
|
@ -284,51 +467,30 @@ export function deriveDisplayGraph({
|
|||
for (const entry of itemsWithLayout) {
|
||||
const preferredPosition = {
|
||||
x: xCursor,
|
||||
y: node.position.y - entry.height - sideOffset,
|
||||
y:
|
||||
auxTargetSide === "top"
|
||||
? node.position.y - entry.height - sideOffset
|
||||
: node.position.y + parentHeight + sideOffset,
|
||||
};
|
||||
const defaultPosition = findNonOverlappingPosition(
|
||||
preferredPosition,
|
||||
entry.width,
|
||||
entry.height,
|
||||
llmDirection,
|
||||
occupiedRects,
|
||||
);
|
||||
const position = auxNodePositions[entry.auxId] ?? defaultPosition;
|
||||
xCursor += entry.width + gap;
|
||||
|
||||
auxNodeIds.push(entry.auxId);
|
||||
if (!auxNodePositions[entry.auxId]) {
|
||||
auxDefaults[entry.auxId] = defaultPosition;
|
||||
}
|
||||
occupiedRects.push(toRect(position, entry.width, entry.height));
|
||||
|
||||
auxNodes.push({
|
||||
id: entry.auxId,
|
||||
type: "aux",
|
||||
data: entry.item.data,
|
||||
appendAuxNodeAndEdge({
|
||||
auxNodes,
|
||||
auxEdges,
|
||||
entry,
|
||||
position,
|
||||
width: entry.width,
|
||||
height: entry.height,
|
||||
style: {
|
||||
width: entry.width,
|
||||
height: entry.height,
|
||||
},
|
||||
draggable: true,
|
||||
selectable: true,
|
||||
focusable: true,
|
||||
connectable: true,
|
||||
});
|
||||
|
||||
auxEdges.push({
|
||||
id: `e-${entry.auxId}-${node.id}`,
|
||||
source: entry.auxId,
|
||||
sourceHandle: HANDLE_IDS.llmInputOut,
|
||||
target: node.id,
|
||||
targetHandle: entry.item.targetHandle,
|
||||
type: "canvas",
|
||||
data: { path: "auto" },
|
||||
selectable: false,
|
||||
focusable: false,
|
||||
parentNode: node,
|
||||
parentWidth,
|
||||
parentHeight,
|
||||
auxTargetHandle,
|
||||
});
|
||||
}
|
||||
continue;
|
||||
|
|
@ -338,7 +500,10 @@ export function deriveDisplayGraph({
|
|||
itemsWithLayout.reduce((sum, entry) => sum + entry.height, 0) +
|
||||
(itemsWithLayout.length - 1) * gap;
|
||||
const maxWidth = Math.max(...itemsWithLayout.map((entry) => entry.width));
|
||||
const baseX = node.position.x - maxWidth - sideOffset;
|
||||
const baseX =
|
||||
auxTargetSide === "right"
|
||||
? node.position.x + parentWidth + sideOffset
|
||||
: node.position.x - maxWidth - sideOffset;
|
||||
let yCursor = node.position.y + (parentHeight - totalHeight) / 2;
|
||||
|
||||
for (const entry of itemsWithLayout) {
|
||||
|
|
@ -350,45 +515,21 @@ export function deriveDisplayGraph({
|
|||
preferredPosition,
|
||||
entry.width,
|
||||
entry.height,
|
||||
llmDirection,
|
||||
occupiedRects,
|
||||
);
|
||||
const position = auxNodePositions[entry.auxId] ?? defaultPosition;
|
||||
yCursor += entry.height + gap;
|
||||
|
||||
auxNodeIds.push(entry.auxId);
|
||||
if (!auxNodePositions[entry.auxId]) {
|
||||
auxDefaults[entry.auxId] = defaultPosition;
|
||||
}
|
||||
occupiedRects.push(toRect(position, entry.width, entry.height));
|
||||
|
||||
auxNodes.push({
|
||||
id: entry.auxId,
|
||||
type: "aux",
|
||||
data: entry.item.data,
|
||||
appendAuxNodeAndEdge({
|
||||
auxNodes,
|
||||
auxEdges,
|
||||
entry,
|
||||
position,
|
||||
width: entry.width,
|
||||
height: entry.height,
|
||||
style: {
|
||||
width: entry.width,
|
||||
height: entry.height,
|
||||
},
|
||||
draggable: true,
|
||||
selectable: true,
|
||||
focusable: true,
|
||||
connectable: true,
|
||||
});
|
||||
|
||||
auxEdges.push({
|
||||
id: `e-${entry.auxId}-${node.id}`,
|
||||
source: entry.auxId,
|
||||
sourceHandle: HANDLE_IDS.llmInputOut,
|
||||
target: node.id,
|
||||
targetHandle: entry.item.targetHandle,
|
||||
type: "canvas",
|
||||
data: { path: "auto" },
|
||||
selectable: false,
|
||||
focusable: false,
|
||||
parentNode: node,
|
||||
parentWidth,
|
||||
parentHeight,
|
||||
auxTargetHandle,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -398,7 +539,5 @@ export function deriveDisplayGraph({
|
|||
edges: [...edges, ...auxEdges].map((edge) =>
|
||||
normalizeEdge(edge, configs, layoutDirection),
|
||||
),
|
||||
auxNodeIds,
|
||||
auxDefaults,
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,36 +1,5 @@
|
|||
import { Position } from "@xyflow/react";
|
||||
import type { LayoutDirection } from "../types";
|
||||
|
||||
export const NODE_HANDLE_CLASS =
|
||||
"pointer-events-auto !size-2.5 !border-border/80 !bg-muted shadow-sm hover:!border-primary/70 hover:!bg-primary/20";
|
||||
|
||||
export const AUX_HANDLE_CLASS =
|
||||
"!size-2 !border-border/80 !bg-muted/80 shadow-sm";
|
||||
|
||||
export type NodeHandleLayout = {
|
||||
isTopBottom: boolean;
|
||||
dataInPosition: Position;
|
||||
dataOutPosition: Position;
|
||||
semanticInPosition: Position;
|
||||
semanticOutPosition: Position;
|
||||
};
|
||||
|
||||
export function getNodeHandleLayout(
|
||||
direction: LayoutDirection,
|
||||
): NodeHandleLayout {
|
||||
const isTopBottom = direction === "TB";
|
||||
return {
|
||||
isTopBottom,
|
||||
dataInPosition: isTopBottom ? Position.Top : Position.Left,
|
||||
dataOutPosition: isTopBottom ? Position.Bottom : Position.Right,
|
||||
semanticInPosition: isTopBottom ? Position.Left : Position.Top,
|
||||
semanticOutPosition: isTopBottom ? Position.Right : Position.Bottom,
|
||||
};
|
||||
}
|
||||
|
||||
export function getAuxSourceHandlePosition(
|
||||
direction: LayoutDirection,
|
||||
): Position {
|
||||
return direction === "TB" ? Position.Bottom : Position.Right;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -23,17 +23,14 @@ export const HANDLE_IDS = {
|
|||
semanticOutBottom: "semantic-out-bottom",
|
||||
semanticOutRight: "semantic-out-right",
|
||||
// llm prompt/scorer lanes
|
||||
llmPromptIn: "llm-prompt-in",
|
||||
llmSystemIn: "llm-system-in",
|
||||
llmInputOut: "llm-input-out",
|
||||
llmInputOutLeft: "llm-input-out-left",
|
||||
llmInputOutRight: "llm-input-out-right",
|
||||
llmInputOutTop: "llm-input-out-top",
|
||||
llmInputOutBottom: "llm-input-out-bottom",
|
||||
} as const;
|
||||
|
||||
export type RecipeHandleId = (typeof HANDLE_IDS)[keyof typeof HANDLE_IDS];
|
||||
|
||||
export function getLlmJudgeScoreHandleId(index: number): string {
|
||||
return `llm-judge-score-in-${index}`;
|
||||
}
|
||||
|
||||
const LEGACY_HANDLE_ALIAS_MAP: Record<string, string> = {
|
||||
[HANDLE_IDS.semanticInLeft]: HANDLE_IDS.semanticIn,
|
||||
[HANDLE_IDS.semanticOutRight]: HANDLE_IDS.semanticOut,
|
||||
|
|
|
|||
|
|
@ -453,7 +453,7 @@ export function importRecipePayload(input: string): ImportResult {
|
|||
return { errors, snapshot: null };
|
||||
}
|
||||
|
||||
const { layouts, edges: uiEdges, layoutDirection } = parseUi(ui);
|
||||
const { layouts, auxNodes, edges: uiEdges, layoutDirection } = parseUi(ui);
|
||||
const resolvedLayoutDirection = layoutDirection ?? "LR";
|
||||
const nodes = buildNodes(configs, layouts);
|
||||
const edges = buildEdges(
|
||||
|
|
@ -462,6 +462,15 @@ export function importRecipePayload(input: string): ImportResult {
|
|||
uiEdges,
|
||||
resolvedLayoutDirection,
|
||||
);
|
||||
const auxNodePositions = Object.fromEntries(
|
||||
auxNodes.flatMap((item) => {
|
||||
const llmId = nameToId.get(item.llm);
|
||||
if (!llmId) {
|
||||
return [];
|
||||
}
|
||||
return [[`aux-${llmId}-${item.key}`, { x: item.x, y: item.y }]];
|
||||
}),
|
||||
);
|
||||
|
||||
const maxY = nodes.reduce(
|
||||
(acc, node) => Math.max(acc, node.position.y),
|
||||
|
|
@ -474,6 +483,7 @@ export function importRecipePayload(input: string): ImportResult {
|
|||
configs: Object.fromEntries(configs.map((config) => [config.id, config])),
|
||||
nodes,
|
||||
edges,
|
||||
auxNodePositions,
|
||||
processors,
|
||||
layoutDirection: resolvedLayoutDirection,
|
||||
nextId,
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import type { Edge } from "@xyflow/react";
|
||||
import type { Edge, XYPosition } from "@xyflow/react";
|
||||
import type {
|
||||
LayoutDirection,
|
||||
RecipeNode,
|
||||
|
|
@ -10,6 +10,7 @@ export type RecipeSnapshot = {
|
|||
configs: Record<string, NodeConfig>;
|
||||
nodes: RecipeNode[];
|
||||
edges: Edge[];
|
||||
auxNodePositions: Record<string, XYPosition>;
|
||||
processors: RecipeProcessorConfig[];
|
||||
layoutDirection: LayoutDirection;
|
||||
nextId: number;
|
||||
|
|
|
|||
|
|
@ -7,14 +7,23 @@ import { isRecord, readString } from "./helpers";
|
|||
type UiInput = {
|
||||
nodes?: unknown;
|
||||
edges?: unknown;
|
||||
aux_nodes?: unknown;
|
||||
layout_direction?: unknown;
|
||||
layoutDirection?: unknown;
|
||||
};
|
||||
|
||||
type ParsedAuxNode = {
|
||||
llm: string;
|
||||
key: string;
|
||||
x: number;
|
||||
y: number;
|
||||
};
|
||||
|
||||
export function parseUi(
|
||||
ui: UiInput | null,
|
||||
): {
|
||||
layouts: Map<string, { x: number; y: number; width?: number }>;
|
||||
auxNodes: ParsedAuxNode[];
|
||||
edges: Array<{
|
||||
from: string;
|
||||
to: string;
|
||||
|
|
@ -25,6 +34,7 @@ export function parseUi(
|
|||
layoutDirection: "LR" | "TB" | null;
|
||||
} {
|
||||
const layouts = new Map<string, { x: number; y: number; width?: number }>();
|
||||
const auxNodes: ParsedAuxNode[] = [];
|
||||
const edges: Array<{
|
||||
from: string;
|
||||
to: string;
|
||||
|
|
@ -72,6 +82,21 @@ export function parseUi(
|
|||
}
|
||||
}
|
||||
}
|
||||
if (ui && Array.isArray(ui.aux_nodes)) {
|
||||
for (const node of ui.aux_nodes) {
|
||||
if (!isRecord(node)) {
|
||||
continue;
|
||||
}
|
||||
const llm = readString(node.llm);
|
||||
const key = readString(node.key);
|
||||
const x = typeof node.x === "number" ? node.x : null;
|
||||
const y = typeof node.y === "number" ? node.y : null;
|
||||
if (!(llm && key && x !== null && y !== null)) {
|
||||
continue;
|
||||
}
|
||||
auxNodes.push({ llm, key, x, y });
|
||||
}
|
||||
}
|
||||
const layoutDirectionRaw =
|
||||
readString(ui?.layout_direction) ?? readString(ui?.layoutDirection);
|
||||
const layoutDirection =
|
||||
|
|
@ -81,7 +106,12 @@ export function parseUi(
|
|||
? "LR"
|
||||
: null;
|
||||
|
||||
return { layouts, edges: edges.length > 0 ? edges : null, layoutDirection };
|
||||
return {
|
||||
layouts,
|
||||
auxNodes,
|
||||
edges: edges.length > 0 ? edges : null,
|
||||
layoutDirection,
|
||||
};
|
||||
}
|
||||
|
||||
export function buildNodes(
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import type { Edge } from "@xyflow/react";
|
||||
import type { Edge, XYPosition } from "@xyflow/react";
|
||||
import type {
|
||||
LayoutDirection,
|
||||
ModelConfig,
|
||||
|
|
@ -70,6 +70,7 @@ export function buildRecipePayload(
|
|||
edges: Edge[],
|
||||
processors: RecipeProcessorConfig[] = [],
|
||||
layoutDirection: LayoutDirection = "LR",
|
||||
auxNodePositions: Record<string, XYPosition> = {},
|
||||
): RecipePayloadResult {
|
||||
const errors: string[] = [];
|
||||
const columns: Record<string, unknown>[] = [];
|
||||
|
|
@ -270,6 +271,27 @@ export function buildRecipePayload(
|
|||
},
|
||||
];
|
||||
});
|
||||
const uiAuxNodes = Object.entries(auxNodePositions).flatMap(
|
||||
([auxId, position]) => {
|
||||
const match = /^aux-([^-]+)-(.+)$/.exec(auxId);
|
||||
if (!match) {
|
||||
return [];
|
||||
}
|
||||
const [, llmId, key] = match;
|
||||
const llmConfig = configs[llmId];
|
||||
if (!(llmConfig && llmConfig.kind === "llm")) {
|
||||
return [];
|
||||
}
|
||||
return [
|
||||
{
|
||||
llm: llmConfig.name,
|
||||
key,
|
||||
x: position.x,
|
||||
y: position.y,
|
||||
},
|
||||
];
|
||||
},
|
||||
);
|
||||
const recipeProcessors = buildProcessors(processors, errors);
|
||||
const seedConfig = firstSeed ? buildSeedConfig(firstSeed, errors) : undefined;
|
||||
const seedDropProcessor = firstSeed
|
||||
|
|
@ -306,6 +328,7 @@ export function buildRecipePayload(
|
|||
nodes: uiNodes,
|
||||
edges: uiEdges,
|
||||
layout_direction: layoutDirection,
|
||||
...(uiAuxNodes.length > 0 && { aux_nodes: uiAuxNodes }),
|
||||
...(firstSeed && { seed_source_type: firstSeed.seed_source_type }),
|
||||
...(firstSeed && { seed_columns: firstSeed.seed_columns ?? [] }),
|
||||
...(firstSeed && {
|
||||
|
|
|
|||
|
|
@ -52,6 +52,13 @@ export type RecipePayload = {
|
|||
layout_direction?: "LR" | "TB";
|
||||
// ui-only, used to preserve seed block mode across imports/refresh
|
||||
seed_source_type?: "hf" | "local" | "unstructured";
|
||||
// ui-only, persisted aux node positions by llm name + aux key
|
||||
aux_nodes?: Array<{
|
||||
llm: string;
|
||||
key: string;
|
||||
x: number;
|
||||
y: number;
|
||||
}>;
|
||||
// ui-only, seed metadata cached for refresh/import UX
|
||||
seed_columns?: string[];
|
||||
seed_drop_columns?: string[];
|
||||
|
|
|
|||
|
|
@ -5,43 +5,25 @@ import type {
|
|||
NodeChange,
|
||||
XYPosition,
|
||||
} from "@xyflow/react";
|
||||
import type { RecipeGraphAuxNodeData } from "../components/recipe-graph-aux-node";
|
||||
import type { RecipeNodeData } from "../types";
|
||||
|
||||
type AnyNode = Node<RecipeNodeData | RecipeGraphAuxNodeData>;
|
||||
|
||||
export function applyAuxNodeChanges(
|
||||
changes: NodeChange<AnyNode>[],
|
||||
export function applyAuxNodeChanges<T extends Node>(
|
||||
changes: NodeChange<T>[],
|
||||
actions: {
|
||||
setAuxNodePosition: (id: string, position: XYPosition) => void;
|
||||
setAuxNodeSize: (
|
||||
id: string,
|
||||
size: { width: number; height: number },
|
||||
) => void;
|
||||
},
|
||||
): void {
|
||||
for (const change of changes) {
|
||||
if (!("id" in change) || !change.id.startsWith("aux-")) {
|
||||
continue;
|
||||
}
|
||||
if (change.type === "position") {
|
||||
const nextPosition = change.position ?? change.positionAbsolute;
|
||||
if (nextPosition) {
|
||||
actions.setAuxNodePosition(change.id, nextPosition);
|
||||
}
|
||||
if (change.type !== "position") {
|
||||
continue;
|
||||
}
|
||||
if (
|
||||
change.type === "dimensions" &&
|
||||
change.dimensions &&
|
||||
change.dimensions.width > 0 &&
|
||||
change.dimensions.height > 0
|
||||
) {
|
||||
actions.setAuxNodeSize(change.id, {
|
||||
width: change.dimensions.width,
|
||||
height: change.dimensions.height,
|
||||
});
|
||||
const nextPosition = change.position ?? change.positionAbsolute;
|
||||
if (!nextPosition) {
|
||||
continue;
|
||||
}
|
||||
actions.setAuxNodePosition(change.id, nextPosition);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -62,4 +44,3 @@ export function filterEdgeChangesByIds(
|
|||
(change): change is EdgeChange<Edge> => "id" in change && ids.has(change.id),
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue