fix(rectification): stop re-parsing settled messages on every stream frame
This commit is contained in:
@@ -53,19 +53,37 @@ function renderProse(text: string, renderMarkdown: MarkdownRenderer | undefined)
|
||||
));
|
||||
}
|
||||
|
||||
const settledMarkdownNodes = new WeakMap<MarkdownRenderer, Map<string, ReactNode>>();
|
||||
|
||||
function rememberedProse(text: string, renderMarkdown: MarkdownRenderer | undefined): ReactNode {
|
||||
if (!text) return null;
|
||||
if (!renderMarkdown) return renderProse(text, undefined);
|
||||
let byText = settledMarkdownNodes.get(renderMarkdown);
|
||||
if (!byText) {
|
||||
byText = new Map();
|
||||
settledMarkdownNodes.set(renderMarkdown, byText);
|
||||
}
|
||||
const cached = byText.get(text);
|
||||
if (cached !== undefined) return cached;
|
||||
const node = renderMarkdown(text);
|
||||
byText.set(text, node);
|
||||
return node;
|
||||
}
|
||||
|
||||
/**
|
||||
* The completed part of a streaming answer. `memo` keeps React from calling the
|
||||
* markdown parser again while `text` is unchanged, so a frame that only grew
|
||||
* the tail costs one small parse instead of one over the whole answer.
|
||||
* Settled prose and the completed prefix of a streaming answer. `memo` plus a
|
||||
* content-keyed parse cache keep React from calling the markdown parser again
|
||||
* while `text` is unchanged, so a parent re-render (or a frame that only grew
|
||||
* the tail) does not parse the whole answer.
|
||||
*/
|
||||
const StableMarkdownPrefix = memo(function StableMarkdownPrefix({
|
||||
export const StableMarkdownPrefix = memo(function StableMarkdownPrefix({
|
||||
text,
|
||||
renderMarkdown,
|
||||
}: Readonly<{
|
||||
text: string;
|
||||
renderMarkdown: MarkdownRenderer | undefined;
|
||||
}>) {
|
||||
return <>{renderProse(text, renderMarkdown)}</>;
|
||||
return <>{rememberedProse(text, renderMarkdown)}</>;
|
||||
});
|
||||
|
||||
export function StreamingMarkdown({
|
||||
@@ -104,7 +122,9 @@ export function ChatMessageContent({
|
||||
const reportBody = !streaming && report
|
||||
? (
|
||||
<>
|
||||
<div className="message-markdown">{renderProse(report, renderMarkdown)}</div>
|
||||
<div className="message-markdown">
|
||||
<StableMarkdownPrefix text={report} renderMarkdown={renderMarkdown} />
|
||||
</div>
|
||||
{foldedAudit}
|
||||
</>
|
||||
)
|
||||
@@ -116,7 +136,7 @@ export function ChatMessageContent({
|
||||
<div className="message-markdown">
|
||||
{streaming
|
||||
? <StreamingMarkdown text={spoken} renderMarkdown={renderMarkdown} />
|
||||
: renderProse(spoken, renderMarkdown)}
|
||||
: <StableMarkdownPrefix text={spoken} renderMarkdown={renderMarkdown} />}
|
||||
</div>
|
||||
) : null}
|
||||
{report
|
||||
|
||||
@@ -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, type ReactNode } from "react";
|
||||
import { memo, useEffect, useLayoutEffect, useRef, type ReactNode } from "react";
|
||||
|
||||
type GsapCore = typeof import("gsap")["gsap"];
|
||||
|
||||
@@ -35,7 +35,7 @@ export function AgentAvatar() {
|
||||
return <span className="agent-avatar" aria-hidden="true" />;
|
||||
}
|
||||
|
||||
export function ChatMessageRow({
|
||||
export const ChatMessageRow = memo(function ChatMessageRow({
|
||||
message,
|
||||
showActivity = message.state !== "settled",
|
||||
vargaSentence,
|
||||
@@ -170,4 +170,4 @@ export function ChatMessageRow({
|
||||
</div>
|
||||
</article>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -5,7 +5,7 @@ import { useQueuedMessage } from "@/hooks/use-queued-message";
|
||||
import { appendQueuedText, queuedDraftSettleAction } from "@/lib/queued-draft";
|
||||
import { createPortal } from "react-dom";
|
||||
import { parseAgentReply } from "@/lib/agent-reply";
|
||||
import { nextActivityView, type AgentActivityView, type ChatMessage, type ChatMessageView } from "@/lib/chat-message-view";
|
||||
import { nextActivityView, type AgentActivityView, type ChatMessage } from "@/lib/chat-message-view";
|
||||
import { createStreamFrameBuffer } from "@/lib/stream-frame-buffer";
|
||||
import {
|
||||
completeActivityTrace,
|
||||
@@ -44,7 +44,6 @@ import {
|
||||
diffRectificationBoard,
|
||||
RECTIFICATION_BOARD_SPLIT_MIN_PX,
|
||||
} from "@/lib/rectification-board-model";
|
||||
import { rectificationTimelineRows } from "@/lib/rectification-timeline-adapter";
|
||||
import {
|
||||
rectificationAdoptingLabel,
|
||||
RECTIFICATION_EMPTY_ACTION_LABEL,
|
||||
@@ -59,7 +58,6 @@ import {
|
||||
RECTIFICATION_QUESTION_UNAVAILABLE_COPY,
|
||||
RECTIFICATION_COLLECT_WAITING_PLACEHOLDER,
|
||||
RECTIFICATION_DELIVERED_COPY,
|
||||
RECTIFICATION_STOPPED_NOTICE,
|
||||
isAbortError,
|
||||
rectificationConversationState,
|
||||
rectificationInitialLiveLabel,
|
||||
@@ -81,7 +79,6 @@ import {
|
||||
buildRectificationTimeline,
|
||||
type RectificationTimelineStage,
|
||||
} from "@/lib/rectification-timeline-scale";
|
||||
import { vargaSentenceFromMethods } from "@/lib/rectification-varga-sentence";
|
||||
import {
|
||||
isPublicRectificationActivity,
|
||||
isPublicRectificationMethod,
|
||||
@@ -99,19 +96,20 @@ import {
|
||||
} from "@/lib/rectification-agentic/v9/choice-action";
|
||||
import { postAdoptVerifyDoneCopy } from "@/lib/rectification-agentic/user-copy";
|
||||
import { isRectificationCaseStatus, isResumableStatus, type RectificationCaseStatus } from "@/lib/rectification-agentic/v9/case-status";
|
||||
import { CHOICE_MODE, CHOICE_SKIP_QUESTION_LABEL, CHOICE_SKIP_QUESTION_MESSAGE, CHOICE_STOP_LABEL, CHOICE_STOP_MESSAGE, isPersistedFocusId, parseRectificationChoiceCard, type ChoiceKey, type RectificationChoiceCard as ChoiceCardModel } from "@/lib/rectification-agentic/v9/choice-card";
|
||||
import { 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 { useVisibilityAwarePoll } from "@/hooks/use-visibility-aware-poll";
|
||||
import { CharacterRemaining } from "./character-remaining";
|
||||
import { ChatComposer } from "./chat-composer";
|
||||
import { ChatMessageRow } from "./chat-message-row";
|
||||
import { ConsultationTimelineLiveRow } from "./consultation-run-timeline";
|
||||
import { JumpToLatestButton } from "./jump-to-latest-button";
|
||||
import { toggleChatMessageFeedback } from "./chat-message-actions";
|
||||
import {
|
||||
ChatMessageActions,
|
||||
toggleChatMessageFeedback,
|
||||
} from "./chat-message-actions";
|
||||
RectificationMessageEntry,
|
||||
type RectificationMessageActions,
|
||||
type RenderMessage,
|
||||
} from "./rectification-message-entry";
|
||||
import { ModelSelector } from "./model-selector";
|
||||
import { RectificationBoard, RectificationBoardPeek } from "./rectification-board";
|
||||
import { RectificationChoiceCard } from "./rectification-choice-card";
|
||||
@@ -124,7 +122,6 @@ import {
|
||||
questionIsAnswered,
|
||||
resolveSelectionCardMessageKey,
|
||||
type SelectionCardLock,
|
||||
type TurnQuestion,
|
||||
} from "@/lib/rectification-agentic/v9/turn-question";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
@@ -239,17 +236,6 @@ type RectificationAgenticChatProps = Readonly<{
|
||||
headerSlot: HTMLElement | null;
|
||||
}>;
|
||||
|
||||
type RenderMessage = ChatMessageView & {
|
||||
renderKey: string;
|
||||
completedReceipt?: CompletedActivityReceiptView;
|
||||
failed?: boolean;
|
||||
stopped?: boolean;
|
||||
turnId?: string;
|
||||
activityTrace?: readonly AgentActivityTraceItem[];
|
||||
question?: TurnQuestion;
|
||||
candidateOffer?: Readonly<{ resultId: string }>;
|
||||
};
|
||||
|
||||
function markQuestionAnswered(
|
||||
current: RenderMessage[],
|
||||
focusId: string,
|
||||
@@ -273,37 +259,6 @@ function snapshotTurns(payload: { turns?: unknown } | null | undefined): readonl
|
||||
return Array.isArray(payload?.turns) ? payload.turns : [];
|
||||
}
|
||||
|
||||
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 completedReceiptFromPersisted(receipt: PersistedTurn["receipt"]): CompletedActivityReceiptView {
|
||||
if (!receipt) return { steps: [], methods: [] };
|
||||
if (Array.isArray(receipt.tool_activities)) {
|
||||
@@ -478,6 +433,13 @@ export function RectificationAgenticChat(props: RectificationAgenticChatProps) {
|
||||
const [copiedMessageKey, setCopiedMessageKey] = useState<string | null>(null);
|
||||
const [regeneratingMessageKey, setRegeneratingMessageKey] = useState<string | null>(null);
|
||||
const [choiceNonce, setChoiceNonce] = useState(0);
|
||||
const messageActionsRef = useRef<RectificationMessageActions>({
|
||||
submitChoice() {},
|
||||
submitStop() {},
|
||||
copyMessage() {},
|
||||
regenerateMessage() {},
|
||||
onFeedback() {},
|
||||
});
|
||||
const conversation = useRef<HTMLElement>(null);
|
||||
const workspace = useRef<HTMLDivElement>(null);
|
||||
const composer = useRef<HTMLTextAreaElement>(null);
|
||||
@@ -1649,6 +1611,21 @@ export function RectificationAgenticChat(props: RectificationAgenticChatProps) {
|
||||
void submitStructuredChoice(STOP_ACTION, "stop");
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
messageActionsRef.current = {
|
||||
submitChoice,
|
||||
submitStop,
|
||||
copyMessage,
|
||||
regenerateMessage,
|
||||
onFeedback(renderKey, requested) {
|
||||
setFeedback((current) => ({
|
||||
...current,
|
||||
[renderKey]: toggleRectificationFeedback(current[renderKey], requested),
|
||||
}));
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
const boardPeek = compactBoard && !boardOpen ? (
|
||||
<RectificationBoardPeek
|
||||
result={candidateResult}
|
||||
@@ -1689,118 +1666,31 @@ export function RectificationAgenticChat(props: RectificationAgenticChatProps) {
|
||||
</div>
|
||||
)}
|
||||
{messages.map((message) => {
|
||||
const showActions = message.role === "assistant"
|
||||
&& message.state === "settled"
|
||||
&& !message.failed
|
||||
&& Boolean(message.text);
|
||||
const regenerating = regeneratingMessageKey === message.renderKey;
|
||||
const canRegenerate = message.renderKey === latestRegeneratableKey
|
||||
&& !busy
|
||||
&& !readonly
|
||||
&& regeneratingMessageKey === null;
|
||||
// Both surfaces render the same step timeline: the tool trace and receipt
|
||||
// are projected onto ConsultationRunTimeline rows. A regenerating reply shows
|
||||
// the queued row until real events fill it in, never a staged label.
|
||||
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
|
||||
&& currentQuestion?.focus_id === question.focus_id
|
||||
&& !questionIsAnswered(question)
|
||||
&& !busy
|
||||
&& !readonly
|
||||
&& regeneratingMessageKey === null
|
||||
&& (
|
||||
question.kind === "collect_spoken"
|
||||
|| (
|
||||
choiceCard
|
||||
&& choiceCard.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 ? 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}
|
||||
/>
|
||||
)}
|
||||
{question.status === "skipped" && (
|
||||
<p className="rectification-question-skipped" role="status">
|
||||
{savedTime ? `已跳过(已采用 ${savedTime})` : "已跳过"}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
: undefined;
|
||||
return (
|
||||
<div
|
||||
key={message.renderKey}
|
||||
className="rectification-message-wrap rectification-message-entry"
|
||||
>
|
||||
{(!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[message.renderKey]}
|
||||
copied={copiedMessageKey === message.renderKey}
|
||||
canRegenerate={canRegenerate}
|
||||
onFeedback={(requested) => setFeedback((current) => ({
|
||||
...current,
|
||||
[message.renderKey]: toggleRectificationFeedback(current[message.renderKey], requested),
|
||||
}))}
|
||||
onCopy={() => void copyMessage(message)}
|
||||
onRegenerate={() => void regenerateMessage(message)}
|
||||
/>
|
||||
)}
|
||||
<RectificationMessageEntry
|
||||
message={message}
|
||||
busy={busy}
|
||||
readonly={readonly}
|
||||
regenerating={regenerating}
|
||||
canRegenerate={canRegenerate}
|
||||
currentQuestionFocusId={currentQuestion?.focus_id ?? null}
|
||||
interactive={!busy && !readonly && regeneratingMessageKey === null}
|
||||
liveChoiceCard={choiceCard}
|
||||
choiceNonce={choiceNonce}
|
||||
savedTime={savedTime}
|
||||
copied={copiedMessageKey === message.renderKey}
|
||||
feedback={feedback[message.renderKey]}
|
||||
actionsRef={messageActionsRef}
|
||||
/>
|
||||
{showSelectionCards && candidateResult && message.renderKey === selectionCardMessageKey && (
|
||||
<>
|
||||
<RectificationRangeDelivery
|
||||
|
||||
@@ -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