"use client"; import { ArrowUp, Check, Copy, RotateCcw, Square, ThumbsDown, ThumbsUp } from "lucide-react"; import { useEffect, useRef, useState } from "react"; import { ChatMessageRow } from "./chat-message-row.tsx"; import { AppLoadingIndicator } from "./app-loading-indicator.tsx"; import { ModelSelector } from "./model-selector.tsx"; import { Button } from "./ui/button.tsx"; import { Textarea } from "./ui/textarea.tsx"; import { RectificationV4Panel, type RectificationV4Continuation, } from "./rectification-v4-panel.tsx"; import { type ConversationalRectificationMessage, type ConversationalRectificationStoredMessage, type ConversationalRectificationController, } from "../hooks/use-conversational-rectification.ts"; import type { ConversationalRectificationTurn } from "../lib/conversational-rectification/contracts.ts"; import type { PublicLanguageModel } from "../lib/public-models.ts"; type SurfaceProps = Readonly<{ controller: ConversationalRectificationController; openingAssistantText?: string; models: readonly PublicLanguageModel[]; selectedModelId: string; onSelectModel: (modelId: string) => void; pendingConsultationQuestion?: string | null; continuationPending?: boolean; onContinueOriginalQuestion?: (question: string) => void; }>; const ANSWER_UNDO_WINDOW_MS = 2_500; function safely(request: Promise) { void request.catch(() => undefined); } export function ConversationalRectificationSurface({ controller, openingAssistantText = "", models, selectedModelId, onSelectModel, pendingConsultationQuestion, continuationPending = false, onContinueOriginalQuestion, }: SurfaceProps) { const composer = useRef(null); const conversationEnd = useRef(null); const [feedback, setFeedback] = useState>({}); const [copiedMessageKey, setCopiedMessageKey] = useState(null); const [regeneratingMessageKey, setRegeneratingMessageKey] = useState(null); const undoTimer = useRef | null>(null); const [submission, setSubmission] = useState | null>(null); const turn = controller.turn; const messageCount = controller.messages?.length ?? 0; const latestMessageText = controller.messages?.[messageCount - 1]?.text ?? turn?.narrative ?? ""; const latestAssistantKey = [...(controller.messages ?? [])] .reverse() .find((message) => message.role === "assistant")?.renderKey ?? `assistant-${turn?.turnVersion ?? 0}`; useEffect(() => () => { if (undoTimer.current) clearTimeout(undoTimer.current); }, []); useEffect(() => { const reduceMotion = window.matchMedia("(prefers-reduced-motion: reduce)").matches; conversationEnd.current?.scrollIntoView({ behavior: controller.pending || submission !== null || reduceMotion ? "auto" : "smooth", block: "end", }); }, [controller.error, controller.pending, latestMessageText, messageCount, submission, turn?.turnVersion]); const pendingQuestion = turn?.status === "completed" ? turn.pendingConsultationQuestion : turn?.pendingConsultationQuestion ?? pendingConsultationQuestion ?? null; if (!turn) { return (
{openingAssistantText ? :
} {controller.error &&

{controller.error}

}
); } const canAnswer = turn.actions.includes("answer") && turn.status !== "abandoned" && turn.status !== "completed"; const canConfirm = turn.actions.includes("confirm") && turn.candidate.status === "ready_for_confirmation" && Boolean(turn.candidate.representativeTime); const canContinue = turn.actions.includes("continue_original_question") && Boolean(pendingQuestion) && Boolean(onContinueOriginalQuestion); const busy = controller.pending || submission !== null; const submit = () => { const text = controller.draft.trim(); if (!canAnswer || !text || busy) return; controller.setDraft(""); setSubmission({ text, phase: "undo", turnVersion: turn.turnVersion }); undoTimer.current = setTimeout(async () => { undoTimer.current = null; setSubmission({ text, phase: "generating", turnVersion: turn.turnVersion }); try { await controller.answer(undefined, text); } catch { controller.setDraft(text); } finally { setSubmission(null); composer.current?.focus(); } }, ANSWER_UNDO_WINDOW_MS); }; const copyMessage = async (message: ConversationalRectificationMessage) => { try { await navigator.clipboard.writeText(message.text); setCopiedMessageKey(message.renderKey); window.setTimeout(() => setCopiedMessageKey((current) => ( current === message.renderKey ? null : current )), 1_500); } catch { // Clipboard permission failures must not interrupt the conversation. } }; const regenerateMessage = async (messageKey: string) => { setRegeneratingMessageKey(messageKey); try { await controller.regenerate(); } finally { setRegeneratingMessageKey((current) => current === messageKey ? null : current); } }; const undoSubmission = () => { if (submission?.phase !== "undo") return; if (undoTimer.current) clearTimeout(undoTimer.current); undoTimer.current = null; controller.setDraft(submission.text); setSubmission(null); requestAnimationFrame(() => composer.current?.focus()); }; return (
{submission?.phase === "undo" ? "消息已发送,可以撤回修改" : controller.pending ? "Jyotisha 正在核对经历" : ""} {(controller.messages ?? [{ role: "assistant" as const, text: turn.narrative, renderKey: `assistant-${turn.turnVersion}`, }]).map((message) => (
{message.role === "assistant" && regeneratingMessageKey !== message.renderKey && (
)}
))} {submission && turn.turnVersion === submission.turnVersion && ( )} {controller.pending && canAnswer && regeneratingMessageKey === null && ( )} {controller.error &&

{controller.error}

}
{(canAnswer || canConfirm || canContinue) &&
{(canConfirm || canContinue) && (
{canConfirm && ( )} {canContinue && ( )}
)} {canAnswer && <> {controller.correctionTarget && (

正在更正:{controller.correctionTarget.dateLabel} · {controller.correctionTarget.summary}

)}
{ event.preventDefault(); submit(); }}>