import { useLayoutEffect, useRef, useState } from "react"; import { plainInlineText } from "@chopin/question/react"; import type { ConversationPlan } from "@chopin/protocol"; import type { ExcerptCorrectionAction } from "./analysis-action"; export function ExcerptCorrection( { canEdit, linked, linkedSubspans, messageId, messageText, onAddExcerpt, outcome, quote, state }: { canEdit: boolean; linked: boolean; linkedSubspans: ConversationPlan.SourceRef[]; messageId: string; messageText: string; onAddExcerpt?: (action: ExcerptCorrectionAction) => Promise; outcome: ConversationPlan.CandidateOutcome; quote: string; state?: ConversationPlan.State; }, ) { let [editing, setEditing] = useState(false); let contributionType = useRef(null); let [kind, setKind] = useState<"option" | "reason" | "constraint">("reason"); let [threadId, setThreadId] = useState(""); let [targetOptionId, setTargetOptionId] = useState(""); let [exactText, setExactText] = useState(quote); let [submitting, setSubmitting] = useState(false); let [error, setError] = useState(""); let [success, setSuccess] = useState(""); let action = useRef<{ key: string; id: string } | undefined>(undefined); let threads = state?.threads.filter(thread => thread.questionnaireId && !["decided", "discarded"].includes(thread.status) ) ?? []; let thread = threads.find(item => item.id === threadId); let options = thread?.contributions.filter(item => item.kind === "option") ?? []; let selectedText = exactText.trim(); let sourceText = messageText.slice(outcome.start, outcome.end); let localStart = selectedText ? sourceText.indexOf(selectedText) : -1; let ambiguous = localStart >= 0 && sourceText.indexOf(selectedText, localStart + 1) >= 0; let start = localStart < 0 ? -1 : outcome.start + localStart; let end = start < 0 ? -1 : start + selectedText.length; let alreadyAdded = linkedSubspans.some(source => source.start === start && source.end === end); let validText = start >= outcome.start && end <= outcome.end && selectedText.length <= 500 && !ambiguous && !alreadyAdded; let validOption = !targetOptionId || kind !== "option" && options.some(item => item.id === targetOptionId); let ready = canEdit && !!onAddExcerpt && !!thread && validText && validOption && !submitting; useLayoutEffect(() => { if (editing) contributionType.current?.focus(); }, [editing]); let resetAction = () => { action.current = undefined; setError(""); setSuccess(""); }; let submit = async () => { if (!ready || !thread || !onAddExcerpt) return; let key = JSON.stringify([ thread.id, thread.version, messageId, start, end, kind, targetOptionId, ]); let currentAction = action.current; if (!currentAction || currentAction.key !== key) { currentAction = { key, id: crypto.randomUUID() }; action.current = currentAction; } setSubmitting(true); setError(""); try { await onAddExcerpt({ actionId: currentAction.id, threadId: thread.id, expectedVersion: thread.version, change: { kind: "add-excerpt", messageId, start, end, contributionKind: kind, ...(targetOptionId && kind !== "option" ? { targetOptionId } : {}), }, }); setSuccess(`Added to ${plainInlineText(thread.question)} as a ${kind}.`); setEditing(false); } catch { setError("Could not add this excerpt. Check the card and connection, then try again."); } finally { setSubmitting(false); } }; if (linked) { return (

Added to this card.

); } if (!editing) { return ( <> {(success || linkedSubspans.length > 0) && (

{success || `Added ${linkedSubspans.length} excerpt${ linkedSubspans.length === 1 ? "" : "s" } to this card.`}

)} ); } return (
{kind !== "option" && ( )}