vzcode 1.57.0 → 1.59.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-DqIc-0Gp.js → index-BJI4JLx9.js} +120 -120
- package/dist/assets/{index-CE_xP06t.css → index-afP-wJTZ.css} +1 -1
- 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 +7 -7
- package/src/client/CodeEditor/getOrCreateEditor.tsx +7 -7
- package/src/client/CodeEditor/index.tsx +2 -0
- package/src/client/VZCodeContext.tsx +155 -0
- package/src/client/VZSidebar/AIChat/Message.tsx +22 -4
- 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 +12 -138
- package/src/client/VZSidebar/AIChat/styles.scss +16 -0
- package/src/server/aiChatHandler/chatOperations.ts +4 -2
- package/src/server/aiChatHandler/index.ts +12 -1
- package/src/server/aiChatHandler/llmStreaming.ts +26 -10
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-
|
|
24
|
-
<link rel="stylesheet" crossorigin href="/assets/index-
|
|
23
|
+
<script type="module" crossorigin src="/assets/index-BJI4JLx9.js"></script>
|
|
24
|
+
<link rel="stylesheet" crossorigin href="/assets/index-afP-wJTZ.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.
|
|
3
|
+
"version": "1.59.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
|
|
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",
|
|
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.
|
|
121
|
-
"@langchain/openai": "^0.6.
|
|
120
|
+
"@langchain/core": "^0.3.67",
|
|
121
|
+
"@langchain/openai": "^0.6.4",
|
|
122
122
|
"@lezer/highlight": "^1.2.1",
|
|
123
123
|
"@livekit/components-react": "^2.9.14",
|
|
124
124
|
"@replit/codemirror-indentation-markers": "^6.5.3",
|
|
@@ -179,17 +179,17 @@
|
|
|
179
179
|
"@tauri-apps/cli": "^2.7.1",
|
|
180
180
|
"@types/react": "^18",
|
|
181
181
|
"@types/react-dom": "^18",
|
|
182
|
-
"@vitejs/plugin-react": "^
|
|
182
|
+
"@vitejs/plugin-react": "^5.0.0",
|
|
183
183
|
"concurrently": "^9.2.0",
|
|
184
184
|
"cross-env": "^10.0.0",
|
|
185
185
|
"eslint": "^9.32.0",
|
|
186
186
|
"globals": "^16.3.0",
|
|
187
187
|
"npm-check-updates": "^18.0.2",
|
|
188
188
|
"prettier": "^3.6.2",
|
|
189
|
-
"sass": "^1.
|
|
189
|
+
"sass": "^1.90.0",
|
|
190
190
|
"ts-node": "^10.9.2",
|
|
191
191
|
"typescript": "^5.9.2",
|
|
192
|
-
"vite": "^7.0
|
|
192
|
+
"vite": "^7.1.0",
|
|
193
193
|
"vitest": "^3.2.4"
|
|
194
194
|
},
|
|
195
195
|
"optionalDependencies": {
|
|
@@ -74,6 +74,7 @@ import { getFileExtension } from '../utils/fileExtension';
|
|
|
74
74
|
import { SparklesSVG } from '../Icons/SparklesSVG';
|
|
75
75
|
import { VZCodeContext } from '../VZCodeContext';
|
|
76
76
|
import { useContext, useMemo } from 'react';
|
|
77
|
+
import { handleAIChatMessage } from '../../server/aiChatHandler';
|
|
77
78
|
|
|
78
79
|
const DEBUG = false;
|
|
79
80
|
|
|
@@ -194,6 +195,7 @@ export const getOrCreateEditor = async ({
|
|
|
194
195
|
rainbowBracketsEnabled = true,
|
|
195
196
|
setIsAIChatOpen,
|
|
196
197
|
setAIChatMessage,
|
|
198
|
+
handleSendMessage,
|
|
197
199
|
}: {
|
|
198
200
|
// TODO pass this in from the outside
|
|
199
201
|
paneId?: PaneId;
|
|
@@ -229,8 +231,9 @@ export const getOrCreateEditor = async ({
|
|
|
229
231
|
view: EditorView,
|
|
230
232
|
) => Promise<readonly Diagnostic[]>;
|
|
231
233
|
rainbowBracketsEnabled?: boolean; // New parameter type
|
|
232
|
-
setIsAIChatOpen:
|
|
233
|
-
setAIChatMessage:
|
|
234
|
+
setIsAIChatOpen: (isAIChatOpen: boolean) => void;
|
|
235
|
+
setAIChatMessage: (message: string) => void;
|
|
236
|
+
handleSendMessage: any; // TODO fix types
|
|
234
237
|
}): Promise<ExtendedEditorCacheValue> => {
|
|
235
238
|
// Cache hit
|
|
236
239
|
|
|
@@ -500,12 +503,9 @@ export const getOrCreateEditor = async ({
|
|
|
500
503
|
root.render(
|
|
501
504
|
<div
|
|
502
505
|
onClick={() => {
|
|
503
|
-
console.log(
|
|
504
|
-
'TODO set the chat prompt to "Implement the TODO"',
|
|
505
|
-
);
|
|
506
506
|
setIsAIChatOpen(true);
|
|
507
|
-
setAIChatMessage('Implement the TODO');
|
|
508
|
-
|
|
507
|
+
// setAIChatMessage('Implement the TODO');
|
|
508
|
+
handleSendMessage('Implement the TODO');
|
|
509
509
|
}}
|
|
510
510
|
>
|
|
511
511
|
<SparklesSVG width={14} height={14} />
|
|
@@ -45,6 +45,7 @@ export const CodeEditor = ({
|
|
|
45
45
|
enableAutoFollow,
|
|
46
46
|
setIsAIChatOpen,
|
|
47
47
|
setAIChatMessage,
|
|
48
|
+
handleSendMessage,
|
|
48
49
|
} = useContext(VZCodeContext);
|
|
49
50
|
|
|
50
51
|
// Set `doc.data.isInteracting` to `true` when the user is interacting
|
|
@@ -116,6 +117,7 @@ export const CodeEditor = ({
|
|
|
116
117
|
esLintSource,
|
|
117
118
|
setIsAIChatOpen,
|
|
118
119
|
setAIChatMessage,
|
|
120
|
+
handleSendMessage,
|
|
119
121
|
});
|
|
120
122
|
|
|
121
123
|
if (isMounted) {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import {
|
|
2
|
+
Context,
|
|
2
3
|
createContext,
|
|
3
4
|
useCallback,
|
|
4
5
|
useReducer,
|
|
@@ -39,6 +40,7 @@ import { useURLSync } from './useURLSync';
|
|
|
39
40
|
import { createInitialState, vzReducer } from './vzReducer';
|
|
40
41
|
import { findPane } from './vzReducer/findPane';
|
|
41
42
|
import { usePresenceAutoFollow } from './usePresenceAutoFollow';
|
|
43
|
+
import { v4 as uuidv4 } from 'uuid';
|
|
42
44
|
|
|
43
45
|
// This context centralizes all the "smart" logic
|
|
44
46
|
// to do with the application state. This includes
|
|
@@ -195,6 +197,15 @@ export type VZCodeContextValue = {
|
|
|
195
197
|
setVoiceChatModalOpen: (state: boolean) => void;
|
|
196
198
|
aiChatMessage: string;
|
|
197
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;
|
|
198
209
|
|
|
199
210
|
// Auto-fork functions for VizHub integration
|
|
200
211
|
autoForkAndRetryAI?: (
|
|
@@ -207,6 +218,17 @@ export type VZCodeContextValue = {
|
|
|
207
218
|
prompt: string;
|
|
208
219
|
modelName: string;
|
|
209
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
|
+
}>;
|
|
210
232
|
};
|
|
211
233
|
|
|
212
234
|
export const VZCodeProvider = ({
|
|
@@ -233,6 +255,7 @@ export const VZCodeProvider = ({
|
|
|
233
255
|
autoForkAndRetryAI,
|
|
234
256
|
clearStoredAIPrompt,
|
|
235
257
|
getStoredAIPrompt,
|
|
258
|
+
additionalWidgets,
|
|
236
259
|
}: {
|
|
237
260
|
content: VizContent;
|
|
238
261
|
shareDBDoc: ShareDBDoc<VizContent>;
|
|
@@ -264,6 +287,11 @@ export const VZCodeProvider = ({
|
|
|
264
287
|
prompt: string;
|
|
265
288
|
modelName: string;
|
|
266
289
|
} | null;
|
|
290
|
+
additionalWidgets?: React.ComponentType<{
|
|
291
|
+
messageId: string;
|
|
292
|
+
chatId: string;
|
|
293
|
+
canUndo: boolean;
|
|
294
|
+
}>;
|
|
267
295
|
}) => {
|
|
268
296
|
// Auto-run Pretter after local changes.
|
|
269
297
|
const { prettierError, runPrettierRef } = usePrettier({
|
|
@@ -324,6 +352,7 @@ export const VZCodeProvider = ({
|
|
|
324
352
|
username,
|
|
325
353
|
enableAutoFollow,
|
|
326
354
|
sidebarPresenceIndicators,
|
|
355
|
+
aiChatMode,
|
|
327
356
|
} = state;
|
|
328
357
|
|
|
329
358
|
const activePane: Pane = findPane(pane, activePaneId);
|
|
@@ -479,6 +508,123 @@ export const VZCodeProvider = ({
|
|
|
479
508
|
|
|
480
509
|
const [aiChatMessage, setAIChatMessage] = useState('');
|
|
481
510
|
|
|
511
|
+
const DEBUG = false;
|
|
512
|
+
|
|
513
|
+
const [isLoading, setIsLoading] = useState(false);
|
|
514
|
+
const [currentChatId] = useState(() => uuidv4());
|
|
515
|
+
const [aiErrorMessage, setAIErrorMessage] = useState<
|
|
516
|
+
string | null
|
|
517
|
+
>(null);
|
|
518
|
+
|
|
519
|
+
const handleSendMessage = useCallback(
|
|
520
|
+
async (
|
|
521
|
+
messageToSend?: string,
|
|
522
|
+
options?: Record<string, string>,
|
|
523
|
+
) => {
|
|
524
|
+
DEBUG &&
|
|
525
|
+
console.log(
|
|
526
|
+
'AIChat: handleSendMessage called with:',
|
|
527
|
+
messageToSend,
|
|
528
|
+
'aiChatMessage:',
|
|
529
|
+
aiChatMessage,
|
|
530
|
+
);
|
|
531
|
+
const messageContent = messageToSend || aiChatMessage;
|
|
532
|
+
|
|
533
|
+
if (
|
|
534
|
+
!messageContent ||
|
|
535
|
+
typeof messageContent !== 'string' ||
|
|
536
|
+
!messageContent.trim() ||
|
|
537
|
+
isLoading
|
|
538
|
+
) {
|
|
539
|
+
return;
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
const currentPrompt = aiChatMessage.trim();
|
|
543
|
+
setAIChatMessage('');
|
|
544
|
+
setIsLoading(true);
|
|
545
|
+
setAIErrorMessage(null); // Clear any previous errors
|
|
546
|
+
|
|
547
|
+
// Call backend endpoint for AI response
|
|
548
|
+
// The server will handle all ShareDB operations including adding the user message
|
|
549
|
+
try {
|
|
550
|
+
const response = await fetch(aiChatEndpoint, {
|
|
551
|
+
method: 'POST',
|
|
552
|
+
headers: {
|
|
553
|
+
'Content-Type': 'application/json',
|
|
554
|
+
},
|
|
555
|
+
body: JSON.stringify({
|
|
556
|
+
...aiChatOptions,
|
|
557
|
+
content: messageContent.trim(),
|
|
558
|
+
chatId: currentChatId,
|
|
559
|
+
mode: aiChatMode,
|
|
560
|
+
...options,
|
|
561
|
+
}),
|
|
562
|
+
});
|
|
563
|
+
|
|
564
|
+
if (!response.ok) {
|
|
565
|
+
throw new Error(
|
|
566
|
+
`HTTP error! status: ${response.status}`,
|
|
567
|
+
);
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
// Parse the response to check for errors
|
|
571
|
+
const responseData = await response.json();
|
|
572
|
+
|
|
573
|
+
// Check if the response contains a VizHub error
|
|
574
|
+
if (
|
|
575
|
+
responseData.outcome === 'failure' &&
|
|
576
|
+
responseData.error
|
|
577
|
+
) {
|
|
578
|
+
const errorMessage = responseData.error.message;
|
|
579
|
+
|
|
580
|
+
// Check if this is the specific permission error that should trigger auto-fork
|
|
581
|
+
if (
|
|
582
|
+
aiErrorMessage ===
|
|
583
|
+
'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
|
+
) {
|
|
585
|
+
// Trigger auto-fork instead of showing error
|
|
586
|
+
try {
|
|
587
|
+
await autoForkAndRetryAI?.(
|
|
588
|
+
currentPrompt,
|
|
589
|
+
aiChatMode,
|
|
590
|
+
);
|
|
591
|
+
// If we reach here, the fork was successful and redirect should happen
|
|
592
|
+
return;
|
|
593
|
+
} catch (forkError) {
|
|
594
|
+
console.error('Auto-fork failed:', forkError);
|
|
595
|
+
setAIErrorMessage(
|
|
596
|
+
'Failed to fork visualization. Please try forking manually.',
|
|
597
|
+
);
|
|
598
|
+
return;
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
// For other errors, show the error message
|
|
603
|
+
setAIErrorMessage(aiErrorMessage);
|
|
604
|
+
return;
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
// The backend handles all ShareDB operations for successful responses
|
|
608
|
+
} catch (error) {
|
|
609
|
+
console.error('Error getting AI response:', error);
|
|
610
|
+
setAIErrorMessage(
|
|
611
|
+
'Failed to send message. Please try again.',
|
|
612
|
+
);
|
|
613
|
+
} finally {
|
|
614
|
+
setIsLoading(false);
|
|
615
|
+
}
|
|
616
|
+
},
|
|
617
|
+
[
|
|
618
|
+
aiChatMessage,
|
|
619
|
+
isLoading,
|
|
620
|
+
aiChatEndpoint,
|
|
621
|
+
aiChatOptions,
|
|
622
|
+
currentChatId,
|
|
623
|
+
aiChatMode,
|
|
624
|
+
autoForkAndRetryAI,
|
|
625
|
+
],
|
|
626
|
+
);
|
|
627
|
+
|
|
482
628
|
// The value provided by this context.
|
|
483
629
|
const value: VZCodeContextValue = {
|
|
484
630
|
content,
|
|
@@ -589,11 +735,20 @@ export const VZCodeProvider = ({
|
|
|
589
735
|
|
|
590
736
|
aiChatMessage,
|
|
591
737
|
setAIChatMessage,
|
|
738
|
+
isLoading,
|
|
739
|
+
setIsLoading,
|
|
740
|
+
currentChatId,
|
|
741
|
+
aiErrorMessage,
|
|
742
|
+
setAIErrorMessage,
|
|
743
|
+
handleSendMessage,
|
|
592
744
|
|
|
593
745
|
// Auto-fork functions for VizHub integration
|
|
594
746
|
autoForkAndRetryAI,
|
|
595
747
|
clearStoredAIPrompt,
|
|
596
748
|
getStoredAIPrompt,
|
|
749
|
+
|
|
750
|
+
// Additional widgets that can be rendered in AI chat messages
|
|
751
|
+
additionalWidgets,
|
|
597
752
|
};
|
|
598
753
|
|
|
599
754
|
return (
|
|
@@ -1,7 +1,12 @@
|
|
|
1
1
|
import { timestampToDate } from '@vizhub/viz-utils';
|
|
2
2
|
import Markdown from 'react-markdown';
|
|
3
3
|
import remarkGfm from 'remark-gfm';
|
|
4
|
-
import
|
|
4
|
+
import React, {
|
|
5
|
+
useMemo,
|
|
6
|
+
memo,
|
|
7
|
+
useState,
|
|
8
|
+
useContext,
|
|
9
|
+
} from 'react';
|
|
5
10
|
import { DiffView } from './DiffView';
|
|
6
11
|
import { UnifiedFilesDiff } from '../../../utils/fileDiff';
|
|
7
12
|
import { enableDiffView } from '../../featureFlags';
|
|
@@ -31,8 +36,12 @@ const MessageComponent = ({
|
|
|
31
36
|
canUndo,
|
|
32
37
|
}: MessageProps) => {
|
|
33
38
|
const [isUndoing, setIsUndoing] = useState(false);
|
|
34
|
-
const {
|
|
35
|
-
|
|
39
|
+
const {
|
|
40
|
+
aiChatUndoEndpoint,
|
|
41
|
+
aiChatOptions = {},
|
|
42
|
+
additionalWidgets,
|
|
43
|
+
handleSendMessage,
|
|
44
|
+
} = useContext(VZCodeContext);
|
|
36
45
|
|
|
37
46
|
// Memoize date formatting to avoid repeated computation
|
|
38
47
|
const formattedTime = useMemo(() => {
|
|
@@ -102,7 +111,16 @@ const MessageComponent = ({
|
|
|
102
111
|
Object.keys(diffData).length > 0 && (
|
|
103
112
|
<DiffView diffData={diffData} />
|
|
104
113
|
)}
|
|
105
|
-
{
|
|
114
|
+
{additionalWidgets &&
|
|
115
|
+
canUndo &&
|
|
116
|
+
chatId &&
|
|
117
|
+
React.createElement(additionalWidgets, {
|
|
118
|
+
messageId: id,
|
|
119
|
+
chatId: chatId,
|
|
120
|
+
canUndo: canUndo,
|
|
121
|
+
handleSendMessage,
|
|
122
|
+
})}
|
|
123
|
+
{!additionalWidgets && canUndo && beforeFiles && (
|
|
106
124
|
<div className="undo-button-container">
|
|
107
125
|
<button
|
|
108
126
|
className="undo-button"
|
|
@@ -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>
|