vzcode 1.56.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-Cp6YlJo2.js"></script>
24
- <link rel="stylesheet" crossorigin href="/assets/index-COSAnnol.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.56.0",
3
+ "version": "1.57.0",
4
4
  "description": "Multiplayer code editor system",
5
5
  "main": "src/index.ts",
6
6
  "type": "module",
@@ -112,7 +112,7 @@ const ChatInputComponent = ({
112
112
  <Form.Group className="ai-chat-input-group">
113
113
  <Form.Control
114
114
  as="textarea"
115
- rows={2}
115
+ rows={5}
116
116
  value={aiChatMessage}
117
117
  onChange={handleChange}
118
118
  onKeyDown={handleKeyDown}
@@ -126,26 +126,25 @@ const ChatInputComponent = ({
126
126
  disabled={isLoading}
127
127
  aria-label="Chat message input"
128
128
  />
129
- {aiChatMessage && (
130
- <div className="ai-chat-input-info">
131
- <span className="ai-chat-hint">
132
- Shift+Enter for new line
133
- </span>
134
- <span className="ai-chat-hint">
135
- Press Enter to send
136
- </span>
137
- </div>
138
- )}
139
- <Button
140
- variant="primary"
141
- onClick={handleSendClick}
142
- disabled={!aiChatMessage.trim() || isLoading}
143
- className="ai-chat-send-button"
144
- aria-label="Send message"
145
- title="Send message (Enter)"
146
- >
147
- Send
148
- </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>
149
148
  </Form.Group>
150
149
  </div>
151
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
 
@@ -146,9 +146,15 @@ export const AIChat = () => {
146
146
  // The backend handles all ShareDB operations for successful responses
147
147
  } catch (error) {
148
148
  console.error('Error getting AI response:', error);
149
- setErrorMessage(
150
- 'Failed to send message. Please try again.',
151
- );
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
+ // );
152
158
  } finally {
153
159
  setIsLoading(false);
154
160
  }
@@ -214,159 +220,158 @@ export const AIChat = () => {
214
220
 
215
221
  return (
216
222
  <div className="ai-chat-container">
217
- <div
218
- style={{
219
- padding: '10px',
220
- flex: 1,
221
- display: 'flex',
222
- flexDirection: 'column',
223
- }}
224
- >
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>
231
- <div className="ai-chat-empty-text">
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 && (
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>
337
231
  <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!'}
232
+ How can I help you?
341
233
  </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">⚠️</span>
357
- <span className="ai-chat-error-message">
358
- {errorMessage}
359
- </span>
360
- <button
361
- className="ai-chat-error-dismiss"
362
- onClick={() => setErrorMessage(null)}
363
- aria-label="Dismiss error"
364
- >
365
- ×
366
- </button>
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
+ )}
367
343
  </div>
368
- </div>
369
- )}
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>
369
+ </div>
370
+ </div>
371
+ )}
372
+ </div>
373
+ </div>
374
+ <div className="ai-chat-input-fixed">
370
375
  <ChatInput
371
376
  aiChatMessage={aiChatMessage}
372
377
  setAIChatMessage={setAIChatMessage}
@@ -5,6 +5,26 @@
5
5
  padding: 0;
6
6
  }
7
7
 
8
+ .ai-chat-content {
9
+ flex: 1;
10
+ overflow: hidden;
11
+ display: flex;
12
+ flex-direction: column;
13
+ }
14
+
15
+ .ai-chat-messages-container {
16
+ flex: 1;
17
+ overflow-y: auto;
18
+ padding: 10px;
19
+ padding-bottom: 0;
20
+ }
21
+
22
+ .ai-chat-input-fixed {
23
+ flex-shrink: 0;
24
+ padding: 10px;
25
+ padding-top: 0;
26
+ }
27
+
8
28
  .ai-chat-empty {
9
29
  flex: 1;
10
30
  display: flex;
@@ -365,6 +385,7 @@
365
385
  .ai-chat-input-container {
366
386
  border-top: 1px solid var(--vh-color-neutral-02);
367
387
  padding-top: 10px;
388
+ background: var(--vh-color-neutral-01);
368
389
  }
369
390
 
370
391
  .ai-chat-mode-toggle {
@@ -427,11 +448,12 @@
427
448
  border: 1px solid var(--vh-color-neutral-03) !important;
428
449
  color: var(--vh-color-neutral-04) !important;
429
450
  border-radius: 8px;
430
- padding: 8px 10px;
451
+ padding: 10px 12px;
431
452
  font-size: 14px;
432
453
  font-family: var(--vzcode-font-family);
433
454
  resize: none;
434
- min-height: 36px;
455
+ min-height: 48px;
456
+ line-height: 1.4;
435
457
 
436
458
  &:focus {
437
459
  outline: none !important;
@@ -450,37 +472,56 @@
450
472
  }
451
473
  }
452
474
 
453
- .ai-chat-input-info {
475
+ .ai-chat-input-footer {
454
476
  display: flex;
455
477
  justify-content: space-between;
478
+ align-items: center;
456
479
  font-size: 12px;
457
480
  color: var(--vh-color-neutral-03);
458
- margin-bottom: 6px;
481
+ min-height: 40px; // Prevent layout jumping
459
482
 
460
483
  .ai-chat-hint {
461
484
  font-style: italic;
485
+ flex: 1;
462
486
  }
463
487
  }
464
488
 
465
489
  .ai-chat-send-button {
466
- background: var(--vh-color-primary-01);
467
- border: none;
468
- color: white;
469
490
  border-radius: 8px;
470
491
  padding: 8px 16px;
471
492
  font-size: 14px;
472
493
  font-weight: 500;
473
494
  cursor: pointer;
474
- transition: background-color 0.2s ease;
475
- align-self: flex-end;
495
+ transition: all 0.2s ease;
476
496
  min-width: 80px;
497
+ flex-shrink: 0;
477
498
 
478
- &:hover:not(:disabled) {
479
- background: var(--vh-color-primary-02);
499
+ &.btn-primary {
500
+ background: var(--vh-color-primary-01);
501
+ border-color: var(--vh-color-primary-01);
502
+ color: white;
503
+
504
+ &:hover:not(:disabled) {
505
+ background: var(--vh-color-primary-02);
506
+ border-color: var(--vh-color-primary-02);
507
+ }
508
+ }
509
+
510
+ &.btn-outline-secondary {
511
+ background: transparent;
512
+ border-color: var(--vh-color-neutral-03);
513
+ color: var(--vh-color-neutral-03);
514
+
515
+ &:hover:not(:disabled) {
516
+ background: var(--vh-color-neutral-03);
517
+ color: var(--vh-color-neutral-01);
518
+ }
480
519
  }
481
520
 
482
521
  &:disabled {
483
522
  background: var(--vh-color-neutral-03);
523
+ border-color: var(--vh-color-neutral-03);
524
+ color: var(--vh-color-neutral-02);
484
525
  cursor: not-allowed;
485
526
  }
486
527
  }