"use client"; import { useCallback, useEffect, useState } from "react"; import { Check, X } from "lucide-react"; import { PillButton } from "@/app/components/ui/pill-button"; import { TabPillButton } from "@/app/components/ui/tab-pill-button"; import type { AssistantEvent, Document } from "../shared/types"; import { FileTypeIcon } from "../shared/FileTypeIcon"; import { AddDocumentsModal } from "../modals/AddDocumentsModal"; type AskInputsEvent = Extract; type AskInputItem = AskInputsEvent["items"][number]; type AskInputsResponse = Extract< AssistantEvent, { type: "ask_inputs_response" } >; export function AskInputPopup({ event, onSubmit, onDismiss, }: { event: AskInputsEvent; onSubmit?: ( response: AskInputsResponse, content: string, files: { filename: string; document_id: string }[], ) => void; onDismiss?: () => void; }) { const [choiceAnswers, setChoiceAnswers] = useState>( {}, ); const [otherOpen, setOtherOpen] = useState>({}); const [otherValues, setOtherValues] = useState>({}); const [docsByInput, setDocsByInput] = useState< Record> >({}); const [skipped, setSkipped] = useState>(() => new Set()); const [confirmed, setConfirmed] = useState>(() => new Set()); const [submitted, setSubmitted] = useState(false); const [dismissed, setDismissed] = useState(false); const [docSelectorTarget, setDocSelectorTarget] = useState<{ inputId: string; typeIndex: number; } | null>(null); const [activeInputId, setActiveInputId] = useState( () => event.items[0]?.id ?? "", ); const docsForItem = useCallback( (inputId: string) => { const seen = new Set(); return Object.values(docsByInput[inputId] ?? {}) .flat() .filter((doc) => { if (seen.has(doc.id)) return false; seen.add(doc.id); return true; }); }, [docsByInput], ); const itemAnswered = useCallback( (item: AskInputItem) => { if (item.kind === "choice") return !!choiceAnswers[item.id]?.trim(); return docsForItem(item.id).length > 0; }, [choiceAnswers, docsForItem], ); const itemResolved = useCallback( (item: AskInputItem) => skipped.has(item.id) || confirmed.has(item.id), [confirmed, skipped], ); const firstUnresolvedId = useCallback( (resolvedId?: string) => event.items.find((item) => { if (item.id === resolvedId) return false; return !itemResolved(item); })?.id ?? null, [event.items, itemResolved], ); const goToNextUnresolved = useCallback( (resolvedId: string) => { const nextId = firstUnresolvedId(resolvedId); if (nextId) setActiveInputId(nextId); }, [firstUnresolvedId], ); const setSkippedFor = (id: string, shouldSkip = true) => { setSkipped((prev) => { const next = new Set(prev); if (shouldSkip) next.add(id); else next.delete(id); return next; }); if (shouldSkip) goToNextUnresolved(id); }; const confirmItem = (id: string) => { setConfirmed((prev) => { const next = new Set(prev); next.add(id); return next; }); goToNextUnresolved(id); }; const addDocs = ( inputId: string, typeIndex: number, selected: Document[], ) => { if (selected.length === 0) return; setSkippedFor(inputId, false); setDocsByInput((prev) => { const byType = prev[inputId] ?? {}; const current = byType[typeIndex] ?? []; const existing = new Set(current.map((doc) => doc.id)); return { ...prev, [inputId]: { ...byType, [typeIndex]: [ ...current, ...selected.filter((doc) => !existing.has(doc.id)), ], }, }; }); }; const removeDoc = (inputId: string, typeIndex: number, docId: string) => { setDocsByInput((prev) => { const byType = prev[inputId] ?? {}; return { ...prev, [inputId]: { ...byType, [typeIndex]: (byType[typeIndex] ?? []).filter( (doc) => doc.id !== docId, ), }, }; }); }; const chooseAnswer = ( item: Extract, answer: string, ) => { const trimmed = answer.trim(); if (!trimmed || submitted) return; setSkippedFor(item.id, false); setChoiceAnswers((prev) => ({ ...prev, [item.id]: trimmed })); setOtherOpen((prev) => ({ ...prev, [item.id]: false })); }; const allResolved = event.items.length > 0 && event.items.every(itemResolved); const canSubmit = !submitted && allResolved && !!onSubmit; const buildResponse = (): AskInputsResponse => { const responses = event.items.map((item) => { if (skipped.has(item.id)) { return item.kind === "choice" ? { id: item.id, kind: "choice" as const, question: item.question, skipped: true, } : { id: item.id, kind: "documents" as const, filenames: [], skipped: true, }; } if (item.kind === "choice") { return { id: item.id, kind: "choice" as const, question: item.question, answer: choiceAnswers[item.id]?.trim() ?? "", }; } return { id: item.id, kind: "documents" as const, filenames: docsForItem(item.id).map((doc) => doc.filename), }; }); return { type: "ask_inputs_response", responses }; }; const responseFiles = (response: AskInputsResponse) => { const responseById = new Map(response.responses.map((item) => [item.id, item])); const docs = event.items.flatMap((item) => { const responseItem = responseById.get(item.id); if ( item.kind !== "documents" || responseItem?.kind !== "documents" || responseItem.skipped ) { return []; } return docsForItem(item.id); }); const seen = new Set(); return docs.flatMap((doc) => { if (seen.has(doc.id)) return []; seen.add(doc.id); return [{ filename: doc.filename, document_id: doc.id }]; }); }; const buildContent = (response: AskInputsResponse) => { const lines = response.responses.map((item, index) => { if (item.kind === "choice") { if (item.skipped) return `${index + 1}. Skipped: ${item.question}`; return `${index + 1}. ${item.question}\n${item.answer ?? ""}`; } if (item.skipped) return `${index + 1}. Skipped document request.`; return `${index + 1}. Documents attached: ${item.filenames.join(", ")}`; }); return `Responses to Mike's questions:\n${lines.join("\n\n")}`; }; const submit = () => { if (!canSubmit) return; const response = buildResponse(); setSubmitted(true); onSubmit?.(response, buildContent(response), responseFiles(response)); }; useEffect(() => { // eslint-disable-next-line react-hooks/set-state-in-effect -- auto-submit when every question is answered; submit() sets state as part of the side effect if (canSubmit) submit(); }); const dismiss = useCallback(() => { if (submitted || dismissed) return; setDismissed(true); onDismiss?.(); }, [dismissed, onDismiss, submitted]); useEffect(() => { if (submitted || dismissed) return; const handler = (e: KeyboardEvent) => { if (e.key === "Escape") dismiss(); }; window.addEventListener("keydown", handler); return () => window.removeEventListener("keydown", handler); }, [submitted, dismissed, dismiss]); if (dismissed) return null; const activeItem = event.items.find((item) => item.id === activeInputId) ?? event.items[0]; return ( <>
{submitted ? ( "Inputs sent" ) : (
{event.items.map((item) => { const isActive = item.id === activeItem?.id; const isResolved = itemResolved(item); const label = item.kind === "choice" ? "Question" : "Documents"; return ( setActiveInputId(item.id) } className="h-6 px-2 font-sans text-[10px]" > {isResolved ? ( ) : ( )} {label} ); })}
)}
{!submitted && ( )}
{activeItem && (
{activeItem.kind === "choice" ? (

{activeItem.question}

) : ( )}
{activeItem.kind === "choice" ? ( chooseAnswer(activeItem, answer) } onOtherOpen={() => { setOtherOpen((prev) => ({ ...prev, [activeItem.id]: true, })); setChoiceAnswers((prev) => ({ ...prev, [activeItem.id]: ( otherValues[ activeItem.id ] ?? "" ).trim(), })); }} onOtherValue={(value) => { setOtherValues((prev) => ({ ...prev, [activeItem.id]: value, })); setChoiceAnswers((prev) => ({ ...prev, [activeItem.id]: value.trim(), })); if (value.trim()) setSkippedFor( activeItem.id, false, ); }} /> ) : ( setDocSelectorTarget({ inputId: activeItem.id, typeIndex, }) } onRemoveDoc={(typeIndex, docId) => removeDoc( activeItem.id, typeIndex, docId, ) } /> )}
{!submitted && (
confirmItem(activeItem.id) } className="h-6 px-3 font-sans text-[10px]" > {confirmed.has(activeItem.id) ? ( <> Confirmed ) : ( "Confirm" )}
)}
)}
setDocSelectorTarget(null)} onSelect={(selected) => { if (!docSelectorTarget) return; // A document can only be added once per input, so docs // already living in another type row are left where they // are; only genuinely new picks join the targeted row. const existing = new Set( docsForItem(docSelectorTarget.inputId).map( (doc) => doc.id, ), ); addDocs( docSelectorTarget.inputId, docSelectorTarget.typeIndex, selected.filter((doc) => !existing.has(doc.id)), ); }} breadcrumb={["Assistant", "Add Documents"]} initialSelectedDocuments={ docSelectorTarget ? docsForItem(docSelectorTarget.inputId) : [] } /> ); } function OptionInput({ item, disabled, selectedAnswer, otherOpen, otherValue, onAnswer, onOtherOpen, onOtherValue, }: { item: Extract; disabled?: boolean; selectedAnswer: string | null; otherOpen: boolean; otherValue: string; onAnswer: (answer: string) => void; onOtherOpen: () => void; onOtherValue: (value: string) => void; }) { return (
{item.options.map((option, idx) => { const answer = option.value.trim(); const isSelected = !otherOpen && selectedAnswer === answer; return ( ); })} {item.allow_other && (
!otherOpen && !disabled && onOtherOpen()} > {item.options.length + 1}. {otherOpen ? (