fix(rectification): put each question inside the assistant message
Independent Staging Quality Gate / validate (push) Successful in 25m7s
Independent Staging Quality Gate / publish (push) Successful in 2m8s

Focuses now carry asked_turn_id so GET rebuilds stem and options on the
same turn. Agent writes spokenPrompt; the live question slot is gone.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Jesse_Chen
2026-09-02 19:55:20 +08:00
co-authored by Cursor
parent a594990a16
commit d94049769e
42 changed files with 1760 additions and 475 deletions
+8 -3
View File
@@ -5,7 +5,7 @@ import { prefetchOnIdle } from "@/components/chat-chunk-prefetch";
import { ChatMessageContent } from "@/components/chat-message-content";
import { ConsultationRunTimeline } from "@/components/consultation-run-timeline";
import type { ChatMessageView } from "@/lib/chat-message-view";
import { useEffect, useLayoutEffect, useRef } from "react";
import { useEffect, useLayoutEffect, useRef, type ReactNode } from "react";
type GsapCore = typeof import("gsap")["gsap"];
@@ -39,10 +39,12 @@ export function ChatMessageRow({
message,
showActivity = message.state !== "settled",
vargaSentence,
afterAnswer,
}: Readonly<{
message: ChatMessageView;
showActivity?: boolean;
vargaSentence?: string | null;
afterAnswer?: ReactNode;
}>) {
const messageRow = useRef<HTMLElement>(null);
const assistantLabel = message.state === "thinking"
@@ -137,8 +139,9 @@ export function ChatMessageRow({
{hasAnswer ? (
<section className="consultation-report-analysis" aria-label="回复">
{spokenAnswer}
{afterAnswer}
</section>
) : null}
) : afterAnswer}
</div>
) : (
<>
@@ -147,17 +150,19 @@ export function ChatMessageRow({
{thinkingPanel}
<section className="consultation-report-analysis" aria-label="回复">
{spokenAnswer}
{afterAnswer}
</section>
</div>
) : (
<>
{thinkingPanel}
{spokenAnswer}
{afterAnswer}
</>
)}
</>
)
) : <p>{message.text}</p>}
) : <><p>{message.text}</p>{afterAnswer}</>}
</div>
</div>
</article>
@@ -9,6 +9,7 @@ export type PersistedRectificationTurn = Readonly<{
role: "user" | "assistant";
text: string | null;
status: string;
question?: unknown;
receipt?: Readonly<{
status: string;
phases: readonly string[];
@@ -57,13 +57,7 @@ import {
type ChoiceOptionId,
} from "@/lib/rectification-agentic/v9/choice-action";
import { isRectificationCaseStatus, isResumableStatus, type RectificationCaseStatus } from "@/lib/rectification-agentic/v9/case-status";
import { composeCollectSpokenAssistantText } from "@/lib/rectification-agentic/v9/collect-prompt";
import {
isPersistedFocusId,
parseRectificationChoiceCard,
type ChoiceKey,
type RectificationChoiceCard as ChoiceCardModel,
} from "@/lib/rectification-agentic/v9/choice-card";
import { CHOICE_MODE, CHOICE_STOP_LABEL, isPersistedFocusId, parseRectificationChoiceCard, type ChoiceKey, type RectificationChoiceCard as ChoiceCardModel } from "@/lib/rectification-agentic/v9/choice-card";
import type { PublicLanguageModel } from "@/lib/public-models";
import { useConversationScrollAnchor } from "@/hooks/use-conversation-scroll-anchor";
import { CharacterRemaining } from "./character-remaining";
@@ -77,13 +71,15 @@ import {
import { ModelSelector } from "./model-selector";
import { RectificationBoard, RectificationBoardPeek } from "./rectification-board";
import { RectificationChoiceCard } from "./rectification-choice-card";
import { Button } from "./ui/button";
import { copyTextForMessage, parseTurnQuestion, questionIsAnswered, type TurnQuestion } from "@/lib/rectification-agentic/v9/turn-question";
import { Button } from "@/components/ui/button";
type PersistedTurn = Readonly<{
id: string;
role: "user" | "assistant";
text: string | null;
status: string;
question?: unknown;
receipt?: Readonly<{
status: string;
phases: readonly string[];
@@ -106,40 +102,24 @@ type CandidateResult = RectificationCandidateResult | null;
type CurrentQuestionModel = Readonly<{
kind: "choice" | "collect_spoken";
prompt: string | null;
focus_id: string | null;
question_id: 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 };
const question = value as { kind?: unknown; prompt?: unknown; focus_id?: unknown; question_id?: 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,
focus_id: typeof question.focus_id === "string" ? question.focus_id : null,
question_id: typeof question.question_id === "string" ? question.question_id : null,
};
}
function collectSpokenPromptFromQuestion(question: CurrentQuestionModel | null): string | null {
return question?.kind === "collect_spoken" ? question.prompt : null;
}
function attachCollectSpokenStem(current: RenderMessage[], prompt: string): RenderMessage[] {
const last = [...current].reverse().find((message) => (
message.role === "assistant"
&& message.state === "settled"
&& !message.failed
&& Boolean(message.text)
));
if (!last) return current;
const combined = composeCollectSpokenAssistantText(last.text, prompt);
if (combined === last.text) return current;
return current.map((item) => (
item.renderKey === last.renderKey ? { ...item, text: combined } : item
));
}
function RectificationCandidateCards({
result,
acceptingCandidateId,
@@ -207,20 +187,93 @@ type RectificationAgenticChatProps = Readonly<{
headerSlot: HTMLElement | null;
}>;
type SettledChoiceAttachment = Readonly<{
card: ChoiceCardModel;
selectedKey: ChoiceKey | "stop";
}>;
type RenderMessage = ChatMessageView & {
renderKey: string;
completedReceipt?: CompletedActivityReceiptView;
failed?: boolean;
turnId?: string;
activityTrace?: readonly AgentActivityTraceItem[];
choiceAttachment?: SettledChoiceAttachment;
question?: TurnQuestion;
};
function mergeTurnQuestions(current: RenderMessage[], turns: readonly unknown[]): RenderMessage[] {
const byId = new Map<string, TurnQuestion | null>();
for (const item of turns) {
if (!item || typeof item !== "object") continue;
const turn = item as { id?: unknown; question?: unknown };
if (typeof turn.id !== "string") continue;
byId.set(turn.id, parseTurnQuestion(turn.question));
}
return current.map((message) => {
if (!message.turnId || !byId.has(message.turnId)) return message;
const question = byId.get(message.turnId) ?? undefined;
return { ...message, question: question ?? undefined };
});
}
function markQuestionAnswered(
current: RenderMessage[],
focusId: string,
selected: ChoiceKey | "stop" | "typed",
): RenderMessage[] {
return current.map((message) => {
const question = message.question;
if (!question || question.focus_id !== focusId || questionIsAnswered(question)) return message;
return {
...message,
question: {
...question,
status: "resolved",
answer_option: selected === "typed" ? null : selected,
},
};
});
}
function snapshotTurns(payload: { turns?: unknown } | null | undefined): readonly unknown[] {
return Array.isArray(payload?.turns) ? payload.turns : [];
}
function appendUnseenAssistantTurns(current: RenderMessage[], turns: readonly unknown[]): RenderMessage[] {
const known = new Set(current.flatMap((message) => message.turnId ? [message.turnId] : []));
const extras = messagesFromTurns(turns as readonly PersistedTurn[]).filter((message) => (
message.role === "assistant"
&& message.turnId
&& !known.has(message.turnId)
));
return extras.length ? [...current, ...extras] : current;
}
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: CHOICE_STOP_LABEL,
stop_message: "先这样",
scoring: question.kind !== "reverse_verify",
probe_id: question.probe_id,
case_revision: null,
focus_id: question.focus_id,
};
}
function completedReceiptFromPersisted(receipt: PersistedTurn["receipt"]): CompletedActivityReceiptView {
if (!receipt) return { steps: [], methods: [] };
if (Array.isArray(receipt.tool_activities)) {
@@ -282,6 +335,7 @@ function messagesFromTurns(initialTurns: readonly PersistedTurn[]): RenderMessag
activityTrace: activityTraceFromReceipt(completedReceipt),
failed,
turnId: turn.id,
question: parseTurnQuestion(turn.question) ?? undefined,
}];
}
if (isIncompleteRunBanner(turn.text ?? "")) return [];
@@ -392,6 +446,7 @@ export function RectificationAgenticChat(props: RectificationAgenticChatProps) {
latest_result?: unknown;
current_question?: unknown;
choice_card?: unknown;
turns?: unknown;
case?: {
status?: unknown;
accepted_time?: unknown;
@@ -421,7 +476,10 @@ export function RectificationAgenticChat(props: RectificationAgenticChatProps) {
}
}, []);
const loadCaseSnapshot = useCallback(async (): Promise<CurrentQuestionModel | null | undefined> => {
const loadCaseSnapshot = useCallback(async (): Promise<{
question: CurrentQuestionModel | null;
turns: readonly unknown[];
} | null | undefined> => {
try {
const response = await fetch(
`/api/rectification/cases/${encodeURIComponent(caseId)}?sessionId=${encodeURIComponent(sessionId)}`,
@@ -430,7 +488,10 @@ export function RectificationAgenticChat(props: RectificationAgenticChatProps) {
if (!response.ok) return undefined;
const payload = await response.json().catch(() => null);
applyCaseSnapshot(payload);
return currentQuestionFromSnapshot(payload?.current_question);
return {
question: currentQuestionFromSnapshot(payload?.current_question),
turns: snapshotTurns(payload),
};
} catch {
// Snapshot refresh is best-effort; the durable Case remains on the server.
return undefined;
@@ -447,8 +508,6 @@ export function RectificationAgenticChat(props: RectificationAgenticChatProps) {
.then((payload) => {
if (controller.signal.aborted) return;
applyCaseSnapshot(payload);
const prompt = collectSpokenPromptFromQuestion(currentQuestionFromSnapshot(payload?.current_question));
if (prompt) setMessages((current) => attachCollectSpokenStem(current, prompt));
})
.catch(() => {
// Snapshot refresh is best-effort; the durable Case remains on the server.
@@ -468,10 +527,10 @@ export function RectificationAgenticChat(props: RectificationAgenticChatProps) {
const userRenderKey = `v9-user-${turnKey}`;
const assistantRenderKey = `v9-assistant-${turnKey}`;
const pendingCollectPrompt = collectSpokenPromptFromQuestion(currentQuestionRef.current);
const pendingFocusId = currentQuestionRef.current?.focus_id;
setMessages((current) => [
...(action === "message" && pendingCollectPrompt
? attachCollectSpokenStem(current, pendingCollectPrompt)
...(action === "message" && pendingFocusId
? markQuestionAnswered(current, pendingFocusId, "typed")
: current),
...(action === "message"
? [{ role: "user", text: trimmed, renderKey: userRenderKey, state: "settled" } satisfies RenderMessage]
@@ -679,16 +738,12 @@ export function RectificationAgenticChat(props: RectificationAgenticChatProps) {
const parsed = completed && !streamFailed ? parseAgentReply(raw) : { text: "", title: undefined };
const succeeded = completed && !streamFailed && Boolean(parsed.text);
const collectPrompt = collectSpokenPromptFromQuestion(currentQuestionRef.current);
const settledText = succeeded && collectPrompt
? composeCollectSpokenAssistantText(parsed.text, collectPrompt)
: parsed.text;
setMessages((current) => current.flatMap((message): RenderMessage[] => {
if (message.renderKey !== assistantRenderKey) return [message];
if (succeeded) {
return [{
...message,
text: settledText,
text: parsed.text,
activityTrace: completeActivityTrace(activityTrace),
state: "settled",
completedReceipt,
@@ -718,18 +773,13 @@ export function RectificationAgenticChat(props: RectificationAgenticChatProps) {
setError((current) => current || userFacingRunFailure("run_failed"));
}
if (succeeded) {
const snapshotPrompt = collectSpokenPromptFromQuestion(await loadCaseSnapshot() ?? null);
const attachedText = snapshotPrompt
? composeCollectSpokenAssistantText(parsed.text, snapshotPrompt)
: settledText;
if (attachedText !== settledText) {
setMessages((current) => current.map((message) => (
message.renderKey === assistantRenderKey ? { ...message, text: attachedText } : message
)));
const snapshot = await loadCaseSnapshot();
if (snapshot?.turns.length) {
setMessages((current) => mergeTurnQuestions(current, snapshot.turns));
}
onMessagesChange?.([
...(action === "message" ? [{ role: "user" as const, text: trimmed }] : []),
{ role: "assistant", text: attachedText },
{ role: "assistant", text: parsed.text },
]);
onCompleted?.();
}
@@ -797,26 +847,12 @@ export function RectificationAgenticChat(props: RectificationAgenticChatProps) {
}
const questionId = choiceCard.question_id;
const actionId = actionIdForChoice(focusId, optionId);
const answeredCard = choiceCard;
const offeringRenderKey = [...messages].reverse().find((message) => (
message.role === "assistant"
&& message.state === "settled"
&& Boolean(message.text)
&& !message.choiceAttachment
))?.renderKey ?? null;
setError("");
keyCounter.current += 1;
const turnKey = keyCounter.current;
const assistantRenderKey = `v9-choice-assistant-${turnKey}`;
setMessages((current) => [
...current.map((message) => (
offeringRenderKey && message.renderKey === offeringRenderKey
? {
...message,
choiceAttachment: { card: answeredCard, selectedKey: optionId },
}
: message
)),
...markQuestionAnswered(current, focusId, optionId),
{
role: "assistant",
text: "",
@@ -852,13 +888,7 @@ export function RectificationAgenticChat(props: RectificationAgenticChatProps) {
});
const payload = await response.json().catch(() => null);
if (!response.ok) {
setMessages((current) => current.flatMap((message) => {
if (message.renderKey === assistantRenderKey) return [];
if (offeringRenderKey && message.renderKey === offeringRenderKey) {
return [{ ...message, choiceAttachment: undefined }];
}
return [message];
}));
setMessages((current) => current.filter((message) => message.renderKey !== assistantRenderKey));
setChoiceNonce((current) => current + 1);
if (payload?.code === "profile_incomplete") {
onProfileIncomplete?.();
@@ -869,34 +899,42 @@ export function RectificationAgenticChat(props: RectificationAgenticChatProps) {
}
const willContinue = shouldContinueAfterStructuredChoice(payload?.nextAction, payload);
onCompleted?.();
await loadCaseSnapshot();
const snapshot = await loadCaseSnapshot();
const turns = snapshot?.turns ?? [];
if (willContinue) {
setMessages((current) => current.filter((message) => message.renderKey !== assistantRenderKey));
setMessages((current) => mergeTurnQuestions(
current.filter((message) => message.renderKey !== assistantRenderKey),
turns,
));
choiceContinuationPending.current = true;
} else {
const narration = typeof payload?.narration === "string" && payload.narration.trim()
? payload.narration.trim()
: "已记录你的选择。";
setMessages((current) => current.map((message) => message.renderKey === assistantRenderKey
? {
...message,
text: narration,
state: "settled" as const,
activity: undefined,
}
: message));
setMessages((current) => {
const withoutPlaceholder = current.filter((message) => message.renderKey !== assistantRenderKey);
const withHistory = appendUnseenAssistantTurns(
mergeTurnQuestions(withoutPlaceholder, turns),
turns,
);
if (withHistory.length > withoutPlaceholder.length) return withHistory;
return [
...withHistory,
{
role: "assistant" as const,
text: narration,
renderKey: assistantRenderKey,
state: "settled" as const,
activity: undefined,
},
];
});
onMessagesChange?.([
{ role: "assistant", text: narration },
]);
}
} catch {
setMessages((current) => current.flatMap((message) => {
if (message.renderKey === assistantRenderKey) return [];
if (offeringRenderKey && message.renderKey === offeringRenderKey) {
return [{ ...message, choiceAttachment: undefined }];
}
return [message];
}));
setMessages((current) => current.filter((message) => message.renderKey !== assistantRenderKey));
setChoiceNonce((current) => current + 1);
setError("选择题处理失败,请稍后重试。");
} finally {
@@ -908,7 +946,6 @@ export function RectificationAgenticChat(props: RectificationAgenticChatProps) {
caseId,
choiceCard,
loadCaseSnapshot,
messages,
onCompleted,
onMessagesChange,
onProfileIncomplete,
@@ -997,7 +1034,7 @@ export function RectificationAgenticChat(props: RectificationAgenticChatProps) {
async function copyMessage(message: RenderMessage) {
try {
await navigator.clipboard.writeText(message.text);
await navigator.clipboard.writeText(copyTextForMessage(message.text, message.question));
setCopiedMessageKey(message.renderKey);
window.setTimeout(() => setCopiedMessageKey((current) => (
current === message.renderKey ? null : current
@@ -1028,10 +1065,7 @@ export function RectificationAgenticChat(props: RectificationAgenticChatProps) {
if (!response.ok || payload?.ok !== true || typeof payload.assistantMessage !== "string") {
throw new Error(payload?.message || payload?.error || "暂时无法重新生成回答");
}
const collectPrompt = collectSpokenPromptFromQuestion(currentQuestion);
const nextText = collectPrompt
? composeCollectSpokenAssistantText(payload.assistantMessage, collectPrompt)
: payload.assistantMessage;
const nextText = payload.assistantMessage;
setMessages((current) => current.map((item) => item.renderKey === message.renderKey
? { ...item, text: nextText, state: "settled" }
: item));
@@ -1067,15 +1101,15 @@ export function RectificationAgenticChat(props: RectificationAgenticChatProps) {
&& !message.failed
&& Boolean(message.text)
));
const answeredQuestionIds = new Set(
messages.flatMap((message) => message.choiceAttachment
? [message.choiceAttachment.card.question_id]
: []),
);
const latestLiveQuestion = [...messages].reverse().find((message) => (
message.role === "assistant"
&& message.question
&& !questionIsAnswered(message.question)
))?.question ?? null;
const showLiveChoiceCard = Boolean(
currentQuestion?.kind === "choice"
&& choiceCard
&& !answeredQuestionIds.has(choiceCard.question_id)
latestLiveQuestion
&& latestLiveQuestion.options?.length === 4
&& currentQuestion?.focus_id === latestLiveQuestion.focus_id
&& !busy
&& !readonly
&& regeneratingMessageKey === null,
@@ -1097,6 +1131,14 @@ export function RectificationAgenticChat(props: RectificationAgenticChatProps) {
? currentQuestion.prompt
: null;
const resumableCase = caseStatus !== null && isResumableStatus(caseStatus);
const liveQuestionOnMessages = messages.some((message) => (
message.role === "assistant"
&& message.state === "settled"
&& message.question
&& currentQuestion
&& message.question.focus_id === currentQuestion.focus_id
&& !questionIsAnswered(message.question)
));
const showMissingQuestion = Boolean(
caseSnapshotLoaded
&& resumableCase
@@ -1110,13 +1152,7 @@ export function RectificationAgenticChat(props: RectificationAgenticChatProps) {
&& !readonly
&& !busy
&& currentQuestion !== null
&& ((currentQuestion.kind === "choice" && !choiceCard)
|| (currentQuestion.kind === "collect_spoken" && !collectSpokenPrompt)),
);
const showQuestionSlot = Boolean(
showLiveChoiceCard
|| showMissingQuestion
|| showUnavailableQuestion,
&& !liveQuestionOnMessages,
);
const canSend = !busy && !readonly && !regeneratingMessageKey;
@@ -1191,28 +1227,48 @@ export function RectificationAgenticChat(props: RectificationAgenticChatProps) {
settled: message.state === "settled",
}),
};
const settledChoice = message.choiceAttachment;
const visibleChoiceCard = settledChoice?.card;
const vargaSentence = !message.failed
? vargaSentenceFromMethods(message.completedReceipt?.methods)
: null;
const isLatestMessage = message.renderKey === messages[messages.length - 1]?.renderKey;
const bubbleText = !regenerating
&& isLatestMessage
&& message.role === "assistant"
&& collectSpokenPrompt
? composeCollectSpokenAssistantText(displayedMessage.text, collectSpokenPrompt)
: displayedMessage.text;
const bubbleMessage = bubbleText === displayedMessage.text
? displayedMessage
: { ...displayedMessage, text: bubbleText };
const question = regenerating ? undefined : message.question;
const liveQuestion = Boolean(
question
&& currentQuestion?.focus_id === question.focus_id
&& !questionIsAnswered(question)
&& choiceCard
&& choiceCard.focus_id === question.focus_id
&& !busy
&& !readonly
&& regeneratingMessageKey === null,
);
const embeddedCard = question ? choiceCardFromQuestion(question, liveQuestion ? choiceCard : null) : null;
const afterAnswer = question && displayedMessage.state === "settled"
? (
<div className="rectification-message-question">
<p className="rectification-message-question__prompt">{question.prompt}</p>
{embeddedCard && (
<RectificationChoiceCard
key={`${embeddedCard.question_id}:${question.answer_option ?? choiceNonce}:${question.status}`}
variant="embedded"
card={embeddedCard}
pending={busy && liveQuestion}
disabled={!liveQuestion}
selectedKey={question.answer_option ?? ""}
onSelect={submitChoice}
onStop={submitStop}
/>
)}
</div>
)
: undefined;
return (
<div key={message.renderKey} className="rectification-message-wrap rectification-message-entry">
{(!message.failed || Boolean(bubbleMessage.text) || regenerating) && (
{(!message.failed || Boolean(displayedMessage.text) || regenerating) && (
<ChatMessageRow
message={bubbleMessage}
message={displayedMessage}
showActivity={displayedMessage.state !== "settled"}
vargaSentence={vargaSentence}
afterAnswer={afterAnswer}
/>
)}
{showActions && !regenerating && (
@@ -1224,7 +1280,7 @@ export function RectificationAgenticChat(props: RectificationAgenticChatProps) {
...current,
[message.renderKey]: toggleRectificationFeedback(current[message.renderKey], requested),
}))}
onCopy={() => void copyMessage({ ...message, text: bubbleText })}
onCopy={() => void copyMessage(message)}
onRegenerate={() => void regenerateMessage(message)}
/>
)}
@@ -1236,72 +1292,9 @@ export function RectificationAgenticChat(props: RectificationAgenticChatProps) {
onAccept={(candidateId) => void acceptCandidate(candidateId)}
/>
)}
{visibleChoiceCard && (
<RectificationChoiceCard
key={settledChoice
? `${visibleChoiceCard.question_id}:answered:${settledChoice.selectedKey}`
: `${visibleChoiceCard.question_id}:${choiceNonce}`}
card={visibleChoiceCard}
pending={false}
disabled={readonly || Boolean(settledChoice)}
selectedKey={settledChoice?.selectedKey ?? ""}
onSelect={submitChoice}
onStop={submitStop}
/>
)}
{isLatestMessage && showQuestionSlot && (
<section className="rectification-question-slot" aria-label="当前问题">
{showLiveChoiceCard && choiceCard && (
<RectificationChoiceCard
key={`${choiceCard.question_id}:${choiceNonce}`}
card={choiceCard}
pending={busy}
disabled={readonly}
selectedKey=""
onSelect={submitChoice}
onStop={submitStop}
/>
)}
{showMissingQuestion && (
<p className="rectification-question-slot__status" role="status">
</p>
)}
{showUnavailableQuestion && (
<p className="rectification-question-slot__status" role="status">
</p>
)}
</section>
)}
</div>
);
})}
{messages.length === 0 && showQuestionSlot && (
<section className="rectification-question-slot" aria-label="当前问题">
{showLiveChoiceCard && choiceCard && (
<RectificationChoiceCard
key={`${choiceCard.question_id}:${choiceNonce}`}
card={choiceCard}
pending={busy}
disabled={readonly}
selectedKey=""
onSelect={submitChoice}
onStop={submitStop}
/>
)}
{showMissingQuestion && (
<p className="rectification-question-slot__status" role="status">
</p>
)}
{showUnavailableQuestion && (
<p className="rectification-question-slot__status" role="status">
</p>
)}
</section>
)}
{savedTime && savedStatus === "confirmed" && (
<p className="rectification-saved" role="status">
{savedTime}
@@ -1328,6 +1321,13 @@ export function RectificationAgenticChat(props: RectificationAgenticChatProps) {
{!conversationAnchor.anchored && (
<JumpToLatestButton onClick={conversationAnchor.anchorToLatest} />
)}
{(showMissingQuestion || showUnavailableQuestion) && (
<p className="rectification-composer-status" role="status">
{showMissingQuestion
? "当前没有可回答的问题,正在等待服务端更新。"
: "当前问题暂时无法显示,请等待服务端更新。"}
</p>
)}
<ChatComposer
inputRef={composer}
value={draft}
@@ -13,6 +13,8 @@ type RectificationChoiceCardProps = Readonly<{
selectedKey?: ChoiceKey | "stop" | "";
onSelect: (key: ChoiceKey) => void;
onStop: () => void;
variant?: "card" | "embedded";
hidePrompt?: boolean;
}>;
export function RectificationChoiceCard(props: RectificationChoiceCardProps) {
@@ -38,9 +40,12 @@ export function RectificationChoiceCard(props: RectificationChoiceCardProps) {
props.onStop();
}
const embedded = props.variant === "embedded";
const hidePrompt = embedded || props.hidePrompt === true;
return (
<section
className={`rectification-choice-card${props.pending ? " is-pending" : ""}${answered ? " is-answered" : ""}`}
className={`rectification-choice-card${embedded ? " is-embedded" : ""}${props.pending ? " is-pending" : ""}${answered ? " is-answered" : ""}`}
aria-label={props.card.scoring ? "校正判断" : "盘外核对"}
>
<fieldset
@@ -48,8 +53,10 @@ export function RectificationChoiceCard(props: RectificationChoiceCardProps) {
disabled={props.pending || props.disabled || answered}
data-answered={answered ? "true" : "false"}
>
<legend>{props.card.prompt}</legend>
{props.card.why ? <p className="rectification-choice-why">{props.card.why}</p> : null}
{hidePrompt
? <legend className="sr-only">{props.card.prompt}</legend>
: <legend>{props.card.prompt}</legend>}
{!hidePrompt && props.card.why ? <p className="rectification-choice-why">{props.card.why}</p> : null}
<div className="birth-time-primary-choices">
{props.card.options.map((option, index) => (
<button