fix(chat): avoid final stream remount
This commit is contained in:
@@ -114,7 +114,7 @@ const StreamingMarkdownBlock = ({
|
|||||||
|
|
||||||
React.useLayoutEffect(() => {
|
React.useLayoutEffect(() => {
|
||||||
setStreamTextState((current) => {
|
setStreamTextState((current) => {
|
||||||
if (current.displayText === text && current.animatedTailLength === 0) {
|
if (current.displayText === text) {
|
||||||
return current;
|
return current;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -7,6 +7,8 @@ import { AgentWorkspace } from "./AgentWorkspace";
|
|||||||
import type { Message } from "./GlobalChatbox.types";
|
import type { Message } from "./GlobalChatbox.types";
|
||||||
|
|
||||||
const renderCounts = new Map<string, number>();
|
const renderCounts = new Map<string, number>();
|
||||||
|
const mountCounts = new Map<string, number>();
|
||||||
|
const unmountCounts = new Map<string, number>();
|
||||||
const streamingFlags = new Map<string, boolean>();
|
const streamingFlags = new Map<string, boolean>();
|
||||||
|
|
||||||
jest.mock("next/image", () => ({
|
jest.mock("next/image", () => ({
|
||||||
@@ -38,6 +40,12 @@ jest.mock("./GlobalChatbox.parts", () => ({
|
|||||||
|
|
||||||
jest.mock("./AgentTurn", () => ({
|
jest.mock("./AgentTurn", () => ({
|
||||||
AgentTurn: ({ message, isStreaming }: { message: Message; isStreaming: boolean }) => {
|
AgentTurn: ({ message, isStreaming }: { message: Message; isStreaming: boolean }) => {
|
||||||
|
React.useEffect(() => {
|
||||||
|
mountCounts.set(message.id, (mountCounts.get(message.id) ?? 0) + 1);
|
||||||
|
return () => {
|
||||||
|
unmountCounts.set(message.id, (unmountCounts.get(message.id) ?? 0) + 1);
|
||||||
|
};
|
||||||
|
}, [message.id]);
|
||||||
renderCounts.set(message.id, (renderCounts.get(message.id) ?? 0) + 1);
|
renderCounts.set(message.id, (renderCounts.get(message.id) ?? 0) + 1);
|
||||||
streamingFlags.set(message.id, isStreaming);
|
streamingFlags.set(message.id, isStreaming);
|
||||||
return <div data-testid={`turn-${message.id}`}>{message.content}</div>;
|
return <div data-testid={`turn-${message.id}`}>{message.content}</div>;
|
||||||
@@ -62,6 +70,8 @@ describe("AgentWorkspace", () => {
|
|||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
renderCounts.clear();
|
renderCounts.clear();
|
||||||
|
mountCounts.clear();
|
||||||
|
unmountCounts.clear();
|
||||||
streamingFlags.clear();
|
streamingFlags.clear();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -123,4 +133,39 @@ describe("AgentWorkspace", () => {
|
|||||||
expect(streamingFlags.get("assistant-1")).toBe(false);
|
expect(streamingFlags.get("assistant-1")).toBe(false);
|
||||||
expect(streamingFlags.get("assistant-2")).toBe(true);
|
expect(streamingFlags.get("assistant-2")).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("does not remount the streaming assistant turn when streaming finishes", () => {
|
||||||
|
const userMessage: Message = {
|
||||||
|
id: "user-1",
|
||||||
|
role: "user",
|
||||||
|
content: "question",
|
||||||
|
};
|
||||||
|
const assistantMessage: Message = {
|
||||||
|
id: "assistant-1",
|
||||||
|
role: "assistant",
|
||||||
|
content: "final answer",
|
||||||
|
};
|
||||||
|
|
||||||
|
const { rerender } = render(
|
||||||
|
<AgentWorkspace
|
||||||
|
{...defaultProps}
|
||||||
|
isStreaming
|
||||||
|
messages={[userMessage, assistantMessage]}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(streamingFlags.get("assistant-1")).toBe(true);
|
||||||
|
|
||||||
|
rerender(
|
||||||
|
<AgentWorkspace
|
||||||
|
{...defaultProps}
|
||||||
|
isStreaming={false}
|
||||||
|
messages={[userMessage, assistantMessage]}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(mountCounts.get("assistant-1")).toBe(1);
|
||||||
|
expect(unmountCounts.get("assistant-1") ?? 0).toBe(0);
|
||||||
|
expect(streamingFlags.get("assistant-1")).toBe(false);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -40,6 +40,7 @@ type AgentWorkspaceProps = {
|
|||||||
type TurnListProps = {
|
type TurnListProps = {
|
||||||
messages: Message[];
|
messages: Message[];
|
||||||
isStreaming: boolean;
|
isStreaming: boolean;
|
||||||
|
streamingMessageId: string | null;
|
||||||
speakingMessageId: string | null;
|
speakingMessageId: string | null;
|
||||||
speechState: SpeechState;
|
speechState: SpeechState;
|
||||||
onSpeak: (messageId: string, text: string) => void;
|
onSpeak: (messageId: string, text: string) => void;
|
||||||
@@ -60,9 +61,12 @@ const sameMessages = (left: Message[], right: Message[]) =>
|
|||||||
left.length === right.length &&
|
left.length === right.length &&
|
||||||
left.every((message, index) => message === right[index]);
|
left.every((message, index) => message === right[index]);
|
||||||
|
|
||||||
|
const TurnItem = React.memo(AgentTurn);
|
||||||
|
|
||||||
const TurnListInner = ({
|
const TurnListInner = ({
|
||||||
messages,
|
messages,
|
||||||
isStreaming,
|
isStreaming,
|
||||||
|
streamingMessageId,
|
||||||
speakingMessageId,
|
speakingMessageId,
|
||||||
speechState,
|
speechState,
|
||||||
onSpeak,
|
onSpeak,
|
||||||
@@ -78,10 +82,10 @@ const TurnListInner = ({
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{messages.map((message) => (
|
{messages.map((message) => (
|
||||||
<AgentTurn
|
<TurnItem
|
||||||
key={message.id}
|
key={message.id}
|
||||||
message={message}
|
message={message}
|
||||||
isStreaming={isStreaming}
|
isStreaming={isStreaming && message.id === streamingMessageId}
|
||||||
messageSpeechState={speakingMessageId === message.id ? speechState : "idle"}
|
messageSpeechState={speakingMessageId === message.id ? speechState : "idle"}
|
||||||
onSpeak={onSpeak}
|
onSpeak={onSpeak}
|
||||||
onPause={onPauseSpeech}
|
onPause={onPauseSpeech}
|
||||||
@@ -103,6 +107,7 @@ const TurnList = React.memo(
|
|||||||
(prevProps, nextProps) =>
|
(prevProps, nextProps) =>
|
||||||
sameMessages(prevProps.messages, nextProps.messages) &&
|
sameMessages(prevProps.messages, nextProps.messages) &&
|
||||||
prevProps.isStreaming === nextProps.isStreaming &&
|
prevProps.isStreaming === nextProps.isStreaming &&
|
||||||
|
prevProps.streamingMessageId === nextProps.streamingMessageId &&
|
||||||
prevProps.speakingMessageId === nextProps.speakingMessageId &&
|
prevProps.speakingMessageId === nextProps.speakingMessageId &&
|
||||||
prevProps.speechState === nextProps.speechState &&
|
prevProps.speechState === nextProps.speechState &&
|
||||||
prevProps.onSpeak === nextProps.onSpeak &&
|
prevProps.onSpeak === nextProps.onSpeak &&
|
||||||
@@ -326,8 +331,6 @@ export const AgentWorkspace = ({
|
|||||||
isStreaming && messages.at(-1)?.role === "assistant"
|
isStreaming && messages.at(-1)?.role === "assistant"
|
||||||
? messages.at(-1)
|
? messages.at(-1)
|
||||||
: undefined;
|
: undefined;
|
||||||
const historyMessages =
|
|
||||||
streamingMessage !== undefined ? messages.slice(0, -1) : messages;
|
|
||||||
const handleScroll = React.useCallback(
|
const handleScroll = React.useCallback(
|
||||||
(event: React.UIEvent<HTMLDivElement>) => {
|
(event: React.UIEvent<HTMLDivElement>) => {
|
||||||
if (!onScrollStateChange) return;
|
if (!onScrollStateChange) return;
|
||||||
@@ -368,26 +371,9 @@ export const AgentWorkspace = ({
|
|||||||
{messages.length > 0 ? (
|
{messages.length > 0 ? (
|
||||||
<Box sx={{ display: "flex", flexDirection: "column", gap: 2 }}>
|
<Box sx={{ display: "flex", flexDirection: "column", gap: 2 }}>
|
||||||
<TurnList
|
<TurnList
|
||||||
messages={historyMessages}
|
messages={messages}
|
||||||
isStreaming={false}
|
|
||||||
speakingMessageId={speakingMessageId}
|
|
||||||
speechState={speechState}
|
|
||||||
onSpeak={onSpeak}
|
|
||||||
onPauseSpeech={onPauseSpeech}
|
|
||||||
onResumeSpeech={onResumeSpeech}
|
|
||||||
onStopSpeech={onStopSpeech}
|
|
||||||
isTtsSupported={isTtsSupported}
|
|
||||||
onCreateBranch={onCreateBranch}
|
|
||||||
onReplyPermission={onReplyPermission}
|
|
||||||
onReplyQuestion={onReplyQuestion}
|
|
||||||
onRejectQuestion={onRejectQuestion}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{streamingMessage ? (
|
|
||||||
<Box sx={{ width: "100%" }}>
|
|
||||||
<TurnList
|
|
||||||
messages={[streamingMessage]}
|
|
||||||
isStreaming={isStreaming}
|
isStreaming={isStreaming}
|
||||||
|
streamingMessageId={streamingMessage?.id ?? null}
|
||||||
speakingMessageId={speakingMessageId}
|
speakingMessageId={speakingMessageId}
|
||||||
speechState={speechState}
|
speechState={speechState}
|
||||||
onSpeak={onSpeak}
|
onSpeak={onSpeak}
|
||||||
@@ -402,8 +388,6 @@ export const AgentWorkspace = ({
|
|||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
) : null}
|
) : null}
|
||||||
</Box>
|
|
||||||
) : null}
|
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user