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.
- package/dist/assets/bootstrap-icons-BeopsB42.woff +0 -0
- package/dist/assets/bootstrap-icons-mSm7cUeB.woff2 +0 -0
- package/dist/assets/{index-DMIy-XE4.css → index-Br76WttW.css} +5 -1
- package/dist/assets/{index-S-R8oY72.js → index-vbF7Aejt.js} +174 -174
- package/dist/index.html +2 -2
- package/package.json +7 -6
- package/src/client/Icons/AdjustmentSVG.tsx +19 -0
- package/src/client/Icons/index.tsx +1 -0
- package/src/client/VZCodeContext/types.ts +13 -1
- package/src/client/VZCodeContext/useVZCodeState.ts +83 -5
- package/src/client/VZRight.tsx +6 -3
- package/src/client/VZSidebar/AIChat/ChatInput.tsx +45 -2
- package/src/client/VZSidebar/AIChat/MessageList.tsx +2 -0
- package/src/client/VZSidebar/AIChat/index.tsx +12 -0
- package/src/client/VZSidebar/EmptyState.tsx +11 -0
- package/src/client/VZSidebar/VisualEditor.tsx +295 -0
- package/src/client/VZSidebar/index.tsx +64 -0
- package/src/client/VZSidebar/styles.scss +213 -1
- package/src/client/bootstrap.ts +3 -0
- package/src/client/useActions.ts +12 -0
- package/src/client/vzReducer/closeTabReducer.test.ts +33 -21
- package/src/client/vzReducer/createInitialState.ts +1 -0
- package/src/client/vzReducer/index.ts +9 -0
- package/src/client/vzReducer/searchReducer.test.ts +1 -1
- package/src/client/vzReducer/visualEditorReducer.ts +9 -0
- package/src/server/aiChatHandler/chatOperations.ts +22 -0
- package/src/server/aiChatHandler/errorHandling.ts +17 -4
- package/src/server/aiChatHandler/index.ts +131 -67
- package/src/server/aiChatHandler/llmStreaming.ts +76 -27
- package/src/types.ts +12 -0
|
@@ -16,8 +16,11 @@ describe('closeTabsReducer', () => {
|
|
|
16
16
|
|
|
17
17
|
const stateWithTabs = {
|
|
18
18
|
...initialState,
|
|
19
|
-
|
|
20
|
-
|
|
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
|
|
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
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
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
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
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
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
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 = {
|
|
@@ -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(
|
|
102
|
+
expect(file1Results.matches[1].index).toBe(17); // position of "HELLO"
|
|
103
103
|
|
|
104
104
|
// Check file2.js results
|
|
105
105
|
const file2Results =
|
|
@@ -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
|
|
50
|
-
|
|
51
|
-
|
|
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 {
|
|
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
|
-
//
|
|
68
|
-
|
|
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
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
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
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
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
|
-
|
|
133
|
-
|
|
134
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
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' };
|