vzcode 1.58.0 → 1.60.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/{buildWorker-C_tGhWXG.js → buildWorker-Dsx6rOdK.js} +1 -1
- package/dist/assets/{index-CE_xP06t.css → index-DMIy-XE4.css} +1 -1
- package/dist/assets/{index-CtGus8Qk.js → index-S-R8oY72.js} +116 -116
- package/dist/assets/{worker-BrwN8AXx.js → worker-BSzbM0Uo.js} +180 -180
- package/dist/assets/{worker-CekYspLa.js → worker-y5jhqCTR.js} +1 -1
- package/dist/index.html +2 -2
- package/package.json +5 -5
- package/src/client/VZCodeContext/index.tsx +29 -0
- package/src/client/VZCodeContext/types.ts +236 -0
- package/src/client/{VZCodeContext.tsx → VZCodeContext/useVZCodeState.ts} +39 -284
- package/src/client/VZSidebar/AIChat/Message.tsx +2 -0
- package/src/client/VZSidebar/AIChat/MessageList.tsx +0 -2
- package/src/client/VZSidebar/AIChat/ThinkingScratchpad.tsx +121 -2
- package/src/client/VZSidebar/AIChat/index.tsx +2 -15
- package/src/client/VZSidebar/AIChat/styles.scss +18 -0
- package/src/server/aiChatHandler/index.ts +3 -0
- package/src/server/aiChatHandler/llmStreaming.ts +3 -0
|
@@ -1,230 +1,33 @@
|
|
|
1
1
|
import {
|
|
2
|
-
Context,
|
|
3
|
-
createContext,
|
|
4
2
|
useCallback,
|
|
5
3
|
useReducer,
|
|
6
4
|
useRef,
|
|
7
5
|
useState,
|
|
8
6
|
} from 'react';
|
|
7
|
+
import { VizContent } from '@vizhub/viz-types';
|
|
8
|
+
import { defaultTheme, useDynamicTheme } from '../themes';
|
|
9
|
+
import { useActions } from '../useActions';
|
|
10
|
+
import { useEditorCache } from '../useEditorCache';
|
|
11
|
+
import { useFileCRUD } from '../useFileCRUD';
|
|
12
|
+
import { useKeyboardShortcuts } from '../useKeyboardShortcuts';
|
|
13
|
+
import { useOpenDirectories } from '../useOpenDirectories';
|
|
14
|
+
import { usePrettier } from '../usePrettier';
|
|
15
|
+
import { useRunCode } from '../useRunCode';
|
|
16
|
+
import { useRuntimeError } from '../useRuntimeError';
|
|
17
|
+
import { useURLSync } from '../useURLSync';
|
|
9
18
|
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';
|
|
19
|
+
createInitialState,
|
|
20
|
+
vzReducer,
|
|
21
|
+
} from '../vzReducer';
|
|
22
|
+
import { findPane } from '../vzReducer/findPane';
|
|
23
|
+
import { usePresenceAutoFollow } from '../usePresenceAutoFollow';
|
|
43
24
|
import { v4 as uuidv4 } from 'uuid';
|
|
25
|
+
import {
|
|
26
|
+
VZCodeContextValue,
|
|
27
|
+
VZCodeProviderProps,
|
|
28
|
+
} from './types';
|
|
44
29
|
|
|
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: (messageToSend?: string) => void;
|
|
206
|
-
|
|
207
|
-
// Auto-fork functions for VizHub integration
|
|
208
|
-
autoForkAndRetryAI?: (
|
|
209
|
-
prompt: string,
|
|
210
|
-
modelName: string,
|
|
211
|
-
commitId?: string,
|
|
212
|
-
) => Promise<void>;
|
|
213
|
-
clearStoredAIPrompt?: () => void;
|
|
214
|
-
getStoredAIPrompt?: () => {
|
|
215
|
-
prompt: string;
|
|
216
|
-
modelName: string;
|
|
217
|
-
} | null;
|
|
218
|
-
|
|
219
|
-
// Additional widgets that can be rendered in AI chat messages
|
|
220
|
-
additionalWidgets?: React.ComponentType<{
|
|
221
|
-
messageId: string;
|
|
222
|
-
chatId: string;
|
|
223
|
-
canUndo: boolean;
|
|
224
|
-
}>;
|
|
225
|
-
};
|
|
226
|
-
|
|
227
|
-
export const VZCodeProvider = ({
|
|
30
|
+
export const useVZCodeState = ({
|
|
228
31
|
content,
|
|
229
32
|
shareDBDoc,
|
|
230
33
|
submitOperation,
|
|
@@ -232,7 +35,6 @@ export const VZCodeProvider = ({
|
|
|
232
35
|
docPresence,
|
|
233
36
|
prettierWorker,
|
|
234
37
|
initialUsername,
|
|
235
|
-
children,
|
|
236
38
|
codeError = null,
|
|
237
39
|
connected,
|
|
238
40
|
pending,
|
|
@@ -249,44 +51,11 @@ export const VZCodeProvider = ({
|
|
|
249
51
|
clearStoredAIPrompt,
|
|
250
52
|
getStoredAIPrompt,
|
|
251
53
|
additionalWidgets,
|
|
252
|
-
}:
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
docPresence: any;
|
|
258
|
-
prettierWorker: Worker;
|
|
259
|
-
initialUsername: Username;
|
|
260
|
-
children: React.ReactNode;
|
|
261
|
-
codeError?: string | null;
|
|
262
|
-
connected?: boolean;
|
|
263
|
-
pending?: boolean;
|
|
264
|
-
liveKitToken?: string | undefined;
|
|
265
|
-
setLiveKitToken?: (state: string) => void;
|
|
266
|
-
liveKitRoomName?: string | undefined;
|
|
267
|
-
setLiveKitRoom?: (state: string) => void;
|
|
268
|
-
liveKitConnection?: boolean;
|
|
269
|
-
setLiveKitConnection?: (state: boolean) => void;
|
|
270
|
-
aiChatEndpoint?: string;
|
|
271
|
-
aiChatUndoEndpoint?: string;
|
|
272
|
-
aiChatOptions?: { [key: string]: any };
|
|
273
|
-
autoForkAndRetryAI?: (
|
|
274
|
-
prompt: string,
|
|
275
|
-
modelName: string,
|
|
276
|
-
commitId?: string,
|
|
277
|
-
) => Promise<void>;
|
|
278
|
-
clearStoredAIPrompt?: () => void;
|
|
279
|
-
getStoredAIPrompt?: () => {
|
|
280
|
-
prompt: string;
|
|
281
|
-
modelName: string;
|
|
282
|
-
} | null;
|
|
283
|
-
additionalWidgets?: React.ComponentType<{
|
|
284
|
-
messageId: string;
|
|
285
|
-
chatId: string;
|
|
286
|
-
canUndo: boolean;
|
|
287
|
-
}>;
|
|
288
|
-
}) => {
|
|
289
|
-
// Auto-run Pretter after local changes.
|
|
54
|
+
}: Omit<
|
|
55
|
+
VZCodeProviderProps,
|
|
56
|
+
'children'
|
|
57
|
+
>): VZCodeContextValue => {
|
|
58
|
+
// Auto-run Prettier after local changes.
|
|
290
59
|
const { prettierError, runPrettierRef } = usePrettier({
|
|
291
60
|
shareDBDoc,
|
|
292
61
|
submitOperation,
|
|
@@ -329,8 +98,6 @@ export const VZCodeProvider = ({
|
|
|
329
98
|
);
|
|
330
99
|
|
|
331
100
|
// Unpack state.
|
|
332
|
-
// print the state object to see what it contains
|
|
333
|
-
// console.log('state: ', state);
|
|
334
101
|
const {
|
|
335
102
|
pane,
|
|
336
103
|
activePaneId,
|
|
@@ -348,7 +115,7 @@ export const VZCodeProvider = ({
|
|
|
348
115
|
aiChatMode,
|
|
349
116
|
} = state;
|
|
350
117
|
|
|
351
|
-
const activePane
|
|
118
|
+
const activePane = findPane(pane, activePaneId);
|
|
352
119
|
if (activePane.type !== 'leafPane') {
|
|
353
120
|
// Should never happen
|
|
354
121
|
throw new Error('Expected leafPane');
|
|
@@ -393,12 +160,11 @@ export const VZCodeProvider = ({
|
|
|
393
160
|
});
|
|
394
161
|
|
|
395
162
|
// The set of open directories.
|
|
396
|
-
// TODO move this into reducer/useActions
|
|
397
163
|
const { isDirectoryOpen, toggleDirectory } =
|
|
398
164
|
useOpenDirectories({ activePane, content });
|
|
399
165
|
|
|
400
166
|
// Cache of CodeMirror editors by file id.
|
|
401
|
-
const editorCache
|
|
167
|
+
const editorCache = useEditorCache();
|
|
402
168
|
|
|
403
169
|
// Handle dynamic theme changes.
|
|
404
170
|
useDynamicTheme(editorCache, theme);
|
|
@@ -460,13 +226,10 @@ export const VZCodeProvider = ({
|
|
|
460
226
|
);
|
|
461
227
|
|
|
462
228
|
// Isolate the files object from the document.
|
|
463
|
-
const files
|
|
464
|
-
? content.files
|
|
465
|
-
: null;
|
|
229
|
+
const files = content ? content.files : null;
|
|
466
230
|
|
|
467
231
|
useKeyboardShortcuts({
|
|
468
232
|
closeTabs,
|
|
469
|
-
// TODO verify that this makes sense
|
|
470
233
|
activeFileId: activePane.activeFileId,
|
|
471
234
|
activePaneId,
|
|
472
235
|
handleOpenCreateFileModal,
|
|
@@ -481,12 +244,9 @@ export const VZCodeProvider = ({
|
|
|
481
244
|
});
|
|
482
245
|
|
|
483
246
|
// Track the currently hovered file id.
|
|
484
|
-
const [hoveredItemId, setHoveredItemId] =
|
|
485
|
-
useState<ItemId | null>(null);
|
|
247
|
+
const [hoveredItemId, setHoveredItemId] = useState(null);
|
|
486
248
|
|
|
487
249
|
// Handle presence-based auto-following
|
|
488
|
-
// This hook manages opening tabs when presence is received on files
|
|
489
|
-
// that are not currently open, independent of CodeMirror extensions
|
|
490
250
|
usePresenceAutoFollow({
|
|
491
251
|
docPresence,
|
|
492
252
|
enableAutoFollow,
|
|
@@ -495,7 +255,6 @@ export const VZCodeProvider = ({
|
|
|
495
255
|
});
|
|
496
256
|
|
|
497
257
|
// Livekit Voice Chat Modal
|
|
498
|
-
|
|
499
258
|
const [voiceChatModalOpen, setVoiceChatModalOpen] =
|
|
500
259
|
useState(false);
|
|
501
260
|
|
|
@@ -510,7 +269,10 @@ export const VZCodeProvider = ({
|
|
|
510
269
|
>(null);
|
|
511
270
|
|
|
512
271
|
const handleSendMessage = useCallback(
|
|
513
|
-
async (
|
|
272
|
+
async (
|
|
273
|
+
messageToSend?: string,
|
|
274
|
+
options?: Record<string, string>,
|
|
275
|
+
) => {
|
|
514
276
|
DEBUG &&
|
|
515
277
|
console.log(
|
|
516
278
|
'AIChat: handleSendMessage called with:',
|
|
@@ -535,7 +297,6 @@ export const VZCodeProvider = ({
|
|
|
535
297
|
setAIErrorMessage(null); // Clear any previous errors
|
|
536
298
|
|
|
537
299
|
// Call backend endpoint for AI response
|
|
538
|
-
// The server will handle all ShareDB operations including adding the user message
|
|
539
300
|
try {
|
|
540
301
|
const response = await fetch(aiChatEndpoint, {
|
|
541
302
|
method: 'POST',
|
|
@@ -544,10 +305,10 @@ export const VZCodeProvider = ({
|
|
|
544
305
|
},
|
|
545
306
|
body: JSON.stringify({
|
|
546
307
|
...aiChatOptions,
|
|
547
|
-
vizId: aiChatOptions.vizId,
|
|
548
308
|
content: messageContent.trim(),
|
|
549
309
|
chatId: currentChatId,
|
|
550
310
|
mode: aiChatMode,
|
|
311
|
+
...options,
|
|
551
312
|
}),
|
|
552
313
|
});
|
|
553
314
|
|
|
@@ -569,7 +330,7 @@ export const VZCodeProvider = ({
|
|
|
569
330
|
|
|
570
331
|
// Check if this is the specific permission error that should trigger auto-fork
|
|
571
332
|
if (
|
|
572
|
-
|
|
333
|
+
errorMessage ===
|
|
573
334
|
'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.'
|
|
574
335
|
) {
|
|
575
336
|
// Trigger auto-fork instead of showing error
|
|
@@ -590,7 +351,7 @@ export const VZCodeProvider = ({
|
|
|
590
351
|
}
|
|
591
352
|
|
|
592
353
|
// For other errors, show the error message
|
|
593
|
-
setAIErrorMessage(
|
|
354
|
+
setAIErrorMessage(errorMessage);
|
|
594
355
|
return;
|
|
595
356
|
}
|
|
596
357
|
|
|
@@ -615,8 +376,8 @@ export const VZCodeProvider = ({
|
|
|
615
376
|
],
|
|
616
377
|
);
|
|
617
378
|
|
|
618
|
-
//
|
|
619
|
-
|
|
379
|
+
// Return the context value
|
|
380
|
+
return {
|
|
620
381
|
content,
|
|
621
382
|
shareDBDoc,
|
|
622
383
|
submitOperation,
|
|
@@ -740,10 +501,4 @@ export const VZCodeProvider = ({
|
|
|
740
501
|
// Additional widgets that can be rendered in AI chat messages
|
|
741
502
|
additionalWidgets,
|
|
742
503
|
};
|
|
743
|
-
|
|
744
|
-
return (
|
|
745
|
-
<VZCodeContext.Provider value={value}>
|
|
746
|
-
{children}
|
|
747
|
-
</VZCodeContext.Provider>
|
|
748
|
-
);
|
|
749
504
|
};
|
|
@@ -40,6 +40,7 @@ const MessageComponent = ({
|
|
|
40
40
|
aiChatUndoEndpoint,
|
|
41
41
|
aiChatOptions = {},
|
|
42
42
|
additionalWidgets,
|
|
43
|
+
handleSendMessage,
|
|
43
44
|
} = useContext(VZCodeContext);
|
|
44
45
|
|
|
45
46
|
// Memoize date formatting to avoid repeated computation
|
|
@@ -117,6 +118,7 @@ const MessageComponent = ({
|
|
|
117
118
|
messageId: id,
|
|
118
119
|
chatId: chatId,
|
|
119
120
|
canUndo: canUndo,
|
|
121
|
+
handleSendMessage,
|
|
120
122
|
})}
|
|
121
123
|
{!additionalWidgets && canUndo && beforeFiles && (
|
|
122
124
|
<div className="undo-button-container">
|
|
@@ -12,13 +12,11 @@ import { VizChatMessage } from '@vizhub/viz-types';
|
|
|
12
12
|
|
|
13
13
|
const MessageListComponent = ({
|
|
14
14
|
messages,
|
|
15
|
-
aiStatus,
|
|
16
15
|
isLoading,
|
|
17
16
|
chatId, // Add chatId prop
|
|
18
17
|
aiScratchpad, // Add aiScratchpad prop
|
|
19
18
|
}: {
|
|
20
19
|
messages: VizChatMessage[];
|
|
21
|
-
aiStatus?: string;
|
|
22
20
|
isLoading: boolean;
|
|
23
21
|
chatId?: string; // Add chatId to the type
|
|
24
22
|
aiScratchpad?: string; // Add aiScratchpad to the type
|
|
@@ -1,4 +1,10 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
memo,
|
|
3
|
+
useRef,
|
|
4
|
+
useEffect,
|
|
5
|
+
useCallback,
|
|
6
|
+
useState,
|
|
7
|
+
} from 'react';
|
|
2
8
|
import Markdown from 'react-markdown';
|
|
3
9
|
import remarkGfm from 'remark-gfm';
|
|
4
10
|
|
|
@@ -11,6 +17,115 @@ const ThinkingScratchpadComponent = ({
|
|
|
11
17
|
content,
|
|
12
18
|
isVisible,
|
|
13
19
|
}: ThinkingScratchpadProps) => {
|
|
20
|
+
const contentRef = useRef<HTMLDivElement>(null);
|
|
21
|
+
const [isUserScrolled, setIsUserScrolled] =
|
|
22
|
+
useState(false);
|
|
23
|
+
const [autoScrollEnabled, setAutoScrollEnabled] =
|
|
24
|
+
useState(true);
|
|
25
|
+
const scrollTimeoutRef =
|
|
26
|
+
useRef<ReturnType<typeof setTimeout>>();
|
|
27
|
+
|
|
28
|
+
// Check if the user is scrolled to the bottom
|
|
29
|
+
const isScrolledToBottom = useCallback(() => {
|
|
30
|
+
const container = contentRef.current;
|
|
31
|
+
if (!container) return true;
|
|
32
|
+
|
|
33
|
+
const threshold = 10; // Allow 10px tolerance for "at bottom"
|
|
34
|
+
const { scrollTop, scrollHeight, clientHeight } =
|
|
35
|
+
container;
|
|
36
|
+
return (
|
|
37
|
+
scrollHeight - scrollTop - clientHeight < threshold
|
|
38
|
+
);
|
|
39
|
+
}, []);
|
|
40
|
+
|
|
41
|
+
// Smooth scroll to bottom
|
|
42
|
+
const scrollToBottom = useCallback(() => {
|
|
43
|
+
if (!autoScrollEnabled) return;
|
|
44
|
+
|
|
45
|
+
const container = contentRef.current;
|
|
46
|
+
if (container) {
|
|
47
|
+
container.scrollTo({
|
|
48
|
+
top: container.scrollHeight,
|
|
49
|
+
behavior: 'smooth',
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
}, [autoScrollEnabled]);
|
|
53
|
+
|
|
54
|
+
// Handle scroll events to detect user manual scrolling
|
|
55
|
+
const handleScroll = useCallback(() => {
|
|
56
|
+
const container = contentRef.current;
|
|
57
|
+
if (!container) return;
|
|
58
|
+
|
|
59
|
+
const isAtBottom = isScrolledToBottom();
|
|
60
|
+
|
|
61
|
+
// Clear any existing timeout
|
|
62
|
+
if (scrollTimeoutRef.current) {
|
|
63
|
+
clearTimeout(scrollTimeoutRef.current);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// If user scrolled up from bottom, disable auto-scroll
|
|
67
|
+
if (!isAtBottom && !isUserScrolled) {
|
|
68
|
+
setIsUserScrolled(true);
|
|
69
|
+
setAutoScrollEnabled(false);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// If user scrolled back to bottom, re-enable auto-scroll after a brief delay
|
|
73
|
+
if (isAtBottom && isUserScrolled) {
|
|
74
|
+
scrollTimeoutRef.current = setTimeout(() => {
|
|
75
|
+
setIsUserScrolled(false);
|
|
76
|
+
setAutoScrollEnabled(true);
|
|
77
|
+
}, 300); // 300ms delay to prevent flickering
|
|
78
|
+
}
|
|
79
|
+
}, [isUserScrolled, isScrolledToBottom]);
|
|
80
|
+
|
|
81
|
+
// Auto-scroll when content changes, but only if auto-scroll is enabled
|
|
82
|
+
useEffect(() => {
|
|
83
|
+
if (
|
|
84
|
+
autoScrollEnabled &&
|
|
85
|
+
!isUserScrolled &&
|
|
86
|
+
isVisible &&
|
|
87
|
+
content
|
|
88
|
+
) {
|
|
89
|
+
// Use a short debounce to prevent multiple scroll calls during rapid updates
|
|
90
|
+
if (scrollTimeoutRef.current) {
|
|
91
|
+
clearTimeout(scrollTimeoutRef.current);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
scrollTimeoutRef.current = setTimeout(() => {
|
|
95
|
+
scrollToBottom();
|
|
96
|
+
}, 50); // 50ms debounce
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return () => {
|
|
100
|
+
if (scrollTimeoutRef.current) {
|
|
101
|
+
clearTimeout(scrollTimeoutRef.current);
|
|
102
|
+
}
|
|
103
|
+
};
|
|
104
|
+
}, [
|
|
105
|
+
content,
|
|
106
|
+
autoScrollEnabled,
|
|
107
|
+
isUserScrolled,
|
|
108
|
+
isVisible,
|
|
109
|
+
scrollToBottom,
|
|
110
|
+
]);
|
|
111
|
+
|
|
112
|
+
// Clean up timeout on unmount
|
|
113
|
+
useEffect(() => {
|
|
114
|
+
return () => {
|
|
115
|
+
if (scrollTimeoutRef.current) {
|
|
116
|
+
clearTimeout(scrollTimeoutRef.current);
|
|
117
|
+
}
|
|
118
|
+
};
|
|
119
|
+
}, []);
|
|
120
|
+
|
|
121
|
+
// Reset scroll state when component becomes visible
|
|
122
|
+
useEffect(() => {
|
|
123
|
+
if (isVisible) {
|
|
124
|
+
setIsUserScrolled(false);
|
|
125
|
+
setAutoScrollEnabled(true);
|
|
126
|
+
}
|
|
127
|
+
}, [isVisible]);
|
|
128
|
+
|
|
14
129
|
if (!isVisible || !content) {
|
|
15
130
|
return null;
|
|
16
131
|
}
|
|
@@ -23,7 +138,11 @@ const ThinkingScratchpadComponent = ({
|
|
|
23
138
|
AI is thinking...
|
|
24
139
|
</span>
|
|
25
140
|
</div>
|
|
26
|
-
<div
|
|
141
|
+
<div
|
|
142
|
+
className="thinking-scratchpad-content"
|
|
143
|
+
ref={contentRef}
|
|
144
|
+
onScroll={handleScroll}
|
|
145
|
+
>
|
|
27
146
|
<Markdown remarkPlugins={[remarkGfm]}>
|
|
28
147
|
{content}
|
|
29
148
|
</Markdown>
|
|
@@ -1,40 +1,27 @@
|
|
|
1
|
-
import {
|
|
2
|
-
useContext,
|
|
3
|
-
useState,
|
|
4
|
-
useCallback,
|
|
5
|
-
useMemo,
|
|
6
|
-
useEffect,
|
|
7
|
-
} from 'react';
|
|
1
|
+
import { useContext, useMemo, useEffect } from 'react';
|
|
8
2
|
import { VZCodeContext } from '../../VZCodeContext';
|
|
9
|
-
import { v4 as uuidv4 } from 'uuid';
|
|
10
3
|
import { MessageList } from './MessageList';
|
|
11
4
|
import { ChatInput } from './ChatInput';
|
|
12
5
|
import './styles.scss';
|
|
13
6
|
|
|
14
|
-
const defaultAIChatEndpoint = '/api/ai-chat/';
|
|
15
|
-
|
|
16
7
|
const DEBUG = false;
|
|
17
8
|
|
|
18
|
-
const showSuggestedRequests =
|
|
9
|
+
const showSuggestedRequests = false;
|
|
19
10
|
|
|
20
11
|
export const AIChat = () => {
|
|
21
12
|
const {
|
|
22
13
|
aiChatFocused,
|
|
23
14
|
content,
|
|
24
|
-
aiChatEndpoint = defaultAIChatEndpoint,
|
|
25
|
-
aiChatOptions = {},
|
|
26
15
|
aiChatMode,
|
|
27
16
|
aiChatMessage,
|
|
28
17
|
isLoading,
|
|
29
18
|
currentChatId,
|
|
30
19
|
aiErrorMessage,
|
|
31
20
|
setAIChatMode,
|
|
32
|
-
autoForkAndRetryAI,
|
|
33
21
|
clearStoredAIPrompt,
|
|
34
22
|
getStoredAIPrompt,
|
|
35
23
|
setAIChatMessage,
|
|
36
24
|
handleSendMessage,
|
|
37
|
-
setIsLoading,
|
|
38
25
|
setAIErrorMessage,
|
|
39
26
|
} = useContext(VZCodeContext);
|
|
40
27
|
|
|
@@ -17,6 +17,8 @@
|
|
|
17
17
|
overflow-y: auto;
|
|
18
18
|
padding: 10px;
|
|
19
19
|
padding-bottom: 0;
|
|
20
|
+
display: flex;
|
|
21
|
+
flex-direction: column;
|
|
20
22
|
}
|
|
21
23
|
|
|
22
24
|
.ai-chat-input-fixed {
|
|
@@ -182,6 +184,7 @@
|
|
|
182
184
|
.undo-button-container {
|
|
183
185
|
display: flex;
|
|
184
186
|
justify-content: flex-end;
|
|
187
|
+
gap: 8px;
|
|
185
188
|
margin-top: 16px;
|
|
186
189
|
}
|
|
187
190
|
|
|
@@ -560,6 +563,21 @@
|
|
|
560
563
|
max-height: 200px;
|
|
561
564
|
overflow-y: auto;
|
|
562
565
|
background: var(--vh-color-neutral-02);
|
|
566
|
+
scroll-behavior: smooth;
|
|
567
|
+
|
|
568
|
+
&::-webkit-scrollbar {
|
|
569
|
+
width: 6px;
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
&::-webkit-scrollbar-track {
|
|
573
|
+
background: var(--vh-color-neutral-02);
|
|
574
|
+
border-radius: 3px;
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
&::-webkit-scrollbar-thumb {
|
|
578
|
+
background: var(--vh-color-neutral-04);
|
|
579
|
+
border-radius: 3px;
|
|
580
|
+
}
|
|
563
581
|
|
|
564
582
|
// Style markdown content in thinking scratchpad
|
|
565
583
|
p {
|