vzcode 2.30.0 → 2.31.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/{index-CD0x02Rq.js → index-BfDRE_ou.js} +113 -113
- package/dist/index.html +1 -1
- package/dist/llm-streaming-server/chatOperations.js +40 -10
- package/package.json +1 -1
- package/src/client/CodeEditor/getOrCreateEditor.tsx +6 -1
- package/src/client/CodeEditor/index.tsx +4 -0
- package/src/client/VZCodeContext/useVZCodeState.ts +1 -0
- package/src/client/VZLeft.tsx +17 -2
- package/src/client/VZMiddle.tsx +12 -0
- package/src/client/VZSidebar/VisualEditor/VisualEditor.tsx +101 -48
- package/src/client/VZSidebar/VisualEditor/utils.ts +113 -0
- package/src/client/VZSidebar/index.tsx +23 -6
- package/src/client/aiChatButtonEvent.ts +61 -0
- package/src/client/index.ts +6 -0
- package/src/client/main.jsx +22 -0
- package/src/llm-streaming-server/chatOperations.ts +53 -12
- package/src/llm-streaming-server/llmStreaming.ts +0 -3
- package/src/llm-streaming-ui/components/Message.tsx +0 -2
package/dist/index.html
CHANGED
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
href="https://fonts.googleapis.com/css2?family=Poppins:wght@300;400;500;600;700&display=swap"
|
|
21
21
|
rel="stylesheet"
|
|
22
22
|
/>
|
|
23
|
-
<script type="module" crossorigin src="/assets/index-
|
|
23
|
+
<script type="module" crossorigin src="/assets/index-BfDRE_ou.js"></script>
|
|
24
24
|
<link rel="stylesheet" crossorigin href="/assets/index-Dyt9tpmu.css">
|
|
25
25
|
</head>
|
|
26
26
|
<body>
|
|
@@ -248,22 +248,50 @@ export const finalizeAIMessage = (shareDBDoc, chatId) => {
|
|
|
248
248
|
shareDBDoc.submitOp(op);
|
|
249
249
|
};
|
|
250
250
|
/**
|
|
251
|
-
* Adds diff data to
|
|
251
|
+
* Adds diff data to a specific AI message by ID
|
|
252
|
+
* Phase 3 Fix: Use explicit message ID instead of assuming
|
|
253
|
+
* last message is AI, preventing bugs with rapid message sends
|
|
252
254
|
*/
|
|
253
|
-
export const addDiffToAIMessage = (shareDBDoc, chatId, diffData, beforeCommitId
|
|
255
|
+
export const addDiffToAIMessage = (shareDBDoc, chatId, diffData, beforeCommitId, // Commit ID before AI changes
|
|
256
|
+
messageId) => {
|
|
254
257
|
const chat = shareDBDoc.data.chats[chatId];
|
|
258
|
+
if (!chat || !chat.messages) {
|
|
259
|
+
console.warn(`Chat ${chatId} not found or has no messages`);
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
255
262
|
const messages = [...chat.messages];
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
if (
|
|
259
|
-
messages
|
|
263
|
+
let targetMessageIndex = -1;
|
|
264
|
+
// Phase 3 Fix: Use explicit message ID if provided
|
|
265
|
+
if (messageId) {
|
|
266
|
+
targetMessageIndex = messages.findIndex((msg) => msg.id === messageId);
|
|
267
|
+
if (targetMessageIndex === -1 ||
|
|
268
|
+
messages[targetMessageIndex].role !== 'assistant') {
|
|
269
|
+
console.warn(`AI message with id ${messageId} not found in chat ${chatId}`);
|
|
270
|
+
return;
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
else {
|
|
274
|
+
// Fallback: Find the most recent AI message (safer than
|
|
275
|
+
// just assuming last message)
|
|
276
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
277
|
+
if (messages[i].role === 'assistant') {
|
|
278
|
+
targetMessageIndex = i;
|
|
279
|
+
break;
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
if (targetMessageIndex === -1) {
|
|
283
|
+
console.warn(`No AI message found in chat ${chatId}`);
|
|
284
|
+
return;
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
// Update the target message with diff data
|
|
288
|
+
if (targetMessageIndex >= 0) {
|
|
260
289
|
const newMessage = {
|
|
261
|
-
...messages[
|
|
290
|
+
...messages[targetMessageIndex],
|
|
262
291
|
diffData,
|
|
263
|
-
...(beforeCommitId && { beforeCommitId }),
|
|
292
|
+
...(beforeCommitId && { beforeCommitId }),
|
|
264
293
|
};
|
|
265
|
-
|
|
266
|
-
messages[lastAIMessageIndex] = newMessage;
|
|
294
|
+
messages[targetMessageIndex] = newMessage;
|
|
267
295
|
const messageOp = diff(shareDBDoc.data, {
|
|
268
296
|
...shareDBDoc.data,
|
|
269
297
|
chats: {
|
|
@@ -276,6 +304,8 @@ export const addDiffToAIMessage = (shareDBDoc, chatId, diffData, beforeCommitId)
|
|
|
276
304
|
},
|
|
277
305
|
});
|
|
278
306
|
shareDBDoc.submitOp(messageOp);
|
|
307
|
+
DEBUG &&
|
|
308
|
+
console.log(`addDiffToAIMessage: Successfully added diff to message ${targetMessageIndex} in chat ${chatId}`);
|
|
279
309
|
}
|
|
280
310
|
};
|
|
281
311
|
/**
|
package/package.json
CHANGED
|
@@ -189,6 +189,7 @@ export const getOrCreateEditor = async ({
|
|
|
189
189
|
rainbowBracketsEnabled = true,
|
|
190
190
|
setIsAIChatOpen,
|
|
191
191
|
setAIChatMessage,
|
|
192
|
+
enableBuiltInAIChat = true,
|
|
192
193
|
}: {
|
|
193
194
|
// TODO pass this in from the outside
|
|
194
195
|
paneId?: PaneId;
|
|
@@ -226,6 +227,10 @@ export const getOrCreateEditor = async ({
|
|
|
226
227
|
rainbowBracketsEnabled?: boolean; // New parameter type
|
|
227
228
|
setIsAIChatOpen: (isAIChatOpen: boolean) => void;
|
|
228
229
|
setAIChatMessage: (message: string) => void;
|
|
230
|
+
// When false, skip the "Implement the TODO" widget, which opens
|
|
231
|
+
// VZCode's built-in AI chat (hosts that own the AI entry point
|
|
232
|
+
// disable it).
|
|
233
|
+
enableBuiltInAIChat?: boolean;
|
|
229
234
|
}): Promise<ExtendedEditorCacheValue> => {
|
|
230
235
|
// Cache hit
|
|
231
236
|
|
|
@@ -580,7 +585,7 @@ export const getOrCreateEditor = async ({
|
|
|
580
585
|
return todoPlugin;
|
|
581
586
|
}
|
|
582
587
|
|
|
583
|
-
if (enableToDoPlugin) {
|
|
588
|
+
if (enableToDoPlugin && enableBuiltInAIChat) {
|
|
584
589
|
extensions.push(createToDoPlugin());
|
|
585
590
|
}
|
|
586
591
|
}
|
|
@@ -22,12 +22,14 @@ export const CodeEditor = ({
|
|
|
22
22
|
customInteractRules,
|
|
23
23
|
aiCopilotEndpoint,
|
|
24
24
|
esLintSource,
|
|
25
|
+
enableBuiltInAIChat = true,
|
|
25
26
|
}: {
|
|
26
27
|
customInteractRules?: Array<InteractRule>;
|
|
27
28
|
aiCopilotEndpoint?: string;
|
|
28
29
|
esLintSource: (
|
|
29
30
|
view: EditorView,
|
|
30
31
|
) => Promise<readonly Diagnostic[]>;
|
|
32
|
+
enableBuiltInAIChat?: boolean;
|
|
31
33
|
}) => {
|
|
32
34
|
const {
|
|
33
35
|
activePane,
|
|
@@ -118,6 +120,7 @@ export const CodeEditor = ({
|
|
|
118
120
|
esLintSource,
|
|
119
121
|
setIsAIChatOpen,
|
|
120
122
|
setAIChatMessage,
|
|
123
|
+
enableBuiltInAIChat,
|
|
121
124
|
});
|
|
122
125
|
|
|
123
126
|
if (isMounted) {
|
|
@@ -143,6 +146,7 @@ export const CodeEditor = ({
|
|
|
143
146
|
esLintSource,
|
|
144
147
|
setIsAIChatOpen,
|
|
145
148
|
setAIChatMessage,
|
|
149
|
+
enableBuiltInAIChat,
|
|
146
150
|
]);
|
|
147
151
|
|
|
148
152
|
// Every time the active file switches from one file to another,
|
package/src/client/VZLeft.tsx
CHANGED
|
@@ -10,10 +10,25 @@ import { VoiceChatModal } from './VZSidebar/VoiceChatModal';
|
|
|
10
10
|
// * The sidebar
|
|
11
11
|
// * The settings modal
|
|
12
12
|
// * The create file modal
|
|
13
|
-
|
|
13
|
+
//
|
|
14
|
+
// `enableAIChat` is forwarded to `VZSidebar` so a host can hide the
|
|
15
|
+
// "Edit with AI" sidebar button. `undefined` falls back to the
|
|
16
|
+
// compile-time feature flag.
|
|
17
|
+
export const VZLeft = ({
|
|
18
|
+
enableUsernameField = true,
|
|
19
|
+
enableAIChat,
|
|
20
|
+
aiChatButtonEvent,
|
|
21
|
+
}: {
|
|
22
|
+
enableUsernameField?: boolean;
|
|
23
|
+
enableAIChat?: boolean;
|
|
24
|
+
aiChatButtonEvent?: string;
|
|
25
|
+
} = {}) => {
|
|
14
26
|
return (
|
|
15
27
|
<div className="left">
|
|
16
|
-
<VZSidebar
|
|
28
|
+
<VZSidebar
|
|
29
|
+
enableAIChat={enableAIChat}
|
|
30
|
+
aiChatButtonEvent={aiChatButtonEvent}
|
|
31
|
+
/>
|
|
17
32
|
<VZSettings
|
|
18
33
|
enableUsernameField={enableUsernameField}
|
|
19
34
|
/>
|
package/src/client/VZMiddle.tsx
CHANGED
|
@@ -28,6 +28,7 @@ const PaneView = ({
|
|
|
28
28
|
aiAssistClickOverride,
|
|
29
29
|
aiCopilotEndpoint,
|
|
30
30
|
esLintSource,
|
|
31
|
+
enableBuiltInAIChat,
|
|
31
32
|
}) => {
|
|
32
33
|
// This prevents the CodeEditor from rendering
|
|
33
34
|
// during SSR.
|
|
@@ -67,6 +68,7 @@ const PaneView = ({
|
|
|
67
68
|
customInteractRules={customInteractRules}
|
|
68
69
|
aiCopilotEndpoint={aiCopilotEndpoint}
|
|
69
70
|
esLintSource={esLintSource}
|
|
71
|
+
enableBuiltInAIChat={enableBuiltInAIChat}
|
|
70
72
|
/>
|
|
71
73
|
);
|
|
72
74
|
})()}
|
|
@@ -104,6 +106,8 @@ export const VZMiddle = ({
|
|
|
104
106
|
aiCopilotEndpoint,
|
|
105
107
|
customInteractRules,
|
|
106
108
|
esLintSource,
|
|
109
|
+
enableAIChat = true,
|
|
110
|
+
aiChatButtonEvent,
|
|
107
111
|
}: {
|
|
108
112
|
enableAIAssist?: boolean;
|
|
109
113
|
aiAssistEndpoint?: string;
|
|
@@ -115,7 +119,14 @@ export const VZMiddle = ({
|
|
|
115
119
|
esLintSource: (
|
|
116
120
|
view: EditorView,
|
|
117
121
|
) => Promise<readonly Diagnostic[]>;
|
|
122
|
+
enableAIChat?: boolean;
|
|
123
|
+
aiChatButtonEvent?: string;
|
|
118
124
|
}) => {
|
|
125
|
+
// The built-in AI chat (including the TODO widget, which prefills a
|
|
126
|
+
// chat message) is only reachable when the feature is enabled and the
|
|
127
|
+
// host hasn't taken over the sidebar button with a custom event.
|
|
128
|
+
const enableBuiltInAIChat =
|
|
129
|
+
enableAIChat && !aiChatButtonEvent;
|
|
119
130
|
const { codeEditorWidth } = useContext(
|
|
120
131
|
SplitPaneResizeContext,
|
|
121
132
|
);
|
|
@@ -152,6 +163,7 @@ export const VZMiddle = ({
|
|
|
152
163
|
aiAssistClickOverride={aiAssistClickOverride}
|
|
153
164
|
aiCopilotEndpoint={aiCopilotEndpoint}
|
|
154
165
|
esLintSource={esLintSource}
|
|
166
|
+
enableBuiltInAIChat={enableBuiltInAIChat}
|
|
155
167
|
/>
|
|
156
168
|
<CodeErrorOverlay
|
|
157
169
|
errorMessage={errorMessage}
|
|
@@ -16,6 +16,10 @@ import { CheckboxWidget } from './CheckboxWidget';
|
|
|
16
16
|
import { TextInputWidget } from './TextInputWidget';
|
|
17
17
|
import { DropdownWidget } from './DropdownWidget';
|
|
18
18
|
import { ColorWidget } from './ColorWidget';
|
|
19
|
+
import {
|
|
20
|
+
getNestedProperty,
|
|
21
|
+
setNestedProperty,
|
|
22
|
+
} from './utils';
|
|
19
23
|
|
|
20
24
|
export const VisualEditor = () => {
|
|
21
25
|
const { files, submitOperation } =
|
|
@@ -66,11 +70,12 @@ export const VisualEditor = () => {
|
|
|
66
70
|
[property]: newValue,
|
|
67
71
|
}));
|
|
68
72
|
|
|
69
|
-
// Update config.json
|
|
70
|
-
const newConfigData =
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
73
|
+
// Update config.json with support for nested properties
|
|
74
|
+
const newConfigData = setNestedProperty(
|
|
75
|
+
configData,
|
|
76
|
+
property,
|
|
77
|
+
newValue,
|
|
78
|
+
);
|
|
74
79
|
|
|
75
80
|
submitOperation((document: VizContent) => ({
|
|
76
81
|
...document,
|
|
@@ -97,11 +102,12 @@ export const VisualEditor = () => {
|
|
|
97
102
|
[property]: newValue,
|
|
98
103
|
}));
|
|
99
104
|
|
|
100
|
-
// Update config.json
|
|
101
|
-
const newConfigData =
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
+
// Update config.json with support for nested properties
|
|
106
|
+
const newConfigData = setNestedProperty(
|
|
107
|
+
configData,
|
|
108
|
+
property,
|
|
109
|
+
newValue,
|
|
110
|
+
);
|
|
105
111
|
|
|
106
112
|
submitOperation((document: VizContent) => ({
|
|
107
113
|
...document,
|
|
@@ -128,11 +134,12 @@ export const VisualEditor = () => {
|
|
|
128
134
|
[property]: newValue,
|
|
129
135
|
}));
|
|
130
136
|
|
|
131
|
-
// Update config.json
|
|
132
|
-
const newConfigData =
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
137
|
+
// Update config.json with support for nested properties
|
|
138
|
+
const newConfigData = setNestedProperty(
|
|
139
|
+
configData,
|
|
140
|
+
property,
|
|
141
|
+
newValue,
|
|
142
|
+
);
|
|
136
143
|
|
|
137
144
|
submitOperation((document: VizContent) => ({
|
|
138
145
|
...document,
|
|
@@ -156,11 +163,12 @@ export const VisualEditor = () => {
|
|
|
156
163
|
[property]: newValue,
|
|
157
164
|
}));
|
|
158
165
|
|
|
159
|
-
// Update config.json
|
|
160
|
-
const newConfigData =
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
166
|
+
// Update config.json with support for nested properties
|
|
167
|
+
const newConfigData = setNestedProperty(
|
|
168
|
+
configData,
|
|
169
|
+
property,
|
|
170
|
+
newValue,
|
|
171
|
+
);
|
|
164
172
|
|
|
165
173
|
submitOperation((document: VizContent) => ({
|
|
166
174
|
...document,
|
|
@@ -224,9 +232,10 @@ export const VisualEditor = () => {
|
|
|
224
232
|
event.currentTarget.value,
|
|
225
233
|
);
|
|
226
234
|
|
|
227
|
-
// Get current hex color from config
|
|
235
|
+
// Get current hex color from config using nested property access
|
|
228
236
|
const currentHex =
|
|
229
|
-
configData
|
|
237
|
+
getNestedProperty<string>(configData, property) ??
|
|
238
|
+
'#000000';
|
|
230
239
|
|
|
231
240
|
// Convert current hex to LCH
|
|
232
241
|
let hclFromRGB: HCLColor;
|
|
@@ -281,11 +290,12 @@ export const VisualEditor = () => {
|
|
|
281
290
|
[`${property}_l`]: newhcl[2],
|
|
282
291
|
}));
|
|
283
292
|
|
|
284
|
-
// Update config.json with hex value
|
|
285
|
-
const newConfigData =
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
293
|
+
// Update config.json with hex value using nested property access
|
|
294
|
+
const newConfigData = setNestedProperty(
|
|
295
|
+
configData,
|
|
296
|
+
property,
|
|
297
|
+
newHex,
|
|
298
|
+
);
|
|
289
299
|
|
|
290
300
|
submitOperation((document: VizContent) => ({
|
|
291
301
|
...document,
|
|
@@ -320,23 +330,36 @@ export const VisualEditor = () => {
|
|
|
320
330
|
} = {};
|
|
321
331
|
visualEditorWidgets.forEach((widget) => {
|
|
322
332
|
if (widget.type === 'slider') {
|
|
323
|
-
newLocalValues[widget.property] =
|
|
324
|
-
configData
|
|
333
|
+
newLocalValues[widget.property] = getNestedProperty(
|
|
334
|
+
configData,
|
|
335
|
+
widget.property,
|
|
336
|
+
);
|
|
325
337
|
} else if (widget.type === 'checkbox') {
|
|
326
|
-
newLocalValues[widget.property] =
|
|
327
|
-
configData
|
|
338
|
+
newLocalValues[widget.property] = getNestedProperty(
|
|
339
|
+
configData,
|
|
340
|
+
widget.property,
|
|
341
|
+
);
|
|
328
342
|
} else if (widget.type === 'textInput') {
|
|
329
|
-
newLocalValues[widget.property] =
|
|
330
|
-
configData
|
|
343
|
+
newLocalValues[widget.property] = getNestedProperty(
|
|
344
|
+
configData,
|
|
345
|
+
widget.property,
|
|
346
|
+
);
|
|
331
347
|
} else if (widget.type === 'dropdown') {
|
|
332
|
-
newLocalValues[widget.property] =
|
|
333
|
-
configData
|
|
348
|
+
newLocalValues[widget.property] = getNestedProperty(
|
|
349
|
+
configData,
|
|
350
|
+
widget.property,
|
|
351
|
+
);
|
|
334
352
|
} else if (widget.type === 'color') {
|
|
335
|
-
newLocalValues[widget.property] =
|
|
336
|
-
configData
|
|
353
|
+
newLocalValues[widget.property] = getNestedProperty(
|
|
354
|
+
configData,
|
|
355
|
+
widget.property,
|
|
356
|
+
);
|
|
337
357
|
|
|
338
358
|
// Convert hex to LCH for internal state
|
|
339
|
-
const hexColor =
|
|
359
|
+
const hexColor = getNestedProperty<string>(
|
|
360
|
+
configData,
|
|
361
|
+
widget.property,
|
|
362
|
+
);
|
|
340
363
|
|
|
341
364
|
if (localValues[widget.property] !== hexColor) {
|
|
342
365
|
if (hexColor) {
|
|
@@ -414,8 +437,14 @@ export const VisualEditor = () => {
|
|
|
414
437
|
if (widgetConfig.type === 'slider') {
|
|
415
438
|
// Use local value if available, otherwise fall back to config value
|
|
416
439
|
const currentValue =
|
|
417
|
-
localValues[
|
|
418
|
-
|
|
440
|
+
(localValues[
|
|
441
|
+
widgetConfig.property
|
|
442
|
+
] as number) ??
|
|
443
|
+
getNestedProperty<number>(
|
|
444
|
+
configData,
|
|
445
|
+
widgetConfig.property,
|
|
446
|
+
) ??
|
|
447
|
+
widgetConfig.min;
|
|
419
448
|
|
|
420
449
|
return (
|
|
421
450
|
<SliderWidget
|
|
@@ -434,8 +463,14 @@ export const VisualEditor = () => {
|
|
|
434
463
|
} else if (widgetConfig.type === 'checkbox') {
|
|
435
464
|
// Use local value if available, otherwise fall back to config value
|
|
436
465
|
const currentValue =
|
|
437
|
-
localValues[
|
|
438
|
-
|
|
466
|
+
(localValues[
|
|
467
|
+
widgetConfig.property
|
|
468
|
+
] as boolean) ??
|
|
469
|
+
getNestedProperty<boolean>(
|
|
470
|
+
configData,
|
|
471
|
+
widgetConfig.property,
|
|
472
|
+
) ??
|
|
473
|
+
false;
|
|
439
474
|
|
|
440
475
|
return (
|
|
441
476
|
<CheckboxWidget
|
|
@@ -451,8 +486,14 @@ export const VisualEditor = () => {
|
|
|
451
486
|
} else if (widgetConfig.type === 'textInput') {
|
|
452
487
|
// Use local value if available, otherwise fall back to config value
|
|
453
488
|
const currentValue =
|
|
454
|
-
localValues[
|
|
455
|
-
|
|
489
|
+
(localValues[
|
|
490
|
+
widgetConfig.property
|
|
491
|
+
] as string) ??
|
|
492
|
+
getNestedProperty<string>(
|
|
493
|
+
configData,
|
|
494
|
+
widgetConfig.property,
|
|
495
|
+
) ??
|
|
496
|
+
'';
|
|
456
497
|
|
|
457
498
|
return (
|
|
458
499
|
<TextInputWidget
|
|
@@ -468,8 +509,15 @@ export const VisualEditor = () => {
|
|
|
468
509
|
} else if (widgetConfig.type === 'dropdown') {
|
|
469
510
|
// Use local value if available, otherwise fall back to config value
|
|
470
511
|
const currentValue =
|
|
471
|
-
localValues[
|
|
472
|
-
|
|
512
|
+
(localValues[
|
|
513
|
+
widgetConfig.property
|
|
514
|
+
] as string) ??
|
|
515
|
+
getNestedProperty<string>(
|
|
516
|
+
configData,
|
|
517
|
+
widgetConfig.property,
|
|
518
|
+
) ??
|
|
519
|
+
widgetConfig.options?.[0] ??
|
|
520
|
+
'';
|
|
473
521
|
const isOpen =
|
|
474
522
|
openDropdown === widgetConfig.property;
|
|
475
523
|
|
|
@@ -495,8 +543,13 @@ export const VisualEditor = () => {
|
|
|
495
543
|
} else if (widgetConfig.type === 'color') {
|
|
496
544
|
// Use local value if available, otherwise fall back to config value
|
|
497
545
|
const currentHex =
|
|
498
|
-
localValues[
|
|
499
|
-
|
|
546
|
+
(localValues[
|
|
547
|
+
widgetConfig.property
|
|
548
|
+
] as string) ??
|
|
549
|
+
getNestedProperty<string>(
|
|
550
|
+
configData,
|
|
551
|
+
widgetConfig.property,
|
|
552
|
+
) ??
|
|
500
553
|
'#000000';
|
|
501
554
|
|
|
502
555
|
// Convert hex to LCH for slider values
|
|
@@ -100,3 +100,116 @@ export const renderSliderBackground = (
|
|
|
100
100
|
|
|
101
101
|
ctx.putImageData(imageData, 0, 0);
|
|
102
102
|
};
|
|
103
|
+
|
|
104
|
+
// Helper function to get nested property value using dot notation
|
|
105
|
+
export const getNestedProperty = <T = unknown>(
|
|
106
|
+
obj: Record<string, unknown>,
|
|
107
|
+
path: string,
|
|
108
|
+
): T | undefined => {
|
|
109
|
+
// Validate path to prevent empty strings, consecutive dots, or leading/trailing dots
|
|
110
|
+
if (
|
|
111
|
+
!path ||
|
|
112
|
+
path.includes('..') ||
|
|
113
|
+
path.startsWith('.') ||
|
|
114
|
+
path.endsWith('.')
|
|
115
|
+
) {
|
|
116
|
+
return undefined;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const keys = path.split('.');
|
|
120
|
+
let value: unknown = obj;
|
|
121
|
+
for (const key of keys) {
|
|
122
|
+
// Skip empty keys that might result from splitting
|
|
123
|
+
if (!key) {
|
|
124
|
+
return undefined;
|
|
125
|
+
}
|
|
126
|
+
if (value === null || value === undefined) {
|
|
127
|
+
return undefined;
|
|
128
|
+
}
|
|
129
|
+
// Type guard to ensure value is an object before accessing property
|
|
130
|
+
if (typeof value !== 'object') {
|
|
131
|
+
return undefined;
|
|
132
|
+
}
|
|
133
|
+
value = (value as Record<string, unknown>)[key];
|
|
134
|
+
}
|
|
135
|
+
return value as T | undefined;
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
// Helper function to set nested property value using dot notation
|
|
139
|
+
export const setNestedProperty = <
|
|
140
|
+
T extends Record<string, unknown>,
|
|
141
|
+
>(
|
|
142
|
+
obj: T,
|
|
143
|
+
path: string,
|
|
144
|
+
value: unknown,
|
|
145
|
+
): T => {
|
|
146
|
+
// Validate path to prevent empty strings, consecutive dots, or leading/trailing dots
|
|
147
|
+
if (
|
|
148
|
+
!path ||
|
|
149
|
+
path.includes('..') ||
|
|
150
|
+
path.startsWith('.') ||
|
|
151
|
+
path.endsWith('.')
|
|
152
|
+
) {
|
|
153
|
+
throw new Error(
|
|
154
|
+
`Invalid property path "${path}" - path cannot be empty, contain consecutive dots, or have leading/trailing dots`,
|
|
155
|
+
);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const keys = path.split('.');
|
|
159
|
+
|
|
160
|
+
// Check for empty keys
|
|
161
|
+
if (keys.some((key) => !key)) {
|
|
162
|
+
throw new Error(
|
|
163
|
+
`Invalid property path "${path}" - path contains empty segments`,
|
|
164
|
+
);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const newObj = { ...obj };
|
|
168
|
+
let current: Record<string, unknown> = newObj;
|
|
169
|
+
|
|
170
|
+
// List of dangerous keys that could lead to prototype pollution
|
|
171
|
+
const dangerousKeys = [
|
|
172
|
+
'__proto__',
|
|
173
|
+
'constructor',
|
|
174
|
+
'prototype',
|
|
175
|
+
];
|
|
176
|
+
|
|
177
|
+
for (let i = 0; i < keys.length - 1; i++) {
|
|
178
|
+
const key = keys[i];
|
|
179
|
+
|
|
180
|
+
// Guard against prototype pollution
|
|
181
|
+
if (dangerousKeys.includes(key)) {
|
|
182
|
+
throw new Error(
|
|
183
|
+
`Cannot set property "${key}" - potential prototype pollution`,
|
|
184
|
+
);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// Create nested object if it doesn't exist or isn't a plain object
|
|
188
|
+
if (
|
|
189
|
+
current[key] == null ||
|
|
190
|
+
typeof current[key] !== 'object' ||
|
|
191
|
+
Array.isArray(current[key])
|
|
192
|
+
) {
|
|
193
|
+
current[key] = {};
|
|
194
|
+
} else {
|
|
195
|
+
// Clone the nested object to avoid mutation
|
|
196
|
+
current[key] = {
|
|
197
|
+
...(current[key] as Record<string, unknown>),
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
current = current[key] as Record<string, unknown>;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// Guard against prototype pollution for the final key
|
|
204
|
+
const finalKey = keys[keys.length - 1];
|
|
205
|
+
if (dangerousKeys.includes(finalKey)) {
|
|
206
|
+
throw new Error(
|
|
207
|
+
`Cannot set property "${finalKey}" - potential prototype pollution`,
|
|
208
|
+
);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// Set the final value
|
|
212
|
+
current[finalKey] = value;
|
|
213
|
+
|
|
214
|
+
return newObj;
|
|
215
|
+
};
|
|
@@ -40,14 +40,22 @@ import { useDragAndDrop } from './useDragAndDrop';
|
|
|
40
40
|
import { createAICopyPasteHandlers } from './aiCopyPaste';
|
|
41
41
|
import {
|
|
42
42
|
enableLiveKit,
|
|
43
|
-
enableAIChat,
|
|
43
|
+
enableAIChat as enableAIChatFeatureFlag,
|
|
44
44
|
} from '../featureFlags';
|
|
45
|
+
import { handleAIChatButtonClick } from '../aiChatButtonEvent';
|
|
45
46
|
import './styles.scss';
|
|
46
47
|
import { VisualEditor } from './VisualEditor';
|
|
47
48
|
|
|
48
49
|
const enableConnectionStatus = true;
|
|
49
50
|
|
|
50
51
|
export const VZSidebar = ({
|
|
52
|
+
// Host-provided override so consumers (e.g. VizHub Studio, which has
|
|
53
|
+
// its own top-bar "AI Edit" entry point) can hide the "Edit with AI"
|
|
54
|
+
// sidebar button. Defaults to the compile-time feature flag.
|
|
55
|
+
enableAIChat = enableAIChatFeatureFlag,
|
|
56
|
+
// When set, clicking the button dispatches this custom event instead
|
|
57
|
+
// of opening VZCode's built-in AI chat, letting the host handle it.
|
|
58
|
+
aiChatButtonEvent,
|
|
51
59
|
createFileTooltipText = (
|
|
52
60
|
<>
|
|
53
61
|
<strong>New file</strong>
|
|
@@ -135,6 +143,8 @@ export const VZSidebar = ({
|
|
|
135
143
|
voiceChatToolTipText?: React.ReactNode;
|
|
136
144
|
aiChatToolTipText?: React.ReactNode;
|
|
137
145
|
visualEditorToolTipText?: React.ReactNode;
|
|
146
|
+
enableAIChat?: boolean;
|
|
147
|
+
aiChatButtonEvent?: string;
|
|
138
148
|
}) => {
|
|
139
149
|
const {
|
|
140
150
|
files,
|
|
@@ -341,11 +351,17 @@ export const VZSidebar = ({
|
|
|
341
351
|
<i
|
|
342
352
|
id="ai-chat-icon"
|
|
343
353
|
className="icon-button icon-button-dark"
|
|
344
|
-
onClick={() =>
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
354
|
+
onClick={() =>
|
|
355
|
+
handleAIChatButtonClick({
|
|
356
|
+
aiChatButtonEvent,
|
|
357
|
+
target: window,
|
|
358
|
+
openAIChat: () => {
|
|
359
|
+
setIsAIChatOpen(true);
|
|
360
|
+
setIsSearchOpen(false);
|
|
361
|
+
setSidebarView(true); // Switch to AI chat view
|
|
362
|
+
},
|
|
363
|
+
})
|
|
364
|
+
}
|
|
349
365
|
>
|
|
350
366
|
<SparklesSVG />
|
|
351
367
|
</i>
|
|
@@ -411,6 +427,7 @@ export const VZSidebar = ({
|
|
|
411
427
|
setIsVisualEditorOpen(true);
|
|
412
428
|
setIsAIChatOpen(false);
|
|
413
429
|
setIsSearchOpen(false);
|
|
430
|
+
setSidebarView(false); // Switch to files view width for visual editor
|
|
414
431
|
}}
|
|
415
432
|
>
|
|
416
433
|
<AdjustmentSVG />
|