vzcode 1.57.0 → 1.59.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-DqIc-0Gp.js"></script>
24
- <link rel="stylesheet" crossorigin href="/assets/index-CE_xP06t.css">
23
+ <script type="module" crossorigin src="/assets/index-BJI4JLx9.js"></script>
24
+ <link rel="stylesheet" crossorigin href="/assets/index-afP-wJTZ.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.57.0",
3
+ "version": "1.59.0",
4
4
  "description": "Multiplayer code editor system",
5
5
  "main": "src/index.ts",
6
6
  "type": "module",
@@ -11,7 +11,7 @@
11
11
  ],
12
12
  "scripts": {
13
13
  "test": "vitest run",
14
- "test-interactive": "cd test/sampleDirectories/kitchenSink && node --import 'data:text/javascript,import { register } from \"node:module\"; import { pathToFileURL } from \"node:url\"; register(\"ts-node/esm\", pathToFileURL(\"./\"));' ../../../src/server/index.ts",
14
+ "test-interactive": "cd test/sampleDirectories/kitchenSink && node --import \"data:text/javascript,import { register } from 'node:module'; import { pathToFileURL } from 'node:url'; register('ts-node/esm', pathToFileURL('./'));\" ../../../src/server/index.ts",
15
15
  "prettier": "prettier {*.*,**/*.*} --write",
16
16
  "typecheck": "tsc --noEmit",
17
17
  "dev": "cross-env EDITOR_PORT=5173 concurrently \"npm run test-interactive\" \"vite\"",
@@ -117,8 +117,8 @@
117
117
  "@codemirror/state": "^6.5.2",
118
118
  "@codemirror/theme-one-dark": "^6.1.3",
119
119
  "@codemirror/view": "^6.38.1",
120
- "@langchain/core": "^0.3.66",
121
- "@langchain/openai": "^0.6.3",
120
+ "@langchain/core": "^0.3.67",
121
+ "@langchain/openai": "^0.6.4",
122
122
  "@lezer/highlight": "^1.2.1",
123
123
  "@livekit/components-react": "^2.9.14",
124
124
  "@replit/codemirror-indentation-markers": "^6.5.3",
@@ -179,17 +179,17 @@
179
179
  "@tauri-apps/cli": "^2.7.1",
180
180
  "@types/react": "^18",
181
181
  "@types/react-dom": "^18",
182
- "@vitejs/plugin-react": "^4.7.0",
182
+ "@vitejs/plugin-react": "^5.0.0",
183
183
  "concurrently": "^9.2.0",
184
184
  "cross-env": "^10.0.0",
185
185
  "eslint": "^9.32.0",
186
186
  "globals": "^16.3.0",
187
187
  "npm-check-updates": "^18.0.2",
188
188
  "prettier": "^3.6.2",
189
- "sass": "^1.89.2",
189
+ "sass": "^1.90.0",
190
190
  "ts-node": "^10.9.2",
191
191
  "typescript": "^5.9.2",
192
- "vite": "^7.0.6",
192
+ "vite": "^7.1.0",
193
193
  "vitest": "^3.2.4"
194
194
  },
195
195
  "optionalDependencies": {
@@ -74,6 +74,7 @@ import { getFileExtension } from '../utils/fileExtension';
74
74
  import { SparklesSVG } from '../Icons/SparklesSVG';
75
75
  import { VZCodeContext } from '../VZCodeContext';
76
76
  import { useContext, useMemo } from 'react';
77
+ import { handleAIChatMessage } from '../../server/aiChatHandler';
77
78
 
78
79
  const DEBUG = false;
79
80
 
@@ -194,6 +195,7 @@ export const getOrCreateEditor = async ({
194
195
  rainbowBracketsEnabled = true,
195
196
  setIsAIChatOpen,
196
197
  setAIChatMessage,
198
+ handleSendMessage,
197
199
  }: {
198
200
  // TODO pass this in from the outside
199
201
  paneId?: PaneId;
@@ -229,8 +231,9 @@ export const getOrCreateEditor = async ({
229
231
  view: EditorView,
230
232
  ) => Promise<readonly Diagnostic[]>;
231
233
  rainbowBracketsEnabled?: boolean; // New parameter type
232
- setIsAIChatOpen: any; // TODO fix types
233
- setAIChatMessage: any; // TODO fix types
234
+ setIsAIChatOpen: (isAIChatOpen: boolean) => void;
235
+ setAIChatMessage: (message: string) => void;
236
+ handleSendMessage: any; // TODO fix types
234
237
  }): Promise<ExtendedEditorCacheValue> => {
235
238
  // Cache hit
236
239
 
@@ -500,12 +503,9 @@ export const getOrCreateEditor = async ({
500
503
  root.render(
501
504
  <div
502
505
  onClick={() => {
503
- console.log(
504
- 'TODO set the chat prompt to "Implement the TODO"',
505
- );
506
506
  setIsAIChatOpen(true);
507
- setAIChatMessage('Implement the TODO');
508
- // submitAIChatMessage()
507
+ // setAIChatMessage('Implement the TODO');
508
+ handleSendMessage('Implement the TODO');
509
509
  }}
510
510
  >
511
511
  <SparklesSVG width={14} height={14} />
@@ -45,6 +45,7 @@ export const CodeEditor = ({
45
45
  enableAutoFollow,
46
46
  setIsAIChatOpen,
47
47
  setAIChatMessage,
48
+ handleSendMessage,
48
49
  } = useContext(VZCodeContext);
49
50
 
50
51
  // Set `doc.data.isInteracting` to `true` when the user is interacting
@@ -116,6 +117,7 @@ export const CodeEditor = ({
116
117
  esLintSource,
117
118
  setIsAIChatOpen,
118
119
  setAIChatMessage,
120
+ handleSendMessage,
119
121
  });
120
122
 
121
123
  if (isMounted) {
@@ -1,4 +1,5 @@
1
1
  import {
2
+ Context,
2
3
  createContext,
3
4
  useCallback,
4
5
  useReducer,
@@ -39,6 +40,7 @@ import { useURLSync } from './useURLSync';
39
40
  import { createInitialState, vzReducer } from './vzReducer';
40
41
  import { findPane } from './vzReducer/findPane';
41
42
  import { usePresenceAutoFollow } from './usePresenceAutoFollow';
43
+ import { v4 as uuidv4 } from 'uuid';
42
44
 
43
45
  // This context centralizes all the "smart" logic
44
46
  // to do with the application state. This includes
@@ -195,6 +197,15 @@ export type VZCodeContextValue = {
195
197
  setVoiceChatModalOpen: (state: boolean) => void;
196
198
  aiChatMessage: string;
197
199
  setAIChatMessage: (message: string) => void;
200
+ isLoading: boolean;
201
+ setIsLoading: (state: boolean) => void;
202
+ currentChatId: string;
203
+ aiErrorMessage: string | null;
204
+ setAIErrorMessage: (state: string | null) => void;
205
+ handleSendMessage: (
206
+ messageToSend?: string,
207
+ options?: Record<string, string>,
208
+ ) => void;
198
209
 
199
210
  // Auto-fork functions for VizHub integration
200
211
  autoForkAndRetryAI?: (
@@ -207,6 +218,17 @@ export type VZCodeContextValue = {
207
218
  prompt: string;
208
219
  modelName: string;
209
220
  } | null;
221
+
222
+ // Additional widgets that can be rendered in AI chat messages
223
+ additionalWidgets?: React.ComponentType<{
224
+ messageId: string;
225
+ chatId: string;
226
+ canUndo: boolean;
227
+ handleSendMessage?: (
228
+ messageToSend?: string,
229
+ options?: Record<string, string>,
230
+ ) => void;
231
+ }>;
210
232
  };
211
233
 
212
234
  export const VZCodeProvider = ({
@@ -233,6 +255,7 @@ export const VZCodeProvider = ({
233
255
  autoForkAndRetryAI,
234
256
  clearStoredAIPrompt,
235
257
  getStoredAIPrompt,
258
+ additionalWidgets,
236
259
  }: {
237
260
  content: VizContent;
238
261
  shareDBDoc: ShareDBDoc<VizContent>;
@@ -264,6 +287,11 @@ export const VZCodeProvider = ({
264
287
  prompt: string;
265
288
  modelName: string;
266
289
  } | null;
290
+ additionalWidgets?: React.ComponentType<{
291
+ messageId: string;
292
+ chatId: string;
293
+ canUndo: boolean;
294
+ }>;
267
295
  }) => {
268
296
  // Auto-run Pretter after local changes.
269
297
  const { prettierError, runPrettierRef } = usePrettier({
@@ -324,6 +352,7 @@ export const VZCodeProvider = ({
324
352
  username,
325
353
  enableAutoFollow,
326
354
  sidebarPresenceIndicators,
355
+ aiChatMode,
327
356
  } = state;
328
357
 
329
358
  const activePane: Pane = findPane(pane, activePaneId);
@@ -479,6 +508,123 @@ export const VZCodeProvider = ({
479
508
 
480
509
  const [aiChatMessage, setAIChatMessage] = useState('');
481
510
 
511
+ const DEBUG = false;
512
+
513
+ const [isLoading, setIsLoading] = useState(false);
514
+ const [currentChatId] = useState(() => uuidv4());
515
+ const [aiErrorMessage, setAIErrorMessage] = useState<
516
+ string | null
517
+ >(null);
518
+
519
+ const handleSendMessage = useCallback(
520
+ async (
521
+ messageToSend?: string,
522
+ options?: Record<string, string>,
523
+ ) => {
524
+ DEBUG &&
525
+ console.log(
526
+ 'AIChat: handleSendMessage called with:',
527
+ messageToSend,
528
+ 'aiChatMessage:',
529
+ aiChatMessage,
530
+ );
531
+ const messageContent = messageToSend || aiChatMessage;
532
+
533
+ if (
534
+ !messageContent ||
535
+ typeof messageContent !== 'string' ||
536
+ !messageContent.trim() ||
537
+ isLoading
538
+ ) {
539
+ return;
540
+ }
541
+
542
+ const currentPrompt = aiChatMessage.trim();
543
+ setAIChatMessage('');
544
+ setIsLoading(true);
545
+ setAIErrorMessage(null); // Clear any previous errors
546
+
547
+ // Call backend endpoint for AI response
548
+ // The server will handle all ShareDB operations including adding the user message
549
+ try {
550
+ const response = await fetch(aiChatEndpoint, {
551
+ method: 'POST',
552
+ headers: {
553
+ 'Content-Type': 'application/json',
554
+ },
555
+ body: JSON.stringify({
556
+ ...aiChatOptions,
557
+ content: messageContent.trim(),
558
+ chatId: currentChatId,
559
+ mode: aiChatMode,
560
+ ...options,
561
+ }),
562
+ });
563
+
564
+ if (!response.ok) {
565
+ throw new Error(
566
+ `HTTP error! status: ${response.status}`,
567
+ );
568
+ }
569
+
570
+ // Parse the response to check for errors
571
+ const responseData = await response.json();
572
+
573
+ // Check if the response contains a VizHub error
574
+ if (
575
+ responseData.outcome === 'failure' &&
576
+ responseData.error
577
+ ) {
578
+ const errorMessage = responseData.error.message;
579
+
580
+ // Check if this is the specific permission error that should trigger auto-fork
581
+ if (
582
+ aiErrorMessage ===
583
+ 'You do not have permission to use AI chat on this visualization. Only users with edit access can use this feature. Fork the viz to edit it.'
584
+ ) {
585
+ // Trigger auto-fork instead of showing error
586
+ try {
587
+ await autoForkAndRetryAI?.(
588
+ currentPrompt,
589
+ aiChatMode,
590
+ );
591
+ // If we reach here, the fork was successful and redirect should happen
592
+ return;
593
+ } catch (forkError) {
594
+ console.error('Auto-fork failed:', forkError);
595
+ setAIErrorMessage(
596
+ 'Failed to fork visualization. Please try forking manually.',
597
+ );
598
+ return;
599
+ }
600
+ }
601
+
602
+ // For other errors, show the error message
603
+ setAIErrorMessage(aiErrorMessage);
604
+ return;
605
+ }
606
+
607
+ // The backend handles all ShareDB operations for successful responses
608
+ } catch (error) {
609
+ console.error('Error getting AI response:', error);
610
+ setAIErrorMessage(
611
+ 'Failed to send message. Please try again.',
612
+ );
613
+ } finally {
614
+ setIsLoading(false);
615
+ }
616
+ },
617
+ [
618
+ aiChatMessage,
619
+ isLoading,
620
+ aiChatEndpoint,
621
+ aiChatOptions,
622
+ currentChatId,
623
+ aiChatMode,
624
+ autoForkAndRetryAI,
625
+ ],
626
+ );
627
+
482
628
  // The value provided by this context.
483
629
  const value: VZCodeContextValue = {
484
630
  content,
@@ -589,11 +735,20 @@ export const VZCodeProvider = ({
589
735
 
590
736
  aiChatMessage,
591
737
  setAIChatMessage,
738
+ isLoading,
739
+ setIsLoading,
740
+ currentChatId,
741
+ aiErrorMessage,
742
+ setAIErrorMessage,
743
+ handleSendMessage,
592
744
 
593
745
  // Auto-fork functions for VizHub integration
594
746
  autoForkAndRetryAI,
595
747
  clearStoredAIPrompt,
596
748
  getStoredAIPrompt,
749
+
750
+ // Additional widgets that can be rendered in AI chat messages
751
+ additionalWidgets,
597
752
  };
598
753
 
599
754
  return (
@@ -1,7 +1,12 @@
1
1
  import { timestampToDate } from '@vizhub/viz-utils';
2
2
  import Markdown from 'react-markdown';
3
3
  import remarkGfm from 'remark-gfm';
4
- import { useMemo, memo, useState, useContext } from 'react';
4
+ import React, {
5
+ useMemo,
6
+ memo,
7
+ useState,
8
+ useContext,
9
+ } from 'react';
5
10
  import { DiffView } from './DiffView';
6
11
  import { UnifiedFilesDiff } from '../../../utils/fileDiff';
7
12
  import { enableDiffView } from '../../featureFlags';
@@ -31,8 +36,12 @@ const MessageComponent = ({
31
36
  canUndo,
32
37
  }: MessageProps) => {
33
38
  const [isUndoing, setIsUndoing] = useState(false);
34
- const { aiChatUndoEndpoint, aiChatOptions = {} } =
35
- useContext(VZCodeContext);
39
+ const {
40
+ aiChatUndoEndpoint,
41
+ aiChatOptions = {},
42
+ additionalWidgets,
43
+ handleSendMessage,
44
+ } = useContext(VZCodeContext);
36
45
 
37
46
  // Memoize date formatting to avoid repeated computation
38
47
  const formattedTime = useMemo(() => {
@@ -102,7 +111,16 @@ const MessageComponent = ({
102
111
  Object.keys(diffData).length > 0 && (
103
112
  <DiffView diffData={diffData} />
104
113
  )}
105
- {canUndo && beforeFiles && (
114
+ {additionalWidgets &&
115
+ canUndo &&
116
+ chatId &&
117
+ React.createElement(additionalWidgets, {
118
+ messageId: id,
119
+ chatId: chatId,
120
+ canUndo: canUndo,
121
+ handleSendMessage,
122
+ })}
123
+ {!additionalWidgets && canUndo && beforeFiles && (
106
124
  <div className="undo-button-container">
107
125
  <button
108
126
  className="undo-button"
@@ -12,13 +12,11 @@ import { VizChatMessage } from '@vizhub/viz-types';
12
12
 
13
13
  const MessageListComponent = ({
14
14
  messages,
15
- aiStatus,
16
15
  isLoading,
17
16
  chatId, // Add chatId prop
18
17
  aiScratchpad, // Add aiScratchpad prop
19
18
  }: {
20
19
  messages: VizChatMessage[];
21
- aiStatus?: string;
22
20
  isLoading: boolean;
23
21
  chatId?: string; // Add chatId to the type
24
22
  aiScratchpad?: string; // Add aiScratchpad to the type
@@ -1,4 +1,10 @@
1
- import { memo } from 'react';
1
+ import {
2
+ memo,
3
+ useRef,
4
+ useEffect,
5
+ useCallback,
6
+ useState,
7
+ } from 'react';
2
8
  import Markdown from 'react-markdown';
3
9
  import remarkGfm from 'remark-gfm';
4
10
 
@@ -11,6 +17,115 @@ const ThinkingScratchpadComponent = ({
11
17
  content,
12
18
  isVisible,
13
19
  }: ThinkingScratchpadProps) => {
20
+ const contentRef = useRef<HTMLDivElement>(null);
21
+ const [isUserScrolled, setIsUserScrolled] =
22
+ useState(false);
23
+ const [autoScrollEnabled, setAutoScrollEnabled] =
24
+ useState(true);
25
+ const scrollTimeoutRef =
26
+ useRef<ReturnType<typeof setTimeout>>();
27
+
28
+ // Check if the user is scrolled to the bottom
29
+ const isScrolledToBottom = useCallback(() => {
30
+ const container = contentRef.current;
31
+ if (!container) return true;
32
+
33
+ const threshold = 10; // Allow 10px tolerance for "at bottom"
34
+ const { scrollTop, scrollHeight, clientHeight } =
35
+ container;
36
+ return (
37
+ scrollHeight - scrollTop - clientHeight < threshold
38
+ );
39
+ }, []);
40
+
41
+ // Smooth scroll to bottom
42
+ const scrollToBottom = useCallback(() => {
43
+ if (!autoScrollEnabled) return;
44
+
45
+ const container = contentRef.current;
46
+ if (container) {
47
+ container.scrollTo({
48
+ top: container.scrollHeight,
49
+ behavior: 'smooth',
50
+ });
51
+ }
52
+ }, [autoScrollEnabled]);
53
+
54
+ // Handle scroll events to detect user manual scrolling
55
+ const handleScroll = useCallback(() => {
56
+ const container = contentRef.current;
57
+ if (!container) return;
58
+
59
+ const isAtBottom = isScrolledToBottom();
60
+
61
+ // Clear any existing timeout
62
+ if (scrollTimeoutRef.current) {
63
+ clearTimeout(scrollTimeoutRef.current);
64
+ }
65
+
66
+ // If user scrolled up from bottom, disable auto-scroll
67
+ if (!isAtBottom && !isUserScrolled) {
68
+ setIsUserScrolled(true);
69
+ setAutoScrollEnabled(false);
70
+ }
71
+
72
+ // If user scrolled back to bottom, re-enable auto-scroll after a brief delay
73
+ if (isAtBottom && isUserScrolled) {
74
+ scrollTimeoutRef.current = setTimeout(() => {
75
+ setIsUserScrolled(false);
76
+ setAutoScrollEnabled(true);
77
+ }, 300); // 300ms delay to prevent flickering
78
+ }
79
+ }, [isUserScrolled, isScrolledToBottom]);
80
+
81
+ // Auto-scroll when content changes, but only if auto-scroll is enabled
82
+ useEffect(() => {
83
+ if (
84
+ autoScrollEnabled &&
85
+ !isUserScrolled &&
86
+ isVisible &&
87
+ content
88
+ ) {
89
+ // Use a short debounce to prevent multiple scroll calls during rapid updates
90
+ if (scrollTimeoutRef.current) {
91
+ clearTimeout(scrollTimeoutRef.current);
92
+ }
93
+
94
+ scrollTimeoutRef.current = setTimeout(() => {
95
+ scrollToBottom();
96
+ }, 50); // 50ms debounce
97
+ }
98
+
99
+ return () => {
100
+ if (scrollTimeoutRef.current) {
101
+ clearTimeout(scrollTimeoutRef.current);
102
+ }
103
+ };
104
+ }, [
105
+ content,
106
+ autoScrollEnabled,
107
+ isUserScrolled,
108
+ isVisible,
109
+ scrollToBottom,
110
+ ]);
111
+
112
+ // Clean up timeout on unmount
113
+ useEffect(() => {
114
+ return () => {
115
+ if (scrollTimeoutRef.current) {
116
+ clearTimeout(scrollTimeoutRef.current);
117
+ }
118
+ };
119
+ }, []);
120
+
121
+ // Reset scroll state when component becomes visible
122
+ useEffect(() => {
123
+ if (isVisible) {
124
+ setIsUserScrolled(false);
125
+ setAutoScrollEnabled(true);
126
+ }
127
+ }, [isVisible]);
128
+
14
129
  if (!isVisible || !content) {
15
130
  return null;
16
131
  }
@@ -23,7 +138,11 @@ const ThinkingScratchpadComponent = ({
23
138
  AI is thinking...
24
139
  </span>
25
140
  </div>
26
- <div className="thinking-scratchpad-content">
141
+ <div
142
+ className="thinking-scratchpad-content"
143
+ ref={contentRef}
144
+ onScroll={handleScroll}
145
+ >
27
146
  <Markdown remarkPlugins={[remarkGfm]}>
28
147
  {content}
29
148
  </Markdown>