fix(rectification): stop re-parsing settled messages on every stream frame
This commit is contained in:
@@ -0,0 +1,259 @@
|
||||
"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 { emptyActivityTrace, 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,
|
||||
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";
|
||||
|
||||
export type RenderMessage = ChatMessageView & {
|
||||
renderKey: string;
|
||||
completedReceipt?: CompletedActivityReceiptView;
|
||||
failed?: boolean;
|
||||
stopped?: boolean;
|
||||
turnId?: string;
|
||||
activityTrace?: readonly AgentActivityTraceItem[];
|
||||
question?: TurnQuestion;
|
||||
candidateOffer?: Readonly<{ resultId: string }>;
|
||||
};
|
||||
|
||||
export type RectificationMessageActions = Readonly<{
|
||||
submitChoice: (key: ChoiceKey) => void;
|
||||
submitStop: () => void;
|
||||
copyMessage: (message: RenderMessage) => void;
|
||||
regenerateMessage: (message: RenderMessage) => void;
|
||||
onFeedback: (renderKey: string, requested: ChatMessageFeedback) => void;
|
||||
}>;
|
||||
|
||||
export type RectificationMessageEntryProps = Readonly<{
|
||||
message: RenderMessage;
|
||||
busy: boolean;
|
||||
readonly: boolean;
|
||||
regenerating: boolean;
|
||||
canRegenerate: boolean;
|
||||
currentQuestionFocusId: string | null;
|
||||
interactive: boolean;
|
||||
liveChoiceCard: ChoiceCardModel | null;
|
||||
choiceNonce: number;
|
||||
savedTime: string | null;
|
||||
copied: boolean;
|
||||
feedback: ChatMessageFeedback | undefined;
|
||||
actionsRef: MutableRefObject<RectificationMessageActions>;
|
||||
}>;
|
||||
|
||||
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,
|
||||
regenerating,
|
||||
canRegenerate,
|
||||
currentQuestionFocusId,
|
||||
interactive,
|
||||
liveChoiceCard,
|
||||
choiceNonce,
|
||||
savedTime,
|
||||
copied,
|
||||
feedback,
|
||||
actionsRef,
|
||||
}: RectificationMessageEntryProps) {
|
||||
const showActions = message.role === "assistant"
|
||||
&& message.state === "settled"
|
||||
&& !message.failed
|
||||
&& Boolean(message.text);
|
||||
const displayedMessage: RenderMessage = regenerating
|
||||
? {
|
||||
...message,
|
||||
text: "",
|
||||
thinkingText: undefined,
|
||||
state: "thinking" as const,
|
||||
activity: undefined,
|
||||
activityTrace: emptyActivityTrace(),
|
||||
completedReceipt: undefined,
|
||||
timeline: [],
|
||||
}
|
||||
: {
|
||||
...message,
|
||||
timeline: rectificationTimelineRows({
|
||||
trace: message.activityTrace,
|
||||
receipt: message.completedReceipt,
|
||||
activity: message.activity,
|
||||
settled: message.state === "settled",
|
||||
}),
|
||||
};
|
||||
const vargaSentence = !message.failed
|
||||
? vargaSentenceFromMethods(message.completedReceipt?.methods)
|
||||
: null;
|
||||
const question = regenerating ? undefined : 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")
|
||||
)
|
||||
),
|
||||
);
|
||||
const unansweredDeadChoice = Boolean(
|
||||
question
|
||||
&& !questionIsAnswered(question)
|
||||
&& !liveQuestion
|
||||
&& (question.kind === "choice" || question.kind === "reverse_verify")
|
||||
);
|
||||
const embeddedCard = question && !unansweredDeadChoice
|
||||
? choiceCardFromQuestion(question, liveQuestion ? liveChoiceCard : 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={(key) => actionsRef.current.submitChoice(key)}
|
||||
onStop={() => actionsRef.current.submitStop()}
|
||||
/>
|
||||
)}
|
||||
{question.status === "skipped" && (
|
||||
<p className="rectification-question-skipped" role="status">
|
||||
{savedTime ? `已跳过(已采用 ${savedTime})` : "已跳过"}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
: undefined;
|
||||
|
||||
return (
|
||||
<>
|
||||
{(!message.failed || Boolean(displayedMessage.text) || regenerating) && (
|
||||
<ChatMessageRow
|
||||
message={displayedMessage}
|
||||
showActivity={displayedMessage.state !== "settled"}
|
||||
vargaSentence={vargaSentence}
|
||||
afterAnswer={afterAnswer}
|
||||
stoppedNotice={message.stopped ? RECTIFICATION_STOPPED_NOTICE : undefined}
|
||||
/>
|
||||
)}
|
||||
{showActions && !regenerating && (
|
||||
<ChatMessageActions
|
||||
feedback={feedback}
|
||||
copied={copied}
|
||||
canRegenerate={canRegenerate}
|
||||
onFeedback={(requested) => actionsRef.current.onFeedback(message.renderKey, requested)}
|
||||
onCopy={() => void actionsRef.current.copyMessage(message)}
|
||||
onRegenerate={() => void actionsRef.current.regenerateMessage(message)}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export const RectificationMessageEntry = memo(function RectificationMessageEntry(
|
||||
props: RectificationMessageEntryProps,
|
||||
) {
|
||||
if (props.regenerating || props.message.state !== "settled") noteStreamingRowRender();
|
||||
else noteSettledRowRender();
|
||||
return <RectificationMessageEntryView {...props} />;
|
||||
});
|
||||
|
||||
export function UnsplitRectificationMessageList({
|
||||
messages,
|
||||
latestRegeneratableKey,
|
||||
copiedMessageKey,
|
||||
feedbackByKey,
|
||||
regeneratingMessageKey,
|
||||
...shared
|
||||
}: Omit<RectificationMessageEntryProps, "message" | "regenerating" | "canRegenerate" | "copied" | "feedback"> & {
|
||||
messages: readonly RenderMessage[];
|
||||
latestRegeneratableKey?: string;
|
||||
copiedMessageKey: string | null;
|
||||
feedbackByKey: Readonly<Record<string, ChatMessageFeedback | undefined>>;
|
||||
regeneratingMessageKey: string | null;
|
||||
}) {
|
||||
noteUnsplitListRender();
|
||||
return (
|
||||
<>
|
||||
{messages.map((message) => {
|
||||
noteSettledRowRender();
|
||||
if (message.state !== "settled") noteStreamingRowRender();
|
||||
return (
|
||||
<RectificationMessageEntryView
|
||||
key={message.renderKey}
|
||||
{...shared}
|
||||
message={message}
|
||||
regenerating={regeneratingMessageKey === message.renderKey}
|
||||
canRegenerate={message.renderKey === latestRegeneratableKey && shared.interactive}
|
||||
copied={copiedMessageKey === message.renderKey}
|
||||
feedback={feedbackByKey[message.renderKey]}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user