"use client"; import { useRef, useEffect, useState, useCallback } from "react"; import { useChat } from "@ai-sdk/react"; import { DefaultChatTransport } from "ai"; import ReactMarkdown from "react-markdown"; import { EditOperation } from "@/lib/chat/editOperations"; interface ChatPanelProps { isOpen: boolean; onClose: () => void; onBuildWorkflow?: (description: string) => Promise; isBuildingWorkflow?: boolean; onApplyEdits?: (operations: EditOperation[]) => { applied: number; skipped: string[] }; workflowState?: { 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, 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 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 and selectedNodeIds from ref const customFetch = useCallback(async (input: RequestInfo | URL, init?: RequestInit) => { if (!init?.body) { return fetch(input, init); } const body = JSON.parse(init.body as string); const bodyWithWorkflow = { ...body, workflowState: workflowStateRef.current, selectedNodeIds: selectedNodeIdsRef.current, }; return fetch(input, { ...init, body: JSON.stringify(bodyWithWorkflow), }); }, []); const [transport] = useState(() => new DefaultChatTransport({ api: "/api/chat", fetch: customFetch })); 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"; // Track processed tool calls to avoid re-applying const processedToolCalls = useRef>(new Set()); // Process tool invocations from AI responses useEffect(() => { messages.forEach((msg) => { msg.parts?.forEach((part) => { if (!part.type.startsWith("tool-")) return; if (!("state" in part) || !("toolCallId" in part) || !("input" in part)) return; if (part.state !== "output-available") return; const callId = (part as Record).toolCallId as string; if (processedToolCalls.current.has(callId)) return; processedToolCalls.current.add(callId); const toolName = part.type.replace("tool-", ""); if (toolName === "createWorkflow" && onBuildWorkflow) { const description = ((part as Record).input as { description?: string }).description; if (description) { onBuildWorkflow(description); } } if (toolName === "editWorkflow" && onApplyEdits) { const operations = ((part as Record).input as { operations?: EditOperation[] }).operations; if (operations) { onApplyEdits(operations); } } }); }); }, [messages, onBuildWorkflow, onApplyEdits]); // Extract conversation description from user messages const getConversationDescription = () => { const userMessages = messages .filter((m) => m.role === "user") .map((m) => { const textContent = m.parts ?.filter((part): part is { type: "text"; text: string } => part.type === "text") .map((part) => part.text) .join("") || ""; return textContent; }) .filter((text) => text.trim().length > 0); return userMessages.join("\n"); }; const handleBuildWorkflow = async () => { if (!onBuildWorkflow) return; const description = getConversationDescription(); if (!description.trim()) return; await onBuildWorkflow(description); }; // Check if conversation has started (has assistant messages) const hasConversation = messages.some((m) => m.role === "assistant"); // Auto-scroll to bottom on new messages useEffect(() => { messagesEndRef.current?.scrollIntoView({ behavior: "smooth" }); }, [messages]); // Reset chip dismissed state when selection changes useEffect(() => { setChipDismissed(false); }, [selectedNodeIds]); if (!isOpen) return null; return (
{/* Header */}

Workflow Assistant Beta

{messages.length > 0 && ( )}
{/* Messages area - nowheel class prevents React Flow from intercepting scroll */}
e.stopPropagation()} > {/* 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?"

)} {messages.map((message) => { // Extract text from message parts const textContent = message.parts ?.filter((part): part is { type: "text"; text: string } => part.type === "text") .map((part) => part.text) .join("") || ""; // Extract tool invocations for display const toolInvocations = message.parts ?.filter((part) => { if (!part.type.startsWith("tool-")) return false; if (!("state" in part)) return false; return part.state === "output-available"; }) || []; return (
{message.role === "user" ? (

{textContent}

) : ( <> {/* Display tool invocation explanations */} {toolInvocations.map((tool, idx) => { if (!("input" in tool)) return null; const toolName = tool.type.replace("tool-", ""); if (toolName === "editWorkflow") { const explanation = (tool.input as { explanation?: string }).explanation; if (explanation) { return (
{explanation}
); } } if (toolName === "createWorkflow") { return (
Building workflow...
); } return null; })} {/* Display text content */} {textContent && (

{children}

, strong: ({ children }) => {children}, em: ({ children }) => {children}, ul: ({ children }) =>
    {children}
, ol: ({ children }) =>
    {children}
, li: ({ children }) =>
  • {children}
  • , blockquote: ({ children }) =>
    {children}
    , code: ({ children }) => {children}, }} > {textContent}
    )} )}
    ); })} {/* Loading indicator */} {isLoading && (
    )}
    {/* Input area */}
    {/* Selection focus chip */} {selectedNodeIds && selectedNodeIds.length > 0 && !chipDismissed && (
    Focused on {selectedNodeIds.length} selected node{selectedNodeIds.length !== 1 ? 's' : ''}
    )}
    { e.preventDefault(); if (input.trim() && !isLoading) { sendMessage({ text: input }); setInput(""); } }} className="p-3" >
    setInput(e.target.value)} placeholder="Type a message..." className="flex-1 bg-neutral-700 border border-neutral-600 rounded-lg px-3 py-2 text-sm text-neutral-200 placeholder-neutral-500 focus:outline-none focus:border-blue-500" disabled={isLoading} />
    {/* Build Workflow button */} {hasConversation && onBuildWorkflow && (
    )}
    ); }