diff --git a/frontend/src/app/globals.css b/frontend/src/app/globals.css index 7cca7c46..302ff338 100644 --- a/frontend/src/app/globals.css +++ b/frontend/src/app/globals.css @@ -2942,6 +2942,30 @@ input:not([type="radio"]):not([type="checkbox"]):not([class^="ant-"]):not([class font-size: var(--type-caption); } +.rectification-question-slot { + display: grid; + gap: var(--space-2); + margin-block: var(--space-3); +} +.rectification-question-slot__spoken { + display: grid; + gap: var(--space-1); + padding: var(--space-3) var(--space-4); + border: 1px solid var(--color-border); + border-radius: var(--radius-md); + background: var(--color-canvas-soft); +} +.rectification-question-slot__prompt { + margin: 0; + color: var(--color-ink); + font-weight: 600; +} +.rectification-question-slot__hint, +.rectification-question-slot__status { + margin: 0; + color: var(--color-ink-secondary); + font-size: var(--type-caption); +} .rectification-snapshot { display: grid; gap: var(--space-3); diff --git a/frontend/src/components/rectification-agentic-chat.tsx b/frontend/src/components/rectification-agentic-chat.tsx index bc42c162..5785a7ee 100644 --- a/frontend/src/components/rectification-agentic-chat.tsx +++ b/frontend/src/components/rectification-agentic-chat.tsx @@ -55,7 +55,7 @@ import { stableChoiceActionKey, type ChoiceOptionId, } from "@/lib/rectification-agentic/v9/choice-action"; -import { finalizeRectificationSpokenAndThinking } from "@/lib/rectification-agentic/v9/spoken-answer"; +import { isRectificationCaseStatus, isResumableStatus, type RectificationCaseStatus } from "@/lib/rectification-agentic/v9/case-status"; import { isPersistedFocusId, parseRectificationChoiceCard, @@ -95,6 +95,23 @@ type PersistedTurn = Readonly<{ }>; type CandidateResult = RectificationCandidateResult | null; +type CurrentQuestionModel = Readonly<{ + kind: "choice" | "collect_spoken"; + prompt: string | null; +}>; + +function currentQuestionFromSnapshot(value: unknown): CurrentQuestionModel | null { + if (!value || typeof value !== "object" || Array.isArray(value)) return null; + const question = value as { kind?: unknown; prompt?: unknown }; + if (question.kind !== "choice" && question.kind !== "collect_spoken") return null; + return { + kind: question.kind, + prompt: typeof question.prompt === "string" && question.prompt.trim() + ? question.prompt.trim() + : null, + }; +} + function RectificationCandidateCards({ result, @@ -177,10 +194,6 @@ type RenderMessage = ChatMessageView & { choiceAttachment?: SettledChoiceAttachment; }; -function turnOfferedSelection(message: RenderMessage): boolean { - return Boolean(message.completedReceipt?.steps.includes("rectification-offer-candidates")); -} - function completedReceiptFromPersisted(receipt: PersistedTurn["receipt"]): CompletedActivityReceiptView { if (!receipt) return { steps: [], methods: [] }; if (Array.isArray(receipt.tool_activities)) { @@ -232,11 +245,10 @@ function messagesFromTurns(initialTurns: readonly PersistedTurn[]): RenderMessag const raw = failed ? "" : turn.text ?? ""; if (failed && !raw) return []; if (isIncompleteRunBanner(raw)) return []; - const split = raw ? finalizeRectificationSpokenAndThinking(raw) : { thinking: "", spoken: raw }; const completedReceipt = completedReceiptFromPersisted(turn.receipt); return [{ role: "assistant", - text: split.spoken, + text: raw, renderKey: key, state: turn.status === "completed" || failed ? "settled" : "thinking", completedReceipt, @@ -286,6 +298,9 @@ export function RectificationAgenticChat(props: RectificationAgenticChatProps) { const [savedStatus, setSavedStatus] = useState<"accepted" | "confirmed" | null>(null); const [candidateResult, setCandidateResult] = useState(null); const [choiceCard, setChoiceCard] = useState(null); + const [currentQuestion, setCurrentQuestion] = useState(null); + const [caseStatus, setCaseStatus] = useState(null); + const [caseSnapshotLoaded, setCaseSnapshotLoaded] = useState(false); const [acceptingCandidateId, setAcceptingCandidateId] = useState(null); const [feedback, setFeedback] = useState>({}); const [copiedMessageKey, setCopiedMessageKey] = useState(null); @@ -309,6 +324,7 @@ export function RectificationAgenticChat(props: RectificationAgenticChatProps) { const [boardDiff, setBoardDiff] = useState(() => diffRectificationBoard(null, null)); const boardId = useId(); const boardTitleId = useId(); + const questionHintId = useId(); useLayoutEffect(() => { const query = window.matchMedia(`(max-width: ${RECTIFICATION_BOARD_SPLIT_MIN_PX - 1}px)`); @@ -387,16 +403,28 @@ export function RectificationAgenticChat(props: RectificationAgenticChatProps) { // from parsing agent text or hidden sentinels. const applyCaseSnapshot = useCallback((payload: { latest_result?: unknown; + current_question?: unknown; choice_card?: unknown; - case?: { accepted_time?: unknown; confirmed_time?: unknown }; + case?: { + status?: unknown; + accepted_time?: unknown; + confirmed_time?: unknown; + }; } | null) => { if (!payload) return; const nextCandidate = parseRectificationCandidateResult(payload.latest_result); + const nextQuestion = currentQuestionFromSnapshot(payload.current_question); const nextChoice = parseRectificationChoiceCard(payload.choice_card); + const nextCaseStatus = isRectificationCaseStatus(payload.case?.status) + ? payload.case.status + : null; const acceptedTime = typeof payload.case?.accepted_time === "string" ? payload.case.accepted_time : null; const confirmedTime = typeof payload.case?.confirmed_time === "string" ? payload.case.confirmed_time : null; setCandidateResult(nextCandidate); + setCurrentQuestion(nextQuestion); setChoiceCard(nextChoice); + setCaseStatus(nextCaseStatus); + setCaseSnapshotLoaded(true); if (confirmedTime) { setSavedTime(confirmedTime); setSavedStatus("confirmed"); @@ -1025,19 +1053,10 @@ export function RectificationAgenticChat(props: RectificationAgenticChatProps) { ? [message.choiceAttachment.card.question_id] : []), ); - const liveChoiceHost = [...messages] - .reverse() - .find((message) => ( - message.role === "assistant" - && message.state === "settled" - && Boolean(message.text) - && !message.choiceAttachment - )); const showLiveChoiceCard = Boolean( - choiceCard - && liveChoiceHost + currentQuestion?.kind === "choice" + && choiceCard && !answeredQuestionIds.has(choiceCard.question_id) - && !turnOfferedSelection(liveChoiceHost) && !busy && !readonly && regeneratingMessageKey === null, @@ -1058,9 +1077,26 @@ export function RectificationAgenticChat(props: RectificationAgenticChatProps) { const selectionCardMessageKey = showSelectionCards && latestSettledAssistant ? latestSettledAssistant.renderKey : undefined; - const liveChoiceMessageKey = showLiveChoiceCard && liveChoiceHost - ? liveChoiceHost.renderKey - : undefined; + const collectSpokenPrompt = currentQuestion?.kind === "collect_spoken" + ? currentQuestion.prompt + : null; + const resumableCase = caseStatus !== null && isResumableStatus(caseStatus); + const showMissingQuestion = Boolean( + caseSnapshotLoaded + && resumableCase + && !readonly + && !busy + && currentQuestion === null, + ); + const showUnavailableQuestion = Boolean( + caseSnapshotLoaded + && resumableCase + && !readonly + && !busy + && currentQuestion !== null + && ((currentQuestion.kind === "choice" && !choiceCard) + || (currentQuestion.kind === "collect_spoken" && !collectSpokenPrompt)), + ); const canSend = !busy && !readonly && !regeneratingMessageKey; function submitChoice(key: ChoiceKey) { @@ -1125,10 +1161,7 @@ export function RectificationAgenticChat(props: RectificationAgenticChatProps) { } : message; const settledChoice = message.choiceAttachment; - const liveChoice = showLiveChoiceCard && message.renderKey === liveChoiceMessageKey - ? choiceCard - : null; - const visibleChoiceCard = settledChoice?.card ?? liveChoice; + const visibleChoiceCard = settledChoice?.card; const vargaSentence = !message.failed ? vargaSentenceFromMethods(message.completedReceipt?.methods) : null; @@ -1173,7 +1206,7 @@ export function RectificationAgenticChat(props: RectificationAgenticChatProps) { ? `${visibleChoiceCard.question_id}:answered:${settledChoice.selectedKey}` : `${visibleChoiceCard.question_id}:${choiceNonce}`} card={visibleChoiceCard} - pending={Boolean(liveChoice) && busy} + pending={false} disabled={readonly || Boolean(settledChoice)} selectedKey={settledChoice?.selectedKey ?? ""} onSelect={submitChoice} @@ -1183,6 +1216,37 @@ export function RectificationAgenticChat(props: RectificationAgenticChatProps) { ); })} +
+ {showLiveChoiceCard && choiceCard && ( + + )} + {collectSpokenPrompt && ( +
+

{collectSpokenPrompt}

+

+ 请在下方输入框回答,可以写你记得的年份、经过和结果。 +

+
+ )} + {showMissingQuestion && ( +

+ 当前没有可回答的问题,正在等待服务端更新。 +

+ )} + {showUnavailableQuestion && ( +

+ 当前问题暂时无法显示,请等待服务端更新。 +

+ )} +
{savedTime && savedStatus === "confirmed" && (

已确认校正时间:{savedTime} @@ -1221,13 +1285,16 @@ export function RectificationAgenticChat(props: RectificationAgenticChatProps) {