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.
- package/dist/assets/bootstrap-icons-BeopsB42.woff +0 -0
- package/dist/assets/bootstrap-icons-mSm7cUeB.woff2 +0 -0
- package/dist/assets/{index-afP-wJTZ.css → index-Br76WttW.css} +5 -1
- package/dist/assets/{index-BJI4JLx9.js → index-EWyCE9d7.js} +142 -142
- package/dist/index.html +2 -2
- package/package.json +7 -6
- package/src/client/Icons/AdjustmentSVG.tsx +19 -0
- package/src/client/Icons/index.tsx +1 -0
- package/src/client/VZCodeContext/index.tsx +29 -0
- package/src/client/VZCodeContext/types.ts +247 -0
- package/src/client/{VZCodeContext.tsx → VZCodeContext/useVZCodeState.ts} +115 -294
- package/src/client/VZRight.tsx +6 -3
- package/src/client/VZSidebar/AIChat/ChatInput.tsx +45 -2
- package/src/client/VZSidebar/AIChat/MessageList.tsx +2 -0
- package/src/client/VZSidebar/AIChat/index.tsx +13 -1
- package/src/client/VZSidebar/AIChat/styles.scss +2 -0
- package/src/client/VZSidebar/EmptyState.tsx +11 -0
- package/src/client/VZSidebar/VisualEditor.tsx +284 -0
- package/src/client/VZSidebar/index.tsx +64 -0
- package/src/client/VZSidebar/styles.scss +213 -1
- package/src/client/bootstrap.ts +3 -0
- package/src/client/useActions.ts +12 -0
- package/src/client/vzReducer/closeTabReducer.test.ts +33 -21
- package/src/client/vzReducer/createInitialState.ts +1 -0
- package/src/client/vzReducer/index.ts +9 -0
- package/src/client/vzReducer/searchReducer.test.ts +1 -1
- package/src/client/vzReducer/visualEditorReducer.ts +9 -0
- package/src/server/aiChatHandler/chatOperations.ts +22 -0
- package/src/server/aiChatHandler/errorHandling.ts +17 -4
- package/src/server/aiChatHandler/index.ts +131 -67
- package/src/server/aiChatHandler/llmStreaming.ts +76 -27
- 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
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
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
|
-
|
|
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
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
-
//
|
|
629
|
-
|
|
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
|
};
|
package/src/client/VZRight.tsx
CHANGED
|
@@ -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 {
|
|
35
|
-
|
|
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
|
-
[
|
|
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);
|