vzcode 1.41.0 → 1.43.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.
@@ -4,6 +4,10 @@ import {
4
4
  } from 'editcodewithai';
5
5
  import { formatMarkdownFiles } from 'llm-code-format';
6
6
 
7
+ // Dev flag for waiting 1 second before starting the LLM function.
8
+ // Useful for debugging and testing purposes, e.g. checking the typing indicator.
9
+ const delayStart = false;
10
+
7
11
  /**
8
12
  * Performs AI editing operations using streaming with incremental OT operations
9
13
  */
@@ -25,16 +29,15 @@ export const performAIEditing = async ({
25
29
  editFormat: 'whole',
26
30
  });
27
31
 
32
+ if (delayStart) {
33
+ await new Promise((resolve) =>
34
+ setTimeout(resolve, 1000),
35
+ );
36
+ }
37
+
28
38
  // Call the LLM function which will handle streaming and incremental file updates
29
39
  const result = await llmFunction(fullPrompt);
30
40
 
31
- // Clear the scratchpad and update status
32
- // clearAIScratchpadAndStatus(
33
- // shareDBDoc,
34
- // chatId,
35
- // 'Done editing with AI.',
36
- // );
37
-
38
41
  runCode();
39
42
 
40
43
  return {
@@ -1,6 +1,13 @@
1
1
  import { dateToTimestamp } from '@vizhub/viz-utils';
2
- import { diff } from '../../client/diff.js';
3
2
  import { randomId } from '../../randomId.js';
3
+ import { ShareDBDoc } from '../../types.js';
4
+ import { diff } from '../../ot.js';
5
+ import {
6
+ VizChatId,
7
+ VizContent,
8
+ VizFileId,
9
+ VizFiles,
10
+ } from '@vizhub/viz-types';
4
11
 
5
12
  /**
6
13
  * Ensures the chats object exists in the ShareDB document
@@ -18,7 +25,10 @@ export const ensureChatsExist = (shareDBDoc) => {
18
25
  /**
19
26
  * Ensures a specific chat exists in the ShareDB document
20
27
  */
21
- export const ensureChatExists = (shareDBDoc, chatId) => {
28
+ export const ensureChatExists = (
29
+ shareDBDoc: ShareDBDoc<VizContent>,
30
+ chatId: VizChatId,
31
+ ) => {
22
32
  if (!shareDBDoc.data.chats[chatId]) {
23
33
  const op = diff(shareDBDoc.data, {
24
34
  ...shareDBDoc.data,
@@ -40,9 +50,9 @@ export const ensureChatExists = (shareDBDoc, chatId) => {
40
50
  * Adds a user message to the chat
41
51
  */
42
52
  export const addUserMessage = (
43
- shareDBDoc,
44
- chatId,
45
- content,
53
+ shareDBDoc: ShareDBDoc<VizContent>,
54
+ chatId: VizChatId,
55
+ content: string,
46
56
  ) => {
47
57
  const userMessage = {
48
58
  id: `user-${Date.now()}`,
@@ -74,9 +84,9 @@ export const addUserMessage = (
74
84
  * Updates AI status in the chat
75
85
  */
76
86
  export const updateAIStatus = (
77
- shareDBDoc,
78
- chatId,
79
- status,
87
+ shareDBDoc: ShareDBDoc<VizContent>,
88
+ chatId: VizChatId,
89
+ status: string,
80
90
  ) => {
81
91
  const op = diff(shareDBDoc.data, {
82
92
  ...shareDBDoc.data,
@@ -95,9 +105,9 @@ export const updateAIStatus = (
95
105
  * Updates AI scratchpad content
96
106
  */
97
107
  export const updateAIScratchpad = (
98
- shareDBDoc,
99
- chatId,
100
- content,
108
+ shareDBDoc: ShareDBDoc<VizContent>,
109
+ chatId: VizChatId,
110
+ content: string,
101
111
  ) => {
102
112
  const op = diff(shareDBDoc.data, {
103
113
  ...shareDBDoc.data,
@@ -122,9 +132,9 @@ export const updateAIScratchpad = (
122
132
  * Clears AI scratchpad and updates status
123
133
  */
124
134
  export const clearAIScratchpadAndStatus = (
125
- shareDBDoc,
126
- chatId,
127
- status,
135
+ shareDBDoc: ShareDBDoc<VizContent>,
136
+ chatId: VizChatId,
137
+ status: string,
128
138
  ) => {
129
139
  const op = diff(shareDBDoc.data, {
130
140
  ...shareDBDoc.data,
@@ -141,12 +151,108 @@ export const clearAIScratchpadAndStatus = (
141
151
  };
142
152
 
143
153
  /**
144
- * Adds an AI response message to the chat
154
+ * Creates an initial empty AI message for streaming
155
+ */
156
+ export const createAIMessage = (
157
+ shareDBDoc: ShareDBDoc<VizContent>,
158
+ chatId: VizChatId,
159
+ ) => {
160
+ const aiMessage = {
161
+ id: `assistant-${Date.now()}`,
162
+ role: 'assistant',
163
+ content: '',
164
+ timestamp: dateToTimestamp(new Date()),
165
+ };
166
+
167
+ const messageOp = diff(shareDBDoc.data, {
168
+ ...shareDBDoc.data,
169
+ chats: {
170
+ ...shareDBDoc.data.chats,
171
+ [chatId]: {
172
+ ...shareDBDoc.data.chats[chatId],
173
+ messages: [
174
+ ...shareDBDoc.data.chats[chatId].messages,
175
+ aiMessage,
176
+ ],
177
+ updatedAt: dateToTimestamp(new Date()),
178
+ },
179
+ },
180
+ });
181
+ shareDBDoc.submitOp(messageOp);
182
+
183
+ return aiMessage.id;
184
+ };
185
+
186
+ /**
187
+ * Updates the content of an AI message during streaming
188
+ */
189
+ export const updateAIMessageContent = (
190
+ shareDBDoc: ShareDBDoc<VizContent>,
191
+ chatId: VizChatId,
192
+ messageId: string,
193
+ content: string,
194
+ ) => {
195
+ const chat = shareDBDoc.data.chats[chatId];
196
+ const messageIndex = chat.messages.findIndex(
197
+ (msg) => msg.id === messageId,
198
+ );
199
+
200
+ if (messageIndex === -1) {
201
+ console.warn(
202
+ `AI message with id ${messageId} not found`,
203
+ );
204
+ return;
205
+ }
206
+
207
+ const updatedMessages = [...chat.messages];
208
+ updatedMessages[messageIndex] = {
209
+ ...updatedMessages[messageIndex],
210
+ content,
211
+ };
212
+
213
+ const messageOp = diff(shareDBDoc.data, {
214
+ ...shareDBDoc.data,
215
+ chats: {
216
+ ...shareDBDoc.data.chats,
217
+ [chatId]: {
218
+ ...chat,
219
+ messages: updatedMessages,
220
+ updatedAt: dateToTimestamp(new Date()),
221
+ },
222
+ },
223
+ });
224
+ shareDBDoc.submitOp(messageOp);
225
+ };
226
+
227
+ /**
228
+ * Finalizes an AI message by clearing temporary fields
229
+ */
230
+ export const finalizeAIMessage = (
231
+ shareDBDoc: ShareDBDoc<VizContent>,
232
+ chatId: VizChatId,
233
+ ) => {
234
+ const op = diff(shareDBDoc.data, {
235
+ ...shareDBDoc.data,
236
+ chats: {
237
+ ...shareDBDoc.data.chats,
238
+ [chatId]: {
239
+ ...shareDBDoc.data.chats[chatId],
240
+ aiScratchpad: undefined,
241
+ aiStatus: undefined,
242
+ updatedAt: dateToTimestamp(new Date()),
243
+ },
244
+ },
245
+ });
246
+ shareDBDoc.submitOp(op);
247
+ };
248
+
249
+ /**
250
+ * Adds an AI response message to the chat (legacy function, kept for compatibility)
145
251
  */
146
252
  export const addAIMessage = (
147
- shareDBDoc,
148
- chatId,
149
- content,
253
+ shareDBDoc: ShareDBDoc<VizContent>,
254
+ chatId: VizChatId,
255
+ content?: string,
150
256
  ) => {
151
257
  const aiResponse = {
152
258
  id: Date.now() + 1,
@@ -178,7 +284,10 @@ export const addAIMessage = (
178
284
  /**
179
285
  * Updates files in the ShareDB document
180
286
  */
181
- export const updateFiles = (shareDBDoc, files) => {
287
+ export const updateFiles = (
288
+ shareDBDoc: ShareDBDoc<VizContent>,
289
+ files: VizFiles,
290
+ ) => {
182
291
  const filesOp = diff(shareDBDoc.data, {
183
292
  ...shareDBDoc.data,
184
293
  files,
@@ -186,30 +295,6 @@ export const updateFiles = (shareDBDoc, files) => {
186
295
  shareDBDoc.submitOp(filesOp);
187
296
  };
188
297
 
189
- /**
190
- * Sets isInteracting flag
191
- */
192
- export const setIsInteracting = (
193
- shareDBDoc,
194
- isInteracting,
195
- ) => {
196
- // Only generate an operation if the value is actually changing
197
- const currentIsInteracting =
198
- shareDBDoc.data.isInteracting;
199
-
200
- if (currentIsInteracting === isInteracting) {
201
- return;
202
- }
203
-
204
- const newState = {
205
- ...shareDBDoc.data,
206
- isInteracting: isInteracting,
207
- };
208
-
209
- const interactingOp = diff(shareDBDoc.data, newState);
210
- shareDBDoc.submitOp(interactingOp);
211
- };
212
-
213
298
  /**
214
299
  * Finds a file ID by searching for a matching file name
215
300
  */
@@ -233,7 +318,10 @@ export const resolveFileId = (
233
318
  /**
234
319
  * Creates a new file with a random ID
235
320
  */
236
- export const createNewFile = (shareDBDoc, fileName) => {
321
+ export const createNewFile = (
322
+ shareDBDoc: ShareDBDoc<VizContent>,
323
+ fileName: string,
324
+ ) => {
237
325
  // Generate a new random file ID
238
326
  const newFileId = randomId();
239
327
 
@@ -270,7 +358,10 @@ export const ensureFileExists = (shareDBDoc, fileName) => {
270
358
  /**
271
359
  * Clears the content of a file
272
360
  */
273
- export const clearFileContent = (shareDBDoc, fileId) => {
361
+ export const clearFileContent = (
362
+ shareDBDoc: ShareDBDoc<VizContent>,
363
+ fileId: VizFileId,
364
+ ) => {
274
365
  const currentFile = shareDBDoc.data.files[fileId];
275
366
 
276
367
  if (currentFile && currentFile.text) {
@@ -295,29 +386,24 @@ export const clearFileContent = (shareDBDoc, fileId) => {
295
386
  * Appends a line to a file using OT operations
296
387
  */
297
388
  export const appendLineToFile = (
298
- shareDBDoc,
299
- fileId,
300
- line,
389
+ shareDBDoc: ShareDBDoc<VizContent>,
390
+ fileId: VizFileId,
391
+ line: string,
301
392
  ) => {
302
393
  const currentFile = shareDBDoc.data.files[fileId];
303
394
  const currentContent = currentFile?.text || '';
304
395
  const newContent = currentContent + line + '\n';
305
396
 
306
- // Create the new file state
307
- const newFileState = {
308
- ...currentFile,
309
- text: newContent,
310
- };
311
-
312
397
  const newDocState = {
313
398
  ...shareDBDoc.data,
314
399
  files: {
315
400
  ...shareDBDoc.data.files,
316
- [fileId]: newFileState,
401
+ [fileId]: {
402
+ ...currentFile,
403
+ text: newContent,
404
+ },
317
405
  },
318
406
  };
319
407
 
320
- // Generate OT operation using the diff utility
321
- const op = diff(shareDBDoc.data, newDocState);
322
- shareDBDoc.submitOp(op);
408
+ shareDBDoc.submitOp(diff(shareDBDoc.data, newDocState));
323
409
  };
@@ -1,5 +1,5 @@
1
1
  import { dateToTimestamp } from '@vizhub/viz-utils';
2
- import { diff } from '../../client/diff.js';
2
+ import { diff } from '../../ot.js';
3
3
 
4
4
  /**
5
5
  * Handles errors by adding an error message to the chat and clearing AI state
@@ -10,6 +10,7 @@ import { handleError } from './errorHandling.js';
10
10
  import { createRunCodeFunction } from '../../runCode.js';
11
11
  import { ShareDBDoc } from '../../types.js';
12
12
  import { VizContent } from '@vizhub/viz-types';
13
+ import { createSubmitOperation } from '../../submitOperation.js';
13
14
 
14
15
  const DEBUG = false;
15
16
 
@@ -32,6 +33,8 @@ export const handleAIChatMessage =
32
33
  content,
33
34
  'chatId:',
34
35
  chatId,
36
+ 'shareDBDoc:',
37
+ shareDBDoc,
35
38
  );
36
39
  }
37
40
 
@@ -55,8 +58,11 @@ export const handleAIChatMessage =
55
58
  chatId,
56
59
  });
57
60
 
58
- // Create server-side runCode function using shared module
59
- const runCode = createRunCodeFunction(shareDBDoc);
61
+ // Create server-side runCode function using shareDBDoc
62
+ const submitOperation =
63
+ createSubmitOperation(shareDBDoc);
64
+ const runCode =
65
+ createRunCodeFunction(submitOperation);
60
66
 
61
67
  // Perform AI editing
62
68
  const editResult = await performAIEditing({
@@ -1,16 +1,38 @@
1
- import { StreamingMarkdownParser } from 'llm-code-format';
1
+ import {
2
+ parseMarkdownFiles,
3
+ StreamingMarkdownParser,
4
+ } from 'llm-code-format';
2
5
  import { ChatOpenAI } from '@langchain/openai';
3
6
  import fs from 'fs';
4
7
  import {
5
8
  updateAIStatus,
6
- updateAIScratchpad,
9
+ createAIMessage,
10
+ updateAIMessageContent,
11
+ finalizeAIMessage,
7
12
  ensureFileExists,
8
13
  clearFileContent,
9
14
  appendLineToFile,
15
+ updateFiles,
10
16
  } from './chatOperations.js';
17
+ import { mergeFileChanges } from 'editcodewithai';
11
18
 
12
19
  const DEBUG = false;
13
20
 
21
+ // Feature flag to enable/disable streaming editing.
22
+ // * If `true`, the AI streaming response will be used to
23
+ // edit files in real-time by submitting ShareDB ops.
24
+ // * If `false`, the updates to code files will be applied
25
+ // only after the AI has finished generating the entire response.
26
+ //
27
+ // Current status: there's a tricky bug with the streaming
28
+ // where the AI edits sometimes don't apply correctly in CodeMirror.
29
+ // It seems that sometimes the op that clears the file content
30
+ // is not applied correctly in the front end, leading to
31
+ // a situation where the AI streaming edits are concatenated into the middle
32
+ // of the file instead of replacing it.
33
+ // See https://github.com/codemirror/codemirror.next/issues/1234
34
+ const enableStreamingEditing = false;
35
+
14
36
  /**
15
37
  * Creates and configures the LLM function for streaming
16
38
  */
@@ -19,23 +41,8 @@ export const createLLMFunction = ({
19
41
  createVizBotLocalPresence,
20
42
  chatId,
21
43
  }) => {
22
- return async (fullPrompt) => {
44
+ return async (fullPrompt: string) => {
23
45
  const localPresence = createVizBotLocalPresence();
24
- // Submit initial presence for VizBot
25
- const vizBotPresence = {
26
- username: 'VizBot',
27
- start: ['files'], // Indicate VizBot is working on files
28
- end: ['files'],
29
- };
30
-
31
- localPresence.submit(vizBotPresence, (error) => {
32
- if (error) {
33
- console.warn(
34
- 'VizBot presence submission error:',
35
- error,
36
- );
37
- }
38
- });
39
46
  const chatModel = new ChatOpenAI({
40
47
  modelName:
41
48
  process.env.VIZHUB_EDIT_WITH_AI_MODEL_NAME ||
@@ -56,13 +63,21 @@ export const createLLMFunction = ({
56
63
  let currentEditingFileId = null;
57
64
  let currentEditingFileName = null;
58
65
 
66
+ // Create initial AI message for streaming
67
+ const aiMessageId = createAIMessage(shareDBDoc, chatId);
68
+
59
69
  // Function to report file edited
60
70
  // This is called when the AI has finished editing a file
61
- // and we want to update the scratchpad with the file name.
71
+ // and we want to update the message content with the file name.
62
72
  const reportFileEdited = () => {
63
73
  if (currentEditingFileName) {
64
74
  fullContent += ` * Edited ${currentEditingFileName}\n`;
65
- updateAIScratchpad(shareDBDoc, chatId, fullContent);
75
+ updateAIMessageContent(
76
+ shareDBDoc,
77
+ chatId,
78
+ aiMessageId,
79
+ fullContent,
80
+ );
66
81
  currentEditingFileName = null;
67
82
  }
68
83
  };
@@ -91,22 +106,6 @@ export const createLLMFunction = ({
91
106
  // (AI will regenerate the entire file content)
92
107
  clearFileContent(shareDBDoc, currentEditingFileId);
93
108
 
94
- // Update VizBot presence to show it's editing this specific file
95
- const filePresence = {
96
- username: 'VizBot',
97
- start: ['files', currentEditingFileId, 'text', 0],
98
- end: ['files', currentEditingFileId, 'text', 0],
99
- };
100
-
101
- localPresence.submit(filePresence, (error) => {
102
- if (error) {
103
- console.warn(
104
- 'VizBot file presence submission error:',
105
- error,
106
- );
107
- }
108
- });
109
-
110
109
  // Update AI status
111
110
  updateAIStatus(
112
111
  shareDBDoc,
@@ -117,6 +116,7 @@ export const createLLMFunction = ({
117
116
  onCodeLine: async (line: string) => {
118
117
  DEBUG && console.log(`Code line: ${line}`);
119
118
 
119
+ // If streaming is enabled, we apply the line immediately
120
120
  if (currentEditingFileId) {
121
121
  // Apply OT operation for this line immediately
122
122
  appendLineToFile(
@@ -165,7 +165,12 @@ export const createLLMFunction = ({
165
165
  reportFileEdited();
166
166
  }
167
167
  fullContent += line + '\n';
168
- updateAIScratchpad(shareDBDoc, chatId, fullContent);
168
+ updateAIMessageContent(
169
+ shareDBDoc,
170
+ chatId,
171
+ aiMessageId,
172
+ fullContent,
173
+ );
169
174
  },
170
175
  };
171
176
 
@@ -176,10 +181,21 @@ export const createLLMFunction = ({
176
181
  // Stream the response
177
182
  const stream = await chatModel.stream(fullPrompt);
178
183
  for await (const chunk of stream) {
179
- if (chunk.content) {
184
+ if (chunk && chunk.content) {
180
185
  const chunkContent = String(chunk.content);
181
186
  chunks.push(chunkContent);
182
- await parser.processChunk(chunkContent);
187
+
188
+ if (enableStreamingEditing) {
189
+ await parser.processChunk(chunkContent);
190
+ } else {
191
+ fullContent += chunkContent;
192
+ updateAIMessageContent(
193
+ shareDBDoc,
194
+ chatId,
195
+ aiMessageId,
196
+ fullContent,
197
+ );
198
+ }
183
199
  }
184
200
 
185
201
  if (!generationId && chunk.lc_kwargs?.id) {
@@ -189,7 +205,15 @@ export const createLLMFunction = ({
189
205
  await parser.flushRemaining();
190
206
  reportFileEdited();
191
207
  updateAIStatus(shareDBDoc, chatId, 'Done editing.');
192
- updateAIScratchpad(shareDBDoc, chatId, fullContent);
208
+ updateAIMessageContent(
209
+ shareDBDoc,
210
+ chatId,
211
+ aiMessageId,
212
+ fullContent,
213
+ );
214
+
215
+ // Finalize the AI message by clearing temporary fields
216
+ finalizeAIMessage(shareDBDoc, chatId);
193
217
 
194
218
  // Clear VizBot presence when done
195
219
  DEBUG &&
@@ -206,6 +230,18 @@ export const createLLMFunction = ({
206
230
  }
207
231
  });
208
232
 
233
+ // If streaming editing is not enabled, we need to
234
+ // apply all the edits at once
235
+ if (!enableStreamingEditing) {
236
+ updateFiles(
237
+ shareDBDoc,
238
+ mergeFileChanges(
239
+ shareDBDoc.data.files,
240
+ parseMarkdownFiles(fullContent, 'bold').files,
241
+ ),
242
+ );
243
+ }
244
+
209
245
  // Write chunks file for debugging
210
246
  if (DEBUG) {
211
247
  const chunksFileJSONpath = `./ai-chunks-${chatId}.json`;
@@ -1,4 +1,4 @@
1
- import { diff } from './client/diff';
1
+ import { diff } from './ot.js';
2
2
 
3
3
  /**
4
4
  * Creates a submitOperation function that can be used to submit diff-based operations to ShareDB.