"use client"; import type { ToolCallMessagePartProps } from "@assistant-ui/react"; import { useSetAtom } from "jotai"; import { CornerDownLeftIcon, Pen } from "lucide-react"; import { useCallback, useEffect, useMemo, useState } from "react"; import { openHitlEditPanelAtom } from "@/atoms/chat/hitl-edit-panel.atom"; import { PlateEditor } from "@/components/editor/plate-editor"; import { TextShimmerLoader } from "@/components/prompt-kit/loader"; import { Button } from "@/components/ui/button"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@/components/ui/select"; import { useHitlPhase } from "@/hooks/use-hitl-phase"; interface InterruptResult { __interrupt__: true; __decided__?: "approve" | "reject" | "edit"; __completed__?: boolean; action_requests: Array<{ name: string; args: Record; description?: string; }>; review_configs: Array<{ action_name: string; allowed_decisions: Array<"approve" | "edit" | "reject">; }>; interrupt_type?: string; message?: string; context?: { accounts?: Array<{ id: number; name: string; workspace_id: string | null; workspace_name: string; workspace_icon: string; auth_expired?: boolean; }>; parent_pages?: Record< number, Array<{ page_id: string; title: string; document_id: number; }> >; error?: string; }; } interface SuccessResult { status: "success"; page_id: string; title: string; url: string; content_preview?: string; content_length?: number; message?: string; } interface ErrorResult { status: "error"; message: string; } interface AuthErrorResult { status: "auth_error"; message: string; connector_id?: number; connector_type: string; } type CreateNotionPageResult = InterruptResult | SuccessResult | ErrorResult | AuthErrorResult; function isInterruptResult(result: unknown): result is InterruptResult { return ( typeof result === "object" && result !== null && "__interrupt__" in result && (result as InterruptResult).__interrupt__ === true ); } function isAuthErrorResult(result: unknown): result is AuthErrorResult { return ( typeof result === "object" && result !== null && "status" in result && (result as AuthErrorResult).status === "auth_error" ); } function isErrorResult(result: unknown): result is ErrorResult { return ( typeof result === "object" && result !== null && "status" in result && (result as ErrorResult).status === "error" ); } function ApprovalCard({ args, interruptData, onDecision, }: { args: Record; interruptData: InterruptResult; onDecision: (decision: { type: "approve" | "reject" | "edit"; message?: string; edited_action?: { name: string; args: Record }; }) => void; }) { const { phase, setProcessing, setRejected } = useHitlPhase(interruptData); const [isPanelOpen, setIsPanelOpen] = useState(false); const openHitlEditPanel = useSetAtom(openHitlEditPanelAtom); const [pendingEdits, setPendingEdits] = useState<{ title: string; content: string } | null>(null); const accounts = interruptData.context?.accounts ?? []; const validAccounts = accounts.filter((a) => !a.auth_expired); const expiredAccounts = accounts.filter((a) => a.auth_expired); const parentPages = interruptData.context?.parent_pages ?? {}; const defaultAccountId = useMemo(() => { if (args.connector_id) return String(args.connector_id); if (validAccounts.length === 1) return String(validAccounts[0].id); return ""; }, [args.connector_id, validAccounts]); const [selectedAccountId, setSelectedAccountId] = useState(defaultAccountId); const [selectedParentPageId, setSelectedParentPageId] = useState( args.parent_page_id ? String(args.parent_page_id) : "__none__" ); const availableParentPages = useMemo(() => { if (!selectedAccountId) return []; return parentPages[Number(selectedAccountId)] ?? []; }, [selectedAccountId, parentPages]); const isTitleValid = useMemo(() => { const title = pendingEdits?.title ?? args.title; return title && typeof title === "string" && title.trim().length > 0; }, [pendingEdits?.title, args.title]); const reviewConfig = interruptData.review_configs[0]; const allowedDecisions = reviewConfig?.allowed_decisions ?? ["approve", "reject"]; const canEdit = allowedDecisions.includes("edit"); const handleApprove = useCallback(() => { if (phase !== "pending") return; if (isPanelOpen || !selectedAccountId || !isTitleValid) return; if (!allowedDecisions.includes("approve")) return; const isEdited = pendingEdits !== null; setProcessing(); onDecision({ type: isEdited ? "edit" : "approve", edited_action: { name: interruptData.action_requests[0].name, args: { ...args, ...(pendingEdits && { title: pendingEdits.title, content: pendingEdits.content }), connector_id: selectedAccountId ? Number(selectedAccountId) : null, parent_page_id: selectedParentPageId === "__none__" ? null : selectedParentPageId, }, }, }); }, [ phase, isPanelOpen, selectedAccountId, isTitleValid, allowedDecisions, setProcessing, onDecision, interruptData, args, selectedParentPageId, pendingEdits, ]); useEffect(() => { 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]); return (
{/* Header */}

{phase === "rejected" ? "Notion Page Rejected" : phase === "processing" || phase === "complete" ? "Notion Page Approved" : "Create Notion Page"}

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

{pendingEdits ? "Page created with your changes" : "Page created"}

) : phase === "rejected" ? (

Page creation was cancelled

) : (

Requires your approval to proceed

)}
{phase === "pending" && canEdit && ( )}
{/* Account/workspace picker — real UI in pending */} {phase === "pending" && interruptData.context && ( <>
{interruptData.context.error ? (

{interruptData.context.error}

) : ( <> {accounts.length > 0 && (

Notion Account *

)} {selectedAccountId && (

Parent Page (optional)

{availableParentPages.length === 0 && selectedAccountId && (

No pages available. Page will be created at workspace root.

)}
)} )}
)} {/* Content preview */}
{(pendingEdits?.title ?? args.title) != null && (

{String(pendingEdits?.title ?? args.title)}

)} {(pendingEdits?.content ?? args.content) != null && (
)}
{/* Action buttons - only shown when pending */} {phase === "pending" && ( <>
{allowedDecisions.includes("approve") && ( )} {allowedDecisions.includes("reject") && ( )}
)}
); } function AuthErrorCard({ result }: { result: AuthErrorResult }) { return (

Notion authentication expired

{result.message}

); } function ErrorCard({ result }: { result: ErrorResult }) { return (

Failed to create Notion page

{result.message}

); } function SuccessCard({ result }: { result: SuccessResult }) { return (

{result.message || "Notion page created successfully"}

Title: {result.title}
{result.url && ( )}
); } export const CreateNotionPageToolUI = ({ args, result }: ToolCallMessagePartProps<{ title: string; content: string }, CreateNotionPageResult>) => { if (!result) return null; if (isInterruptResult(result)) { return ( { const event = new CustomEvent("hitl-decision", { detail: { decisions: [decision] }, }); window.dispatchEvent(event); }} /> ); } if (isAuthErrorResult(result)) { return ; } if ( typeof result === "object" && result !== null && "status" in result && (result as { status: string }).status === "rejected" ) { return null; } if (isErrorResult(result)) { return ; } return ; };