vzcode 1.44.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.
Files changed (42) hide show
  1. package/dist/assets/{buildWorker-CRLuRnCr.js → buildWorker-C_tGhWXG.js} +135 -74
  2. package/dist/assets/index-CC0CUUDi.js +457 -0
  3. package/dist/assets/{index-Beqaj63x.css → index-CQyHWTWE.css} +1 -1
  4. package/dist/assets/worker-CekYspLa.js +453 -0
  5. package/dist/index.html +2 -2
  6. package/package.json +13 -7
  7. package/src/client/CodeEditor/{getOrCreateEditor.ts → getOrCreateEditor.tsx} +129 -1
  8. package/src/client/CodeEditor/index.tsx +11 -4
  9. package/src/client/Icons/SparklesSVG.tsx +9 -3
  10. package/src/client/RunCodeWidget/index.tsx +18 -15
  11. package/src/client/SplitPaneResizeContext.tsx +26 -108
  12. package/src/client/VZCodeContext.tsx +15 -1
  13. package/src/client/VZRight.tsx +32 -4
  14. package/src/client/VZSidebar/AIChat/ChatInput.tsx +80 -10
  15. package/src/client/VZSidebar/AIChat/DiffView.scss +154 -0
  16. package/src/client/VZSidebar/AIChat/DiffView.tsx +133 -0
  17. package/src/client/VZSidebar/AIChat/Message.tsx +24 -0
  18. package/src/client/VZSidebar/AIChat/MessageList.tsx +134 -12
  19. package/src/client/VZSidebar/AIChat/ThinkingScratchpad.tsx +37 -0
  20. package/src/client/VZSidebar/AIChat/index.tsx +155 -17
  21. package/src/client/VZSidebar/AIChat/styles.scss +223 -1
  22. package/src/client/VZSidebar/aiCopyPaste.ts +15 -2
  23. package/src/client/VZSidebar/index.tsx +10 -2
  24. package/src/client/bootstrap.ts +11 -1
  25. package/src/client/featureFlags.ts +2 -0
  26. package/src/client/useActions.ts +12 -0
  27. package/src/client/usePrettier/index.ts +1 -3
  28. package/src/client/vzReducer/aiChatReducer.ts +13 -0
  29. package/src/client/vzReducer/createInitialState.ts +1 -0
  30. package/src/client/vzReducer/index.ts +9 -0
  31. package/src/client/vzReducer/searchReducer.test.ts +44 -2
  32. package/src/runCode.ts +5 -17
  33. package/src/server/aiChatHandler/aiEditing.ts +56 -0
  34. package/src/server/aiChatHandler/chatOperations.ts +78 -1
  35. package/src/server/aiChatHandler/index.ts +33 -9
  36. package/src/server/aiChatHandler/llmStreaming.ts +101 -19
  37. package/src/server/computeInitialDocument.ts +25 -6
  38. package/src/server/generateAIResponse.ts +4 -0
  39. package/src/server/index.ts +83 -2
  40. package/src/utils/fileDiff.ts +114 -0
  41. package/dist/assets/index-C46Hp5ym.js +0 -340
  42. package/dist/assets/worker-DkYJN5WQ.js +0 -453
@@ -7,8 +7,12 @@ import {
7
7
  mergeFileChanges,
8
8
  } from 'editcodewithai';
9
9
  import { VizFiles } from '@vizhub/viz-types';
10
- import { vizFilesToFileCollection } from '@vizhub/viz-utils';
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,
@@ -81,8 +85,16 @@ export const createAICopyPasteHandlers = (
81
85
  return;
82
86
  }
83
87
 
88
+ // Preprocess to remove formatting instructions section to avoid creating extra files
89
+ const preprocessed = normalized
90
+ .split('## Formatting Instructions')[0]
91
+ .trim();
92
+
84
93
  // Parse the markdown files format
85
- const parsed = parseMarkdownFiles(normalized, 'bold');
94
+ const parsed = parseMarkdownFiles(
95
+ preprocessed,
96
+ 'bold',
97
+ );
86
98
 
87
99
  if (
88
100
  parsed.files &&
@@ -98,6 +110,7 @@ export const createAICopyPasteHandlers = (
98
110
  submitOperation((document) => ({
99
111
  ...document,
100
112
  files: mergedFiles,
113
+ runId: generateRunId(),
101
114
  }));
102
115
 
103
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 Chat (beta)</strong>
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 />
@@ -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 { Form, Button, Modal, OverlayTrigger, Tooltip };
15
+ export {
16
+ Form,
17
+ Button,
18
+ ButtonGroup,
19
+ ToggleButton,
20
+ Modal,
21
+ OverlayTrigger,
22
+ Tooltip,
23
+ };
@@ -2,3 +2,5 @@ export const enableLiveKit =
2
2
  import.meta.env.VITE_ENABLE_LIVEKIT === 'true';
3
3
 
4
4
  export const enableAIChat = true;
5
+
6
+ export const enableDiffView = true;
@@ -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
+ };
@@ -27,6 +27,7 @@ export const createInitialState = ({
27
27
  isSearchOpen: false,
28
28
  isAIChatOpen: false,
29
29
  aiChatFocused: false,
30
+ aiChatMode: 'edit',
30
31
  isSettingsOpen: false,
31
32
  isDocOpen: false,
32
33
  editorWantsFocus: false,
@@ -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 = (files: any) => ({
8
- data: { files },
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 flashing `isInteracting` to `true`.
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
- isInteracting: true,
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.
@@ -3,11 +3,51 @@ import {
3
3
  prepareFilesForPrompt,
4
4
  } from 'editcodewithai';
5
5
  import { formatMarkdownFiles } from 'llm-code-format';
6
+ import {
7
+ createFilesSnapshot,
8
+ generateFilesUnifiedDiff,
9
+ } from '../../utils/fileDiff.js';
6
10
 
7
11
  // Dev flag for waiting 1 second before starting the LLM function.
8
12
  // Useful for debugging and testing purposes, e.g. checking the typing indicator.
9
13
  const delayStart = false;
10
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
+
11
51
  /**
12
52
  * Performs AI editing operations using streaming with incremental OT operations
13
53
  */
@@ -17,6 +57,11 @@ export const performAIEditing = async ({
17
57
  llmFunction,
18
58
  runCode,
19
59
  }) => {
60
+ // 1. Capture the current state of files before editing
61
+ const beforeFiles = createFilesSnapshot(
62
+ shareDBDoc.data.files,
63
+ );
64
+
20
65
  const preparedFiles = prepareFilesForPrompt(
21
66
  shareDBDoc.data.files,
22
67
  );
@@ -40,8 +85,19 @@ export const performAIEditing = async ({
40
85
 
41
86
  runCode();
42
87
 
88
+ // 3. Capture the state of files after editing and generate diff
89
+ const afterFiles = createFilesSnapshot(
90
+ shareDBDoc.data.files,
91
+ );
92
+ const diffData = generateFilesUnifiedDiff(
93
+ beforeFiles,
94
+ afterFiles,
95
+ );
96
+
43
97
  return {
44
98
  content: result.content,
45
99
  generationId: result.generationId,
100
+ diffData,
101
+ beforeFiles, // Store the snapshot for undo functionality
46
102
  };
47
103
  };
@@ -1,4 +1,7 @@
1
- import { dateToTimestamp } from '@vizhub/viz-utils';
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';
@@ -246,6 +249,48 @@ export const finalizeAIMessage = (
246
249
  shareDBDoc.submitOp(op);
247
250
  };
248
251
 
252
+ /**
253
+ * Adds diff data to the most recent AI message
254
+ */
255
+ export const addDiffToAIMessage = (
256
+ shareDBDoc: ShareDBDoc<VizContent>,
257
+ chatId: VizChatId,
258
+ diffData: any,
259
+ beforeFiles?: VizFiles, // Optional snapshot for undo functionality
260
+ ) => {
261
+ const chat = shareDBDoc.data.chats[chatId];
262
+ const messages = [...chat.messages];
263
+
264
+ // Find the most recent AI message
265
+ const lastAIMessageIndex = messages.length - 1;
266
+ if (
267
+ lastAIMessageIndex >= 0 &&
268
+ messages[lastAIMessageIndex].role === 'assistant'
269
+ ) {
270
+ const newMessage = {
271
+ ...messages[lastAIMessageIndex],
272
+ diffData,
273
+ ...(beforeFiles && { beforeFiles }), // Add beforeFiles only if provided
274
+ };
275
+ // Use type assertion to extend the message with diffData
276
+ (messages[lastAIMessageIndex] as any) = newMessage;
277
+
278
+ const messageOp = diff(shareDBDoc.data, {
279
+ ...shareDBDoc.data,
280
+ chats: {
281
+ ...shareDBDoc.data.chats,
282
+ [chatId]: {
283
+ ...chat,
284
+ messages,
285
+ updatedAt: dateToTimestamp(new Date()),
286
+ },
287
+ },
288
+ });
289
+
290
+ shareDBDoc.submitOp(messageOp);
291
+ }
292
+ };
293
+
249
294
  /**
250
295
  * Adds an AI response message to the chat (legacy function, kept for compatibility)
251
296
  */
@@ -407,3 +452,35 @@ export const appendLineToFile = (
407
452
 
408
453
  shareDBDoc.submitOp(diff(shareDBDoc.data, newDocState));
409
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
+ };
@@ -3,9 +3,13 @@ import {
3
3
  ensureChatsExist,
4
4
  ensureChatExists,
5
5
  addUserMessage,
6
+ addDiffToAIMessage,
6
7
  } from './chatOperations.js';
7
8
  import { createLLMFunction } from './llmStreaming.js';
8
- import { performAIEditing } from './aiEditing.js';
9
+ import {
10
+ performAIEditing,
11
+ performAIChat,
12
+ } from './aiEditing.js';
9
13
  import { handleError } from './errorHandling.js';
10
14
  import { createRunCodeFunction } from '../../runCode.js';
11
15
  import { ShareDBDoc } from '../../types.js';
@@ -25,7 +29,7 @@ export const handleAIChatMessage =
25
29
  onCreditDeduction?: any;
26
30
  }) =>
27
31
  async (req: any, res: any) => {
28
- const { content, chatId } = req.body;
32
+ const { content, chatId, mode = 'edit' } = req.body;
29
33
 
30
34
  if (DEBUG) {
31
35
  console.log(
@@ -64,13 +68,33 @@ export const handleAIChatMessage =
64
68
  const runCode =
65
69
  createRunCodeFunction(submitOperation);
66
70
 
67
- // Perform AI editing
68
- const editResult = await performAIEditing({
69
- prompt: content,
70
- shareDBDoc,
71
- llmFunction,
72
- runCode,
73
- });
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
+ });
85
+
86
+ // Add diff data to the AI message if there are changes
87
+ if (
88
+ editResult.diffData &&
89
+ Object.keys(editResult.diffData).length > 0
90
+ ) {
91
+ addDiffToAIMessage(
92
+ shareDBDoc,
93
+ chatId,
94
+ editResult.diffData,
95
+ (editResult as any).beforeFiles, // Pass the beforeFiles snapshot for undo (only available for edit mode)
96
+ );
97
+ }
74
98
 
75
99
  // Handle credit deduction if callback is provided
76
100
  if (