vzcode 1.38.0 → 1.40.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-Cw7RI1uP.js → index-ubSLcdJf.js} +91 -91
- package/dist/index.html +1 -1
- package/package.json +9 -7
- package/src/client/CodeEditor/getOrCreateEditor.ts +0 -3
- package/src/client/CodeEditor/index.tsx +0 -1
- package/src/client/CodeEditor/json1PresenceDisplay.ts +28 -58
- package/src/client/VZCodeContext.tsx +11 -0
- package/src/client/VZSidebar/index.tsx +6 -0
- package/src/client/useKeyboardShortcuts.ts +7 -1
- package/src/client/usePresenceAutoFollow.ts +85 -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.ts +44 -0
- package/src/server/aiChatHandler/{chatOperations.js → chatOperations.ts} +135 -7
- package/src/server/aiChatHandler/{index.js → index.ts} +34 -30
- package/src/server/aiChatHandler/llmStreaming.ts +223 -0
- package/src/server/{generateAIResponse.js → generateAIResponse.ts} +3 -2
- package/src/server/{handleAIAssist.js → handleAIAssist.ts} +11 -4
- package/src/server/{index.js → index.ts} +31 -27
- package/src/submitOperation.js +16 -0
- package/src/submitOperation.ts +25 -0
- package/src/server/aiChatHandler/aiEditing.js +0 -42
- package/src/server/aiChatHandler/llmStreaming.js +0 -105
- /package/src/server/aiChatHandler/{errorHandling.js → errorHandling.ts} +0 -0
- /package/src/server/aiChatHandler/{validation.js → validation.ts} +0 -0
- /package/src/server/{computeInitialDocument.js → computeInitialDocument.ts} +0 -0
- /package/src/server/{config.js → config.ts} +0 -0
- /package/src/server/{featureFlags.js → featureFlags.ts} +0 -0
- /package/src/server/{handleAIChatMessage.js → handleAIChatMessage.ts} +0 -0
- /package/src/server/{handleAICopilot.js → handleAICopilot.ts} +0 -0
- /package/src/server/{isDirectory.js → isDirectory.ts} +0 -0
- /package/src/server/{livekit.js → livekit.ts} +0 -0
- /package/src/server/{setupEnv.js → setupEnv.ts} +0 -0
|
@@ -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,131 @@ 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 = (
|
|
217
|
+
fileName: string,
|
|
218
|
+
shareDBDoc: any,
|
|
219
|
+
) => {
|
|
220
|
+
const files = shareDBDoc.data.files;
|
|
221
|
+
|
|
222
|
+
// Search through all files to find matching name
|
|
223
|
+
for (const [fileId, file] of Object.entries(files)) {
|
|
224
|
+
if ((file as any).name === fileName) {
|
|
225
|
+
return fileId;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// If file doesn't exist, return null
|
|
230
|
+
return null;
|
|
231
|
+
};
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Creates a new file with a random ID
|
|
235
|
+
*/
|
|
236
|
+
export const createNewFile = (shareDBDoc, fileName) => {
|
|
237
|
+
// Generate a new random file ID
|
|
238
|
+
const newFileId = randomId();
|
|
239
|
+
|
|
240
|
+
const newState = {
|
|
241
|
+
...shareDBDoc.data,
|
|
242
|
+
files: {
|
|
243
|
+
...shareDBDoc.data.files,
|
|
244
|
+
[newFileId]: {
|
|
245
|
+
name: fileName,
|
|
246
|
+
text: '',
|
|
247
|
+
},
|
|
248
|
+
},
|
|
249
|
+
};
|
|
250
|
+
|
|
251
|
+
const op = diff(shareDBDoc.data, newState);
|
|
252
|
+
shareDBDoc.submitOp(op);
|
|
253
|
+
return newFileId;
|
|
254
|
+
};
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* Ensures a file exists, creating it if necessary
|
|
258
|
+
*/
|
|
259
|
+
export const ensureFileExists = (shareDBDoc, fileName) => {
|
|
260
|
+
let fileId = resolveFileId(fileName, shareDBDoc);
|
|
261
|
+
|
|
262
|
+
if (!fileId) {
|
|
263
|
+
// File doesn't exist, create it
|
|
264
|
+
fileId = createNewFile(shareDBDoc, fileName);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
return fileId;
|
|
268
|
+
};
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* Clears the content of a file
|
|
272
|
+
*/
|
|
273
|
+
export const clearFileContent = (shareDBDoc, fileId) => {
|
|
274
|
+
const currentFile = shareDBDoc.data.files[fileId];
|
|
275
|
+
|
|
276
|
+
if (currentFile && currentFile.text) {
|
|
277
|
+
// Clear the file content
|
|
278
|
+
const newState = {
|
|
279
|
+
...shareDBDoc.data,
|
|
280
|
+
files: {
|
|
281
|
+
...shareDBDoc.data.files,
|
|
282
|
+
[fileId]: {
|
|
283
|
+
...currentFile,
|
|
284
|
+
text: '',
|
|
285
|
+
},
|
|
286
|
+
},
|
|
287
|
+
};
|
|
288
|
+
|
|
289
|
+
const op = diff(shareDBDoc.data, newState);
|
|
290
|
+
shareDBDoc.submitOp(op);
|
|
291
|
+
}
|
|
292
|
+
};
|
|
293
|
+
|
|
294
|
+
/**
|
|
295
|
+
* Appends a line to a file using OT operations
|
|
296
|
+
*/
|
|
297
|
+
export const appendLineToFile = (
|
|
298
|
+
shareDBDoc,
|
|
299
|
+
fileId,
|
|
300
|
+
line,
|
|
301
|
+
) => {
|
|
302
|
+
const currentFile = shareDBDoc.data.files[fileId];
|
|
303
|
+
const currentContent = currentFile?.text || '';
|
|
304
|
+
const newContent = currentContent + line + '\n';
|
|
305
|
+
|
|
306
|
+
// Create the new file state
|
|
307
|
+
const newFileState = {
|
|
308
|
+
...currentFile,
|
|
309
|
+
text: newContent,
|
|
310
|
+
};
|
|
311
|
+
|
|
312
|
+
const newDocState = {
|
|
313
|
+
...shareDBDoc.data,
|
|
314
|
+
files: {
|
|
315
|
+
...shareDBDoc.data.files,
|
|
316
|
+
[fileId]: newFileState,
|
|
317
|
+
},
|
|
318
|
+
};
|
|
319
|
+
|
|
320
|
+
// Generate OT operation using the diff utility
|
|
321
|
+
const op = diff(shareDBDoc.data, newDocState);
|
|
322
|
+
shareDBDoc.submitOp(op);
|
|
323
|
+
};
|
|
@@ -3,20 +3,30 @@ 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
|
+
import { ShareDBDoc } from '../../types.js';
|
|
12
|
+
import { VizContent } from '@vizhub/viz-types';
|
|
11
13
|
|
|
12
|
-
const
|
|
14
|
+
const DEBUG = false;
|
|
13
15
|
|
|
14
16
|
export const handleAIChatMessage =
|
|
15
|
-
(
|
|
16
|
-
|
|
17
|
+
({
|
|
18
|
+
shareDBDoc,
|
|
19
|
+
createVizBotLocalPresence,
|
|
20
|
+
onCreditDeduction,
|
|
21
|
+
}: {
|
|
22
|
+
shareDBDoc: ShareDBDoc<VizContent>;
|
|
23
|
+
createVizBotLocalPresence: () => any;
|
|
24
|
+
onCreditDeduction?: any;
|
|
25
|
+
}) =>
|
|
26
|
+
async (req: any, res: any) => {
|
|
17
27
|
const { content, chatId } = req.body;
|
|
18
28
|
|
|
19
|
-
if (
|
|
29
|
+
if (DEBUG) {
|
|
20
30
|
console.log(
|
|
21
31
|
'[handleAIChatMessage] content:',
|
|
22
32
|
content,
|
|
@@ -31,9 +41,6 @@ export const handleAIChatMessage =
|
|
|
31
41
|
}
|
|
32
42
|
|
|
33
43
|
try {
|
|
34
|
-
// Get existing files from ShareDB doc
|
|
35
|
-
const files = shareDBDoc.data.files;
|
|
36
|
-
|
|
37
44
|
// Ensure chats structure exists
|
|
38
45
|
ensureChatsExist(shareDBDoc);
|
|
39
46
|
ensureChatExists(shareDBDoc, chatId);
|
|
@@ -42,38 +49,35 @@ export const handleAIChatMessage =
|
|
|
42
49
|
addUserMessage(shareDBDoc, chatId, content);
|
|
43
50
|
|
|
44
51
|
// Create LLM function for streaming
|
|
45
|
-
const llmFunction = createLLMFunction(
|
|
52
|
+
const llmFunction = createLLMFunction({
|
|
46
53
|
shareDBDoc,
|
|
54
|
+
createVizBotLocalPresence,
|
|
47
55
|
chatId,
|
|
48
|
-
);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
// Create server-side runCode function using shared module
|
|
59
|
+
const runCode = createRunCodeFunction(shareDBDoc);
|
|
49
60
|
|
|
50
61
|
// Perform AI editing
|
|
51
|
-
const editResult = await performAIEditing(
|
|
62
|
+
const editResult = await performAIEditing({
|
|
63
|
+
prompt: content,
|
|
52
64
|
shareDBDoc,
|
|
53
|
-
chatId,
|
|
54
|
-
content,
|
|
55
|
-
files,
|
|
56
65
|
llmFunction,
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
// Add AI response message
|
|
60
|
-
const aiResponse = addAIMessage(
|
|
61
|
-
shareDBDoc,
|
|
62
|
-
chatId,
|
|
63
|
-
editResult.content,
|
|
64
|
-
);
|
|
66
|
+
runCode,
|
|
67
|
+
});
|
|
65
68
|
|
|
66
69
|
// Handle credit deduction if callback is provided
|
|
67
70
|
if (
|
|
68
|
-
|
|
69
|
-
editResult.upstreamCostCents
|
|
71
|
+
onCreditDeduction &&
|
|
72
|
+
(editResult as any).upstreamCostCents
|
|
70
73
|
) {
|
|
71
74
|
try {
|
|
72
|
-
await
|
|
73
|
-
upstreamCostCents: editResult
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
75
|
+
await onCreditDeduction({
|
|
76
|
+
upstreamCostCents: (editResult as any)
|
|
77
|
+
.upstreamCostCents,
|
|
78
|
+
provider: (editResult as any).provider,
|
|
79
|
+
inputTokens: (editResult as any).inputTokens,
|
|
80
|
+
outputTokens: (editResult as any).outputTokens,
|
|
77
81
|
});
|
|
78
82
|
} catch (creditError) {
|
|
79
83
|
console.error(
|
|
@@ -84,7 +88,7 @@ export const handleAIChatMessage =
|
|
|
84
88
|
}
|
|
85
89
|
}
|
|
86
90
|
|
|
87
|
-
res.status(200).json(
|
|
91
|
+
res.status(200).json('success');
|
|
88
92
|
} catch (error) {
|
|
89
93
|
handleError(shareDBDoc, chatId, error, res);
|
|
90
94
|
}
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
import { StreamingMarkdownParser } from 'llm-code-format';
|
|
2
|
+
import { ChatOpenAI } from '@langchain/openai';
|
|
3
|
+
import fs from 'fs';
|
|
4
|
+
import {
|
|
5
|
+
updateAIStatus,
|
|
6
|
+
updateAIScratchpad,
|
|
7
|
+
ensureFileExists,
|
|
8
|
+
clearFileContent,
|
|
9
|
+
appendLineToFile,
|
|
10
|
+
} from './chatOperations.js';
|
|
11
|
+
|
|
12
|
+
const DEBUG = false;
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Creates and configures the LLM function for streaming
|
|
16
|
+
*/
|
|
17
|
+
export const createLLMFunction = ({
|
|
18
|
+
shareDBDoc,
|
|
19
|
+
createVizBotLocalPresence,
|
|
20
|
+
chatId,
|
|
21
|
+
}) => {
|
|
22
|
+
return async (fullPrompt) => {
|
|
23
|
+
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
|
+
const chatModel = new ChatOpenAI({
|
|
40
|
+
modelName:
|
|
41
|
+
process.env.VIZHUB_EDIT_WITH_AI_MODEL_NAME ||
|
|
42
|
+
'anthropic/claude-sonnet-4',
|
|
43
|
+
configuration: {
|
|
44
|
+
apiKey: process.env.VIZHUB_EDIT_WITH_AI_API_KEY,
|
|
45
|
+
baseURL: process.env.VIZHUB_EDIT_WITH_AI_BASE_URL,
|
|
46
|
+
defaultHeaders: {
|
|
47
|
+
'HTTP-Referer': 'https://vizhub.com',
|
|
48
|
+
'X-Title': 'VizHub',
|
|
49
|
+
},
|
|
50
|
+
},
|
|
51
|
+
streaming: true,
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
let fullContent = '';
|
|
55
|
+
let generationId = '';
|
|
56
|
+
let currentEditingFileId = null;
|
|
57
|
+
let currentEditingFileName = null;
|
|
58
|
+
|
|
59
|
+
// Function to report file edited
|
|
60
|
+
// This is called when the AI has finished editing a file
|
|
61
|
+
// and we want to update the scratchpad with the file name.
|
|
62
|
+
const reportFileEdited = () => {
|
|
63
|
+
if (currentEditingFileName) {
|
|
64
|
+
fullContent += ` * Edited ${currentEditingFileName}\n`;
|
|
65
|
+
updateAIScratchpad(shareDBDoc, chatId, fullContent);
|
|
66
|
+
currentEditingFileName = null;
|
|
67
|
+
}
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
// Define callbacks for streaming parser
|
|
71
|
+
const callbacks = {
|
|
72
|
+
onFileNameChange: (fileName, format) => {
|
|
73
|
+
DEBUG &&
|
|
74
|
+
console.log(
|
|
75
|
+
`File changed to: ${fileName} (${format})`,
|
|
76
|
+
);
|
|
77
|
+
|
|
78
|
+
// Find existing file or create new one
|
|
79
|
+
currentEditingFileId = ensureFileExists(
|
|
80
|
+
shareDBDoc,
|
|
81
|
+
fileName,
|
|
82
|
+
);
|
|
83
|
+
|
|
84
|
+
reportFileEdited();
|
|
85
|
+
currentEditingFileName = fileName;
|
|
86
|
+
|
|
87
|
+
// Clear the file content to start fresh
|
|
88
|
+
// (AI will regenerate the entire file content)
|
|
89
|
+
clearFileContent(shareDBDoc, currentEditingFileId);
|
|
90
|
+
|
|
91
|
+
// Update VizBot presence to show it's editing this specific file
|
|
92
|
+
const filePresence = {
|
|
93
|
+
username: 'VizBot',
|
|
94
|
+
start: ['files', currentEditingFileId, 'text', 0],
|
|
95
|
+
end: ['files', currentEditingFileId, 'text', 0],
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
localPresence.submit(filePresence, (error) => {
|
|
99
|
+
if (error) {
|
|
100
|
+
console.warn(
|
|
101
|
+
'VizBot file presence submission error:',
|
|
102
|
+
error,
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
// Update AI status
|
|
108
|
+
updateAIStatus(
|
|
109
|
+
shareDBDoc,
|
|
110
|
+
chatId,
|
|
111
|
+
'Editing ' + fileName,
|
|
112
|
+
);
|
|
113
|
+
},
|
|
114
|
+
onCodeLine: (line) => {
|
|
115
|
+
DEBUG && console.log(`Code line: ${line}`);
|
|
116
|
+
|
|
117
|
+
if (currentEditingFileId) {
|
|
118
|
+
// Apply OT operation for this line immediately
|
|
119
|
+
appendLineToFile(
|
|
120
|
+
shareDBDoc,
|
|
121
|
+
currentEditingFileId,
|
|
122
|
+
line,
|
|
123
|
+
);
|
|
124
|
+
|
|
125
|
+
// Update VizBot presence to show cursor at the end of the file
|
|
126
|
+
const currentFile =
|
|
127
|
+
shareDBDoc.data.files[currentEditingFileId];
|
|
128
|
+
if (currentFile && currentFile.text) {
|
|
129
|
+
const textLength = currentFile.text.length;
|
|
130
|
+
const filePresence = {
|
|
131
|
+
username: 'VizBot',
|
|
132
|
+
start: [
|
|
133
|
+
'files',
|
|
134
|
+
currentEditingFileId,
|
|
135
|
+
'text',
|
|
136
|
+
textLength,
|
|
137
|
+
],
|
|
138
|
+
end: [
|
|
139
|
+
'files',
|
|
140
|
+
currentEditingFileId,
|
|
141
|
+
'text',
|
|
142
|
+
textLength,
|
|
143
|
+
],
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
localPresence.submit(filePresence, (error) => {
|
|
147
|
+
if (error) {
|
|
148
|
+
console.warn(
|
|
149
|
+
'VizBot line presence submission error:',
|
|
150
|
+
error,
|
|
151
|
+
);
|
|
152
|
+
}
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
},
|
|
157
|
+
onNonCodeLine: (line) => {
|
|
158
|
+
// We want to report a file edited only if the line is not empty,
|
|
159
|
+
// because sometimes the LLMs leave a newline between the file name
|
|
160
|
+
// declaration and th
|
|
161
|
+
if (line.trim() !== '') {
|
|
162
|
+
reportFileEdited();
|
|
163
|
+
}
|
|
164
|
+
fullContent += line + '\n';
|
|
165
|
+
updateAIScratchpad(shareDBDoc, chatId, fullContent);
|
|
166
|
+
},
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
const parser = new StreamingMarkdownParser(callbacks);
|
|
170
|
+
|
|
171
|
+
const chunks = [];
|
|
172
|
+
|
|
173
|
+
// Stream the response
|
|
174
|
+
const stream = await chatModel.stream(fullPrompt);
|
|
175
|
+
for await (const chunk of stream) {
|
|
176
|
+
if (chunk.content) {
|
|
177
|
+
const chunkContent = String(chunk.content);
|
|
178
|
+
chunks.push(chunkContent);
|
|
179
|
+
parser.processChunk(chunkContent);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
if (!generationId && chunk.lc_kwargs?.id) {
|
|
183
|
+
generationId = chunk.lc_kwargs.id;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
parser.flushRemaining();
|
|
187
|
+
reportFileEdited();
|
|
188
|
+
updateAIStatus(shareDBDoc, chatId, 'Done editing.');
|
|
189
|
+
updateAIScratchpad(shareDBDoc, chatId, fullContent);
|
|
190
|
+
|
|
191
|
+
// Clear VizBot presence when done
|
|
192
|
+
DEBUG &&
|
|
193
|
+
console.log(
|
|
194
|
+
'AI editing done, clearing VizBot presence',
|
|
195
|
+
);
|
|
196
|
+
localPresence.submit(null, (error) => {
|
|
197
|
+
DEBUG && console.log('VizBot presence cleared');
|
|
198
|
+
if (error) {
|
|
199
|
+
console.warn(
|
|
200
|
+
'VizBot presence cleanup error:',
|
|
201
|
+
error,
|
|
202
|
+
);
|
|
203
|
+
}
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
// Write chunks file for debugging
|
|
207
|
+
if (DEBUG) {
|
|
208
|
+
const chunksFileJSONpath = `./ai-chunks-${chatId}.json`;
|
|
209
|
+
fs.writeFileSync(
|
|
210
|
+
chunksFileJSONpath,
|
|
211
|
+
JSON.stringify(chunks, null, 2),
|
|
212
|
+
);
|
|
213
|
+
console.log(
|
|
214
|
+
`AI chunks written to ${chunksFileJSONpath}`,
|
|
215
|
+
);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
return {
|
|
219
|
+
content: fullContent,
|
|
220
|
+
generationId: generationId,
|
|
221
|
+
};
|
|
222
|
+
};
|
|
223
|
+
};
|
|
@@ -28,7 +28,8 @@ const slowdown = false;
|
|
|
28
28
|
|
|
29
29
|
// The options passed into the OpenAI client
|
|
30
30
|
// new OpenAI(openAIOptions)
|
|
31
|
-
const openAIOptions
|
|
31
|
+
const openAIOptions: { apiKey?: string; baseURL?: string } =
|
|
32
|
+
{};
|
|
32
33
|
|
|
33
34
|
// Support specifying the API key via an environment variable
|
|
34
35
|
// If VZCODE_AI_API_KEY is not set, note that the OpenAI client
|
|
@@ -71,7 +72,7 @@ const opComesFromAIAssist = (ops, source) =>
|
|
|
71
72
|
|
|
72
73
|
// Keeps track of the currently ongoing AI streams.
|
|
73
74
|
// There could be many streams at the same time.
|
|
74
|
-
const streams = {};
|
|
75
|
+
export const streams: Record<string, any> = {};
|
|
75
76
|
|
|
76
77
|
export const generateAIResponse = async ({
|
|
77
78
|
inputText,
|
|
@@ -1,9 +1,12 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
generateAIResponse,
|
|
3
|
+
streams,
|
|
4
|
+
} from './generateAIResponse.js';
|
|
2
5
|
|
|
3
6
|
const debug = false;
|
|
4
7
|
|
|
5
8
|
export const handleAIAssist =
|
|
6
|
-
(shareDBDoc) => async (req, res) => {
|
|
9
|
+
(shareDBDoc: any) => async (req: any, res: any) => {
|
|
7
10
|
const { inputText, insertionCursor, fileId } = req.body;
|
|
8
11
|
|
|
9
12
|
if (debug) {
|
|
@@ -16,17 +19,21 @@ export const handleAIAssist =
|
|
|
16
19
|
}
|
|
17
20
|
|
|
18
21
|
try {
|
|
22
|
+
// Generate a unique streamId for this request
|
|
23
|
+
const streamId = `stream_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
|
24
|
+
|
|
19
25
|
await generateAIResponse({
|
|
20
26
|
inputText,
|
|
21
27
|
insertionCursor,
|
|
22
28
|
fileId,
|
|
29
|
+
streamId,
|
|
23
30
|
shareDBDoc,
|
|
24
31
|
});
|
|
25
32
|
|
|
26
33
|
res
|
|
27
34
|
.status(200)
|
|
28
35
|
.send({ message: 'Operation successful!' });
|
|
29
|
-
} catch (error) {
|
|
36
|
+
} catch (error: any) {
|
|
30
37
|
console.error('handleAIAssist error:', error);
|
|
31
38
|
res.status(500).send({
|
|
32
39
|
message: 'Internal Server Error',
|
|
@@ -35,7 +42,7 @@ export const handleAIAssist =
|
|
|
35
42
|
}
|
|
36
43
|
};
|
|
37
44
|
|
|
38
|
-
export function haltGeneration(streamId) {
|
|
45
|
+
export function haltGeneration(streamId: string) {
|
|
39
46
|
const stream = streams[streamId];
|
|
40
47
|
|
|
41
48
|
// Stream can be undefined here if the user
|
|
@@ -4,7 +4,6 @@ import bodyParser from 'body-parser';
|
|
|
4
4
|
import express from 'express';
|
|
5
5
|
import fs from 'fs';
|
|
6
6
|
import http from 'http';
|
|
7
|
-
import ngrok from 'ngrok';
|
|
8
7
|
import open from 'open';
|
|
9
8
|
import path from 'path';
|
|
10
9
|
import ShareDB from 'sharedb';
|
|
@@ -51,13 +50,6 @@ ShareDB.types.register(json1Presence.type);
|
|
|
51
50
|
|
|
52
51
|
const app = express();
|
|
53
52
|
|
|
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
53
|
// Use ShareDB over WebSocket
|
|
62
54
|
const shareDBBackend = new ShareDB({
|
|
63
55
|
// Enable presence
|
|
@@ -82,7 +74,7 @@ wss.on('connection', (ws) => {
|
|
|
82
74
|
});
|
|
83
75
|
|
|
84
76
|
// Handle disconnections
|
|
85
|
-
ws.on('close', (
|
|
77
|
+
ws.on('close', () => {
|
|
86
78
|
clientStream.end();
|
|
87
79
|
});
|
|
88
80
|
});
|
|
@@ -97,6 +89,22 @@ app.use(express.static(dir));
|
|
|
97
89
|
// which is a representation of files on disk.
|
|
98
90
|
const shareDBConnection = shareDBBackend.connect();
|
|
99
91
|
const shareDBDoc = shareDBConnection.get('documents', '1');
|
|
92
|
+
|
|
93
|
+
// Set up presence for VizBot following the same pattern as useShareDB.ts
|
|
94
|
+
const docPresence = shareDBConnection.getDocPresence(
|
|
95
|
+
'documents',
|
|
96
|
+
'1',
|
|
97
|
+
);
|
|
98
|
+
|
|
99
|
+
// Create local presence for VizBot with a unique ID
|
|
100
|
+
const generateVizBotId = () => {
|
|
101
|
+
const timestamp = Date.now().toString(36);
|
|
102
|
+
return `vizbot-${timestamp}`;
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
const createVizBotLocalPresence = () =>
|
|
106
|
+
docPresence.create(generateVizBotId());
|
|
107
|
+
|
|
100
108
|
shareDBDoc.create(initialDocument, json1Presence.type.uri);
|
|
101
109
|
|
|
102
110
|
// Handle AI Assist requests.
|
|
@@ -117,7 +125,11 @@ app.post(
|
|
|
117
125
|
app.post(
|
|
118
126
|
'/ai-chat-message',
|
|
119
127
|
bodyParser.json(),
|
|
120
|
-
handleAIChatMessage(
|
|
128
|
+
handleAIChatMessage({
|
|
129
|
+
shareDBDoc,
|
|
130
|
+
createVizBotLocalPresence,
|
|
131
|
+
onCreditDeduction: undefined,
|
|
132
|
+
}),
|
|
121
133
|
);
|
|
122
134
|
|
|
123
135
|
// Livekit Token Generator
|
|
@@ -325,21 +337,13 @@ shareDBDoc.subscribe(() => {
|
|
|
325
337
|
});
|
|
326
338
|
|
|
327
339
|
server.listen(port, async () => {
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
}
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
// variable (for development) or the default port.
|
|
338
|
-
let livePort = process.env.EDITOR_PORT || port;
|
|
339
|
-
console.log(`EDITOR_PORT: ${process.env.EDITOR_PORT}`);
|
|
340
|
-
console.log(
|
|
341
|
-
`Editor is live at http://localhost:${livePort}`,
|
|
342
|
-
);
|
|
343
|
-
open(`http://localhost:${livePort}`);
|
|
344
|
-
}
|
|
340
|
+
// Note: ngrok support can be added when the package is properly installed
|
|
341
|
+
// Sets the port to the one specified in the environment
|
|
342
|
+
// variable (for development) or the default port.
|
|
343
|
+
let livePort = process.env.EDITOR_PORT || port;
|
|
344
|
+
console.log(`EDITOR_PORT: ${process.env.EDITOR_PORT}`);
|
|
345
|
+
console.log(
|
|
346
|
+
`Editor is live at http://localhost:${livePort}`,
|
|
347
|
+
);
|
|
348
|
+
open(`http://localhost:${livePort}`);
|
|
345
349
|
});
|
|
@@ -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
|
+
};
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { diff } from './client/diff';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Creates a submitOperation function that can be used to submit diff-based operations to ShareDB.
|
|
5
|
+
* This is the core logic extracted from useSubmitOperation for reuse across client and server.
|
|
6
|
+
*/
|
|
7
|
+
export const createSubmitOperation = <T>(shareDBDoc: {
|
|
8
|
+
data: T;
|
|
9
|
+
submitOp: (op: any) => void;
|
|
10
|
+
}): ((next: (data: T) => T) => void) => {
|
|
11
|
+
return (next) => {
|
|
12
|
+
const data: T = shareDBDoc.data;
|
|
13
|
+
const op = diff(data, next(data));
|
|
14
|
+
if (op && shareDBDoc) {
|
|
15
|
+
shareDBDoc.submitOp(op);
|
|
16
|
+
}
|
|
17
|
+
};
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Type definition for the submitOperation function
|
|
22
|
+
*/
|
|
23
|
+
export type SubmitOperationFunction<T> = (
|
|
24
|
+
next: (data: T) => T,
|
|
25
|
+
) => void;
|