|
|
|
@ -1,76 +1,203 @@ |
|
|
|
import { WorkflowNode } from "@/types"; |
|
|
|
import { WorkflowEdge } from "@/types/workflow"; |
|
|
|
import type { |
|
|
|
ImageInputNodeData, |
|
|
|
AnnotationNodeData, |
|
|
|
PromptNodeData, |
|
|
|
NanoBananaNodeData, |
|
|
|
GenerateVideoNodeData, |
|
|
|
LLMGenerateNodeData, |
|
|
|
SplitGridNodeData, |
|
|
|
OutputNodeData, |
|
|
|
} from "@/types"; |
|
|
|
|
|
|
|
/** |
|
|
|
* Binary fields by node type - fields containing base64 data URLs |
|
|
|
*/ |
|
|
|
const BINARY_FIELDS_BY_TYPE: Record<string, string[]> = { |
|
|
|
imageInput: ["image"], |
|
|
|
annotation: ["sourceImage", "outputImage"], |
|
|
|
nanoBanana: ["inputImages", "outputImage"], |
|
|
|
generateVideo: ["inputImages", "outputVideo"], |
|
|
|
llmGenerate: ["inputImages"], |
|
|
|
splitGrid: ["sourceImage"], |
|
|
|
output: ["image", "video"], |
|
|
|
prompt: [], |
|
|
|
}; |
|
|
|
|
|
|
|
/** |
|
|
|
* History fields to strip completely (irrelevant for editing context) |
|
|
|
*/ |
|
|
|
const HISTORY_FIELDS = [ |
|
|
|
"imageHistory", |
|
|
|
"videoHistory", |
|
|
|
"selectedHistoryIndex", |
|
|
|
"selectedVideoHistoryIndex", |
|
|
|
]; |
|
|
|
|
|
|
|
/** |
|
|
|
* Ref fields to strip completely (internal storage tracking) |
|
|
|
*/ |
|
|
|
const REF_FIELDS = [ |
|
|
|
"imageRef", |
|
|
|
"outputImageRef", |
|
|
|
"sourceImageRef", |
|
|
|
"inputImageRefs", |
|
|
|
"outputVideoRef", |
|
|
|
]; |
|
|
|
|
|
|
|
/** |
|
|
|
* Stripped node with binary data replaced by metadata placeholders |
|
|
|
*/ |
|
|
|
export interface StrippedNode { |
|
|
|
id: string; |
|
|
|
type: string; |
|
|
|
position: { x: number; y: number }; |
|
|
|
data: Record<string, unknown>; |
|
|
|
} |
|
|
|
|
|
|
|
/** |
|
|
|
* Lightweight workflow context for LLM consumption. |
|
|
|
* Strips all base64 image data, history arrays, and internal state. |
|
|
|
*/ |
|
|
|
export interface WorkflowContext { |
|
|
|
nodeCount: number; |
|
|
|
nodes: Array<{ |
|
|
|
id: string; |
|
|
|
type: string; |
|
|
|
title: string; |
|
|
|
model?: string; |
|
|
|
}>; |
|
|
|
nodes: StrippedNode[]; |
|
|
|
connections: Array<{ |
|
|
|
from: string; |
|
|
|
to: string; |
|
|
|
type: string; |
|
|
|
sourceHandle: string | null; |
|
|
|
targetHandle: string | null; |
|
|
|
}>; |
|
|
|
isEmpty: boolean; |
|
|
|
} |
|
|
|
|
|
|
|
/** |
|
|
|
* Builds a lightweight workflow context from nodes and edges. |
|
|
|
* Omits all base64 image data, history arrays, and internal state. |
|
|
|
* Estimates the size of a base64 data URL in kilobytes. |
|
|
|
*/ |
|
|
|
function estimateBase64Size(dataUrl: string): number { |
|
|
|
// Base64 encoding overhead: every 3 bytes becomes 4 characters
|
|
|
|
// So to get original size: (length * 3) / 4
|
|
|
|
return Math.round((dataUrl.length * 3) / 4 / 1024); |
|
|
|
} |
|
|
|
|
|
|
|
/** |
|
|
|
* Formats a binary field placeholder with metadata. |
|
|
|
*/ |
|
|
|
function formatBinaryPlaceholder( |
|
|
|
value: string | string[], |
|
|
|
fieldName: string, |
|
|
|
context?: string |
|
|
|
): string { |
|
|
|
// Handle array fields (inputImages)
|
|
|
|
if (Array.isArray(value)) { |
|
|
|
if (value.length === 0) { |
|
|
|
return "[no images]"; |
|
|
|
} |
|
|
|
const count = value.length; |
|
|
|
return `[${count} image(s)]`; |
|
|
|
} |
|
|
|
|
|
|
|
// Handle single string fields
|
|
|
|
const sizeKB = estimateBase64Size(value); |
|
|
|
const isVideo = fieldName.toLowerCase().includes("video"); |
|
|
|
const type = isVideo ? "video" : "image"; |
|
|
|
|
|
|
|
// Format: [type: context, sizeKB] or [type: sizeKB] if no context
|
|
|
|
if (context) { |
|
|
|
return `[${type}: ${context}, ${sizeKB}KB]`; |
|
|
|
} |
|
|
|
return `[${type}: ${sizeKB}KB]`; |
|
|
|
} |
|
|
|
|
|
|
|
/** |
|
|
|
* Strips binary data from workflow nodes, preserving all parameters and adding metadata placeholders. |
|
|
|
* |
|
|
|
* @param nodes - Current workflow nodes |
|
|
|
* @param edges - Current workflow edges |
|
|
|
* @returns Lightweight workflow context suitable for LLM injection |
|
|
|
* @param nodes - Workflow nodes to strip |
|
|
|
* @returns Stripped nodes with binary data replaced by metadata |
|
|
|
*/ |
|
|
|
export function buildWorkflowContext( |
|
|
|
nodes: WorkflowNode[], |
|
|
|
edges: WorkflowEdge[] |
|
|
|
): WorkflowContext { |
|
|
|
const isEmpty = nodes.length === 0; |
|
|
|
export function stripBinaryData(nodes: WorkflowNode[]): StrippedNode[] { |
|
|
|
return nodes.map((node) => { |
|
|
|
const strippedData: Record<string, unknown> = {}; |
|
|
|
const binaryFields = BINARY_FIELDS_BY_TYPE[node.type] || []; |
|
|
|
|
|
|
|
// Map nodes to lightweight summaries
|
|
|
|
const nodeSummaries = nodes.map((node) => { |
|
|
|
// Extract title: use customTitle if available, otherwise generate from type
|
|
|
|
const title = |
|
|
|
(node.data as { customTitle?: string }).customTitle || |
|
|
|
generateNodeTitle(node.type); |
|
|
|
// Copy all data fields except binary, history, and ref fields
|
|
|
|
for (const [key, value] of Object.entries(node.data)) { |
|
|
|
// Skip history fields
|
|
|
|
if (HISTORY_FIELDS.includes(key)) { |
|
|
|
continue; |
|
|
|
} |
|
|
|
// Skip ref fields
|
|
|
|
if (REF_FIELDS.includes(key)) { |
|
|
|
continue; |
|
|
|
} |
|
|
|
|
|
|
|
// For nanoBanana and generateVideo nodes, include model name
|
|
|
|
let model: string | undefined; |
|
|
|
// Handle binary fields
|
|
|
|
if (binaryFields.includes(key)) { |
|
|
|
if (value === null || value === undefined) { |
|
|
|
strippedData[key] = value; |
|
|
|
} else { |
|
|
|
// Add context based on node type
|
|
|
|
let context: string | undefined; |
|
|
|
if (node.type === "nanoBanana" || node.type === "generateVideo") { |
|
|
|
const nodeData = node.data as NanoBananaNodeData | GenerateVideoNodeData; |
|
|
|
model = nodeData.selectedModel?.displayName; |
|
|
|
context = nodeData.selectedModel?.displayName; |
|
|
|
} else if (node.type === "imageInput") { |
|
|
|
const nodeData = node.data as ImageInputNodeData; |
|
|
|
if (nodeData.dimensions) { |
|
|
|
context = `${nodeData.dimensions.width}x${nodeData.dimensions.height}`; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
strippedData[key] = formatBinaryPlaceholder( |
|
|
|
value as string | string[], |
|
|
|
key, |
|
|
|
context |
|
|
|
); |
|
|
|
} |
|
|
|
} else { |
|
|
|
// Copy non-binary field as-is
|
|
|
|
strippedData[key] = value; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
return { |
|
|
|
id: node.id, |
|
|
|
type: node.type, |
|
|
|
title, |
|
|
|
...(model && { model }), |
|
|
|
position: node.position, |
|
|
|
data: strippedData, |
|
|
|
}; |
|
|
|
}); |
|
|
|
} |
|
|
|
|
|
|
|
/** |
|
|
|
* Builds a lightweight workflow context from nodes and edges. |
|
|
|
* Omits all base64 image data, history arrays, and internal state. |
|
|
|
* |
|
|
|
* @param nodes - Current workflow nodes |
|
|
|
* @param edges - Current workflow edges |
|
|
|
* @returns Lightweight workflow context suitable for LLM injection |
|
|
|
*/ |
|
|
|
export function buildWorkflowContext( |
|
|
|
nodes: WorkflowNode[], |
|
|
|
edges: WorkflowEdge[] |
|
|
|
): WorkflowContext { |
|
|
|
const isEmpty = nodes.length === 0; |
|
|
|
|
|
|
|
// Strip binary data from all nodes
|
|
|
|
const strippedNodes = stripBinaryData(nodes); |
|
|
|
|
|
|
|
// Map edges to connection summaries
|
|
|
|
const connections = edges.map((edge) => ({ |
|
|
|
from: edge.source, |
|
|
|
to: edge.target, |
|
|
|
type: edge.sourceHandle || "unknown", |
|
|
|
sourceHandle: edge.sourceHandle || null, |
|
|
|
targetHandle: edge.targetHandle || null, |
|
|
|
})); |
|
|
|
|
|
|
|
return { |
|
|
|
nodeCount: nodes.length, |
|
|
|
nodes: nodeSummaries, |
|
|
|
nodes: strippedNodes, |
|
|
|
connections, |
|
|
|
isEmpty, |
|
|
|
}; |
|
|
|
@ -92,8 +219,8 @@ export function formatContextForPrompt(context: WorkflowContext): string { |
|
|
|
// List nodes
|
|
|
|
lines.push(`Current workflow has ${context.nodeCount} node(s):`); |
|
|
|
for (const node of context.nodes) { |
|
|
|
const modelInfo = node.model ? ` (${node.model})` : ""; |
|
|
|
lines.push(` - ${node.id}: ${node.title}${modelInfo}`); |
|
|
|
const title = (node.data.customTitle as string) || generateNodeTitle(node.type); |
|
|
|
lines.push(` - ${node.id}: ${title}`); |
|
|
|
} |
|
|
|
|
|
|
|
// List connections
|
|
|
|
@ -101,7 +228,8 @@ export function formatContextForPrompt(context: WorkflowContext): string { |
|
|
|
lines.push(""); |
|
|
|
lines.push("Connections:"); |
|
|
|
for (const conn of context.connections) { |
|
|
|
lines.push(` - ${conn.from} → ${conn.to} (${conn.type})`); |
|
|
|
const handleInfo = conn.sourceHandle ? ` (${conn.sourceHandle})` : ""; |
|
|
|
lines.push(` - ${conn.from} → ${conn.to}${handleInfo}`); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|