vzcode 1.55.0 → 1.57.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-DILO4YFE.js"></script>
24
- <link rel="stylesheet" crossorigin href="/assets/index-GFaZu2lY.css">
23
+ <script type="module" crossorigin src="/assets/index-DqIc-0Gp.js"></script>
24
+ <link rel="stylesheet" crossorigin href="/assets/index-CE_xP06t.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.55.0",
3
+ "version": "1.57.0",
4
4
  "description": "Multiplayer code editor system",
5
5
  "main": "src/index.ts",
6
6
  "type": "module",
@@ -50,6 +50,10 @@ const ChatInputComponent = ({
50
50
  [onSendMessage],
51
51
  );
52
52
 
53
+ const handleSendClick = useCallback(() => {
54
+ onSendMessage();
55
+ }, [onSendMessage]);
56
+
53
57
  const handleChange = useCallback(
54
58
  (event: React.ChangeEvent<HTMLTextAreaElement>) => {
55
59
  setAIChatMessage(event.target.value);
@@ -108,7 +112,7 @@ const ChatInputComponent = ({
108
112
  <Form.Group className="ai-chat-input-group">
109
113
  <Form.Control
110
114
  as="textarea"
111
- rows={2}
115
+ rows={5}
112
116
  value={aiChatMessage}
113
117
  onChange={handleChange}
114
118
  onKeyDown={handleKeyDown}
@@ -122,26 +126,25 @@ const ChatInputComponent = ({
122
126
  disabled={isLoading}
123
127
  aria-label="Chat message input"
124
128
  />
125
- {aiChatMessage && (
126
- <div className="ai-chat-input-info">
127
- <span className="ai-chat-hint">
128
- Shift+Enter for new line
129
- </span>
130
- <span className="ai-chat-hint">
131
- Press Enter to send
132
- </span>
133
- </div>
134
- )}
135
- <Button
136
- variant="primary"
137
- onClick={onSendMessage}
138
- disabled={!aiChatMessage.trim() || isLoading}
139
- className="ai-chat-send-button"
140
- aria-label="Send message"
141
- title="Send message (Enter)"
142
- >
143
- Send
144
- </Button>
129
+ <div className="ai-chat-input-footer">
130
+ <span className="ai-chat-hint">
131
+ {aiChatMessage ? 'Press Enter to send' : ''}
132
+ </span>
133
+ <Button
134
+ variant={
135
+ aiChatMessage.trim()
136
+ ? 'primary'
137
+ : 'outline-secondary'
138
+ }
139
+ onClick={handleSendClick}
140
+ disabled={!aiChatMessage.trim() || isLoading}
141
+ className="ai-chat-send-button"
142
+ aria-label="Send message"
143
+ title="Send message (Enter)"
144
+ >
145
+ Send
146
+ </Button>
147
+ </div>
145
148
  </Form.Group>
146
149
  </div>
147
150
  );
@@ -119,6 +119,14 @@
119
119
  border-color: var(--d2h-ins-border-color) !important;
120
120
  }
121
121
 
122
+ // Style clickable file names
123
+ .d2h-file-name {
124
+ &:hover {
125
+ text-decoration: underline !important;
126
+ opacity: 0.8;
127
+ }
128
+ }
129
+
122
130
  // Fix wrapper border
123
131
  .d2h-file-wrapper {
124
132
  border: 1px solid var(--d2h-border-color) !important;
@@ -1,4 +1,9 @@
1
- import React from 'react';
1
+ import React, {
2
+ useContext,
3
+ useEffect,
4
+ useMemo,
5
+ useRef,
6
+ } from 'react';
2
7
  import {
3
8
  UnifiedFilesDiff,
4
9
  parseUnifiedDiffStats,
@@ -7,6 +12,8 @@ import {
7
12
  import * as Diff2Html from 'diff2html';
8
13
  import 'diff2html/bundles/css/diff2html.min.css';
9
14
  import './DiffView.scss';
15
+ import { VZCodeContext } from '../../VZCodeContext';
16
+ import { VizFileId, VizFiles } from '@vizhub/viz-types';
10
17
 
11
18
  interface DiffViewProps {
12
19
  diffData: UnifiedFilesDiff;
@@ -15,6 +22,10 @@ interface DiffViewProps {
15
22
  export const DiffView: React.FC<DiffViewProps> = ({
16
23
  diffData,
17
24
  }) => {
25
+ const { content, openTab, setIsAIChatOpen } =
26
+ useContext(VZCodeContext);
27
+ const diffContainerRef = useRef<HTMLDivElement>(null);
28
+
18
29
  const unifiedDiffs = Object.values(diffData).filter(
19
30
  (diff) => diff.length > 0,
20
31
  );
@@ -42,6 +53,98 @@ export const DiffView: React.FC<DiffViewProps> = ({
42
53
  outputFormat: 'line-by-line',
43
54
  });
44
55
 
56
+ // Create a mapping from file name to file ID for click handling
57
+ const fileNameToIdMap = useMemo(() => {
58
+ const map = new Map<string, VizFileId>();
59
+ Object.entries(diffData).forEach(([fileId, diff]) => {
60
+ // Extract file name from the unified diff
61
+ // The diff contains lines like "--- a/filename" and "+++ b/filename"
62
+ const lines = diff.split('\n');
63
+ for (const line of lines) {
64
+ if (
65
+ line.startsWith('--- a/') ||
66
+ line.startsWith('+++ b/')
67
+ ) {
68
+ const fileName = line.substring(6); // Remove "--- a/" or "+++ b/"
69
+ map.set(fileName, fileId as VizFileId);
70
+ break;
71
+ }
72
+ }
73
+ });
74
+ return map;
75
+ }, [diffData]);
76
+
77
+ // Add click handlers to file names after HTML is rendered
78
+ useEffect(() => {
79
+ if (!diffContainerRef.current) return;
80
+
81
+ const handleFileNameClick = (event: MouseEvent) => {
82
+ const target = event.target as HTMLElement;
83
+ if (target.classList.contains('d2h-file-name')) {
84
+ event.preventDefault();
85
+ event.stopPropagation();
86
+
87
+ console.log('File name clicked');
88
+
89
+ const fileName = target.textContent?.trim();
90
+
91
+ console.log('Clicked file name:', fileName);
92
+ if (fileName) {
93
+ const files: VizFiles = content?.files;
94
+ if (files) {
95
+ // export type VizFiles = {
96
+ // [fileId: VizFileId]: VizFile;
97
+ // };
98
+ // export type VizFileId = string;
99
+ // export type VizFile = {
100
+ // name: string;
101
+ // text: string;
102
+ // };
103
+
104
+ // TODO get fileId from content.files
105
+ const fileId = Object.entries(files).find(
106
+ ([id, file]) => file.name === fileName,
107
+ )?.[0];
108
+ console.log('Mapped file ID:', fileId);
109
+
110
+ if (fileId) {
111
+ // Open the file tab and switch to files view
112
+ openTab({ fileId, isTransient: false });
113
+ setIsAIChatOpen(false);
114
+ }
115
+ }
116
+ }
117
+ }
118
+ };
119
+
120
+ const container = diffContainerRef.current;
121
+ const fileNameElements = container.querySelectorAll(
122
+ '.d2h-file-name',
123
+ );
124
+
125
+ // Add click event listeners and cursor pointer style
126
+ fileNameElements.forEach((element) => {
127
+ element.addEventListener(
128
+ 'click',
129
+ handleFileNameClick,
130
+ );
131
+ (element as HTMLElement).style.cursor = 'pointer';
132
+ (element as HTMLElement).style.textDecoration =
133
+ 'underline';
134
+ (element as HTMLElement).style.color = '#58a6ff'; // GitHub blue link color
135
+ });
136
+
137
+ return () => {
138
+ // Cleanup event listeners
139
+ fileNameElements.forEach((element) => {
140
+ element.removeEventListener(
141
+ 'click',
142
+ handleFileNameClick,
143
+ );
144
+ });
145
+ };
146
+ }, [diffHtml, content, openTab, setIsAIChatOpen]);
147
+
45
148
  return (
46
149
  <div className="diff-view">
47
150
  <div className="diff-summary">
@@ -65,6 +168,7 @@ export const DiffView: React.FC<DiffViewProps> = ({
65
168
 
66
169
  <div
67
170
  className="diff-files"
171
+ ref={diffContainerRef}
68
172
  dangerouslySetInnerHTML={{ __html: diffHtml }}
69
173
  />
70
174
  </div>
@@ -12,7 +12,7 @@ const StreamingMessageComponent = ({
12
12
  status,
13
13
  }: StreamingMessageProps) => {
14
14
  // Don't render if there's no content and no status
15
- if (!content.trim() && !status) {
15
+ if (!content.trim()) {
16
16
  return null;
17
17
  }
18
18
 
@@ -68,13 +68,15 @@ export const AIChat = () => {
68
68
  aiChatMessage,
69
69
  );
70
70
  const messageContent = messageToSend || aiChatMessage;
71
+
71
72
  if (
72
73
  !messageContent ||
73
74
  typeof messageContent !== 'string' ||
74
75
  !messageContent.trim() ||
75
76
  isLoading
76
- )
77
+ ) {
77
78
  return;
79
+ }
78
80
 
79
81
  const currentPrompt = aiChatMessage.trim();
80
82
  setAIChatMessage('');
@@ -144,9 +146,15 @@ export const AIChat = () => {
144
146
  // The backend handles all ShareDB operations for successful responses
145
147
  } catch (error) {
146
148
  console.error('Error getting AI response:', error);
147
- setErrorMessage(
148
- 'Failed to send message. Please try again.',
149
- );
149
+ // Fail silently in case of timeout,
150
+ // which happens frequently with the longer running LLM calls.
151
+ // TODO refactor this so that the network request to the server
152
+ // always returns immediately, and we track the `isLoading` state
153
+ // within the ShareDB document itself.
154
+ //
155
+ // setErrorMessage(
156
+ // 'Failed to send message. Please try again.',
157
+ // );
150
158
  } finally {
151
159
  setIsLoading(false);
152
160
  }
@@ -212,159 +220,158 @@ export const AIChat = () => {
212
220
 
213
221
  return (
214
222
  <div className="ai-chat-container">
215
- <div
216
- style={{
217
- padding: '10px',
218
- flex: 1,
219
- display: 'flex',
220
- flexDirection: 'column',
221
- }}
222
- >
223
- {isEmptyState ? (
224
- <div className="ai-chat-empty">
225
- <div className="ai-chat-empty-icon">✨</div>
226
- <h3 className="ai-chat-empty-title">
227
- Edit with AI
228
- </h3>
229
- <div className="ai-chat-empty-text">
230
- How can I help you?
231
- </div>
232
- {showSuggestedRequests && (
233
- <div className="ai-chat-empty-examples">
234
- {aiChatMode === 'ask' ? (
235
- <>
236
- <h4>Try asking questions like:</h4>
237
- <div className="ai-chat-suggested-prompts">
238
- <button
239
- className="ai-chat-suggested-prompt"
240
- onClick={() =>
241
- handleSendMessage(
242
- 'Explain how this works',
243
- )
244
- }
245
- >
246
- "Explain how this works"
247
- </button>
248
- <button
249
- className="ai-chat-suggested-prompt"
250
- onClick={() =>
251
- handleSendMessage(
252
- 'How could I change it so that the circles are bigger?',
253
- )
254
- }
255
- >
256
- "How could I change it so that the
257
- circles are bigger?"
258
- </button>
259
- <button
260
- className="ai-chat-suggested-prompt"
261
- onClick={() =>
262
- handleSendMessage(
263
- 'What does this function do?',
264
- )
265
- }
266
- >
267
- "What does this function do?"
268
- </button>
269
- <button
270
- className="ai-chat-suggested-prompt"
271
- onClick={() =>
272
- handleSendMessage(
273
- 'How can I make this more accessible?',
274
- )
275
- }
276
- >
277
- "How can I make this more
278
- accessible?"
279
- </button>
280
- </div>
281
- </>
282
- ) : (
283
- <>
284
- <h4>Try edit requests like these:</h4>
285
- <div className="ai-chat-suggested-prompts">
286
- <button
287
- className="ai-chat-suggested-prompt"
288
- onClick={() =>
289
- handleSendMessage(
290
- 'Change the circles to squares',
291
- )
292
- }
293
- >
294
- "Change the circles to squares"
295
- </button>
296
- <button
297
- className="ai-chat-suggested-prompt"
298
- onClick={() =>
299
- handleSendMessage(
300
- 'Add a button that toggles the animation',
301
- )
302
- }
303
- >
304
- "Add a button that toggles the
305
- animation"
306
- </button>
307
- <button
308
- className="ai-chat-suggested-prompt"
309
- onClick={() =>
310
- handleSendMessage(
311
- 'Fix the CSS so the layout is responsive',
312
- )
313
- }
314
- >
315
- "Fix the CSS so the layout is
316
- responsive"
317
- </button>
318
- <button
319
- className="ai-chat-suggested-prompt"
320
- onClick={() =>
321
- handleSendMessage(
322
- 'Refactor this function to use async/await',
323
- )
324
- }
325
- >
326
- "Refactor this function to use
327
- async/await"
328
- </button>
329
- </div>
330
- </>
331
- )}
332
- </div>
333
- )}
334
- {showSuggestedRequests && (
223
+ <div className="ai-chat-content">
224
+ <div className="ai-chat-messages-container">
225
+ {isEmptyState ? (
226
+ <div className="ai-chat-empty">
227
+ <div className="ai-chat-empty-icon">✨</div>
228
+ <h3 className="ai-chat-empty-title">
229
+ Edit with AI
230
+ </h3>
335
231
  <div className="ai-chat-empty-text">
336
- {aiChatMode === 'ask'
337
- ? 'Type your question below to get started!'
338
- : 'Type your edit request below to get started!'}
232
+ How can I help you?
233
+ </div>
234
+ {showSuggestedRequests && (
235
+ <div className="ai-chat-empty-examples">
236
+ {aiChatMode === 'ask' ? (
237
+ <>
238
+ <h4>Try asking questions like:</h4>
239
+ <div className="ai-chat-suggested-prompts">
240
+ <button
241
+ className="ai-chat-suggested-prompt"
242
+ onClick={() =>
243
+ handleSendMessage(
244
+ 'Explain how this works',
245
+ )
246
+ }
247
+ >
248
+ "Explain how this works"
249
+ </button>
250
+ <button
251
+ className="ai-chat-suggested-prompt"
252
+ onClick={() =>
253
+ handleSendMessage(
254
+ 'How could I change it so that the circles are bigger?',
255
+ )
256
+ }
257
+ >
258
+ "How could I change it so that the
259
+ circles are bigger?"
260
+ </button>
261
+ <button
262
+ className="ai-chat-suggested-prompt"
263
+ onClick={() =>
264
+ handleSendMessage(
265
+ 'What does this function do?',
266
+ )
267
+ }
268
+ >
269
+ "What does this function do?"
270
+ </button>
271
+ <button
272
+ className="ai-chat-suggested-prompt"
273
+ onClick={() =>
274
+ handleSendMessage(
275
+ 'How can I make this more accessible?',
276
+ )
277
+ }
278
+ >
279
+ "How can I make this more
280
+ accessible?"
281
+ </button>
282
+ </div>
283
+ </>
284
+ ) : (
285
+ <>
286
+ <h4>Try edit requests like these:</h4>
287
+ <div className="ai-chat-suggested-prompts">
288
+ <button
289
+ className="ai-chat-suggested-prompt"
290
+ onClick={() =>
291
+ handleSendMessage(
292
+ 'Change the circles to squares',
293
+ )
294
+ }
295
+ >
296
+ "Change the circles to squares"
297
+ </button>
298
+ <button
299
+ className="ai-chat-suggested-prompt"
300
+ onClick={() =>
301
+ handleSendMessage(
302
+ 'Add a button that toggles the animation',
303
+ )
304
+ }
305
+ >
306
+ "Add a button that toggles the
307
+ animation"
308
+ </button>
309
+ <button
310
+ className="ai-chat-suggested-prompt"
311
+ onClick={() =>
312
+ handleSendMessage(
313
+ 'Fix the CSS so the layout is responsive',
314
+ )
315
+ }
316
+ >
317
+ "Fix the CSS so the layout is
318
+ responsive"
319
+ </button>
320
+ <button
321
+ className="ai-chat-suggested-prompt"
322
+ onClick={() =>
323
+ handleSendMessage(
324
+ 'Refactor this function to use async/await',
325
+ )
326
+ }
327
+ >
328
+ "Refactor this function to use
329
+ async/await"
330
+ </button>
331
+ </div>
332
+ </>
333
+ )}
334
+ </div>
335
+ )}
336
+ {showSuggestedRequests && (
337
+ <div className="ai-chat-empty-text">
338
+ {aiChatMode === 'ask'
339
+ ? 'Type your question below to get started!'
340
+ : 'Type your edit request below to get started!'}
341
+ </div>
342
+ )}
343
+ </div>
344
+ ) : (
345
+ <MessageList
346
+ messages={messages}
347
+ aiStatus={aiStatus}
348
+ isLoading={isLoading}
349
+ chatId={currentChatId}
350
+ aiScratchpad={aiScratchpad}
351
+ />
352
+ )}
353
+ {errorMessage && (
354
+ <div className="ai-chat-error">
355
+ <div className="ai-chat-error-content">
356
+ <span className="ai-chat-error-icon">
357
+ ⚠️
358
+ </span>
359
+ <span className="ai-chat-error-message">
360
+ {errorMessage}
361
+ </span>
362
+ <button
363
+ className="ai-chat-error-dismiss"
364
+ onClick={() => setErrorMessage(null)}
365
+ aria-label="Dismiss error"
366
+ >
367
+ ×
368
+ </button>
339
369
  </div>
340
- )}
341
- </div>
342
- ) : (
343
- <MessageList
344
- messages={messages}
345
- aiStatus={aiStatus}
346
- isLoading={isLoading}
347
- chatId={currentChatId}
348
- aiScratchpad={aiScratchpad}
349
- />
350
- )}
351
- {errorMessage && (
352
- <div className="ai-chat-error">
353
- <div className="ai-chat-error-content">
354
- <span className="ai-chat-error-icon">⚠️</span>
355
- <span className="ai-chat-error-message">
356
- {errorMessage}
357
- </span>
358
- <button
359
- className="ai-chat-error-dismiss"
360
- onClick={() => setErrorMessage(null)}
361
- aria-label="Dismiss error"
362
- >
363
- ×
364
- </button>
365
370
  </div>
366
- </div>
367
- )}
371
+ )}
372
+ </div>
373
+ </div>
374
+ <div className="ai-chat-input-fixed">
368
375
  <ChatInput
369
376
  aiChatMessage={aiChatMessage}
370
377
  setAIChatMessage={setAIChatMessage}