vzcode 1.45.0 → 1.46.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-C1lTrzOZ.js → index-CC0CUUDi.js} +113 -113
- package/dist/assets/{index-DeeOcIzw.css → index-CQyHWTWE.css} +1 -1
- package/dist/index.html +2 -2
- package/package.json +7 -3
- package/src/client/CodeEditor/index.tsx +7 -4
- package/src/client/RunCodeWidget/index.tsx +18 -15
- package/src/client/SplitPaneResizeContext.tsx +26 -108
- package/src/client/VZCodeContext.tsx +8 -1
- package/src/client/VZRight.tsx +32 -4
- package/src/client/VZSidebar/AIChat/ChatInput.tsx +72 -2
- package/src/client/VZSidebar/AIChat/DiffView.scss +29 -0
- package/src/client/VZSidebar/AIChat/DiffView.tsx +62 -1
- package/src/client/VZSidebar/AIChat/Message.tsx +14 -1
- package/src/client/VZSidebar/AIChat/MessageList.tsx +134 -13
- package/src/client/VZSidebar/AIChat/ThinkingScratchpad.tsx +37 -0
- package/src/client/VZSidebar/AIChat/index.tsx +148 -12
- package/src/client/VZSidebar/AIChat/styles.scss +223 -1
- package/src/client/VZSidebar/aiCopyPaste.ts +6 -1
- package/src/client/VZSidebar/index.tsx +10 -2
- package/src/client/bootstrap.ts +11 -1
- package/src/client/useActions.ts +12 -0
- package/src/client/usePrettier/index.ts +1 -3
- package/src/client/vzReducer/aiChatReducer.ts +13 -0
- package/src/client/vzReducer/createInitialState.ts +1 -0
- package/src/client/vzReducer/index.ts +9 -0
- package/src/client/vzReducer/searchReducer.test.ts +44 -2
- package/src/runCode.ts +5 -17
- package/src/server/aiChatHandler/aiEditing.ts +37 -0
- package/src/server/aiChatHandler/chatOperations.ts +38 -1
- package/src/server/aiChatHandler/index.ts +20 -9
- package/src/server/aiChatHandler/llmStreaming.ts +101 -19
- package/src/server/generateAIResponse.ts +4 -0
- package/src/server/index.ts +52 -0
|
@@ -7,8 +7,12 @@ import {
|
|
|
7
7
|
mergeFileChanges,
|
|
8
8
|
} from 'editcodewithai';
|
|
9
9
|
import { VizFiles } from '@vizhub/viz-types';
|
|
10
|
-
import {
|
|
10
|
+
import {
|
|
11
|
+
generateRunId,
|
|
12
|
+
vizFilesToFileCollection,
|
|
13
|
+
} from '@vizhub/viz-utils';
|
|
11
14
|
import JSZip from 'jszip';
|
|
15
|
+
import { generate } from '@langchain/core/dist/utils/fast-json-patch';
|
|
12
16
|
|
|
13
17
|
export const createAICopyPasteHandlers = (
|
|
14
18
|
files: VizFiles,
|
|
@@ -106,6 +110,7 @@ export const createAICopyPasteHandlers = (
|
|
|
106
110
|
submitOperation((document) => ({
|
|
107
111
|
...document,
|
|
108
112
|
files: mergedFiles,
|
|
113
|
+
runId: generateRunId(),
|
|
109
114
|
}));
|
|
110
115
|
|
|
111
116
|
const fileCount = Object.keys(parsed.files).length;
|
|
@@ -110,7 +110,7 @@ export const VZSidebar = ({
|
|
|
110
110
|
),
|
|
111
111
|
aiChatToolTipText = (
|
|
112
112
|
<div>
|
|
113
|
-
<strong>AI
|
|
113
|
+
<strong>VizBot - AI Code Editor</strong>
|
|
114
114
|
</div>
|
|
115
115
|
),
|
|
116
116
|
}: {
|
|
@@ -184,7 +184,7 @@ export const VZSidebar = ({
|
|
|
184
184
|
[files, submitOperation],
|
|
185
185
|
);
|
|
186
186
|
|
|
187
|
-
const { sidebarWidth } = useContext(
|
|
187
|
+
const { sidebarWidth, setSidebarView } = useContext(
|
|
188
188
|
SplitPaneResizeContext,
|
|
189
189
|
);
|
|
190
190
|
|
|
@@ -282,6 +282,11 @@ export const VZSidebar = ({
|
|
|
282
282
|
previousPendingRef.current = pending;
|
|
283
283
|
}, [pending, connected, isConnecting]);
|
|
284
284
|
|
|
285
|
+
// Automatically adjust sidebar width when view changes
|
|
286
|
+
useEffect(() => {
|
|
287
|
+
setSidebarView(isAIChatOpen);
|
|
288
|
+
}, [isAIChatOpen, setSidebarView]);
|
|
289
|
+
|
|
285
290
|
return (
|
|
286
291
|
<div
|
|
287
292
|
className="vz-sidebar"
|
|
@@ -307,6 +312,7 @@ export const VZSidebar = ({
|
|
|
307
312
|
onClick={() => {
|
|
308
313
|
setIsSearchOpen(false);
|
|
309
314
|
setIsAIChatOpen(false);
|
|
315
|
+
setSidebarView(false); // Switch to files view
|
|
310
316
|
}}
|
|
311
317
|
>
|
|
312
318
|
<FolderSVG />
|
|
@@ -327,6 +333,7 @@ export const VZSidebar = ({
|
|
|
327
333
|
onClick={() => {
|
|
328
334
|
setIsSearchOpen(true);
|
|
329
335
|
setIsAIChatOpen(false);
|
|
336
|
+
setSidebarView(false); // Switch to files view (search uses same width as files)
|
|
330
337
|
}}
|
|
331
338
|
>
|
|
332
339
|
<SearchSVG />
|
|
@@ -348,6 +355,7 @@ export const VZSidebar = ({
|
|
|
348
355
|
onClick={() => {
|
|
349
356
|
setIsAIChatOpen(true);
|
|
350
357
|
setIsSearchOpen(false);
|
|
358
|
+
setSidebarView(true); // Switch to AI chat view
|
|
351
359
|
}}
|
|
352
360
|
>
|
|
353
361
|
<SparklesSVG />
|
package/src/client/bootstrap.ts
CHANGED
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
// Otherwise we get `Error [ERR_UNSUPPORTED_DIR_IMPORT]: Directory import`
|
|
4
4
|
import Form from 'react-bootstrap/cjs/Form.js';
|
|
5
5
|
import Button from 'react-bootstrap/cjs/Button.js';
|
|
6
|
+
import ButtonGroup from 'react-bootstrap/cjs/ButtonGroup.js';
|
|
7
|
+
import ToggleButton from 'react-bootstrap/cjs/ToggleButton.js';
|
|
6
8
|
import Modal from 'react-bootstrap/cjs/Modal.js';
|
|
7
9
|
import OverlayTrigger from 'react-bootstrap/cjs/OverlayTrigger.js';
|
|
8
10
|
import Tooltip from 'react-bootstrap/cjs/Tooltip.js';
|
|
@@ -10,4 +12,12 @@ import Tooltip from 'react-bootstrap/cjs/Tooltip.js';
|
|
|
10
12
|
// Pull in custom CSS from vizhub-ui.
|
|
11
13
|
import 'vizhub-ui/dist/vizhub-ui.css';
|
|
12
14
|
|
|
13
|
-
export {
|
|
15
|
+
export {
|
|
16
|
+
Form,
|
|
17
|
+
Button,
|
|
18
|
+
ButtonGroup,
|
|
19
|
+
ToggleButton,
|
|
20
|
+
Modal,
|
|
21
|
+
OverlayTrigger,
|
|
22
|
+
Tooltip,
|
|
23
|
+
};
|
package/src/client/useActions.ts
CHANGED
|
@@ -168,6 +168,17 @@ export const useActions = (
|
|
|
168
168
|
});
|
|
169
169
|
}, [dispatch]);
|
|
170
170
|
|
|
171
|
+
// Set the AI chat mode (ask or edit)
|
|
172
|
+
const setAIChatMode = useCallback(
|
|
173
|
+
(mode: 'ask' | 'edit') => {
|
|
174
|
+
dispatch({
|
|
175
|
+
type: 'set_ai_chat_mode',
|
|
176
|
+
mode,
|
|
177
|
+
});
|
|
178
|
+
},
|
|
179
|
+
[dispatch],
|
|
180
|
+
);
|
|
181
|
+
|
|
171
182
|
// True to show the settings modal.
|
|
172
183
|
const setIsSettingsOpen = useCallback(
|
|
173
184
|
(value: boolean) => {
|
|
@@ -251,6 +262,7 @@ export const useActions = (
|
|
|
251
262
|
toggleSearchFocused,
|
|
252
263
|
setIsAIChatOpen,
|
|
253
264
|
toggleAIChatFocused,
|
|
265
|
+
setAIChatMode,
|
|
254
266
|
setIsSettingsOpen,
|
|
255
267
|
setIsDocOpen,
|
|
256
268
|
closeSettings,
|
|
@@ -1,7 +1,5 @@
|
|
|
1
|
-
import { shouldTriggerRun } from '../shouldTriggerRun';
|
|
2
|
-
import { JSONOp } from '../../ot';
|
|
3
1
|
import { VizFileId, VizContent } from '@vizhub/viz-types';
|
|
4
|
-
import { ShareDBDoc } from '../../types';
|
|
2
|
+
import { JSONOp, ShareDBDoc } from '../../types';
|
|
5
3
|
import { useEffect, useRef, useState } from 'react';
|
|
6
4
|
|
|
7
5
|
// The time in milliseconds by which auto-saving is debounced.
|
|
@@ -29,3 +29,16 @@ export const toggleAIChatFocusedReducer = (
|
|
|
29
29
|
}
|
|
30
30
|
return state;
|
|
31
31
|
};
|
|
32
|
+
|
|
33
|
+
export const setAIChatModeReducer = (
|
|
34
|
+
state: VZState,
|
|
35
|
+
action: VZAction,
|
|
36
|
+
): VZState => {
|
|
37
|
+
if (action.type === 'set_ai_chat_mode') {
|
|
38
|
+
return {
|
|
39
|
+
...state,
|
|
40
|
+
aiChatMode: action.mode,
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
return state;
|
|
44
|
+
};
|
|
@@ -33,6 +33,7 @@ import {
|
|
|
33
33
|
import {
|
|
34
34
|
setIsAIChatOpenReducer,
|
|
35
35
|
toggleAIChatFocusedReducer,
|
|
36
|
+
setAIChatModeReducer,
|
|
36
37
|
} from './aiChatReducer';
|
|
37
38
|
import { toggleAutoFollowReducer } from './toggleAutoFollowReducer';
|
|
38
39
|
import { updatePresenceIndicatorReducer } from './updatePresenceIndicatorReducer';
|
|
@@ -62,6 +63,9 @@ export type VZState = {
|
|
|
62
63
|
// True if the AI chat input should focus on the next render.
|
|
63
64
|
aiChatFocused: boolean;
|
|
64
65
|
|
|
66
|
+
// The current AI chat mode: 'ask' or 'edit'
|
|
67
|
+
aiChatMode: 'ask' | 'edit';
|
|
68
|
+
|
|
65
69
|
// True to show the settings modal.
|
|
66
70
|
isSettingsOpen: boolean;
|
|
67
71
|
|
|
@@ -133,6 +137,10 @@ export type VZAction =
|
|
|
133
137
|
// * Toggles focused variable to trigger AI chat input focus
|
|
134
138
|
| { type: 'toggle_ai_chat_focused' }
|
|
135
139
|
|
|
140
|
+
// `set_ai_chat_mode`
|
|
141
|
+
// * Sets the AI chat mode (ask or edit)
|
|
142
|
+
| { type: 'set_ai_chat_mode'; mode: 'ask' | 'edit' }
|
|
143
|
+
|
|
136
144
|
// `set_search`
|
|
137
145
|
// * Sets the current search pattern
|
|
138
146
|
| { type: 'set_search'; value: string }
|
|
@@ -212,6 +220,7 @@ const reducers = [
|
|
|
212
220
|
setIsSearchOpenReducer,
|
|
213
221
|
setIsAIChatOpenReducer,
|
|
214
222
|
toggleAIChatFocusedReducer,
|
|
223
|
+
setAIChatModeReducer,
|
|
215
224
|
setIsSettingsOpenReducer,
|
|
216
225
|
setIsDocOpenReducer,
|
|
217
226
|
editorNoLongerWantsFocusReducer,
|
|
@@ -2,10 +2,52 @@ import { describe, expect, it } from 'vitest';
|
|
|
2
2
|
import { setSearchResultsReducer } from './searchReducer';
|
|
3
3
|
import { VZAction, VZState, createInitialState } from '.';
|
|
4
4
|
import { defaultTheme } from '../themes';
|
|
5
|
+
import { ShareDBDoc } from '../../types';
|
|
6
|
+
import { VizContent } from '@vizhub/viz-types';
|
|
5
7
|
|
|
6
8
|
// Mock ShareDBDoc for testing
|
|
7
|
-
const createMockShareDBDoc = (
|
|
8
|
-
|
|
9
|
+
const createMockShareDBDoc = (
|
|
10
|
+
files: any,
|
|
11
|
+
): ShareDBDoc<VizContent> => ({
|
|
12
|
+
data: { files, id: 'mock-id' },
|
|
13
|
+
ingestSnapshot: (snapshot: any, callback: any) => {
|
|
14
|
+
// No-op for testing
|
|
15
|
+
if (callback) callback();
|
|
16
|
+
},
|
|
17
|
+
subscribe: (callback: any) => {
|
|
18
|
+
// No-op for testing
|
|
19
|
+
if (callback) callback();
|
|
20
|
+
},
|
|
21
|
+
on: (
|
|
22
|
+
event: string,
|
|
23
|
+
callback: (op: any, source: boolean) => void,
|
|
24
|
+
) => {
|
|
25
|
+
// No-op for testing
|
|
26
|
+
},
|
|
27
|
+
off: (
|
|
28
|
+
event: string,
|
|
29
|
+
callback: (op: any, source: boolean) => void,
|
|
30
|
+
) => {
|
|
31
|
+
// No-op for testing
|
|
32
|
+
},
|
|
33
|
+
removeListener: (
|
|
34
|
+
event: string,
|
|
35
|
+
callback: (op: any, source: boolean) => void,
|
|
36
|
+
) => {
|
|
37
|
+
// No-op for testing
|
|
38
|
+
},
|
|
39
|
+
submitOp: (
|
|
40
|
+
op: any,
|
|
41
|
+
options?: any,
|
|
42
|
+
callback?: () => void,
|
|
43
|
+
) => {
|
|
44
|
+
// No-op for testing
|
|
45
|
+
if (callback) callback();
|
|
46
|
+
},
|
|
47
|
+
whenNothingPending: (callback: () => void) => {
|
|
48
|
+
// No-op for testing
|
|
49
|
+
if (callback) callback();
|
|
50
|
+
},
|
|
9
51
|
});
|
|
10
52
|
|
|
11
53
|
const initialState: VZState = createInitialState({
|
package/src/runCode.ts
CHANGED
|
@@ -1,30 +1,18 @@
|
|
|
1
|
+
import { generateRunId } from '@vizhub/viz-utils';
|
|
1
2
|
import { SubmitOperation } from './types';
|
|
2
3
|
import { VizContent } from '@vizhub/viz-types';
|
|
3
4
|
|
|
4
5
|
/**
|
|
5
|
-
* Creates a runCode function that triggers code execution by
|
|
6
|
+
* Creates a runCode function that triggers code execution by changing the `runId` in the content.
|
|
6
7
|
* This works for both client-side (with submitOperation) and server-side (with ShareDB document).
|
|
7
8
|
*/
|
|
8
|
-
export const createRunCodeFunction =
|
|
9
|
-
submitOperation: SubmitOperation
|
|
10
|
-
) => {
|
|
11
|
-
return () => {
|
|
12
|
-
// Use the unified submitOperation approach for both client and server
|
|
9
|
+
export const createRunCodeFunction =
|
|
10
|
+
(submitOperation: SubmitOperation) => () => {
|
|
13
11
|
submitOperation((content: VizContent) => ({
|
|
14
12
|
...content,
|
|
15
|
-
|
|
13
|
+
runId: generateRunId(),
|
|
16
14
|
}));
|
|
17
|
-
|
|
18
|
-
setTimeout(() => {
|
|
19
|
-
// This somewhat cryptic logic
|
|
20
|
-
// deletes the `isInteracting` property
|
|
21
|
-
// from the document.
|
|
22
|
-
submitOperation(
|
|
23
|
-
({ isInteracting, ...newDocument }) => newDocument,
|
|
24
|
-
);
|
|
25
|
-
}, 100);
|
|
26
15
|
};
|
|
27
|
-
};
|
|
28
16
|
|
|
29
17
|
/**
|
|
30
18
|
* Creates a runCodeRef object compatible with React refs.
|
|
@@ -12,6 +12,42 @@ import {
|
|
|
12
12
|
// Useful for debugging and testing purposes, e.g. checking the typing indicator.
|
|
13
13
|
const delayStart = false;
|
|
14
14
|
|
|
15
|
+
/**
|
|
16
|
+
* Performs AI chat without editing - just generates a response
|
|
17
|
+
*/
|
|
18
|
+
export const performAIChat = async ({
|
|
19
|
+
prompt,
|
|
20
|
+
shareDBDoc,
|
|
21
|
+
llmFunction,
|
|
22
|
+
}) => {
|
|
23
|
+
const preparedFiles = prepareFilesForPrompt(
|
|
24
|
+
shareDBDoc.data.files,
|
|
25
|
+
);
|
|
26
|
+
const filesContext = formatMarkdownFiles(preparedFiles);
|
|
27
|
+
|
|
28
|
+
// 2. Assemble the final prompt for Q&A mode
|
|
29
|
+
const fullPrompt = assembleFullPrompt({
|
|
30
|
+
filesContext,
|
|
31
|
+
prompt,
|
|
32
|
+
editFormat: 'whole',
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
if (delayStart) {
|
|
36
|
+
await new Promise((resolve) =>
|
|
37
|
+
setTimeout(resolve, 1000),
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Call the LLM function which will handle streaming but won't edit files
|
|
42
|
+
const result = await llmFunction(fullPrompt);
|
|
43
|
+
|
|
44
|
+
return {
|
|
45
|
+
content: result.content,
|
|
46
|
+
generationId: result.generationId,
|
|
47
|
+
diffData: {}, // No file changes in ask mode
|
|
48
|
+
};
|
|
49
|
+
};
|
|
50
|
+
|
|
15
51
|
/**
|
|
16
52
|
* Performs AI editing operations using streaming with incremental OT operations
|
|
17
53
|
*/
|
|
@@ -62,5 +98,6 @@ export const performAIEditing = async ({
|
|
|
62
98
|
content: result.content,
|
|
63
99
|
generationId: result.generationId,
|
|
64
100
|
diffData,
|
|
101
|
+
beforeFiles, // Store the snapshot for undo functionality
|
|
65
102
|
};
|
|
66
103
|
};
|
|
@@ -1,4 +1,7 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
dateToTimestamp,
|
|
3
|
+
generateRunId,
|
|
4
|
+
} from '@vizhub/viz-utils';
|
|
2
5
|
import { randomId } from '../../randomId.js';
|
|
3
6
|
import { ShareDBDoc } from '../../types.js';
|
|
4
7
|
import { diff } from '../../ot.js';
|
|
@@ -253,6 +256,7 @@ export const addDiffToAIMessage = (
|
|
|
253
256
|
shareDBDoc: ShareDBDoc<VizContent>,
|
|
254
257
|
chatId: VizChatId,
|
|
255
258
|
diffData: any,
|
|
259
|
+
beforeFiles?: VizFiles, // Optional snapshot for undo functionality
|
|
256
260
|
) => {
|
|
257
261
|
const chat = shareDBDoc.data.chats[chatId];
|
|
258
262
|
const messages = [...chat.messages];
|
|
@@ -266,6 +270,7 @@ export const addDiffToAIMessage = (
|
|
|
266
270
|
const newMessage = {
|
|
267
271
|
...messages[lastAIMessageIndex],
|
|
268
272
|
diffData,
|
|
273
|
+
...(beforeFiles && { beforeFiles }), // Add beforeFiles only if provided
|
|
269
274
|
};
|
|
270
275
|
// Use type assertion to extend the message with diffData
|
|
271
276
|
(messages[lastAIMessageIndex] as any) = newMessage;
|
|
@@ -447,3 +452,35 @@ export const appendLineToFile = (
|
|
|
447
452
|
|
|
448
453
|
shareDBDoc.submitOp(diff(shareDBDoc.data, newDocState));
|
|
449
454
|
};
|
|
455
|
+
|
|
456
|
+
/**
|
|
457
|
+
* Undoes the last AI edit by restoring files and removing the AI message
|
|
458
|
+
*/
|
|
459
|
+
export const undoAIEdit = (
|
|
460
|
+
shareDBDoc: ShareDBDoc<VizContent>,
|
|
461
|
+
chatId: VizChatId,
|
|
462
|
+
messageId: string,
|
|
463
|
+
beforeFiles: VizFiles,
|
|
464
|
+
) => {
|
|
465
|
+
const chat = shareDBDoc.data.chats[chatId];
|
|
466
|
+
const messages = chat.messages.filter(
|
|
467
|
+
(msg) => msg.id !== messageId,
|
|
468
|
+
);
|
|
469
|
+
|
|
470
|
+
const op = diff(shareDBDoc.data, {
|
|
471
|
+
...shareDBDoc.data,
|
|
472
|
+
files: beforeFiles,
|
|
473
|
+
chats: {
|
|
474
|
+
...shareDBDoc.data.chats,
|
|
475
|
+
[chatId]: {
|
|
476
|
+
...chat,
|
|
477
|
+
messages,
|
|
478
|
+
updatedAt: dateToTimestamp(new Date()),
|
|
479
|
+
},
|
|
480
|
+
},
|
|
481
|
+
// Trigger a re-run
|
|
482
|
+
runId: generateRunId(),
|
|
483
|
+
});
|
|
484
|
+
|
|
485
|
+
shareDBDoc.submitOp(op);
|
|
486
|
+
};
|
|
@@ -6,7 +6,10 @@ import {
|
|
|
6
6
|
addDiffToAIMessage,
|
|
7
7
|
} from './chatOperations.js';
|
|
8
8
|
import { createLLMFunction } from './llmStreaming.js';
|
|
9
|
-
import {
|
|
9
|
+
import {
|
|
10
|
+
performAIEditing,
|
|
11
|
+
performAIChat,
|
|
12
|
+
} from './aiEditing.js';
|
|
10
13
|
import { handleError } from './errorHandling.js';
|
|
11
14
|
import { createRunCodeFunction } from '../../runCode.js';
|
|
12
15
|
import { ShareDBDoc } from '../../types.js';
|
|
@@ -26,7 +29,7 @@ export const handleAIChatMessage =
|
|
|
26
29
|
onCreditDeduction?: any;
|
|
27
30
|
}) =>
|
|
28
31
|
async (req: any, res: any) => {
|
|
29
|
-
const { content, chatId } = req.body;
|
|
32
|
+
const { content, chatId, mode = 'edit' } = req.body;
|
|
30
33
|
|
|
31
34
|
if (DEBUG) {
|
|
32
35
|
console.log(
|
|
@@ -65,13 +68,20 @@ export const handleAIChatMessage =
|
|
|
65
68
|
const runCode =
|
|
66
69
|
createRunCodeFunction(submitOperation);
|
|
67
70
|
|
|
68
|
-
// Perform AI editing
|
|
69
|
-
const editResult =
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
71
|
+
// Perform AI editing or chat based on mode
|
|
72
|
+
const editResult =
|
|
73
|
+
mode === 'ask'
|
|
74
|
+
? await performAIChat({
|
|
75
|
+
prompt: content,
|
|
76
|
+
shareDBDoc,
|
|
77
|
+
llmFunction,
|
|
78
|
+
})
|
|
79
|
+
: await performAIEditing({
|
|
80
|
+
prompt: content,
|
|
81
|
+
shareDBDoc,
|
|
82
|
+
llmFunction,
|
|
83
|
+
runCode,
|
|
84
|
+
});
|
|
75
85
|
|
|
76
86
|
// Add diff data to the AI message if there are changes
|
|
77
87
|
if (
|
|
@@ -82,6 +92,7 @@ export const handleAIChatMessage =
|
|
|
82
92
|
shareDBDoc,
|
|
83
93
|
chatId,
|
|
84
94
|
editResult.diffData,
|
|
95
|
+
(editResult as any).beforeFiles, // Pass the beforeFiles snapshot for undo (only available for edit mode)
|
|
85
96
|
);
|
|
86
97
|
}
|
|
87
98
|
|
|
@@ -3,7 +3,9 @@ import {
|
|
|
3
3
|
StreamingMarkdownParser,
|
|
4
4
|
} from 'llm-code-format';
|
|
5
5
|
import { ChatOpenAI } from '@langchain/openai';
|
|
6
|
+
import OpenAI from 'openai';
|
|
6
7
|
import fs from 'fs';
|
|
8
|
+
import { generateRunId } from '@vizhub/viz-utils';
|
|
7
9
|
import {
|
|
8
10
|
updateAIStatus,
|
|
9
11
|
createAIMessage,
|
|
@@ -13,8 +15,12 @@ import {
|
|
|
13
15
|
clearFileContent,
|
|
14
16
|
appendLineToFile,
|
|
15
17
|
updateFiles,
|
|
18
|
+
updateAIScratchpad,
|
|
16
19
|
} from './chatOperations.js';
|
|
17
20
|
import { mergeFileChanges } from 'editcodewithai';
|
|
21
|
+
import { diff } from '../../ot.js';
|
|
22
|
+
import { VizChatId, VizContent } from '@vizhub/viz-types';
|
|
23
|
+
import { ShareDBDoc } from '../../types.js';
|
|
18
24
|
|
|
19
25
|
const DEBUG = false;
|
|
20
26
|
|
|
@@ -34,28 +40,30 @@ const DEBUG = false;
|
|
|
34
40
|
const enableStreamingEditing = false;
|
|
35
41
|
|
|
36
42
|
/**
|
|
37
|
-
* Creates and configures the LLM function for streaming
|
|
43
|
+
* Creates and configures the LLM function for streaming with reasoning tokens
|
|
38
44
|
*/
|
|
39
45
|
export const createLLMFunction = ({
|
|
40
46
|
shareDBDoc,
|
|
41
47
|
createVizBotLocalPresence,
|
|
42
48
|
chatId,
|
|
49
|
+
}: {
|
|
50
|
+
shareDBDoc: ShareDBDoc<VizContent>;
|
|
51
|
+
createVizBotLocalPresence: () => any;
|
|
52
|
+
chatId: VizChatId;
|
|
43
53
|
}) => {
|
|
44
54
|
return async (fullPrompt: string) => {
|
|
45
55
|
const localPresence = createVizBotLocalPresence();
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
},
|
|
56
|
+
|
|
57
|
+
// Create OpenRouter client for reasoning token support
|
|
58
|
+
const openRouterClient = new OpenAI({
|
|
59
|
+
apiKey: process.env.VIZHUB_EDIT_WITH_AI_API_KEY,
|
|
60
|
+
baseURL:
|
|
61
|
+
process.env.VIZHUB_EDIT_WITH_AI_BASE_URL ||
|
|
62
|
+
'https://openrouter.ai/api/v1',
|
|
63
|
+
defaultHeaders: {
|
|
64
|
+
'HTTP-Referer': 'https://vizhub.com',
|
|
65
|
+
'X-Title': 'VizHub',
|
|
57
66
|
},
|
|
58
|
-
streaming: true,
|
|
59
67
|
});
|
|
60
68
|
|
|
61
69
|
let fullContent = '';
|
|
@@ -177,12 +185,58 @@ export const createLLMFunction = ({
|
|
|
177
185
|
const parser = new StreamingMarkdownParser(callbacks);
|
|
178
186
|
|
|
179
187
|
const chunks = [];
|
|
188
|
+
let reasoningContent = '';
|
|
189
|
+
|
|
190
|
+
// Stream the response with reasoning tokens
|
|
191
|
+
const modelName =
|
|
192
|
+
process.env.VIZHUB_EDIT_WITH_AI_MODEL_NAME ||
|
|
193
|
+
'anthropic/claude-3.5-sonnet';
|
|
194
|
+
const stream = await (
|
|
195
|
+
openRouterClient.chat.completions.create as any
|
|
196
|
+
)({
|
|
197
|
+
model: modelName,
|
|
198
|
+
messages: [{ role: 'user', content: fullPrompt }],
|
|
199
|
+
max_tokens: 8192,
|
|
200
|
+
reasoning: {
|
|
201
|
+
effort: 'medium',
|
|
202
|
+
exclude: false,
|
|
203
|
+
},
|
|
204
|
+
usage: { include: true },
|
|
205
|
+
stream: true,
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
let reasoningStarted = false;
|
|
209
|
+
let contentStarted = false;
|
|
180
210
|
|
|
181
|
-
// Stream the response
|
|
182
|
-
const stream = await chatModel.stream(fullPrompt);
|
|
183
211
|
for await (const chunk of stream) {
|
|
184
|
-
|
|
185
|
-
|
|
212
|
+
const delta = chunk.choices[0]?.delta as any; // Type assertion for OpenRouter-specific reasoning fields
|
|
213
|
+
|
|
214
|
+
if (delta?.reasoning) {
|
|
215
|
+
// Handle reasoning tokens (thinking)
|
|
216
|
+
if (!reasoningStarted) {
|
|
217
|
+
reasoningStarted = true;
|
|
218
|
+
updateAIStatus(shareDBDoc, chatId, 'Thinking...');
|
|
219
|
+
}
|
|
220
|
+
reasoningContent += delta.reasoning;
|
|
221
|
+
updateAIScratchpad(
|
|
222
|
+
shareDBDoc,
|
|
223
|
+
chatId,
|
|
224
|
+
reasoningContent,
|
|
225
|
+
);
|
|
226
|
+
} else if (delta?.content) {
|
|
227
|
+
// Handle regular content tokens
|
|
228
|
+
if (reasoningStarted && !contentStarted) {
|
|
229
|
+
// Clear reasoning when content starts
|
|
230
|
+
contentStarted = true;
|
|
231
|
+
updateAIScratchpad(shareDBDoc, chatId, '');
|
|
232
|
+
updateAIStatus(
|
|
233
|
+
shareDBDoc,
|
|
234
|
+
chatId,
|
|
235
|
+
'Generating response...',
|
|
236
|
+
);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
const chunkContent = delta.content;
|
|
186
240
|
chunks.push(chunkContent);
|
|
187
241
|
|
|
188
242
|
if (enableStreamingEditing) {
|
|
@@ -196,14 +250,20 @@ export const createLLMFunction = ({
|
|
|
196
250
|
fullContent,
|
|
197
251
|
);
|
|
198
252
|
}
|
|
253
|
+
} else if (chunk.usage) {
|
|
254
|
+
// Handle usage information
|
|
255
|
+
DEBUG && console.log('Usage:', chunk.usage);
|
|
199
256
|
}
|
|
200
257
|
|
|
201
|
-
if (!generationId && chunk.
|
|
202
|
-
generationId = chunk.
|
|
258
|
+
if (!generationId && chunk.id) {
|
|
259
|
+
generationId = chunk.id;
|
|
203
260
|
}
|
|
204
261
|
}
|
|
205
262
|
await parser.flushRemaining();
|
|
206
263
|
reportFileEdited();
|
|
264
|
+
|
|
265
|
+
// Final cleanup - clear scratchpad and set final status
|
|
266
|
+
updateAIScratchpad(shareDBDoc, chatId, '');
|
|
207
267
|
updateAIStatus(shareDBDoc, chatId, 'Done editing.');
|
|
208
268
|
updateAIMessageContent(
|
|
209
269
|
shareDBDoc,
|
|
@@ -242,6 +302,28 @@ export const createLLMFunction = ({
|
|
|
242
302
|
);
|
|
243
303
|
}
|
|
244
304
|
|
|
305
|
+
// Generate a new runId to trigger a run when AI finishes editing
|
|
306
|
+
// This will trigger a re-run without hot reloading
|
|
307
|
+
const newRunId = generateRunId();
|
|
308
|
+
const runIdOp = diff(shareDBDoc.data, {
|
|
309
|
+
...shareDBDoc.data,
|
|
310
|
+
runId: newRunId,
|
|
311
|
+
});
|
|
312
|
+
shareDBDoc.submitOp(runIdOp, (error) => {
|
|
313
|
+
if (error) {
|
|
314
|
+
console.warn(
|
|
315
|
+
'Error setting runId after AI editing:',
|
|
316
|
+
error,
|
|
317
|
+
);
|
|
318
|
+
} else {
|
|
319
|
+
DEBUG &&
|
|
320
|
+
console.log(
|
|
321
|
+
'Set new runId after AI editing:',
|
|
322
|
+
newRunId,
|
|
323
|
+
);
|
|
324
|
+
}
|
|
325
|
+
});
|
|
326
|
+
|
|
245
327
|
// Write chunks file for debugging
|
|
246
328
|
if (DEBUG) {
|
|
247
329
|
const chunksFileJSONpath = `./ai-chunks-${chatId}.json`;
|
|
@@ -150,6 +150,10 @@ export const generateAIResponse = async ({
|
|
|
150
150
|
// model: 'gpt-3.5-turbo',
|
|
151
151
|
model: 'gpt-4o',
|
|
152
152
|
messages,
|
|
153
|
+
reasoning: {
|
|
154
|
+
effort: 'medium',
|
|
155
|
+
exclude: false,
|
|
156
|
+
} as any, // Type assertion for OpenRouter-specific reasoning parameter
|
|
153
157
|
stream: true,
|
|
154
158
|
});
|
|
155
159
|
|