From aaafef4b771006d6d9da12c999b0efe7432bb88a Mon Sep 17 00:00:00 2001 From: shrimbly Date: Sat, 31 Jan 2026 19:59:03 +1300 Subject: [PATCH] feat(35-03): rewrite chatWorkflowState with stripBinaryData and add selection scoping - Import stripBinaryData from contextBuilder - Rewrite chatWorkflowState memo to use stripBinaryData for rich but compact node data - Compute selectedNodeIds from nodes.filter(n => n.selected) - Pass selectedNodeIds as new prop to ChatPanel - Update ChatPanel to accept and forward selectedNodeIds in API requests - Add dismissible "Focused on N selected nodes" chip above input when nodes selected - Add error handling for oversized payloads with user-friendly message Co-Authored-By: Claude Opus 4.5 --- src/components/ChatPanel.tsx | 65 +++++++++++++++++++++++++++++-- src/components/WorkflowCanvas.tsx | 37 +++++++++++------- 2 files changed, 84 insertions(+), 18 deletions(-) diff --git a/src/components/ChatPanel.tsx b/src/components/ChatPanel.tsx index 9da30f22..d31b76bd 100644 --- a/src/components/ChatPanel.tsx +++ b/src/components/ChatPanel.tsx @@ -16,18 +16,23 @@ interface ChatPanelProps { nodes: { id: string; type: string; data: Record }[]; edges: { id: string; source: string; target: string; sourceHandle?: string; targetHandle?: string }[] }; + selectedNodeIds?: string[]; } -export function ChatPanel({ isOpen, onClose, onBuildWorkflow, isBuildingWorkflow = false, onApplyEdits, workflowState }: ChatPanelProps) { +export function ChatPanel({ isOpen, onClose, onBuildWorkflow, isBuildingWorkflow = false, onApplyEdits, workflowState, selectedNodeIds }: ChatPanelProps) { const messagesEndRef = useRef(null); const [input, setInput] = useState(""); + const [chipDismissed, setChipDismissed] = useState(false); + const [errorMessage, setErrorMessage] = useState(null); - // Use a ref so the fetch closure always reads the latest workflowState + // Use a ref so the fetch closure always reads the latest workflowState and selectedNodeIds // without needing to re-create the transport const workflowStateRef = useRef(workflowState); workflowStateRef.current = workflowState; + const selectedNodeIdsRef = useRef(selectedNodeIds); + selectedNodeIdsRef.current = selectedNodeIds; - // Stable fetch function that reads workflowState from ref + // Stable fetch function that reads workflowState and selectedNodeIds from ref const customFetch = useCallback(async (input: RequestInfo | URL, init?: RequestInit) => { if (!init?.body) { return fetch(input, init); @@ -37,6 +42,7 @@ export function ChatPanel({ isOpen, onClose, onBuildWorkflow, isBuildingWorkflow const bodyWithWorkflow = { ...body, workflowState: workflowStateRef.current, + selectedNodeIds: selectedNodeIdsRef.current, }; return fetch(input, { @@ -49,6 +55,15 @@ export function ChatPanel({ isOpen, onClose, onBuildWorkflow, isBuildingWorkflow const { messages, sendMessage, setMessages, status } = useChat({ transport, + onError: (error) => { + // Check if this is an oversized payload error (413 or token limit) + const errorMsg = error.message.toLowerCase(); + if (errorMsg.includes("413") || errorMsg.includes("too large") || errorMsg.includes("token limit") || errorMsg.includes("payload")) { + setErrorMessage("This workflow is too large for the AI to process. Try selecting fewer nodes."); + } else { + setErrorMessage(error.message); + } + }, }); const isLoading = status === "streaming" || status === "submitted"; @@ -119,6 +134,11 @@ export function ChatPanel({ isOpen, onClose, onBuildWorkflow, isBuildingWorkflow messagesEndRef.current?.scrollIntoView({ behavior: "smooth" }); }, [messages]); + // Reset chip dismissed state when selection changes + useEffect(() => { + setChipDismissed(false); + }, [selectedNodeIds]); + if (!isOpen) return null; return ( @@ -157,7 +177,27 @@ export function ChatPanel({ isOpen, onClose, onBuildWorkflow, isBuildingWorkflow style={{ touchAction: 'pan-y' }} onWheelCapture={(e) => e.stopPropagation()} > - {messages.length === 0 && ( + {/* Error message display */} + {errorMessage && ( +
+
+ + + +
+

{errorMessage}

+ +
+
+
+ )} + + {messages.length === 0 && !errorMessage && (

Ask me anything about creating workflows!

e.g., "How do I create product photos with different backgrounds?"

@@ -261,6 +301,23 @@ export function ChatPanel({ isOpen, onClose, onBuildWorkflow, isBuildingWorkflow {/* Input area */}
+ {/* Selection focus chip */} + {selectedNodeIds && selectedNodeIds.length > 0 && !chipDismissed && ( +
+
+ Focused on {selectedNodeIds.length} selected node{selectedNodeIds.length !== 1 ? 's' : ''} + +
+
+ )}
{ e.preventDefault(); diff --git a/src/components/WorkflowCanvas.tsx b/src/components/WorkflowCanvas.tsx index f06aa490..c313268e 100644 --- a/src/components/WorkflowCanvas.tsx +++ b/src/components/WorkflowCanvas.tsx @@ -41,6 +41,7 @@ import { logger } from "@/utils/logger"; import { WelcomeModal } from "./quickstart"; import { ChatPanel } from "./ChatPanel"; import { EditOperation } from "@/lib/chat/editOperations"; +import { stripBinaryData } from "@/lib/chat/contextBuilder"; const nodeTypes: NodeTypes = { imageInput: ImageInputNode, @@ -596,20 +597,27 @@ export function WorkflowCanvas() { }, [loadWorkflow, showToast, captureSnapshot]); // Create lightweight workflow state for chat (strip base64 images) - const chatWorkflowState = useMemo(() => ({ - nodes: nodes.map(n => ({ - id: n.id, - type: n.type || '', - data: { customTitle: (n.data as { customTitle?: string }).customTitle }, - })), - edges: edges.map(e => ({ - id: e.id, - source: e.source, - target: e.target, - sourceHandle: e.sourceHandle || undefined, - targetHandle: e.targetHandle || undefined, - })), - }), [nodes, edges]); + const chatWorkflowState = useMemo(() => { + const strippedNodes = stripBinaryData(nodes); + return { + nodes: strippedNodes.map(n => ({ + id: n.id, + type: n.type, + position: n.position, + data: n.data, + })), + edges: edges.map(e => ({ + id: e.id, + source: e.source, + target: e.target, + sourceHandle: e.sourceHandle || undefined, + targetHandle: e.targetHandle || undefined, + })), + }; + }, [nodes, edges]); + + // Compute selected node IDs for chat context scoping + const selectedNodeIds = useMemo(() => nodes.filter(n => n.selected).map(n => n.id), [nodes]); // Handle applying edit operations from chat const handleApplyEdits = useCallback((operations: EditOperation[]) => { @@ -1421,6 +1429,7 @@ export function WorkflowCanvas() { isBuildingWorkflow={isBuildingWorkflow} onApplyEdits={handleApplyEdits} workflowState={chatWorkflowState} + selectedNodeIds={selectedNodeIds} />
);