vzcode 1.60.0 → 1.61.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 (30) hide show
  1. package/dist/assets/bootstrap-icons-BeopsB42.woff +0 -0
  2. package/dist/assets/bootstrap-icons-mSm7cUeB.woff2 +0 -0
  3. package/dist/assets/{index-DMIy-XE4.css → index-Br76WttW.css} +5 -1
  4. package/dist/assets/{index-S-R8oY72.js → index-EWyCE9d7.js} +148 -148
  5. package/dist/index.html +2 -2
  6. package/package.json +7 -6
  7. package/src/client/Icons/AdjustmentSVG.tsx +19 -0
  8. package/src/client/Icons/index.tsx +1 -0
  9. package/src/client/VZCodeContext/types.ts +12 -1
  10. package/src/client/VZCodeContext/useVZCodeState.ts +81 -5
  11. package/src/client/VZRight.tsx +6 -3
  12. package/src/client/VZSidebar/AIChat/ChatInput.tsx +45 -2
  13. package/src/client/VZSidebar/AIChat/MessageList.tsx +2 -0
  14. package/src/client/VZSidebar/AIChat/index.tsx +12 -0
  15. package/src/client/VZSidebar/EmptyState.tsx +11 -0
  16. package/src/client/VZSidebar/VisualEditor.tsx +284 -0
  17. package/src/client/VZSidebar/index.tsx +64 -0
  18. package/src/client/VZSidebar/styles.scss +213 -1
  19. package/src/client/bootstrap.ts +3 -0
  20. package/src/client/useActions.ts +12 -0
  21. package/src/client/vzReducer/closeTabReducer.test.ts +33 -21
  22. package/src/client/vzReducer/createInitialState.ts +1 -0
  23. package/src/client/vzReducer/index.ts +9 -0
  24. package/src/client/vzReducer/searchReducer.test.ts +1 -1
  25. package/src/client/vzReducer/visualEditorReducer.ts +9 -0
  26. package/src/server/aiChatHandler/chatOperations.ts +22 -0
  27. package/src/server/aiChatHandler/errorHandling.ts +17 -4
  28. package/src/server/aiChatHandler/index.ts +131 -67
  29. package/src/server/aiChatHandler/llmStreaming.ts +76 -27
  30. package/src/types.ts +12 -0
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-S-R8oY72.js"></script>
24
- <link rel="stylesheet" crossorigin href="/assets/index-DMIy-XE4.css">
23
+ <script type="module" crossorigin src="/assets/index-EWyCE9d7.js"></script>
24
+ <link rel="stylesheet" crossorigin href="/assets/index-Br76WttW.css">
25
25
  </head>
26
26
  <body>
27
27
  <div id="root"></div>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vzcode",
3
- "version": "1.60.0",
3
+ "version": "1.61.0",
4
4
  "description": "Multiplayer code editor system",
5
5
  "main": "src/index.ts",
6
6
  "type": "module",
@@ -11,7 +11,7 @@
11
11
  ],
12
12
  "scripts": {
13
13
  "test": "vitest run",
14
- "test-interactive": "cd test/sampleDirectories/kitchenSink && node --import \"data:text/javascript,import { register } from 'node:module'; import { pathToFileURL } from 'node:url'; register('ts-node/esm', pathToFileURL('./'));\" ../../../src/server/index.ts",
14
+ "test-interactive": "cd test/sampleDirectories/visualEditor && node --import \"data:text/javascript,import { register } from 'node:module'; import { pathToFileURL } from 'node:url'; register('ts-node/esm', pathToFileURL('./'));\" ../../../src/server/index.ts",
15
15
  "prettier": "prettier {*.*,**/*.*} --write",
16
16
  "typecheck": "tsc --noEmit",
17
17
  "dev": "cross-env EDITOR_PORT=5173 concurrently \"npm run test-interactive\" \"vite\"",
@@ -117,8 +117,8 @@
117
117
  "@codemirror/state": "^6.5.2",
118
118
  "@codemirror/theme-one-dark": "^6.1.3",
119
119
  "@codemirror/view": "^6.38.1",
120
- "@langchain/core": "^0.3.67",
121
- "@langchain/openai": "^0.6.4",
120
+ "@langchain/core": "^0.3.68",
121
+ "@langchain/openai": "^0.6.7",
122
122
  "@lezer/highlight": "^1.2.1",
123
123
  "@livekit/components-react": "^2.9.14",
124
124
  "@replit/codemirror-indentation-markers": "^6.5.3",
@@ -141,6 +141,7 @@
141
141
  "@vizhub/viz-types": "^0.4.0",
142
142
  "@vizhub/viz-utils": "^1.4.0",
143
143
  "body-parser": "^2.2.0",
144
+ "bootstrap-icons": "^1.13.1",
144
145
  "codemirror": "^6.0.2",
145
146
  "codemirror-copilot": "^0.0.7",
146
147
  "codemirror-ot": "^5.0.0",
@@ -166,7 +167,7 @@
166
167
  "react-bootstrap": "^2.10.10",
167
168
  "react-dom": "^18",
168
169
  "react-markdown": "^10.1.0",
169
- "react-router-dom": "^7.7.1",
170
+ "react-router-dom": "^7.8.0",
170
171
  "remark-gfm": "^4.0.1",
171
172
  "sharedb": "^5.2.2",
172
173
  "sharedb-client-browser": "^5.2.2",
@@ -189,7 +190,7 @@
189
190
  "sass": "^1.90.0",
190
191
  "ts-node": "^10.9.2",
191
192
  "typescript": "^5.9.2",
192
- "vite": "^7.1.0",
193
+ "vite": "^7.1.1",
193
194
  "vitest": "^3.2.4"
194
195
  },
195
196
  "optionalDependencies": {
@@ -0,0 +1,19 @@
1
+ //From Hero Icons
2
+ export const AdjustmentSVG = () => (
3
+ <svg
4
+ xmlns="http://www.w3.org/2000/svg"
5
+ fill="none"
6
+ viewBox="0 0 24 24"
7
+ strokeWidth={1.5}
8
+ stroke="currentColor"
9
+ className="size-6"
10
+ width={32}
11
+ height={32}
12
+ >
13
+ <path
14
+ strokeLinecap="round"
15
+ strokeLinejoin="round"
16
+ d="M10.5 6h9.75M10.5 6a1.5 1.5 0 1 1-3 0m3 0a1.5 1.5 0 1 0-3 0M3.75 6H7.5m3 12h9.75m-9.75 0a1.5 1.5 0 0 1-3 0m3 0a1.5 1.5 0 0 0-3 0m-3.75 0H7.5m9-6h3.75m-3.75 0a1.5 1.5 0 0 1-3 0m3 0a1.5 1.5 0 0 0-3 0m-9.75 0h9.75"
17
+ />
18
+ </svg>
19
+ );
@@ -21,3 +21,4 @@ export { HtmlSVG } from './HtmlSVG';
21
21
  export { CssSVG } from './CssSVG';
22
22
  export { JsonSVG } from './JsonSVG';
23
23
  export { MarkdownSVG } from './MarkdownSVG';
24
+ export { AdjustmentSVG } from './AdjustmentSVG';
@@ -90,6 +90,11 @@ export type VZCodeContextValue = {
90
90
  setIsSearchOpen: (isSearchOpen: boolean) => void;
91
91
  setSearch: (pattern: string) => void;
92
92
 
93
+ isVisualEditorOpen: boolean;
94
+ setIsVisualEditorOpen: (
95
+ isVisualEditorOpen: boolean,
96
+ ) => void;
97
+
93
98
  isAIChatOpen: boolean;
94
99
  setIsAIChatOpen: (isAIChatOpen: boolean) => void;
95
100
  aiChatFocused: boolean;
@@ -137,6 +142,8 @@ export type VZCodeContextValue = {
137
142
 
138
143
  codeEditorRef: React.RefObject<HTMLDivElement>;
139
144
 
145
+ iframeRef: React.MutableRefObject<HTMLIFrameElement>;
146
+
140
147
  connected: boolean;
141
148
  pending: boolean;
142
149
 
@@ -164,7 +171,6 @@ export type VZCodeContextValue = {
164
171
  aiChatMessage: string;
165
172
  setAIChatMessage: (message: string) => void;
166
173
  isLoading: boolean;
167
- setIsLoading: (state: boolean) => void;
168
174
  currentChatId: string;
169
175
  aiErrorMessage: string | null;
170
176
  setAIErrorMessage: (state: string | null) => void;
@@ -173,6 +179,11 @@ export type VZCodeContextValue = {
173
179
  options?: Record<string, string>,
174
180
  ) => void;
175
181
 
182
+ // Message history navigation
183
+ navigateMessageHistoryUp: () => void;
184
+ navigateMessageHistoryDown: () => void;
185
+ resetMessageHistoryNavigation: () => void;
186
+
176
187
  // Auto-fork functions for VizHub integration
177
188
  autoForkAndRetryAI?: (
178
189
  prompt: string,
@@ -1,5 +1,6 @@
1
1
  import {
2
2
  useCallback,
3
+ useMemo,
3
4
  useReducer,
4
5
  useRef,
5
6
  useState,
@@ -75,6 +76,8 @@ export const useVZCodeState = ({
75
76
 
76
77
  const codeEditorRef = useRef(null);
77
78
 
79
+ const iframeRef = useRef(null);
80
+
78
81
  // The error message shows errors in order of priority:
79
82
  // * `runtimeError` - errors from runtime execution, highest priority
80
83
  // * `prettierError` - errors from Prettier, client-side only
@@ -104,6 +107,7 @@ export const useVZCodeState = ({
104
107
  theme,
105
108
  search,
106
109
  isSearchOpen,
110
+ isVisualEditorOpen,
107
111
  isAIChatOpen,
108
112
  aiChatFocused,
109
113
  isSettingsOpen,
@@ -136,6 +140,7 @@ export const useVZCodeState = ({
136
140
  setSearchLineVisibility,
137
141
  setSearchFocusedIndex,
138
142
  toggleSearchFocused,
143
+ setIsVisualEditorOpen,
139
144
  setIsAIChatOpen,
140
145
  toggleAIChatFocused,
141
146
  setAIChatMode,
@@ -262,12 +267,72 @@ export const useVZCodeState = ({
262
267
 
263
268
  const DEBUG = false;
264
269
 
265
- const [isLoading, setIsLoading] = useState(false);
270
+ // Compute isLoading based on the current chat's aiStatus
266
271
  const [currentChatId] = useState(() => uuidv4());
272
+ const currentChat = content?.chats?.[currentChatId];
273
+ const isLoading = currentChat?.aiStatus === 'generating';
274
+
275
+ // Message history for up/down arrow navigation - using ShareDB document data
276
+ const messageHistory = useMemo(() => {
277
+ if (!currentChat?.messages) return [];
278
+ // Extract user messages in chronological order for history navigation
279
+ return currentChat.messages
280
+ .filter((msg) => msg.role === 'user')
281
+ .map((msg) => msg.content);
282
+ }, [currentChat?.messages]);
283
+
284
+ const [historyIndex, setHistoryIndex] = useState(-1);
285
+ const [currentDraft, setCurrentDraft] = useState('');
286
+
267
287
  const [aiErrorMessage, setAIErrorMessage] = useState<
268
288
  string | null
269
289
  >(null);
270
290
 
291
+ // Message history navigation functions
292
+ const navigateMessageHistoryUp = useCallback(() => {
293
+ if (messageHistory.length === 0) return;
294
+
295
+ // If we're not navigating history, save current draft
296
+ if (historyIndex === -1) {
297
+ setCurrentDraft(aiChatMessage);
298
+ }
299
+
300
+ const newIndex =
301
+ historyIndex < messageHistory.length - 1
302
+ ? historyIndex + 1
303
+ : historyIndex;
304
+ setHistoryIndex(newIndex);
305
+ setAIChatMessage(
306
+ messageHistory[messageHistory.length - 1 - newIndex],
307
+ );
308
+ }, [messageHistory, historyIndex, aiChatMessage]);
309
+
310
+ const navigateMessageHistoryDown = useCallback(() => {
311
+ if (historyIndex === -1) return;
312
+
313
+ const newIndex = historyIndex - 1;
314
+ if (newIndex === -1) {
315
+ // Return to draft
316
+ setHistoryIndex(-1);
317
+ setAIChatMessage(currentDraft);
318
+ setCurrentDraft('');
319
+ } else {
320
+ setHistoryIndex(newIndex);
321
+ setAIChatMessage(
322
+ messageHistory[
323
+ messageHistory.length - 1 - newIndex
324
+ ],
325
+ );
326
+ }
327
+ }, [messageHistory, historyIndex, currentDraft]);
328
+
329
+ const resetMessageHistoryNavigation = useCallback(() => {
330
+ if (historyIndex !== -1) {
331
+ setHistoryIndex(-1);
332
+ setCurrentDraft('');
333
+ }
334
+ }, [historyIndex]);
335
+
271
336
  const handleSendMessage = useCallback(
272
337
  async (
273
338
  messageToSend?: string,
@@ -293,7 +358,11 @@ export const useVZCodeState = ({
293
358
 
294
359
  const currentPrompt = aiChatMessage.trim();
295
360
  setAIChatMessage('');
296
- setIsLoading(true);
361
+
362
+ // Reset history navigation when sending a message
363
+ setHistoryIndex(-1);
364
+ setCurrentDraft('');
365
+
297
366
  setAIErrorMessage(null); // Clear any previous errors
298
367
 
299
368
  // Call backend endpoint for AI response
@@ -356,13 +425,12 @@ export const useVZCodeState = ({
356
425
  }
357
426
 
358
427
  // The backend handles all ShareDB operations for successful responses
428
+ // The loading state is now managed via ShareDB aiStatus
359
429
  } catch (error) {
360
430
  console.error('Error getting AI response:', error);
361
431
  setAIErrorMessage(
362
432
  'Failed to send message. Please try again.',
363
433
  );
364
- } finally {
365
- setIsLoading(false);
366
434
  }
367
435
  },
368
436
  [
@@ -415,6 +483,9 @@ export const useVZCodeState = ({
415
483
  setSearchFocusedIndex,
416
484
  toggleSearchFocused,
417
485
 
486
+ isVisualEditorOpen,
487
+ setIsVisualEditorOpen,
488
+
418
489
  isAIChatOpen,
419
490
  setIsAIChatOpen,
420
491
  aiChatFocused,
@@ -463,6 +534,7 @@ export const useVZCodeState = ({
463
534
  runCodeRef,
464
535
  sidebarRef,
465
536
  codeEditorRef,
537
+ iframeRef,
466
538
 
467
539
  connected,
468
540
  pending,
@@ -487,12 +559,16 @@ export const useVZCodeState = ({
487
559
  aiChatMessage,
488
560
  setAIChatMessage,
489
561
  isLoading,
490
- setIsLoading,
491
562
  currentChatId,
492
563
  aiErrorMessage,
493
564
  setAIErrorMessage,
494
565
  handleSendMessage,
495
566
 
567
+ // Message history navigation
568
+ navigateMessageHistoryUp,
569
+ navigateMessageHistoryDown,
570
+ resetMessageHistoryNavigation,
571
+
496
572
  // Auto-fork functions for VizHub integration
497
573
  autoForkAndRetryAI,
498
574
  clearStoredAIPrompt,
@@ -23,7 +23,6 @@ type ExtendedVizContent = VizContent & {
23
23
  const enableIframe = true;
24
24
 
25
25
  export const VZRight = () => {
26
- const iframeRef = useRef<HTMLIFrameElement>(null);
27
26
  const runtimeRef = useRef<VizHubRuntime | null>(null);
28
27
  const isFirstRunRef = useRef(true);
29
28
  const lastRunIdRef = useRef<string | undefined>(
@@ -31,8 +30,12 @@ export const VZRight = () => {
31
30
  );
32
31
 
33
32
  // Get access to the current files.
34
- const { content, handleRuntimeError, clearRuntimeError } =
35
- useContext(VZCodeContext);
33
+ const {
34
+ content,
35
+ handleRuntimeError,
36
+ clearRuntimeError,
37
+ iframeRef,
38
+ } = useContext(VZCodeContext);
36
39
 
37
40
  const files = useMemo(
38
41
  () =>
@@ -20,6 +20,9 @@ interface ChatInputProps {
20
20
  focused: boolean;
21
21
  aiChatMode: 'ask' | 'edit';
22
22
  setAIChatMode: (mode: 'ask' | 'edit') => void;
23
+ navigateMessageHistoryUp: () => void;
24
+ navigateMessageHistoryDown: () => void;
25
+ resetMessageHistoryNavigation: () => void;
23
26
  }
24
27
 
25
28
  const ChatInputComponent = ({
@@ -30,6 +33,9 @@ const ChatInputComponent = ({
30
33
  focused,
31
34
  aiChatMode,
32
35
  setAIChatMode,
36
+ navigateMessageHistoryUp,
37
+ navigateMessageHistoryDown,
38
+ resetMessageHistoryNavigation,
33
39
  }: ChatInputProps) => {
34
40
  const inputRef = useRef<HTMLTextAreaElement>(null);
35
41
 
@@ -45,9 +51,44 @@ const ChatInputComponent = ({
45
51
  if (event.key === 'Enter' && !event.shiftKey) {
46
52
  event.preventDefault();
47
53
  onSendMessage();
54
+ } else if (event.key === 'ArrowUp') {
55
+ // Only navigate history if cursor is at the beginning of the first line
56
+ const textarea =
57
+ event.target as HTMLTextAreaElement;
58
+ const { selectionStart, value } = textarea;
59
+ const lines = value
60
+ .substring(0, selectionStart)
61
+ .split('\n');
62
+
63
+ // Check if we're at the beginning of the first line
64
+ if (lines.length === 1 && selectionStart === 0) {
65
+ event.preventDefault();
66
+ navigateMessageHistoryUp();
67
+ }
68
+ } else if (event.key === 'ArrowDown') {
69
+ // Only navigate history if cursor is at the end of the last line
70
+ const textarea =
71
+ event.target as HTMLTextAreaElement;
72
+ const { selectionStart, value } = textarea;
73
+ const remainingText =
74
+ value.substring(selectionStart);
75
+ const remainingLines = remainingText.split('\n');
76
+
77
+ // Check if we're at the end of the last line
78
+ if (
79
+ remainingLines.length === 1 &&
80
+ remainingLines[0] === ''
81
+ ) {
82
+ event.preventDefault();
83
+ navigateMessageHistoryDown();
84
+ }
48
85
  }
49
86
  },
50
- [onSendMessage],
87
+ [
88
+ onSendMessage,
89
+ navigateMessageHistoryUp,
90
+ navigateMessageHistoryDown,
91
+ ],
51
92
  );
52
93
 
53
94
  const handleSendClick = useCallback(() => {
@@ -57,8 +98,10 @@ const ChatInputComponent = ({
57
98
  const handleChange = useCallback(
58
99
  (event: React.ChangeEvent<HTMLTextAreaElement>) => {
59
100
  setAIChatMessage(event.target.value);
101
+ // Reset history navigation when user starts typing
102
+ resetMessageHistoryNavigation();
60
103
  },
61
- [setAIChatMessage],
104
+ [setAIChatMessage, resetMessageHistoryNavigation],
62
105
  );
63
106
 
64
107
  return (
@@ -15,11 +15,13 @@ const MessageListComponent = ({
15
15
  isLoading,
16
16
  chatId, // Add chatId prop
17
17
  aiScratchpad, // Add aiScratchpad prop
18
+ aiStatus, // Add aiStatus prop
18
19
  }: {
19
20
  messages: VizChatMessage[];
20
21
  isLoading: boolean;
21
22
  chatId?: string; // Add chatId to the type
22
23
  aiScratchpad?: string; // Add aiScratchpad to the type
24
+ aiStatus?: string; // Add aiStatus to the type
23
25
  }) => {
24
26
  const messagesEndRef = useRef<HTMLDivElement>(null);
25
27
  const messagesContainerRef = useRef<HTMLDivElement>(null);
@@ -23,6 +23,9 @@ export const AIChat = () => {
23
23
  setAIChatMessage,
24
24
  handleSendMessage,
25
25
  setAIErrorMessage,
26
+ navigateMessageHistoryUp,
27
+ navigateMessageHistoryDown,
28
+ resetMessageHistoryNavigation,
26
29
  } = useContext(VZCodeContext);
27
30
 
28
31
  // Get current chat data from content
@@ -254,6 +257,15 @@ export const AIChat = () => {
254
257
  focused={aiChatFocused}
255
258
  aiChatMode={aiChatMode}
256
259
  setAIChatMode={setAIChatMode}
260
+ navigateMessageHistoryUp={
261
+ navigateMessageHistoryUp
262
+ }
263
+ navigateMessageHistoryDown={
264
+ navigateMessageHistoryDown
265
+ }
266
+ resetMessageHistoryNavigation={
267
+ resetMessageHistoryNavigation
268
+ }
257
269
  />
258
270
  </div>
259
271
  </div>
@@ -0,0 +1,11 @@
1
+ import React from 'react';
2
+
3
+ interface EmptyStateProps {
4
+ children: React.ReactNode;
5
+ }
6
+
7
+ export const EmptyState: React.FC<EmptyStateProps> = ({
8
+ children,
9
+ }) => {
10
+ return <div className="empty-state">{children}</div>;
11
+ };