vzcode 2.28.0 → 2.29.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/bindings_wasm_bg--2fP_mkJ.wasm +0 -0
- package/dist/assets/buildWorker-BV4WiKg8.js +687 -0
- package/dist/assets/index-CCmGeE_1.css +1 -0
- package/dist/assets/index-JcHUw7hN.js +403 -0
- package/dist/assets/worker-AbbeoMbH.js +340 -0
- package/dist/assets/worker-B71kfwKL.js +125 -0
- package/dist/assets/{worker-k7HZ3t-V.js → worker-CoJaqtpi.js} +12 -12
- package/dist/index.html +2 -2
- package/dist/llm-streaming-server/chatOperations.js +38 -0
- package/dist/llm-streaming-server/llmStreaming.js +0 -1
- package/dist/server/aiChatHandler/index.js +44 -8
- package/package.json +34 -34
- package/src/client/VZCodeContext/types.ts +15 -3
- package/src/client/VZCodeContext/useVZCodeState.ts +36 -0
- package/src/llm-streaming-server/chatOperations.ts +48 -0
- package/src/llm-streaming-server/llmStreaming.ts +3 -1
- package/src/server/aiChatHandler/index.ts +77 -11
- package/src/types.ts +3 -0
- package/dist/assets/bindings_wasm_bg-9w8E-TmY.wasm +0 -0
- package/dist/assets/buildWorker-DylWm2VX.js +0 -687
- package/dist/assets/index-CTdYkAQU.js +0 -403
- package/dist/assets/index-Dyt9tpmu.css +0 -1
- package/dist/assets/worker-D8p1JlHW.js +0 -342
- package/dist/assets/worker-b9bkZkXK.js +0 -116
package/dist/index.html
CHANGED
|
@@ -20,8 +20,8 @@
|
|
|
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-
|
|
24
|
-
<link rel="stylesheet" crossorigin href="/assets/index-
|
|
23
|
+
<script type="module" crossorigin src="/assets/index-JcHUw7hN.js"></script>
|
|
24
|
+
<link rel="stylesheet" crossorigin href="/assets/index-CCmGeE_1.css">
|
|
25
25
|
</head>
|
|
26
26
|
<body>
|
|
27
27
|
<div id="root"></div>
|
|
@@ -58,6 +58,44 @@ export const addUserMessage = (shareDBDoc, chatId, content) => {
|
|
|
58
58
|
shareDBDoc.submitOp(userMessageOp);
|
|
59
59
|
return userMessage;
|
|
60
60
|
};
|
|
61
|
+
/**
|
|
62
|
+
* Persists AI metadata fields on a chat (Phase 3).
|
|
63
|
+
*
|
|
64
|
+
* `baseCommitId` records the commit the current AI attempt is applied on
|
|
65
|
+
* top of. `escalationLevel` records how many times the user has pressed
|
|
66
|
+
* "Try Harder". Persisting these on the chat makes retries idempotent and
|
|
67
|
+
* keeps the level consistent across reloads and clients.
|
|
68
|
+
*
|
|
69
|
+
* Only fields that are explicitly provided are written.
|
|
70
|
+
*/
|
|
71
|
+
export const setChatAIMetadata = (shareDBDoc, chatId, metadata) => {
|
|
72
|
+
const chat = shareDBDoc.data.chats[chatId];
|
|
73
|
+
if (!chat) {
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
const updates = {};
|
|
77
|
+
if (metadata.baseCommitId !== undefined) {
|
|
78
|
+
updates.baseCommitId = metadata.baseCommitId;
|
|
79
|
+
}
|
|
80
|
+
if (metadata.escalationLevel !== undefined) {
|
|
81
|
+
updates.escalationLevel = metadata.escalationLevel;
|
|
82
|
+
}
|
|
83
|
+
if (Object.keys(updates).length === 0) {
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
const op = diff(shareDBDoc.data, {
|
|
87
|
+
...shareDBDoc.data,
|
|
88
|
+
chats: {
|
|
89
|
+
...shareDBDoc.data.chats,
|
|
90
|
+
[chatId]: {
|
|
91
|
+
...chat,
|
|
92
|
+
...updates,
|
|
93
|
+
updatedAt: dateToTimestamp(new Date()),
|
|
94
|
+
},
|
|
95
|
+
},
|
|
96
|
+
});
|
|
97
|
+
shareDBDoc.submitOp(op);
|
|
98
|
+
};
|
|
61
99
|
const DEBUG = false;
|
|
62
100
|
/**
|
|
63
101
|
* Updates AI status in the chat
|
|
@@ -162,7 +162,6 @@ enableReasoningTokens = false, model, aiRequestOptions, }) => {
|
|
|
162
162
|
model: modelName,
|
|
163
163
|
messages: [{ role: 'user', content: fullPrompt }],
|
|
164
164
|
stream: true,
|
|
165
|
-
...aiRequestOptions,
|
|
166
165
|
};
|
|
167
166
|
// Only include reasoning configuration if reasoning tokens are enabled
|
|
168
167
|
// if (enableReasoningTokens) {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { validateRequest } from '../../llm-streaming-server/validation.js';
|
|
2
|
-
import { ensureChatsExist, ensureChatExists, addUserMessage, setAIStatus, } from '../../llm-streaming-server/chatOperations.js';
|
|
2
|
+
import { ensureChatsExist, ensureChatExists, addUserMessage, setAIStatus, setChatAIMetadata, } from '../../llm-streaming-server/chatOperations.js';
|
|
3
3
|
import { createLLMFunction } from '../../llm-streaming-server/llmStreaming.js';
|
|
4
4
|
import { performAIEditing } from '../../llm-streaming-server/aiEditing.js';
|
|
5
5
|
import { handleError, handleBackgroundError, } from '../../llm-streaming-server/errorHandling.js';
|
|
@@ -7,7 +7,7 @@ import { createRunCodeFunction } from '../../runCode.js';
|
|
|
7
7
|
import { createSubmitOperation } from '../../submitOperation.js';
|
|
8
8
|
import { getGenerationMetadata } from 'editcodewithai';
|
|
9
9
|
const DEBUG = false;
|
|
10
|
-
export const handleAIChatMessage = ({ shareDBDoc, onCreditDeduction, model, aiRequestOptions, enableReasoningTokens, }) => async (req, res) => {
|
|
10
|
+
export const handleAIChatMessage = ({ shareDBDoc, onCreditDeduction, onGenerationFinished, model, aiRequestOptions, enableReasoningTokens, baseCommitId, escalationLevel, }) => async (req, res) => {
|
|
11
11
|
const { content, chatId } = req.body;
|
|
12
12
|
if (DEBUG) {
|
|
13
13
|
console.log('[handleAIChatMessage] content:', content, 'chatId:', chatId, 'shareDBDoc:', shareDBDoc);
|
|
@@ -22,6 +22,12 @@ export const handleAIChatMessage = ({ shareDBDoc, onCreditDeduction, model, aiRe
|
|
|
22
22
|
ensureChatExists(shareDBDoc, chatId);
|
|
23
23
|
// Add user message to chat
|
|
24
24
|
addUserMessage(shareDBDoc, chatId, content);
|
|
25
|
+
// Persist explicit escalation metadata on the chat, so that
|
|
26
|
+
// retries are idempotent and the level survives reloads.
|
|
27
|
+
setChatAIMetadata(shareDBDoc, chatId, {
|
|
28
|
+
baseCommitId,
|
|
29
|
+
escalationLevel,
|
|
30
|
+
});
|
|
25
31
|
// Return success immediately - AI generation continues in background
|
|
26
32
|
res.status(200).json('success');
|
|
27
33
|
// Continue AI processing in background (don't await)
|
|
@@ -33,6 +39,7 @@ export const handleAIChatMessage = ({ shareDBDoc, onCreditDeduction, model, aiRe
|
|
|
33
39
|
aiRequestOptions,
|
|
34
40
|
enableReasoningTokens,
|
|
35
41
|
onCreditDeduction,
|
|
42
|
+
onGenerationFinished,
|
|
36
43
|
}).catch((error) => {
|
|
37
44
|
console.error('Background AI processing error:', error);
|
|
38
45
|
// Handle error without HTTP response
|
|
@@ -46,7 +53,7 @@ export const handleAIChatMessage = ({ shareDBDoc, onCreditDeduction, model, aiRe
|
|
|
46
53
|
/**
|
|
47
54
|
* Processes the AI request asynchronously in the background
|
|
48
55
|
*/
|
|
49
|
-
const processAIRequestAsync = async ({ shareDBDoc, chatId, content, model, aiRequestOptions, enableReasoningTokens, onCreditDeduction, }) => {
|
|
56
|
+
const processAIRequestAsync = async ({ shareDBDoc, chatId, content, model, aiRequestOptions, enableReasoningTokens, onCreditDeduction, onGenerationFinished, }) => {
|
|
50
57
|
try {
|
|
51
58
|
// Create LLM function for streaming
|
|
52
59
|
const llmFunction = createLLMFunction({
|
|
@@ -66,18 +73,34 @@ const processAIRequestAsync = async ({ shareDBDoc, chatId, content, model, aiReq
|
|
|
66
73
|
llmFunction,
|
|
67
74
|
runCode,
|
|
68
75
|
});
|
|
69
|
-
//
|
|
76
|
+
// Billing is best-effort. It must NEVER prevent the edit from
|
|
77
|
+
// being committed. Any metadata failure is logged and swallowed.
|
|
78
|
+
let metrics = null;
|
|
70
79
|
if (onCreditDeduction && editResult.generationId) {
|
|
71
80
|
try {
|
|
72
|
-
|
|
81
|
+
metrics = await getGenerationMetadata({
|
|
73
82
|
apiKey: aiRequestOptions?.apiKey ||
|
|
74
83
|
process.env.VZCODE_EDIT_WITH_AI_API_KEY,
|
|
75
84
|
generationId: editResult.generationId,
|
|
76
|
-
})
|
|
85
|
+
});
|
|
86
|
+
await onCreditDeduction(metrics);
|
|
77
87
|
}
|
|
78
88
|
catch (creditError) {
|
|
79
|
-
console.error('Credit deduction error:', creditError);
|
|
80
|
-
|
|
89
|
+
console.error('Credit deduction error (edit will still be finalized):', creditError);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
// Always notify settlement on success, regardless of billing
|
|
93
|
+
// outcome, so the caller can commit the edit and release locks.
|
|
94
|
+
if (onGenerationFinished) {
|
|
95
|
+
try {
|
|
96
|
+
await onGenerationFinished({
|
|
97
|
+
success: true,
|
|
98
|
+
editResult,
|
|
99
|
+
metrics,
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
catch (settleError) {
|
|
103
|
+
console.error('onGenerationFinished (success) error:', settleError);
|
|
81
104
|
}
|
|
82
105
|
}
|
|
83
106
|
// Clear the AI status to indicate completion
|
|
@@ -86,6 +109,19 @@ const processAIRequestAsync = async ({ shareDBDoc, chatId, content, model, aiReq
|
|
|
86
109
|
catch (error) {
|
|
87
110
|
// Set error status and add error message to chat
|
|
88
111
|
setAIStatus(shareDBDoc, chatId, 'error');
|
|
112
|
+
// Always notify settlement on failure so the caller can roll back
|
|
113
|
+
// the pre-restore snapshot and release locks.
|
|
114
|
+
if (onGenerationFinished) {
|
|
115
|
+
try {
|
|
116
|
+
await onGenerationFinished({
|
|
117
|
+
success: false,
|
|
118
|
+
error,
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
catch (settleError) {
|
|
122
|
+
console.error('onGenerationFinished (failure) error:', settleError);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
89
125
|
handleBackgroundError(shareDBDoc, chatId, error);
|
|
90
126
|
}
|
|
91
127
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "vzcode",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.29.0",
|
|
4
4
|
"description": "Multiplayer code editor system",
|
|
5
5
|
"main": "src/index.ts",
|
|
6
6
|
"type": "module",
|
|
@@ -117,11 +117,11 @@
|
|
|
117
117
|
"@codemirror/lang-json": "^6.0.2",
|
|
118
118
|
"@codemirror/lang-markdown": "^6.5.0",
|
|
119
119
|
"@codemirror/lint": "^6.9.2",
|
|
120
|
-
"@codemirror/state": "^6.5.
|
|
120
|
+
"@codemirror/state": "^6.5.3",
|
|
121
121
|
"@codemirror/theme-one-dark": "^6.1.3",
|
|
122
|
-
"@codemirror/view": "^6.
|
|
123
|
-
"@langchain/core": "^1.1.
|
|
124
|
-
"@langchain/openai": "^1.
|
|
122
|
+
"@codemirror/view": "^6.39.8",
|
|
123
|
+
"@langchain/core": "^1.1.8",
|
|
124
|
+
"@langchain/openai": "^1.2.0",
|
|
125
125
|
"@lezer/highlight": "^1.2.3",
|
|
126
126
|
"@livekit/components-react": "^2.9.17",
|
|
127
127
|
"@replit/codemirror-indentation-markers": "^6.5.3",
|
|
@@ -130,15 +130,15 @@
|
|
|
130
130
|
"@replit/codemirror-vscode-keymap": "^6.0.2",
|
|
131
131
|
"@teamwork/websocket-json-stream": "^2.0.0",
|
|
132
132
|
"@typescript/vfs": "^1.6.2",
|
|
133
|
-
"@uiw/codemirror-theme-abcdef": "^4.25.
|
|
134
|
-
"@uiw/codemirror-theme-dracula": "^4.25.
|
|
135
|
-
"@uiw/codemirror-theme-eclipse": "^4.25.
|
|
136
|
-
"@uiw/codemirror-theme-github": "^4.25.
|
|
137
|
-
"@uiw/codemirror-theme-material": "^4.25.
|
|
138
|
-
"@uiw/codemirror-theme-nord": "^4.25.
|
|
139
|
-
"@uiw/codemirror-theme-okaidia": "^4.25.
|
|
140
|
-
"@uiw/codemirror-theme-xcode": "^4.25.
|
|
141
|
-
"@uiw/codemirror-themes": "^4.25.
|
|
133
|
+
"@uiw/codemirror-theme-abcdef": "^4.25.4",
|
|
134
|
+
"@uiw/codemirror-theme-dracula": "^4.25.4",
|
|
135
|
+
"@uiw/codemirror-theme-eclipse": "^4.25.4",
|
|
136
|
+
"@uiw/codemirror-theme-github": "^4.25.4",
|
|
137
|
+
"@uiw/codemirror-theme-material": "^4.25.4",
|
|
138
|
+
"@uiw/codemirror-theme-nord": "^4.25.4",
|
|
139
|
+
"@uiw/codemirror-theme-okaidia": "^4.25.4",
|
|
140
|
+
"@uiw/codemirror-theme-xcode": "^4.25.4",
|
|
141
|
+
"@uiw/codemirror-themes": "^4.25.4",
|
|
142
142
|
"@valtown/codemirror-ts": "^2.3.1",
|
|
143
143
|
"@vizhub/runtime": "^4.5.0",
|
|
144
144
|
"@vizhub/viz-types": "^0.5.0",
|
|
@@ -154,25 +154,25 @@
|
|
|
154
154
|
"d3-color": "^3.1.0",
|
|
155
155
|
"diff": "^8.0.2",
|
|
156
156
|
"@dmsnell/diff-match-patch": "^1.1.0",
|
|
157
|
-
"diff2html": "
|
|
157
|
+
"diff2html": "3.4.52",
|
|
158
158
|
"dotenv": "^17.2.3",
|
|
159
159
|
"editcodewithai": "^2.4.0",
|
|
160
|
-
"eslint-linter-browserify": "^9.39.
|
|
160
|
+
"eslint-linter-browserify": "^9.39.2",
|
|
161
161
|
"express": "^5.2.1",
|
|
162
162
|
"ignore": "^7.0.5",
|
|
163
163
|
"json0-ot-diff": "^1.1.2",
|
|
164
164
|
"jszip": "^3.10.1",
|
|
165
|
-
"livekit-server-sdk": "^2.
|
|
165
|
+
"livekit-server-sdk": "^2.15.0",
|
|
166
166
|
"llm-code-format": "^3.1.0",
|
|
167
|
-
"lucide-react": "^0.
|
|
168
|
-
"npm": "^11.
|
|
167
|
+
"lucide-react": "^0.562.0",
|
|
168
|
+
"npm": "^11.7.0",
|
|
169
169
|
"open": "^11.0.0",
|
|
170
|
-
"prettier-plugin-svelte": "^3.4.
|
|
170
|
+
"prettier-plugin-svelte": "^3.4.1",
|
|
171
171
|
"react": "^18",
|
|
172
172
|
"react-bootstrap": "^2.10.10",
|
|
173
173
|
"react-dom": "^18",
|
|
174
174
|
"react-markdown": "^10.1.0",
|
|
175
|
-
"react-router-dom": "^7.
|
|
175
|
+
"react-router-dom": "^7.11.0",
|
|
176
176
|
"remark-gfm": "^4.0.1",
|
|
177
177
|
"sharedb": "^5.2.2",
|
|
178
178
|
"sharedb-client-browser": "^5.2.2",
|
|
@@ -181,32 +181,32 @@
|
|
|
181
181
|
"ws": "^8.18.3"
|
|
182
182
|
},
|
|
183
183
|
"devDependencies": {
|
|
184
|
-
"@eslint/js": "^9.39.
|
|
185
|
-
"@tauri-apps/cli": "^2.9.
|
|
184
|
+
"@eslint/js": "^9.39.2",
|
|
185
|
+
"@tauri-apps/cli": "^2.9.6",
|
|
186
186
|
"@types/d3-color": "^3.1.3",
|
|
187
187
|
"@types/react": "^18",
|
|
188
188
|
"@types/react-dom": "^18",
|
|
189
|
-
"@typescript-eslint/eslint-plugin": "^8.
|
|
190
|
-
"@typescript-eslint/parser": "^8.
|
|
191
|
-
"@vitejs/plugin-react": "^5.1.
|
|
189
|
+
"@typescript-eslint/eslint-plugin": "^8.51.0",
|
|
190
|
+
"@typescript-eslint/parser": "^8.51.0",
|
|
191
|
+
"@vitejs/plugin-react": "^5.1.2",
|
|
192
192
|
"concurrently": "^9.2.1",
|
|
193
193
|
"cross-env": "^10.1.0",
|
|
194
|
-
"eslint": "^9.39.
|
|
194
|
+
"eslint": "^9.39.2",
|
|
195
195
|
"eslint-plugin-jsx-a11y": "^6.10.2",
|
|
196
196
|
"eslint-plugin-react": "^7.37.5",
|
|
197
197
|
"eslint-plugin-react-hooks": "^7.0.1",
|
|
198
|
-
"globals": "^
|
|
199
|
-
"npm-check-updates": "^19.
|
|
198
|
+
"globals": "^17.0.0",
|
|
199
|
+
"npm-check-updates": "^19.2.0",
|
|
200
200
|
"prettier": "^3.7.4",
|
|
201
|
-
"sass": "^1.
|
|
201
|
+
"sass": "^1.97.1",
|
|
202
202
|
"ts-node": "^10.9.2",
|
|
203
203
|
"tsx": "^4.21.0",
|
|
204
204
|
"typescript": "^5.9.3",
|
|
205
|
-
"vite": "^7.
|
|
206
|
-
"vitest": "^4.0.
|
|
205
|
+
"vite": "^7.3.0",
|
|
206
|
+
"vitest": "^4.0.16"
|
|
207
207
|
},
|
|
208
208
|
"optionalDependencies": {
|
|
209
|
-
"@rollup/rollup-darwin-arm64": "^4.
|
|
210
|
-
"@rollup/rollup-win32-x64-msvc": "^4.
|
|
209
|
+
"@rollup/rollup-darwin-arm64": "^4.54.0",
|
|
210
|
+
"@rollup/rollup-win32-x64-msvc": "^4.54.0"
|
|
211
211
|
}
|
|
212
212
|
}
|
|
@@ -178,7 +178,7 @@ export type VZCodeContextValue = {
|
|
|
178
178
|
handleSendMessage: (
|
|
179
179
|
messageToSend?: string,
|
|
180
180
|
options?: Record<string, string>,
|
|
181
|
-
) =>
|
|
181
|
+
) => Promise<any>;
|
|
182
182
|
|
|
183
183
|
// Message history navigation
|
|
184
184
|
navigateMessageHistoryUp: () => void;
|
|
@@ -204,11 +204,18 @@ export type VZCodeContextValue = {
|
|
|
204
204
|
handleSendMessage?: (
|
|
205
205
|
messageToSend?: string,
|
|
206
206
|
options?: Record<string, string>,
|
|
207
|
-
) =>
|
|
207
|
+
) => Promise<any>;
|
|
208
208
|
}) => React.ReactNode;
|
|
209
209
|
|
|
210
210
|
// Feature flags
|
|
211
211
|
enableMinimalEditFlow: boolean;
|
|
212
|
+
|
|
213
|
+
// Analytics callback for chat message submissions
|
|
214
|
+
onAIChatMessageSubmitted?: (params: {
|
|
215
|
+
messageLength: number;
|
|
216
|
+
isRetry?: boolean;
|
|
217
|
+
isUndo?: boolean;
|
|
218
|
+
}) => void;
|
|
212
219
|
};
|
|
213
220
|
|
|
214
221
|
export interface VZCodeProviderProps {
|
|
@@ -247,11 +254,16 @@ export interface VZCodeProviderProps {
|
|
|
247
254
|
handleSendMessage?: (
|
|
248
255
|
messageToSend?: string,
|
|
249
256
|
options?: Record<string, string>,
|
|
250
|
-
) =>
|
|
257
|
+
) => Promise<any>;
|
|
251
258
|
}) => React.ReactNode;
|
|
252
259
|
iframeRef?: React.MutableRefObject<HTMLIFrameElement>;
|
|
253
260
|
handleChatError?: (
|
|
254
261
|
error: string,
|
|
255
262
|
message?: string,
|
|
256
263
|
) => void;
|
|
264
|
+
onAIChatMessageSubmitted?: (params: {
|
|
265
|
+
messageLength: number;
|
|
266
|
+
isRetry?: boolean;
|
|
267
|
+
isUndo?: boolean;
|
|
268
|
+
}) => void;
|
|
257
269
|
}
|
|
@@ -54,6 +54,7 @@ export const useVZCodeState = ({
|
|
|
54
54
|
additionalWidgets,
|
|
55
55
|
iframeRef: externalIframeRef,
|
|
56
56
|
handleChatError,
|
|
57
|
+
onAIChatMessageSubmitted,
|
|
57
58
|
}: Omit<
|
|
58
59
|
VZCodeProviderProps,
|
|
59
60
|
'children'
|
|
@@ -312,6 +313,22 @@ export const useVZCodeState = ({
|
|
|
312
313
|
[handleChatError],
|
|
313
314
|
);
|
|
314
315
|
|
|
316
|
+
// Phase 5: Restore the prompt into the chat input when the server
|
|
317
|
+
// populates `currentChatDraft` (e.g. after Undo), then clear it so it
|
|
318
|
+
// is applied exactly once.
|
|
319
|
+
useEffect(() => {
|
|
320
|
+
const draft = (content as any)?.currentChatDraft;
|
|
321
|
+
if (typeof draft === 'string' && draft.trim()) {
|
|
322
|
+
setAIChatMessage(draft);
|
|
323
|
+
submitOperation((currentContent) => {
|
|
324
|
+
const next: any = { ...currentContent };
|
|
325
|
+
next.currentChatDraft = undefined;
|
|
326
|
+
return next;
|
|
327
|
+
});
|
|
328
|
+
}
|
|
329
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
330
|
+
}, [(content as any)?.currentChatDraft]);
|
|
331
|
+
|
|
315
332
|
// Message history navigation functions
|
|
316
333
|
const navigateMessageHistoryUp = useCallback(() => {
|
|
317
334
|
if (messageHistory.length === 0) return;
|
|
@@ -388,6 +405,21 @@ export const useVZCodeState = ({
|
|
|
388
405
|
|
|
389
406
|
setAIErrorMessage(null); // Clear any previous errors
|
|
390
407
|
|
|
408
|
+
// Track AI chat message submission
|
|
409
|
+
const isRetry = options?.isRetry === 'true';
|
|
410
|
+
const isUndo = options?.isUndo === 'true';
|
|
411
|
+
if (
|
|
412
|
+
!DEBUG &&
|
|
413
|
+
onAIChatMessageSubmitted &&
|
|
414
|
+
aiChatOptions?.vizId
|
|
415
|
+
) {
|
|
416
|
+
onAIChatMessageSubmitted({
|
|
417
|
+
messageLength: messageContent.trim().length,
|
|
418
|
+
isRetry,
|
|
419
|
+
isUndo,
|
|
420
|
+
});
|
|
421
|
+
}
|
|
422
|
+
|
|
391
423
|
// Generate new chat ID only for fresh user messages
|
|
392
424
|
// Keep existing chat ID for retries (Try Harder) and undos
|
|
393
425
|
const isRetryOrUndo =
|
|
@@ -493,6 +525,7 @@ export const useVZCodeState = ({
|
|
|
493
525
|
|
|
494
526
|
// The backend handles all ShareDB operations for successful responses
|
|
495
527
|
// The loading state is now managed via ShareDB aiStatus
|
|
528
|
+
return responseData;
|
|
496
529
|
} catch (error) {
|
|
497
530
|
console.error('Error getting AI response:', error);
|
|
498
531
|
setAIErrorMessage(
|
|
@@ -732,5 +765,8 @@ export const useVZCodeState = ({
|
|
|
732
765
|
|
|
733
766
|
// Feature flags
|
|
734
767
|
enableMinimalEditFlow,
|
|
768
|
+
|
|
769
|
+
// Analytics callback for chat message submissions
|
|
770
|
+
onAIChatMessageSubmitted,
|
|
735
771
|
};
|
|
736
772
|
};
|
|
@@ -81,6 +81,54 @@ export const addUserMessage = (
|
|
|
81
81
|
return userMessage;
|
|
82
82
|
};
|
|
83
83
|
|
|
84
|
+
/**
|
|
85
|
+
* Persists AI metadata fields on a chat (Phase 3).
|
|
86
|
+
*
|
|
87
|
+
* `baseCommitId` records the commit the current AI attempt is applied on
|
|
88
|
+
* top of. `escalationLevel` records how many times the user has pressed
|
|
89
|
+
* "Try Harder". Persisting these on the chat makes retries idempotent and
|
|
90
|
+
* keeps the level consistent across reloads and clients.
|
|
91
|
+
*
|
|
92
|
+
* Only fields that are explicitly provided are written.
|
|
93
|
+
*/
|
|
94
|
+
export const setChatAIMetadata = (
|
|
95
|
+
shareDBDoc: ShareDBDoc<VizContent>,
|
|
96
|
+
chatId: VizChatId,
|
|
97
|
+
metadata: {
|
|
98
|
+
baseCommitId?: string;
|
|
99
|
+
escalationLevel?: number;
|
|
100
|
+
},
|
|
101
|
+
) => {
|
|
102
|
+
const chat = shareDBDoc.data.chats[chatId];
|
|
103
|
+
if (!chat) {
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const updates: Record<string, unknown> = {};
|
|
108
|
+
if (metadata.baseCommitId !== undefined) {
|
|
109
|
+
updates.baseCommitId = metadata.baseCommitId;
|
|
110
|
+
}
|
|
111
|
+
if (metadata.escalationLevel !== undefined) {
|
|
112
|
+
updates.escalationLevel = metadata.escalationLevel;
|
|
113
|
+
}
|
|
114
|
+
if (Object.keys(updates).length === 0) {
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const op = diff(shareDBDoc.data, {
|
|
119
|
+
...shareDBDoc.data,
|
|
120
|
+
chats: {
|
|
121
|
+
...shareDBDoc.data.chats,
|
|
122
|
+
[chatId]: {
|
|
123
|
+
...chat,
|
|
124
|
+
...updates,
|
|
125
|
+
updatedAt: dateToTimestamp(new Date()),
|
|
126
|
+
},
|
|
127
|
+
},
|
|
128
|
+
});
|
|
129
|
+
shareDBDoc.submitOp(op);
|
|
130
|
+
};
|
|
131
|
+
|
|
84
132
|
const DEBUG = false;
|
|
85
133
|
|
|
86
134
|
/**
|
|
@@ -78,6 +78,7 @@ export const createLLMFunction = ({
|
|
|
78
78
|
},
|
|
79
79
|
});
|
|
80
80
|
|
|
81
|
+
|
|
81
82
|
let fullContent = '';
|
|
82
83
|
let generationId = '';
|
|
83
84
|
let currentEditingFileName = null;
|
|
@@ -252,12 +253,13 @@ export const createLLMFunction = ({
|
|
|
252
253
|
const chunks = [];
|
|
253
254
|
let reasoningContent = '';
|
|
254
255
|
|
|
256
|
+
|
|
257
|
+
|
|
255
258
|
// Configure reasoning tokens based on enableReasoningTokens flag
|
|
256
259
|
const requestConfig: any = {
|
|
257
260
|
model: modelName,
|
|
258
261
|
messages: [{ role: 'user', content: fullPrompt }],
|
|
259
262
|
stream: true,
|
|
260
|
-
...aiRequestOptions,
|
|
261
263
|
};
|
|
262
264
|
|
|
263
265
|
// Only include reasoning configuration if reasoning tokens are enabled
|
|
@@ -4,6 +4,7 @@ import {
|
|
|
4
4
|
ensureChatExists,
|
|
5
5
|
addUserMessage,
|
|
6
6
|
setAIStatus,
|
|
7
|
+
setChatAIMetadata,
|
|
7
8
|
} from '../../llm-streaming-server/chatOperations.js';
|
|
8
9
|
import { createLLMFunction } from '../../llm-streaming-server/llmStreaming.js';
|
|
9
10
|
import { performAIEditing } from '../../llm-streaming-server/aiEditing.js';
|
|
@@ -23,15 +24,31 @@ export const handleAIChatMessage =
|
|
|
23
24
|
({
|
|
24
25
|
shareDBDoc,
|
|
25
26
|
onCreditDeduction,
|
|
27
|
+
onGenerationFinished,
|
|
26
28
|
model,
|
|
27
29
|
aiRequestOptions,
|
|
28
30
|
enableReasoningTokens,
|
|
31
|
+
baseCommitId,
|
|
32
|
+
escalationLevel,
|
|
29
33
|
}: {
|
|
30
34
|
shareDBDoc: ShareDBDoc<VizContent>;
|
|
31
35
|
onCreditDeduction?: any;
|
|
36
|
+
onGenerationFinished?: (result: {
|
|
37
|
+
success: boolean;
|
|
38
|
+
editResult?: any;
|
|
39
|
+
metrics?: any;
|
|
40
|
+
error?: any;
|
|
41
|
+
}) => Promise<void> | void;
|
|
32
42
|
model?: string;
|
|
33
43
|
aiRequestOptions?: any;
|
|
34
44
|
enableReasoningTokens?: boolean;
|
|
45
|
+
// Phase 3: explicit escalation base. Persisted on the chat so that
|
|
46
|
+
// repeated "Try Harder" clicks are idempotent and do not depend on
|
|
47
|
+
// the fragile `parent(currentCommit)` heuristic.
|
|
48
|
+
baseCommitId?: string;
|
|
49
|
+
// Explicit escalation level, persisted on the chat so it survives
|
|
50
|
+
// reloads and is shared across clients.
|
|
51
|
+
escalationLevel?: number;
|
|
35
52
|
}) =>
|
|
36
53
|
async (req: any, res: any) => {
|
|
37
54
|
const { content, chatId } = req.body;
|
|
@@ -60,6 +77,13 @@ export const handleAIChatMessage =
|
|
|
60
77
|
// Add user message to chat
|
|
61
78
|
addUserMessage(shareDBDoc, chatId, content);
|
|
62
79
|
|
|
80
|
+
// Persist explicit escalation metadata on the chat, so that
|
|
81
|
+
// retries are idempotent and the level survives reloads.
|
|
82
|
+
setChatAIMetadata(shareDBDoc, chatId, {
|
|
83
|
+
baseCommitId,
|
|
84
|
+
escalationLevel,
|
|
85
|
+
});
|
|
86
|
+
|
|
63
87
|
// Return success immediately - AI generation continues in background
|
|
64
88
|
res.status(200).json('success');
|
|
65
89
|
|
|
@@ -72,6 +96,7 @@ export const handleAIChatMessage =
|
|
|
72
96
|
aiRequestOptions,
|
|
73
97
|
enableReasoningTokens,
|
|
74
98
|
onCreditDeduction,
|
|
99
|
+
onGenerationFinished,
|
|
75
100
|
}).catch((error) => {
|
|
76
101
|
console.error(
|
|
77
102
|
'Background AI processing error:',
|
|
@@ -96,6 +121,7 @@ const processAIRequestAsync = async ({
|
|
|
96
121
|
aiRequestOptions,
|
|
97
122
|
enableReasoningTokens,
|
|
98
123
|
onCreditDeduction,
|
|
124
|
+
onGenerationFinished,
|
|
99
125
|
}: {
|
|
100
126
|
shareDBDoc: ShareDBDoc<VizContent>;
|
|
101
127
|
chatId: string;
|
|
@@ -104,6 +130,12 @@ const processAIRequestAsync = async ({
|
|
|
104
130
|
aiRequestOptions?: any;
|
|
105
131
|
enableReasoningTokens?: boolean;
|
|
106
132
|
onCreditDeduction?: any;
|
|
133
|
+
onGenerationFinished?: (result: {
|
|
134
|
+
success: boolean;
|
|
135
|
+
editResult?: any;
|
|
136
|
+
metrics?: any;
|
|
137
|
+
error?: any;
|
|
138
|
+
}) => Promise<void> | void;
|
|
107
139
|
}) => {
|
|
108
140
|
try {
|
|
109
141
|
// Create LLM function for streaming
|
|
@@ -128,23 +160,40 @@ const processAIRequestAsync = async ({
|
|
|
128
160
|
runCode,
|
|
129
161
|
});
|
|
130
162
|
|
|
131
|
-
//
|
|
163
|
+
// Billing is best-effort. It must NEVER prevent the edit from
|
|
164
|
+
// being committed. Any metadata failure is logged and swallowed.
|
|
165
|
+
let metrics: any = null;
|
|
132
166
|
if (onCreditDeduction && editResult.generationId) {
|
|
133
167
|
try {
|
|
134
|
-
await
|
|
135
|
-
|
|
136
|
-
apiKey
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
);
|
|
168
|
+
metrics = await getGenerationMetadata({
|
|
169
|
+
apiKey:
|
|
170
|
+
aiRequestOptions?.apiKey ||
|
|
171
|
+
process.env.VZCODE_EDIT_WITH_AI_API_KEY,
|
|
172
|
+
generationId: editResult.generationId,
|
|
173
|
+
});
|
|
174
|
+
await onCreditDeduction(metrics);
|
|
142
175
|
} catch (creditError) {
|
|
143
176
|
console.error(
|
|
144
|
-
'Credit deduction error:',
|
|
177
|
+
'Credit deduction error (edit will still be finalized):',
|
|
145
178
|
creditError,
|
|
146
179
|
);
|
|
147
|
-
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// Always notify settlement on success, regardless of billing
|
|
184
|
+
// outcome, so the caller can commit the edit and release locks.
|
|
185
|
+
if (onGenerationFinished) {
|
|
186
|
+
try {
|
|
187
|
+
await onGenerationFinished({
|
|
188
|
+
success: true,
|
|
189
|
+
editResult,
|
|
190
|
+
metrics,
|
|
191
|
+
});
|
|
192
|
+
} catch (settleError) {
|
|
193
|
+
console.error(
|
|
194
|
+
'onGenerationFinished (success) error:',
|
|
195
|
+
settleError,
|
|
196
|
+
);
|
|
148
197
|
}
|
|
149
198
|
}
|
|
150
199
|
|
|
@@ -153,6 +202,23 @@ const processAIRequestAsync = async ({
|
|
|
153
202
|
} catch (error) {
|
|
154
203
|
// Set error status and add error message to chat
|
|
155
204
|
setAIStatus(shareDBDoc, chatId, 'error');
|
|
205
|
+
|
|
206
|
+
// Always notify settlement on failure so the caller can roll back
|
|
207
|
+
// the pre-restore snapshot and release locks.
|
|
208
|
+
if (onGenerationFinished) {
|
|
209
|
+
try {
|
|
210
|
+
await onGenerationFinished({
|
|
211
|
+
success: false,
|
|
212
|
+
error,
|
|
213
|
+
});
|
|
214
|
+
} catch (settleError) {
|
|
215
|
+
console.error(
|
|
216
|
+
'onGenerationFinished (failure) error:',
|
|
217
|
+
settleError,
|
|
218
|
+
);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
156
222
|
handleBackgroundError(shareDBDoc, chatId, error);
|
|
157
223
|
}
|
|
158
224
|
};
|
package/src/types.ts
CHANGED
|
@@ -262,6 +262,9 @@ export interface ExtendedVizChat extends VizChat {
|
|
|
262
262
|
currentStatus?: string;
|
|
263
263
|
isStreaming?: boolean;
|
|
264
264
|
model?: string; // The LLM model used for this chat
|
|
265
|
+
// Phase 3: explicit escalation metadata persisted on the chat.
|
|
266
|
+
baseCommitId?: string; // Commit the current AI attempt is applied on top of
|
|
267
|
+
escalationLevel?: number; // How many times "Try Harder" has been pressed
|
|
265
268
|
}
|
|
266
269
|
|
|
267
270
|
// Extended VizChatMessage with progressive rendering support
|
|
Binary file
|