"use client"; import { memo, type MutableRefObject } from "react"; import { ChatMessageActions, type ChatMessageFeedback, } from "@/components/chat-message-actions"; import { ChatMessageRow } from "@/components/chat-message-row"; import { RectificationChoiceCard } from "@/components/rectification-choice-card"; import type { AgentActivityTraceItem } from "@/lib/agent-activity-trace"; import type { ChatMessageView } from "@/lib/chat-message-view"; import { noteSettledRowRender, noteStreamingRowRender, noteUnsplitListRender, } from "@/lib/home-streaming-render-probe"; import type { CompletedActivityReceiptView } from "@/lib/rectification-activity-receipt"; import { CHOICE_MODE, CHOICE_SKIP_QUESTION_LABEL, CHOICE_SKIP_QUESTION_MESSAGE, CHOICE_STOP_LABEL, CHOICE_STOP_MESSAGE, type ChoiceKey, type RectificationChoiceCard as ChoiceCardModel, } from "@/lib/rectification-agentic/v9/choice-card"; import { questionIsAnswered, questionIsDeadUnanswered, type TurnQuestion, } from "@/lib/rectification-agentic/v9/turn-question"; import { RECTIFICATION_STOPPED_NOTICE } from "@/lib/rectification-surface-state"; import { rectificationTimelineRows } from "@/lib/rectification-timeline-adapter"; import { vargaSentenceFromMethods } from "@/lib/rectification-varga-sentence"; import { SIGN_LABELS, type SegmentSummary } from "@/lib/rectification-agentic/core/segment-summary"; export type RenderMessage = ChatMessageView & { renderKey: string; completedReceipt?: CompletedActivityReceiptView; failed?: boolean; stopped?: boolean; turnId?: string; activityTrace?: readonly AgentActivityTraceItem[]; question?: TurnQuestion; candidateOffer?: Readonly<{ resultId: string }>; segmentConsistency?: SegmentSummary; /** BUG-1136: what this evidence turn recorded, from the ledger. */ recordedEvidence?: readonly string[]; }; export type RectificationMessageActions = Readonly<{ submitChoice: (key: ChoiceKey) => void; submitStop: () => void; copyMessage: (message: RenderMessage) => void; onFeedback: (renderKey: string, requested: ChatMessageFeedback) => void; }>; export type RectificationMessageEntryProps = Readonly<{ message: RenderMessage; busy: boolean; readonly: boolean; currentQuestionFocusId: string | null; interactive: boolean; liveChoiceCard: ChoiceCardModel | null; choiceNonce: number; savedTime: string | null; copied: boolean; feedback: ChatMessageFeedback | undefined; actionsRef: MutableRefObject; }>; export function choiceCardFromQuestion( question: TurnQuestion, live: ChoiceCardModel | null, ): ChoiceCardModel | null { if (live && live.focus_id === question.focus_id && live.options.length === 4) { return { ...live, prompt: question.prompt }; } if (!question.options || question.options.length !== 4) return null; return { question_id: question.question_id, method_id: "", prompt: question.prompt, why: "", varga: null, choice_mode: CHOICE_MODE, options: question.options.map((option) => ({ key: option.key, label: option.label, answer_class: "unsure", role: "primary", })), stop_label: question.kind === "reverse_verify" ? CHOICE_SKIP_QUESTION_LABEL : CHOICE_STOP_LABEL, stop_message: question.kind === "reverse_verify" ? CHOICE_SKIP_QUESTION_MESSAGE : CHOICE_STOP_MESSAGE, scoring: question.kind !== "reverse_verify", probe_id: question.probe_id, case_revision: null, focus_id: question.focus_id, ...(live?.skip_this_probe ? { skip_this_probe: true } : {}), }; } function RectificationMessageEntryView({ message, busy, currentQuestionFocusId, interactive, liveChoiceCard, choiceNonce, savedTime, copied, feedback, actionsRef, }: RectificationMessageEntryProps) { const showActions = message.role === "assistant" && message.state === "settled" && !message.failed && Boolean(message.text); // BUG-1137 (D3): the step rows are the waiting state while the reply is // written; once it settles the rectification reply carries no // 「已完成 N 步」 receipt — those steps are the system's own bookkeeping. // Consultation replies keep theirs (ChatMessageRow is unchanged). const settled = message.state === "settled"; const displayedMessage: RenderMessage = { ...message, timeline: settled ? [] : rectificationTimelineRows({ trace: message.activityTrace, receipt: message.completedReceipt, activity: message.activity, settled, }), }; const vargaSentence = !message.failed && !settled ? vargaSentenceFromMethods(message.completedReceipt?.methods) : null; const question = message.question; const liveQuestion = Boolean( question && currentQuestionFocusId === question.focus_id && !questionIsAnswered(question) && interactive && ( question.kind === "collect_spoken" || ( liveChoiceCard && liveChoiceCard.focus_id === question.focus_id && (question.kind === "choice" || question.kind === "reverse_verify") ) ), ); // A superseded focus the reader never answered has no answer to show either: // its options would render greyed and unclickable (BUG-917). const unansweredDeadChoice = Boolean( question && (!questionIsAnswered(question) || (question.status === "superseded" && !question.answer_option)) && !liveQuestion && (question.kind === "choice" || question.kind === "reverse_verify") ); const embeddedCard = question && !unansweredDeadChoice ? choiceCardFromQuestion(question, liveQuestion ? liveChoiceCard : null) : null; // BUG-1135 (D1, supersedes BUG-917's 「stem without options」): a choice // nobody can answer any more draws nothing on its message — only the live // focus prints a stem, so the same stem never shows twice. const replacedQuestion = Boolean( question && unansweredDeadChoice && (questionIsDeadUnanswered(question) || (currentQuestionFocusId !== null && currentQuestionFocusId !== question.focus_id)), ); // BUG-1136 (D2): more than two recorded items read as 「记下了 N 件事」 in // the body; the items themselves (ledger lines) sit here, collapsed. const recorded = settled && (message.recordedEvidence?.length ?? 0) > 2 ? message.recordedEvidence! : null; const recordedList = recorded ? (
{`看记下的 ${recorded.length} 件`}
) : null; const questionBlock = question && displayedMessage.state === "settled" && !replacedQuestion ? (

{question.prompt}

{embeddedCard && ( actionsRef.current.submitChoice(key)} onStop={() => actionsRef.current.submitStop()} /> )} {question.status === "skipped" && (

{savedTime ? `已跳过(已采用 ${savedTime})` : "已跳过"}

)}
) : null; const afterAnswer = recordedList || questionBlock ? <>{recordedList}{questionBlock} : undefined; return ( <> {(!message.failed || Boolean(displayedMessage.text)) && ( )} {message.segmentConsistency?.no_rectification_needed && (
盘型一致,不用校正
    {message.segmentConsistency.charts.map((chart) => (
  • {chart.chart} 上升:{chart.sign === null ? "未核验" : SIGN_LABELS[chart.sign]}(全窗一致)
  • ))}

完整出生窗口内目标盘型一致;不等于确认了某一分钟。

)} {/* BUG-1056: copy and feedback only; the rectification reply has no rewrite action. */} {showActions && ( actionsRef.current.onFeedback(message.renderKey, requested)} onCopy={() => void actionsRef.current.copyMessage(message)} /> )} ); } export const RectificationMessageEntry = memo(function RectificationMessageEntry( props: RectificationMessageEntryProps, ) { if (props.message.state !== "settled") noteStreamingRowRender(); else noteSettledRowRender(); return ; }); export function UnsplitRectificationMessageList({ messages, copiedMessageKey, feedbackByKey, ...shared }: Omit & { messages: readonly RenderMessage[]; copiedMessageKey: string | null; feedbackByKey: Readonly>; }) { noteUnsplitListRender(); return ( <> {messages.map((message) => { noteSettledRowRender(); if (message.state !== "settled") noteStreamingRowRender(); return ( ); })} ); }