vzcode 1.60.0 → 1.62.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-vbF7Aejt.js} +174 -174
  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 +13 -1
  10. package/src/client/VZCodeContext/useVZCodeState.ts +83 -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 +295 -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-vbF7Aejt.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.62.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,
@@ -233,4 +244,5 @@ export interface VZCodeProviderProps {
233
244
  chatId: string;
234
245
  canUndo: boolean;
235
246
  }>;
247
+ iframeRef?: React.MutableRefObject<HTMLIFrameElement>;
236
248
  }
@@ -1,5 +1,6 @@
1
1
  import {
2
2
  useCallback,
3
+ useMemo,
3
4
  useReducer,
4
5
  useRef,
5
6
  useState,
@@ -51,6 +52,7 @@ export const useVZCodeState = ({
51
52
  clearStoredAIPrompt,
52
53
  getStoredAIPrompt,
53
54
  additionalWidgets,
55
+ iframeRef: externalIframeRef,
54
56
  }: Omit<
55
57
  VZCodeProviderProps,
56
58
  'children'
@@ -75,6 +77,9 @@ export const useVZCodeState = ({
75
77
 
76
78
  const codeEditorRef = useRef(null);
77
79
 
80
+ // Use external iframeRef if provided, otherwise create our own
81
+ const iframeRef = externalIframeRef || useRef(null);
82
+
78
83
  // The error message shows errors in order of priority:
79
84
  // * `runtimeError` - errors from runtime execution, highest priority
80
85
  // * `prettierError` - errors from Prettier, client-side only
@@ -104,6 +109,7 @@ export const useVZCodeState = ({
104
109
  theme,
105
110
  search,
106
111
  isSearchOpen,
112
+ isVisualEditorOpen,
107
113
  isAIChatOpen,
108
114
  aiChatFocused,
109
115
  isSettingsOpen,
@@ -136,6 +142,7 @@ export const useVZCodeState = ({
136
142
  setSearchLineVisibility,
137
143
  setSearchFocusedIndex,
138
144
  toggleSearchFocused,
145
+ setIsVisualEditorOpen,
139
146
  setIsAIChatOpen,
140
147
  toggleAIChatFocused,
141
148
  setAIChatMode,
@@ -262,12 +269,72 @@ export const useVZCodeState = ({
262
269
 
263
270
  const DEBUG = false;
264
271
 
265
- const [isLoading, setIsLoading] = useState(false);
272
+ // Compute isLoading based on the current chat's aiStatus
266
273
  const [currentChatId] = useState(() => uuidv4());
274
+ const currentChat = content?.chats?.[currentChatId];
275
+ const isLoading = currentChat?.aiStatus === 'generating';
276
+
277
+ // Message history for up/down arrow navigation - using ShareDB document data
278
+ const messageHistory = useMemo(() => {
279
+ if (!currentChat?.messages) return [];
280
+ // Extract user messages in chronological order for history navigation
281
+ return currentChat.messages
282
+ .filter((msg) => msg.role === 'user')
283
+ .map((msg) => msg.content);
284
+ }, [currentChat?.messages]);
285
+
286
+ const [historyIndex, setHistoryIndex] = useState(-1);
287
+ const [currentDraft, setCurrentDraft] = useState('');
288
+
267
289
  const [aiErrorMessage, setAIErrorMessage] = useState<
268
290
  string | null
269
291
  >(null);
270
292
 
293
+ // Message history navigation functions
294
+ const navigateMessageHistoryUp = useCallback(() => {
295
+ if (messageHistory.length === 0) return;
296
+
297
+ // If we're not navigating history, save current draft
298
+ if (historyIndex === -1) {
299
+ setCurrentDraft(aiChatMessage);
300
+ }
301
+
302
+ const newIndex =
303
+ historyIndex < messageHistory.length - 1
304
+ ? historyIndex + 1
305
+ : historyIndex;
306
+ setHistoryIndex(newIndex);
307
+ setAIChatMessage(
308
+ messageHistory[messageHistory.length - 1 - newIndex],
309
+ );
310
+ }, [messageHistory, historyIndex, aiChatMessage]);
311
+
312
+ const navigateMessageHistoryDown = useCallback(() => {
313
+ if (historyIndex === -1) return;
314
+
315
+ const newIndex = historyIndex - 1;
316
+ if (newIndex === -1) {
317
+ // Return to draft
318
+ setHistoryIndex(-1);
319
+ setAIChatMessage(currentDraft);
320
+ setCurrentDraft('');
321
+ } else {
322
+ setHistoryIndex(newIndex);
323
+ setAIChatMessage(
324
+ messageHistory[
325
+ messageHistory.length - 1 - newIndex
326
+ ],
327
+ );
328
+ }
329
+ }, [messageHistory, historyIndex, currentDraft]);
330
+
331
+ const resetMessageHistoryNavigation = useCallback(() => {
332
+ if (historyIndex !== -1) {
333
+ setHistoryIndex(-1);
334
+ setCurrentDraft('');
335
+ }
336
+ }, [historyIndex]);
337
+
271
338
  const handleSendMessage = useCallback(
272
339
  async (
273
340
  messageToSend?: string,
@@ -293,7 +360,11 @@ export const useVZCodeState = ({
293
360
 
294
361
  const currentPrompt = aiChatMessage.trim();
295
362
  setAIChatMessage('');
296
- setIsLoading(true);
363
+
364
+ // Reset history navigation when sending a message
365
+ setHistoryIndex(-1);
366
+ setCurrentDraft('');
367
+
297
368
  setAIErrorMessage(null); // Clear any previous errors
298
369
 
299
370
  // Call backend endpoint for AI response
@@ -356,13 +427,12 @@ export const useVZCodeState = ({
356
427
  }
357
428
 
358
429
  // The backend handles all ShareDB operations for successful responses
430
+ // The loading state is now managed via ShareDB aiStatus
359
431
  } catch (error) {
360
432
  console.error('Error getting AI response:', error);
361
433
  setAIErrorMessage(
362
434
  'Failed to send message. Please try again.',
363
435
  );
364
- } finally {
365
- setIsLoading(false);
366
436
  }
367
437
  },
368
438
  [
@@ -415,6 +485,9 @@ export const useVZCodeState = ({
415
485
  setSearchFocusedIndex,
416
486
  toggleSearchFocused,
417
487
 
488
+ isVisualEditorOpen,
489
+ setIsVisualEditorOpen,
490
+
418
491
  isAIChatOpen,
419
492
  setIsAIChatOpen,
420
493
  aiChatFocused,
@@ -463,6 +536,7 @@ export const useVZCodeState = ({
463
536
  runCodeRef,
464
537
  sidebarRef,
465
538
  codeEditorRef,
539
+ iframeRef,
466
540
 
467
541
  connected,
468
542
  pending,
@@ -487,12 +561,16 @@ export const useVZCodeState = ({
487
561
  aiChatMessage,
488
562
  setAIChatMessage,
489
563
  isLoading,
490
- setIsLoading,
491
564
  currentChatId,
492
565
  aiErrorMessage,
493
566
  setAIErrorMessage,
494
567
  handleSendMessage,
495
568
 
569
+ // Message history navigation
570
+ navigateMessageHistoryUp,
571
+ navigateMessageHistoryDown,
572
+ resetMessageHistoryNavigation,
573
+
496
574
  // Auto-fork functions for VizHub integration
497
575
  autoForkAndRetryAI,
498
576
  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
+ };