vzcode 1.46.0 → 1.49.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/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-CC0CUUDi.js"></script>
24
- <link rel="stylesheet" crossorigin href="/assets/index-CQyHWTWE.css">
23
+ <script type="module" crossorigin src="/assets/index-DrUY_A13.js"></script>
24
+ <link rel="stylesheet" crossorigin href="/assets/index-BXyJ2hMf.css">
25
25
  </head>
26
26
  <body>
27
27
  <div id="root"></div>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vzcode",
3
- "version": "1.46.0",
3
+ "version": "1.49.0",
4
4
  "description": "Multiplayer code editor system",
5
5
  "main": "src/index.ts",
6
6
  "type": "module",
@@ -118,6 +118,7 @@ function App() {
118
118
  liveKitConnection={liveKitConnection}
119
119
  setLiveKitConnection={setLiveKitConnection}
120
120
  aiChatEndpoint="/ai-chat-message"
121
+ aiChatUndoEndpoint="/ai-chat-undo"
121
122
  aiChatOptions={{}}
122
123
  >
123
124
  <LiveKitRoom
@@ -20,7 +20,7 @@ export type SplitPaneResizeContextValue = {
20
20
 
21
21
  // Default widths for different sidebar views
22
22
  const filesViewWidth = 300;
23
- const aiChatViewWidth = 600;
23
+ const aiChatViewWidth = 800;
24
24
  const initialCodeEditorWidthDefault = 800;
25
25
 
26
26
  const initialValue: SplitPaneResizeContextValue = {
@@ -129,6 +129,7 @@ export type VZCodeContextValue = {
129
129
  aiChatMode: 'ask' | 'edit';
130
130
  setAIChatMode: (mode: 'ask' | 'edit') => void;
131
131
  aiChatEndpoint?: string;
132
+ aiChatUndoEndpoint?: string;
132
133
  aiChatOptions?: { [key: string]: any };
133
134
  setSearchResults: (files: ShareDBDoc<VizContent>) => void;
134
135
  setSearchFileVisibility: (
@@ -215,6 +216,7 @@ export const VZCodeProvider = ({
215
216
  liveKitConnection,
216
217
  setLiveKitConnection,
217
218
  aiChatEndpoint,
219
+ aiChatUndoEndpoint,
218
220
  aiChatOptions,
219
221
  }: {
220
222
  content: VizContent;
@@ -235,6 +237,7 @@ export const VZCodeProvider = ({
235
237
  liveKitConnection?: boolean;
236
238
  setLiveKitConnection?: (state: boolean) => void;
237
239
  aiChatEndpoint?: string;
240
+ aiChatUndoEndpoint?: string;
238
241
  aiChatOptions?: { [key: string]: any };
239
242
  }) => {
240
243
  // Auto-run Pretter after local changes.
@@ -497,6 +500,7 @@ export const VZCodeProvider = ({
497
500
  aiChatMode: state.aiChatMode,
498
501
  setAIChatMode,
499
502
  aiChatEndpoint,
503
+ aiChatUndoEndpoint,
500
504
  aiChatOptions,
501
505
 
502
506
  isSettingsOpen,
@@ -1,4 +1,4 @@
1
- import React, { useState } from 'react';
1
+ import React, { useState, useContext } from 'react';
2
2
  import {
3
3
  UnifiedFilesDiff,
4
4
  parseUnifiedDiffStats,
@@ -6,6 +6,7 @@ import {
6
6
  } from '../../../utils/fileDiff';
7
7
  import * as Diff2Html from 'diff2html';
8
8
  import 'diff2html/bundles/css/diff2html.min.css';
9
+ import { VZCodeContext } from '../../VZCodeContext';
9
10
  import './DiffView.scss';
10
11
 
11
12
  interface DiffViewProps {
@@ -24,6 +25,7 @@ export const DiffView: React.FC<DiffViewProps> = ({
24
25
  canUndo = false,
25
26
  }) => {
26
27
  const [isUndoing, setIsUndoing] = useState(false);
28
+ const { aiChatUndoEndpoint } = useContext(VZCodeContext);
27
29
 
28
30
  const unifiedDiffs = Object.values(diffData).filter(
29
31
  (diff) => diff.length > 0,
@@ -38,14 +40,15 @@ export const DiffView: React.FC<DiffViewProps> = ({
38
40
  !messageId ||
39
41
  !chatId ||
40
42
  !beforeFiles ||
41
- isUndoing
43
+ isUndoing ||
44
+ !aiChatUndoEndpoint
42
45
  ) {
43
46
  return;
44
47
  }
45
48
 
46
49
  setIsUndoing(true);
47
50
  try {
48
- const response = await fetch('/ai-chat-undo', {
51
+ const response = await fetch(aiChatUndoEndpoint, {
49
52
  method: 'POST',
50
53
  headers: {
51
54
  'Content-Type': 'application/json',
@@ -10,7 +10,7 @@ import { MessageList } from './MessageList';
10
10
  import { ChatInput } from './ChatInput';
11
11
  import './styles.scss';
12
12
 
13
- const defaultAIChatEndpoint = '/ai-chat-message';
13
+ const defaultAIChatEndpoint = '/api/ai-chat/';
14
14
 
15
15
  export const AIChat = () => {
16
16
  const { aiChatMessage, setAIChatMessage } =
@@ -18,6 +18,9 @@ export const AIChat = () => {
18
18
 
19
19
  const [isLoading, setIsLoading] = useState(false);
20
20
  const [currentChatId] = useState(() => uuidv4());
21
+ const [errorMessage, setErrorMessage] = useState<
22
+ string | null
23
+ >(null);
21
24
 
22
25
  const {
23
26
  aiChatFocused,
@@ -52,6 +55,7 @@ export const AIChat = () => {
52
55
 
53
56
  setAIChatMessage('');
54
57
  setIsLoading(true);
58
+ setErrorMessage(null); // Clear any previous errors
55
59
 
56
60
  // Call backend endpoint for AI response
57
61
  // The server will handle all ShareDB operations including adding the user message
@@ -63,6 +67,7 @@ export const AIChat = () => {
63
67
  },
64
68
  body: JSON.stringify({
65
69
  ...aiChatOptions,
70
+ vizId: aiChatOptions.vizId,
66
71
  content: aiChatMessage.trim(),
67
72
  chatId: currentChatId,
68
73
  mode: aiChatMode,
@@ -75,11 +80,24 @@ export const AIChat = () => {
75
80
  );
76
81
  }
77
82
 
78
- // The backend handles all ShareDB operations
79
- await response.json();
83
+ // Parse the response to check for errors
84
+ const responseData = await response.json();
85
+
86
+ // Check if the response contains a VizHub error
87
+ if (
88
+ responseData.outcome === 'failure' &&
89
+ responseData.error
90
+ ) {
91
+ setErrorMessage(responseData.error.message);
92
+ return;
93
+ }
94
+
95
+ // The backend handles all ShareDB operations for successful responses
80
96
  } catch (error) {
81
97
  console.error('Error getting AI response:', error);
82
- // Server will handle error messages too
98
+ setErrorMessage(
99
+ 'Failed to send message. Please try again.',
100
+ );
83
101
  } finally {
84
102
  setIsLoading(false);
85
103
  }
@@ -224,6 +242,23 @@ export const AIChat = () => {
224
242
  aiScratchpad={aiScratchpad}
225
243
  />
226
244
  )}
245
+ {errorMessage && (
246
+ <div className="ai-chat-error">
247
+ <div className="ai-chat-error-content">
248
+ <span className="ai-chat-error-icon">⚠️</span>
249
+ <span className="ai-chat-error-message">
250
+ {errorMessage}
251
+ </span>
252
+ <button
253
+ className="ai-chat-error-dismiss"
254
+ onClick={() => setErrorMessage(null)}
255
+ aria-label="Dismiss error"
256
+ >
257
+ ×
258
+ </button>
259
+ </div>
260
+ </div>
261
+ )}
227
262
  <ChatInput
228
263
  aiChatMessage={aiChatMessage}
229
264
  setAIChatMessage={setAIChatMessage}
@@ -272,6 +272,61 @@
272
272
  }
273
273
  }
274
274
 
275
+ .ai-chat-error {
276
+ margin: 10px 0;
277
+ padding: 0;
278
+ border-radius: 8px;
279
+ background: rgba(220, 53, 69, 0.1);
280
+ border: 1px solid rgba(220, 53, 69, 0.3);
281
+
282
+ .ai-chat-error-content {
283
+ display: flex;
284
+ align-items: center;
285
+ padding: 12px;
286
+ gap: 10px;
287
+
288
+ .ai-chat-error-icon {
289
+ font-size: 16px;
290
+ flex-shrink: 0;
291
+ }
292
+
293
+ .ai-chat-error-message {
294
+ flex: 1;
295
+ font-size: 14px;
296
+ color: #dc3545;
297
+ line-height: 1.4;
298
+ font-weight: 500;
299
+ }
300
+
301
+ .ai-chat-error-dismiss {
302
+ background: none;
303
+ border: none;
304
+ color: #dc3545;
305
+ font-size: 18px;
306
+ font-weight: bold;
307
+ cursor: pointer;
308
+ padding: 0;
309
+ width: 20px;
310
+ height: 20px;
311
+ display: flex;
312
+ align-items: center;
313
+ justify-content: center;
314
+ border-radius: 50%;
315
+ transition: background-color 0.2s ease;
316
+ flex-shrink: 0;
317
+
318
+ &:hover {
319
+ background: rgba(220, 53, 69, 0.1);
320
+ }
321
+
322
+ &:focus {
323
+ outline: 2px solid rgba(220, 53, 69, 0.3);
324
+ outline-offset: 1px;
325
+ }
326
+ }
327
+ }
328
+ }
329
+
275
330
  .ai-chat-input-container {
276
331
  border-top: 1px solid var(--vh-color-neutral-02);
277
332
  padding-top: 10px;
@@ -298,6 +298,29 @@ export const VZSidebar = ({
298
298
  tabIndex={-1}
299
299
  >
300
300
  <div className="sidebar-section-buttons">
301
+ {enableAIChat && (
302
+ <OverlayTrigger
303
+ placement="right"
304
+ overlay={
305
+ <Tooltip id="ai-chat-tooltip">
306
+ {aiChatToolTipText}
307
+ </Tooltip>
308
+ }
309
+ >
310
+ <i
311
+ id="ai-chat-icon"
312
+ className="icon-button icon-button-dark"
313
+ onClick={() => {
314
+ setIsAIChatOpen(true);
315
+ setIsSearchOpen(false);
316
+ setSidebarView(true); // Switch to AI chat view
317
+ }}
318
+ >
319
+ <SparklesSVG />
320
+ </i>
321
+ </OverlayTrigger>
322
+ )}
323
+
301
324
  <OverlayTrigger
302
325
  placement="right"
303
326
  overlay={
@@ -340,29 +363,6 @@ export const VZSidebar = ({
340
363
  </i>
341
364
  </OverlayTrigger>
342
365
 
343
- {enableAIChat && (
344
- <OverlayTrigger
345
- placement="right"
346
- overlay={
347
- <Tooltip id="ai-chat-tooltip">
348
- {aiChatToolTipText}
349
- </Tooltip>
350
- }
351
- >
352
- <i
353
- id="ai-chat-icon"
354
- className="icon-button icon-button-dark"
355
- onClick={() => {
356
- setIsAIChatOpen(true);
357
- setIsSearchOpen(false);
358
- setSidebarView(true); // Switch to AI chat view
359
- }}
360
- >
361
- <SparklesSVG />
362
- </i>
363
- </OverlayTrigger>
364
- )}
365
-
366
366
  <OverlayTrigger
367
367
  placement="right"
368
368
  overlay={
@@ -25,7 +25,7 @@ export const createInitialState = ({
25
25
  focusedChildIndex: null,
26
26
  },
27
27
  isSearchOpen: false,
28
- isAIChatOpen: false,
28
+ isAIChatOpen: true,
29
29
  aiChatFocused: false,
30
30
  aiChatMode: 'edit',
31
31
  isSettingsOpen: false,
@@ -0,0 +1,97 @@
1
+ import { undoAIEdit } from './chatOperations.js';
2
+ import { handleError } from './errorHandling.js';
3
+ import { ShareDBDoc } from '../../types.js';
4
+ import { VizContent } from '@vizhub/viz-types';
5
+
6
+ const DEBUG = false;
7
+
8
+ /**
9
+ * Validates incoming request data for AI chat undo
10
+ */
11
+ const validateUndoRequest = (req, res) => {
12
+ const { chatId, messageId } = req.body;
13
+
14
+ if (!chatId || typeof chatId !== 'string') {
15
+ res.status(400).json({
16
+ error:
17
+ 'Invalid request: chatId is required and must be a string',
18
+ });
19
+ return false;
20
+ }
21
+
22
+ if (!messageId || typeof messageId !== 'string') {
23
+ res.status(400).json({
24
+ error:
25
+ 'Invalid request: messageId is required and must be a string',
26
+ });
27
+ return false;
28
+ }
29
+
30
+ return true;
31
+ };
32
+
33
+ export const handleAIChatUndo =
34
+ ({
35
+ shareDBDoc,
36
+ }: {
37
+ shareDBDoc: ShareDBDoc<VizContent>;
38
+ }) =>
39
+ async (req: any, res: any) => {
40
+ const { chatId, messageId } = req.body;
41
+
42
+ if (DEBUG) {
43
+ console.log(
44
+ '[handleAIChatUndo] chatId:',
45
+ chatId,
46
+ 'messageId:',
47
+ messageId,
48
+ 'shareDBDoc:',
49
+ shareDBDoc,
50
+ );
51
+ }
52
+
53
+ // Validate request
54
+ if (!validateUndoRequest(req, res)) {
55
+ return;
56
+ }
57
+
58
+ try {
59
+ const chat = shareDBDoc.data.chats?.[chatId];
60
+ if (!chat) {
61
+ res.status(404).json({
62
+ error: 'Chat not found',
63
+ });
64
+ return;
65
+ }
66
+
67
+ const message = chat.messages.find(
68
+ (msg) => msg.id === messageId,
69
+ );
70
+
71
+ if (
72
+ !message ||
73
+ message.role !== 'assistant' ||
74
+ !(message as any).beforeFiles
75
+ ) {
76
+ res.status(400).json({
77
+ error: 'Invalid message for undo',
78
+ });
79
+ return;
80
+ }
81
+
82
+ // Perform the undo operation
83
+ undoAIEdit(
84
+ shareDBDoc,
85
+ chatId,
86
+ messageId,
87
+ (message as any).beforeFiles,
88
+ );
89
+
90
+ res.status(200).json({ success: true });
91
+ } catch (error) {
92
+ if (DEBUG) {
93
+ console.error('[handleAIChatUndo] Error:', error);
94
+ }
95
+ handleError(shareDBDoc, chatId, error, res);
96
+ }
97
+ };
@@ -0,0 +1,2 @@
1
+ // Re-export the handleAIChatUndo from the new modular structure
2
+ export { handleAIChatUndo } from './aiChatHandler/undoHandler.js';
@@ -14,7 +14,7 @@ import { computeInitialDocument } from './computeInitialDocument.js';
14
14
  import { handleAIAssist } from './handleAIAssist.js';
15
15
  import { handleAICopilot } from './handleAICopilot.js';
16
16
  import { handleAIChatMessage } from './handleAIChatMessage.js';
17
- import { undoAIEdit } from './aiChatHandler/chatOperations.js';
17
+ import { handleAIChatUndo } from './handleAIChatUndo.js';
18
18
  import { isDirectory } from './isDirectory.js';
19
19
  import { createToken } from './livekit.js';
20
20
  import './setupEnv.js';
@@ -146,51 +146,9 @@ app.post(
146
146
  app.post(
147
147
  '/ai-chat-undo',
148
148
  bodyParser.json(),
149
- async (req, res) => {
150
- const { chatId, messageId } = req.body;
151
-
152
- if (!chatId || !messageId) {
153
- return res
154
- .status(400)
155
- .json({ error: 'Missing chatId or messageId' });
156
- }
157
-
158
- try {
159
- const chat = shareDBDoc.data.chats?.[chatId];
160
- if (!chat) {
161
- return res
162
- .status(404)
163
- .json({ error: 'Chat not found' });
164
- }
165
-
166
- const message = chat.messages.find(
167
- (msg) => msg.id === messageId,
168
- );
169
- if (
170
- !message ||
171
- message.role !== 'assistant' ||
172
- !message.beforeFiles
173
- ) {
174
- return res
175
- .status(400)
176
- .json({ error: 'Invalid message for undo' });
177
- }
178
-
179
- undoAIEdit(
180
- shareDBDoc,
181
- chatId,
182
- messageId,
183
- message.beforeFiles,
184
- );
185
-
186
- res.status(200).json({ success: true });
187
- } catch (error) {
188
- console.error('Error undoing AI edit:', error);
189
- res
190
- .status(500)
191
- .json({ error: 'Failed to undo edit' });
192
- }
193
- },
149
+ handleAIChatUndo({
150
+ shareDBDoc,
151
+ }),
194
152
  );
195
153
 
196
154
  // Livekit Token Generator