新增 TurnList 组件,优化消息渲染逻辑
Build Push and Deploy / docker-image (push) Successful in 1m19s
Build Push and Deploy / deploy-fallback-log (push) Has been skipped

This commit is contained in:
2026-06-03 17:49:39 +08:00
parent 06a3f32d2d
commit 20ca410e0a
2 changed files with 239 additions and 33 deletions
+142 -33
View File
@@ -13,6 +13,7 @@ import { AgentTurn } from "./AgentTurn";
import { TypingIndicator } from "./GlobalChatbox.parts";
import type {
BranchGroup,
BranchState,
BranchTransition,
Message,
SpeechState,
@@ -36,6 +37,96 @@ type AgentWorkspaceProps = {
onCycleBranch: (rootMessageId: string, direction: -1 | 1) => void;
};
type TurnListProps = {
messages: Message[];
branchGroups: BranchGroup[];
speakingMessageId: string | null;
speechState: SpeechState;
onSpeak: (messageId: string, text: string) => void;
onPauseSpeech: () => void;
onResumeSpeech: () => void;
onStopSpeech: () => void;
isTtsSupported: boolean;
onRegenerate: () => void;
onEditResubmit: (messageId: string, newContent: string) => void;
onCycleBranch: (rootMessageId: string, direction: -1 | 1) => void;
};
const sameMessages = (left: Message[], right: Message[]) =>
left.length === right.length &&
left.every((message, index) => message === right[index]);
const TurnListInner = ({
messages,
branchGroups,
speakingMessageId,
speechState,
onSpeak,
onPauseSpeech,
onResumeSpeech,
onStopSpeech,
isTtsSupported,
onRegenerate,
onEditResubmit,
onCycleBranch,
}: TurnListProps) => {
const branchStateByRootId = React.useMemo(() => {
const next = new Map<string, BranchState>();
branchGroups.forEach((group) => {
if (group.branches.length > 1) {
next.set(group.rootMessageId, {
activeIndex: group.activeIndex,
total: group.branches.length,
});
}
});
return next;
}, [branchGroups]);
return (
<>
{messages.map((message) => {
const rootMessageId = message.branchRootId ?? message.id;
return (
<AgentTurn
key={rootMessageId}
message={message}
branchState={branchStateByRootId.get(rootMessageId)}
messageSpeechState={speakingMessageId === message.id ? speechState : "idle"}
onSpeak={onSpeak}
onPause={onPauseSpeech}
onResume={onResumeSpeech}
onStopSpeech={onStopSpeech}
isTtsSupported={isTtsSupported}
onRegenerate={onRegenerate}
onEditResubmit={onEditResubmit}
onCycleBranch={onCycleBranch}
/>
);
})}
</>
);
};
const TurnList = React.memo(
TurnListInner,
(prevProps, nextProps) =>
sameMessages(prevProps.messages, nextProps.messages) &&
prevProps.branchGroups === nextProps.branchGroups &&
prevProps.speakingMessageId === nextProps.speakingMessageId &&
prevProps.speechState === nextProps.speechState &&
prevProps.onSpeak === nextProps.onSpeak &&
prevProps.onPauseSpeech === nextProps.onPauseSpeech &&
prevProps.onResumeSpeech === nextProps.onResumeSpeech &&
prevProps.onStopSpeech === nextProps.onStopSpeech &&
prevProps.isTtsSupported === nextProps.isTtsSupported &&
prevProps.onRegenerate === nextProps.onRegenerate &&
prevProps.onEditResubmit === nextProps.onEditResubmit &&
prevProps.onCycleBranch === nextProps.onCycleBranch,
);
TurnList.displayName = "TurnList";
const EmptyState = () => {
const theme = useTheme();
const capabilities = [
@@ -182,37 +273,12 @@ export const AgentWorkspace = ({
const transitionMessages = branchTransition
? messages.slice(branchTransition.parentCount)
: [];
const renderTurn = (message: Message) => {
const rootMessageId = message.branchRootId ?? message.id;
const branchGroup = branchGroups.find(
(group) => group.rootMessageId === rootMessageId,
);
return (
<AgentTurn
key={rootMessageId}
message={message}
branchState={
branchGroup && branchGroup.branches.length > 1
? {
activeIndex: branchGroup.activeIndex,
total: branchGroup.branches.length,
}
: undefined
}
messageSpeechState={speakingMessageId === message.id ? speechState : "idle"}
onSpeak={onSpeak}
onPause={onPauseSpeech}
onResume={onResumeSpeech}
onStopSpeech={onStopSpeech}
isTtsSupported={isTtsSupported}
onRegenerate={onRegenerate}
onEditResubmit={onEditResubmit}
onCycleBranch={onCycleBranch}
/>
);
};
const streamingMessage =
!branchTransition && isStreaming && messages.at(-1)?.role === "assistant"
? messages.at(-1)
: undefined;
const historyMessages =
streamingMessage !== undefined ? messages.slice(0, -1) : stableMessages;
return (
<Box
@@ -232,7 +298,37 @@ export const AgentWorkspace = ({
{messages.length > 0 ? (
<Box sx={{ display: "flex", flexDirection: "column", gap: 2 }}>
{stableMessages.map(renderTurn)}
<TurnList
messages={historyMessages}
branchGroups={branchGroups}
speakingMessageId={speakingMessageId}
speechState={speechState}
onSpeak={onSpeak}
onPauseSpeech={onPauseSpeech}
onResumeSpeech={onResumeSpeech}
onStopSpeech={onStopSpeech}
isTtsSupported={isTtsSupported}
onRegenerate={onRegenerate}
onEditResubmit={onEditResubmit}
onCycleBranch={onCycleBranch}
/>
{streamingMessage ? (
<TurnList
messages={[streamingMessage]}
branchGroups={branchGroups}
speakingMessageId={speakingMessageId}
speechState={speechState}
onSpeak={onSpeak}
onPauseSpeech={onPauseSpeech}
onResumeSpeech={onResumeSpeech}
onStopSpeech={onStopSpeech}
isTtsSupported={isTtsSupported}
onRegenerate={onRegenerate}
onEditResubmit={onEditResubmit}
onCycleBranch={onCycleBranch}
/>
) : null}
{branchTransition ? (
<AnimatePresence initial={false} mode="wait">
@@ -244,7 +340,20 @@ export const AgentWorkspace = ({
transition={{ duration: 0.18, ease: "easeOut" }}
style={{ display: "flex", flexDirection: "column", gap: 16 }}
>
{transitionMessages.map(renderTurn)}
<TurnList
messages={transitionMessages}
branchGroups={branchGroups}
speakingMessageId={speakingMessageId}
speechState={speechState}
onSpeak={onSpeak}
onPauseSpeech={onPauseSpeech}
onResumeSpeech={onResumeSpeech}
onStopSpeech={onStopSpeech}
isTtsSupported={isTtsSupported}
onRegenerate={onRegenerate}
onEditResubmit={onEditResubmit}
onCycleBranch={onCycleBranch}
/>
</motion.div>
</AnimatePresence>
) : null}