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.
Files changed (35) hide show
  1. package/dist/assets/{index-Cw7RI1uP.js → index-ubSLcdJf.js} +91 -91
  2. package/dist/index.html +1 -1
  3. package/package.json +9 -7
  4. package/src/client/CodeEditor/getOrCreateEditor.ts +0 -3
  5. package/src/client/CodeEditor/index.tsx +0 -1
  6. package/src/client/CodeEditor/json1PresenceDisplay.ts +28 -58
  7. package/src/client/VZCodeContext.tsx +11 -0
  8. package/src/client/VZSidebar/index.tsx +6 -0
  9. package/src/client/useKeyboardShortcuts.ts +7 -1
  10. package/src/client/usePresenceAutoFollow.ts +85 -0
  11. package/src/client/useRunCode.tsx +2 -16
  12. package/src/client/useSubmitOperation.ts +4 -11
  13. package/src/runCode.js +48 -0
  14. package/src/runCode.ts +58 -0
  15. package/src/server/aiChatHandler/aiEditing.ts +44 -0
  16. package/src/server/aiChatHandler/{chatOperations.js → chatOperations.ts} +135 -7
  17. package/src/server/aiChatHandler/{index.js → index.ts} +34 -30
  18. package/src/server/aiChatHandler/llmStreaming.ts +223 -0
  19. package/src/server/{generateAIResponse.js → generateAIResponse.ts} +3 -2
  20. package/src/server/{handleAIAssist.js → handleAIAssist.ts} +11 -4
  21. package/src/server/{index.js → index.ts} +31 -27
  22. package/src/submitOperation.js +16 -0
  23. package/src/submitOperation.ts +25 -0
  24. package/src/server/aiChatHandler/aiEditing.js +0 -42
  25. package/src/server/aiChatHandler/llmStreaming.js +0 -105
  26. /package/src/server/aiChatHandler/{errorHandling.js → errorHandling.ts} +0 -0
  27. /package/src/server/aiChatHandler/{validation.js → validation.ts} +0 -0
  28. /package/src/server/{computeInitialDocument.js → computeInitialDocument.ts} +0 -0
  29. /package/src/server/{config.js → config.ts} +0 -0
  30. /package/src/server/{featureFlags.js → featureFlags.ts} +0 -0
  31. /package/src/server/{handleAIChatMessage.js → handleAIChatMessage.ts} +0 -0
  32. /package/src/server/{handleAICopilot.js → handleAICopilot.ts} +0 -0
  33. /package/src/server/{isDirectory.js → isDirectory.ts} +0 -0
  34. /package/src/server/{livekit.js → livekit.ts} +0 -0
  35. /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-Cw7RI1uP.js"></script>
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.38.0",
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.js",
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.js",
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.4.0",
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.1",
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
  }
@@ -107,7 +107,6 @@ export const CodeEditor = ({
107
107
  usernameRef,
108
108
  customInteractRules,
109
109
  enableAutoFollowRef,
110
- openTab,
111
110
  aiCopilotEndpoint,
112
111
  esLintSource,
113
112
  });
@@ -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 debug = false;
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 (debug) {
64
+ if (DEBUG) {
72
65
  console.log(
73
66
  `Received presence for id ${id}`,
74
67
  presence,
75
- ); // Debug statement
68
+ );
76
69
  }
77
70
 
78
- // If presence === null, the user has disconnected / exited
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
- return;
82
- }
83
-
84
- // Check if the presence is for the current file or not.
85
- const isPresenceInCurrentFile = pathMatches(
86
- path,
87
- presence,
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 auto-follow is enabled, and the presence is NOT
98
- // in the current file, then open the tab of the other user.
99
- if (enableAutoFollowRef.current) {
100
- openTab({
101
- fileId: presence.start[1] as VizFileId,
102
- isTransient: true,
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
- //console.log("File User Color:" + assignUserColor(presence.username));
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
- // Ensure position is valid
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
- // console.warn(`Invalid cursor position for id ${id}: ${from}`); // Debug statement
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
- // Somehow this triggers re-rendering of the Decorations.
175
- // Not sure if this is the correct usage of the API.
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
- const position: number = editor.posAtDOM(element);
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 { VizContent } from '@vizhub/viz-types';
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 { diff } from './diff';
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
- (next) => {
12
- const data: T = shareDBDoc.data;
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
+ };