vzcode 1.45.0 → 1.47.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.
Files changed (33) hide show
  1. package/dist/assets/{index-C1lTrzOZ.js → index-BMYXp1Uw.js} +114 -114
  2. package/dist/assets/{index-DeeOcIzw.css → index-BXyJ2hMf.css} +1 -1
  3. package/dist/index.html +2 -2
  4. package/package.json +7 -3
  5. package/src/client/CodeEditor/index.tsx +7 -4
  6. package/src/client/RunCodeWidget/index.tsx +18 -15
  7. package/src/client/SplitPaneResizeContext.tsx +26 -108
  8. package/src/client/VZCodeContext.tsx +8 -1
  9. package/src/client/VZRight.tsx +32 -4
  10. package/src/client/VZSidebar/AIChat/ChatInput.tsx +72 -2
  11. package/src/client/VZSidebar/AIChat/DiffView.scss +29 -0
  12. package/src/client/VZSidebar/AIChat/DiffView.tsx +62 -1
  13. package/src/client/VZSidebar/AIChat/Message.tsx +14 -1
  14. package/src/client/VZSidebar/AIChat/MessageList.tsx +134 -13
  15. package/src/client/VZSidebar/AIChat/ThinkingScratchpad.tsx +37 -0
  16. package/src/client/VZSidebar/AIChat/index.tsx +187 -16
  17. package/src/client/VZSidebar/AIChat/styles.scss +278 -1
  18. package/src/client/VZSidebar/aiCopyPaste.ts +6 -1
  19. package/src/client/VZSidebar/index.tsx +10 -2
  20. package/src/client/bootstrap.ts +11 -1
  21. package/src/client/useActions.ts +12 -0
  22. package/src/client/usePrettier/index.ts +1 -3
  23. package/src/client/vzReducer/aiChatReducer.ts +13 -0
  24. package/src/client/vzReducer/createInitialState.ts +1 -0
  25. package/src/client/vzReducer/index.ts +9 -0
  26. package/src/client/vzReducer/searchReducer.test.ts +44 -2
  27. package/src/runCode.ts +5 -17
  28. package/src/server/aiChatHandler/aiEditing.ts +37 -0
  29. package/src/server/aiChatHandler/chatOperations.ts +38 -1
  30. package/src/server/aiChatHandler/index.ts +20 -9
  31. package/src/server/aiChatHandler/llmStreaming.ts +101 -19
  32. package/src/server/generateAIResponse.ts +4 -0
  33. package/src/server/index.ts +52 -0
@@ -13,14 +13,21 @@ interface MessageProps {
13
13
  timestamp: number;
14
14
  isStreaming?: boolean;
15
15
  diffData?: UnifiedFilesDiff;
16
+ beforeFiles?: any;
17
+ chatId?: string;
18
+ canUndo?: boolean;
16
19
  }
17
20
 
18
21
  const MessageComponent = ({
22
+ id,
19
23
  role,
20
24
  content,
21
25
  timestamp,
22
26
  isStreaming,
23
27
  diffData,
28
+ beforeFiles,
29
+ chatId,
30
+ canUndo,
24
31
  }: MessageProps) => {
25
32
  // Memoize date formatting to avoid repeated computation
26
33
  const formattedTime = useMemo(() => {
@@ -47,7 +54,13 @@ const MessageComponent = ({
47
54
  {enableDiffView &&
48
55
  diffData &&
49
56
  Object.keys(diffData).length > 0 && (
50
- <DiffView diffData={diffData} />
57
+ <DiffView
58
+ diffData={diffData}
59
+ messageId={id}
60
+ chatId={chatId}
61
+ beforeFiles={beforeFiles}
62
+ canUndo={canUndo}
63
+ />
51
64
  )}
52
65
  </div>
53
66
  <div className="ai-chat-message-time">
@@ -3,31 +3,118 @@ import {
3
3
  useEffect,
4
4
  useCallback,
5
5
  memo,
6
+ useState,
6
7
  } from 'react';
7
8
  import { Message } from './Message';
8
9
  import { TypingIndicator } from './TypingIndicator';
10
+ import { ThinkingScratchpad } from './ThinkingScratchpad';
9
11
  import { VizChatMessage } from '@vizhub/viz-types';
10
12
 
11
13
  const MessageListComponent = ({
12
14
  messages,
13
15
  aiStatus,
14
16
  isLoading,
17
+ chatId, // Add chatId prop
18
+ aiScratchpad, // Add aiScratchpad prop
15
19
  }: {
16
20
  messages: VizChatMessage[];
17
21
  aiStatus?: string;
18
22
  isLoading: boolean;
23
+ chatId?: string; // Add chatId to the type
24
+ aiScratchpad?: string; // Add aiScratchpad to the type
19
25
  }) => {
20
26
  const messagesEndRef = useRef<HTMLDivElement>(null);
27
+ const messagesContainerRef = useRef<HTMLDivElement>(null);
28
+ const [isUserScrolled, setIsUserScrolled] =
29
+ useState(false);
30
+ const [autoScrollEnabled, setAutoScrollEnabled] =
31
+ useState(true);
32
+ const scrollTimeoutRef =
33
+ useRef<ReturnType<typeof setTimeout>>();
21
34
 
35
+ // Check if the user is scrolled to the bottom
36
+ const isScrolledToBottom = useCallback(() => {
37
+ const container = messagesContainerRef.current;
38
+ if (!container) return true;
39
+
40
+ const threshold = 50; // Allow 50px tolerance for "at bottom"
41
+ const { scrollTop, scrollHeight, clientHeight } =
42
+ container;
43
+ return (
44
+ scrollHeight - scrollTop - clientHeight < threshold
45
+ );
46
+ }, []);
47
+
48
+ // Smooth scroll to bottom with linear transition
22
49
  const scrollToBottom = useCallback(() => {
50
+ if (!autoScrollEnabled) return;
51
+
23
52
  messagesEndRef.current?.scrollIntoView({
24
53
  behavior: 'smooth',
54
+ block: 'end',
25
55
  });
26
- }, []);
56
+ }, [autoScrollEnabled]);
57
+
58
+ // Handle scroll events to detect user manual scrolling
59
+ const handleScroll = useCallback(() => {
60
+ const container = messagesContainerRef.current;
61
+ if (!container) return;
62
+
63
+ const isAtBottom = isScrolledToBottom();
64
+
65
+ // Clear any existing timeout
66
+ if (scrollTimeoutRef.current) {
67
+ clearTimeout(scrollTimeoutRef.current);
68
+ }
69
+
70
+ // If user scrolled up from bottom, disable auto-scroll
71
+ if (!isAtBottom && !isUserScrolled) {
72
+ setIsUserScrolled(true);
73
+ setAutoScrollEnabled(false);
74
+ }
75
+
76
+ // If user scrolled back to bottom, re-enable auto-scroll after a brief delay
77
+ if (isAtBottom && isUserScrolled) {
78
+ scrollTimeoutRef.current = setTimeout(() => {
79
+ setIsUserScrolled(false);
80
+ setAutoScrollEnabled(true);
81
+ }, 500); // 500ms delay to prevent flickering
82
+ }
83
+ }, [isUserScrolled, isScrolledToBottom]);
27
84
 
85
+ // Auto-scroll when messages change, but only if auto-scroll is enabled
28
86
  useEffect(() => {
29
- scrollToBottom();
30
- }, [messages]);
87
+ if (autoScrollEnabled && !isUserScrolled) {
88
+ // Use a short debounce to prevent multiple scroll calls during rapid updates
89
+ if (scrollTimeoutRef.current) {
90
+ clearTimeout(scrollTimeoutRef.current);
91
+ }
92
+
93
+ scrollTimeoutRef.current = setTimeout(() => {
94
+ scrollToBottom();
95
+ }, 100); // 100ms debounce
96
+ }
97
+
98
+ return () => {
99
+ if (scrollTimeoutRef.current) {
100
+ clearTimeout(scrollTimeoutRef.current);
101
+ }
102
+ };
103
+ }, [
104
+ messages,
105
+ autoScrollEnabled,
106
+ isUserScrolled,
107
+ scrollToBottom,
108
+ ]);
109
+
110
+ // Clean up timeout on unmount
111
+ useEffect(() => {
112
+ return () => {
113
+ if (scrollTimeoutRef.current) {
114
+ clearTimeout(scrollTimeoutRef.current);
115
+ }
116
+ };
117
+ }, []);
31
118
 
32
119
  // Check if AI generation has started (last message is from assistant)
33
120
  const lastMessage = messages[messages.length - 1];
@@ -39,18 +126,52 @@ const MessageListComponent = ({
39
126
  const showTypingIndicator =
40
127
  isLoading && !aiGenerationStarted;
41
128
 
129
+ // Show thinking scratchpad when AI is thinking (has scratchpad content)
130
+ const showThinkingScratchpad = Boolean(
131
+ aiScratchpad && aiScratchpad.trim(),
132
+ );
133
+
42
134
  return (
43
- <div className="ai-chat-messages">
44
- {messages.map((msg) => (
45
- <Message
46
- key={msg.id}
47
- id={msg.id}
48
- role={msg.role}
49
- content={msg.content}
50
- timestamp={msg.timestamp}
51
- diffData={(msg as any).diffData}
135
+ <div
136
+ className="ai-chat-messages"
137
+ ref={messagesContainerRef}
138
+ onScroll={handleScroll}
139
+ >
140
+ {messages.map((msg, index) => {
141
+ // Only the most recent assistant message with diffData can be undone
142
+ const isLastAssistantMessage =
143
+ msg.role === 'assistant' &&
144
+ index === messages.length - 1 &&
145
+ !isLoading; // Can't undo while AI is still generating
146
+
147
+ const canUndo =
148
+ isLastAssistantMessage &&
149
+ (msg as any).diffData &&
150
+ (msg as any).beforeFiles &&
151
+ Object.keys((msg as any).diffData || {}).length >
152
+ 0;
153
+
154
+ return (
155
+ <Message
156
+ key={msg.id}
157
+ id={msg.id}
158
+ role={msg.role}
159
+ content={msg.content}
160
+ timestamp={msg.timestamp}
161
+ diffData={(msg as any).diffData}
162
+ beforeFiles={(msg as any).beforeFiles}
163
+ chatId={chatId}
164
+ canUndo={canUndo}
165
+ />
166
+ );
167
+ })}
168
+
169
+ {showThinkingScratchpad && (
170
+ <ThinkingScratchpad
171
+ content={aiScratchpad || ''}
172
+ isVisible={showThinkingScratchpad}
52
173
  />
53
- ))}
174
+ )}
54
175
 
55
176
  {showTypingIndicator && <TypingIndicator />}
56
177
  <div ref={messagesEndRef} />
@@ -0,0 +1,37 @@
1
+ import { memo } from 'react';
2
+ import Markdown from 'react-markdown';
3
+ import remarkGfm from 'remark-gfm';
4
+
5
+ interface ThinkingScratchpadProps {
6
+ content: string;
7
+ isVisible: boolean;
8
+ }
9
+
10
+ const ThinkingScratchpadComponent = ({
11
+ content,
12
+ isVisible,
13
+ }: ThinkingScratchpadProps) => {
14
+ if (!isVisible || !content) {
15
+ return null;
16
+ }
17
+
18
+ return (
19
+ <div className="thinking-scratchpad">
20
+ <div className="thinking-scratchpad-header">
21
+ <span className="thinking-scratchpad-icon">🧠</span>
22
+ <span className="thinking-scratchpad-title">
23
+ VizBot is thinking...
24
+ </span>
25
+ </div>
26
+ <div className="thinking-scratchpad-content">
27
+ <Markdown remarkPlugins={[remarkGfm]}>
28
+ {content}
29
+ </Markdown>
30
+ </div>
31
+ </div>
32
+ );
33
+ };
34
+
35
+ export const ThinkingScratchpad = memo(
36
+ ThinkingScratchpadComponent,
37
+ );
@@ -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,18 +18,24 @@ 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,
24
27
  content,
25
28
  aiChatEndpoint = defaultAIChatEndpoint,
26
29
  aiChatOptions = {},
30
+ aiChatMode,
31
+ setAIChatMode,
27
32
  } = useContext(VZCodeContext);
28
33
 
29
34
  // Get current chat data from content
30
35
  const currentChat = content?.chats?.[currentChatId];
31
36
  const rawMessages = currentChat?.messages || [];
32
37
  const aiStatus = currentChat?.aiStatus;
38
+ const aiScratchpad = currentChat?.aiScratchpad;
33
39
 
34
40
  // Transform messages to ensure they have required id field - memoized to avoid recreation
35
41
  const messages = useMemo(
@@ -41,11 +47,15 @@ export const AIChat = () => {
41
47
  [rawMessages],
42
48
  );
43
49
 
50
+ // Check if this is the first time opening the chat (no messages)
51
+ const isEmptyState = rawMessages.length === 0;
52
+
44
53
  const handleSendMessage = useCallback(async () => {
45
54
  if (!aiChatMessage.trim() || isLoading) return;
46
55
 
47
56
  setAIChatMessage('');
48
57
  setIsLoading(true);
58
+ setErrorMessage(null); // Clear any previous errors
49
59
 
50
60
  // Call backend endpoint for AI response
51
61
  // The server will handle all ShareDB operations including adding the user message
@@ -57,8 +67,10 @@ export const AIChat = () => {
57
67
  },
58
68
  body: JSON.stringify({
59
69
  ...aiChatOptions,
70
+ vizId: aiChatOptions.vizId,
60
71
  content: aiChatMessage.trim(),
61
72
  chatId: currentChatId,
73
+ mode: aiChatMode,
62
74
  }),
63
75
  });
64
76
 
@@ -68,11 +80,24 @@ export const AIChat = () => {
68
80
  );
69
81
  }
70
82
 
71
- // The backend handles all ShareDB operations
72
- 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
73
96
  } catch (error) {
74
97
  console.error('Error getting AI response:', error);
75
- // Server will handle error messages too
98
+ setErrorMessage(
99
+ 'Failed to send message. Please try again.',
100
+ );
76
101
  } finally {
77
102
  setIsLoading(false);
78
103
  }
@@ -86,18 +111,164 @@ export const AIChat = () => {
86
111
 
87
112
  return (
88
113
  <div className="ai-chat-container">
89
- <MessageList
90
- messages={messages}
91
- aiStatus={aiStatus}
92
- isLoading={isLoading}
93
- />
94
- <ChatInput
95
- aiChatMessage={aiChatMessage}
96
- setAIChatMessage={setAIChatMessage}
97
- onSendMessage={handleSendMessage}
98
- isLoading={isLoading}
99
- focused={aiChatFocused}
100
- />
114
+ <div
115
+ style={{
116
+ padding: '10px',
117
+ flex: 1,
118
+ display: 'flex',
119
+ flexDirection: 'column',
120
+ }}
121
+ >
122
+ {isEmptyState ? (
123
+ <div className="ai-chat-empty">
124
+ <div className="ai-chat-empty-icon">✨</div>
125
+ <h3 className="ai-chat-empty-title">
126
+ Hi, I'm VizBot!
127
+ </h3>
128
+ <div className="ai-chat-empty-text">
129
+ How can I help you?
130
+ </div>
131
+ <div className="ai-chat-empty-examples">
132
+ {aiChatMode === 'ask' ? (
133
+ <>
134
+ <h4>Try asking questions like:</h4>
135
+ <div className="ai-chat-suggested-prompts">
136
+ <button
137
+ className="ai-chat-suggested-prompt"
138
+ onClick={() =>
139
+ setAIChatMessage(
140
+ 'Explain how this works',
141
+ )
142
+ }
143
+ >
144
+ "Explain how this works"
145
+ </button>
146
+ <button
147
+ className="ai-chat-suggested-prompt"
148
+ onClick={() =>
149
+ setAIChatMessage(
150
+ 'How could I change it so that the circles are bigger?',
151
+ )
152
+ }
153
+ >
154
+ "How could I change it so that the
155
+ circles are bigger?"
156
+ </button>
157
+ <button
158
+ className="ai-chat-suggested-prompt"
159
+ onClick={() =>
160
+ setAIChatMessage(
161
+ 'What does this function do?',
162
+ )
163
+ }
164
+ >
165
+ "What does this function do?"
166
+ </button>
167
+ <button
168
+ className="ai-chat-suggested-prompt"
169
+ onClick={() =>
170
+ setAIChatMessage(
171
+ 'How can I make this more accessible?',
172
+ )
173
+ }
174
+ >
175
+ "How can I make this more accessible?"
176
+ </button>
177
+ </div>
178
+ </>
179
+ ) : (
180
+ <>
181
+ <h4>Try edit requests like these:</h4>
182
+ <div className="ai-chat-suggested-prompts">
183
+ <button
184
+ className="ai-chat-suggested-prompt"
185
+ onClick={() =>
186
+ setAIChatMessage(
187
+ 'Change the circles to squares',
188
+ )
189
+ }
190
+ >
191
+ "Change the circles to squares"
192
+ </button>
193
+ <button
194
+ className="ai-chat-suggested-prompt"
195
+ onClick={() =>
196
+ setAIChatMessage(
197
+ 'Add a button that toggles the animation',
198
+ )
199
+ }
200
+ >
201
+ "Add a button that toggles the
202
+ animation"
203
+ </button>
204
+ <button
205
+ className="ai-chat-suggested-prompt"
206
+ onClick={() =>
207
+ setAIChatMessage(
208
+ 'Fix the CSS so the layout is responsive',
209
+ )
210
+ }
211
+ >
212
+ "Fix the CSS so the layout is
213
+ responsive"
214
+ </button>
215
+ <button
216
+ className="ai-chat-suggested-prompt"
217
+ onClick={() =>
218
+ setAIChatMessage(
219
+ 'Refactor this function to use async/await',
220
+ )
221
+ }
222
+ >
223
+ "Refactor this function to use
224
+ async/await"
225
+ </button>
226
+ </div>
227
+ </>
228
+ )}
229
+ </div>
230
+ <div className="ai-chat-empty-text">
231
+ {aiChatMode === 'ask'
232
+ ? 'Type your question below to get started!'
233
+ : 'Type your edit request below to get started!'}
234
+ </div>
235
+ </div>
236
+ ) : (
237
+ <MessageList
238
+ messages={messages}
239
+ aiStatus={aiStatus}
240
+ isLoading={isLoading}
241
+ chatId={currentChatId}
242
+ aiScratchpad={aiScratchpad}
243
+ />
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
+ )}
262
+ <ChatInput
263
+ aiChatMessage={aiChatMessage}
264
+ setAIChatMessage={setAIChatMessage}
265
+ onSendMessage={handleSendMessage}
266
+ isLoading={isLoading}
267
+ focused={aiChatFocused}
268
+ aiChatMode={aiChatMode}
269
+ setAIChatMode={setAIChatMode}
270
+ />
271
+ </div>
101
272
  </div>
102
273
  );
103
274
  };