vzcode 2.15.0 → 2.17.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/{bindings_wasm_bg-9w8E-TmY.wasm → bindings_wasm_bg-D9vNLG9F.wasm} +0 -0
- package/dist/assets/buildWorker-DL4wXpRr.js +695 -0
- package/dist/assets/index-BiPmUreW.js +474 -0
- package/dist/assets/{index-DLm5FQ0E.css → index-BvGPtSrr.css} +1 -1
- package/dist/assets/worker-RJ-cjbld.js +327 -0
- 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 +89 -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 +124 -56
- package/src/client/VZSidebar/AIChat/MessageList.tsx +155 -114
- package/src/client/VZSidebar/AIChat/ThinkingScratchpad.tsx +4 -118
- package/src/client/VZSidebar/AIChat/index.tsx +59 -19
- package/src/client/VZSidebar/AIChat/styles.scss +190 -43
- 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 +328 -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
- package/dist/assets/buildWorker-Bi6wsfQk.js +0 -695
- package/dist/assets/index-BpUpNto4.js +0 -461
- package/dist/assets/worker-BSzbM0Uo.js +0 -330
- package/src/client/VZSidebar/AIChat/StreamingMessage.tsx +0 -35
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import React, { useCallback } from 'react';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Props for the JumpToLatestButton component
|
|
5
|
+
*/
|
|
6
|
+
interface JumpToLatestButtonProps {
|
|
7
|
+
/** Whether the button is visible */
|
|
8
|
+
visible: boolean;
|
|
9
|
+
/** Callback when button is clicked */
|
|
10
|
+
onClick: () => void;
|
|
11
|
+
/** Additional CSS class name */
|
|
12
|
+
className?: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Circular "down arrow" button that floats at the bottom center of scroll area
|
|
17
|
+
* Appears when auto-scroll is disabled and user is not at bottom
|
|
18
|
+
*/
|
|
19
|
+
export const JumpToLatestButton: React.FC<
|
|
20
|
+
JumpToLatestButtonProps
|
|
21
|
+
> = ({ visible, onClick, className = '' }) => {
|
|
22
|
+
/**
|
|
23
|
+
* Handle button click
|
|
24
|
+
*/
|
|
25
|
+
const handleClick = useCallback(() => {
|
|
26
|
+
onClick();
|
|
27
|
+
}, [onClick]);
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Handle keyboard events for accessibility
|
|
31
|
+
*/
|
|
32
|
+
const handleKeyDown = useCallback(
|
|
33
|
+
(event: React.KeyboardEvent) => {
|
|
34
|
+
if (event.key === 'Enter' || event.key === ' ') {
|
|
35
|
+
event.preventDefault();
|
|
36
|
+
onClick();
|
|
37
|
+
}
|
|
38
|
+
},
|
|
39
|
+
[onClick],
|
|
40
|
+
);
|
|
41
|
+
|
|
42
|
+
if (!visible) {
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return (
|
|
47
|
+
<button
|
|
48
|
+
className={`jump-to-latest-button ${className}`.trim()}
|
|
49
|
+
onClick={handleClick}
|
|
50
|
+
onKeyDown={handleKeyDown}
|
|
51
|
+
aria-label="Jump to latest"
|
|
52
|
+
tabIndex={0}
|
|
53
|
+
type="button"
|
|
54
|
+
>
|
|
55
|
+
{/* Down arrow icon using Unicode or can be replaced with SVG */}
|
|
56
|
+
<span
|
|
57
|
+
className="jump-to-latest-icon"
|
|
58
|
+
aria-hidden="true"
|
|
59
|
+
>
|
|
60
|
+
▼
|
|
61
|
+
</span>
|
|
62
|
+
</button>
|
|
63
|
+
);
|
|
64
|
+
};
|
|
@@ -1,77 +1,145 @@
|
|
|
1
|
+
import React, {
|
|
2
|
+
useMemo,
|
|
3
|
+
useContext,
|
|
4
|
+
forwardRef,
|
|
5
|
+
} from 'react';
|
|
1
6
|
import { timestampToDate } from '@vizhub/viz-utils';
|
|
2
7
|
import Markdown from 'react-markdown';
|
|
3
8
|
import remarkGfm from 'remark-gfm';
|
|
4
|
-
import
|
|
5
|
-
import {
|
|
9
|
+
import { StreamingEvent } from '../../../types.js';
|
|
10
|
+
import { IndividualFileDiff } from './IndividualFileDiff';
|
|
11
|
+
import { VZCodeContext } from '../../VZCodeContext';
|
|
12
|
+
import { DiffView, DiffViewRef } from './DiffView';
|
|
6
13
|
import { UnifiedFilesDiff } from '../../../utils/fileDiff';
|
|
7
14
|
import { enableDiffView } from '../../featureFlags';
|
|
8
|
-
|
|
15
|
+
|
|
16
|
+
const DEBUG = false;
|
|
9
17
|
|
|
10
18
|
interface MessageProps {
|
|
11
19
|
id: string;
|
|
12
20
|
role: 'user' | 'assistant';
|
|
13
|
-
content
|
|
21
|
+
content?: string; // For non-streaming messages
|
|
14
22
|
timestamp: number;
|
|
15
|
-
|
|
16
|
-
|
|
23
|
+
events?: StreamingEvent[]; // For streaming messages
|
|
24
|
+
isActive?: boolean; // Is this the currently streaming message?
|
|
17
25
|
chatId?: string;
|
|
18
26
|
showAdditionalWidgets?: boolean;
|
|
27
|
+
isStreaming?: boolean;
|
|
28
|
+
diffData?: UnifiedFilesDiff;
|
|
19
29
|
}
|
|
20
30
|
|
|
21
|
-
const
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
31
|
+
export const Message = forwardRef<
|
|
32
|
+
DiffViewRef,
|
|
33
|
+
React.PropsWithChildren<MessageProps>
|
|
34
|
+
>(
|
|
35
|
+
(
|
|
36
|
+
{
|
|
37
|
+
id,
|
|
38
|
+
role,
|
|
39
|
+
content,
|
|
40
|
+
timestamp,
|
|
41
|
+
events = [],
|
|
42
|
+
isActive,
|
|
43
|
+
children,
|
|
44
|
+
chatId,
|
|
45
|
+
showAdditionalWidgets = false,
|
|
46
|
+
isStreaming = false,
|
|
47
|
+
diffData,
|
|
48
|
+
},
|
|
49
|
+
ref,
|
|
50
|
+
) => {
|
|
51
|
+
const { additionalWidgets, handleSendMessage } =
|
|
52
|
+
useContext(VZCodeContext);
|
|
33
53
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
54
|
+
DEBUG &&
|
|
55
|
+
console.log(
|
|
56
|
+
'StreamingMessage: Rendered with events:',
|
|
57
|
+
events,
|
|
58
|
+
);
|
|
59
|
+
|
|
60
|
+
// Memoize date formatting to avoid repeated computation
|
|
61
|
+
const formattedTime = useMemo(() => {
|
|
62
|
+
return timestampToDate(timestamp).toLocaleTimeString(
|
|
63
|
+
[],
|
|
64
|
+
{
|
|
65
|
+
hour: '2-digit',
|
|
66
|
+
minute: '2-digit',
|
|
67
|
+
},
|
|
68
|
+
);
|
|
69
|
+
}, [timestamp]);
|
|
44
70
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
71
|
+
// Memoize the className string to avoid recreation
|
|
72
|
+
const messageClassName = useMemo(() => {
|
|
73
|
+
return `ai-chat-message ${role}${isStreaming ? ' streaming' : ''}`;
|
|
74
|
+
}, [role, isStreaming]);
|
|
49
75
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
{content
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
Object.keys(diffData).length > 0 && (
|
|
59
|
-
<DiffView diffData={diffData} />
|
|
76
|
+
return (
|
|
77
|
+
<div className={messageClassName}>
|
|
78
|
+
<div className="ai-chat-message-content">
|
|
79
|
+
{/* Render regular content for non-streaming messages */}
|
|
80
|
+
{content && (
|
|
81
|
+
<Markdown remarkPlugins={[remarkGfm]}>
|
|
82
|
+
{content}
|
|
83
|
+
</Markdown>
|
|
60
84
|
)}
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
85
|
+
|
|
86
|
+
{/* Render diff view if present */}
|
|
87
|
+
{enableDiffView &&
|
|
88
|
+
diffData &&
|
|
89
|
+
Object.keys(diffData).length > 0 && (
|
|
90
|
+
<DiffView diffData={diffData} ref={ref} />
|
|
91
|
+
)}
|
|
92
|
+
|
|
93
|
+
{/* Render events in order for streaming messages */}
|
|
94
|
+
{events.map((event, index) => {
|
|
95
|
+
switch (event.type) {
|
|
96
|
+
case 'text_chunk':
|
|
97
|
+
return (
|
|
98
|
+
<div
|
|
99
|
+
key={`text-${index}`}
|
|
100
|
+
className="text-chunk"
|
|
101
|
+
>
|
|
102
|
+
<Markdown remarkPlugins={[remarkGfm]}>
|
|
103
|
+
{event.content}
|
|
104
|
+
</Markdown>
|
|
105
|
+
</div>
|
|
106
|
+
);
|
|
107
|
+
case 'file_complete':
|
|
108
|
+
return (
|
|
109
|
+
<IndividualFileDiff
|
|
110
|
+
key={`file-${event.fileName}-${index}`}
|
|
111
|
+
fileName={event.fileName}
|
|
112
|
+
beforeContent={
|
|
113
|
+
event.beforeContent || ''
|
|
114
|
+
}
|
|
115
|
+
afterContent={event.afterContent || ''}
|
|
116
|
+
/>
|
|
117
|
+
);
|
|
118
|
+
case 'file_start':
|
|
119
|
+
// File start events are now handled by centralized status logic in MessageList
|
|
120
|
+
return null;
|
|
121
|
+
default:
|
|
122
|
+
return null;
|
|
123
|
+
}
|
|
68
124
|
})}
|
|
125
|
+
{/* Additional widgets are now shown within the "Done" status indicator */}
|
|
126
|
+
{showAdditionalWidgets &&
|
|
127
|
+
additionalWidgets &&
|
|
128
|
+
chatId &&
|
|
129
|
+
!isStreaming && // Only show here for non-streaming messages (completed messages)
|
|
130
|
+
additionalWidgets({
|
|
131
|
+
messageId: id,
|
|
132
|
+
chatId: chatId,
|
|
133
|
+
handleSendMessage,
|
|
134
|
+
})}
|
|
135
|
+
{isActive && children}
|
|
136
|
+
</div>
|
|
137
|
+
<div className="ai-chat-message-time">
|
|
138
|
+
{formattedTime}
|
|
139
|
+
</div>
|
|
69
140
|
</div>
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
</div>
|
|
74
|
-
);
|
|
75
|
-
};
|
|
141
|
+
);
|
|
142
|
+
},
|
|
143
|
+
);
|
|
76
144
|
|
|
77
|
-
|
|
145
|
+
Message.displayName = 'Message';
|
|
@@ -1,141 +1,84 @@
|
|
|
1
1
|
import {
|
|
2
2
|
useRef,
|
|
3
3
|
useEffect,
|
|
4
|
-
useCallback,
|
|
5
4
|
memo,
|
|
6
5
|
useState,
|
|
6
|
+
useContext,
|
|
7
7
|
} from 'react';
|
|
8
8
|
import { Message } from './Message';
|
|
9
9
|
import { TypingIndicator } from './TypingIndicator';
|
|
10
10
|
import { ThinkingScratchpad } from './ThinkingScratchpad';
|
|
11
|
+
import { AIEditingStatusIndicator } from './FileEditingIndicator';
|
|
11
12
|
import { VizChatMessage } from '@vizhub/viz-types';
|
|
13
|
+
import { ExtendedVizChatMessage } from '../../../types.js';
|
|
14
|
+
import { DiffViewRef } from './DiffView.js';
|
|
15
|
+
import { VZCodeContext } from '../../VZCodeContext';
|
|
12
16
|
|
|
13
17
|
const MessageListComponent = ({
|
|
14
18
|
messages,
|
|
15
19
|
isLoading,
|
|
16
20
|
chatId, // Add chatId prop
|
|
17
21
|
aiScratchpad, // Add aiScratchpad prop
|
|
22
|
+
currentStatus, // Add current status prop
|
|
23
|
+
onNewEvent,
|
|
24
|
+
onJumpToLatest,
|
|
25
|
+
beforeRender,
|
|
26
|
+
afterRender,
|
|
18
27
|
}: {
|
|
19
28
|
messages: VizChatMessage[];
|
|
20
29
|
isLoading: boolean;
|
|
21
30
|
chatId?: string; // Add chatId to the type
|
|
22
31
|
aiScratchpad?: string; // Add aiScratchpad to the type
|
|
32
|
+
currentStatus?: string; // Add current status to the type
|
|
33
|
+
onNewEvent: (targetElement?: HTMLElement) => void;
|
|
34
|
+
onJumpToLatest: (targetElement?: HTMLElement) => void;
|
|
35
|
+
beforeRender: () => number;
|
|
36
|
+
afterRender: (prevScrollHeight: number) => void;
|
|
23
37
|
}) => {
|
|
24
38
|
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
|
-
}
|
|
39
|
+
const diffViewRef = useRef<DiffViewRef>(null);
|
|
67
40
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
setAutoScrollEnabled(false);
|
|
72
|
-
}
|
|
41
|
+
// Get additional widgets from context
|
|
42
|
+
const { additionalWidgets, handleSendMessage } =
|
|
43
|
+
useContext(VZCodeContext);
|
|
73
44
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
setIsUserScrolled(false);
|
|
78
|
-
setAutoScrollEnabled(true);
|
|
79
|
-
}, 500); // 500ms delay to prevent flickering
|
|
80
|
-
}
|
|
81
|
-
}, [isUserScrolled, isScrolledToBottom]);
|
|
45
|
+
// Track previous loading state to detect when AI generation completes
|
|
46
|
+
const [prevIsLoading, setPrevIsLoading] =
|
|
47
|
+
useState(isLoading);
|
|
82
48
|
|
|
83
|
-
// Auto-scroll when messages change
|
|
49
|
+
// Auto-scroll when messages change
|
|
84
50
|
useEffect(() => {
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
51
|
+
// Get scroll height before render for anchoring
|
|
52
|
+
const prevScrollHeight = beforeRender();
|
|
53
|
+
|
|
54
|
+
// Determine if we should scroll to a specific diff or to the bottom
|
|
55
|
+
const lastMessageHasDiff =
|
|
56
|
+
messages.length > 0 &&
|
|
57
|
+
(messages[messages.length - 1] as any).diffData;
|
|
58
|
+
|
|
59
|
+
let targetElement: HTMLElement | null = null;
|
|
60
|
+
if (lastMessageHasDiff && diffViewRef.current) {
|
|
61
|
+
targetElement =
|
|
62
|
+
diffViewRef.current.getFirstHunkElement();
|
|
94
63
|
}
|
|
95
64
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
clearTimeout(scrollTimeoutRef.current);
|
|
99
|
-
}
|
|
100
|
-
};
|
|
101
|
-
}, [
|
|
102
|
-
messages,
|
|
103
|
-
autoScrollEnabled,
|
|
104
|
-
isUserScrolled,
|
|
105
|
-
scrollToBottom,
|
|
106
|
-
]);
|
|
65
|
+
// Trigger auto-scroll if enabled
|
|
66
|
+
onNewEvent(targetElement);
|
|
107
67
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
68
|
+
// Adjust scroll position after render for anchoring
|
|
69
|
+
afterRender(prevScrollHeight);
|
|
70
|
+
}, [messages, onNewEvent, beforeRender, afterRender]);
|
|
111
71
|
|
|
112
72
|
// Scroll to bottom when AI generation completes (loading changes from true to false)
|
|
113
73
|
useEffect(() => {
|
|
114
|
-
// If AI was generating and now it's finished, scroll to bottom
|
|
74
|
+
// If AI was generating and now it's finished, force scroll to bottom
|
|
115
75
|
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);
|
|
76
|
+
// Force jump to latest regardless of current auto-scroll state
|
|
77
|
+
onJumpToLatest();
|
|
126
78
|
}
|
|
127
79
|
|
|
128
80
|
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
|
-
}, []);
|
|
81
|
+
}, [isLoading, prevIsLoading, onJumpToLatest]);
|
|
139
82
|
|
|
140
83
|
// Check if AI generation has started (last message is from assistant)
|
|
141
84
|
const lastMessage = messages[messages.length - 1];
|
|
@@ -152,6 +95,56 @@ const MessageListComponent = ({
|
|
|
152
95
|
aiScratchpad && aiScratchpad.trim(),
|
|
153
96
|
);
|
|
154
97
|
|
|
98
|
+
// Determine the single, most relevant status to display with priority:
|
|
99
|
+
// 1. Active file editing status from streaming events
|
|
100
|
+
// 2. General AI status from currentStatus
|
|
101
|
+
const getConsolidatedStatus = () => {
|
|
102
|
+
// Check if there's an active streaming message with file editing
|
|
103
|
+
if (lastMessage && lastMessage.role === 'assistant') {
|
|
104
|
+
const extendedMsg =
|
|
105
|
+
lastMessage as ExtendedVizChatMessage;
|
|
106
|
+
if (
|
|
107
|
+
extendedMsg.streamingEvents &&
|
|
108
|
+
extendedMsg.streamingEvents.length > 0
|
|
109
|
+
) {
|
|
110
|
+
// Look for active file editing (file_start without corresponding file_complete)
|
|
111
|
+
const fileStates = new Map<string, boolean>();
|
|
112
|
+
|
|
113
|
+
extendedMsg.streamingEvents.forEach((event) => {
|
|
114
|
+
if (event.type === 'file_start') {
|
|
115
|
+
fileStates.set(event.fileName, false); // Mark as editing
|
|
116
|
+
} else if (event.type === 'file_complete') {
|
|
117
|
+
fileStates.set(event.fileName, true); // Mark as complete
|
|
118
|
+
}
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
// Find the first file that's still being edited
|
|
122
|
+
for (const [fileName, isComplete] of fileStates) {
|
|
123
|
+
if (!isComplete) {
|
|
124
|
+
return {
|
|
125
|
+
status: `Editing ${fileName}...`,
|
|
126
|
+
fileName,
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Fall back to general AI status if no active file editing
|
|
134
|
+
if (currentStatus) {
|
|
135
|
+
return { status: currentStatus };
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
return null;
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
const consolidatedStatus = getConsolidatedStatus();
|
|
142
|
+
|
|
143
|
+
// Show consolidated status indicator when there's a status to display
|
|
144
|
+
const showConsolidatedStatusIndicator = Boolean(
|
|
145
|
+
consolidatedStatus,
|
|
146
|
+
);
|
|
147
|
+
|
|
155
148
|
// Find the most recent assistant message index
|
|
156
149
|
const lastAssistantMessageIndex = messages
|
|
157
150
|
.map((m, i) => (m.role === 'assistant' ? i : -1))
|
|
@@ -161,37 +154,85 @@ const MessageListComponent = ({
|
|
|
161
154
|
return (
|
|
162
155
|
<div
|
|
163
156
|
className="ai-chat-messages"
|
|
164
|
-
|
|
165
|
-
|
|
157
|
+
role="log"
|
|
158
|
+
aria-live="polite"
|
|
159
|
+
aria-relevant="additions"
|
|
166
160
|
>
|
|
167
161
|
{messages.map((msg, index) => {
|
|
162
|
+
const isLastMessage = index === messages.length - 1;
|
|
163
|
+
// Cast to extended message to check for streaming events
|
|
164
|
+
const extendedMsg = msg as ExtendedVizChatMessage;
|
|
165
|
+
|
|
166
|
+
// Check if this is a streaming message
|
|
167
|
+
const isStreamingMessage =
|
|
168
|
+
!!extendedMsg.isProgressive;
|
|
169
|
+
|
|
168
170
|
// Only show additionalWidgets for the most recent assistant message
|
|
169
171
|
const showAdditionalWidgets =
|
|
170
172
|
msg.role === 'assistant' &&
|
|
171
173
|
index === lastAssistantMessageIndex;
|
|
172
174
|
|
|
175
|
+
// Use Message for all messages now
|
|
173
176
|
return (
|
|
174
177
|
<Message
|
|
175
178
|
key={msg.id}
|
|
176
179
|
id={msg.id}
|
|
177
180
|
role={msg.role}
|
|
178
|
-
content={
|
|
181
|
+
content={
|
|
182
|
+
isStreamingMessage ? undefined : msg.content
|
|
183
|
+
}
|
|
179
184
|
timestamp={msg.timestamp}
|
|
180
|
-
|
|
185
|
+
events={
|
|
186
|
+
isStreamingMessage
|
|
187
|
+
? extendedMsg.streamingEvents || []
|
|
188
|
+
: []
|
|
189
|
+
}
|
|
190
|
+
isActive={index === lastAssistantMessageIndex}
|
|
181
191
|
chatId={chatId}
|
|
182
192
|
showAdditionalWidgets={showAdditionalWidgets}
|
|
183
|
-
|
|
193
|
+
isStreaming={isStreamingMessage}
|
|
194
|
+
diffData={(msg as any).diffData}
|
|
195
|
+
ref={
|
|
196
|
+
isLastMessage && (msg as any).diffData
|
|
197
|
+
? diffViewRef
|
|
198
|
+
: null
|
|
199
|
+
}
|
|
200
|
+
>
|
|
201
|
+
{isStreamingMessage &&
|
|
202
|
+
showThinkingScratchpad && (
|
|
203
|
+
<ThinkingScratchpad
|
|
204
|
+
content={aiScratchpad || ''}
|
|
205
|
+
isVisible={showThinkingScratchpad}
|
|
206
|
+
/>
|
|
207
|
+
)}
|
|
208
|
+
|
|
209
|
+
{isStreamingMessage &&
|
|
210
|
+
showConsolidatedStatusIndicator && (
|
|
211
|
+
<AIEditingStatusIndicator
|
|
212
|
+
status={consolidatedStatus?.status || ''}
|
|
213
|
+
fileName={consolidatedStatus?.fileName}
|
|
214
|
+
additionalWidgets={
|
|
215
|
+
consolidatedStatus?.status === 'Done' &&
|
|
216
|
+
showAdditionalWidgets &&
|
|
217
|
+
additionalWidgets &&
|
|
218
|
+
chatId
|
|
219
|
+
? additionalWidgets({
|
|
220
|
+
messageId: msg.id,
|
|
221
|
+
chatId: chatId,
|
|
222
|
+
handleSendMessage,
|
|
223
|
+
})
|
|
224
|
+
: undefined
|
|
225
|
+
}
|
|
226
|
+
/>
|
|
227
|
+
)}
|
|
228
|
+
|
|
229
|
+
{isStreamingMessage && showTypingIndicator && (
|
|
230
|
+
<TypingIndicator />
|
|
231
|
+
)}
|
|
232
|
+
</Message>
|
|
184
233
|
);
|
|
185
234
|
})}
|
|
186
235
|
|
|
187
|
-
{showThinkingScratchpad && (
|
|
188
|
-
<ThinkingScratchpad
|
|
189
|
-
content={aiScratchpad || ''}
|
|
190
|
-
isVisible={showThinkingScratchpad}
|
|
191
|
-
/>
|
|
192
|
-
)}
|
|
193
|
-
|
|
194
|
-
{showTypingIndicator && <TypingIndicator />}
|
|
195
236
|
<div ref={messagesEndRef} />
|
|
196
237
|
</div>
|
|
197
238
|
);
|