vzcode 1.37.0 → 1.39.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-BRU-VQpy.js → index-0y6ZxgSv.js} +171 -107
- package/dist/assets/{index-lvLw6dCW.css → index-DKLiE98a.css} +1 -1
- package/dist/index.html +2 -2
- package/package.json +8 -7
- package/src/client/CodeEditor/getOrCreateEditor.ts +0 -3
- package/src/client/CodeEditor/index.tsx +0 -1
- package/src/client/CodeEditor/json1PresenceDisplay.ts +4 -18
- package/src/client/VZCodeContext.tsx +11 -0
- package/src/client/VZSidebar/aiCopyPaste.ts +124 -0
- package/src/client/VZSidebar/index.tsx +44 -12
- package/src/client/VZSidebar/styles.scss +49 -0
- package/src/client/index.ts +1 -0
- package/src/client/useKeyboardShortcuts.ts +7 -1
- package/src/client/usePresenceAutoFollow.ts +86 -0
- package/src/client/useRunCode.tsx +2 -16
- package/src/client/useSubmitOperation.ts +4 -11
- package/src/runCode.js +48 -0
- package/src/runCode.ts +58 -0
- package/src/server/aiChatHandler/aiEditing.js +32 -30
- package/src/server/aiChatHandler/chatOperations.js +132 -7
- package/src/server/aiChatHandler/index.js +17 -23
- package/src/server/aiChatHandler/llmStreaming.js +144 -32
- package/src/server/index.js +19 -9
- package/src/submitOperation.js +16 -0
- package/src/submitOperation.ts +25 -0
package/src/runCode.ts
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { SubmitOperation } from './types';
|
|
2
|
+
import { VizContent } from '@vizhub/viz-types';
|
|
3
|
+
import { createSubmitOperation } from './submitOperation';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Creates a runCode function that triggers code execution by flashing `isInteracting` to `true`.
|
|
7
|
+
* This works for both client-side (with submitOperation) and server-side (with ShareDB document).
|
|
8
|
+
*/
|
|
9
|
+
export const createRunCodeFunction = (
|
|
10
|
+
submitOperationOrDoc:
|
|
11
|
+
| SubmitOperation
|
|
12
|
+
| { data: any; submitOp: (ops: any) => void },
|
|
13
|
+
) => {
|
|
14
|
+
return () => {
|
|
15
|
+
let submitOperation: SubmitOperation;
|
|
16
|
+
|
|
17
|
+
// Check if this is a client-side submitOperation or server-side ShareDB document
|
|
18
|
+
if (typeof submitOperationOrDoc === 'function') {
|
|
19
|
+
// Client-side: already a submitOperation function
|
|
20
|
+
submitOperation =
|
|
21
|
+
submitOperationOrDoc as SubmitOperation;
|
|
22
|
+
} else {
|
|
23
|
+
// Server-side: create submitOperation from ShareDB document
|
|
24
|
+
submitOperation = createSubmitOperation(
|
|
25
|
+
submitOperationOrDoc,
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Use the unified submitOperation approach for both client and server
|
|
30
|
+
submitOperation((content: VizContent) => ({
|
|
31
|
+
...content,
|
|
32
|
+
isInteracting: true,
|
|
33
|
+
}));
|
|
34
|
+
|
|
35
|
+
setTimeout(() => {
|
|
36
|
+
// This somewhat cryptic logic
|
|
37
|
+
// deletes the `isInteracting` property
|
|
38
|
+
// from the document.
|
|
39
|
+
submitOperation(
|
|
40
|
+
({ isInteracting, ...newDocument }) => newDocument,
|
|
41
|
+
);
|
|
42
|
+
}, 0);
|
|
43
|
+
};
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Creates a runCodeRef object compatible with React refs.
|
|
48
|
+
* This is useful for maintaining compatibility with existing code that expects a ref.
|
|
49
|
+
*/
|
|
50
|
+
export const createRunCodeRef = (
|
|
51
|
+
submitOperationOrDoc:
|
|
52
|
+
| SubmitOperation
|
|
53
|
+
| { data: any; submitOp: (ops: any) => void },
|
|
54
|
+
) => {
|
|
55
|
+
return {
|
|
56
|
+
current: createRunCodeFunction(submitOperationOrDoc),
|
|
57
|
+
};
|
|
58
|
+
};
|
|
@@ -1,42 +1,44 @@
|
|
|
1
|
-
import { performAiEdit } from 'editcodewithai';
|
|
2
1
|
import {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
} from '
|
|
2
|
+
assembleFullPrompt,
|
|
3
|
+
prepareFilesForPrompt,
|
|
4
|
+
} from 'editcodewithai';
|
|
5
|
+
import { formatMarkdownFiles } from 'llm-code-format';
|
|
7
6
|
|
|
8
7
|
/**
|
|
9
|
-
* Performs AI editing operations
|
|
8
|
+
* Performs AI editing operations using streaming with incremental OT operations
|
|
10
9
|
*/
|
|
11
|
-
export const performAIEditing = async (
|
|
10
|
+
export const performAIEditing = async ({
|
|
11
|
+
prompt,
|
|
12
12
|
shareDBDoc,
|
|
13
|
-
chatId,
|
|
14
|
-
content,
|
|
15
|
-
files,
|
|
16
13
|
llmFunction,
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
files,
|
|
21
|
-
llmFunction,
|
|
22
|
-
apiKey: process.env.VIZHUB_EDIT_WITH_AI_API_KEY,
|
|
23
|
-
});
|
|
24
|
-
|
|
25
|
-
// Clear the scratchpad and update status
|
|
26
|
-
clearAIScratchpadAndStatus(
|
|
27
|
-
shareDBDoc,
|
|
28
|
-
chatId,
|
|
29
|
-
'Done editing with AI.',
|
|
14
|
+
runCode,
|
|
15
|
+
}) => {
|
|
16
|
+
const preparedFiles = prepareFilesForPrompt(
|
|
17
|
+
shareDBDoc.data.files,
|
|
30
18
|
);
|
|
19
|
+
const filesContext = formatMarkdownFiles(preparedFiles);
|
|
31
20
|
|
|
32
|
-
//
|
|
33
|
-
|
|
21
|
+
// 2. Assemble the final prompt
|
|
22
|
+
const fullPrompt = assembleFullPrompt({
|
|
23
|
+
filesContext,
|
|
24
|
+
prompt,
|
|
25
|
+
editFormat: 'whole',
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
// Call the LLM function which will handle streaming and incremental file updates
|
|
29
|
+
const result = await llmFunction(fullPrompt);
|
|
34
30
|
|
|
35
|
-
//
|
|
36
|
-
|
|
31
|
+
// Clear the scratchpad and update status
|
|
32
|
+
// clearAIScratchpadAndStatus(
|
|
33
|
+
// shareDBDoc,
|
|
34
|
+
// chatId,
|
|
35
|
+
// 'Done editing with AI.',
|
|
36
|
+
// );
|
|
37
37
|
|
|
38
|
-
|
|
39
|
-
setIsInteracting(shareDBDoc, false);
|
|
38
|
+
runCode();
|
|
40
39
|
|
|
41
|
-
return
|
|
40
|
+
return {
|
|
41
|
+
content: result.content,
|
|
42
|
+
generationId: result.generationId,
|
|
43
|
+
};
|
|
42
44
|
};
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { dateToTimestamp } from '@vizhub/viz-utils';
|
|
2
2
|
import { diff } from '../../client/diff.js';
|
|
3
|
+
import { randomId } from '../../randomId.js';
|
|
3
4
|
|
|
4
5
|
/**
|
|
5
6
|
* Ensures the chats object exists in the ShareDB document
|
|
@@ -108,7 +109,13 @@ export const updateAIScratchpad = (
|
|
|
108
109
|
},
|
|
109
110
|
},
|
|
110
111
|
});
|
|
111
|
-
|
|
112
|
+
|
|
113
|
+
// op is `null` if there are no changes
|
|
114
|
+
// This can happen if the content is the same as before
|
|
115
|
+
// In that case, we don't need to submit an operation
|
|
116
|
+
if (op) {
|
|
117
|
+
shareDBDoc.submitOp(op);
|
|
118
|
+
}
|
|
112
119
|
};
|
|
113
120
|
|
|
114
121
|
/**
|
|
@@ -174,8 +181,7 @@ export const addAIMessage = (
|
|
|
174
181
|
export const updateFiles = (shareDBDoc, files) => {
|
|
175
182
|
const filesOp = diff(shareDBDoc.data, {
|
|
176
183
|
...shareDBDoc.data,
|
|
177
|
-
files
|
|
178
|
-
isInteracting: true,
|
|
184
|
+
files,
|
|
179
185
|
});
|
|
180
186
|
shareDBDoc.submitOp(filesOp);
|
|
181
187
|
};
|
|
@@ -187,9 +193,128 @@ export const setIsInteracting = (
|
|
|
187
193
|
shareDBDoc,
|
|
188
194
|
isInteracting,
|
|
189
195
|
) => {
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
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);
|
|
194
210
|
shareDBDoc.submitOp(interactingOp);
|
|
195
211
|
};
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Finds a file ID by searching for a matching file name
|
|
215
|
+
*/
|
|
216
|
+
export const resolveFileId = (fileName, shareDBDoc) => {
|
|
217
|
+
const files = shareDBDoc.data.files;
|
|
218
|
+
|
|
219
|
+
// Search through all files to find matching name
|
|
220
|
+
for (const [fileId, file] of Object.entries(files)) {
|
|
221
|
+
if (file.name === fileName) {
|
|
222
|
+
return fileId;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// If file doesn't exist, return null
|
|
227
|
+
return null;
|
|
228
|
+
};
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Creates a new file with a random ID
|
|
232
|
+
*/
|
|
233
|
+
export const createNewFile = (shareDBDoc, fileName) => {
|
|
234
|
+
// Generate a new random file ID
|
|
235
|
+
const newFileId = randomId();
|
|
236
|
+
|
|
237
|
+
const newState = {
|
|
238
|
+
...shareDBDoc.data,
|
|
239
|
+
files: {
|
|
240
|
+
...shareDBDoc.data.files,
|
|
241
|
+
[newFileId]: {
|
|
242
|
+
name: fileName,
|
|
243
|
+
text: '',
|
|
244
|
+
},
|
|
245
|
+
},
|
|
246
|
+
};
|
|
247
|
+
|
|
248
|
+
const op = diff(shareDBDoc.data, newState);
|
|
249
|
+
shareDBDoc.submitOp(op);
|
|
250
|
+
return newFileId;
|
|
251
|
+
};
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* Ensures a file exists, creating it if necessary
|
|
255
|
+
*/
|
|
256
|
+
export const ensureFileExists = (shareDBDoc, fileName) => {
|
|
257
|
+
let fileId = resolveFileId(fileName, shareDBDoc);
|
|
258
|
+
|
|
259
|
+
if (!fileId) {
|
|
260
|
+
// File doesn't exist, create it
|
|
261
|
+
fileId = createNewFile(shareDBDoc, fileName);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
return fileId;
|
|
265
|
+
};
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* Clears the content of a file
|
|
269
|
+
*/
|
|
270
|
+
export const clearFileContent = (shareDBDoc, fileId) => {
|
|
271
|
+
const currentFile = shareDBDoc.data.files[fileId];
|
|
272
|
+
|
|
273
|
+
if (currentFile && currentFile.text) {
|
|
274
|
+
// Clear the file content
|
|
275
|
+
const newState = {
|
|
276
|
+
...shareDBDoc.data,
|
|
277
|
+
files: {
|
|
278
|
+
...shareDBDoc.data.files,
|
|
279
|
+
[fileId]: {
|
|
280
|
+
...currentFile,
|
|
281
|
+
text: '',
|
|
282
|
+
},
|
|
283
|
+
},
|
|
284
|
+
};
|
|
285
|
+
|
|
286
|
+
const op = diff(shareDBDoc.data, newState);
|
|
287
|
+
shareDBDoc.submitOp(op);
|
|
288
|
+
}
|
|
289
|
+
};
|
|
290
|
+
|
|
291
|
+
/**
|
|
292
|
+
* Appends a line to a file using OT operations
|
|
293
|
+
*/
|
|
294
|
+
export const appendLineToFile = (
|
|
295
|
+
shareDBDoc,
|
|
296
|
+
fileId,
|
|
297
|
+
line,
|
|
298
|
+
) => {
|
|
299
|
+
const currentFile = shareDBDoc.data.files[fileId];
|
|
300
|
+
const currentContent = currentFile?.text || '';
|
|
301
|
+
const newContent = currentContent + line + '\n';
|
|
302
|
+
|
|
303
|
+
// Create the new file state
|
|
304
|
+
const newFileState = {
|
|
305
|
+
...currentFile,
|
|
306
|
+
text: newContent,
|
|
307
|
+
};
|
|
308
|
+
|
|
309
|
+
const newDocState = {
|
|
310
|
+
...shareDBDoc.data,
|
|
311
|
+
files: {
|
|
312
|
+
...shareDBDoc.data.files,
|
|
313
|
+
[fileId]: newFileState,
|
|
314
|
+
},
|
|
315
|
+
};
|
|
316
|
+
|
|
317
|
+
// Generate OT operation using the diff utility
|
|
318
|
+
const op = diff(shareDBDoc.data, newDocState);
|
|
319
|
+
shareDBDoc.submitOp(op);
|
|
320
|
+
};
|
|
@@ -3,20 +3,20 @@ import {
|
|
|
3
3
|
ensureChatsExist,
|
|
4
4
|
ensureChatExists,
|
|
5
5
|
addUserMessage,
|
|
6
|
-
addAIMessage,
|
|
7
6
|
} from './chatOperations.js';
|
|
8
7
|
import { createLLMFunction } from './llmStreaming.js';
|
|
9
8
|
import { performAIEditing } from './aiEditing.js';
|
|
10
9
|
import { handleError } from './errorHandling.js';
|
|
10
|
+
import { createRunCodeFunction } from '../../runCode.js';
|
|
11
11
|
|
|
12
|
-
const
|
|
12
|
+
const DEBUG = false;
|
|
13
13
|
|
|
14
14
|
export const handleAIChatMessage =
|
|
15
|
-
(shareDBDoc,
|
|
15
|
+
({ shareDBDoc, localPresence, onCreditDeduction }) =>
|
|
16
16
|
async (req, res) => {
|
|
17
17
|
const { content, chatId } = req.body;
|
|
18
18
|
|
|
19
|
-
if (
|
|
19
|
+
if (DEBUG) {
|
|
20
20
|
console.log(
|
|
21
21
|
'[handleAIChatMessage] content:',
|
|
22
22
|
content,
|
|
@@ -31,9 +31,6 @@ export const handleAIChatMessage =
|
|
|
31
31
|
}
|
|
32
32
|
|
|
33
33
|
try {
|
|
34
|
-
// Get existing files from ShareDB doc
|
|
35
|
-
const files = shareDBDoc.data.files;
|
|
36
|
-
|
|
37
34
|
// Ensure chats structure exists
|
|
38
35
|
ensureChatsExist(shareDBDoc);
|
|
39
36
|
ensureChatExists(shareDBDoc, chatId);
|
|
@@ -42,34 +39,31 @@ export const handleAIChatMessage =
|
|
|
42
39
|
addUserMessage(shareDBDoc, chatId, content);
|
|
43
40
|
|
|
44
41
|
// Create LLM function for streaming
|
|
45
|
-
const llmFunction = createLLMFunction(
|
|
42
|
+
const llmFunction = createLLMFunction({
|
|
46
43
|
shareDBDoc,
|
|
44
|
+
localPresence,
|
|
47
45
|
chatId,
|
|
48
|
-
);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
// Create server-side runCode function using shared module
|
|
49
|
+
const runCode = createRunCodeFunction(shareDBDoc);
|
|
49
50
|
|
|
50
51
|
// Perform AI editing
|
|
51
|
-
const editResult = await performAIEditing(
|
|
52
|
+
const editResult = await performAIEditing({
|
|
53
|
+
prompt: content,
|
|
52
54
|
shareDBDoc,
|
|
53
55
|
chatId,
|
|
54
|
-
content,
|
|
55
|
-
files,
|
|
56
56
|
llmFunction,
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
// Add AI response message
|
|
60
|
-
const aiResponse = addAIMessage(
|
|
61
|
-
shareDBDoc,
|
|
62
|
-
chatId,
|
|
63
|
-
editResult.content,
|
|
64
|
-
);
|
|
57
|
+
runCode,
|
|
58
|
+
});
|
|
65
59
|
|
|
66
60
|
// Handle credit deduction if callback is provided
|
|
67
61
|
if (
|
|
68
|
-
|
|
62
|
+
onCreditDeduction &&
|
|
69
63
|
editResult.upstreamCostCents
|
|
70
64
|
) {
|
|
71
65
|
try {
|
|
72
|
-
await
|
|
66
|
+
await onCreditDeduction({
|
|
73
67
|
upstreamCostCents: editResult.upstreamCostCents,
|
|
74
68
|
provider: editResult.provider,
|
|
75
69
|
inputTokens: editResult.inputTokens,
|
|
@@ -84,7 +78,7 @@ export const handleAIChatMessage =
|
|
|
84
78
|
}
|
|
85
79
|
}
|
|
86
80
|
|
|
87
|
-
res.status(200).json(
|
|
81
|
+
res.status(200).json('success');
|
|
88
82
|
} catch (error) {
|
|
89
83
|
handleError(shareDBDoc, chatId, error, res);
|
|
90
84
|
}
|
|
@@ -1,17 +1,40 @@
|
|
|
1
1
|
import { StreamingMarkdownParser } from 'llm-code-format';
|
|
2
2
|
import { ChatOpenAI } from '@langchain/openai';
|
|
3
|
+
import fs from 'fs';
|
|
3
4
|
import {
|
|
4
5
|
updateAIStatus,
|
|
5
6
|
updateAIScratchpad,
|
|
7
|
+
ensureFileExists,
|
|
8
|
+
clearFileContent,
|
|
9
|
+
appendLineToFile,
|
|
6
10
|
} from './chatOperations.js';
|
|
7
11
|
|
|
8
|
-
const
|
|
12
|
+
const DEBUG = false;
|
|
9
13
|
|
|
10
14
|
/**
|
|
11
15
|
* Creates and configures the LLM function for streaming
|
|
12
16
|
*/
|
|
13
|
-
export const createLLMFunction = (
|
|
17
|
+
export const createLLMFunction = ({
|
|
18
|
+
shareDBDoc,
|
|
19
|
+
localPresence,
|
|
20
|
+
chatId,
|
|
21
|
+
}) => {
|
|
14
22
|
return async (fullPrompt) => {
|
|
23
|
+
// Submit initial presence for VizBot
|
|
24
|
+
const vizBotPresence = {
|
|
25
|
+
username: 'VizBot',
|
|
26
|
+
start: ['files'], // Indicate VizBot is working on files
|
|
27
|
+
end: ['files'],
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
localPresence.submit(vizBotPresence, (error) => {
|
|
31
|
+
if (error) {
|
|
32
|
+
console.warn(
|
|
33
|
+
'VizBot presence submission error:',
|
|
34
|
+
error,
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
});
|
|
15
38
|
const chatModel = new ChatOpenAI({
|
|
16
39
|
modelName:
|
|
17
40
|
process.env.VIZHUB_EDIT_WITH_AI_MODEL_NAME ||
|
|
@@ -29,22 +52,58 @@ export const createLLMFunction = (shareDBDoc, chatId) => {
|
|
|
29
52
|
|
|
30
53
|
let fullContent = '';
|
|
31
54
|
let generationId = '';
|
|
55
|
+
let currentEditingFileId = null;
|
|
56
|
+
let currentEditingFileName = null;
|
|
32
57
|
|
|
33
|
-
//
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
58
|
+
// Function to report file edited
|
|
59
|
+
// This is called when the AI has finished editing a file
|
|
60
|
+
// and we want to update the scratchpad with the file name.
|
|
61
|
+
const reportFileEdited = () => {
|
|
62
|
+
if (currentEditingFileName) {
|
|
63
|
+
fullContent += ` * Edited ${currentEditingFileName}\n`;
|
|
64
|
+
updateAIScratchpad(shareDBDoc, chatId, fullContent);
|
|
65
|
+
currentEditingFileName = null;
|
|
66
|
+
}
|
|
67
|
+
};
|
|
40
68
|
|
|
41
69
|
// Define callbacks for streaming parser
|
|
42
70
|
const callbacks = {
|
|
43
71
|
onFileNameChange: (fileName, format) => {
|
|
44
|
-
|
|
72
|
+
DEBUG &&
|
|
45
73
|
console.log(
|
|
46
74
|
`File changed to: ${fileName} (${format})`,
|
|
47
75
|
);
|
|
76
|
+
|
|
77
|
+
// Find existing file or create new one
|
|
78
|
+
currentEditingFileId = ensureFileExists(
|
|
79
|
+
shareDBDoc,
|
|
80
|
+
fileName,
|
|
81
|
+
);
|
|
82
|
+
|
|
83
|
+
reportFileEdited();
|
|
84
|
+
currentEditingFileName = fileName;
|
|
85
|
+
|
|
86
|
+
// Clear the file content to start fresh
|
|
87
|
+
// (AI will regenerate the entire file content)
|
|
88
|
+
clearFileContent(shareDBDoc, currentEditingFileId);
|
|
89
|
+
|
|
90
|
+
// Update VizBot presence to show it's editing this specific file
|
|
91
|
+
const filePresence = {
|
|
92
|
+
username: 'VizBot',
|
|
93
|
+
start: ['files', currentEditingFileId, 'text', 0],
|
|
94
|
+
end: ['files', currentEditingFileId, 'text', 0],
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
localPresence.submit(filePresence, (error) => {
|
|
98
|
+
if (error) {
|
|
99
|
+
console.warn(
|
|
100
|
+
'VizBot file presence submission error:',
|
|
101
|
+
error,
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
// Update AI status
|
|
48
107
|
updateAIStatus(
|
|
49
108
|
shareDBDoc,
|
|
50
109
|
chatId,
|
|
@@ -52,37 +111,70 @@ export const createLLMFunction = (shareDBDoc, chatId) => {
|
|
|
52
111
|
);
|
|
53
112
|
},
|
|
54
113
|
onCodeLine: (line) => {
|
|
55
|
-
|
|
114
|
+
DEBUG && console.log(`Code line: ${line}`);
|
|
115
|
+
|
|
116
|
+
if (currentEditingFileId) {
|
|
117
|
+
// Apply OT operation for this line immediately
|
|
118
|
+
appendLineToFile(
|
|
119
|
+
shareDBDoc,
|
|
120
|
+
currentEditingFileId,
|
|
121
|
+
line,
|
|
122
|
+
);
|
|
123
|
+
|
|
124
|
+
// Update VizBot presence to show cursor at the end of the file
|
|
125
|
+
const currentFile =
|
|
126
|
+
shareDBDoc.data.files[currentEditingFileId];
|
|
127
|
+
if (currentFile && currentFile.text) {
|
|
128
|
+
const textLength = currentFile.text.length;
|
|
129
|
+
const filePresence = {
|
|
130
|
+
username: 'VizBot',
|
|
131
|
+
start: [
|
|
132
|
+
'files',
|
|
133
|
+
currentEditingFileId,
|
|
134
|
+
'text',
|
|
135
|
+
textLength,
|
|
136
|
+
],
|
|
137
|
+
end: [
|
|
138
|
+
'files',
|
|
139
|
+
currentEditingFileId,
|
|
140
|
+
'text',
|
|
141
|
+
textLength,
|
|
142
|
+
],
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
localPresence.submit(filePresence, (error) => {
|
|
146
|
+
if (error) {
|
|
147
|
+
console.warn(
|
|
148
|
+
'VizBot line presence submission error:',
|
|
149
|
+
error,
|
|
150
|
+
);
|
|
151
|
+
}
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
}
|
|
56
155
|
},
|
|
57
156
|
onNonCodeLine: (line) => {
|
|
58
|
-
|
|
157
|
+
// We want to report a file edited only if the line is not empty,
|
|
158
|
+
// because sometimes the LLMs leave a newline between the file name
|
|
159
|
+
// declaration and th
|
|
160
|
+
if (line.trim() !== '') {
|
|
161
|
+
reportFileEdited();
|
|
162
|
+
}
|
|
163
|
+
fullContent += line + '\n';
|
|
164
|
+
updateAIScratchpad(shareDBDoc, chatId, fullContent);
|
|
59
165
|
},
|
|
60
166
|
};
|
|
61
167
|
|
|
62
168
|
const parser = new StreamingMarkdownParser(callbacks);
|
|
63
169
|
|
|
170
|
+
const chunks = [];
|
|
171
|
+
|
|
64
172
|
// Stream the response
|
|
65
173
|
const stream = await chatModel.stream(fullPrompt);
|
|
66
174
|
for await (const chunk of stream) {
|
|
67
175
|
if (chunk.content) {
|
|
68
176
|
const chunkContent = String(chunk.content);
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
// Throttle updates
|
|
72
|
-
const now = Date.now();
|
|
73
|
-
const shouldUpdate =
|
|
74
|
-
now - lastUpdateTime >= UPDATE_THROTTLE_MS;
|
|
75
|
-
|
|
76
|
-
if (shouldUpdate) {
|
|
77
|
-
updateAIScratchpad(
|
|
78
|
-
shareDBDoc,
|
|
79
|
-
chatId,
|
|
80
|
-
fullContent,
|
|
81
|
-
);
|
|
82
|
-
expectedAiScratchpad = fullContent;
|
|
83
|
-
lastUpdateTime = now;
|
|
84
|
-
}
|
|
85
|
-
|
|
177
|
+
chunks.push(chunkContent);
|
|
86
178
|
parser.processChunk(chunkContent);
|
|
87
179
|
}
|
|
88
180
|
|
|
@@ -91,10 +183,30 @@ export const createLLMFunction = (shareDBDoc, chatId) => {
|
|
|
91
183
|
}
|
|
92
184
|
}
|
|
93
185
|
parser.flushRemaining();
|
|
186
|
+
reportFileEdited();
|
|
187
|
+
updateAIStatus(shareDBDoc, chatId, 'Done editing.');
|
|
188
|
+
updateAIScratchpad(shareDBDoc, chatId, fullContent);
|
|
189
|
+
|
|
190
|
+
// Clear VizBot presence when done
|
|
191
|
+
localPresence.destroy((error) => {
|
|
192
|
+
if (error) {
|
|
193
|
+
console.warn(
|
|
194
|
+
'VizBot presence cleanup error:',
|
|
195
|
+
error,
|
|
196
|
+
);
|
|
197
|
+
}
|
|
198
|
+
});
|
|
94
199
|
|
|
95
|
-
//
|
|
96
|
-
if (
|
|
97
|
-
|
|
200
|
+
// Write chunks file for debugging
|
|
201
|
+
if (DEBUG) {
|
|
202
|
+
const chunksFileJSONpath = `./ai-chunks-${chatId}.json`;
|
|
203
|
+
fs.writeFileSync(
|
|
204
|
+
chunksFileJSONpath,
|
|
205
|
+
JSON.stringify(chunks, null, 2),
|
|
206
|
+
);
|
|
207
|
+
console.log(
|
|
208
|
+
`AI chunks written to ${chunksFileJSONpath}`,
|
|
209
|
+
);
|
|
98
210
|
}
|
|
99
211
|
|
|
100
212
|
return {
|
package/src/server/index.js
CHANGED
|
@@ -51,13 +51,6 @@ ShareDB.types.register(json1Presence.type);
|
|
|
51
51
|
|
|
52
52
|
const app = express();
|
|
53
53
|
|
|
54
|
-
// TODO make this configurable
|
|
55
|
-
// See https://github.com/vizhub-core/vzcode/issues/95
|
|
56
|
-
app.post('/saveTime', (req, res) => {
|
|
57
|
-
//autoSaveDebounceTimeMS = req.body.autoSaveDebounceTimeMS;
|
|
58
|
-
console.log('autoSaveDebounceTimeMS', req.body);
|
|
59
|
-
});
|
|
60
|
-
|
|
61
54
|
// Use ShareDB over WebSocket
|
|
62
55
|
const shareDBBackend = new ShareDB({
|
|
63
56
|
// Enable presence
|
|
@@ -82,7 +75,7 @@ wss.on('connection', (ws) => {
|
|
|
82
75
|
});
|
|
83
76
|
|
|
84
77
|
// Handle disconnections
|
|
85
|
-
ws.on('close', (
|
|
78
|
+
ws.on('close', () => {
|
|
86
79
|
clientStream.end();
|
|
87
80
|
});
|
|
88
81
|
});
|
|
@@ -97,6 +90,23 @@ app.use(express.static(dir));
|
|
|
97
90
|
// which is a representation of files on disk.
|
|
98
91
|
const shareDBConnection = shareDBBackend.connect();
|
|
99
92
|
const shareDBDoc = shareDBConnection.get('documents', '1');
|
|
93
|
+
|
|
94
|
+
// Set up presence for VizBot following the same pattern as useShareDB.ts
|
|
95
|
+
const docPresence = shareDBConnection.getDocPresence(
|
|
96
|
+
'documents',
|
|
97
|
+
'1',
|
|
98
|
+
);
|
|
99
|
+
|
|
100
|
+
// Create local presence for VizBot with a unique ID
|
|
101
|
+
const generateVizBotId = () => {
|
|
102
|
+
const timestamp = Date.now().toString(36);
|
|
103
|
+
return `vizbot-${timestamp}`;
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
const localPresence = docPresence.create(
|
|
107
|
+
generateVizBotId(),
|
|
108
|
+
);
|
|
109
|
+
|
|
100
110
|
shareDBDoc.create(initialDocument, json1Presence.type.uri);
|
|
101
111
|
|
|
102
112
|
// Handle AI Assist requests.
|
|
@@ -117,7 +127,7 @@ app.post(
|
|
|
117
127
|
app.post(
|
|
118
128
|
'/ai-chat-message',
|
|
119
129
|
bodyParser.json(),
|
|
120
|
-
handleAIChatMessage(shareDBDoc),
|
|
130
|
+
handleAIChatMessage({ shareDBDoc, localPresence }),
|
|
121
131
|
);
|
|
122
132
|
|
|
123
133
|
// Livekit Token Generator
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { diff } from './client/diff.js';
|
|
2
|
+
// TODO migrate the server-side code to use TypeScript,
|
|
3
|
+
// and delete this file once the migration is complete (use src/submitOperation.ts instead).
|
|
4
|
+
/**
|
|
5
|
+
* Creates a submitOperation function that can be used to submit diff-based operations to ShareDB.
|
|
6
|
+
* This is the core logic extracted from useSubmitOperation for reuse across client and server.
|
|
7
|
+
*/
|
|
8
|
+
export const createSubmitOperation = (shareDBDoc) => {
|
|
9
|
+
return (next) => {
|
|
10
|
+
const data = shareDBDoc.data;
|
|
11
|
+
const op = diff(data, next(data));
|
|
12
|
+
if (op && shareDBDoc) {
|
|
13
|
+
shareDBDoc.submitOp(op);
|
|
14
|
+
}
|
|
15
|
+
};
|
|
16
|
+
};
|