vzcode 1.59.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 (32) 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-afP-wJTZ.css → index-Br76WttW.css} +5 -1
  4. package/dist/assets/{index-BJI4JLx9.js → index-EWyCE9d7.js} +142 -142
  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/index.tsx +29 -0
  10. package/src/client/VZCodeContext/types.ts +247 -0
  11. package/src/client/{VZCodeContext.tsx → VZCodeContext/useVZCodeState.ts} +115 -294
  12. package/src/client/VZRight.tsx +6 -3
  13. package/src/client/VZSidebar/AIChat/ChatInput.tsx +45 -2
  14. package/src/client/VZSidebar/AIChat/MessageList.tsx +2 -0
  15. package/src/client/VZSidebar/AIChat/index.tsx +13 -1
  16. package/src/client/VZSidebar/AIChat/styles.scss +2 -0
  17. package/src/client/VZSidebar/EmptyState.tsx +11 -0
  18. package/src/client/VZSidebar/VisualEditor.tsx +284 -0
  19. package/src/client/VZSidebar/index.tsx +64 -0
  20. package/src/client/VZSidebar/styles.scss +213 -1
  21. package/src/client/bootstrap.ts +3 -0
  22. package/src/client/useActions.ts +12 -0
  23. package/src/client/vzReducer/closeTabReducer.test.ts +33 -21
  24. package/src/client/vzReducer/createInitialState.ts +1 -0
  25. package/src/client/vzReducer/index.ts +9 -0
  26. package/src/client/vzReducer/searchReducer.test.ts +1 -1
  27. package/src/client/vzReducer/visualEditorReducer.ts +9 -0
  28. package/src/server/aiChatHandler/chatOperations.ts +22 -0
  29. package/src/server/aiChatHandler/errorHandling.ts +17 -4
  30. package/src/server/aiChatHandler/index.ts +131 -67
  31. package/src/server/aiChatHandler/llmStreaming.ts +76 -27
  32. package/src/types.ts +12 -0
@@ -1,237 +1,34 @@
1
1
  import {
2
- Context,
3
- createContext,
4
2
  useCallback,
3
+ useMemo,
5
4
  useReducer,
6
5
  useRef,
7
6
  useState,
8
7
  } from 'react';
8
+ import { VizContent } from '@vizhub/viz-types';
9
+ import { defaultTheme, useDynamicTheme } from '../themes';
10
+ import { useActions } from '../useActions';
11
+ import { useEditorCache } from '../useEditorCache';
12
+ import { useFileCRUD } from '../useFileCRUD';
13
+ import { useKeyboardShortcuts } from '../useKeyboardShortcuts';
14
+ import { useOpenDirectories } from '../useOpenDirectories';
15
+ import { usePrettier } from '../usePrettier';
16
+ import { useRunCode } from '../useRunCode';
17
+ import { useRuntimeError } from '../useRuntimeError';
18
+ import { useURLSync } from '../useURLSync';
9
19
  import {
10
- ItemId,
11
- LeafPane,
12
- Pane,
13
- PaneId,
14
- PresenceIndicator,
15
- SearchFileVisibility,
16
- SearchResults,
17
- ShareDBDoc,
18
- SubmitOperation,
19
- TabState,
20
- Username,
21
- } from '../types';
22
- import { VizFiles, VizContent } from '@vizhub/viz-types';
23
- import {
24
- ThemeLabel,
25
- defaultTheme,
26
- useDynamicTheme,
27
- } from './themes';
28
- import { useActions } from './useActions';
29
- import {
30
- EditorCache,
31
- useEditorCache,
32
- } from './useEditorCache';
33
- import { useFileCRUD } from './useFileCRUD';
34
- import { useKeyboardShortcuts } from './useKeyboardShortcuts';
35
- import { useOpenDirectories } from './useOpenDirectories';
36
- import { usePrettier } from './usePrettier';
37
- import { useRunCode } from './useRunCode';
38
- import { useRuntimeError } from './useRuntimeError';
39
- import { useURLSync } from './useURLSync';
40
- import { createInitialState, vzReducer } from './vzReducer';
41
- import { findPane } from './vzReducer/findPane';
42
- import { usePresenceAutoFollow } from './usePresenceAutoFollow';
20
+ createInitialState,
21
+ vzReducer,
22
+ } from '../vzReducer';
23
+ import { findPane } from '../vzReducer/findPane';
24
+ import { usePresenceAutoFollow } from '../usePresenceAutoFollow';
43
25
  import { v4 as uuidv4 } from 'uuid';
26
+ import {
27
+ VZCodeContextValue,
28
+ VZCodeProviderProps,
29
+ } from './types';
44
30
 
45
- // This context centralizes all the "smart" logic
46
- // to do with the application state. This includes
47
- // * Accessing and manipulating ShareDB data
48
- // * Centralized application state
49
- export const VZCodeContext =
50
- createContext<VZCodeContextValue>(null);
51
-
52
- // The type of the object provided by this context.
53
- export type VZCodeContextValue = {
54
- content: VizContent | null;
55
- shareDBDoc: ShareDBDoc<VizContent> | null;
56
- submitOperation: (
57
- next: (content: VizContent) => VizContent,
58
- ) => void;
59
- // TODO pull in this type from ShareDB if possible
60
- localPresence: any;
61
- // TODO pull in this type from ShareDB if possible
62
- docPresence: any;
63
-
64
- files: VizFiles | null;
65
- createFile: (fileName: string, text?: string) => void;
66
- renameFile: (fileId: string, fileName: string) => void;
67
- deleteFile: (fileId: string) => void;
68
- createDirectory: (
69
- fileName: string,
70
- text?: string,
71
- ) => void;
72
- renameDirectory: (
73
- directoryId: string,
74
- directoryOldName: string,
75
- directoryName: string,
76
- ) => void;
77
- deleteDirectory: (directoryId: string) => void;
78
-
79
- setActiveFileId: (fileId: string | null) => void;
80
- setActiveFileLeft: () => void;
81
- setActiveFileRight: () => void;
82
-
83
- activePaneId: PaneId;
84
- pane: Pane;
85
- activePane: LeafPane;
86
-
87
- openTab: (tabState: TabState) => void;
88
- closeTabs: (fileIds: string[]) => void;
89
-
90
- isSettingsOpen: boolean;
91
- setIsSettingsOpen: (isSettingsOpen: boolean) => void;
92
- closeSettings: () => void;
93
-
94
- isDocOpen: boolean;
95
- setIsDocOpen: (isDocOpen: boolean) => void;
96
- closeDoc: () => void;
97
-
98
- theme: ThemeLabel;
99
- setTheme: (theme: ThemeLabel) => void;
100
-
101
- username: Username;
102
- setUsername: (username: Username) => void;
103
-
104
- isDirectoryOpen: (path: string) => boolean;
105
- toggleDirectory: (path: string) => void;
106
-
107
- editorCache: EditorCache;
108
-
109
- // Whether the editor should be focused.
110
- editorWantsFocus: boolean;
111
- // Signals that the editor no longer wants focus.
112
- editorNoLongerWantsFocus: () => void;
113
-
114
- errorMessage: string | null;
115
-
116
- // Runtime error handling
117
- handleRuntimeError: (
118
- formattedErrorMessage: string,
119
- ) => void;
120
- clearRuntimeError: () => void;
121
-
122
- search: SearchResults;
123
- isSearchOpen: boolean;
124
- setIsSearchOpen: (isSearchOpen: boolean) => void;
125
- setSearch: (pattern: string) => void;
126
-
127
- isAIChatOpen: boolean;
128
- setIsAIChatOpen: (isAIChatOpen: boolean) => void;
129
- aiChatFocused: boolean;
130
- toggleAIChatFocused: () => void;
131
- aiChatMode: 'ask' | 'edit';
132
- setAIChatMode: (mode: 'ask' | 'edit') => void;
133
- aiChatEndpoint?: string;
134
- aiChatUndoEndpoint?: string;
135
- aiChatOptions?: { [key: string]: any };
136
- setSearchResults: (files: ShareDBDoc<VizContent>) => void;
137
- setSearchFileVisibility: (
138
- files: ShareDBDoc<VizContent>,
139
- id: string,
140
- visibility: SearchFileVisibility,
141
- ) => void;
142
- setSearchLineVisibility: (
143
- files: ShareDBDoc<VizContent>,
144
- id: string,
145
- line: number,
146
- ) => void;
147
- setSearchFocusedIndex: (
148
- focusedIndex: number,
149
- childIndex: number,
150
- ) => void;
151
- toggleSearchFocused: () => void;
152
-
153
- isCreateFileModalOpen: boolean;
154
- handleOpenCreateFileModal: () => void;
155
- handleCloseCreateFileModal: () => void;
156
- handleCreateFileClick: (newFileName: string) => void;
157
-
158
- isCreateDirModalOpen: boolean;
159
- handleOpenCreateDirModal: () => void;
160
- handleCloseCreateDirModal: () => void;
161
- handleCreateDirClick: (newFileName: string) => void;
162
-
163
- runPrettierRef: React.MutableRefObject<
164
- null | (() => void)
165
- >;
166
- runCodeRef: React.MutableRefObject<
167
- null | ((hardRerun?: boolean) => void)
168
- >;
169
-
170
- sidebarRef: React.RefObject<HTMLDivElement>;
171
-
172
- codeEditorRef: React.RefObject<HTMLDivElement>;
173
-
174
- connected: boolean;
175
- pending: boolean;
176
-
177
- hoveredItemId: ItemId | null;
178
- setHoveredItemId: (itemId: ItemId | null) => void;
179
-
180
- enableAutoFollow: boolean;
181
- toggleAutoFollow: () => void;
182
-
183
- updatePresenceIndicator: (
184
- presenceIndicator: PresenceIndicator,
185
- ) => void;
186
-
187
- sidebarPresenceIndicators: Array<PresenceIndicator>;
188
- splitCurrentPane: () => void;
189
-
190
- liveKitToken: string;
191
- setLiveKitToken: (state: string) => void;
192
- liveKitRoomName: string;
193
- setLiveKitRoom: (state: string) => void;
194
- liveKitConnection: boolean;
195
- setLiveKitConnection: (state: boolean) => void;
196
- voiceChatModalOpen: boolean;
197
- setVoiceChatModalOpen: (state: boolean) => void;
198
- aiChatMessage: string;
199
- setAIChatMessage: (message: string) => void;
200
- isLoading: boolean;
201
- setIsLoading: (state: boolean) => void;
202
- currentChatId: string;
203
- aiErrorMessage: string | null;
204
- setAIErrorMessage: (state: string | null) => void;
205
- handleSendMessage: (
206
- messageToSend?: string,
207
- options?: Record<string, string>,
208
- ) => void;
209
-
210
- // Auto-fork functions for VizHub integration
211
- autoForkAndRetryAI?: (
212
- prompt: string,
213
- modelName: string,
214
- commitId?: string,
215
- ) => Promise<void>;
216
- clearStoredAIPrompt?: () => void;
217
- getStoredAIPrompt?: () => {
218
- prompt: string;
219
- modelName: string;
220
- } | null;
221
-
222
- // Additional widgets that can be rendered in AI chat messages
223
- additionalWidgets?: React.ComponentType<{
224
- messageId: string;
225
- chatId: string;
226
- canUndo: boolean;
227
- handleSendMessage?: (
228
- messageToSend?: string,
229
- options?: Record<string, string>,
230
- ) => void;
231
- }>;
232
- };
233
-
234
- export const VZCodeProvider = ({
31
+ export const useVZCodeState = ({
235
32
  content,
236
33
  shareDBDoc,
237
34
  submitOperation,
@@ -239,7 +36,6 @@ export const VZCodeProvider = ({
239
36
  docPresence,
240
37
  prettierWorker,
241
38
  initialUsername,
242
- children,
243
39
  codeError = null,
244
40
  connected,
245
41
  pending,
@@ -256,44 +52,11 @@ export const VZCodeProvider = ({
256
52
  clearStoredAIPrompt,
257
53
  getStoredAIPrompt,
258
54
  additionalWidgets,
259
- }: {
260
- content: VizContent;
261
- shareDBDoc: ShareDBDoc<VizContent>;
262
- submitOperation: SubmitOperation;
263
- localPresence: any;
264
- docPresence: any;
265
- prettierWorker: Worker;
266
- initialUsername: Username;
267
- children: React.ReactNode;
268
- codeError?: string | null;
269
- connected?: boolean;
270
- pending?: boolean;
271
- liveKitToken?: string | undefined;
272
- setLiveKitToken?: (state: string) => void;
273
- liveKitRoomName?: string | undefined;
274
- setLiveKitRoom?: (state: string) => void;
275
- liveKitConnection?: boolean;
276
- setLiveKitConnection?: (state: boolean) => void;
277
- aiChatEndpoint?: string;
278
- aiChatUndoEndpoint?: string;
279
- aiChatOptions?: { [key: string]: any };
280
- autoForkAndRetryAI?: (
281
- prompt: string,
282
- modelName: string,
283
- commitId?: string,
284
- ) => Promise<void>;
285
- clearStoredAIPrompt?: () => void;
286
- getStoredAIPrompt?: () => {
287
- prompt: string;
288
- modelName: string;
289
- } | null;
290
- additionalWidgets?: React.ComponentType<{
291
- messageId: string;
292
- chatId: string;
293
- canUndo: boolean;
294
- }>;
295
- }) => {
296
- // Auto-run Pretter after local changes.
55
+ }: Omit<
56
+ VZCodeProviderProps,
57
+ 'children'
58
+ >): VZCodeContextValue => {
59
+ // Auto-run Prettier after local changes.
297
60
  const { prettierError, runPrettierRef } = usePrettier({
298
61
  shareDBDoc,
299
62
  submitOperation,
@@ -313,6 +76,8 @@ export const VZCodeProvider = ({
313
76
 
314
77
  const codeEditorRef = useRef(null);
315
78
 
79
+ const iframeRef = useRef(null);
80
+
316
81
  // The error message shows errors in order of priority:
317
82
  // * `runtimeError` - errors from runtime execution, highest priority
318
83
  // * `prettierError` - errors from Prettier, client-side only
@@ -336,14 +101,13 @@ export const VZCodeProvider = ({
336
101
  );
337
102
 
338
103
  // Unpack state.
339
- // print the state object to see what it contains
340
- // console.log('state: ', state);
341
104
  const {
342
105
  pane,
343
106
  activePaneId,
344
107
  theme,
345
108
  search,
346
109
  isSearchOpen,
110
+ isVisualEditorOpen,
347
111
  isAIChatOpen,
348
112
  aiChatFocused,
349
113
  isSettingsOpen,
@@ -355,7 +119,7 @@ export const VZCodeProvider = ({
355
119
  aiChatMode,
356
120
  } = state;
357
121
 
358
- const activePane: Pane = findPane(pane, activePaneId);
122
+ const activePane = findPane(pane, activePaneId);
359
123
  if (activePane.type !== 'leafPane') {
360
124
  // Should never happen
361
125
  throw new Error('Expected leafPane');
@@ -376,6 +140,7 @@ export const VZCodeProvider = ({
376
140
  setSearchLineVisibility,
377
141
  setSearchFocusedIndex,
378
142
  toggleSearchFocused,
143
+ setIsVisualEditorOpen,
379
144
  setIsAIChatOpen,
380
145
  toggleAIChatFocused,
381
146
  setAIChatMode,
@@ -400,12 +165,11 @@ export const VZCodeProvider = ({
400
165
  });
401
166
 
402
167
  // The set of open directories.
403
- // TODO move this into reducer/useActions
404
168
  const { isDirectoryOpen, toggleDirectory } =
405
169
  useOpenDirectories({ activePane, content });
406
170
 
407
171
  // Cache of CodeMirror editors by file id.
408
- const editorCache: EditorCache = useEditorCache();
172
+ const editorCache = useEditorCache();
409
173
 
410
174
  // Handle dynamic theme changes.
411
175
  useDynamicTheme(editorCache, theme);
@@ -467,13 +231,10 @@ export const VZCodeProvider = ({
467
231
  );
468
232
 
469
233
  // Isolate the files object from the document.
470
- const files: VizFiles | null = content
471
- ? content.files
472
- : null;
234
+ const files = content ? content.files : null;
473
235
 
474
236
  useKeyboardShortcuts({
475
237
  closeTabs,
476
- // TODO verify that this makes sense
477
238
  activeFileId: activePane.activeFileId,
478
239
  activePaneId,
479
240
  handleOpenCreateFileModal,
@@ -488,12 +249,9 @@ export const VZCodeProvider = ({
488
249
  });
489
250
 
490
251
  // Track the currently hovered file id.
491
- const [hoveredItemId, setHoveredItemId] =
492
- useState<ItemId | null>(null);
252
+ const [hoveredItemId, setHoveredItemId] = useState(null);
493
253
 
494
254
  // Handle presence-based auto-following
495
- // This hook manages opening tabs when presence is received on files
496
- // that are not currently open, independent of CodeMirror extensions
497
255
  usePresenceAutoFollow({
498
256
  docPresence,
499
257
  enableAutoFollow,
@@ -502,7 +260,6 @@ export const VZCodeProvider = ({
502
260
  });
503
261
 
504
262
  // Livekit Voice Chat Modal
505
-
506
263
  const [voiceChatModalOpen, setVoiceChatModalOpen] =
507
264
  useState(false);
508
265
 
@@ -510,12 +267,72 @@ export const VZCodeProvider = ({
510
267
 
511
268
  const DEBUG = false;
512
269
 
513
- const [isLoading, setIsLoading] = useState(false);
270
+ // Compute isLoading based on the current chat's aiStatus
514
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
+
515
287
  const [aiErrorMessage, setAIErrorMessage] = useState<
516
288
  string | null
517
289
  >(null);
518
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
+
519
336
  const handleSendMessage = useCallback(
520
337
  async (
521
338
  messageToSend?: string,
@@ -541,11 +358,14 @@ export const VZCodeProvider = ({
541
358
 
542
359
  const currentPrompt = aiChatMessage.trim();
543
360
  setAIChatMessage('');
544
- setIsLoading(true);
361
+
362
+ // Reset history navigation when sending a message
363
+ setHistoryIndex(-1);
364
+ setCurrentDraft('');
365
+
545
366
  setAIErrorMessage(null); // Clear any previous errors
546
367
 
547
368
  // Call backend endpoint for AI response
548
- // The server will handle all ShareDB operations including adding the user message
549
369
  try {
550
370
  const response = await fetch(aiChatEndpoint, {
551
371
  method: 'POST',
@@ -579,7 +399,7 @@ export const VZCodeProvider = ({
579
399
 
580
400
  // Check if this is the specific permission error that should trigger auto-fork
581
401
  if (
582
- aiErrorMessage ===
402
+ errorMessage ===
583
403
  'You do not have permission to use AI chat on this visualization. Only users with edit access can use this feature. Fork the viz to edit it.'
584
404
  ) {
585
405
  // Trigger auto-fork instead of showing error
@@ -600,18 +420,17 @@ export const VZCodeProvider = ({
600
420
  }
601
421
 
602
422
  // For other errors, show the error message
603
- setAIErrorMessage(aiErrorMessage);
423
+ setAIErrorMessage(errorMessage);
604
424
  return;
605
425
  }
606
426
 
607
427
  // The backend handles all ShareDB operations for successful responses
428
+ // The loading state is now managed via ShareDB aiStatus
608
429
  } catch (error) {
609
430
  console.error('Error getting AI response:', error);
610
431
  setAIErrorMessage(
611
432
  'Failed to send message. Please try again.',
612
433
  );
613
- } finally {
614
- setIsLoading(false);
615
434
  }
616
435
  },
617
436
  [
@@ -625,8 +444,8 @@ export const VZCodeProvider = ({
625
444
  ],
626
445
  );
627
446
 
628
- // The value provided by this context.
629
- const value: VZCodeContextValue = {
447
+ // Return the context value
448
+ return {
630
449
  content,
631
450
  shareDBDoc,
632
451
  submitOperation,
@@ -664,6 +483,9 @@ export const VZCodeProvider = ({
664
483
  setSearchFocusedIndex,
665
484
  toggleSearchFocused,
666
485
 
486
+ isVisualEditorOpen,
487
+ setIsVisualEditorOpen,
488
+
667
489
  isAIChatOpen,
668
490
  setIsAIChatOpen,
669
491
  aiChatFocused,
@@ -712,6 +534,7 @@ export const VZCodeProvider = ({
712
534
  runCodeRef,
713
535
  sidebarRef,
714
536
  codeEditorRef,
537
+ iframeRef,
715
538
 
716
539
  connected,
717
540
  pending,
@@ -736,12 +559,16 @@ export const VZCodeProvider = ({
736
559
  aiChatMessage,
737
560
  setAIChatMessage,
738
561
  isLoading,
739
- setIsLoading,
740
562
  currentChatId,
741
563
  aiErrorMessage,
742
564
  setAIErrorMessage,
743
565
  handleSendMessage,
744
566
 
567
+ // Message history navigation
568
+ navigateMessageHistoryUp,
569
+ navigateMessageHistoryDown,
570
+ resetMessageHistoryNavigation,
571
+
745
572
  // Auto-fork functions for VizHub integration
746
573
  autoForkAndRetryAI,
747
574
  clearStoredAIPrompt,
@@ -750,10 +577,4 @@ export const VZCodeProvider = ({
750
577
  // Additional widgets that can be rendered in AI chat messages
751
578
  additionalWidgets,
752
579
  };
753
-
754
- return (
755
- <VZCodeContext.Provider value={value}>
756
- {children}
757
- </VZCodeContext.Provider>
758
- );
759
580
  };
@@ -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);