vzcode 1.45.0 → 1.47.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/{index-C1lTrzOZ.js → index-BMYXp1Uw.js} +114 -114
- package/dist/assets/{index-DeeOcIzw.css → index-BXyJ2hMf.css} +1 -1
- package/dist/index.html +2 -2
- package/package.json +7 -3
- package/src/client/CodeEditor/index.tsx +7 -4
- package/src/client/RunCodeWidget/index.tsx +18 -15
- package/src/client/SplitPaneResizeContext.tsx +26 -108
- package/src/client/VZCodeContext.tsx +8 -1
- package/src/client/VZRight.tsx +32 -4
- package/src/client/VZSidebar/AIChat/ChatInput.tsx +72 -2
- package/src/client/VZSidebar/AIChat/DiffView.scss +29 -0
- package/src/client/VZSidebar/AIChat/DiffView.tsx +62 -1
- package/src/client/VZSidebar/AIChat/Message.tsx +14 -1
- package/src/client/VZSidebar/AIChat/MessageList.tsx +134 -13
- package/src/client/VZSidebar/AIChat/ThinkingScratchpad.tsx +37 -0
- package/src/client/VZSidebar/AIChat/index.tsx +187 -16
- package/src/client/VZSidebar/AIChat/styles.scss +278 -1
- package/src/client/VZSidebar/aiCopyPaste.ts +6 -1
- package/src/client/VZSidebar/index.tsx +10 -2
- package/src/client/bootstrap.ts +11 -1
- package/src/client/useActions.ts +12 -0
- package/src/client/usePrettier/index.ts +1 -3
- package/src/client/vzReducer/aiChatReducer.ts +13 -0
- package/src/client/vzReducer/createInitialState.ts +1 -0
- package/src/client/vzReducer/index.ts +9 -0
- package/src/client/vzReducer/searchReducer.test.ts +44 -2
- package/src/runCode.ts +5 -17
- package/src/server/aiChatHandler/aiEditing.ts +37 -0
- package/src/server/aiChatHandler/chatOperations.ts +38 -1
- package/src/server/aiChatHandler/index.ts +20 -9
- package/src/server/aiChatHandler/llmStreaming.ts +101 -19
- package/src/server/generateAIResponse.ts +4 -0
- package/src/server/index.ts +52 -0
package/dist/index.html
CHANGED
|
@@ -20,8 +20,8 @@
|
|
|
20
20
|
href="https://fonts.googleapis.com/css2?family=Poppins:wght@300;400;500;600;700&display=swap"
|
|
21
21
|
rel="stylesheet"
|
|
22
22
|
/>
|
|
23
|
-
<script type="module" crossorigin src="/assets/index-
|
|
24
|
-
<link rel="stylesheet" crossorigin href="/assets/index-
|
|
23
|
+
<script type="module" crossorigin src="/assets/index-BMYXp1Uw.js"></script>
|
|
24
|
+
<link rel="stylesheet" crossorigin href="/assets/index-BXyJ2hMf.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.47.0",
|
|
4
4
|
"description": "Multiplayer code editor system",
|
|
5
5
|
"main": "src/index.ts",
|
|
6
6
|
"type": "module",
|
|
@@ -18,6 +18,9 @@
|
|
|
18
18
|
"build": "vite build",
|
|
19
19
|
"build-release": "vite build --mode release",
|
|
20
20
|
"preview": "concurrently \"npm run test-interactive\" \"vite preview\"",
|
|
21
|
+
"tauri": "tauri",
|
|
22
|
+
"tauri:dev": "tauri dev",
|
|
23
|
+
"tauri:build": "tauri build",
|
|
21
24
|
"prepublishOnly": "npm run build-release",
|
|
22
25
|
"upgrade": "ncu -x react,react-dom,@types/react,@types/react-dom -u"
|
|
23
26
|
},
|
|
@@ -135,8 +138,8 @@
|
|
|
135
138
|
"@uiw/codemirror-themes": "^4.24.2",
|
|
136
139
|
"@valtown/codemirror-ts": "^2.3.1",
|
|
137
140
|
"@vizhub/runtime": "^4.3.0",
|
|
138
|
-
"@vizhub/viz-types": "^0.
|
|
139
|
-
"@vizhub/viz-utils": "^1.
|
|
141
|
+
"@vizhub/viz-types": "^0.4.0",
|
|
142
|
+
"@vizhub/viz-utils": "^1.4.0",
|
|
140
143
|
"body-parser": "^2.2.0",
|
|
141
144
|
"codemirror": "^6.0.2",
|
|
142
145
|
"codemirror-copilot": "^0.0.7",
|
|
@@ -173,6 +176,7 @@
|
|
|
173
176
|
},
|
|
174
177
|
"devDependencies": {
|
|
175
178
|
"@eslint/js": "^9.32.0",
|
|
179
|
+
"@tauri-apps/cli": "^2.7.1",
|
|
176
180
|
"@types/react": "^18",
|
|
177
181
|
"@types/react-dom": "^18",
|
|
178
182
|
"@vitejs/plugin-react": "^4.7.0",
|
|
@@ -43,7 +43,6 @@ export const CodeEditor = ({
|
|
|
43
43
|
theme,
|
|
44
44
|
codeEditorRef,
|
|
45
45
|
enableAutoFollow,
|
|
46
|
-
openTab,
|
|
47
46
|
setIsAIChatOpen,
|
|
48
47
|
setAIChatMessage,
|
|
49
48
|
} = useContext(VZCodeContext);
|
|
@@ -63,13 +62,17 @@ export const CodeEditor = ({
|
|
|
63
62
|
clearTimeout(interactTimeoutRef.current);
|
|
64
63
|
}
|
|
65
64
|
|
|
66
|
-
// Set `isInteracting: undefined` after a delay
|
|
65
|
+
// Set `isInteracting: undefined` after a delay,
|
|
66
|
+
// so that when the runId changes in the future,
|
|
67
|
+
// it will trigger a hard re-run, not a hot reload.
|
|
67
68
|
interactTimeoutRef.current = setTimeout(() => {
|
|
68
69
|
interactTimeoutRef.current = null;
|
|
69
70
|
|
|
70
|
-
// This logic deletes the `isInteracting` property from the document
|
|
71
|
+
// This logic deletes the `isInteracting` property from the document
|
|
71
72
|
submitOperation(
|
|
72
|
-
({ isInteracting, ...newDocument }) =>
|
|
73
|
+
({ isInteracting, ...newDocument }) => ({
|
|
74
|
+
...newDocument,
|
|
75
|
+
}),
|
|
73
76
|
);
|
|
74
77
|
}, 800);
|
|
75
78
|
}, [submitOperation]);
|
|
@@ -28,24 +28,27 @@ export const RunCodeWidget = ({
|
|
|
28
28
|
useContext(VZCodeContext);
|
|
29
29
|
const [isRunning, setIsRunning] = useState(false);
|
|
30
30
|
|
|
31
|
-
const handleClick = useCallback(
|
|
32
|
-
|
|
31
|
+
const handleClick = useCallback(
|
|
32
|
+
(event?: React.MouseEvent) => {
|
|
33
|
+
setIsRunning(true); // Set the running state to true
|
|
33
34
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
35
|
+
// Run Prettier
|
|
36
|
+
const runPrettier = runPrettierRef.current;
|
|
37
|
+
if (runPrettier !== null) {
|
|
38
|
+
runPrettier();
|
|
39
|
+
}
|
|
39
40
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
41
|
+
// Run the code
|
|
42
|
+
const runCode = runCodeRef.current;
|
|
43
|
+
if (runCode !== null) {
|
|
44
|
+
runCode();
|
|
45
|
+
}
|
|
45
46
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
47
|
+
// Optional: reset the icon state after animation completes (e.g., 1 second)
|
|
48
|
+
setTimeout(() => setIsRunning(false), 1000);
|
|
49
|
+
},
|
|
50
|
+
[runCodeRef, runPrettierRef],
|
|
51
|
+
);
|
|
49
52
|
|
|
50
53
|
const handleSplitEditor = useCallback(() => {
|
|
51
54
|
splitCurrentPane();
|
|
@@ -2,9 +2,8 @@
|
|
|
2
2
|
import {
|
|
3
3
|
createContext,
|
|
4
4
|
useReducer,
|
|
5
|
-
useEffect,
|
|
6
5
|
useCallback,
|
|
7
|
-
|
|
6
|
+
useEffect,
|
|
8
7
|
} from 'react';
|
|
9
8
|
|
|
10
9
|
export type Side = 'left' | 'right';
|
|
@@ -16,63 +15,22 @@ export type SplitPaneResizeContextValue = {
|
|
|
16
15
|
isDraggingRight: boolean;
|
|
17
16
|
isDraggingLeft: boolean;
|
|
18
17
|
setIsDragging: (a: boolean, side: Side) => void;
|
|
18
|
+
setSidebarView: (isAIChatOpen: boolean) => void;
|
|
19
19
|
};
|
|
20
20
|
|
|
21
|
-
//
|
|
22
|
-
const
|
|
23
|
-
|
|
24
|
-
// The amount of time to wait idle before writing to localStorage.
|
|
25
|
-
// This is to avoid writing to localStorage on every resize event.
|
|
26
|
-
// MS = milliseconds
|
|
27
|
-
const localStorageWriteDebounceMS = 800;
|
|
28
|
-
|
|
29
|
-
// Properties in LocalStorage
|
|
30
|
-
const sidebarWidthProperty = 'vzCodeSidebarWidth';
|
|
31
|
-
const codeEditorWidthProperty = 'vzCodeCodeEditorWidth';
|
|
32
|
-
|
|
33
|
-
const initialSidebarWidthDefault = 250;
|
|
34
|
-
let initialSidebarWidth: number =
|
|
35
|
-
initialSidebarWidthDefault;
|
|
36
|
-
|
|
21
|
+
// Default widths for different sidebar views
|
|
22
|
+
const filesViewWidth = 300;
|
|
23
|
+
const aiChatViewWidth = 600;
|
|
37
24
|
const initialCodeEditorWidthDefault = 800;
|
|
38
|
-
let initialCodeEditorWidth: number =
|
|
39
|
-
initialCodeEditorWidthDefault;
|
|
40
|
-
|
|
41
|
-
// If no value is found in localStorage, we'll use the default.
|
|
42
|
-
let needsInitialization = false;
|
|
43
|
-
|
|
44
|
-
// If we're in the browser,
|
|
45
|
-
if (typeof window !== 'undefined' && enableLocalStorage) {
|
|
46
|
-
//check localStorage for a previously stored width.
|
|
47
|
-
const sidebarWidthLocal: string | null =
|
|
48
|
-
window.localStorage.getItem(sidebarWidthProperty);
|
|
49
|
-
|
|
50
|
-
const codeEditorWidthLocal: string | null =
|
|
51
|
-
window.localStorage.getItem(codeEditorWidthProperty);
|
|
52
|
-
|
|
53
|
-
// If there is a previously stored width,
|
|
54
|
-
if (sidebarWidthLocal !== null) {
|
|
55
|
-
// use it as the initial width.
|
|
56
|
-
initialSidebarWidth = +sidebarWidthLocal;
|
|
57
|
-
needsInitialization = true;
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
if (codeEditorWidthLocal !== null) {
|
|
61
|
-
// use it as the initial width.
|
|
62
|
-
initialCodeEditorWidth = +codeEditorWidthLocal;
|
|
63
|
-
needsInitialization = true;
|
|
64
|
-
}
|
|
65
|
-
} else {
|
|
66
|
-
// If we're not in the browser, use the default initial width.
|
|
67
|
-
}
|
|
68
25
|
|
|
69
26
|
const initialValue: SplitPaneResizeContextValue = {
|
|
70
|
-
codeEditorWidth:
|
|
71
|
-
sidebarWidth:
|
|
27
|
+
codeEditorWidth: initialCodeEditorWidthDefault,
|
|
28
|
+
sidebarWidth: filesViewWidth, // Default to files view
|
|
72
29
|
moveSplitPane: () => {},
|
|
73
30
|
isDraggingRight: false,
|
|
74
31
|
isDraggingLeft: false,
|
|
75
32
|
setIsDragging: () => {},
|
|
33
|
+
setSidebarView: () => {},
|
|
76
34
|
};
|
|
77
35
|
|
|
78
36
|
export const SplitPaneResizeContext =
|
|
@@ -84,30 +42,13 @@ type SplitPaneState = {
|
|
|
84
42
|
codeEditorWidth: number;
|
|
85
43
|
isDraggingLeft: boolean;
|
|
86
44
|
isDraggingRight: boolean;
|
|
87
|
-
|
|
88
|
-
// False on first render
|
|
89
|
-
isInitialized: boolean;
|
|
90
45
|
};
|
|
91
46
|
|
|
92
47
|
const initialState: SplitPaneState = {
|
|
93
|
-
|
|
94
|
-
// codeEditorWidth: initialCodeEditorWidth,
|
|
95
|
-
|
|
96
|
-
// These need to match between SSR and the first
|
|
97
|
-
// client-side render (hydration), to avoid
|
|
98
|
-
// React hydration errors like this one:
|
|
99
|
-
// * Warning: Prop `style` did not match.
|
|
100
|
-
// * Server: "left:210px;width:20px"
|
|
101
|
-
// * Client: "left:218px;width:20px"
|
|
102
|
-
|
|
103
|
-
sidebarWidth: initialSidebarWidthDefault,
|
|
48
|
+
sidebarWidth: filesViewWidth,
|
|
104
49
|
codeEditorWidth: initialCodeEditorWidthDefault,
|
|
105
50
|
isDraggingLeft: false,
|
|
106
51
|
isDraggingRight: false,
|
|
107
|
-
|
|
108
|
-
// Solution for hydration errors: we set the value from localStorage
|
|
109
|
-
// in a layoutEffect that happens after first render.
|
|
110
|
-
isInitialized: false,
|
|
111
52
|
};
|
|
112
53
|
|
|
113
54
|
type SplitPaneAction =
|
|
@@ -122,7 +63,8 @@ type SplitPaneAction =
|
|
|
122
63
|
isDragging: boolean;
|
|
123
64
|
}
|
|
124
65
|
| {
|
|
125
|
-
type: '
|
|
66
|
+
type: 'setSidebarView';
|
|
67
|
+
isAIChatOpen: boolean;
|
|
126
68
|
};
|
|
127
69
|
|
|
128
70
|
const splitPaneReducer = (
|
|
@@ -130,13 +72,6 @@ const splitPaneReducer = (
|
|
|
130
72
|
action: SplitPaneAction,
|
|
131
73
|
) => {
|
|
132
74
|
switch (action.type) {
|
|
133
|
-
case 'initialize':
|
|
134
|
-
return {
|
|
135
|
-
...state,
|
|
136
|
-
sidebarWidth: initialSidebarWidth,
|
|
137
|
-
codeEditorWidth: initialCodeEditorWidth,
|
|
138
|
-
isInitialized: true,
|
|
139
|
-
};
|
|
140
75
|
case 'move':
|
|
141
76
|
return action.side === 'left'
|
|
142
77
|
? {
|
|
@@ -160,6 +95,13 @@ const splitPaneReducer = (
|
|
|
160
95
|
...state,
|
|
161
96
|
isDraggingRight: action.isDragging,
|
|
162
97
|
};
|
|
98
|
+
case 'setSidebarView':
|
|
99
|
+
return {
|
|
100
|
+
...state,
|
|
101
|
+
sidebarWidth: action.isAIChatOpen
|
|
102
|
+
? aiChatViewWidth
|
|
103
|
+
: filesViewWidth,
|
|
104
|
+
};
|
|
163
105
|
default:
|
|
164
106
|
return state;
|
|
165
107
|
}
|
|
@@ -185,45 +127,20 @@ export const SplitPaneResizeProvider = ({ children }) => {
|
|
|
185
127
|
[dispatch],
|
|
186
128
|
);
|
|
187
129
|
|
|
130
|
+
const setSidebarView = useCallback(
|
|
131
|
+
(isAIChatOpen: boolean) => {
|
|
132
|
+
dispatch({ type: 'setSidebarView', isAIChatOpen });
|
|
133
|
+
},
|
|
134
|
+
[dispatch],
|
|
135
|
+
);
|
|
136
|
+
|
|
188
137
|
const {
|
|
189
138
|
sidebarWidth,
|
|
190
139
|
codeEditorWidth,
|
|
191
140
|
isDraggingLeft,
|
|
192
141
|
isDraggingRight,
|
|
193
|
-
isInitialized,
|
|
194
142
|
} = state;
|
|
195
143
|
|
|
196
|
-
// Logic around initializing from localStorage.
|
|
197
|
-
// Only run this on the client.
|
|
198
|
-
if (typeof window !== 'undefined') {
|
|
199
|
-
useLayoutEffect(() => {
|
|
200
|
-
if (needsInitialization && !isInitialized) {
|
|
201
|
-
dispatch({ type: 'initialize' });
|
|
202
|
-
}
|
|
203
|
-
}, [isInitialized]);
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
// Logic around storing the values in localStorage.
|
|
207
|
-
useEffect(() => {
|
|
208
|
-
if (sidebarWidth !== initialSidebarWidth) {
|
|
209
|
-
setTimeout(() => {
|
|
210
|
-
window.localStorage.setItem(
|
|
211
|
-
sidebarWidthProperty,
|
|
212
|
-
'' + sidebarWidth,
|
|
213
|
-
);
|
|
214
|
-
}, localStorageWriteDebounceMS);
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
if (codeEditorWidth !== initialCodeEditorWidth) {
|
|
218
|
-
setTimeout(() => {
|
|
219
|
-
window.localStorage.setItem(
|
|
220
|
-
codeEditorWidthProperty,
|
|
221
|
-
'' + codeEditorWidth,
|
|
222
|
-
);
|
|
223
|
-
}, localStorageWriteDebounceMS);
|
|
224
|
-
}
|
|
225
|
-
}, [codeEditorWidth, sidebarWidth]);
|
|
226
|
-
|
|
227
144
|
const value: SplitPaneResizeContextValue = {
|
|
228
145
|
codeEditorWidth,
|
|
229
146
|
sidebarWidth,
|
|
@@ -231,6 +148,7 @@ export const SplitPaneResizeProvider = ({ children }) => {
|
|
|
231
148
|
isDraggingLeft,
|
|
232
149
|
isDraggingRight,
|
|
233
150
|
setIsDragging,
|
|
151
|
+
setSidebarView,
|
|
234
152
|
};
|
|
235
153
|
|
|
236
154
|
return (
|
|
@@ -126,6 +126,8 @@ export type VZCodeContextValue = {
|
|
|
126
126
|
setIsAIChatOpen: (isAIChatOpen: boolean) => void;
|
|
127
127
|
aiChatFocused: boolean;
|
|
128
128
|
toggleAIChatFocused: () => void;
|
|
129
|
+
aiChatMode: 'ask' | 'edit';
|
|
130
|
+
setAIChatMode: (mode: 'ask' | 'edit') => void;
|
|
129
131
|
aiChatEndpoint?: string;
|
|
130
132
|
aiChatOptions?: { [key: string]: any };
|
|
131
133
|
setSearchResults: (files: ShareDBDoc<VizContent>) => void;
|
|
@@ -158,7 +160,9 @@ export type VZCodeContextValue = {
|
|
|
158
160
|
runPrettierRef: React.MutableRefObject<
|
|
159
161
|
null | (() => void)
|
|
160
162
|
>;
|
|
161
|
-
runCodeRef: React.MutableRefObject<
|
|
163
|
+
runCodeRef: React.MutableRefObject<
|
|
164
|
+
null | ((hardRerun?: boolean) => void)
|
|
165
|
+
>;
|
|
162
166
|
|
|
163
167
|
sidebarRef: React.RefObject<HTMLDivElement>;
|
|
164
168
|
|
|
@@ -317,6 +321,7 @@ export const VZCodeProvider = ({
|
|
|
317
321
|
toggleSearchFocused,
|
|
318
322
|
setIsAIChatOpen,
|
|
319
323
|
toggleAIChatFocused,
|
|
324
|
+
setAIChatMode,
|
|
320
325
|
setIsSettingsOpen,
|
|
321
326
|
setIsDocOpen,
|
|
322
327
|
closeSettings,
|
|
@@ -489,6 +494,8 @@ export const VZCodeProvider = ({
|
|
|
489
494
|
setIsAIChatOpen,
|
|
490
495
|
aiChatFocused,
|
|
491
496
|
toggleAIChatFocused,
|
|
497
|
+
aiChatMode: state.aiChatMode,
|
|
498
|
+
setAIChatMode,
|
|
492
499
|
aiChatEndpoint,
|
|
493
500
|
aiChatOptions,
|
|
494
501
|
|
package/src/client/VZRight.tsx
CHANGED
|
@@ -11,6 +11,14 @@ import {
|
|
|
11
11
|
import BuildWorker from './buildWorker?worker';
|
|
12
12
|
import { VZCodeContext } from './VZCodeContext';
|
|
13
13
|
import { vizFilesToFileCollection } from '@vizhub/viz-utils';
|
|
14
|
+
import { VizContent } from '@vizhub/viz-types';
|
|
15
|
+
|
|
16
|
+
// Extend VizContent type to include hardRerun property
|
|
17
|
+
type ExtendedVizContent = VizContent & {
|
|
18
|
+
// TODO remove this, use the runId property from VizContent instead.
|
|
19
|
+
// If `VizContent.runId` changes, it will trigger a hard rerun.
|
|
20
|
+
hardRerun?: boolean;
|
|
21
|
+
};
|
|
14
22
|
|
|
15
23
|
const enableIframe = true;
|
|
16
24
|
|
|
@@ -18,6 +26,9 @@ export const VZRight = () => {
|
|
|
18
26
|
const iframeRef = useRef<HTMLIFrameElement>(null);
|
|
19
27
|
const runtimeRef = useRef<VizHubRuntime | null>(null);
|
|
20
28
|
const isFirstRunRef = useRef(true);
|
|
29
|
+
const lastRunIdRef = useRef<string | undefined>(
|
|
30
|
+
undefined,
|
|
31
|
+
);
|
|
21
32
|
|
|
22
33
|
// Get access to the current files.
|
|
23
34
|
const { content, handleRuntimeError, clearRuntimeError } =
|
|
@@ -32,6 +43,15 @@ export const VZRight = () => {
|
|
|
32
43
|
);
|
|
33
44
|
|
|
34
45
|
const isInteracting = content?.isInteracting || false;
|
|
46
|
+
const runId = content?.runId;
|
|
47
|
+
const hardRerun =
|
|
48
|
+
(content as ExtendedVizContent)?.hardRerun || false;
|
|
49
|
+
|
|
50
|
+
// Check if runId has changed
|
|
51
|
+
const runIdChanged = runId !== lastRunIdRef.current;
|
|
52
|
+
if (runIdChanged) {
|
|
53
|
+
lastRunIdRef.current = runId;
|
|
54
|
+
}
|
|
35
55
|
|
|
36
56
|
useEffect(() => {
|
|
37
57
|
if (!files) return;
|
|
@@ -51,14 +71,22 @@ export const VZRight = () => {
|
|
|
51
71
|
});
|
|
52
72
|
}
|
|
53
73
|
|
|
54
|
-
// Run code in the iframe
|
|
55
|
-
|
|
74
|
+
// Run code in the iframe when:
|
|
75
|
+
// 1. First run
|
|
76
|
+
// 2. User is interacting with widgets
|
|
77
|
+
// 3. runId changed (indicating AI finished or other trigger)
|
|
78
|
+
if (
|
|
79
|
+
isFirstRunRef.current ||
|
|
80
|
+
isInteracting ||
|
|
81
|
+
runIdChanged
|
|
82
|
+
) {
|
|
56
83
|
// Clear runtime errors when new code runs
|
|
57
84
|
clearRuntimeError();
|
|
58
85
|
|
|
59
86
|
runtimeRef.current?.run({
|
|
60
87
|
files,
|
|
61
|
-
|
|
88
|
+
// Enable hot reloading when interacting, disable when runId changed without interaction
|
|
89
|
+
enableHotReloading: isInteracting,
|
|
62
90
|
enableSourcemap: true,
|
|
63
91
|
vizId: 'example-viz',
|
|
64
92
|
});
|
|
@@ -70,7 +98,7 @@ export const VZRight = () => {
|
|
|
70
98
|
// runtime.cleanup();
|
|
71
99
|
// worker.terminate();
|
|
72
100
|
// };
|
|
73
|
-
}, [files, isInteracting]);
|
|
101
|
+
}, [files, isInteracting, runIdChanged, hardRerun]);
|
|
74
102
|
|
|
75
103
|
return (
|
|
76
104
|
<div className="right">
|
|
@@ -4,7 +4,12 @@ import {
|
|
|
4
4
|
useCallback,
|
|
5
5
|
memo,
|
|
6
6
|
} from 'react';
|
|
7
|
-
import {
|
|
7
|
+
import {
|
|
8
|
+
Form,
|
|
9
|
+
Button,
|
|
10
|
+
ButtonGroup,
|
|
11
|
+
ToggleButton,
|
|
12
|
+
} from '../../bootstrap';
|
|
8
13
|
|
|
9
14
|
interface ChatInputProps {
|
|
10
15
|
aiChatMessage: string;
|
|
@@ -12,6 +17,8 @@ interface ChatInputProps {
|
|
|
12
17
|
onSendMessage: () => void;
|
|
13
18
|
isLoading: boolean;
|
|
14
19
|
focused: boolean;
|
|
20
|
+
aiChatMode: 'ask' | 'edit';
|
|
21
|
+
setAIChatMode: (mode: 'ask' | 'edit') => void;
|
|
15
22
|
}
|
|
16
23
|
|
|
17
24
|
const ChatInputComponent = ({
|
|
@@ -20,6 +27,8 @@ const ChatInputComponent = ({
|
|
|
20
27
|
onSendMessage,
|
|
21
28
|
isLoading,
|
|
22
29
|
focused,
|
|
30
|
+
aiChatMode,
|
|
31
|
+
setAIChatMode,
|
|
23
32
|
}: ChatInputProps) => {
|
|
24
33
|
const inputRef = useRef<HTMLTextAreaElement>(null);
|
|
25
34
|
|
|
@@ -49,6 +58,50 @@ const ChatInputComponent = ({
|
|
|
49
58
|
|
|
50
59
|
return (
|
|
51
60
|
<div className="ai-chat-input-container">
|
|
61
|
+
<div
|
|
62
|
+
className="ai-chat-mode-toggle"
|
|
63
|
+
style={{ marginBottom: '8px' }}
|
|
64
|
+
>
|
|
65
|
+
<ButtonGroup size="sm">
|
|
66
|
+
<ToggleButton
|
|
67
|
+
id="ai-chat-mode-ask"
|
|
68
|
+
type="radio"
|
|
69
|
+
variant={
|
|
70
|
+
aiChatMode === 'ask'
|
|
71
|
+
? 'primary'
|
|
72
|
+
: 'outline-primary'
|
|
73
|
+
}
|
|
74
|
+
name="ai-chat-mode"
|
|
75
|
+
value="ask"
|
|
76
|
+
checked={aiChatMode === 'ask'}
|
|
77
|
+
onChange={() => setAIChatMode('ask')}
|
|
78
|
+
disabled={isLoading}
|
|
79
|
+
>
|
|
80
|
+
💬 Ask
|
|
81
|
+
</ToggleButton>
|
|
82
|
+
<ToggleButton
|
|
83
|
+
id="ai-chat-mode-edit"
|
|
84
|
+
type="radio"
|
|
85
|
+
variant={
|
|
86
|
+
aiChatMode === 'edit'
|
|
87
|
+
? 'primary'
|
|
88
|
+
: 'outline-primary'
|
|
89
|
+
}
|
|
90
|
+
name="ai-chat-mode"
|
|
91
|
+
value="edit"
|
|
92
|
+
checked={aiChatMode === 'edit'}
|
|
93
|
+
onChange={() => setAIChatMode('edit')}
|
|
94
|
+
disabled={isLoading}
|
|
95
|
+
>
|
|
96
|
+
✏️ Edit
|
|
97
|
+
</ToggleButton>
|
|
98
|
+
</ButtonGroup>
|
|
99
|
+
<div className="ai-chat-mode-description">
|
|
100
|
+
{aiChatMode === 'ask'
|
|
101
|
+
? 'Ask questions without editing files'
|
|
102
|
+
: 'Get answers and code edits'}
|
|
103
|
+
</div>
|
|
104
|
+
</div>
|
|
52
105
|
<Form.Group className="ai-chat-input-group">
|
|
53
106
|
<Form.Control
|
|
54
107
|
as="textarea"
|
|
@@ -57,15 +110,32 @@ const ChatInputComponent = ({
|
|
|
57
110
|
onChange={handleChange}
|
|
58
111
|
onKeyDown={handleKeyDown}
|
|
59
112
|
ref={inputRef}
|
|
60
|
-
placeholder=
|
|
113
|
+
placeholder={
|
|
114
|
+
aiChatMode === 'edit'
|
|
115
|
+
? 'Ask me to make code changes...'
|
|
116
|
+
: 'Ask me anything about your code...'
|
|
117
|
+
}
|
|
61
118
|
spellCheck="false"
|
|
62
119
|
disabled={isLoading}
|
|
120
|
+
aria-label="Chat message input"
|
|
63
121
|
/>
|
|
122
|
+
{aiChatMessage && (
|
|
123
|
+
<div className="ai-chat-input-info">
|
|
124
|
+
<span className="ai-chat-hint">
|
|
125
|
+
Shift+Enter for new line
|
|
126
|
+
</span>
|
|
127
|
+
<span className="ai-chat-hint">
|
|
128
|
+
Press Enter to send
|
|
129
|
+
</span>
|
|
130
|
+
</div>
|
|
131
|
+
)}
|
|
64
132
|
<Button
|
|
65
133
|
variant="primary"
|
|
66
134
|
onClick={onSendMessage}
|
|
67
135
|
disabled={!aiChatMessage.trim() || isLoading}
|
|
68
136
|
className="ai-chat-send-button"
|
|
137
|
+
aria-label="Send message"
|
|
138
|
+
title="Send message (Enter)"
|
|
69
139
|
>
|
|
70
140
|
Send
|
|
71
141
|
</Button>
|
|
@@ -3,6 +3,9 @@
|
|
|
3
3
|
|
|
4
4
|
.diff-summary {
|
|
5
5
|
margin-bottom: 8px;
|
|
6
|
+
display: flex;
|
|
7
|
+
justify-content: space-between;
|
|
8
|
+
align-items: center;
|
|
6
9
|
|
|
7
10
|
.diff-stats {
|
|
8
11
|
display: flex;
|
|
@@ -28,6 +31,32 @@
|
|
|
28
31
|
}
|
|
29
32
|
}
|
|
30
33
|
|
|
34
|
+
.undo-button-container {
|
|
35
|
+
display: flex;
|
|
36
|
+
justify-content: flex-end;
|
|
37
|
+
margin-top: 16px;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
.undo-button {
|
|
41
|
+
background-color: #dc3545;
|
|
42
|
+
color: white;
|
|
43
|
+
border: none;
|
|
44
|
+
border-radius: 4px;
|
|
45
|
+
padding: 4px 8px;
|
|
46
|
+
font-size: 12px;
|
|
47
|
+
cursor: pointer;
|
|
48
|
+
transition: background-color 0.2s;
|
|
49
|
+
|
|
50
|
+
&:hover:not(:disabled) {
|
|
51
|
+
background-color: #c82333;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
&:disabled {
|
|
55
|
+
background-color: #6c757d;
|
|
56
|
+
cursor: not-allowed;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
31
60
|
.diff-files {
|
|
32
61
|
display: flex;
|
|
33
62
|
flex-direction: column;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import React from 'react';
|
|
1
|
+
import React, { useState } from 'react';
|
|
2
2
|
import {
|
|
3
3
|
UnifiedFilesDiff,
|
|
4
4
|
parseUnifiedDiffStats,
|
|
@@ -10,11 +10,21 @@ import './DiffView.scss';
|
|
|
10
10
|
|
|
11
11
|
interface DiffViewProps {
|
|
12
12
|
diffData: UnifiedFilesDiff;
|
|
13
|
+
messageId?: string;
|
|
14
|
+
chatId?: string;
|
|
15
|
+
beforeFiles?: any;
|
|
16
|
+
canUndo?: boolean;
|
|
13
17
|
}
|
|
14
18
|
|
|
15
19
|
export const DiffView: React.FC<DiffViewProps> = ({
|
|
16
20
|
diffData,
|
|
21
|
+
messageId,
|
|
22
|
+
chatId,
|
|
23
|
+
beforeFiles,
|
|
24
|
+
canUndo = false,
|
|
17
25
|
}) => {
|
|
26
|
+
const [isUndoing, setIsUndoing] = useState(false);
|
|
27
|
+
|
|
18
28
|
const unifiedDiffs = Object.values(diffData).filter(
|
|
19
29
|
(diff) => diff.length > 0,
|
|
20
30
|
);
|
|
@@ -23,6 +33,45 @@ export const DiffView: React.FC<DiffViewProps> = ({
|
|
|
23
33
|
return null;
|
|
24
34
|
}
|
|
25
35
|
|
|
36
|
+
const handleUndo = async () => {
|
|
37
|
+
if (
|
|
38
|
+
!messageId ||
|
|
39
|
+
!chatId ||
|
|
40
|
+
!beforeFiles ||
|
|
41
|
+
isUndoing
|
|
42
|
+
) {
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
setIsUndoing(true);
|
|
47
|
+
try {
|
|
48
|
+
const response = await fetch('/ai-chat-undo', {
|
|
49
|
+
method: 'POST',
|
|
50
|
+
headers: {
|
|
51
|
+
'Content-Type': 'application/json',
|
|
52
|
+
},
|
|
53
|
+
body: JSON.stringify({
|
|
54
|
+
chatId,
|
|
55
|
+
messageId,
|
|
56
|
+
}),
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
if (!response.ok) {
|
|
60
|
+
throw new Error(
|
|
61
|
+
`HTTP error! status: ${response.status}`,
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// The server will handle the ShareDB operations to undo the changes
|
|
66
|
+
// The UI will update automatically via ShareDB
|
|
67
|
+
} catch (error) {
|
|
68
|
+
console.error('Error undoing AI edit:', error);
|
|
69
|
+
// TODO: Show user-friendly error message
|
|
70
|
+
} finally {
|
|
71
|
+
setIsUndoing(false);
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
|
|
26
75
|
// Calculate statistics from all unified diffs
|
|
27
76
|
let totalAdditions = 0;
|
|
28
77
|
let totalDeletions = 0;
|
|
@@ -67,6 +116,18 @@ export const DiffView: React.FC<DiffViewProps> = ({
|
|
|
67
116
|
className="diff-files"
|
|
68
117
|
dangerouslySetInnerHTML={{ __html: diffHtml }}
|
|
69
118
|
/>
|
|
119
|
+
{canUndo && beforeFiles && (
|
|
120
|
+
<div className="undo-button-container">
|
|
121
|
+
<button
|
|
122
|
+
className="undo-button"
|
|
123
|
+
onClick={handleUndo}
|
|
124
|
+
disabled={isUndoing}
|
|
125
|
+
title="Undo this AI edit"
|
|
126
|
+
>
|
|
127
|
+
{isUndoing ? 'Undoing...' : 'Undo'}
|
|
128
|
+
</button>
|
|
129
|
+
</div>
|
|
130
|
+
)}
|
|
70
131
|
</div>
|
|
71
132
|
);
|
|
72
133
|
};
|