"use client"; import type { ToolCallMessagePartProps } from "@assistant-ui/react"; import { useAtomValue } from "jotai"; import { AlertCircle, Code, CornerDownLeftIcon, ExternalLink, Pencil, Workflow, } from "lucide-react"; import Link from "next/link"; import { useCallback, useEffect, useMemo, useState } from "react"; import { activeSearchSpaceIdAtom } from "@/atoms/search-spaces/search-space-query.atoms"; import { TextShimmerLoader } from "@/components/prompt-kit/loader"; import { Button } from "@/components/ui/button"; import { automationCreateRequest } from "@/contracts/types/automation.types"; import type { HitlDecision, InterruptResult } from "@/features/chat-messages/hitl"; import { isInterruptResult, useHitlDecision, useHitlPhase } from "@/features/chat-messages/hitl"; import { AutomationDraftPreview } from "./automation-draft-preview"; const editArgsSchema = automationCreateRequest.omit({ search_space_id: true }); // ---------------------------------------------------------------------------- // Result discrimination — mirrors the backend return shapes in // app/agents/multi_agent_chat/main_agent/tools/automation/create.py. // ---------------------------------------------------------------------------- type AutomationCreateContext = { search_space_id?: number; }; interface SavedResult { status: "saved"; automation_id: number; name: string; } interface RejectedResult { status: "rejected"; message?: string; } interface InvalidResult { status: "invalid"; issues: string[]; raw?: unknown; } interface ErrorResult { status: "error"; message: string; } type CreateAutomationResult = | InterruptResult | SavedResult | RejectedResult | InvalidResult | ErrorResult; function hasStatus(value: unknown, status: string): boolean { return ( typeof value === "object" && value !== null && "status" in value && (value as { status: unknown }).status === status ); } // ---------------------------------------------------------------------------- // Approval card — pending → processing → complete / rejected. // // Edit toggle reuses the same primitives as the Create-via-JSON page: raw // textarea, Format, Zod validation against ``AutomationCreate`` (minus the // ``search_space_id`` field, which the backend injects). Approve dispatches // an ``edit`` decision with the parsed args when edits are pending, otherwise // a plain ``approve``. Multi-turn chat refinement still works as a fallback. // ---------------------------------------------------------------------------- interface ApprovalCardProps { args: Record; interruptData: InterruptResult; onDecision: (decision: HitlDecision) => void; } function ApprovalCard({ args, interruptData, onDecision }: ApprovalCardProps) { const { phase, setProcessing, setRejected } = useHitlPhase(interruptData); const reviewConfig = interruptData.review_configs[0]; const allowedDecisions = reviewConfig?.allowed_decisions ?? ["approve", "reject"]; const canApprove = allowedDecisions.includes("approve"); const canReject = allowedDecisions.includes("reject"); const canEdit = allowedDecisions.includes("edit"); const [pendingEdits, setPendingEdits] = useState | null>(null); const [isEditing, setIsEditing] = useState(false); const effectiveArgs = pendingEdits ?? args; const draft = useMemo(() => extractDraft(effectiveArgs), [effectiveArgs]); const handleApprove = useCallback(() => { if (phase !== "pending" || !canApprove || isEditing) return; setProcessing(); onDecision({ type: pendingEdits ? "edit" : "approve", edited_action: { name: interruptData.action_requests[0]?.name ?? "create_automation", args: pendingEdits ?? args, }, }); }, [phase, canApprove, isEditing, setProcessing, onDecision, interruptData, args, pendingEdits]); const handleReject = useCallback(() => { if (phase !== "pending" || !canReject || isEditing) return; setRejected(); onDecision({ type: "reject", message: "User rejected the automation draft." }); }, [phase, canReject, isEditing, setRejected, onDecision]); useEffect(() => { if (isEditing) return; const handler = (e: KeyboardEvent) => { if (e.key === "Enter" && !e.shiftKey && !e.ctrlKey && !e.metaKey) { handleApprove(); } }; window.addEventListener("keydown", handler); return () => window.removeEventListener("keydown", handler); }, [handleApprove, isEditing]); return (

{phase === "rejected" ? "Automation cancelled" : phase === "processing" ? "Saving automation" : phase === "complete" ? "Automation saved" : "Create automation"}

{phase === "processing" ? ( ) : phase === "complete" ? (

{pendingEdits ? "Automation saved with your edits" : "Automation created from this draft"}

) : phase === "rejected" ? (

No automation was saved — ask in chat to refine and try again.

) : (

{pendingEdits ? "Showing your edits. Approve to save, or edit again." : "Review and approve to save. Edit for fine-tuning, or reply in chat for a redraft."}

)}
{phase === "pending" && canEdit && !isEditing && ( )}
{isEditing ? ( { setPendingEdits(parsed); setIsEditing(false); }} onCancel={() => setIsEditing(false)} /> ) : ( )}
{phase === "pending" && !isEditing && ( <>
{canApprove && ( )} {canReject && ( )}
)}
); } interface JsonEditorProps { initialValue: Record; onSave: (parsed: Record) => void; onCancel: () => void; } function JsonEditor({ initialValue, onSave, onCancel }: JsonEditorProps) { const [text, setText] = useState(() => JSON.stringify(initialValue, null, 2)); const [issues, setIssues] = useState([]); function handleFormat() { try { setText(JSON.stringify(JSON.parse(text), null, 2)); setIssues([]); } catch (err) { setIssues([`Cannot format — not valid JSON: ${(err as Error).message}`]); } } function handleSave() { setIssues([]); let parsed: unknown; try { parsed = JSON.parse(text); } catch (err) { setIssues([`Invalid JSON: ${(err as Error).message}`]); return; } const result = editArgsSchema.safeParse(parsed); if (!result.success) { setIssues( result.error.issues.map((issue) => `${issue.path.join(".") || "(root)"}: ${issue.message}`) ); return; } onSave(result.data as unknown as Record); } return (