vzcode 1.60.0 → 1.62.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 (30) hide show
  1. package/dist/assets/bootstrap-icons-BeopsB42.woff +0 -0
  2. package/dist/assets/bootstrap-icons-mSm7cUeB.woff2 +0 -0
  3. package/dist/assets/{index-DMIy-XE4.css → index-Br76WttW.css} +5 -1
  4. package/dist/assets/{index-S-R8oY72.js → index-vbF7Aejt.js} +174 -174
  5. package/dist/index.html +2 -2
  6. package/package.json +7 -6
  7. package/src/client/Icons/AdjustmentSVG.tsx +19 -0
  8. package/src/client/Icons/index.tsx +1 -0
  9. package/src/client/VZCodeContext/types.ts +13 -1
  10. package/src/client/VZCodeContext/useVZCodeState.ts +83 -5
  11. package/src/client/VZRight.tsx +6 -3
  12. package/src/client/VZSidebar/AIChat/ChatInput.tsx +45 -2
  13. package/src/client/VZSidebar/AIChat/MessageList.tsx +2 -0
  14. package/src/client/VZSidebar/AIChat/index.tsx +12 -0
  15. package/src/client/VZSidebar/EmptyState.tsx +11 -0
  16. package/src/client/VZSidebar/VisualEditor.tsx +295 -0
  17. package/src/client/VZSidebar/index.tsx +64 -0
  18. package/src/client/VZSidebar/styles.scss +213 -1
  19. package/src/client/bootstrap.ts +3 -0
  20. package/src/client/useActions.ts +12 -0
  21. package/src/client/vzReducer/closeTabReducer.test.ts +33 -21
  22. package/src/client/vzReducer/createInitialState.ts +1 -0
  23. package/src/client/vzReducer/index.ts +9 -0
  24. package/src/client/vzReducer/searchReducer.test.ts +1 -1
  25. package/src/client/vzReducer/visualEditorReducer.ts +9 -0
  26. package/src/server/aiChatHandler/chatOperations.ts +22 -0
  27. package/src/server/aiChatHandler/errorHandling.ts +17 -4
  28. package/src/server/aiChatHandler/index.ts +131 -67
  29. package/src/server/aiChatHandler/llmStreaming.ts +76 -27
  30. package/src/types.ts +12 -0
@@ -16,8 +16,11 @@ describe('closeTabsReducer', () => {
16
16
 
17
17
  const stateWithTabs = {
18
18
  ...initialState,
19
- tabList: [{ fileId: 'file1', isTransient: false }],
20
- activeFileId: 'file1',
19
+ pane: {
20
+ ...initialState.pane,
21
+ tabList: [{ fileId: 'file1', isTransient: false }],
22
+ activeFileId: 'file1',
23
+ },
21
24
  };
22
25
 
23
26
  const newState = closeTabsReducer(
@@ -58,7 +61,7 @@ describe('closeTabsReducer', () => {
58
61
  expect(newState.pane.activeFileId).toBe('file1');
59
62
  });
60
63
 
61
- it.only('Activates previous tab after closing an active tab', () => {
64
+ it('Activates previous tab after closing an active tab', () => {
62
65
  const action: VZAction = {
63
66
  type: 'close_tabs',
64
67
  fileIdsToClose: ['file2'],
@@ -116,12 +119,15 @@ describe('closeTabsReducer', () => {
116
119
  it('Closes a tab that is not currently active', () => {
117
120
  const stateWithMultipleTabs = {
118
121
  ...initialState,
119
- tabList: [
120
- { fileId: 'file1', isTransient: false },
121
- { fileId: 'file2', isTransient: false },
122
- { fileId: 'file3', isTransient: false },
123
- ],
124
- activeFileId: 'file3',
122
+ pane: {
123
+ ...initialState.pane,
124
+ tabList: [
125
+ { fileId: 'file1', isTransient: false },
126
+ { fileId: 'file2', isTransient: false },
127
+ { fileId: 'file3', isTransient: false },
128
+ ],
129
+ activeFileId: 'file3',
130
+ },
125
131
  };
126
132
 
127
133
  const action: VZAction = {
@@ -142,12 +148,15 @@ describe('closeTabsReducer', () => {
142
148
  it('Closes the first tab out of many, when it is active', () => {
143
149
  const stateWithMultipleTabs = {
144
150
  ...initialState,
145
- tabList: [
146
- { fileId: 'file1', isTransient: false },
147
- { fileId: 'file2', isTransient: false },
148
- { fileId: 'file3', isTransient: false },
149
- ],
150
- activeFileId: 'file1',
151
+ pane: {
152
+ ...initialState.pane,
153
+ tabList: [
154
+ { fileId: 'file1', isTransient: false },
155
+ { fileId: 'file2', isTransient: false },
156
+ { fileId: 'file3', isTransient: false },
157
+ ],
158
+ activeFileId: 'file1',
159
+ },
151
160
  };
152
161
 
153
162
  const action: VZAction = {
@@ -168,12 +177,15 @@ describe('closeTabsReducer', () => {
168
177
  it('Closes multiple tabs at once', () => {
169
178
  const stateWithMultipleTabs = {
170
179
  ...initialState,
171
- tabList: [
172
- { fileId: 'file1', isTransient: false },
173
- { fileId: 'file2', isTransient: false },
174
- { fileId: 'file3', isTransient: false },
175
- ],
176
- activeFileId: 'file2',
180
+ pane: {
181
+ ...initialState.pane,
182
+ tabList: [
183
+ { fileId: 'file1', isTransient: false },
184
+ { fileId: 'file2', isTransient: false },
185
+ { fileId: 'file3', isTransient: false },
186
+ ],
187
+ activeFileId: 'file2',
188
+ },
177
189
  };
178
190
 
179
191
  const action: VZAction = {
@@ -24,6 +24,7 @@ export const createInitialState = ({
24
24
  focusedIndex: null,
25
25
  focusedChildIndex: null,
26
26
  },
27
+ isVisualEditorOpen: false,
27
28
  isSearchOpen: false,
28
29
  isAIChatOpen: true,
29
30
  aiChatFocused: false,
@@ -38,6 +38,7 @@ import {
38
38
  import { toggleAutoFollowReducer } from './toggleAutoFollowReducer';
39
39
  import { updatePresenceIndicatorReducer } from './updatePresenceIndicatorReducer';
40
40
  import { splitCurrentPaneReducer } from './splitCurrentPaneReducer';
41
+ import { setIsVisualEditorOpenReducer } from './visualEditorReducer';
41
42
  export { createInitialState } from './createInitialState';
42
43
 
43
44
  // The shape of the state managed by the reducer.
@@ -57,6 +58,9 @@ export type VZState = {
57
58
  // True to show the search instead of files
58
59
  isSearchOpen: boolean;
59
60
 
61
+ // True to show the visual editor instead of files
62
+ isVisualEditorOpen: boolean;
63
+
60
64
  // True to show the AI chat instead of files
61
65
  isAIChatOpen: boolean;
62
66
 
@@ -133,6 +137,10 @@ export type VZAction =
133
137
  // * Sets whether the AI chat tab is open.
134
138
  | { type: 'set_is_ai_chat_open'; value: boolean }
135
139
 
140
+ // 'set_is_visual_editor_open'
141
+ // * Sets whether the Visual Editor is open.
142
+ | { type: 'set_is_visual_editor_open'; value: boolean }
143
+
136
144
  // `toggle_ai_chat_focused`
137
145
  // * Toggles focused variable to trigger AI chat input focus
138
146
  | { type: 'toggle_ai_chat_focused' }
@@ -219,6 +227,7 @@ const reducers = [
219
227
  toggleSearchFocusedReducer,
220
228
  setIsSearchOpenReducer,
221
229
  setIsAIChatOpenReducer,
230
+ setIsVisualEditorOpenReducer,
222
231
  toggleAIChatFocusedReducer,
223
232
  setAIChatModeReducer,
224
233
  setIsSettingsOpenReducer,
@@ -99,7 +99,7 @@ describe('searchReducer', () => {
99
99
  expect(file1Results.matches[0].line).toBe(1);
100
100
  expect(file1Results.matches[0].index).toBe(6); // position of "Hello"
101
101
  expect(file1Results.matches[1].line).toBe(2);
102
- expect(file1Results.matches[1].index).toBe(18); // position of "HELLO"
102
+ expect(file1Results.matches[1].index).toBe(17); // position of "HELLO"
103
103
 
104
104
  // Check file2.js results
105
105
  const file2Results =
@@ -0,0 +1,9 @@
1
+ import { VZAction, VZState } from '.';
2
+
3
+ export const setIsVisualEditorOpenReducer = (
4
+ state: VZState,
5
+ action: VZAction,
6
+ ): VZState =>
7
+ action.type === 'set_is_visual_editor_open'
8
+ ? { ...state, isVisualEditorOpen: action.value }
9
+ : state;
@@ -227,6 +227,28 @@ export const updateAIMessageContent = (
227
227
  shareDBDoc.submitOp(messageOp);
228
228
  };
229
229
 
230
+ /**
231
+ * Sets the AI status for a chat
232
+ */
233
+ export const setAIStatus = (
234
+ shareDBDoc: ShareDBDoc<VizContent>,
235
+ chatId: VizChatId,
236
+ status: string | undefined,
237
+ ) => {
238
+ const op = diff(shareDBDoc.data, {
239
+ ...shareDBDoc.data,
240
+ chats: {
241
+ ...shareDBDoc.data.chats,
242
+ [chatId]: {
243
+ ...shareDBDoc.data.chats[chatId],
244
+ aiStatus: status,
245
+ updatedAt: dateToTimestamp(new Date()),
246
+ },
247
+ },
248
+ });
249
+ shareDBDoc.submitOp(op);
250
+ };
251
+
230
252
  /**
231
253
  * Finalizes an AI message by clearing temporary fields
232
254
  */
@@ -46,8 +46,21 @@ export const handleError = (
46
46
  );
47
47
  }
48
48
 
49
- res.status(500).json({
50
- error: 'Internal server error',
51
- message: error.message,
52
- });
49
+ if (res) {
50
+ res.status(500).json({
51
+ error: 'Internal server error',
52
+ message: error.message,
53
+ });
54
+ }
55
+ };
56
+
57
+ /**
58
+ * Handles errors in background processing (without HTTP response)
59
+ */
60
+ export const handleBackgroundError = (
61
+ shareDBDoc,
62
+ chatId,
63
+ error,
64
+ ) => {
65
+ handleError(shareDBDoc, chatId, error, null);
53
66
  };
@@ -4,13 +4,17 @@ import {
4
4
  ensureChatExists,
5
5
  addUserMessage,
6
6
  addDiffToAIMessage,
7
+ setAIStatus,
7
8
  } from './chatOperations.js';
8
9
  import { createLLMFunction } from './llmStreaming.js';
9
10
  import {
10
11
  performAIEditing,
11
12
  performAIChat,
12
13
  } from './aiEditing.js';
13
- import { handleError } from './errorHandling.js';
14
+ import {
15
+ handleError,
16
+ handleBackgroundError,
17
+ } from './errorHandling.js';
14
18
  import { createRunCodeFunction } from '../../runCode.js';
15
19
  import { ShareDBDoc } from '../../types.js';
16
20
  import { VizContent } from '@vizhub/viz-types';
@@ -25,12 +29,14 @@ export const handleAIChatMessage =
25
29
  onCreditDeduction,
26
30
  getCurrentCommitId,
27
31
  model,
32
+ aiRequestOptions,
28
33
  }: {
29
34
  shareDBDoc: ShareDBDoc<VizContent>;
30
35
  createAIEditLocalPresence: () => any;
31
36
  onCreditDeduction?: any;
32
37
  getCurrentCommitId?: () => string | null;
33
38
  model?: string;
39
+ aiRequestOptions?: any;
34
40
  }) =>
35
41
  async (req: any, res: any) => {
36
42
  const { content, chatId, mode = 'edit' } = req.body;
@@ -56,81 +62,139 @@ export const handleAIChatMessage =
56
62
  ensureChatsExist(shareDBDoc);
57
63
  ensureChatExists(shareDBDoc, chatId);
58
64
 
59
- // Capture the current commit ID before making changes (for VizHub integration)
60
- const beforeCommitId = getCurrentCommitId
61
- ? getCurrentCommitId()
62
- : null;
63
-
64
65
  // Add user message to chat
65
66
  addUserMessage(shareDBDoc, chatId, content);
66
67
 
67
- // Create LLM function for streaming
68
- const llmFunction = createLLMFunction({
68
+ // Return success immediately - AI generation continues in background
69
+ res.status(200).json('success');
70
+
71
+ // Set AI status to indicate generation is starting
72
+ setAIStatus(shareDBDoc, chatId, 'generating');
73
+
74
+ // Continue AI processing in background (don't await)
75
+ processAIRequestAsync({
69
76
  shareDBDoc,
70
- createAIEditLocalPresence,
71
77
  chatId,
78
+ content,
79
+ mode,
80
+ createAIEditLocalPresence,
81
+ getCurrentCommitId,
72
82
  model,
83
+ aiRequestOptions,
84
+ onCreditDeduction,
85
+ }).catch((error) => {
86
+ console.error(
87
+ 'Background AI processing error:',
88
+ error,
89
+ );
90
+ // Handle error without HTTP response
91
+ handleBackgroundError(shareDBDoc, chatId, error);
73
92
  });
93
+ } catch (error) {
94
+ handleError(shareDBDoc, chatId, error, res);
95
+ }
96
+ };
74
97
 
75
- // Create server-side runCode function using shareDBDoc
76
- const submitOperation =
77
- createSubmitOperation(shareDBDoc);
78
- const runCode =
79
- createRunCodeFunction(submitOperation);
80
-
81
- // Perform AI editing or chat based on mode
82
- const editResult =
83
- mode === 'ask'
84
- ? await performAIChat({
85
- prompt: content,
86
- shareDBDoc,
87
- llmFunction,
88
- })
89
- : await performAIEditing({
90
- prompt: content,
91
- shareDBDoc,
92
- llmFunction,
93
- runCode,
94
- });
95
-
96
- // Add diff data to the AI message if there are changes
97
- if (
98
- editResult.diffData &&
99
- Object.keys(editResult.diffData).length > 0
100
- ) {
101
- addDiffToAIMessage(
102
- shareDBDoc,
103
- chatId,
104
- editResult.diffData,
105
- (editResult as any).beforeFiles, // Pass the beforeFiles snapshot for undo (legacy)
106
- beforeCommitId, // Pass the commit ID before AI changes (for VizHub integration)
107
- );
108
- }
98
+ /**
99
+ * Processes the AI request asynchronously in the background
100
+ */
101
+ const processAIRequestAsync = async ({
102
+ shareDBDoc,
103
+ chatId,
104
+ content,
105
+ mode,
106
+ createAIEditLocalPresence,
107
+ getCurrentCommitId,
108
+ model,
109
+ aiRequestOptions,
110
+ onCreditDeduction,
111
+ }: {
112
+ shareDBDoc: ShareDBDoc<VizContent>;
113
+ chatId: string;
114
+ content: string;
115
+ mode: string;
116
+ createAIEditLocalPresence: () => any;
117
+ getCurrentCommitId?: () => string | null;
118
+ model?: string;
119
+ aiRequestOptions?: any;
120
+ onCreditDeduction?: any;
121
+ }) => {
122
+ try {
123
+ // Capture the current commit ID before making changes (for VizHub integration)
124
+ const beforeCommitId = getCurrentCommitId
125
+ ? getCurrentCommitId()
126
+ : null;
109
127
 
110
- // Handle credit deduction if callback is provided
111
- if (
112
- onCreditDeduction &&
113
- (editResult as any).upstreamCostCents
114
- ) {
115
- try {
116
- await onCreditDeduction({
117
- upstreamCostCents: (editResult as any)
118
- .upstreamCostCents,
119
- provider: (editResult as any).provider,
120
- inputTokens: (editResult as any).inputTokens,
121
- outputTokens: (editResult as any).outputTokens,
128
+ // Create LLM function for streaming
129
+ const llmFunction = createLLMFunction({
130
+ shareDBDoc,
131
+ createAIEditLocalPresence,
132
+ chatId,
133
+ model,
134
+ aiRequestOptions,
135
+ });
136
+
137
+ // Create server-side runCode function using shareDBDoc
138
+ const submitOperation =
139
+ createSubmitOperation(shareDBDoc);
140
+ const runCode = createRunCodeFunction(submitOperation);
141
+
142
+ // Perform AI editing or chat based on mode
143
+ const editResult =
144
+ mode === 'ask'
145
+ ? await performAIChat({
146
+ prompt: content,
147
+ shareDBDoc,
148
+ llmFunction,
149
+ })
150
+ : await performAIEditing({
151
+ prompt: content,
152
+ shareDBDoc,
153
+ llmFunction,
154
+ runCode,
122
155
  });
123
- } catch (creditError) {
124
- console.error(
125
- 'Credit deduction error:',
126
- creditError,
127
- );
128
- // Don't fail the request if credit deduction fails
129
- }
130
- }
131
156
 
132
- res.status(200).json('success');
133
- } catch (error) {
134
- handleError(shareDBDoc, chatId, error, res);
157
+ // Add diff data to the AI message if there are changes
158
+ if (
159
+ editResult.diffData &&
160
+ Object.keys(editResult.diffData).length > 0
161
+ ) {
162
+ addDiffToAIMessage(
163
+ shareDBDoc,
164
+ chatId,
165
+ editResult.diffData,
166
+ (editResult as any).beforeFiles, // Pass the beforeFiles snapshot for undo (legacy)
167
+ beforeCommitId, // Pass the commit ID before AI changes (for VizHub integration)
168
+ );
135
169
  }
136
- };
170
+
171
+ // Handle credit deduction if callback is provided
172
+ if (
173
+ onCreditDeduction &&
174
+ (editResult as any).upstreamCostCents
175
+ ) {
176
+ try {
177
+ await onCreditDeduction({
178
+ upstreamCostCents: (editResult as any)
179
+ .upstreamCostCents,
180
+ provider: (editResult as any).provider,
181
+ inputTokens: (editResult as any).inputTokens,
182
+ outputTokens: (editResult as any).outputTokens,
183
+ });
184
+ } catch (creditError) {
185
+ console.error(
186
+ 'Credit deduction error:',
187
+ creditError,
188
+ );
189
+ // Don't fail the request if credit deduction fails
190
+ }
191
+ }
192
+
193
+ // Clear the AI status to indicate completion
194
+ setAIStatus(shareDBDoc, chatId, undefined);
195
+ } catch (error) {
196
+ // Set error status and add error message to chat
197
+ setAIStatus(shareDBDoc, chatId, 'error');
198
+ handleBackgroundError(shareDBDoc, chatId, error);
199
+ }
200
+ };
@@ -23,6 +23,17 @@ import { ShareDBDoc } from '../../types.js';
23
23
 
24
24
  const DEBUG = false;
25
25
 
26
+ // Useful for testing/debugging the streaming behavior
27
+ const slowMode = false;
28
+
29
+ // Throttle the streaming updates, so that we don't
30
+ // overwhelm the ShareDB server with too many updates.
31
+ // It happened actually, before adding this.
32
+ // MongoDB VizHub server got in fact overloaded with
33
+ // too many updates from the AI streaming response, with
34
+ // warning: "Replication Oplog Window has gone below 1 hour"
35
+ const THROTTLE_INTERVAL_MS = 100;
36
+
26
37
  // Feature flag to enable/disable streaming editing.
27
38
  // * If `true`, the AI streaming response will be used to
28
39
  // edit files in real-time by submitting ShareDB ops.
@@ -50,12 +61,14 @@ export const createLLMFunction = ({
50
61
  // and reasoning content is not processed in the streaming response.
51
62
  enableReasoningTokens = false,
52
63
  model,
64
+ aiRequestOptions,
53
65
  }: {
54
66
  shareDBDoc: ShareDBDoc<VizContent>;
55
67
  createAIEditLocalPresence: () => any;
56
68
  chatId: VizChatId;
57
69
  enableReasoningTokens?: boolean;
58
70
  model?: string;
71
+ aiRequestOptions?: any;
59
72
  }) => {
60
73
  return async (fullPrompt: string) => {
61
74
  const localPresence = enableStreamingEditing
@@ -82,18 +95,63 @@ export const createLLMFunction = ({
82
95
  // Create initial AI message for streaming
83
96
  const aiMessageId = createAIMessage(shareDBDoc, chatId);
84
97
 
98
+ // --- throttle wrapper ---
99
+ function makeThrottledUpdater() {
100
+ let lastCall = 0;
101
+ let latestContent = '';
102
+ let timer: NodeJS.Timeout | null = null;
103
+
104
+ function invoke() {
105
+ updateAIMessageContent(
106
+ shareDBDoc,
107
+ chatId,
108
+ aiMessageId,
109
+ latestContent,
110
+ );
111
+ lastCall = Date.now();
112
+ }
113
+
114
+ const fn = (content: string) => {
115
+ latestContent = content;
116
+ const now = Date.now();
117
+
118
+ if (now - lastCall >= THROTTLE_INTERVAL_MS) {
119
+ // safe to call immediately
120
+ invoke();
121
+ } else if (!timer) {
122
+ // schedule for later
123
+ timer = setTimeout(
124
+ () => {
125
+ timer = null;
126
+ invoke();
127
+ },
128
+ THROTTLE_INTERVAL_MS - (now - lastCall),
129
+ );
130
+ }
131
+ };
132
+
133
+ // expose a flush() helper to force an immediate write
134
+ fn.flush = () => {
135
+ if (timer) {
136
+ clearTimeout(timer);
137
+ timer = null;
138
+ }
139
+ invoke();
140
+ };
141
+
142
+ return fn;
143
+ }
144
+
145
+ const throttledUpdateAIMessageContent =
146
+ makeThrottledUpdater();
147
+
85
148
  // Function to report file edited
86
149
  // This is called when the AI has finished editing a file
87
150
  // and we want to update the message content with the file name.
88
151
  const reportFileEdited = () => {
89
152
  if (currentEditingFileName) {
90
153
  fullContent += ` * Edited ${currentEditingFileName}\n`;
91
- updateAIMessageContent(
92
- shareDBDoc,
93
- chatId,
94
- aiMessageId,
95
- fullContent,
96
- );
154
+ throttledUpdateAIMessageContent(fullContent);
97
155
  currentEditingFileName = null;
98
156
  }
99
157
  };
@@ -186,12 +244,7 @@ export const createLLMFunction = ({
186
244
  reportFileEdited();
187
245
  }
188
246
  fullContent += line + '\n';
189
- updateAIMessageContent(
190
- shareDBDoc,
191
- chatId,
192
- aiMessageId,
193
- fullContent,
194
- );
247
+ throttledUpdateAIMessageContent(fullContent);
195
248
  },
196
249
  };
197
250
 
@@ -210,11 +263,9 @@ export const createLLMFunction = ({
210
263
  const requestConfig: any = {
211
264
  model: modelName,
212
265
  messages: [{ role: 'user', content: fullPrompt }],
213
- provider: {
214
- sort: 'price',
215
- },
216
266
  usage: { include: true },
217
267
  stream: true,
268
+ ...aiRequestOptions,
218
269
  };
219
270
 
220
271
  // Only include reasoning configuration if reasoning tokens are enabled
@@ -233,6 +284,11 @@ export const createLLMFunction = ({
233
284
  let contentStarted = false;
234
285
 
235
286
  for await (const chunk of stream) {
287
+ if (slowMode) {
288
+ await new Promise((resolve) =>
289
+ setTimeout(resolve, 500),
290
+ );
291
+ }
236
292
  const delta = chunk.choices[0]?.delta as any; // Type assertion for OpenRouter-specific reasoning fields
237
293
 
238
294
  if (delta?.reasoning && enableReasoningTokens) {
@@ -267,12 +323,7 @@ export const createLLMFunction = ({
267
323
  await parser.processChunk(chunkContent);
268
324
  } else {
269
325
  fullContent += chunkContent;
270
- updateAIMessageContent(
271
- shareDBDoc,
272
- chatId,
273
- aiMessageId,
274
- fullContent,
275
- );
326
+ throttledUpdateAIMessageContent(fullContent);
276
327
  }
277
328
  } else if (chunk.usage) {
278
329
  // Handle usage information
@@ -289,12 +340,10 @@ export const createLLMFunction = ({
289
340
  // Final cleanup - clear scratchpad and set final status
290
341
  updateAIScratchpad(shareDBDoc, chatId, '');
291
342
  updateAIStatus(shareDBDoc, chatId, 'Done editing.');
292
- updateAIMessageContent(
293
- shareDBDoc,
294
- chatId,
295
- aiMessageId,
296
- fullContent,
297
- );
343
+ throttledUpdateAIMessageContent(fullContent);
344
+
345
+ // Flush to ensure the final content is written immediately
346
+ throttledUpdateAIMessageContent.flush();
298
347
 
299
348
  // Finalize the AI message by clearing temporary fields
300
349
  finalizeAIMessage(shareDBDoc, chatId);
package/src/types.ts CHANGED
@@ -191,3 +191,15 @@ export type Username = string;
191
191
  export type SubmitOperation = (
192
192
  next: (content: VizContent) => VizContent,
193
193
  ) => void;
194
+
195
+ // A value that has the capacity to be changed in the visual editor
196
+ export type VisualEditorConfigEntry =
197
+ | {
198
+ type: 'number';
199
+ property: string;
200
+ label: string;
201
+ min: number;
202
+ max: number;
203
+ value: number;
204
+ }
205
+ | { type: 'boolean' };