fix(rectification): put each question inside the assistant message
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:
@@ -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}
|
||||
|
||||
Reference in New Issue
Block a user