vzcode 2.15.0 → 2.17.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/{bindings_wasm_bg-9w8E-TmY.wasm → bindings_wasm_bg-D9vNLG9F.wasm} +0 -0
- package/dist/assets/buildWorker-DL4wXpRr.js +695 -0
- package/dist/assets/index-BiPmUreW.js +474 -0
- package/dist/assets/{index-DLm5FQ0E.css → index-BvGPtSrr.css} +1 -1
- package/dist/assets/worker-RJ-cjbld.js +327 -0
- package/dist/index.html +2 -2
- package/dist/server/aiChatHandler/chatOperations.js +168 -35
- package/dist/server/aiChatHandler/index.js +11 -30
- package/dist/server/aiChatHandler/llmStreaming.js +105 -118
- package/package.json +11 -11
- package/src/client/AIAssist/AIAssistWidget/index.tsx +12 -1
- package/src/client/App/useShareDB.ts +1 -1
- package/src/client/CodeEditor/index.tsx +9 -10
- package/src/client/VZCodeContext/types.ts +3 -1
- package/src/client/VZCodeContext/useVZCodeState.ts +7 -5
- package/src/client/VZRight.tsx +10 -2
- package/src/client/VZSidebar/AIChat/ChatInput.tsx +1 -7
- package/src/client/VZSidebar/AIChat/DiffView.scss +1 -0
- package/src/client/VZSidebar/AIChat/DiffView.tsx +72 -29
- package/src/client/VZSidebar/AIChat/FileEditingIndicator.tsx +89 -0
- package/src/client/VZSidebar/AIChat/IndividualFileDiff.tsx +86 -0
- package/src/client/VZSidebar/AIChat/JumpToLatestButton.tsx +64 -0
- package/src/client/VZSidebar/AIChat/Message.tsx +124 -56
- package/src/client/VZSidebar/AIChat/MessageList.tsx +155 -114
- package/src/client/VZSidebar/AIChat/ThinkingScratchpad.tsx +4 -118
- package/src/client/VZSidebar/AIChat/index.tsx +59 -19
- package/src/client/VZSidebar/AIChat/styles.scss +190 -43
- package/src/client/VZSidebar/Item.tsx +2 -2
- package/src/client/VZSidebar/Search.tsx +13 -6
- package/src/client/VZSidebar/VisualEditor/VisualEditor.tsx +39 -23
- package/src/client/VZSidebar/VisualEditor/utils.ts +1 -1
- package/src/client/VZSidebar/index.tsx +12 -10
- package/src/client/VZSidebar/useDragAndDrop.tsx +225 -214
- package/src/client/featureFlags.ts +3 -0
- package/src/client/hooks/useAutoScroll.ts +328 -0
- package/src/client/tabsSearchParameters.ts +1 -17
- package/src/client/useFileCRUD.ts +5 -2
- package/src/client/useKeyboardShortcuts.ts +7 -0
- package/src/client/useOpenDirectories.ts +2 -1
- package/src/client/usePrettier/index.ts +1 -1
- package/src/client/useURLSync.ts +1 -0
- package/src/client/utils/scrollUtils.ts +170 -0
- package/src/client/vzReducer/searchReducer.ts +6 -3
- package/src/server/aiChatHandler/chatOperations.ts +224 -43
- package/src/server/aiChatHandler/index.ts +8 -48
- package/src/server/aiChatHandler/llmStreaming.ts +149 -170
- package/src/types.ts +81 -1
- package/dist/assets/buildWorker-Bi6wsfQk.js +0 -695
- package/dist/assets/index-BpUpNto4.js +0 -461
- package/dist/assets/worker-BSzbM0Uo.js +0 -330
- package/src/client/VZSidebar/AIChat/StreamingMessage.tsx +0 -35
package/dist/index.html
CHANGED
|
@@ -20,8 +20,8 @@
|
|
|
20
20
|
href="https://fonts.googleapis.com/css2?family=Poppins:wght@300;400;500;600;700&display=swap"
|
|
21
21
|
rel="stylesheet"
|
|
22
22
|
/>
|
|
23
|
-
<script type="module" crossorigin src="/assets/index-
|
|
24
|
-
<link rel="stylesheet" crossorigin href="/assets/index-
|
|
23
|
+
<script type="module" crossorigin src="/assets/index-BiPmUreW.js"></script>
|
|
24
|
+
<link rel="stylesheet" crossorigin href="/assets/index-BvGPtSrr.css">
|
|
25
25
|
</head>
|
|
26
26
|
<body>
|
|
27
27
|
<div id="root"></div>
|
|
@@ -60,10 +60,13 @@ export const addUserMessage = (shareDBDoc, chatId, content) => {
|
|
|
60
60
|
shareDBDoc.submitOp(userMessageOp);
|
|
61
61
|
return userMessage;
|
|
62
62
|
};
|
|
63
|
+
const DEBUG = false;
|
|
63
64
|
/**
|
|
64
65
|
* Updates AI status in the chat
|
|
65
66
|
*/
|
|
66
67
|
export const updateAIStatus = (shareDBDoc, chatId, status) => {
|
|
68
|
+
DEBUG &&
|
|
69
|
+
console.log(`ChatOperations: updateAIStatus called with status: "${status}" for chatId: ${chatId}`);
|
|
67
70
|
const op = diff(shareDBDoc.data, {
|
|
68
71
|
...shareDBDoc.data,
|
|
69
72
|
chats: {
|
|
@@ -74,7 +77,11 @@ export const updateAIStatus = (shareDBDoc, chatId, status) => {
|
|
|
74
77
|
},
|
|
75
78
|
},
|
|
76
79
|
});
|
|
80
|
+
DEBUG &&
|
|
81
|
+
console.log(`ChatOperations: Submitting operation for status update:`, op);
|
|
77
82
|
shareDBDoc.submitOp(op);
|
|
83
|
+
DEBUG &&
|
|
84
|
+
console.log(`ChatOperations: Status update operation submitted successfully`);
|
|
78
85
|
};
|
|
79
86
|
/**
|
|
80
87
|
* Updates AI scratchpad content
|
|
@@ -307,54 +314,180 @@ export const createNewFile = (shareDBDoc, fileName) => {
|
|
|
307
314
|
shareDBDoc.submitOp(op);
|
|
308
315
|
return newFileId;
|
|
309
316
|
};
|
|
317
|
+
// ============================================================================
|
|
318
|
+
// Streaming Chat Operations
|
|
319
|
+
// ============================================================================
|
|
310
320
|
/**
|
|
311
|
-
*
|
|
321
|
+
* Creates a streaming AI message with events array
|
|
312
322
|
*/
|
|
313
|
-
export const
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
323
|
+
export const createStreamingAIMessage = (shareDBDoc, chatId) => {
|
|
324
|
+
const aiMessage = {
|
|
325
|
+
id: `assistant-${Date.now()}`,
|
|
326
|
+
role: 'assistant',
|
|
327
|
+
content: '',
|
|
328
|
+
timestamp: dateToTimestamp(new Date()),
|
|
329
|
+
streamingEvents: [],
|
|
330
|
+
isProgressive: true,
|
|
331
|
+
};
|
|
332
|
+
const messageOp = diff(shareDBDoc.data, {
|
|
333
|
+
...shareDBDoc.data,
|
|
334
|
+
chats: {
|
|
335
|
+
...shareDBDoc.data.chats,
|
|
336
|
+
[chatId]: {
|
|
337
|
+
...shareDBDoc.data.chats[chatId],
|
|
338
|
+
messages: [
|
|
339
|
+
...shareDBDoc.data.chats[chatId].messages,
|
|
340
|
+
aiMessage,
|
|
341
|
+
],
|
|
342
|
+
updatedAt: dateToTimestamp(new Date()),
|
|
343
|
+
isStreaming: true,
|
|
344
|
+
},
|
|
345
|
+
},
|
|
346
|
+
});
|
|
347
|
+
shareDBDoc.submitOp(messageOp);
|
|
348
|
+
return aiMessage.id;
|
|
320
349
|
};
|
|
321
350
|
/**
|
|
322
|
-
*
|
|
351
|
+
* Adds a streaming event to the most recent AI message
|
|
323
352
|
*/
|
|
324
|
-
export const
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
353
|
+
export const addStreamingEvent = (shareDBDoc, chatId, event) => {
|
|
354
|
+
DEBUG &&
|
|
355
|
+
console.log(`ChatOperations: Adding streaming event:`, event);
|
|
356
|
+
const chat = shareDBDoc.data.chats[chatId];
|
|
357
|
+
const messages = [...chat.messages];
|
|
358
|
+
const lastMessageIndex = messages.length - 1;
|
|
359
|
+
if (lastMessageIndex >= 0 &&
|
|
360
|
+
messages[lastMessageIndex].role === 'assistant') {
|
|
361
|
+
const lastMessage = messages[lastMessageIndex];
|
|
362
|
+
const updatedEvents = [
|
|
363
|
+
...(lastMessage.streamingEvents || []),
|
|
364
|
+
event,
|
|
365
|
+
];
|
|
366
|
+
messages[lastMessageIndex] = {
|
|
367
|
+
...lastMessage,
|
|
368
|
+
streamingEvents: updatedEvents,
|
|
369
|
+
};
|
|
370
|
+
const messageOp = diff(shareDBDoc.data, {
|
|
329
371
|
...shareDBDoc.data,
|
|
330
|
-
|
|
331
|
-
...shareDBDoc.data.
|
|
332
|
-
[
|
|
333
|
-
...
|
|
334
|
-
|
|
372
|
+
chats: {
|
|
373
|
+
...shareDBDoc.data.chats,
|
|
374
|
+
[chatId]: {
|
|
375
|
+
...chat,
|
|
376
|
+
messages,
|
|
377
|
+
updatedAt: dateToTimestamp(new Date()),
|
|
335
378
|
},
|
|
336
379
|
},
|
|
337
|
-
};
|
|
338
|
-
|
|
339
|
-
shareDBDoc.submitOp(op);
|
|
380
|
+
});
|
|
381
|
+
shareDBDoc.submitOp(messageOp);
|
|
340
382
|
}
|
|
341
383
|
};
|
|
342
384
|
/**
|
|
343
|
-
*
|
|
385
|
+
* Updates streaming status for a chat
|
|
344
386
|
*/
|
|
345
|
-
export const
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
const
|
|
349
|
-
const newDocState = {
|
|
387
|
+
export const updateStreamingStatus = (shareDBDoc, chatId, status, isStreaming = true) => {
|
|
388
|
+
DEBUG &&
|
|
389
|
+
console.log(`ChatOperations: Updating streaming status: "${status}"`);
|
|
390
|
+
const op = diff(shareDBDoc.data, {
|
|
350
391
|
...shareDBDoc.data,
|
|
351
|
-
|
|
352
|
-
...shareDBDoc.data.
|
|
353
|
-
[
|
|
354
|
-
...
|
|
355
|
-
|
|
392
|
+
chats: {
|
|
393
|
+
...shareDBDoc.data.chats,
|
|
394
|
+
[chatId]: {
|
|
395
|
+
...shareDBDoc.data.chats[chatId],
|
|
396
|
+
currentStatus: status,
|
|
397
|
+
isStreaming,
|
|
398
|
+
updatedAt: dateToTimestamp(new Date()),
|
|
356
399
|
},
|
|
357
400
|
},
|
|
358
|
-
};
|
|
359
|
-
shareDBDoc.submitOp(
|
|
401
|
+
});
|
|
402
|
+
shareDBDoc.submitOp(op);
|
|
403
|
+
};
|
|
404
|
+
/**
|
|
405
|
+
* Finalizes streaming message and clears streaming state
|
|
406
|
+
*/
|
|
407
|
+
export const finalizeStreamingMessage = (shareDBDoc, chatId) => {
|
|
408
|
+
DEBUG &&
|
|
409
|
+
console.log(`ChatOperations: Finalizing streaming message`);
|
|
410
|
+
const chat = shareDBDoc.data.chats[chatId];
|
|
411
|
+
const messages = [...chat.messages];
|
|
412
|
+
const lastMessageIndex = messages.length - 1;
|
|
413
|
+
if (lastMessageIndex >= 0 &&
|
|
414
|
+
messages[lastMessageIndex].role === 'assistant') {
|
|
415
|
+
const lastMessage = messages[lastMessageIndex];
|
|
416
|
+
messages[lastMessageIndex] = {
|
|
417
|
+
...lastMessage,
|
|
418
|
+
isComplete: true,
|
|
419
|
+
};
|
|
420
|
+
const messageOp = diff(shareDBDoc.data, {
|
|
421
|
+
...shareDBDoc.data,
|
|
422
|
+
chats: {
|
|
423
|
+
...shareDBDoc.data.chats,
|
|
424
|
+
[chatId]: {
|
|
425
|
+
...chat,
|
|
426
|
+
messages,
|
|
427
|
+
currentStatus: 'Done',
|
|
428
|
+
isStreaming: false,
|
|
429
|
+
updatedAt: dateToTimestamp(new Date()),
|
|
430
|
+
},
|
|
431
|
+
},
|
|
432
|
+
});
|
|
433
|
+
shareDBDoc.submitOp(messageOp);
|
|
434
|
+
}
|
|
360
435
|
};
|
|
436
|
+
// /**
|
|
437
|
+
// * Ensures a file exists, creating it if necessary
|
|
438
|
+
// */
|
|
439
|
+
// export const ensureFileExists = (shareDBDoc, fileName) => {
|
|
440
|
+
// let fileId = resolveFileId(fileName, shareDBDoc);
|
|
441
|
+
// if (!fileId) {
|
|
442
|
+
// // File doesn't exist, create it
|
|
443
|
+
// fileId = createNewFile(shareDBDoc, fileName);
|
|
444
|
+
// }
|
|
445
|
+
// return fileId;
|
|
446
|
+
// };
|
|
447
|
+
// /**
|
|
448
|
+
// * Clears the content of a file
|
|
449
|
+
// */
|
|
450
|
+
// export const clearFileContent = (
|
|
451
|
+
// shareDBDoc: ShareDBDoc<VizContent>,
|
|
452
|
+
// fileId: VizFileId,
|
|
453
|
+
// ) => {
|
|
454
|
+
// const currentFile = shareDBDoc.data.files[fileId];
|
|
455
|
+
// if (currentFile && currentFile.text) {
|
|
456
|
+
// // Clear the file content
|
|
457
|
+
// const newState = {
|
|
458
|
+
// ...shareDBDoc.data,
|
|
459
|
+
// files: {
|
|
460
|
+
// ...shareDBDoc.data.files,
|
|
461
|
+
// [fileId]: {
|
|
462
|
+
// ...currentFile,
|
|
463
|
+
// text: '',
|
|
464
|
+
// },
|
|
465
|
+
// },
|
|
466
|
+
// };
|
|
467
|
+
// const op = diff(shareDBDoc.data, newState);
|
|
468
|
+
// shareDBDoc.submitOp(op);
|
|
469
|
+
// }
|
|
470
|
+
// };
|
|
471
|
+
// /**
|
|
472
|
+
// * Appends a line to a file using OT operations
|
|
473
|
+
// */
|
|
474
|
+
// export const appendLineToFile = (
|
|
475
|
+
// shareDBDoc: ShareDBDoc<VizContent>,
|
|
476
|
+
// fileId: VizFileId,
|
|
477
|
+
// line: string,
|
|
478
|
+
// ) => {
|
|
479
|
+
// const currentFile = shareDBDoc.data.files[fileId];
|
|
480
|
+
// const currentContent = currentFile?.text || '';
|
|
481
|
+
// const newContent = currentContent + line + '\n';
|
|
482
|
+
// const newDocState = {
|
|
483
|
+
// ...shareDBDoc.data,
|
|
484
|
+
// files: {
|
|
485
|
+
// ...shareDBDoc.data.files,
|
|
486
|
+
// [fileId]: {
|
|
487
|
+
// ...currentFile,
|
|
488
|
+
// text: newContent,
|
|
489
|
+
// },
|
|
490
|
+
// },
|
|
491
|
+
// };
|
|
492
|
+
// shareDBDoc.submitOp(diff(shareDBDoc.data, newDocState));
|
|
493
|
+
// };
|
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
import { validateRequest } from './validation.js';
|
|
2
|
-
import { ensureChatsExist, ensureChatExists, addUserMessage,
|
|
2
|
+
import { ensureChatsExist, ensureChatExists, addUserMessage, setAIStatus, } from './chatOperations.js';
|
|
3
3
|
import { createLLMFunction } from './llmStreaming.js';
|
|
4
|
-
import { performAIEditing
|
|
4
|
+
import { performAIEditing } from './aiEditing.js';
|
|
5
5
|
import { handleError, handleBackgroundError, } from './errorHandling.js';
|
|
6
6
|
import { createRunCodeFunction } from '../../runCode.js';
|
|
7
7
|
import { createSubmitOperation } from '../../submitOperation.js';
|
|
8
8
|
import { getGenerationMetadata } from 'editcodewithai';
|
|
9
9
|
const DEBUG = false;
|
|
10
|
-
export const handleAIChatMessage = ({ shareDBDoc, onCreditDeduction,
|
|
11
|
-
const { content, chatId
|
|
10
|
+
export const handleAIChatMessage = ({ shareDBDoc, onCreditDeduction, model, aiRequestOptions, }) => async (req, res) => {
|
|
11
|
+
const { content, chatId } = req.body;
|
|
12
12
|
if (DEBUG) {
|
|
13
13
|
console.log('[handleAIChatMessage] content:', content, 'chatId:', chatId, 'shareDBDoc:', shareDBDoc);
|
|
14
14
|
}
|
|
@@ -24,15 +24,11 @@ export const handleAIChatMessage = ({ shareDBDoc, onCreditDeduction, getCurrentC
|
|
|
24
24
|
addUserMessage(shareDBDoc, chatId, content);
|
|
25
25
|
// Return success immediately - AI generation continues in background
|
|
26
26
|
res.status(200).json('success');
|
|
27
|
-
// Set AI status to indicate generation is starting
|
|
28
|
-
setAIStatus(shareDBDoc, chatId, 'generating');
|
|
29
27
|
// Continue AI processing in background (don't await)
|
|
30
28
|
processAIRequestAsync({
|
|
31
29
|
shareDBDoc,
|
|
32
30
|
chatId,
|
|
33
31
|
content,
|
|
34
|
-
mode,
|
|
35
|
-
getCurrentCommitId,
|
|
36
32
|
model,
|
|
37
33
|
aiRequestOptions,
|
|
38
34
|
onCreditDeduction,
|
|
@@ -49,12 +45,8 @@ export const handleAIChatMessage = ({ shareDBDoc, onCreditDeduction, getCurrentC
|
|
|
49
45
|
/**
|
|
50
46
|
* Processes the AI request asynchronously in the background
|
|
51
47
|
*/
|
|
52
|
-
const processAIRequestAsync = async ({ shareDBDoc, chatId, content,
|
|
48
|
+
const processAIRequestAsync = async ({ shareDBDoc, chatId, content, model, aiRequestOptions, onCreditDeduction, }) => {
|
|
53
49
|
try {
|
|
54
|
-
// Capture the current commit ID before making changes (for VizHub integration)
|
|
55
|
-
const beforeCommitId = getCurrentCommitId
|
|
56
|
-
? getCurrentCommitId()
|
|
57
|
-
: null;
|
|
58
50
|
// Create LLM function for streaming
|
|
59
51
|
const llmFunction = createLLMFunction({
|
|
60
52
|
shareDBDoc,
|
|
@@ -66,23 +58,12 @@ const processAIRequestAsync = async ({ shareDBDoc, chatId, content, mode, getCur
|
|
|
66
58
|
const submitOperation = createSubmitOperation(shareDBDoc);
|
|
67
59
|
const runCode = createRunCodeFunction(submitOperation);
|
|
68
60
|
// Perform AI editing or chat based on mode
|
|
69
|
-
const editResult =
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
: await performAIEditing({
|
|
76
|
-
prompt: content,
|
|
77
|
-
shareDBDoc,
|
|
78
|
-
llmFunction,
|
|
79
|
-
runCode,
|
|
80
|
-
});
|
|
81
|
-
// Add diff data to the AI message if there are changes
|
|
82
|
-
if (editResult.diffData &&
|
|
83
|
-
Object.keys(editResult.diffData).length > 0) {
|
|
84
|
-
addDiffToAIMessage(shareDBDoc, chatId, editResult.diffData, beforeCommitId);
|
|
85
|
-
}
|
|
61
|
+
const editResult = await performAIEditing({
|
|
62
|
+
prompt: content,
|
|
63
|
+
shareDBDoc,
|
|
64
|
+
llmFunction,
|
|
65
|
+
runCode,
|
|
66
|
+
});
|
|
86
67
|
// Handle credit deduction if callback is provided
|
|
87
68
|
if (onCreditDeduction && editResult.generationId) {
|
|
88
69
|
try {
|
|
@@ -1,34 +1,12 @@
|
|
|
1
|
-
import fs from 'fs';
|
|
2
1
|
import OpenAI from 'openai';
|
|
3
2
|
import { parseMarkdownFiles, StreamingMarkdownParser, } from 'llm-code-format';
|
|
4
3
|
import { mergeFileChanges } from 'editcodewithai';
|
|
5
4
|
import { generateRunId } from '@vizhub/viz-utils';
|
|
6
|
-
import {
|
|
5
|
+
import { updateFiles, updateAIScratchpad, createStreamingAIMessage, addStreamingEvent, updateStreamingStatus, finalizeStreamingMessage, } from './chatOperations.js';
|
|
7
6
|
import { diff } from '../../ot.js';
|
|
8
7
|
const DEBUG = false;
|
|
9
8
|
// Useful for testing/debugging the streaming behavior
|
|
10
9
|
const slowMode = false;
|
|
11
|
-
// Throttle the streaming updates, so that we don't
|
|
12
|
-
// overwhelm the ShareDB server with too many updates.
|
|
13
|
-
// It happened actually, before adding this.
|
|
14
|
-
// MongoDB VizHub server got in fact overloaded with
|
|
15
|
-
// too many updates from the AI streaming response, with
|
|
16
|
-
// warning: "Replication Oplog Window has gone below 1 hour"
|
|
17
|
-
const THROTTLE_INTERVAL_MS = 500;
|
|
18
|
-
// Feature flag to enable/disable streaming editing.
|
|
19
|
-
// * If `true`, the AI streaming response will be used to
|
|
20
|
-
// edit files in real-time by submitting ShareDB ops.
|
|
21
|
-
// * If `false`, the updates to code files will be applied
|
|
22
|
-
// only after the AI has finished generating the entire response.
|
|
23
|
-
//
|
|
24
|
-
// Current status: there's a tricky bug with the streaming
|
|
25
|
-
// where the AI edits sometimes don't apply correctly in CodeMirror.
|
|
26
|
-
// It seems that sometimes the op that clears the file content
|
|
27
|
-
// is not applied correctly in the front end, leading to
|
|
28
|
-
// a situation where the AI streaming edits are concatenated into the middle
|
|
29
|
-
// of the file instead of replacing it.
|
|
30
|
-
// See https://github.com/codemirror/codemirror.next/issues/1234
|
|
31
|
-
const enableStreamingEditing = false;
|
|
32
10
|
/**
|
|
33
11
|
* Creates and configures the LLM function for streaming with reasoning tokens
|
|
34
12
|
*/
|
|
@@ -50,87 +28,92 @@ enableReasoningTokens = false, model, aiRequestOptions, }) => {
|
|
|
50
28
|
});
|
|
51
29
|
let fullContent = '';
|
|
52
30
|
let generationId = '';
|
|
53
|
-
let currentEditingFileId = null;
|
|
54
31
|
let currentEditingFileName = null;
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
//
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
latestContent = content;
|
|
68
|
-
const now = Date.now();
|
|
69
|
-
if (now - lastCall >= THROTTLE_INTERVAL_MS) {
|
|
70
|
-
// safe to call immediately
|
|
71
|
-
invoke();
|
|
72
|
-
}
|
|
73
|
-
else if (!timer) {
|
|
74
|
-
// schedule for later
|
|
75
|
-
timer = setTimeout(() => {
|
|
76
|
-
timer = null;
|
|
77
|
-
invoke();
|
|
78
|
-
}, THROTTLE_INTERVAL_MS - (now - lastCall));
|
|
32
|
+
let accumulatedTextChunk = '';
|
|
33
|
+
let currentFileContent = '';
|
|
34
|
+
// Create streaming AI message
|
|
35
|
+
createStreamingAIMessage(shareDBDoc, chatId);
|
|
36
|
+
// Set initial content generation status
|
|
37
|
+
updateStreamingStatus(shareDBDoc, chatId, 'Formulating a plan...');
|
|
38
|
+
// Helper to get original file content
|
|
39
|
+
const getOriginalFileContent = (fileName) => {
|
|
40
|
+
const files = shareDBDoc.data.files;
|
|
41
|
+
for (const file of Object.values(files)) {
|
|
42
|
+
if (file.name === fileName) {
|
|
43
|
+
return file.text || '';
|
|
79
44
|
}
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
45
|
+
}
|
|
46
|
+
return '';
|
|
47
|
+
};
|
|
48
|
+
// Helper to emit text chunk when accumulated
|
|
49
|
+
const emitTextChunk = async () => {
|
|
50
|
+
if (accumulatedTextChunk.trim()) {
|
|
51
|
+
DEBUG &&
|
|
52
|
+
console.log('LLMStreaming: Emitting text chunk:', accumulatedTextChunk.substring(0, 100) + '...');
|
|
53
|
+
await addStreamingEvent(shareDBDoc, chatId, {
|
|
54
|
+
type: 'text_chunk',
|
|
55
|
+
content: accumulatedTextChunk,
|
|
56
|
+
timestamp: Date.now(),
|
|
57
|
+
});
|
|
58
|
+
accumulatedTextChunk = '';
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
// Helper to complete file editing
|
|
62
|
+
const completeFileEditing = async (fileName) => {
|
|
63
|
+
if (fileName && currentFileContent) {
|
|
64
|
+
DEBUG &&
|
|
65
|
+
console.log(`LLMStreaming: Completing file editing for ${fileName}`);
|
|
66
|
+
await addStreamingEvent(shareDBDoc, chatId, {
|
|
67
|
+
type: 'file_complete',
|
|
68
|
+
fileName,
|
|
69
|
+
beforeContent: getOriginalFileContent(fileName),
|
|
70
|
+
afterContent: currentFileContent,
|
|
71
|
+
timestamp: Date.now(),
|
|
72
|
+
});
|
|
73
|
+
currentFileContent = '';
|
|
100
74
|
}
|
|
101
75
|
};
|
|
102
76
|
// Define callbacks for streaming parser
|
|
103
77
|
const callbacks = {
|
|
104
78
|
onFileNameChange: async (fileName, format) => {
|
|
105
79
|
DEBUG &&
|
|
106
|
-
console.log(`File changed to: ${fileName} (${format})`);
|
|
107
|
-
//
|
|
108
|
-
|
|
109
|
-
|
|
80
|
+
console.log(`LLMStreaming: File changed to: ${fileName} (${format})`);
|
|
81
|
+
// Emit any accumulated text chunk first
|
|
82
|
+
await emitTextChunk();
|
|
83
|
+
// Complete previous file if any
|
|
84
|
+
if (currentEditingFileName) {
|
|
85
|
+
await completeFileEditing(currentEditingFileName);
|
|
86
|
+
}
|
|
87
|
+
// Start new file
|
|
110
88
|
currentEditingFileName = fileName;
|
|
111
|
-
|
|
112
|
-
//
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
89
|
+
currentFileContent = '';
|
|
90
|
+
// Emit file start event
|
|
91
|
+
await addStreamingEvent(shareDBDoc, chatId, {
|
|
92
|
+
type: 'file_start',
|
|
93
|
+
fileName,
|
|
94
|
+
timestamp: Date.now(),
|
|
95
|
+
});
|
|
96
|
+
// Update status
|
|
97
|
+
updateStreamingStatus(shareDBDoc, chatId, `Editing ${fileName}...`);
|
|
116
98
|
},
|
|
117
99
|
onCodeLine: async (line) => {
|
|
118
100
|
DEBUG && console.log(`Code line: ${line}`);
|
|
119
|
-
//
|
|
120
|
-
|
|
121
|
-
// Apply OT operation for this line immediately
|
|
122
|
-
appendLineToFile(shareDBDoc, currentEditingFileId, line);
|
|
123
|
-
}
|
|
101
|
+
// Accumulate code content for the current file
|
|
102
|
+
currentFileContent += line + '\n';
|
|
124
103
|
},
|
|
125
104
|
onNonCodeLine: async (line) => {
|
|
126
|
-
|
|
127
|
-
//
|
|
128
|
-
// declaration and th
|
|
105
|
+
DEBUG && console.log(`Non-code line: ${line}`);
|
|
106
|
+
// Accumulate non-code content as text chunk
|
|
129
107
|
if (line.trim() !== '') {
|
|
130
|
-
|
|
108
|
+
accumulatedTextChunk += line + '\n';
|
|
109
|
+
// Update status for subsequent non-code chunks
|
|
110
|
+
if (firstNonCodeChunkProcessed) {
|
|
111
|
+
updateStreamingStatus(shareDBDoc, chatId, 'Describing changes...');
|
|
112
|
+
}
|
|
113
|
+
else {
|
|
114
|
+
firstNonCodeChunkProcessed = true;
|
|
115
|
+
}
|
|
131
116
|
}
|
|
132
|
-
fullContent += line + '\n';
|
|
133
|
-
throttledUpdateAIMessageContent(fullContent);
|
|
134
117
|
},
|
|
135
118
|
};
|
|
136
119
|
const parser = new StreamingMarkdownParser(callbacks);
|
|
@@ -158,6 +141,7 @@ enableReasoningTokens = false, model, aiRequestOptions, }) => {
|
|
|
158
141
|
const stream = await openRouterClient.chat.completions.create(requestConfig);
|
|
159
142
|
let reasoningStarted = false;
|
|
160
143
|
let contentStarted = false;
|
|
144
|
+
let firstNonCodeChunkProcessed = false;
|
|
161
145
|
for await (const chunk of stream) {
|
|
162
146
|
if (slowMode) {
|
|
163
147
|
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
@@ -167,28 +151,30 @@ enableReasoningTokens = false, model, aiRequestOptions, }) => {
|
|
|
167
151
|
// Handle reasoning tokens (thinking) - only if enabled
|
|
168
152
|
if (!reasoningStarted) {
|
|
169
153
|
reasoningStarted = true;
|
|
170
|
-
|
|
154
|
+
updateStreamingStatus(shareDBDoc, chatId, 'Thinking...');
|
|
171
155
|
}
|
|
172
156
|
reasoningContent += delta.reasoning;
|
|
173
157
|
updateAIScratchpad(shareDBDoc, chatId, reasoningContent);
|
|
174
158
|
}
|
|
175
159
|
else if (delta?.content) {
|
|
176
160
|
// Handle regular content tokens
|
|
177
|
-
if (
|
|
178
|
-
// Clear reasoning when content starts
|
|
161
|
+
if (!contentStarted) {
|
|
179
162
|
contentStarted = true;
|
|
180
|
-
|
|
181
|
-
|
|
163
|
+
if (reasoningStarted) {
|
|
164
|
+
// Clear reasoning when content starts
|
|
165
|
+
updateAIScratchpad(shareDBDoc, chatId, '');
|
|
166
|
+
}
|
|
167
|
+
// // Set initial content generation status
|
|
168
|
+
// updateStreamingStatus(
|
|
169
|
+
// shareDBDoc,
|
|
170
|
+
// chatId,
|
|
171
|
+
// 'Formulating a plan...',
|
|
172
|
+
// );
|
|
182
173
|
}
|
|
183
174
|
const chunkContent = delta.content;
|
|
184
175
|
chunks.push(chunkContent);
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
}
|
|
188
|
-
else {
|
|
189
|
-
fullContent += chunkContent;
|
|
190
|
-
throttledUpdateAIMessageContent(fullContent);
|
|
191
|
-
}
|
|
176
|
+
await parser.processChunk(chunkContent);
|
|
177
|
+
fullContent += chunkContent;
|
|
192
178
|
}
|
|
193
179
|
else if (chunk.usage) {
|
|
194
180
|
// Handle usage information
|
|
@@ -199,20 +185,16 @@ enableReasoningTokens = false, model, aiRequestOptions, }) => {
|
|
|
199
185
|
}
|
|
200
186
|
}
|
|
201
187
|
await parser.flushRemaining();
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
// Flush to ensure the final content is written immediately
|
|
208
|
-
throttledUpdateAIMessageContent.flush();
|
|
209
|
-
// Finalize the AI message by clearing temporary fields
|
|
210
|
-
finalizeAIMessage(shareDBDoc, chatId);
|
|
211
|
-
// If streaming editing is not enabled, we need to
|
|
212
|
-
// apply all the edits at once
|
|
213
|
-
if (!enableStreamingEditing) {
|
|
214
|
-
updateFiles(shareDBDoc, mergeFileChanges(shareDBDoc.data.files, parseMarkdownFiles(fullContent, 'bold').files));
|
|
188
|
+
// Emit any remaining text chunk
|
|
189
|
+
await emitTextChunk();
|
|
190
|
+
// Complete final file if any
|
|
191
|
+
if (currentEditingFileName) {
|
|
192
|
+
await completeFileEditing(currentEditingFileName);
|
|
215
193
|
}
|
|
194
|
+
// Apply all the edits at once
|
|
195
|
+
updateFiles(shareDBDoc, mergeFileChanges(shareDBDoc.data.files, parseMarkdownFiles(fullContent, 'bold').files));
|
|
196
|
+
// Finalize streaming message
|
|
197
|
+
await finalizeStreamingMessage(shareDBDoc, chatId);
|
|
216
198
|
// Generate a new runId to trigger a run when AI finishes editing
|
|
217
199
|
// This will trigger a re-run without hot reloading
|
|
218
200
|
const newRunId = generateRunId();
|
|
@@ -230,11 +212,16 @@ enableReasoningTokens = false, model, aiRequestOptions, }) => {
|
|
|
230
212
|
}
|
|
231
213
|
});
|
|
232
214
|
// Write chunks file for debugging
|
|
233
|
-
if (DEBUG) {
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
215
|
+
// if (DEBUG) {
|
|
216
|
+
// const chunksFileJSONpath = `./ai-chunks-${chatId}.json`;
|
|
217
|
+
// fs.writeFileSync(
|
|
218
|
+
// chunksFileJSONpath,
|
|
219
|
+
// JSON.stringify(chunks, null, 2),
|
|
220
|
+
// );
|
|
221
|
+
// console.log(
|
|
222
|
+
// `AI chunks written to ${chunksFileJSONpath}`,
|
|
223
|
+
// );
|
|
224
|
+
// }
|
|
238
225
|
return {
|
|
239
226
|
content: fullContent,
|
|
240
227
|
generationId: generationId,
|