vzcode 2.15.0 → 2.16.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/assets/{index-BpUpNto4.js → index-C2BLPyQP.js} +138 -125
- package/dist/assets/{index-DLm5FQ0E.css → index-DVR90LwF.css} +1 -1
- package/dist/index.html +2 -2
- package/dist/server/aiChatHandler/chatOperations.js +168 -35
- package/dist/server/aiChatHandler/index.js +11 -30
- package/dist/server/aiChatHandler/llmStreaming.js +105 -118
- package/package.json +11 -11
- package/src/client/AIAssist/AIAssistWidget/index.tsx +12 -1
- package/src/client/App/useShareDB.ts +1 -1
- package/src/client/CodeEditor/index.tsx +9 -10
- package/src/client/VZCodeContext/types.ts +3 -1
- package/src/client/VZCodeContext/useVZCodeState.ts +7 -5
- package/src/client/VZRight.tsx +10 -2
- package/src/client/VZSidebar/AIChat/ChatInput.tsx +1 -7
- package/src/client/VZSidebar/AIChat/DiffView.scss +1 -0
- package/src/client/VZSidebar/AIChat/DiffView.tsx +72 -29
- package/src/client/VZSidebar/AIChat/FileEditingIndicator.tsx +81 -0
- package/src/client/VZSidebar/AIChat/IndividualFileDiff.tsx +86 -0
- package/src/client/VZSidebar/AIChat/JumpToLatestButton.tsx +64 -0
- package/src/client/VZSidebar/AIChat/Message.tsx +68 -53
- package/src/client/VZSidebar/AIChat/MessageList.tsx +139 -118
- package/src/client/VZSidebar/AIChat/StreamingMessage.tsx +63 -21
- package/src/client/VZSidebar/AIChat/ThinkingScratchpad.tsx +4 -118
- package/src/client/VZSidebar/AIChat/index.tsx +62 -19
- package/src/client/VZSidebar/AIChat/styles.scss +159 -16
- package/src/client/VZSidebar/Item.tsx +2 -2
- package/src/client/VZSidebar/Search.tsx +13 -6
- package/src/client/VZSidebar/VisualEditor/VisualEditor.tsx +39 -23
- package/src/client/VZSidebar/VisualEditor/utils.ts +1 -1
- package/src/client/VZSidebar/index.tsx +12 -10
- package/src/client/VZSidebar/useDragAndDrop.tsx +225 -214
- package/src/client/featureFlags.ts +3 -0
- package/src/client/hooks/useAutoScroll.ts +329 -0
- package/src/client/tabsSearchParameters.ts +1 -17
- package/src/client/useFileCRUD.ts +5 -2
- package/src/client/useKeyboardShortcuts.ts +7 -0
- package/src/client/useOpenDirectories.ts +2 -1
- package/src/client/usePrettier/index.ts +1 -1
- package/src/client/useURLSync.ts +1 -0
- package/src/client/utils/scrollUtils.ts +170 -0
- package/src/client/vzReducer/searchReducer.ts +6 -3
- package/src/server/aiChatHandler/chatOperations.ts +224 -43
- package/src/server/aiChatHandler/index.ts +8 -48
- package/src/server/aiChatHandler/llmStreaming.ts +149 -170
- package/src/types.ts +81 -1
|
@@ -1,8 +1,13 @@
|
|
|
1
1
|
import { timestampToDate } from '@vizhub/viz-utils';
|
|
2
2
|
import Markdown from 'react-markdown';
|
|
3
3
|
import remarkGfm from 'remark-gfm';
|
|
4
|
-
import React, {
|
|
5
|
-
|
|
4
|
+
import React, {
|
|
5
|
+
useMemo,
|
|
6
|
+
memo,
|
|
7
|
+
useContext,
|
|
8
|
+
forwardRef,
|
|
9
|
+
} from 'react';
|
|
10
|
+
import { DiffView, DiffViewRef } from './DiffView';
|
|
6
11
|
import { UnifiedFilesDiff } from '../../../utils/fileDiff';
|
|
7
12
|
import { enableDiffView } from '../../featureFlags';
|
|
8
13
|
import { VZCodeContext } from '../../VZCodeContext';
|
|
@@ -18,60 +23,70 @@ interface MessageProps {
|
|
|
18
23
|
showAdditionalWidgets?: boolean;
|
|
19
24
|
}
|
|
20
25
|
|
|
21
|
-
const MessageComponent =
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
26
|
+
const MessageComponent = forwardRef<
|
|
27
|
+
DiffViewRef,
|
|
28
|
+
MessageProps
|
|
29
|
+
>(
|
|
30
|
+
(
|
|
31
|
+
{
|
|
32
|
+
id,
|
|
33
|
+
role,
|
|
34
|
+
content,
|
|
35
|
+
timestamp,
|
|
36
|
+
isStreaming,
|
|
37
|
+
diffData,
|
|
38
|
+
chatId,
|
|
39
|
+
showAdditionalWidgets = false,
|
|
40
|
+
},
|
|
41
|
+
ref,
|
|
42
|
+
) => {
|
|
43
|
+
const { additionalWidgets, handleSendMessage } =
|
|
44
|
+
useContext(VZCodeContext);
|
|
33
45
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
46
|
+
// Memoize date formatting to avoid repeated computation
|
|
47
|
+
const formattedTime = useMemo(() => {
|
|
48
|
+
return timestampToDate(timestamp).toLocaleTimeString(
|
|
49
|
+
[],
|
|
50
|
+
{
|
|
51
|
+
hour: '2-digit',
|
|
52
|
+
minute: '2-digit',
|
|
53
|
+
},
|
|
54
|
+
);
|
|
55
|
+
}, [timestamp]);
|
|
44
56
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
57
|
+
// Memoize the className string to avoid recreation
|
|
58
|
+
const messageClassName = useMemo(() => {
|
|
59
|
+
return `ai-chat-message ${role}${isStreaming ? ' streaming' : ''}`;
|
|
60
|
+
}, [role, isStreaming]);
|
|
49
61
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
62
|
+
return (
|
|
63
|
+
<div className={messageClassName}>
|
|
64
|
+
<div className="ai-chat-message-content">
|
|
65
|
+
<Markdown remarkPlugins={[remarkGfm]}>
|
|
66
|
+
{content}
|
|
67
|
+
</Markdown>
|
|
68
|
+
{enableDiffView &&
|
|
69
|
+
diffData &&
|
|
70
|
+
Object.keys(diffData).length > 0 && (
|
|
71
|
+
<DiffView diffData={diffData} ref={ref} />
|
|
72
|
+
)}
|
|
73
|
+
{showAdditionalWidgets &&
|
|
74
|
+
additionalWidgets &&
|
|
75
|
+
chatId &&
|
|
76
|
+
additionalWidgets({
|
|
77
|
+
messageId: id,
|
|
78
|
+
chatId: chatId,
|
|
79
|
+
handleSendMessage,
|
|
80
|
+
})}
|
|
81
|
+
</div>
|
|
82
|
+
<div className="ai-chat-message-time">
|
|
83
|
+
{formattedTime}
|
|
84
|
+
</div>
|
|
72
85
|
</div>
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
86
|
+
);
|
|
87
|
+
},
|
|
88
|
+
);
|
|
89
|
+
|
|
90
|
+
MessageComponent.displayName = 'MessageComponent';
|
|
76
91
|
|
|
77
92
|
export const Message = memo(MessageComponent);
|
|
@@ -1,141 +1,74 @@
|
|
|
1
|
-
import {
|
|
2
|
-
useRef,
|
|
3
|
-
useEffect,
|
|
4
|
-
useCallback,
|
|
5
|
-
memo,
|
|
6
|
-
useState,
|
|
7
|
-
} from 'react';
|
|
1
|
+
import { useRef, useEffect, memo, useState } from 'react';
|
|
8
2
|
import { Message } from './Message';
|
|
3
|
+
import { StreamingMessage } from './StreamingMessage';
|
|
9
4
|
import { TypingIndicator } from './TypingIndicator';
|
|
10
5
|
import { ThinkingScratchpad } from './ThinkingScratchpad';
|
|
6
|
+
import { AIEditingStatusIndicator } from './FileEditingIndicator';
|
|
11
7
|
import { VizChatMessage } from '@vizhub/viz-types';
|
|
8
|
+
import { ExtendedVizChatMessage } from '../../../types.js';
|
|
9
|
+
import { DiffViewRef } from './DiffView.js';
|
|
12
10
|
|
|
13
11
|
const MessageListComponent = ({
|
|
14
12
|
messages,
|
|
15
13
|
isLoading,
|
|
16
14
|
chatId, // Add chatId prop
|
|
17
15
|
aiScratchpad, // Add aiScratchpad prop
|
|
16
|
+
currentStatus, // Add current status prop
|
|
17
|
+
onNewEvent,
|
|
18
|
+
onJumpToLatest,
|
|
19
|
+
beforeRender,
|
|
20
|
+
afterRender,
|
|
18
21
|
}: {
|
|
19
22
|
messages: VizChatMessage[];
|
|
20
23
|
isLoading: boolean;
|
|
21
24
|
chatId?: string; // Add chatId to the type
|
|
22
25
|
aiScratchpad?: string; // Add aiScratchpad to the type
|
|
26
|
+
currentStatus?: string; // Add current status to the type
|
|
27
|
+
onNewEvent: (targetElement?: HTMLElement) => void;
|
|
28
|
+
onJumpToLatest: (targetElement?: HTMLElement) => void;
|
|
29
|
+
beforeRender: () => number;
|
|
30
|
+
afterRender: (prevScrollHeight: number) => void;
|
|
23
31
|
}) => {
|
|
24
32
|
const messagesEndRef = useRef<HTMLDivElement>(null);
|
|
25
|
-
const
|
|
26
|
-
const [isUserScrolled, setIsUserScrolled] =
|
|
27
|
-
useState(false);
|
|
28
|
-
const [autoScrollEnabled, setAutoScrollEnabled] =
|
|
29
|
-
useState(true);
|
|
30
|
-
const scrollTimeoutRef =
|
|
31
|
-
useRef<ReturnType<typeof setTimeout>>();
|
|
32
|
-
|
|
33
|
-
// Check if the user is scrolled to the bottom
|
|
34
|
-
const isScrolledToBottom = useCallback(() => {
|
|
35
|
-
const container = messagesContainerRef.current;
|
|
36
|
-
if (!container) return true;
|
|
37
|
-
|
|
38
|
-
const threshold = 50; // Allow 50px tolerance for "at bottom"
|
|
39
|
-
const { scrollTop, scrollHeight, clientHeight } =
|
|
40
|
-
container;
|
|
41
|
-
return (
|
|
42
|
-
scrollHeight - scrollTop - clientHeight < threshold
|
|
43
|
-
);
|
|
44
|
-
}, []);
|
|
45
|
-
|
|
46
|
-
// Smooth scroll to bottom with linear transition
|
|
47
|
-
const scrollToBottom = useCallback(() => {
|
|
48
|
-
if (!autoScrollEnabled) return;
|
|
49
|
-
|
|
50
|
-
messagesEndRef.current?.scrollIntoView({
|
|
51
|
-
behavior: 'smooth',
|
|
52
|
-
block: 'end',
|
|
53
|
-
});
|
|
54
|
-
}, [autoScrollEnabled]);
|
|
55
|
-
|
|
56
|
-
// Handle scroll events to detect user manual scrolling
|
|
57
|
-
const handleScroll = useCallback(() => {
|
|
58
|
-
const container = messagesContainerRef.current;
|
|
59
|
-
if (!container) return;
|
|
60
|
-
|
|
61
|
-
const isAtBottom = isScrolledToBottom();
|
|
62
|
-
|
|
63
|
-
// Clear any existing timeout
|
|
64
|
-
if (scrollTimeoutRef.current) {
|
|
65
|
-
clearTimeout(scrollTimeoutRef.current);
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
// If user scrolled up from bottom, disable auto-scroll
|
|
69
|
-
if (!isAtBottom && !isUserScrolled) {
|
|
70
|
-
setIsUserScrolled(true);
|
|
71
|
-
setAutoScrollEnabled(false);
|
|
72
|
-
}
|
|
33
|
+
const diffViewRef = useRef<DiffViewRef>(null);
|
|
73
34
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
setIsUserScrolled(false);
|
|
78
|
-
setAutoScrollEnabled(true);
|
|
79
|
-
}, 500); // 500ms delay to prevent flickering
|
|
80
|
-
}
|
|
81
|
-
}, [isUserScrolled, isScrolledToBottom]);
|
|
35
|
+
// Track previous loading state to detect when AI generation completes
|
|
36
|
+
const [prevIsLoading, setPrevIsLoading] =
|
|
37
|
+
useState(isLoading);
|
|
82
38
|
|
|
83
|
-
// Auto-scroll when messages change
|
|
39
|
+
// Auto-scroll when messages change
|
|
84
40
|
useEffect(() => {
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
41
|
+
// Get scroll height before render for anchoring
|
|
42
|
+
const prevScrollHeight = beforeRender();
|
|
43
|
+
|
|
44
|
+
// Determine if we should scroll to a specific diff or to the bottom
|
|
45
|
+
const lastMessageHasDiff =
|
|
46
|
+
messages.length > 0 &&
|
|
47
|
+
(messages[messages.length - 1] as any).diffData;
|
|
48
|
+
|
|
49
|
+
let targetElement: HTMLElement | null = null;
|
|
50
|
+
if (lastMessageHasDiff && diffViewRef.current) {
|
|
51
|
+
targetElement =
|
|
52
|
+
diffViewRef.current.getFirstHunkElement();
|
|
94
53
|
}
|
|
95
54
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
clearTimeout(scrollTimeoutRef.current);
|
|
99
|
-
}
|
|
100
|
-
};
|
|
101
|
-
}, [
|
|
102
|
-
messages,
|
|
103
|
-
autoScrollEnabled,
|
|
104
|
-
isUserScrolled,
|
|
105
|
-
scrollToBottom,
|
|
106
|
-
]);
|
|
55
|
+
// Trigger auto-scroll if enabled
|
|
56
|
+
onNewEvent(targetElement);
|
|
107
57
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
58
|
+
// Adjust scroll position after render for anchoring
|
|
59
|
+
afterRender(prevScrollHeight);
|
|
60
|
+
}, [messages, onNewEvent, beforeRender, afterRender]);
|
|
111
61
|
|
|
112
62
|
// Scroll to bottom when AI generation completes (loading changes from true to false)
|
|
113
63
|
useEffect(() => {
|
|
114
|
-
// If AI was generating and now it's finished, scroll to bottom
|
|
64
|
+
// If AI was generating and now it's finished, force scroll to bottom
|
|
115
65
|
if (prevIsLoading && !isLoading) {
|
|
116
|
-
// Force
|
|
117
|
-
|
|
118
|
-
messagesEndRef.current?.scrollIntoView({
|
|
119
|
-
behavior: 'smooth',
|
|
120
|
-
block: 'end',
|
|
121
|
-
});
|
|
122
|
-
|
|
123
|
-
// Re-enable auto-scroll for future messages
|
|
124
|
-
setIsUserScrolled(false);
|
|
125
|
-
setAutoScrollEnabled(true);
|
|
66
|
+
// Force jump to latest regardless of current auto-scroll state
|
|
67
|
+
onJumpToLatest();
|
|
126
68
|
}
|
|
127
69
|
|
|
128
70
|
setPrevIsLoading(isLoading);
|
|
129
|
-
}, [isLoading, prevIsLoading]);
|
|
130
|
-
|
|
131
|
-
// Clean up timeout on unmount
|
|
132
|
-
useEffect(() => {
|
|
133
|
-
return () => {
|
|
134
|
-
if (scrollTimeoutRef.current) {
|
|
135
|
-
clearTimeout(scrollTimeoutRef.current);
|
|
136
|
-
}
|
|
137
|
-
};
|
|
138
|
-
}, []);
|
|
71
|
+
}, [isLoading, prevIsLoading, onJumpToLatest]);
|
|
139
72
|
|
|
140
73
|
// Check if AI generation has started (last message is from assistant)
|
|
141
74
|
const lastMessage = messages[messages.length - 1];
|
|
@@ -152,6 +85,56 @@ const MessageListComponent = ({
|
|
|
152
85
|
aiScratchpad && aiScratchpad.trim(),
|
|
153
86
|
);
|
|
154
87
|
|
|
88
|
+
// Determine the single, most relevant status to display with priority:
|
|
89
|
+
// 1. Active file editing status from streaming events
|
|
90
|
+
// 2. General AI status from currentStatus
|
|
91
|
+
const getConsolidatedStatus = () => {
|
|
92
|
+
// Check if there's an active streaming message with file editing
|
|
93
|
+
if (lastMessage && lastMessage.role === 'assistant') {
|
|
94
|
+
const extendedMsg =
|
|
95
|
+
lastMessage as ExtendedVizChatMessage;
|
|
96
|
+
if (
|
|
97
|
+
extendedMsg.streamingEvents &&
|
|
98
|
+
extendedMsg.streamingEvents.length > 0
|
|
99
|
+
) {
|
|
100
|
+
// Look for active file editing (file_start without corresponding file_complete)
|
|
101
|
+
const fileStates = new Map<string, boolean>();
|
|
102
|
+
|
|
103
|
+
extendedMsg.streamingEvents.forEach((event) => {
|
|
104
|
+
if (event.type === 'file_start') {
|
|
105
|
+
fileStates.set(event.fileName, false); // Mark as editing
|
|
106
|
+
} else if (event.type === 'file_complete') {
|
|
107
|
+
fileStates.set(event.fileName, true); // Mark as complete
|
|
108
|
+
}
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
// Find the first file that's still being edited
|
|
112
|
+
for (const [fileName, isComplete] of fileStates) {
|
|
113
|
+
if (!isComplete) {
|
|
114
|
+
return {
|
|
115
|
+
status: `Editing ${fileName}...`,
|
|
116
|
+
fileName,
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Fall back to general AI status if no active file editing
|
|
124
|
+
if (currentStatus) {
|
|
125
|
+
return { status: currentStatus };
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return null;
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
const consolidatedStatus = getConsolidatedStatus();
|
|
132
|
+
|
|
133
|
+
// Show consolidated status indicator when there's a status to display
|
|
134
|
+
const showConsolidatedStatusIndicator = Boolean(
|
|
135
|
+
consolidatedStatus,
|
|
136
|
+
);
|
|
137
|
+
|
|
155
138
|
// Find the most recent assistant message index
|
|
156
139
|
const lastAssistantMessageIndex = messages
|
|
157
140
|
.map((m, i) => (m.role === 'assistant' ? i : -1))
|
|
@@ -161,15 +144,56 @@ const MessageListComponent = ({
|
|
|
161
144
|
return (
|
|
162
145
|
<div
|
|
163
146
|
className="ai-chat-messages"
|
|
164
|
-
|
|
165
|
-
|
|
147
|
+
role="log"
|
|
148
|
+
aria-live="polite"
|
|
149
|
+
aria-relevant="additions"
|
|
166
150
|
>
|
|
167
151
|
{messages.map((msg, index) => {
|
|
152
|
+
const isLastMessage = index === messages.length - 1;
|
|
153
|
+
// Cast to extended message to check for streaming events
|
|
154
|
+
const extendedMsg = msg as ExtendedVizChatMessage;
|
|
155
|
+
|
|
156
|
+
// Check if this is a streaming message
|
|
157
|
+
const isStreamingMessage =
|
|
158
|
+
!!extendedMsg.isProgressive;
|
|
159
|
+
|
|
168
160
|
// Only show additionalWidgets for the most recent assistant message
|
|
169
161
|
const showAdditionalWidgets =
|
|
170
162
|
msg.role === 'assistant' &&
|
|
171
163
|
index === lastAssistantMessageIndex;
|
|
172
164
|
|
|
165
|
+
// Use StreamingMessage for assistant messages with streaming events
|
|
166
|
+
if (
|
|
167
|
+
msg.role === 'assistant' &&
|
|
168
|
+
isStreamingMessage
|
|
169
|
+
) {
|
|
170
|
+
return (
|
|
171
|
+
<StreamingMessage
|
|
172
|
+
key={msg.id}
|
|
173
|
+
timestamp={msg.timestamp}
|
|
174
|
+
events={extendedMsg.streamingEvents || []}
|
|
175
|
+
isActive={index === lastAssistantMessageIndex}
|
|
176
|
+
>
|
|
177
|
+
{showThinkingScratchpad && (
|
|
178
|
+
<ThinkingScratchpad
|
|
179
|
+
content={aiScratchpad || ''}
|
|
180
|
+
isVisible={showThinkingScratchpad}
|
|
181
|
+
/>
|
|
182
|
+
)}
|
|
183
|
+
|
|
184
|
+
{showConsolidatedStatusIndicator && (
|
|
185
|
+
<AIEditingStatusIndicator
|
|
186
|
+
status={consolidatedStatus?.status || ''}
|
|
187
|
+
fileName={consolidatedStatus?.fileName}
|
|
188
|
+
/>
|
|
189
|
+
)}
|
|
190
|
+
|
|
191
|
+
{showTypingIndicator && <TypingIndicator />}
|
|
192
|
+
</StreamingMessage>
|
|
193
|
+
);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// Use regular Message component for user messages and non-streaming assistant messages
|
|
173
197
|
return (
|
|
174
198
|
<Message
|
|
175
199
|
key={msg.id}
|
|
@@ -180,18 +204,15 @@ const MessageListComponent = ({
|
|
|
180
204
|
diffData={(msg as any).diffData}
|
|
181
205
|
chatId={chatId}
|
|
182
206
|
showAdditionalWidgets={showAdditionalWidgets}
|
|
207
|
+
ref={
|
|
208
|
+
isLastMessage && (msg as any).diffData
|
|
209
|
+
? diffViewRef
|
|
210
|
+
: null
|
|
211
|
+
}
|
|
183
212
|
/>
|
|
184
213
|
);
|
|
185
214
|
})}
|
|
186
215
|
|
|
187
|
-
{showThinkingScratchpad && (
|
|
188
|
-
<ThinkingScratchpad
|
|
189
|
-
content={aiScratchpad || ''}
|
|
190
|
-
isVisible={showThinkingScratchpad}
|
|
191
|
-
/>
|
|
192
|
-
)}
|
|
193
|
-
|
|
194
|
-
{showTypingIndicator && <TypingIndicator />}
|
|
195
216
|
<div ref={messagesEndRef} />
|
|
196
217
|
</div>
|
|
197
218
|
);
|
|
@@ -1,35 +1,77 @@
|
|
|
1
|
+
import React, { useMemo } from 'react';
|
|
2
|
+
import { timestampToDate } from '@vizhub/viz-utils';
|
|
1
3
|
import Markdown from 'react-markdown';
|
|
2
4
|
import remarkGfm from 'remark-gfm';
|
|
3
|
-
import {
|
|
5
|
+
import { StreamingEvent } from '../../../types.js';
|
|
6
|
+
import { IndividualFileDiff } from './IndividualFileDiff';
|
|
7
|
+
|
|
8
|
+
const DEBUG = false;
|
|
4
9
|
|
|
5
10
|
interface StreamingMessageProps {
|
|
6
|
-
|
|
7
|
-
|
|
11
|
+
timestamp: number;
|
|
12
|
+
events: StreamingEvent[];
|
|
13
|
+
isActive?: boolean; // Is this the currently streaming message?
|
|
14
|
+
children?: React.ReactNode;
|
|
8
15
|
}
|
|
9
16
|
|
|
10
|
-
const
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
17
|
+
export const StreamingMessage: React.FC<
|
|
18
|
+
StreamingMessageProps
|
|
19
|
+
> = ({ timestamp, events, isActive, children }) => {
|
|
20
|
+
DEBUG &&
|
|
21
|
+
console.log(
|
|
22
|
+
'StreamingMessage: Rendered with events:',
|
|
23
|
+
events,
|
|
24
|
+
);
|
|
25
|
+
|
|
26
|
+
// Memoize date formatting to avoid repeated computation
|
|
27
|
+
const formattedTime = useMemo(() => {
|
|
28
|
+
return timestampToDate(timestamp).toLocaleTimeString(
|
|
29
|
+
[],
|
|
30
|
+
{
|
|
31
|
+
hour: '2-digit',
|
|
32
|
+
minute: '2-digit',
|
|
33
|
+
},
|
|
34
|
+
);
|
|
35
|
+
}, [timestamp]);
|
|
18
36
|
|
|
19
37
|
return (
|
|
20
38
|
<div className="ai-chat-message assistant streaming">
|
|
21
|
-
{status && (
|
|
22
|
-
<div className="ai-chat-status">{status}</div>
|
|
23
|
-
)}
|
|
24
39
|
<div className="ai-chat-message-content">
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
40
|
+
{/* Render events in order */}
|
|
41
|
+
{events.map((event, index) => {
|
|
42
|
+
switch (event.type) {
|
|
43
|
+
case 'text_chunk':
|
|
44
|
+
return (
|
|
45
|
+
<div
|
|
46
|
+
key={`text-${index}`}
|
|
47
|
+
className="text-chunk"
|
|
48
|
+
>
|
|
49
|
+
<Markdown remarkPlugins={[remarkGfm]}>
|
|
50
|
+
{event.content}
|
|
51
|
+
</Markdown>
|
|
52
|
+
</div>
|
|
53
|
+
);
|
|
54
|
+
case 'file_complete':
|
|
55
|
+
return (
|
|
56
|
+
<IndividualFileDiff
|
|
57
|
+
key={`file-${event.fileName}-${index}`}
|
|
58
|
+
fileName={event.fileName}
|
|
59
|
+
beforeContent={event.beforeContent || ''}
|
|
60
|
+
afterContent={event.afterContent || ''}
|
|
61
|
+
/>
|
|
62
|
+
);
|
|
63
|
+
case 'file_start':
|
|
64
|
+
// File start events are now handled by centralized status logic in MessageList
|
|
65
|
+
return null;
|
|
66
|
+
default:
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
})}
|
|
70
|
+
{isActive && children}
|
|
71
|
+
</div>
|
|
72
|
+
<div className="ai-chat-message-time">
|
|
73
|
+
{formattedTime}
|
|
28
74
|
</div>
|
|
29
75
|
</div>
|
|
30
76
|
);
|
|
31
77
|
};
|
|
32
|
-
|
|
33
|
-
export const StreamingMessage = memo(
|
|
34
|
-
StreamingMessageComponent,
|
|
35
|
-
);
|