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
@@ -1,5 +1,6 @@
1
1
  import { dateToTimestamp } from '@vizhub/viz-utils';
2
2
  import { diff } from '../../client/diff.js';
3
+ import { randomId } from '../../randomId.js';
3
4
 
4
5
  /**
5
6
  * Ensures the chats object exists in the ShareDB document
@@ -108,7 +109,13 @@ export const updateAIScratchpad = (
108
109
  },
109
110
  },
110
111
  });
111
- shareDBDoc.submitOp(op);
112
+
113
+ // op is `null` if there are no changes
114
+ // This can happen if the content is the same as before
115
+ // In that case, we don't need to submit an operation
116
+ if (op) {
117
+ shareDBDoc.submitOp(op);
118
+ }
112
119
  };
113
120
 
114
121
  /**
@@ -174,8 +181,7 @@ export const addAIMessage = (
174
181
  export const updateFiles = (shareDBDoc, files) => {
175
182
  const filesOp = diff(shareDBDoc.data, {
176
183
  ...shareDBDoc.data,
177
- files: files,
178
- isInteracting: true,
184
+ files,
179
185
  });
180
186
  shareDBDoc.submitOp(filesOp);
181
187
  };
@@ -187,9 +193,131 @@ export const setIsInteracting = (
187
193
  shareDBDoc,
188
194
  isInteracting,
189
195
  ) => {
190
- const interactingOp = diff(
191
- { isInteracting: !isInteracting },
192
- { isInteracting: isInteracting },
193
- );
196
+ // Only generate an operation if the value is actually changing
197
+ const currentIsInteracting =
198
+ shareDBDoc.data.isInteracting;
199
+
200
+ if (currentIsInteracting === isInteracting) {
201
+ return;
202
+ }
203
+
204
+ const newState = {
205
+ ...shareDBDoc.data,
206
+ isInteracting: isInteracting,
207
+ };
208
+
209
+ const interactingOp = diff(shareDBDoc.data, newState);
194
210
  shareDBDoc.submitOp(interactingOp);
195
211
  };
212
+
213
+ /**
214
+ * Finds a file ID by searching for a matching file name
215
+ */
216
+ export const resolveFileId = (
217
+ fileName: string,
218
+ shareDBDoc: any,
219
+ ) => {
220
+ const files = shareDBDoc.data.files;
221
+
222
+ // Search through all files to find matching name
223
+ for (const [fileId, file] of Object.entries(files)) {
224
+ if ((file as any).name === fileName) {
225
+ return fileId;
226
+ }
227
+ }
228
+
229
+ // If file doesn't exist, return null
230
+ return null;
231
+ };
232
+
233
+ /**
234
+ * Creates a new file with a random ID
235
+ */
236
+ export const createNewFile = (shareDBDoc, fileName) => {
237
+ // Generate a new random file ID
238
+ const newFileId = randomId();
239
+
240
+ const newState = {
241
+ ...shareDBDoc.data,
242
+ files: {
243
+ ...shareDBDoc.data.files,
244
+ [newFileId]: {
245
+ name: fileName,
246
+ text: '',
247
+ },
248
+ },
249
+ };
250
+
251
+ const op = diff(shareDBDoc.data, newState);
252
+ shareDBDoc.submitOp(op);
253
+ return newFileId;
254
+ };
255
+
256
+ /**
257
+ * Ensures a file exists, creating it if necessary
258
+ */
259
+ export const ensureFileExists = (shareDBDoc, fileName) => {
260
+ let fileId = resolveFileId(fileName, shareDBDoc);
261
+
262
+ if (!fileId) {
263
+ // File doesn't exist, create it
264
+ fileId = createNewFile(shareDBDoc, fileName);
265
+ }
266
+
267
+ return fileId;
268
+ };
269
+
270
+ /**
271
+ * Clears the content of a file
272
+ */
273
+ export const clearFileContent = (shareDBDoc, fileId) => {
274
+ const currentFile = shareDBDoc.data.files[fileId];
275
+
276
+ if (currentFile && currentFile.text) {
277
+ // Clear the file content
278
+ const newState = {
279
+ ...shareDBDoc.data,
280
+ files: {
281
+ ...shareDBDoc.data.files,
282
+ [fileId]: {
283
+ ...currentFile,
284
+ text: '',
285
+ },
286
+ },
287
+ };
288
+
289
+ const op = diff(shareDBDoc.data, newState);
290
+ shareDBDoc.submitOp(op);
291
+ }
292
+ };
293
+
294
+ /**
295
+ * Appends a line to a file using OT operations
296
+ */
297
+ export const appendLineToFile = (
298
+ shareDBDoc,
299
+ fileId,
300
+ line,
301
+ ) => {
302
+ const currentFile = shareDBDoc.data.files[fileId];
303
+ const currentContent = currentFile?.text || '';
304
+ const newContent = currentContent + line + '\n';
305
+
306
+ // Create the new file state
307
+ const newFileState = {
308
+ ...currentFile,
309
+ text: newContent,
310
+ };
311
+
312
+ const newDocState = {
313
+ ...shareDBDoc.data,
314
+ files: {
315
+ ...shareDBDoc.data.files,
316
+ [fileId]: newFileState,
317
+ },
318
+ };
319
+
320
+ // Generate OT operation using the diff utility
321
+ const op = diff(shareDBDoc.data, newDocState);
322
+ shareDBDoc.submitOp(op);
323
+ };
@@ -3,20 +3,30 @@ import {
3
3
  ensureChatsExist,
4
4
  ensureChatExists,
5
5
  addUserMessage,
6
- addAIMessage,
7
6
  } from './chatOperations.js';
8
7
  import { createLLMFunction } from './llmStreaming.js';
9
8
  import { performAIEditing } from './aiEditing.js';
10
9
  import { handleError } from './errorHandling.js';
10
+ import { createRunCodeFunction } from '../../runCode.js';
11
+ import { ShareDBDoc } from '../../types.js';
12
+ import { VizContent } from '@vizhub/viz-types';
11
13
 
12
- const debug = false;
14
+ const DEBUG = false;
13
15
 
14
16
  export const handleAIChatMessage =
15
- (shareDBDoc, options = {}) =>
16
- async (req, res) => {
17
+ ({
18
+ shareDBDoc,
19
+ createVizBotLocalPresence,
20
+ onCreditDeduction,
21
+ }: {
22
+ shareDBDoc: ShareDBDoc<VizContent>;
23
+ createVizBotLocalPresence: () => any;
24
+ onCreditDeduction?: any;
25
+ }) =>
26
+ async (req: any, res: any) => {
17
27
  const { content, chatId } = req.body;
18
28
 
19
- if (debug) {
29
+ if (DEBUG) {
20
30
  console.log(
21
31
  '[handleAIChatMessage] content:',
22
32
  content,
@@ -31,9 +41,6 @@ export const handleAIChatMessage =
31
41
  }
32
42
 
33
43
  try {
34
- // Get existing files from ShareDB doc
35
- const files = shareDBDoc.data.files;
36
-
37
44
  // Ensure chats structure exists
38
45
  ensureChatsExist(shareDBDoc);
39
46
  ensureChatExists(shareDBDoc, chatId);
@@ -42,38 +49,35 @@ export const handleAIChatMessage =
42
49
  addUserMessage(shareDBDoc, chatId, content);
43
50
 
44
51
  // Create LLM function for streaming
45
- const llmFunction = createLLMFunction(
52
+ const llmFunction = createLLMFunction({
46
53
  shareDBDoc,
54
+ createVizBotLocalPresence,
47
55
  chatId,
48
- );
56
+ });
57
+
58
+ // Create server-side runCode function using shared module
59
+ const runCode = createRunCodeFunction(shareDBDoc);
49
60
 
50
61
  // Perform AI editing
51
- const editResult = await performAIEditing(
62
+ const editResult = await performAIEditing({
63
+ prompt: content,
52
64
  shareDBDoc,
53
- chatId,
54
- content,
55
- files,
56
65
  llmFunction,
57
- );
58
-
59
- // Add AI response message
60
- const aiResponse = addAIMessage(
61
- shareDBDoc,
62
- chatId,
63
- editResult.content,
64
- );
66
+ runCode,
67
+ });
65
68
 
66
69
  // Handle credit deduction if callback is provided
67
70
  if (
68
- options.onCreditDeduction &&
69
- editResult.upstreamCostCents
71
+ onCreditDeduction &&
72
+ (editResult as any).upstreamCostCents
70
73
  ) {
71
74
  try {
72
- await options.onCreditDeduction({
73
- upstreamCostCents: editResult.upstreamCostCents,
74
- provider: editResult.provider,
75
- inputTokens: editResult.inputTokens,
76
- outputTokens: editResult.outputTokens,
75
+ await onCreditDeduction({
76
+ upstreamCostCents: (editResult as any)
77
+ .upstreamCostCents,
78
+ provider: (editResult as any).provider,
79
+ inputTokens: (editResult as any).inputTokens,
80
+ outputTokens: (editResult as any).outputTokens,
77
81
  });
78
82
  } catch (creditError) {
79
83
  console.error(
@@ -84,7 +88,7 @@ export const handleAIChatMessage =
84
88
  }
85
89
  }
86
90
 
87
- res.status(200).json(aiResponse);
91
+ res.status(200).json('success');
88
92
  } catch (error) {
89
93
  handleError(shareDBDoc, chatId, error, res);
90
94
  }
@@ -0,0 +1,223 @@
1
+ import { StreamingMarkdownParser } from 'llm-code-format';
2
+ import { ChatOpenAI } from '@langchain/openai';
3
+ import fs from 'fs';
4
+ import {
5
+ updateAIStatus,
6
+ updateAIScratchpad,
7
+ ensureFileExists,
8
+ clearFileContent,
9
+ appendLineToFile,
10
+ } from './chatOperations.js';
11
+
12
+ const DEBUG = false;
13
+
14
+ /**
15
+ * Creates and configures the LLM function for streaming
16
+ */
17
+ export const createLLMFunction = ({
18
+ shareDBDoc,
19
+ createVizBotLocalPresence,
20
+ chatId,
21
+ }) => {
22
+ return async (fullPrompt) => {
23
+ const localPresence = createVizBotLocalPresence();
24
+ // Submit initial presence for VizBot
25
+ const vizBotPresence = {
26
+ username: 'VizBot',
27
+ start: ['files'], // Indicate VizBot is working on files
28
+ end: ['files'],
29
+ };
30
+
31
+ localPresence.submit(vizBotPresence, (error) => {
32
+ if (error) {
33
+ console.warn(
34
+ 'VizBot presence submission error:',
35
+ error,
36
+ );
37
+ }
38
+ });
39
+ const chatModel = new ChatOpenAI({
40
+ modelName:
41
+ process.env.VIZHUB_EDIT_WITH_AI_MODEL_NAME ||
42
+ 'anthropic/claude-sonnet-4',
43
+ configuration: {
44
+ apiKey: process.env.VIZHUB_EDIT_WITH_AI_API_KEY,
45
+ baseURL: process.env.VIZHUB_EDIT_WITH_AI_BASE_URL,
46
+ defaultHeaders: {
47
+ 'HTTP-Referer': 'https://vizhub.com',
48
+ 'X-Title': 'VizHub',
49
+ },
50
+ },
51
+ streaming: true,
52
+ });
53
+
54
+ let fullContent = '';
55
+ let generationId = '';
56
+ let currentEditingFileId = null;
57
+ let currentEditingFileName = null;
58
+
59
+ // Function to report file edited
60
+ // This is called when the AI has finished editing a file
61
+ // and we want to update the scratchpad with the file name.
62
+ const reportFileEdited = () => {
63
+ if (currentEditingFileName) {
64
+ fullContent += ` * Edited ${currentEditingFileName}\n`;
65
+ updateAIScratchpad(shareDBDoc, chatId, fullContent);
66
+ currentEditingFileName = null;
67
+ }
68
+ };
69
+
70
+ // Define callbacks for streaming parser
71
+ const callbacks = {
72
+ onFileNameChange: (fileName, format) => {
73
+ DEBUG &&
74
+ console.log(
75
+ `File changed to: ${fileName} (${format})`,
76
+ );
77
+
78
+ // Find existing file or create new one
79
+ currentEditingFileId = ensureFileExists(
80
+ shareDBDoc,
81
+ fileName,
82
+ );
83
+
84
+ reportFileEdited();
85
+ currentEditingFileName = fileName;
86
+
87
+ // Clear the file content to start fresh
88
+ // (AI will regenerate the entire file content)
89
+ clearFileContent(shareDBDoc, currentEditingFileId);
90
+
91
+ // Update VizBot presence to show it's editing this specific file
92
+ const filePresence = {
93
+ username: 'VizBot',
94
+ start: ['files', currentEditingFileId, 'text', 0],
95
+ end: ['files', currentEditingFileId, 'text', 0],
96
+ };
97
+
98
+ localPresence.submit(filePresence, (error) => {
99
+ if (error) {
100
+ console.warn(
101
+ 'VizBot file presence submission error:',
102
+ error,
103
+ );
104
+ }
105
+ });
106
+
107
+ // Update AI status
108
+ updateAIStatus(
109
+ shareDBDoc,
110
+ chatId,
111
+ 'Editing ' + fileName,
112
+ );
113
+ },
114
+ onCodeLine: (line) => {
115
+ DEBUG && console.log(`Code line: ${line}`);
116
+
117
+ if (currentEditingFileId) {
118
+ // Apply OT operation for this line immediately
119
+ appendLineToFile(
120
+ shareDBDoc,
121
+ currentEditingFileId,
122
+ line,
123
+ );
124
+
125
+ // Update VizBot presence to show cursor at the end of the file
126
+ const currentFile =
127
+ shareDBDoc.data.files[currentEditingFileId];
128
+ if (currentFile && currentFile.text) {
129
+ const textLength = currentFile.text.length;
130
+ const filePresence = {
131
+ username: 'VizBot',
132
+ start: [
133
+ 'files',
134
+ currentEditingFileId,
135
+ 'text',
136
+ textLength,
137
+ ],
138
+ end: [
139
+ 'files',
140
+ currentEditingFileId,
141
+ 'text',
142
+ textLength,
143
+ ],
144
+ };
145
+
146
+ localPresence.submit(filePresence, (error) => {
147
+ if (error) {
148
+ console.warn(
149
+ 'VizBot line presence submission error:',
150
+ error,
151
+ );
152
+ }
153
+ });
154
+ }
155
+ }
156
+ },
157
+ onNonCodeLine: (line) => {
158
+ // We want to report a file edited only if the line is not empty,
159
+ // because sometimes the LLMs leave a newline between the file name
160
+ // declaration and th
161
+ if (line.trim() !== '') {
162
+ reportFileEdited();
163
+ }
164
+ fullContent += line + '\n';
165
+ updateAIScratchpad(shareDBDoc, chatId, fullContent);
166
+ },
167
+ };
168
+
169
+ const parser = new StreamingMarkdownParser(callbacks);
170
+
171
+ const chunks = [];
172
+
173
+ // Stream the response
174
+ const stream = await chatModel.stream(fullPrompt);
175
+ for await (const chunk of stream) {
176
+ if (chunk.content) {
177
+ const chunkContent = String(chunk.content);
178
+ chunks.push(chunkContent);
179
+ parser.processChunk(chunkContent);
180
+ }
181
+
182
+ if (!generationId && chunk.lc_kwargs?.id) {
183
+ generationId = chunk.lc_kwargs.id;
184
+ }
185
+ }
186
+ parser.flushRemaining();
187
+ reportFileEdited();
188
+ updateAIStatus(shareDBDoc, chatId, 'Done editing.');
189
+ updateAIScratchpad(shareDBDoc, chatId, fullContent);
190
+
191
+ // Clear VizBot presence when done
192
+ DEBUG &&
193
+ console.log(
194
+ 'AI editing done, clearing VizBot presence',
195
+ );
196
+ localPresence.submit(null, (error) => {
197
+ DEBUG && console.log('VizBot presence cleared');
198
+ if (error) {
199
+ console.warn(
200
+ 'VizBot presence cleanup error:',
201
+ error,
202
+ );
203
+ }
204
+ });
205
+
206
+ // Write chunks file for debugging
207
+ if (DEBUG) {
208
+ const chunksFileJSONpath = `./ai-chunks-${chatId}.json`;
209
+ fs.writeFileSync(
210
+ chunksFileJSONpath,
211
+ JSON.stringify(chunks, null, 2),
212
+ );
213
+ console.log(
214
+ `AI chunks written to ${chunksFileJSONpath}`,
215
+ );
216
+ }
217
+
218
+ return {
219
+ content: fullContent,
220
+ generationId: generationId,
221
+ };
222
+ };
223
+ };
@@ -28,7 +28,8 @@ const slowdown = false;
28
28
 
29
29
  // The options passed into the OpenAI client
30
30
  // new OpenAI(openAIOptions)
31
- const openAIOptions = {};
31
+ const openAIOptions: { apiKey?: string; baseURL?: string } =
32
+ {};
32
33
 
33
34
  // Support specifying the API key via an environment variable
34
35
  // If VZCODE_AI_API_KEY is not set, note that the OpenAI client
@@ -71,7 +72,7 @@ const opComesFromAIAssist = (ops, source) =>
71
72
 
72
73
  // Keeps track of the currently ongoing AI streams.
73
74
  // There could be many streams at the same time.
74
- const streams = {};
75
+ export const streams: Record<string, any> = {};
75
76
 
76
77
  export const generateAIResponse = async ({
77
78
  inputText,
@@ -1,9 +1,12 @@
1
- import { generateAIResponse } from './generateAIResponse.js';
1
+ import {
2
+ generateAIResponse,
3
+ streams,
4
+ } from './generateAIResponse.js';
2
5
 
3
6
  const debug = false;
4
7
 
5
8
  export const handleAIAssist =
6
- (shareDBDoc) => async (req, res) => {
9
+ (shareDBDoc: any) => async (req: any, res: any) => {
7
10
  const { inputText, insertionCursor, fileId } = req.body;
8
11
 
9
12
  if (debug) {
@@ -16,17 +19,21 @@ export const handleAIAssist =
16
19
  }
17
20
 
18
21
  try {
22
+ // Generate a unique streamId for this request
23
+ const streamId = `stream_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
24
+
19
25
  await generateAIResponse({
20
26
  inputText,
21
27
  insertionCursor,
22
28
  fileId,
29
+ streamId,
23
30
  shareDBDoc,
24
31
  });
25
32
 
26
33
  res
27
34
  .status(200)
28
35
  .send({ message: 'Operation successful!' });
29
- } catch (error) {
36
+ } catch (error: any) {
30
37
  console.error('handleAIAssist error:', error);
31
38
  res.status(500).send({
32
39
  message: 'Internal Server Error',
@@ -35,7 +42,7 @@ export const handleAIAssist =
35
42
  }
36
43
  };
37
44
 
38
- export function haltGeneration(streamId) {
45
+ export function haltGeneration(streamId: string) {
39
46
  const stream = streams[streamId];
40
47
 
41
48
  // Stream can be undefined here if the user
@@ -4,7 +4,6 @@ import bodyParser from 'body-parser';
4
4
  import express from 'express';
5
5
  import fs from 'fs';
6
6
  import http from 'http';
7
- import ngrok from 'ngrok';
8
7
  import open from 'open';
9
8
  import path from 'path';
10
9
  import ShareDB from 'sharedb';
@@ -51,13 +50,6 @@ ShareDB.types.register(json1Presence.type);
51
50
 
52
51
  const app = express();
53
52
 
54
- // TODO make this configurable
55
- // See https://github.com/vizhub-core/vzcode/issues/95
56
- app.post('/saveTime', (req, res) => {
57
- //autoSaveDebounceTimeMS = req.body.autoSaveDebounceTimeMS;
58
- console.log('autoSaveDebounceTimeMS', req.body);
59
- });
60
-
61
53
  // Use ShareDB over WebSocket
62
54
  const shareDBBackend = new ShareDB({
63
55
  // Enable presence
@@ -82,7 +74,7 @@ wss.on('connection', (ws) => {
82
74
  });
83
75
 
84
76
  // Handle disconnections
85
- ws.on('close', (code) => {
77
+ ws.on('close', () => {
86
78
  clientStream.end();
87
79
  });
88
80
  });
@@ -97,6 +89,22 @@ app.use(express.static(dir));
97
89
  // which is a representation of files on disk.
98
90
  const shareDBConnection = shareDBBackend.connect();
99
91
  const shareDBDoc = shareDBConnection.get('documents', '1');
92
+
93
+ // Set up presence for VizBot following the same pattern as useShareDB.ts
94
+ const docPresence = shareDBConnection.getDocPresence(
95
+ 'documents',
96
+ '1',
97
+ );
98
+
99
+ // Create local presence for VizBot with a unique ID
100
+ const generateVizBotId = () => {
101
+ const timestamp = Date.now().toString(36);
102
+ return `vizbot-${timestamp}`;
103
+ };
104
+
105
+ const createVizBotLocalPresence = () =>
106
+ docPresence.create(generateVizBotId());
107
+
100
108
  shareDBDoc.create(initialDocument, json1Presence.type.uri);
101
109
 
102
110
  // Handle AI Assist requests.
@@ -117,7 +125,11 @@ app.post(
117
125
  app.post(
118
126
  '/ai-chat-message',
119
127
  bodyParser.json(),
120
- handleAIChatMessage(shareDBDoc),
128
+ handleAIChatMessage({
129
+ shareDBDoc,
130
+ createVizBotLocalPresence,
131
+ onCreditDeduction: undefined,
132
+ }),
121
133
  );
122
134
 
123
135
  // Livekit Token Generator
@@ -325,21 +337,13 @@ shareDBDoc.subscribe(() => {
325
337
  });
326
338
 
327
339
  server.listen(port, async () => {
328
- if (process.env.NGROK_TOKEN) {
329
- (async function () {
330
- await ngrok.authtoken(process.env.NGROK_TOKEN);
331
- const url = await ngrok.connect(port);
332
- console.log(`Editor is live at ${url}`);
333
- open(url);
334
- })();
335
- } else {
336
- // Sets the port to the one specified in the environment
337
- // variable (for development) or the default port.
338
- let livePort = process.env.EDITOR_PORT || port;
339
- console.log(`EDITOR_PORT: ${process.env.EDITOR_PORT}`);
340
- console.log(
341
- `Editor is live at http://localhost:${livePort}`,
342
- );
343
- open(`http://localhost:${livePort}`);
344
- }
340
+ // Note: ngrok support can be added when the package is properly installed
341
+ // Sets the port to the one specified in the environment
342
+ // variable (for development) or the default port.
343
+ let livePort = process.env.EDITOR_PORT || port;
344
+ console.log(`EDITOR_PORT: ${process.env.EDITOR_PORT}`);
345
+ console.log(
346
+ `Editor is live at http://localhost:${livePort}`,
347
+ );
348
+ open(`http://localhost:${livePort}`);
345
349
  });
@@ -0,0 +1,16 @@
1
+ import { diff } from './client/diff.js';
2
+ // TODO migrate the server-side code to use TypeScript,
3
+ // and delete this file once the migration is complete (use src/submitOperation.ts instead).
4
+ /**
5
+ * Creates a submitOperation function that can be used to submit diff-based operations to ShareDB.
6
+ * This is the core logic extracted from useSubmitOperation for reuse across client and server.
7
+ */
8
+ export const createSubmitOperation = (shareDBDoc) => {
9
+ return (next) => {
10
+ const data = shareDBDoc.data;
11
+ const op = diff(data, next(data));
12
+ if (op && shareDBDoc) {
13
+ shareDBDoc.submitOp(op);
14
+ }
15
+ };
16
+ };
@@ -0,0 +1,25 @@
1
+ import { diff } from './client/diff';
2
+
3
+ /**
4
+ * Creates a submitOperation function that can be used to submit diff-based operations to ShareDB.
5
+ * This is the core logic extracted from useSubmitOperation for reuse across client and server.
6
+ */
7
+ export const createSubmitOperation = <T>(shareDBDoc: {
8
+ data: T;
9
+ submitOp: (op: any) => void;
10
+ }): ((next: (data: T) => T) => void) => {
11
+ return (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
+ };
19
+
20
+ /**
21
+ * Type definition for the submitOperation function
22
+ */
23
+ export type SubmitOperationFunction<T> = (
24
+ next: (data: T) => T,
25
+ ) => void;