vzcode 2.14.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.
Files changed (51) hide show
  1. package/dist/assets/bindings_wasm_bg-9w8E-TmY.wasm +0 -0
  2. package/dist/assets/buildWorker-Bi6wsfQk.js +695 -0
  3. package/dist/assets/{index-AX9wEDr7.js → index-C2BLPyQP.js} +138 -125
  4. package/dist/assets/{index-DLm5FQ0E.css → index-DVR90LwF.css} +1 -1
  5. package/dist/assets/worker-CmHJK_do.js +136 -0
  6. package/dist/index.html +2 -2
  7. package/dist/server/aiChatHandler/chatOperations.js +168 -35
  8. package/dist/server/aiChatHandler/index.js +11 -30
  9. package/dist/server/aiChatHandler/llmStreaming.js +105 -118
  10. package/package.json +20 -20
  11. package/src/client/AIAssist/AIAssistWidget/index.tsx +12 -1
  12. package/src/client/App/useShareDB.ts +1 -1
  13. package/src/client/CodeEditor/index.tsx +9 -10
  14. package/src/client/VZCodeContext/types.ts +3 -1
  15. package/src/client/VZCodeContext/useVZCodeState.ts +7 -5
  16. package/src/client/VZRight.tsx +10 -2
  17. package/src/client/VZSidebar/AIChat/ChatInput.tsx +1 -7
  18. package/src/client/VZSidebar/AIChat/DiffView.scss +1 -0
  19. package/src/client/VZSidebar/AIChat/DiffView.tsx +72 -29
  20. package/src/client/VZSidebar/AIChat/FileEditingIndicator.tsx +81 -0
  21. package/src/client/VZSidebar/AIChat/IndividualFileDiff.tsx +86 -0
  22. package/src/client/VZSidebar/AIChat/JumpToLatestButton.tsx +64 -0
  23. package/src/client/VZSidebar/AIChat/Message.tsx +68 -53
  24. package/src/client/VZSidebar/AIChat/MessageList.tsx +139 -118
  25. package/src/client/VZSidebar/AIChat/StreamingMessage.tsx +63 -21
  26. package/src/client/VZSidebar/AIChat/ThinkingScratchpad.tsx +4 -118
  27. package/src/client/VZSidebar/AIChat/index.tsx +62 -19
  28. package/src/client/VZSidebar/AIChat/styles.scss +159 -16
  29. package/src/client/VZSidebar/Item.tsx +2 -2
  30. package/src/client/VZSidebar/Search.tsx +13 -6
  31. package/src/client/VZSidebar/VisualEditor/VisualEditor.tsx +39 -23
  32. package/src/client/VZSidebar/VisualEditor/utils.ts +1 -1
  33. package/src/client/VZSidebar/index.tsx +12 -10
  34. package/src/client/VZSidebar/useDragAndDrop.tsx +225 -214
  35. package/src/client/featureFlags.ts +3 -0
  36. package/src/client/hooks/useAutoScroll.ts +329 -0
  37. package/src/client/tabsSearchParameters.ts +1 -17
  38. package/src/client/useFileCRUD.ts +5 -2
  39. package/src/client/useKeyboardShortcuts.ts +7 -0
  40. package/src/client/useOpenDirectories.ts +2 -1
  41. package/src/client/usePrettier/index.ts +1 -1
  42. package/src/client/useURLSync.ts +1 -0
  43. package/src/client/utils/scrollUtils.ts +170 -0
  44. package/src/client/vzReducer/searchReducer.ts +6 -3
  45. package/src/server/aiChatHandler/chatOperations.ts +224 -43
  46. package/src/server/aiChatHandler/index.ts +8 -48
  47. package/src/server/aiChatHandler/llmStreaming.ts +149 -170
  48. package/src/types.ts +81 -1
  49. package/dist/assets/bindings_wasm_bg-CH5ekNIA.wasm +0 -0
  50. package/dist/assets/buildWorker-D4WATavZ.js +0 -682
  51. package/dist/assets/worker-CHvi_bXc.js +0 -136
@@ -1,54 +1,30 @@
1
- import fs from 'fs';
2
1
  import OpenAI from 'openai';
3
2
  import {
4
3
  parseMarkdownFiles,
5
4
  StreamingMarkdownParser,
6
5
  } from 'llm-code-format';
7
6
  import { mergeFileChanges } from 'editcodewithai';
8
- import { VizChatId, VizContent } from '@vizhub/viz-types';
7
+ import { VizChatId } from '@vizhub/viz-types';
9
8
  import { generateRunId } from '@vizhub/viz-utils';
10
9
  import {
11
- updateAIStatus,
12
- createAIMessage,
13
- updateAIMessageContent,
14
- finalizeAIMessage,
15
- ensureFileExists,
16
- clearFileContent,
17
- appendLineToFile,
18
10
  updateFiles,
19
11
  updateAIScratchpad,
12
+ createStreamingAIMessage,
13
+ addStreamingEvent,
14
+ updateStreamingStatus,
15
+ finalizeStreamingMessage,
20
16
  } from './chatOperations.js';
21
17
  import { diff } from '../../ot.js';
22
- import { ShareDBDoc } from '../../types.js';
18
+ import {
19
+ ShareDBDoc,
20
+ ExtendedVizContent,
21
+ } from '../../types.js';
23
22
 
24
23
  const DEBUG = false;
25
24
 
26
25
  // Useful for testing/debugging the streaming behavior
27
26
  const slowMode = false;
28
27
 
29
- // Throttle the streaming updates, so that we don't
30
- // overwhelm the ShareDB server with too many updates.
31
- // It happened actually, before adding this.
32
- // MongoDB VizHub server got in fact overloaded with
33
- // too many updates from the AI streaming response, with
34
- // warning: "Replication Oplog Window has gone below 1 hour"
35
- const THROTTLE_INTERVAL_MS = 100;
36
-
37
- // Feature flag to enable/disable streaming editing.
38
- // * If `true`, the AI streaming response will be used to
39
- // edit files in real-time by submitting ShareDB ops.
40
- // * If `false`, the updates to code files will be applied
41
- // only after the AI has finished generating the entire response.
42
- //
43
- // Current status: there's a tricky bug with the streaming
44
- // where the AI edits sometimes don't apply correctly in CodeMirror.
45
- // It seems that sometimes the op that clears the file content
46
- // is not applied correctly in the front end, leading to
47
- // a situation where the AI streaming edits are concatenated into the middle
48
- // of the file instead of replacing it.
49
- // See https://github.com/codemirror/codemirror.next/issues/1234
50
- const enableStreamingEditing = false;
51
-
52
28
  /**
53
29
  * Creates and configures the LLM function for streaming with reasoning tokens
54
30
  */
@@ -62,7 +38,7 @@ export const createLLMFunction = ({
62
38
  model,
63
39
  aiRequestOptions,
64
40
  }: {
65
- shareDBDoc: ShareDBDoc<VizContent>;
41
+ shareDBDoc: ShareDBDoc<ExtendedVizContent>;
66
42
  chatId: VizChatId;
67
43
  enableReasoningTokens?: boolean;
68
44
  model?: string;
@@ -83,70 +59,67 @@ export const createLLMFunction = ({
83
59
 
84
60
  let fullContent = '';
85
61
  let generationId = '';
86
- let currentEditingFileId = null;
87
62
  let currentEditingFileName = null;
88
-
89
- // Create initial AI message for streaming
90
- const aiMessageId = createAIMessage(shareDBDoc, chatId);
91
-
92
- // --- throttle wrapper ---
93
- function makeThrottledUpdater() {
94
- let lastCall = 0;
95
- let latestContent = '';
96
- let timer: NodeJS.Timeout | null = null;
97
-
98
- function invoke() {
99
- updateAIMessageContent(
100
- shareDBDoc,
101
- chatId,
102
- aiMessageId,
103
- latestContent,
104
- );
105
- lastCall = Date.now();
63
+ let accumulatedTextChunk = '';
64
+ let currentFileContent = '';
65
+
66
+ // Create streaming AI message
67
+ createStreamingAIMessage(shareDBDoc, chatId);
68
+
69
+ // Set initial content generation status
70
+ updateStreamingStatus(
71
+ shareDBDoc,
72
+ chatId,
73
+ 'Formulating a plan...',
74
+ );
75
+
76
+ // Helper to get original file content
77
+ const getOriginalFileContent = (
78
+ fileName: string,
79
+ ): string => {
80
+ const files = shareDBDoc.data.files;
81
+ for (const file of Object.values(files)) {
82
+ if ((file as any).name === fileName) {
83
+ return (file as any).text || '';
84
+ }
106
85
  }
86
+ return '';
87
+ };
107
88
 
108
- const fn = (content: string) => {
109
- latestContent = content;
110
- const now = Date.now();
111
-
112
- if (now - lastCall >= THROTTLE_INTERVAL_MS) {
113
- // safe to call immediately
114
- invoke();
115
- } else if (!timer) {
116
- // schedule for later
117
- timer = setTimeout(
118
- () => {
119
- timer = null;
120
- invoke();
121
- },
122
- THROTTLE_INTERVAL_MS - (now - lastCall),
89
+ // Helper to emit text chunk when accumulated
90
+ const emitTextChunk = async () => {
91
+ if (accumulatedTextChunk.trim()) {
92
+ DEBUG &&
93
+ console.log(
94
+ 'LLMStreaming: Emitting text chunk:',
95
+ accumulatedTextChunk.substring(0, 100) + '...',
123
96
  );
124
- }
125
- };
126
-
127
- // expose a flush() helper to force an immediate write
128
- fn.flush = () => {
129
- if (timer) {
130
- clearTimeout(timer);
131
- timer = null;
132
- }
133
- invoke();
134
- };
135
-
136
- return fn;
137
- }
97
+ await addStreamingEvent(shareDBDoc, chatId, {
98
+ type: 'text_chunk',
99
+ content: accumulatedTextChunk,
100
+ timestamp: Date.now(),
101
+ });
102
+ accumulatedTextChunk = '';
103
+ }
104
+ };
138
105
 
139
- const throttledUpdateAIMessageContent =
140
- makeThrottledUpdater();
141
-
142
- // Function to report file edited
143
- // This is called when the AI has finished editing a file
144
- // and we want to update the message content with the file name.
145
- const reportFileEdited = () => {
146
- if (currentEditingFileName) {
147
- fullContent += ` * Edited ${currentEditingFileName}\n`;
148
- throttledUpdateAIMessageContent(fullContent);
149
- currentEditingFileName = null;
106
+ // Helper to complete file editing
107
+ const completeFileEditing = async (
108
+ fileName: string,
109
+ ) => {
110
+ if (fileName && currentFileContent) {
111
+ DEBUG &&
112
+ console.log(
113
+ `LLMStreaming: Completing file editing for ${fileName}`,
114
+ );
115
+ await addStreamingEvent(shareDBDoc, chatId, {
116
+ type: 'file_complete',
117
+ fileName,
118
+ beforeContent: getOriginalFileContent(fileName),
119
+ afterContent: currentFileContent,
120
+ timestamp: Date.now(),
121
+ });
122
+ currentFileContent = '';
150
123
  }
151
124
  };
152
125
 
@@ -158,51 +131,57 @@ export const createLLMFunction = ({
158
131
  ) => {
159
132
  DEBUG &&
160
133
  console.log(
161
- `File changed to: ${fileName} (${format})`,
134
+ `LLMStreaming: File changed to: ${fileName} (${format})`,
162
135
  );
163
136
 
164
- // Find existing file or create new one
165
- currentEditingFileId = ensureFileExists(
166
- shareDBDoc,
167
- fileName,
168
- );
137
+ // Emit any accumulated text chunk first
138
+ await emitTextChunk();
139
+
140
+ // Complete previous file if any
141
+ if (currentEditingFileName) {
142
+ await completeFileEditing(currentEditingFileName);
143
+ }
169
144
 
170
- reportFileEdited();
145
+ // Start new file
171
146
  currentEditingFileName = fileName;
147
+ currentFileContent = '';
172
148
 
173
- // Clear the file content to start fresh
174
- // (AI will regenerate the entire file content)
175
- clearFileContent(shareDBDoc, currentEditingFileId);
149
+ // Emit file start event
150
+ await addStreamingEvent(shareDBDoc, chatId, {
151
+ type: 'file_start',
152
+ fileName,
153
+ timestamp: Date.now(),
154
+ });
176
155
 
177
- // Update AI status
178
- updateAIStatus(
156
+ // Update status
157
+ updateStreamingStatus(
179
158
  shareDBDoc,
180
159
  chatId,
181
- 'Editing ' + fileName,
160
+ `Editing ${fileName}...`,
182
161
  );
183
162
  },
184
163
  onCodeLine: async (line: string) => {
185
164
  DEBUG && console.log(`Code line: ${line}`);
186
-
187
- // If streaming is enabled, we apply the line immediately
188
- if (currentEditingFileId) {
189
- // Apply OT operation for this line immediately
190
- appendLineToFile(
191
- shareDBDoc,
192
- currentEditingFileId,
193
- line,
194
- );
195
- }
165
+ // Accumulate code content for the current file
166
+ currentFileContent += line + '\n';
196
167
  },
197
168
  onNonCodeLine: async (line: string) => {
198
- // We want to report a file edited only if the line is not empty,
199
- // because sometimes the LLMs leave a newline between the file name
200
- // declaration and th
169
+ DEBUG && console.log(`Non-code line: ${line}`);
170
+ // Accumulate non-code content as text chunk
201
171
  if (line.trim() !== '') {
202
- reportFileEdited();
172
+ accumulatedTextChunk += line + '\n';
173
+
174
+ // Update status for subsequent non-code chunks
175
+ if (firstNonCodeChunkProcessed) {
176
+ updateStreamingStatus(
177
+ shareDBDoc,
178
+ chatId,
179
+ 'Describing changes...',
180
+ );
181
+ } else {
182
+ firstNonCodeChunkProcessed = true;
183
+ }
203
184
  }
204
- fullContent += line + '\n';
205
- throttledUpdateAIMessageContent(fullContent);
206
185
  },
207
186
  };
208
187
 
@@ -240,6 +219,7 @@ export const createLLMFunction = ({
240
219
 
241
220
  let reasoningStarted = false;
242
221
  let contentStarted = false;
222
+ let firstNonCodeChunkProcessed = false;
243
223
 
244
224
  for await (const chunk of stream) {
245
225
  if (slowMode) {
@@ -253,7 +233,11 @@ export const createLLMFunction = ({
253
233
  // Handle reasoning tokens (thinking) - only if enabled
254
234
  if (!reasoningStarted) {
255
235
  reasoningStarted = true;
256
- updateAIStatus(shareDBDoc, chatId, 'Thinking...');
236
+ updateStreamingStatus(
237
+ shareDBDoc,
238
+ chatId,
239
+ 'Thinking...',
240
+ );
257
241
  }
258
242
  reasoningContent += delta.reasoning;
259
243
  updateAIScratchpad(
@@ -263,26 +247,25 @@ export const createLLMFunction = ({
263
247
  );
264
248
  } else if (delta?.content) {
265
249
  // Handle regular content tokens
266
- if (reasoningStarted && !contentStarted) {
267
- // Clear reasoning when content starts
250
+ if (!contentStarted) {
268
251
  contentStarted = true;
269
- updateAIScratchpad(shareDBDoc, chatId, '');
270
- updateAIStatus(
271
- shareDBDoc,
272
- chatId,
273
- 'Generating response...',
274
- );
252
+ if (reasoningStarted) {
253
+ // Clear reasoning when content starts
254
+ updateAIScratchpad(shareDBDoc, chatId, '');
255
+ }
256
+ // // Set initial content generation status
257
+ // updateStreamingStatus(
258
+ // shareDBDoc,
259
+ // chatId,
260
+ // 'Formulating a plan...',
261
+ // );
275
262
  }
276
263
 
277
264
  const chunkContent = delta.content;
278
265
  chunks.push(chunkContent);
279
266
 
280
- if (enableStreamingEditing) {
281
- await parser.processChunk(chunkContent);
282
- } else {
283
- fullContent += chunkContent;
284
- throttledUpdateAIMessageContent(fullContent);
285
- }
267
+ await parser.processChunk(chunkContent);
268
+ fullContent += chunkContent;
286
269
  } else if (chunk.usage) {
287
270
  // Handle usage information
288
271
  DEBUG && console.log('Usage:', chunk.usage);
@@ -293,31 +276,27 @@ export const createLLMFunction = ({
293
276
  }
294
277
  }
295
278
  await parser.flushRemaining();
296
- reportFileEdited();
297
-
298
- // Final cleanup - clear scratchpad and set final status
299
- updateAIScratchpad(shareDBDoc, chatId, '');
300
- updateAIStatus(shareDBDoc, chatId, 'Done editing.');
301
- throttledUpdateAIMessageContent(fullContent);
302
-
303
- // Flush to ensure the final content is written immediately
304
- throttledUpdateAIMessageContent.flush();
305
-
306
- // Finalize the AI message by clearing temporary fields
307
- finalizeAIMessage(shareDBDoc, chatId);
308
-
309
- // If streaming editing is not enabled, we need to
310
- // apply all the edits at once
311
- if (!enableStreamingEditing) {
312
- updateFiles(
313
- shareDBDoc,
314
- mergeFileChanges(
315
- shareDBDoc.data.files,
316
- parseMarkdownFiles(fullContent, 'bold').files,
317
- ),
318
- );
279
+
280
+ // Emit any remaining text chunk
281
+ await emitTextChunk();
282
+
283
+ // Complete final file if any
284
+ if (currentEditingFileName) {
285
+ await completeFileEditing(currentEditingFileName);
319
286
  }
320
287
 
288
+ // Apply all the edits at once
289
+ updateFiles(
290
+ shareDBDoc,
291
+ mergeFileChanges(
292
+ shareDBDoc.data.files,
293
+ parseMarkdownFiles(fullContent, 'bold').files,
294
+ ),
295
+ );
296
+
297
+ // Finalize streaming message
298
+ await finalizeStreamingMessage(shareDBDoc, chatId);
299
+
321
300
  // Generate a new runId to trigger a run when AI finishes editing
322
301
  // This will trigger a re-run without hot reloading
323
302
  const newRunId = generateRunId();
@@ -341,16 +320,16 @@ export const createLLMFunction = ({
341
320
  });
342
321
 
343
322
  // Write chunks file for debugging
344
- if (DEBUG) {
345
- const chunksFileJSONpath = `./ai-chunks-${chatId}.json`;
346
- fs.writeFileSync(
347
- chunksFileJSONpath,
348
- JSON.stringify(chunks, null, 2),
349
- );
350
- console.log(
351
- `AI chunks written to ${chunksFileJSONpath}`,
352
- );
353
- }
323
+ // if (DEBUG) {
324
+ // const chunksFileJSONpath = `./ai-chunks-${chatId}.json`;
325
+ // fs.writeFileSync(
326
+ // chunksFileJSONpath,
327
+ // JSON.stringify(chunks, null, 2),
328
+ // );
329
+ // console.log(
330
+ // `AI chunks written to ${chunksFileJSONpath}`,
331
+ // );
332
+ // }
354
333
 
355
334
  return {
356
335
  content: fullContent,
package/src/types.ts CHANGED
@@ -1,4 +1,10 @@
1
- import { VizContent, VizFileId } from '@vizhub/viz-types';
1
+ import {
2
+ VizContent,
3
+ VizFileId,
4
+ VizChat,
5
+ VizChatMessage,
6
+ VizChatId,
7
+ } from '@vizhub/viz-types';
2
8
 
3
9
  // ItemId
4
10
  // * A unique ID for an item in the sidebar.
@@ -215,3 +221,77 @@ export type VisualEditorConfigEntry =
215
221
  options: string[];
216
222
  }
217
223
  | { type: 'color'; property: string; label: string };
224
+
225
+ // ============================================================================
226
+ // AI Chat Streaming Types
227
+ // ============================================================================
228
+
229
+ // Streaming Events for Progressive AI Chat
230
+ export type StreamingEvent =
231
+ | {
232
+ type: 'text_chunk';
233
+ content: string;
234
+ timestamp: number;
235
+ }
236
+ | {
237
+ type: 'file_start';
238
+ fileName: string;
239
+ timestamp: number;
240
+ }
241
+ | {
242
+ type: 'file_complete';
243
+ fileName: string;
244
+ beforeContent: string;
245
+ afterContent: string;
246
+ timestamp: number;
247
+ }
248
+ | {
249
+ type: 'status_update';
250
+ status: string;
251
+ timestamp: number;
252
+ };
253
+
254
+ // Extended VizChat with streaming capabilities
255
+ export interface ExtendedVizChat extends VizChat {
256
+ streamingEvents?: StreamingEvent[];
257
+ currentStatus?: string;
258
+ isStreaming?: boolean;
259
+ }
260
+
261
+ // Extended VizChatMessage with progressive rendering support
262
+ export interface ExtendedVizChatMessage
263
+ extends VizChatMessage {
264
+ streamingEvents?: StreamingEvent[];
265
+ isProgressive?: boolean; // Flag to indicate this message uses progressive rendering
266
+ isComplete?: boolean; // Flag to indicate if streaming is complete
267
+ }
268
+
269
+ // Extended VizContent that uses our extended chat types
270
+ export interface ExtendedVizContent
271
+ extends Omit<VizContent, 'chats'> {
272
+ chats?: { [chatId: VizChatId]: ExtendedVizChat };
273
+ }
274
+
275
+ // File diff data for individual files (not the combined diff)
276
+ export interface IndividualFileDiff {
277
+ fileName: string;
278
+ beforeContent: string;
279
+ afterContent: string;
280
+ unifiedDiff?: string; // Generated on demand
281
+ timestamp: number;
282
+ }
283
+
284
+ // Streaming Chat Message Format (simplified)
285
+ export interface StreamingChatMessage {
286
+ id: string;
287
+ role: 'user' | 'assistant';
288
+ timestamp: number;
289
+
290
+ // For user messages
291
+ content?: string;
292
+
293
+ // For assistant messages (streaming)
294
+ events?: StreamingEvent[];
295
+ currentStatus?: string;
296
+ isComplete?: boolean;
297
+ }