vzcode 1.38.0 → 1.40.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-Cw7RI1uP.js → index-ubSLcdJf.js} +91 -91
- package/dist/index.html +1 -1
- package/package.json +9 -7
- package/src/client/CodeEditor/getOrCreateEditor.ts +0 -3
- package/src/client/CodeEditor/index.tsx +0 -1
- package/src/client/CodeEditor/json1PresenceDisplay.ts +28 -58
- package/src/client/VZCodeContext.tsx +11 -0
- package/src/client/VZSidebar/index.tsx +6 -0
- package/src/client/useKeyboardShortcuts.ts +7 -1
- package/src/client/usePresenceAutoFollow.ts +85 -0
- package/src/client/useRunCode.tsx +2 -16
- package/src/client/useSubmitOperation.ts +4 -11
- package/src/runCode.js +48 -0
- package/src/runCode.ts +58 -0
- package/src/server/aiChatHandler/aiEditing.ts +44 -0
- package/src/server/aiChatHandler/{chatOperations.js → chatOperations.ts} +135 -7
- package/src/server/aiChatHandler/{index.js → index.ts} +34 -30
- package/src/server/aiChatHandler/llmStreaming.ts +223 -0
- package/src/server/{generateAIResponse.js → generateAIResponse.ts} +3 -2
- package/src/server/{handleAIAssist.js → handleAIAssist.ts} +11 -4
- package/src/server/{index.js → index.ts} +31 -27
- package/src/submitOperation.js +16 -0
- package/src/submitOperation.ts +25 -0
- package/src/server/aiChatHandler/aiEditing.js +0 -42
- package/src/server/aiChatHandler/llmStreaming.js +0 -105
- /package/src/server/aiChatHandler/{errorHandling.js → errorHandling.ts} +0 -0
- /package/src/server/aiChatHandler/{validation.js → validation.ts} +0 -0
- /package/src/server/{computeInitialDocument.js → computeInitialDocument.ts} +0 -0
- /package/src/server/{config.js → config.ts} +0 -0
- /package/src/server/{featureFlags.js → featureFlags.ts} +0 -0
- /package/src/server/{handleAIChatMessage.js → handleAIChatMessage.ts} +0 -0
- /package/src/server/{handleAICopilot.js → handleAICopilot.ts} +0 -0
- /package/src/server/{isDirectory.js → isDirectory.ts} +0 -0
- /package/src/server/{livekit.js → livekit.ts} +0 -0
- /package/src/server/{setupEnv.js → setupEnv.ts} +0 -0
package/dist/index.html
CHANGED
|
@@ -20,7 +20,7 @@
|
|
|
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-
|
|
23
|
+
<script type="module" crossorigin src="/assets/index-ubSLcdJf.js"></script>
|
|
24
24
|
<link rel="stylesheet" crossorigin href="/assets/index-DKLiE98a.css">
|
|
25
25
|
</head>
|
|
26
26
|
<body>
|
package/package.json
CHANGED
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "vzcode",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.40.0",
|
|
4
4
|
"description": "Multiplayer code editor system",
|
|
5
5
|
"main": "src/index.ts",
|
|
6
6
|
"type": "module",
|
|
7
|
-
"bin": "src/server/index.
|
|
7
|
+
"bin": "src/server/index.ts",
|
|
8
8
|
"files": [
|
|
9
9
|
"dist/**",
|
|
10
10
|
"src/**"
|
|
11
11
|
],
|
|
12
12
|
"scripts": {
|
|
13
13
|
"test": "vitest run",
|
|
14
|
-
"test-interactive": "cd test/sampleDirectories/kitchenSink && node ../../../src/server/index.
|
|
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\"",
|
|
@@ -139,21 +139,19 @@
|
|
|
139
139
|
"body-parser": "^2.2.0",
|
|
140
140
|
"codemirror": "^6.0.2",
|
|
141
141
|
"codemirror-copilot": "^0.0.7",
|
|
142
|
-
"codemirror-ot": "^4.
|
|
142
|
+
"codemirror-ot": "^4.5.0",
|
|
143
143
|
"color-hash": "^2.0.2",
|
|
144
144
|
"comlink": "^4.4.2",
|
|
145
145
|
"d3-array": "^3.2.4",
|
|
146
146
|
"diff-match-patch": "^1.0.5",
|
|
147
147
|
"dotenv": "^17.2.0",
|
|
148
148
|
"editcodewithai": "^2.2.0",
|
|
149
|
-
"eslint": "^9.31.0",
|
|
150
149
|
"eslint-linter-browserify": "^9.31.0",
|
|
151
150
|
"express": "^5.1.0",
|
|
152
|
-
"globals": "^16.3.0",
|
|
153
151
|
"ignore": "^7.0.5",
|
|
154
152
|
"json0-ot-diff": "^1.1.2",
|
|
155
153
|
"livekit-server-sdk": "^2.13.1",
|
|
156
|
-
"llm-code-format": "^2.0
|
|
154
|
+
"llm-code-format": "^2.1.0",
|
|
157
155
|
"ngrok": "^5.0.0-beta.2",
|
|
158
156
|
"npm": "^11.4.2",
|
|
159
157
|
"open": "^10.2.0",
|
|
@@ -171,14 +169,18 @@
|
|
|
171
169
|
"ws": "^8.18.3"
|
|
172
170
|
},
|
|
173
171
|
"devDependencies": {
|
|
172
|
+
"@eslint/js": "^9.31.0",
|
|
174
173
|
"@types/react": "^18",
|
|
175
174
|
"@types/react-dom": "^18",
|
|
176
175
|
"@vitejs/plugin-react": "^4.7.0",
|
|
177
176
|
"concurrently": "^9.2.0",
|
|
178
177
|
"cross-env": "^7.0.3",
|
|
178
|
+
"eslint": "^9.31.0",
|
|
179
|
+
"globals": "^16.3.0",
|
|
179
180
|
"npm-check-updates": "^18.0.1",
|
|
180
181
|
"prettier": "^3.6.2",
|
|
181
182
|
"sass": "^1.89.2",
|
|
183
|
+
"ts-node": "^10.9.2",
|
|
182
184
|
"typescript": "^5.8.3",
|
|
183
185
|
"vite": "^7.0.5",
|
|
184
186
|
"vitest": "^3.2.4"
|
|
@@ -176,7 +176,6 @@ export const getOrCreateEditor = async ({
|
|
|
176
176
|
usernameRef,
|
|
177
177
|
customInteractRules,
|
|
178
178
|
enableAutoFollowRef,
|
|
179
|
-
openTab,
|
|
180
179
|
aiCopilotEndpoint,
|
|
181
180
|
esLintSource,
|
|
182
181
|
rainbowBracketsEnabled = true,
|
|
@@ -213,7 +212,6 @@ export const getOrCreateEditor = async ({
|
|
|
213
212
|
// Ref to a boolean that determines whether to
|
|
214
213
|
// enable auto-following the cursors of remote users.
|
|
215
214
|
enableAutoFollowRef: React.MutableRefObject<boolean>;
|
|
216
|
-
openTab: (tabState: TabState) => void;
|
|
217
215
|
aiCopilotEndpoint?: string;
|
|
218
216
|
esLintSource: (
|
|
219
217
|
view: EditorView,
|
|
@@ -285,7 +283,6 @@ export const getOrCreateEditor = async ({
|
|
|
285
283
|
path: textPath,
|
|
286
284
|
docPresence,
|
|
287
285
|
enableAutoFollowRef,
|
|
288
|
-
openTab,
|
|
289
286
|
}),
|
|
290
287
|
);
|
|
291
288
|
}
|
|
@@ -6,20 +6,14 @@ import {
|
|
|
6
6
|
} from '@codemirror/view';
|
|
7
7
|
import { Annotation, RangeSet } from '@codemirror/state';
|
|
8
8
|
import { assignUserColor } from '../presenceColor';
|
|
9
|
-
import { VizFileId } from '@vizhub/viz-types';
|
|
10
9
|
import {
|
|
11
10
|
Presence,
|
|
12
11
|
PresenceId,
|
|
13
|
-
TabState,
|
|
14
12
|
Username,
|
|
15
13
|
} from '../../types';
|
|
16
14
|
|
|
17
|
-
const
|
|
15
|
+
const DEBUG = false;
|
|
18
16
|
|
|
19
|
-
// export let enableAutoFollow = false;
|
|
20
|
-
// export const toggleAutoFollowButton = () => {
|
|
21
|
-
// enableAutoFollow = !enableAutoFollow;
|
|
22
|
-
// };
|
|
23
17
|
// Deals with receiving the broadcasted presence cursor locations
|
|
24
18
|
// from other clients and displaying them.
|
|
25
19
|
//
|
|
@@ -32,12 +26,10 @@ export const json1PresenceDisplay = ({
|
|
|
32
26
|
path,
|
|
33
27
|
docPresence,
|
|
34
28
|
enableAutoFollowRef,
|
|
35
|
-
openTab,
|
|
36
29
|
}: {
|
|
37
30
|
path: Array<string>;
|
|
38
31
|
docPresence: any;
|
|
39
32
|
enableAutoFollowRef: React.MutableRefObject<boolean>;
|
|
40
|
-
openTab: (tabState: TabState) => void;
|
|
41
33
|
}) => [
|
|
42
34
|
ViewPlugin.fromClass(
|
|
43
35
|
class {
|
|
@@ -65,50 +57,43 @@ export const json1PresenceDisplay = ({
|
|
|
65
57
|
// this.scrollToCursor(view);
|
|
66
58
|
// });
|
|
67
59
|
// Receive remote presence changes.
|
|
60
|
+
// Receive remote presence changes.
|
|
68
61
|
docPresence.on(
|
|
69
62
|
'receive',
|
|
70
63
|
(id: PresenceId, presence: Presence) => {
|
|
71
|
-
if (
|
|
64
|
+
if (DEBUG) {
|
|
72
65
|
console.log(
|
|
73
66
|
`Received presence for id ${id}`,
|
|
74
67
|
presence,
|
|
75
|
-
);
|
|
68
|
+
);
|
|
76
69
|
}
|
|
77
70
|
|
|
78
|
-
// If presence
|
|
71
|
+
// If presence is null, the user has disconnected.
|
|
72
|
+
// We must remove them from our local state.
|
|
79
73
|
if (!presence) {
|
|
80
74
|
delete presenceState[id];
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
// If the presence is in the current file, update the presence state.
|
|
91
|
-
if (isPresenceInCurrentFile) {
|
|
92
|
-
presenceState[id] = presence;
|
|
93
|
-
} else if (presence) {
|
|
94
|
-
// Otherwise, delete the presence state.
|
|
95
|
-
delete presenceState[id];
|
|
75
|
+
// Also remove their cursor position to prevent errors.
|
|
76
|
+
delete this.cursorPosition[id];
|
|
77
|
+
} else {
|
|
78
|
+
// Otherwise, the user is active. Check if the presence
|
|
79
|
+
// is for the current file.
|
|
80
|
+
const isPresenceInCurrentFile = pathMatches(
|
|
81
|
+
path,
|
|
82
|
+
presence,
|
|
83
|
+
);
|
|
96
84
|
|
|
97
|
-
// If
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
85
|
+
// If it's the current file, add/update their state.
|
|
86
|
+
if (isPresenceInCurrentFile) {
|
|
87
|
+
presenceState[id] = presence;
|
|
88
|
+
} else {
|
|
89
|
+
// If it's for another file, remove them from this view's state.
|
|
90
|
+
delete presenceState[id];
|
|
91
|
+
delete this.cursorPosition[id];
|
|
104
92
|
}
|
|
105
93
|
}
|
|
106
|
-
// Update decorations to reflect new presence state.
|
|
107
|
-
// TODO consider mutating this rather than recomputing it on each change.
|
|
108
94
|
|
|
109
95
|
const presenceDecorations = [];
|
|
110
96
|
|
|
111
|
-
// Object.keys(presenceState).map((id) => {
|
|
112
97
|
for (const id of Object.keys(presenceState)) {
|
|
113
98
|
const presence: Presence = presenceState[id];
|
|
114
99
|
const { start, end } = presence;
|
|
@@ -118,7 +103,7 @@ export const json1PresenceDisplay = ({
|
|
|
118
103
|
presence.username,
|
|
119
104
|
);
|
|
120
105
|
const { username } = presence;
|
|
121
|
-
|
|
106
|
+
|
|
122
107
|
presenceDecorations.push({
|
|
123
108
|
from,
|
|
124
109
|
to: from,
|
|
@@ -126,10 +111,6 @@ export const json1PresenceDisplay = ({
|
|
|
126
111
|
side: -1,
|
|
127
112
|
block: false,
|
|
128
113
|
widget: new PresenceWidget(
|
|
129
|
-
// TODO see if we can figure out why
|
|
130
|
-
// updateDOM was not being called when passing
|
|
131
|
-
// the presence id as the id
|
|
132
|
-
// id,
|
|
133
114
|
'' + Math.random(),
|
|
134
115
|
userColor,
|
|
135
116
|
username,
|
|
@@ -137,10 +118,7 @@ export const json1PresenceDisplay = ({
|
|
|
137
118
|
}),
|
|
138
119
|
});
|
|
139
120
|
|
|
140
|
-
// This is `true` when the presence is a cursor,
|
|
141
|
-
// with no selection.
|
|
142
121
|
if (from !== to) {
|
|
143
|
-
// This is the case when the presence is a selection.
|
|
144
122
|
presenceDecorations.push({
|
|
145
123
|
from,
|
|
146
124
|
to,
|
|
@@ -156,34 +134,26 @@ export const json1PresenceDisplay = ({
|
|
|
156
134
|
});
|
|
157
135
|
}
|
|
158
136
|
if (view.state.doc.length >= from) {
|
|
159
|
-
|
|
160
|
-
this.cursorPosition[id] = from; // Store the cursor position, important to run if we cant get the regular scroll to work
|
|
161
|
-
// console.log(`Stored cursor position for id ${id}: ${from}`); // Debug statement
|
|
137
|
+
this.cursorPosition[id] = from;
|
|
162
138
|
} else {
|
|
163
|
-
//
|
|
139
|
+
// The cursor position is invalid, so remove it.
|
|
140
|
+
delete this.cursorPosition[id];
|
|
164
141
|
}
|
|
165
142
|
}
|
|
166
143
|
|
|
167
144
|
this.decorations = Decoration.set(
|
|
168
145
|
presenceDecorations,
|
|
169
|
-
// Without this argument, we get the following error:
|
|
170
|
-
// Uncaught Error: Ranges must be added sorted by `from` position and `startSide`
|
|
171
146
|
true,
|
|
172
147
|
);
|
|
173
148
|
|
|
174
|
-
//
|
|
175
|
-
//
|
|
176
|
-
// Inspired by https://github.com/yjs/y-codemirror.next/blob/main/src/y-remote-selections.js
|
|
177
|
-
// Set timeout so that the current CodeMirror update finishes
|
|
178
|
-
// before the next ones that render presence begin.
|
|
149
|
+
// This dispatch triggers the re-rendering of decorations.
|
|
150
|
+
// It now correctly runs after a deletion.
|
|
179
151
|
setTimeout(() => {
|
|
180
152
|
view.dispatch({
|
|
181
153
|
annotations: [presenceAnnotation.of(true)],
|
|
182
154
|
});
|
|
183
155
|
}, 0);
|
|
184
156
|
|
|
185
|
-
// Auto-follow all users when their presence is broadcast
|
|
186
|
-
// by scrolling them into view.
|
|
187
157
|
if (enableAutoFollowRef.current) {
|
|
188
158
|
this.scrollToCursor(view);
|
|
189
159
|
}
|
|
@@ -37,6 +37,7 @@ import { useRunCode } from './useRunCode';
|
|
|
37
37
|
import { useURLSync } from './useURLSync';
|
|
38
38
|
import { createInitialState, vzReducer } from './vzReducer';
|
|
39
39
|
import { findPane } from './vzReducer/findPane';
|
|
40
|
+
import { usePresenceAutoFollow } from './usePresenceAutoFollow';
|
|
40
41
|
|
|
41
42
|
// This context centralizes all the "smart" logic
|
|
42
43
|
// to do with the application state. This includes
|
|
@@ -408,6 +409,16 @@ export const VZCodeProvider = ({
|
|
|
408
409
|
const [hoveredItemId, setHoveredItemId] =
|
|
409
410
|
useState<ItemId | null>(null);
|
|
410
411
|
|
|
412
|
+
// Handle presence-based auto-following
|
|
413
|
+
// This hook manages opening tabs when presence is received on files
|
|
414
|
+
// that are not currently open, independent of CodeMirror extensions
|
|
415
|
+
usePresenceAutoFollow({
|
|
416
|
+
docPresence,
|
|
417
|
+
enableAutoFollow,
|
|
418
|
+
openTab,
|
|
419
|
+
activePane,
|
|
420
|
+
});
|
|
421
|
+
|
|
411
422
|
// Livekit Voice Chat Modal
|
|
412
423
|
|
|
413
424
|
const [voiceChatModalOpen, setVoiceChatModalOpen] =
|
|
@@ -224,6 +224,12 @@ export const VZSidebar = ({
|
|
|
224
224
|
presenceId: PresenceId,
|
|
225
225
|
update: Presence,
|
|
226
226
|
) => {
|
|
227
|
+
// Sometimes this can be null, so we check.
|
|
228
|
+
// This can happen when the user disconnects.
|
|
229
|
+
// If this happens, we do not update the presence indicator.
|
|
230
|
+
// TODO test removal of presence indicator.
|
|
231
|
+
if (!update) return;
|
|
232
|
+
|
|
227
233
|
const presenceIndicator: PresenceIndicator = {
|
|
228
234
|
username: update.username,
|
|
229
235
|
fileId: update.start[1] as VizFileId,
|
|
@@ -378,7 +378,13 @@ export const useKeyboardShortcuts = ({
|
|
|
378
378
|
return;
|
|
379
379
|
}
|
|
380
380
|
|
|
381
|
-
|
|
381
|
+
let position: number;
|
|
382
|
+
try {
|
|
383
|
+
position = editor.posAtDOM(element);
|
|
384
|
+
} catch (error) {
|
|
385
|
+
// Element is not within the current editor's document
|
|
386
|
+
return;
|
|
387
|
+
}
|
|
382
388
|
const identifier: SyntaxNode = tree.resolveInner(
|
|
383
389
|
position,
|
|
384
390
|
1,
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { useEffect, useRef } from 'react';
|
|
2
|
+
import { Presence, PresenceId } from '../types';
|
|
3
|
+
import { VizFileId } from '@vizhub/viz-types';
|
|
4
|
+
|
|
5
|
+
const DEBUG = false;
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Custom hook that handles auto-following users by opening tabs when presence
|
|
9
|
+
* is received on files that are not currently open. This logic is independent
|
|
10
|
+
* of CodeMirror extensions and works regardless of editor cache state.
|
|
11
|
+
*/
|
|
12
|
+
export const usePresenceAutoFollow = ({
|
|
13
|
+
docPresence,
|
|
14
|
+
enableAutoFollow,
|
|
15
|
+
openTab,
|
|
16
|
+
activePane,
|
|
17
|
+
}) => {
|
|
18
|
+
// Track enableAutoFollow on a ref for use in event handlers
|
|
19
|
+
const enableAutoFollowRef = useRef(enableAutoFollow);
|
|
20
|
+
useEffect(() => {
|
|
21
|
+
enableAutoFollowRef.current = enableAutoFollow;
|
|
22
|
+
}, [enableAutoFollow]);
|
|
23
|
+
|
|
24
|
+
// Track active file for comparison
|
|
25
|
+
const activeFileIdRef = useRef(activePane?.activeFileId);
|
|
26
|
+
useEffect(() => {
|
|
27
|
+
activeFileIdRef.current = activePane?.activeFileId;
|
|
28
|
+
}, [activePane?.activeFileId]);
|
|
29
|
+
|
|
30
|
+
useEffect(() => {
|
|
31
|
+
if (!docPresence) return;
|
|
32
|
+
|
|
33
|
+
const handlePresenceReceive = (
|
|
34
|
+
id: PresenceId,
|
|
35
|
+
presence: Presence,
|
|
36
|
+
) => {
|
|
37
|
+
if (DEBUG) {
|
|
38
|
+
console.log(
|
|
39
|
+
`[usePresenceAutoFollow] Received presence for id ${id}`,
|
|
40
|
+
presence,
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// If presence is null, the user has disconnected
|
|
45
|
+
if (!presence) {
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// If auto-follow is not enabled, do nothing
|
|
50
|
+
if (!enableAutoFollowRef.current) {
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Extract the file ID from the presence path
|
|
55
|
+
// Presence path structure: [path, to, file, fileId, position]
|
|
56
|
+
// We want the fileId which should be at index 1 for files
|
|
57
|
+
const presenceFileId = presence.start[1] as VizFileId;
|
|
58
|
+
|
|
59
|
+
// If the presence is for the currently active file, do nothing
|
|
60
|
+
// (the CodeMirror extension will handle displaying the cursor)
|
|
61
|
+
if (presenceFileId === activeFileIdRef.current) {
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// If the presence is for a different file, open that tab
|
|
66
|
+
DEBUG &&
|
|
67
|
+
console.log(
|
|
68
|
+
`[usePresenceAutoFollow] Auto-opening tab for ${presence.username} in file ${presenceFileId}`,
|
|
69
|
+
);
|
|
70
|
+
|
|
71
|
+
openTab({
|
|
72
|
+
fileId: presenceFileId,
|
|
73
|
+
isTransient: true,
|
|
74
|
+
});
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
// Subscribe to presence changes
|
|
78
|
+
docPresence.on('receive', handlePresenceReceive);
|
|
79
|
+
|
|
80
|
+
// Cleanup subscription on unmount
|
|
81
|
+
return () => {
|
|
82
|
+
docPresence.off('receive', handlePresenceReceive);
|
|
83
|
+
};
|
|
84
|
+
}, [docPresence, openTab]);
|
|
85
|
+
};
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { useEffect, useRef } from 'react';
|
|
2
2
|
import { SubmitOperation } from '../types';
|
|
3
|
-
import {
|
|
3
|
+
import { createRunCodeFunction } from '../runCode';
|
|
4
4
|
|
|
5
5
|
// Runs the code by flashing `isInteracting` to `true`.
|
|
6
6
|
export const useRunCode = (
|
|
@@ -8,21 +8,7 @@ export const useRunCode = (
|
|
|
8
8
|
) => {
|
|
9
9
|
const runCodeRef = useRef(null);
|
|
10
10
|
useEffect(() => {
|
|
11
|
-
const runCode = ()
|
|
12
|
-
submitOperation((content: VizContent) => ({
|
|
13
|
-
...content,
|
|
14
|
-
isInteracting: true,
|
|
15
|
-
}));
|
|
16
|
-
setTimeout(() => {
|
|
17
|
-
// This somewhat cryptic logic
|
|
18
|
-
// deletes the `isInteracting` property
|
|
19
|
-
// from the document.
|
|
20
|
-
submitOperation(
|
|
21
|
-
({ isInteracting, ...newDocument }) =>
|
|
22
|
-
newDocument,
|
|
23
|
-
);
|
|
24
|
-
}, 0);
|
|
25
|
-
};
|
|
11
|
+
const runCode = createRunCodeFunction(submitOperation);
|
|
26
12
|
runCodeRef.current = runCode;
|
|
27
13
|
}, [submitOperation]);
|
|
28
14
|
return runCodeRef;
|
|
@@ -1,22 +1,15 @@
|
|
|
1
1
|
import { useCallback } from 'react';
|
|
2
2
|
import { ShareDBDoc } from '../types'; // Assuming ShareDBDoc is already generic
|
|
3
|
-
import {
|
|
3
|
+
import { createSubmitOperation } from '../submitOperation';
|
|
4
4
|
|
|
5
5
|
// A generic helper function to submit diff-based operations to ShareDB
|
|
6
6
|
export const useSubmitOperation = <T>(
|
|
7
7
|
shareDBDoc: ShareDBDoc<T>,
|
|
8
8
|
): ((next: (data: T) => T) => void) => {
|
|
9
9
|
const submitOperation: (next: (data: T) => T) => void =
|
|
10
|
-
useCallback(
|
|
11
|
-
(
|
|
12
|
-
|
|
13
|
-
const op = diff(data, next(data));
|
|
14
|
-
if (op && shareDBDoc) {
|
|
15
|
-
shareDBDoc.submitOp(op);
|
|
16
|
-
}
|
|
17
|
-
},
|
|
18
|
-
[shareDBDoc],
|
|
19
|
-
);
|
|
10
|
+
useCallback(() => {
|
|
11
|
+
return createSubmitOperation(shareDBDoc);
|
|
12
|
+
}, [shareDBDoc])();
|
|
20
13
|
|
|
21
14
|
return submitOperation;
|
|
22
15
|
};
|
package/src/runCode.js
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { createSubmitOperation } from './submitOperation.js';
|
|
2
|
+
// TODO migrate the server-side code to use TypeScript,
|
|
3
|
+
// and delete this file once the migration is complete (use src/runCode.ts instead).
|
|
4
|
+
/**
|
|
5
|
+
* Creates a runCode function that triggers code execution by flashing `isInteracting` to `true`.
|
|
6
|
+
* This works for both client-side (with submitOperation) and server-side (with ShareDB document).
|
|
7
|
+
*/
|
|
8
|
+
export const createRunCodeFunction = (
|
|
9
|
+
submitOperationOrDoc,
|
|
10
|
+
) => {
|
|
11
|
+
return () => {
|
|
12
|
+
let submitOperation;
|
|
13
|
+
|
|
14
|
+
// Check if this is a client-side submitOperation or server-side ShareDB document
|
|
15
|
+
if (typeof submitOperationOrDoc === 'function') {
|
|
16
|
+
// Client-side: already a submitOperation function
|
|
17
|
+
submitOperation = submitOperationOrDoc;
|
|
18
|
+
} else {
|
|
19
|
+
// Server-side: create submitOperation from ShareDB document
|
|
20
|
+
submitOperation = createSubmitOperation(
|
|
21
|
+
submitOperationOrDoc,
|
|
22
|
+
);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Use the unified submitOperation approach for both client and server
|
|
26
|
+
submitOperation((content) => ({
|
|
27
|
+
...content,
|
|
28
|
+
isInteracting: true,
|
|
29
|
+
}));
|
|
30
|
+
|
|
31
|
+
setTimeout(() => {
|
|
32
|
+
submitOperation((content) => ({
|
|
33
|
+
...content,
|
|
34
|
+
isInteracting: false,
|
|
35
|
+
}));
|
|
36
|
+
}, 0);
|
|
37
|
+
};
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Creates a runCodeRef object compatible with React refs.
|
|
42
|
+
* This is useful for maintaining compatibility with existing code that expects a ref.
|
|
43
|
+
*/
|
|
44
|
+
export const createRunCodeRef = (submitOperationOrDoc) => {
|
|
45
|
+
return {
|
|
46
|
+
current: createRunCodeFunction(submitOperationOrDoc),
|
|
47
|
+
};
|
|
48
|
+
};
|
package/src/runCode.ts
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { SubmitOperation } from './types';
|
|
2
|
+
import { VizContent } from '@vizhub/viz-types';
|
|
3
|
+
import { createSubmitOperation } from './submitOperation';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Creates a runCode function that triggers code execution by flashing `isInteracting` to `true`.
|
|
7
|
+
* This works for both client-side (with submitOperation) and server-side (with ShareDB document).
|
|
8
|
+
*/
|
|
9
|
+
export const createRunCodeFunction = (
|
|
10
|
+
submitOperationOrDoc:
|
|
11
|
+
| SubmitOperation
|
|
12
|
+
| { data: any; submitOp: (ops: any) => void },
|
|
13
|
+
) => {
|
|
14
|
+
return () => {
|
|
15
|
+
let submitOperation: SubmitOperation;
|
|
16
|
+
|
|
17
|
+
// Check if this is a client-side submitOperation or server-side ShareDB document
|
|
18
|
+
if (typeof submitOperationOrDoc === 'function') {
|
|
19
|
+
// Client-side: already a submitOperation function
|
|
20
|
+
submitOperation =
|
|
21
|
+
submitOperationOrDoc as SubmitOperation;
|
|
22
|
+
} else {
|
|
23
|
+
// Server-side: create submitOperation from ShareDB document
|
|
24
|
+
submitOperation = createSubmitOperation(
|
|
25
|
+
submitOperationOrDoc,
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Use the unified submitOperation approach for both client and server
|
|
30
|
+
submitOperation((content: VizContent) => ({
|
|
31
|
+
...content,
|
|
32
|
+
isInteracting: true,
|
|
33
|
+
}));
|
|
34
|
+
|
|
35
|
+
setTimeout(() => {
|
|
36
|
+
// This somewhat cryptic logic
|
|
37
|
+
// deletes the `isInteracting` property
|
|
38
|
+
// from the document.
|
|
39
|
+
submitOperation(
|
|
40
|
+
({ isInteracting, ...newDocument }) => newDocument,
|
|
41
|
+
);
|
|
42
|
+
}, 0);
|
|
43
|
+
};
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Creates a runCodeRef object compatible with React refs.
|
|
48
|
+
* This is useful for maintaining compatibility with existing code that expects a ref.
|
|
49
|
+
*/
|
|
50
|
+
export const createRunCodeRef = (
|
|
51
|
+
submitOperationOrDoc:
|
|
52
|
+
| SubmitOperation
|
|
53
|
+
| { data: any; submitOp: (ops: any) => void },
|
|
54
|
+
) => {
|
|
55
|
+
return {
|
|
56
|
+
current: createRunCodeFunction(submitOperationOrDoc),
|
|
57
|
+
};
|
|
58
|
+
};
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import {
|
|
2
|
+
assembleFullPrompt,
|
|
3
|
+
prepareFilesForPrompt,
|
|
4
|
+
} from 'editcodewithai';
|
|
5
|
+
import { formatMarkdownFiles } from 'llm-code-format';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Performs AI editing operations using streaming with incremental OT operations
|
|
9
|
+
*/
|
|
10
|
+
export const performAIEditing = async ({
|
|
11
|
+
prompt,
|
|
12
|
+
shareDBDoc,
|
|
13
|
+
llmFunction,
|
|
14
|
+
runCode,
|
|
15
|
+
}) => {
|
|
16
|
+
const preparedFiles = prepareFilesForPrompt(
|
|
17
|
+
shareDBDoc.data.files,
|
|
18
|
+
);
|
|
19
|
+
const filesContext = formatMarkdownFiles(preparedFiles);
|
|
20
|
+
|
|
21
|
+
// 2. Assemble the final prompt
|
|
22
|
+
const fullPrompt = assembleFullPrompt({
|
|
23
|
+
filesContext,
|
|
24
|
+
prompt,
|
|
25
|
+
editFormat: 'whole',
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
// Call the LLM function which will handle streaming and incremental file updates
|
|
29
|
+
const result = await llmFunction(fullPrompt);
|
|
30
|
+
|
|
31
|
+
// Clear the scratchpad and update status
|
|
32
|
+
// clearAIScratchpadAndStatus(
|
|
33
|
+
// shareDBDoc,
|
|
34
|
+
// chatId,
|
|
35
|
+
// 'Done editing with AI.',
|
|
36
|
+
// );
|
|
37
|
+
|
|
38
|
+
runCode();
|
|
39
|
+
|
|
40
|
+
return {
|
|
41
|
+
content: result.content,
|
|
42
|
+
generationId: result.generationId,
|
|
43
|
+
};
|
|
44
|
+
};
|