vzcode 2.22.0 → 2.26.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/{buildWorker-Bi6wsfQk.js → buildWorker-DylWm2VX.js} +64 -72
- package/dist/assets/index-CkFweu5-.js +403 -0
- package/dist/assets/{index-fSroLVgi.css → index-Dyt9tpmu.css} +1 -5
- package/dist/assets/{worker-BSzbM0Uo.js → worker-VPLhQVba.js} +230 -230
- package/dist/assets/worker-b9bkZkXK.js +116 -0
- package/dist/assets/{worker-Cm3I3tnR.js → worker-k7HZ3t-V.js} +3 -20
- package/dist/index.html +2 -2
- package/dist/llm-streaming-server/chatOperations.js +19 -0
- package/dist/llm-streaming-server/llmStreaming.js +21 -14
- package/dist/server/aiChatHandler/index.js +6 -3
- package/package.json +23 -22
- package/src/llm-streaming-server/chatOperations.ts +27 -0
- package/src/llm-streaming-server/llmStreaming.ts +27 -14
- package/src/llm-streaming-ui/components/Message.tsx +8 -7
- package/src/llm-streaming-ui/components/MessageList.tsx +5 -0
- package/src/llm-streaming-ui/components/index.tsx +2 -2
- package/src/llm-streaming-ui/components/styles.scss +10 -0
- package/src/server/aiChatHandler/index.ts +9 -1
- package/src/types.ts +1 -0
- package/dist/assets/index-CfDs-dAu.js +0 -476
- package/dist/assets/worker-KiuLM-X4.js +0 -136
- package/dist/server/aiChatHandler/aiEditing.js +0 -58
- package/dist/server/aiChatHandler/chatOperations.js +0 -494
- package/dist/server/aiChatHandler/errorHandling.js +0 -49
- package/dist/server/aiChatHandler/llmStreaming.js +0 -265
- package/dist/server/aiChatHandler/validation.js +0 -19
- package/src/client/VZSidebar/AIChat/ChatInput.tsx +0 -237
- package/src/client/VZSidebar/AIChat/DiffView.scss +0 -173
- package/src/client/VZSidebar/AIChat/DiffView.tsx +0 -236
- package/src/client/VZSidebar/AIChat/FileEditingIndicator.tsx +0 -89
- package/src/client/VZSidebar/AIChat/IndividualFileDiff.tsx +0 -86
- package/src/client/VZSidebar/AIChat/JumpToLatestButton.tsx +0 -64
- package/src/client/VZSidebar/AIChat/Message.tsx +0 -145
- package/src/client/VZSidebar/AIChat/MessageList.tsx +0 -241
- package/src/client/VZSidebar/AIChat/ThinkingScratchpad.tsx +0 -42
- package/src/client/VZSidebar/AIChat/TypingIndicator.tsx +0 -19
- package/src/client/VZSidebar/AIChat/index.tsx +0 -388
- package/src/client/VZSidebar/AIChat/styles.scss +0 -831
- package/src/client/VZSidebar/AIChat/useSpeechRecognition.ts +0 -116
- package/src/server/aiChatHandler/aiEditing.ts +0 -102
- package/src/server/aiChatHandler/chatOperations.ts +0 -655
- package/src/server/aiChatHandler/errorHandling.ts +0 -66
- package/src/server/aiChatHandler/llmStreaming.ts +0 -403
- package/src/server/aiChatHandler/validation.ts +0 -24
|
@@ -1,236 +0,0 @@
|
|
|
1
|
-
import React, {
|
|
2
|
-
useContext,
|
|
3
|
-
useEffect,
|
|
4
|
-
useRef,
|
|
5
|
-
forwardRef,
|
|
6
|
-
useImperativeHandle,
|
|
7
|
-
} from 'react';
|
|
8
|
-
import {
|
|
9
|
-
UnifiedFilesDiff,
|
|
10
|
-
parseUnifiedDiffStats,
|
|
11
|
-
combineUnifiedDiffs,
|
|
12
|
-
isFileDeletion,
|
|
13
|
-
getDeletedFileName,
|
|
14
|
-
} from '../../../utils/fileDiff';
|
|
15
|
-
import * as Diff2Html from 'diff2html';
|
|
16
|
-
import 'diff2html/bundles/css/diff2html.min.css';
|
|
17
|
-
import './DiffView.scss';
|
|
18
|
-
import { VZCodeContext } from '../../VZCodeContext';
|
|
19
|
-
import {
|
|
20
|
-
scrollToFirstDiff,
|
|
21
|
-
getHeaderOffset,
|
|
22
|
-
announceDiffSummary,
|
|
23
|
-
} from '../../utils/scrollUtils';
|
|
24
|
-
import { getFileId } from '@vizhub/viz-utils';
|
|
25
|
-
|
|
26
|
-
interface DiffViewProps {
|
|
27
|
-
diffData: UnifiedFilesDiff;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
// Methods that can be called on DiffView from parent components
|
|
31
|
-
export interface DiffViewRef {
|
|
32
|
-
scrollToFirstHunk: () => void;
|
|
33
|
-
focusDiffContainer: () => void;
|
|
34
|
-
announceSummary: () => void;
|
|
35
|
-
getFirstHunkElement: () => HTMLElement | null;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
export const DiffView = forwardRef<
|
|
39
|
-
DiffViewRef,
|
|
40
|
-
DiffViewProps
|
|
41
|
-
>(({ diffData }, ref) => {
|
|
42
|
-
const { content, openTab, setIsAIChatOpen } =
|
|
43
|
-
useContext(VZCodeContext);
|
|
44
|
-
const diffContainerRef = useRef<HTMLDivElement>(null);
|
|
45
|
-
|
|
46
|
-
const allDiffs = Object.values(diffData).filter(
|
|
47
|
-
(diff) => diff.length > 0,
|
|
48
|
-
);
|
|
49
|
-
|
|
50
|
-
// Separate deleted files from regular diffs
|
|
51
|
-
const deletedFiles: string[] = [];
|
|
52
|
-
const regularDiffs: string[] = [];
|
|
53
|
-
|
|
54
|
-
for (const diff of allDiffs) {
|
|
55
|
-
if (isFileDeletion(diff)) {
|
|
56
|
-
deletedFiles.push(diff);
|
|
57
|
-
} else {
|
|
58
|
-
regularDiffs.push(diff);
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
// Calculate statistics from regular unified diffs only
|
|
63
|
-
let totalAdditions = 0;
|
|
64
|
-
let totalDeletions = 0;
|
|
65
|
-
|
|
66
|
-
for (const unifiedDiff of regularDiffs) {
|
|
67
|
-
const stats = parseUnifiedDiffStats(unifiedDiff);
|
|
68
|
-
totalAdditions += stats.additions;
|
|
69
|
-
totalDeletions += stats.deletions;
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
// Combine regular unified diffs and convert to HTML using diff2html
|
|
73
|
-
const combinedUnifiedDiff = combineUnifiedDiffs(diffData);
|
|
74
|
-
const diffHtml = Diff2Html.html(combinedUnifiedDiff, {
|
|
75
|
-
drawFileList: false,
|
|
76
|
-
matching: 'words',
|
|
77
|
-
diffStyle: 'word',
|
|
78
|
-
outputFormat: 'line-by-line',
|
|
79
|
-
});
|
|
80
|
-
|
|
81
|
-
// Expose methods for parent components to control scrolling and focus
|
|
82
|
-
useImperativeHandle(
|
|
83
|
-
ref,
|
|
84
|
-
() => ({
|
|
85
|
-
scrollToFirstHunk: () => {
|
|
86
|
-
if (diffContainerRef.current) {
|
|
87
|
-
const headerOffset = getHeaderOffset();
|
|
88
|
-
scrollToFirstDiff(
|
|
89
|
-
diffContainerRef.current,
|
|
90
|
-
headerOffset,
|
|
91
|
-
);
|
|
92
|
-
}
|
|
93
|
-
},
|
|
94
|
-
focusDiffContainer: () => {
|
|
95
|
-
if (diffContainerRef.current) {
|
|
96
|
-
diffContainerRef.current.tabIndex = -1;
|
|
97
|
-
diffContainerRef.current.focus();
|
|
98
|
-
}
|
|
99
|
-
},
|
|
100
|
-
announceSummary: () => {
|
|
101
|
-
if (diffContainerRef.current) {
|
|
102
|
-
announceDiffSummary(diffContainerRef.current);
|
|
103
|
-
}
|
|
104
|
-
},
|
|
105
|
-
getFirstHunkElement: () => {
|
|
106
|
-
if (diffContainerRef.current) {
|
|
107
|
-
return diffContainerRef.current.querySelector(
|
|
108
|
-
'.d2h-diff-tbody tr',
|
|
109
|
-
);
|
|
110
|
-
}
|
|
111
|
-
return null;
|
|
112
|
-
},
|
|
113
|
-
}),
|
|
114
|
-
[],
|
|
115
|
-
);
|
|
116
|
-
|
|
117
|
-
// Add click handlers to file names after HTML is rendered
|
|
118
|
-
useEffect(() => {
|
|
119
|
-
if (!diffContainerRef.current) return;
|
|
120
|
-
|
|
121
|
-
const handleFileNameClick = (event: MouseEvent) => {
|
|
122
|
-
const target = event.target as HTMLElement;
|
|
123
|
-
if (target.classList.contains('d2h-file-name')) {
|
|
124
|
-
event.preventDefault();
|
|
125
|
-
event.stopPropagation();
|
|
126
|
-
|
|
127
|
-
console.log('File name clicked');
|
|
128
|
-
|
|
129
|
-
const fileName = target.textContent?.trim();
|
|
130
|
-
|
|
131
|
-
console.log('Clicked file name:', fileName);
|
|
132
|
-
if (fileName && content) {
|
|
133
|
-
const fileId = getFileId(content, fileName);
|
|
134
|
-
|
|
135
|
-
if (fileId) {
|
|
136
|
-
// Open the file tab and switch to files view
|
|
137
|
-
openTab({ fileId, isTransient: false });
|
|
138
|
-
setIsAIChatOpen(false);
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
|
-
};
|
|
143
|
-
|
|
144
|
-
const container = diffContainerRef.current;
|
|
145
|
-
const fileNameElements = container.querySelectorAll(
|
|
146
|
-
'.d2h-file-name',
|
|
147
|
-
);
|
|
148
|
-
|
|
149
|
-
// Add click event listeners and cursor pointer style
|
|
150
|
-
fileNameElements.forEach((element) => {
|
|
151
|
-
element.addEventListener(
|
|
152
|
-
'click',
|
|
153
|
-
handleFileNameClick,
|
|
154
|
-
);
|
|
155
|
-
(element as HTMLElement).style.cursor = 'pointer';
|
|
156
|
-
(element as HTMLElement).style.textDecoration =
|
|
157
|
-
'underline';
|
|
158
|
-
(element as HTMLElement).style.color = '#58a6ff'; // GitHub blue link color
|
|
159
|
-
});
|
|
160
|
-
|
|
161
|
-
return () => {
|
|
162
|
-
// Cleanup event listeners
|
|
163
|
-
fileNameElements.forEach((element) => {
|
|
164
|
-
element.removeEventListener(
|
|
165
|
-
'click',
|
|
166
|
-
handleFileNameClick,
|
|
167
|
-
);
|
|
168
|
-
});
|
|
169
|
-
};
|
|
170
|
-
}, [diffHtml, content, openTab, setIsAIChatOpen]);
|
|
171
|
-
|
|
172
|
-
if (allDiffs.length === 0) {
|
|
173
|
-
return null;
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
return (
|
|
177
|
-
<div className="diff-view">
|
|
178
|
-
<div className="diff-summary" id="diff-summary">
|
|
179
|
-
<div className="diff-stats">
|
|
180
|
-
<span className="files-changed">
|
|
181
|
-
{allDiffs.length} file
|
|
182
|
-
{allDiffs.length !== 1 ? 's' : ''} changed
|
|
183
|
-
</span>
|
|
184
|
-
{totalAdditions > 0 && (
|
|
185
|
-
<span className="additions">
|
|
186
|
-
+{totalAdditions}
|
|
187
|
-
</span>
|
|
188
|
-
)}
|
|
189
|
-
{totalDeletions > 0 && (
|
|
190
|
-
<span className="deletions">
|
|
191
|
-
-{totalDeletions}
|
|
192
|
-
</span>
|
|
193
|
-
)}
|
|
194
|
-
{deletedFiles.length > 0 && (
|
|
195
|
-
<span className="deletions">
|
|
196
|
-
{deletedFiles.length} deleted
|
|
197
|
-
</span>
|
|
198
|
-
)}
|
|
199
|
-
</div>
|
|
200
|
-
</div>
|
|
201
|
-
|
|
202
|
-
{/* Render deleted files */}
|
|
203
|
-
{deletedFiles.map((deletionMarker, index) => {
|
|
204
|
-
const fileName = getDeletedFileName(deletionMarker);
|
|
205
|
-
return (
|
|
206
|
-
<div key={index} className="deleted-file">
|
|
207
|
-
<div className="deleted-file-header">
|
|
208
|
-
<span className="deleted-file-name">
|
|
209
|
-
{fileName}
|
|
210
|
-
</span>
|
|
211
|
-
<span className="deleted-file-status">
|
|
212
|
-
File deleted
|
|
213
|
-
</span>
|
|
214
|
-
</div>
|
|
215
|
-
</div>
|
|
216
|
-
);
|
|
217
|
-
})}
|
|
218
|
-
|
|
219
|
-
{/* Render regular diffs */}
|
|
220
|
-
{regularDiffs.length > 0 && (
|
|
221
|
-
<div
|
|
222
|
-
className="diff-files"
|
|
223
|
-
ref={diffContainerRef}
|
|
224
|
-
tabIndex={-1}
|
|
225
|
-
role="region"
|
|
226
|
-
aria-label="Code diff content"
|
|
227
|
-
aria-describedby="diff-summary"
|
|
228
|
-
dangerouslySetInnerHTML={{ __html: diffHtml }}
|
|
229
|
-
/>
|
|
230
|
-
)}
|
|
231
|
-
</div>
|
|
232
|
-
);
|
|
233
|
-
});
|
|
234
|
-
|
|
235
|
-
// Add display name for debugging
|
|
236
|
-
DiffView.displayName = 'DiffView';
|
|
@@ -1,89 +0,0 @@
|
|
|
1
|
-
import { Spinner } from '../../AIAssist/Spinner';
|
|
2
|
-
|
|
3
|
-
interface AIEditingStatusIndicatorProps {
|
|
4
|
-
status: string;
|
|
5
|
-
fileName?: string;
|
|
6
|
-
additionalWidgets?: React.ReactNode;
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
export const AIEditingStatusIndicator: React.FC<
|
|
10
|
-
AIEditingStatusIndicatorProps
|
|
11
|
-
> = ({ status, fileName, additionalWidgets }) => {
|
|
12
|
-
const getStatusDisplay = (status: string) => {
|
|
13
|
-
switch (status) {
|
|
14
|
-
case 'Analyzing request...':
|
|
15
|
-
return (
|
|
16
|
-
<>
|
|
17
|
-
🔍 <span>Analyzing request...</span>
|
|
18
|
-
</>
|
|
19
|
-
);
|
|
20
|
-
case 'Formulating a plan...':
|
|
21
|
-
return (
|
|
22
|
-
<>
|
|
23
|
-
💭 <span>Formulating a plan...</span>
|
|
24
|
-
</>
|
|
25
|
-
);
|
|
26
|
-
case 'Describing changes...':
|
|
27
|
-
return (
|
|
28
|
-
<>
|
|
29
|
-
📝 <span>Describing changes...</span>
|
|
30
|
-
</>
|
|
31
|
-
);
|
|
32
|
-
case 'Thinking...':
|
|
33
|
-
return (
|
|
34
|
-
<>
|
|
35
|
-
🤔 <span>Thinking...</span>
|
|
36
|
-
</>
|
|
37
|
-
);
|
|
38
|
-
case 'Done':
|
|
39
|
-
return (
|
|
40
|
-
<div className="file-editing-done-container">
|
|
41
|
-
<div className="file-editing-done-status">
|
|
42
|
-
✅ <span>Done</span>
|
|
43
|
-
</div>
|
|
44
|
-
{additionalWidgets && (
|
|
45
|
-
<div className="file-editing-done-widgets">
|
|
46
|
-
{additionalWidgets}
|
|
47
|
-
</div>
|
|
48
|
-
)}
|
|
49
|
-
</div>
|
|
50
|
-
);
|
|
51
|
-
default:
|
|
52
|
-
// Handle file editing status (e.g., "Editing filename.js...")
|
|
53
|
-
if (status.startsWith('Editing ') && fileName) {
|
|
54
|
-
return (
|
|
55
|
-
<>
|
|
56
|
-
✏️{' '}
|
|
57
|
-
<span>
|
|
58
|
-
Editing <code>{fileName}</code>...
|
|
59
|
-
</span>
|
|
60
|
-
</>
|
|
61
|
-
);
|
|
62
|
-
} else if (status.startsWith('Editing ')) {
|
|
63
|
-
return (
|
|
64
|
-
<>
|
|
65
|
-
✏️ <span>{status}</span>
|
|
66
|
-
</>
|
|
67
|
-
);
|
|
68
|
-
}
|
|
69
|
-
return <span>{status}</span>;
|
|
70
|
-
}
|
|
71
|
-
};
|
|
72
|
-
|
|
73
|
-
const showSpinner = status !== 'Done';
|
|
74
|
-
|
|
75
|
-
return (
|
|
76
|
-
<div className="file-editing-indicator">
|
|
77
|
-
<div className="file-editing-header">
|
|
78
|
-
{showSpinner && (
|
|
79
|
-
<div className="file-editing-icon">
|
|
80
|
-
<Spinner height={16} fadeIn={false} />
|
|
81
|
-
</div>
|
|
82
|
-
)}
|
|
83
|
-
<div className="file-editing-text">
|
|
84
|
-
{getStatusDisplay(status)}
|
|
85
|
-
</div>
|
|
86
|
-
</div>
|
|
87
|
-
</div>
|
|
88
|
-
);
|
|
89
|
-
};
|
|
@@ -1,86 +0,0 @@
|
|
|
1
|
-
import React, { useMemo } from 'react';
|
|
2
|
-
import * as Diff2Html from 'diff2html';
|
|
3
|
-
import 'diff2html/bundles/css/diff2html.min.css';
|
|
4
|
-
import './DiffView.scss';
|
|
5
|
-
import { generateFileUnifiedDiff } from '../../../utils/fileDiff';
|
|
6
|
-
|
|
7
|
-
interface IndividualFileDiffProps {
|
|
8
|
-
fileName: string;
|
|
9
|
-
beforeContent: string;
|
|
10
|
-
afterContent: string;
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
export const IndividualFileDiff: React.FC<
|
|
14
|
-
IndividualFileDiffProps
|
|
15
|
-
> = ({ fileName, beforeContent, afterContent }) => {
|
|
16
|
-
// Generate unified diff for this single file
|
|
17
|
-
const unifiedDiff = useMemo(() => {
|
|
18
|
-
return generateFileUnifiedDiff(
|
|
19
|
-
'temp-id',
|
|
20
|
-
fileName,
|
|
21
|
-
beforeContent,
|
|
22
|
-
afterContent,
|
|
23
|
-
);
|
|
24
|
-
}, [fileName, beforeContent, afterContent]);
|
|
25
|
-
|
|
26
|
-
// Convert to HTML using diff2html
|
|
27
|
-
const diffHtml = useMemo(() => {
|
|
28
|
-
if (!unifiedDiff) return '';
|
|
29
|
-
|
|
30
|
-
return Diff2Html.html(unifiedDiff, {
|
|
31
|
-
drawFileList: false,
|
|
32
|
-
matching: 'words',
|
|
33
|
-
diffStyle: 'word',
|
|
34
|
-
outputFormat: 'line-by-line',
|
|
35
|
-
});
|
|
36
|
-
}, [unifiedDiff]);
|
|
37
|
-
|
|
38
|
-
// Calculate simple stats
|
|
39
|
-
const stats = useMemo(() => {
|
|
40
|
-
const lines = unifiedDiff.split('\n');
|
|
41
|
-
let additions = 0;
|
|
42
|
-
let deletions = 0;
|
|
43
|
-
|
|
44
|
-
lines.forEach((line) => {
|
|
45
|
-
if (line.startsWith('+') && !line.startsWith('+++')) {
|
|
46
|
-
additions++;
|
|
47
|
-
} else if (
|
|
48
|
-
line.startsWith('-') &&
|
|
49
|
-
!line.startsWith('---')
|
|
50
|
-
) {
|
|
51
|
-
deletions++;
|
|
52
|
-
}
|
|
53
|
-
});
|
|
54
|
-
|
|
55
|
-
return { additions, deletions };
|
|
56
|
-
}, [unifiedDiff]);
|
|
57
|
-
|
|
58
|
-
if (!unifiedDiff) {
|
|
59
|
-
return null;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
return (
|
|
63
|
-
<div className="diff-view" data-file={fileName}>
|
|
64
|
-
<div className="diff-summary">
|
|
65
|
-
<div className="diff-stats">
|
|
66
|
-
<span className="files-changed">{fileName}</span>
|
|
67
|
-
{stats.additions > 0 && (
|
|
68
|
-
<span className="additions">
|
|
69
|
-
+{stats.additions}
|
|
70
|
-
</span>
|
|
71
|
-
)}
|
|
72
|
-
{stats.deletions > 0 && (
|
|
73
|
-
<span className="deletions">
|
|
74
|
-
-{stats.deletions}
|
|
75
|
-
</span>
|
|
76
|
-
)}
|
|
77
|
-
</div>
|
|
78
|
-
</div>
|
|
79
|
-
|
|
80
|
-
<div
|
|
81
|
-
className="diff-files"
|
|
82
|
-
dangerouslySetInnerHTML={{ __html: diffHtml }}
|
|
83
|
-
/>
|
|
84
|
-
</div>
|
|
85
|
-
);
|
|
86
|
-
};
|
|
@@ -1,64 +0,0 @@
|
|
|
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,145 +0,0 @@
|
|
|
1
|
-
import React, {
|
|
2
|
-
useMemo,
|
|
3
|
-
useContext,
|
|
4
|
-
forwardRef,
|
|
5
|
-
} from 'react';
|
|
6
|
-
import { timestampToDate } from '@vizhub/viz-utils';
|
|
7
|
-
import Markdown from 'react-markdown';
|
|
8
|
-
import remarkGfm from 'remark-gfm';
|
|
9
|
-
import { StreamingEvent } from '../../../types.js';
|
|
10
|
-
import { IndividualFileDiff } from './IndividualFileDiff';
|
|
11
|
-
import { VZCodeContext } from '../../VZCodeContext';
|
|
12
|
-
import { DiffView, DiffViewRef } from './DiffView';
|
|
13
|
-
import { UnifiedFilesDiff } from '../../../utils/fileDiff';
|
|
14
|
-
import { enableDiffView } from '../../featureFlags';
|
|
15
|
-
|
|
16
|
-
const DEBUG = false;
|
|
17
|
-
|
|
18
|
-
interface MessageProps {
|
|
19
|
-
id: string;
|
|
20
|
-
role: 'user' | 'assistant';
|
|
21
|
-
content?: string; // For non-streaming messages
|
|
22
|
-
timestamp: number;
|
|
23
|
-
events?: StreamingEvent[]; // For streaming messages
|
|
24
|
-
isActive?: boolean; // Is this the currently streaming message?
|
|
25
|
-
chatId?: string;
|
|
26
|
-
showAdditionalWidgets?: boolean;
|
|
27
|
-
isStreaming?: boolean;
|
|
28
|
-
diffData?: UnifiedFilesDiff;
|
|
29
|
-
}
|
|
30
|
-
|
|
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);
|
|
53
|
-
|
|
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]);
|
|
70
|
-
|
|
71
|
-
// Memoize the className string to avoid recreation
|
|
72
|
-
const messageClassName = useMemo(() => {
|
|
73
|
-
return `ai-chat-message ${role}${isStreaming ? ' streaming' : ''}`;
|
|
74
|
-
}, [role, isStreaming]);
|
|
75
|
-
|
|
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>
|
|
84
|
-
)}
|
|
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
|
-
}
|
|
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>
|
|
140
|
-
</div>
|
|
141
|
-
);
|
|
142
|
-
},
|
|
143
|
-
);
|
|
144
|
-
|
|
145
|
-
Message.displayName = 'Message';
|