vzcode 2.22.0 → 2.25.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 (44) hide show
  1. package/dist/assets/{buildWorker-Bi6wsfQk.js → buildWorker-DylWm2VX.js} +64 -72
  2. package/dist/assets/index-CkFweu5-.js +403 -0
  3. package/dist/assets/{index-fSroLVgi.css → index-Dyt9tpmu.css} +1 -5
  4. package/dist/assets/{worker-BSzbM0Uo.js → worker-VPLhQVba.js} +230 -230
  5. package/dist/assets/worker-b9bkZkXK.js +116 -0
  6. package/dist/assets/{worker-Cm3I3tnR.js → worker-k7HZ3t-V.js} +3 -20
  7. package/dist/index.html +2 -2
  8. package/dist/llm-streaming-server/chatOperations.js +19 -0
  9. package/dist/llm-streaming-server/llmStreaming.js +18 -13
  10. package/dist/server/aiChatHandler/index.js +4 -2
  11. package/package.json +22 -21
  12. package/src/llm-streaming-server/chatOperations.ts +27 -0
  13. package/src/llm-streaming-server/llmStreaming.ts +24 -14
  14. package/src/llm-streaming-ui/components/Message.tsx +8 -7
  15. package/src/llm-streaming-ui/components/MessageList.tsx +5 -0
  16. package/src/llm-streaming-ui/components/index.tsx +2 -2
  17. package/src/llm-streaming-ui/components/styles.scss +10 -0
  18. package/src/server/aiChatHandler/index.ts +6 -0
  19. package/src/types.ts +1 -0
  20. package/dist/assets/index-CfDs-dAu.js +0 -476
  21. package/dist/assets/worker-KiuLM-X4.js +0 -136
  22. package/dist/server/aiChatHandler/aiEditing.js +0 -58
  23. package/dist/server/aiChatHandler/chatOperations.js +0 -494
  24. package/dist/server/aiChatHandler/errorHandling.js +0 -49
  25. package/dist/server/aiChatHandler/llmStreaming.js +0 -265
  26. package/dist/server/aiChatHandler/validation.js +0 -19
  27. package/src/client/VZSidebar/AIChat/ChatInput.tsx +0 -237
  28. package/src/client/VZSidebar/AIChat/DiffView.scss +0 -173
  29. package/src/client/VZSidebar/AIChat/DiffView.tsx +0 -236
  30. package/src/client/VZSidebar/AIChat/FileEditingIndicator.tsx +0 -89
  31. package/src/client/VZSidebar/AIChat/IndividualFileDiff.tsx +0 -86
  32. package/src/client/VZSidebar/AIChat/JumpToLatestButton.tsx +0 -64
  33. package/src/client/VZSidebar/AIChat/Message.tsx +0 -145
  34. package/src/client/VZSidebar/AIChat/MessageList.tsx +0 -241
  35. package/src/client/VZSidebar/AIChat/ThinkingScratchpad.tsx +0 -42
  36. package/src/client/VZSidebar/AIChat/TypingIndicator.tsx +0 -19
  37. package/src/client/VZSidebar/AIChat/index.tsx +0 -388
  38. package/src/client/VZSidebar/AIChat/styles.scss +0 -831
  39. package/src/client/VZSidebar/AIChat/useSpeechRecognition.ts +0 -116
  40. package/src/server/aiChatHandler/aiEditing.ts +0 -102
  41. package/src/server/aiChatHandler/chatOperations.ts +0 -655
  42. package/src/server/aiChatHandler/errorHandling.ts +0 -66
  43. package/src/server/aiChatHandler/llmStreaming.ts +0 -403
  44. package/src/server/aiChatHandler/validation.ts +0 -24
@@ -1,241 +0,0 @@
1
- import {
2
- useRef,
3
- useEffect,
4
- memo,
5
- useState,
6
- useContext,
7
- } from 'react';
8
- import { Message } from './Message';
9
- import { TypingIndicator } from './TypingIndicator';
10
- import { ThinkingScratchpad } from './ThinkingScratchpad';
11
- import { AIEditingStatusIndicator } from './FileEditingIndicator';
12
- import { VizChatMessage } from '@vizhub/viz-types';
13
- import { ExtendedVizChatMessage } from '../../../types.js';
14
- import { DiffViewRef } from './DiffView.js';
15
- import { VZCodeContext } from '../../VZCodeContext';
16
-
17
- const MessageListComponent = ({
18
- messages,
19
- isLoading,
20
- chatId, // Add chatId prop
21
- aiScratchpad, // Add aiScratchpad prop
22
- currentStatus, // Add current status prop
23
- onNewEvent,
24
- onJumpToLatest,
25
- beforeRender,
26
- afterRender,
27
- }: {
28
- messages: VizChatMessage[];
29
- isLoading: boolean;
30
- chatId?: string; // Add chatId to the type
31
- aiScratchpad?: string; // Add aiScratchpad to the type
32
- currentStatus?: string; // Add current status to the type
33
- onNewEvent: (targetElement?: HTMLElement) => void;
34
- onJumpToLatest: (targetElement?: HTMLElement) => void;
35
- beforeRender: () => number;
36
- afterRender: (prevScrollHeight: number) => void;
37
- }) => {
38
- const messagesEndRef = useRef<HTMLDivElement>(null);
39
- const diffViewRef = useRef<DiffViewRef>(null);
40
-
41
- // Get additional widgets from context
42
- const { additionalWidgets, handleSendMessage } =
43
- useContext(VZCodeContext);
44
-
45
- // Track previous loading state to detect when AI generation completes
46
- const [prevIsLoading, setPrevIsLoading] =
47
- useState(isLoading);
48
-
49
- // Auto-scroll when messages change
50
- useEffect(() => {
51
- // Get scroll height before render for anchoring
52
- const prevScrollHeight = beforeRender();
53
-
54
- // Determine if we should scroll to a specific diff or to the bottom
55
- const lastMessageHasDiff =
56
- messages.length > 0 &&
57
- (messages[messages.length - 1] as any).diffData;
58
-
59
- let targetElement: HTMLElement | null = null;
60
- if (lastMessageHasDiff && diffViewRef.current) {
61
- targetElement =
62
- diffViewRef.current.getFirstHunkElement();
63
- }
64
-
65
- // Trigger auto-scroll if enabled
66
- onNewEvent(targetElement);
67
-
68
- // Adjust scroll position after render for anchoring
69
- afterRender(prevScrollHeight);
70
- }, [messages, onNewEvent, beforeRender, afterRender]);
71
-
72
- // Scroll to bottom when AI generation completes (loading changes from true to false)
73
- useEffect(() => {
74
- // If AI was generating and now it's finished, force scroll to bottom
75
- if (prevIsLoading && !isLoading) {
76
- // Force jump to latest regardless of current auto-scroll state
77
- onJumpToLatest();
78
- }
79
-
80
- setPrevIsLoading(isLoading);
81
- }, [isLoading, prevIsLoading, onJumpToLatest]);
82
-
83
- // Check if AI generation has started (last message is from assistant)
84
- const lastMessage = messages[messages.length - 1];
85
- const aiGenerationStarted =
86
- lastMessage?.role === 'assistant' &&
87
- lastMessage.content !== '';
88
-
89
- // Show typing indicator only when loading and AI generation hasn't started yet
90
- const showTypingIndicator =
91
- isLoading && !aiGenerationStarted;
92
-
93
- // Show thinking scratchpad when AI is thinking (has scratchpad content)
94
- const showThinkingScratchpad = Boolean(
95
- aiScratchpad && aiScratchpad.trim(),
96
- );
97
-
98
- // Determine the single, most relevant status to display with priority:
99
- // 1. Active file editing status from streaming events
100
- // 2. General AI status from currentStatus
101
- const getConsolidatedStatus = () => {
102
- // Check if there's an active streaming message with file editing
103
- if (lastMessage && lastMessage.role === 'assistant') {
104
- const extendedMsg =
105
- lastMessage as ExtendedVizChatMessage;
106
- if (
107
- extendedMsg.streamingEvents &&
108
- extendedMsg.streamingEvents.length > 0
109
- ) {
110
- // Look for active file editing (file_start without corresponding file_complete)
111
- const fileStates = new Map<string, boolean>();
112
-
113
- extendedMsg.streamingEvents.forEach((event) => {
114
- if (event.type === 'file_start') {
115
- fileStates.set(event.fileName, false); // Mark as editing
116
- } else if (event.type === 'file_complete') {
117
- fileStates.set(event.fileName, true); // Mark as complete
118
- }
119
- });
120
-
121
- // Find the first file that's still being edited
122
- for (const [fileName, isComplete] of fileStates) {
123
- if (!isComplete) {
124
- return {
125
- status: `Editing ${fileName}...`,
126
- fileName,
127
- };
128
- }
129
- }
130
- }
131
- }
132
-
133
- // Fall back to general AI status if no active file editing
134
- if (currentStatus) {
135
- return { status: currentStatus };
136
- }
137
-
138
- return null;
139
- };
140
-
141
- const consolidatedStatus = getConsolidatedStatus();
142
-
143
- // Show consolidated status indicator when there's a status to display
144
- const showConsolidatedStatusIndicator = Boolean(
145
- consolidatedStatus,
146
- );
147
-
148
- // Find the most recent assistant message index
149
- const lastAssistantMessageIndex = messages
150
- .map((m, i) => (m.role === 'assistant' ? i : -1))
151
- .filter((i) => i !== -1)
152
- .pop();
153
-
154
- return (
155
- <div
156
- className="ai-chat-messages"
157
- role="log"
158
- aria-live="polite"
159
- aria-relevant="additions"
160
- >
161
- {messages.map((msg, index) => {
162
- const isLastMessage = index === messages.length - 1;
163
- // Cast to extended message to check for streaming events
164
- const extendedMsg = msg as ExtendedVizChatMessage;
165
-
166
- // Check if this is a streaming message
167
- const isStreamingMessage =
168
- !!extendedMsg.isProgressive;
169
-
170
- // Only show additionalWidgets for the most recent assistant message
171
- const showAdditionalWidgets =
172
- msg.role === 'assistant' &&
173
- index === lastAssistantMessageIndex;
174
-
175
- // Use Message for all messages now
176
- return (
177
- <Message
178
- key={msg.id}
179
- id={msg.id}
180
- role={msg.role}
181
- content={
182
- isStreamingMessage ? undefined : msg.content
183
- }
184
- timestamp={msg.timestamp}
185
- events={
186
- isStreamingMessage
187
- ? extendedMsg.streamingEvents || []
188
- : []
189
- }
190
- isActive={index === lastAssistantMessageIndex}
191
- chatId={chatId}
192
- showAdditionalWidgets={showAdditionalWidgets}
193
- isStreaming={isStreamingMessage}
194
- diffData={(msg as any).diffData}
195
- ref={
196
- isLastMessage && (msg as any).diffData
197
- ? diffViewRef
198
- : null
199
- }
200
- >
201
- {isStreamingMessage &&
202
- showThinkingScratchpad && (
203
- <ThinkingScratchpad
204
- content={aiScratchpad || ''}
205
- isVisible={showThinkingScratchpad}
206
- />
207
- )}
208
-
209
- {isStreamingMessage &&
210
- showConsolidatedStatusIndicator && (
211
- <AIEditingStatusIndicator
212
- status={consolidatedStatus?.status || ''}
213
- fileName={consolidatedStatus?.fileName}
214
- additionalWidgets={
215
- consolidatedStatus?.status === 'Done' &&
216
- showAdditionalWidgets &&
217
- additionalWidgets &&
218
- chatId
219
- ? additionalWidgets({
220
- messageId: msg.id,
221
- chatId: chatId,
222
- handleSendMessage,
223
- })
224
- : undefined
225
- }
226
- />
227
- )}
228
-
229
- {isStreamingMessage && showTypingIndicator && (
230
- <TypingIndicator />
231
- )}
232
- </Message>
233
- );
234
- })}
235
-
236
- <div ref={messagesEndRef} />
237
- </div>
238
- );
239
- };
240
-
241
- export const MessageList = memo(MessageListComponent);
@@ -1,42 +0,0 @@
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
- AI is thinking...
24
- </span>
25
- </div>
26
- <div
27
- className="thinking-scratchpad-content"
28
- role="log"
29
- aria-live="polite"
30
- aria-relevant="additions"
31
- >
32
- <Markdown remarkPlugins={[remarkGfm]}>
33
- {content}
34
- </Markdown>
35
- </div>
36
- </div>
37
- );
38
- };
39
-
40
- export const ThinkingScratchpad = memo(
41
- ThinkingScratchpadComponent,
42
- );
@@ -1,19 +0,0 @@
1
- import { memo } from 'react';
2
-
3
- const TypingIndicatorComponent = () => {
4
- return (
5
- <div className="ai-chat-message assistant">
6
- <div className="ai-chat-message-content">
7
- <div className="ai-chat-typing">
8
- <span></span>
9
- <span></span>
10
- <span></span>
11
- </div>
12
- </div>
13
- </div>
14
- );
15
- };
16
-
17
- export const TypingIndicator = memo(
18
- TypingIndicatorComponent,
19
- );
@@ -1,388 +0,0 @@
1
- import {
2
- useContext,
3
- useMemo,
4
- useEffect,
5
- useCallback,
6
- } from 'react';
7
- import { VZCodeContext } from '../../VZCodeContext';
8
- import { MessageList } from './MessageList';
9
- import { ChatInput } from './ChatInput';
10
- import { ExtendedVizChat } from '../../../types.js';
11
- import { useAutoScroll } from '../../hooks/useAutoScroll';
12
- import { JumpToLatestButton } from './JumpToLatestButton';
13
- import './styles.scss';
14
-
15
- const DEBUG = false;
16
-
17
- const showSuggestedRequests = false;
18
-
19
- const showPreviousChats = false;
20
-
21
- // Component for displaying the list of existing chats
22
- const ChatList = ({
23
- chats,
24
- selectedChatId,
25
- onSelectChat,
26
- getChatTitle,
27
- }) => {
28
- return (
29
- <div className="ai-chat-list">
30
- <div className="ai-chat-list-header">
31
- <h4>Previous Chats</h4>
32
- </div>
33
- <div className="ai-chat-suggested-prompts">
34
- {chats.map((chat) => (
35
- <button
36
- key={chat.id}
37
- className={`ai-chat-suggested-prompt ${
38
- selectedChatId === chat.id ? 'selected' : ''
39
- }`}
40
- onClick={() => onSelectChat(chat.id)}
41
- >
42
- {getChatTitle(chat)}
43
- </button>
44
- ))}
45
- </div>
46
- </div>
47
- );
48
- };
49
-
50
- export const AIChat = () => {
51
- const {
52
- aiChatFocused,
53
- content,
54
- aiChatMode,
55
- aiChatMessage,
56
- currentChatId,
57
- selectedChatId,
58
- setSelectedChatId,
59
- aiErrorMessage,
60
- setAIChatMode,
61
- getStoredAIPrompt,
62
- setAIChatMessage,
63
- handleSendMessage,
64
- setAIErrorMessage,
65
- navigateMessageHistoryUp,
66
- navigateMessageHistoryDown,
67
- resetMessageHistoryNavigation,
68
- enableMinimalEditFlow,
69
- } = useContext(VZCodeContext);
70
-
71
- // Use the new simplified auto-scroll hook
72
- const {
73
- containerRef: messagesContainerRef,
74
- showJumpButton,
75
- onNewEvent,
76
- onJumpToLatest,
77
- beforeRender,
78
- afterRender,
79
- } = useAutoScroll({ threshold: 24 });
80
-
81
- // Get the active chat ID and chat data
82
- // If selectedChatId is set, use it; otherwise, if no chat selected, don't default to currentChatId
83
- const activeChatId = selectedChatId;
84
- const currentChat = activeChatId
85
- ? content?.chats?.[activeChatId]
86
- : null;
87
- const rawMessages = useMemo(
88
- () => currentChat?.messages || [],
89
- [currentChat?.messages],
90
- );
91
- const aiStatus = currentChat?.aiStatus;
92
- const aiScratchpad = currentChat?.aiScratchpad;
93
- const currentStatus = (currentChat as ExtendedVizChat)
94
- ?.currentStatus;
95
-
96
- // Debug logging for AI status
97
- DEBUG && console.log('AIChat: currentChat:', currentChat);
98
- DEBUG && console.log('AIChat: aiStatus:', aiStatus);
99
- DEBUG &&
100
- console.log(
101
- 'AIChat: enableMinimalEditFlow:',
102
- enableMinimalEditFlow,
103
- );
104
-
105
- // Transform messages to ensure they have required id field - memoized to avoid recreation
106
- const messages = useMemo(
107
- () =>
108
- rawMessages.map((msg, index) => ({
109
- ...msg,
110
- id: msg.id || `msg-${index}`,
111
- })),
112
- [rawMessages],
113
- );
114
-
115
- // Check if this is the first time opening the chat (no messages) or no chat selected
116
- const isEmptyState =
117
- !selectedChatId || rawMessages.length === 0;
118
-
119
- // Get all existing chats
120
- const allChats = content?.chats || {};
121
- const existingChats = Object.values(allChats).filter(
122
- (chat) => chat.messages.length > 0,
123
- );
124
- const hasExistingChats = existingChats.length > 0;
125
-
126
- // Generate title for a chat (first 50 characters of first user message)
127
- const getChatTitle = (chat) => {
128
- const firstUserMessage = chat.messages.find(
129
- (msg) => msg.role === 'user',
130
- );
131
- if (!firstUserMessage) return 'New Chat';
132
- return (
133
- firstUserMessage.content.slice(0, 50) +
134
- (firstUserMessage.content.length > 50 ? '...' : '')
135
- );
136
- };
137
-
138
- // Wrapper for handleSendMessage to automatically select the chat when sending
139
- const handleSendMessageWithSelection = useCallback(() => {
140
- // If no chat is selected, select the currentChatId when sending a message
141
- if (!selectedChatId) {
142
- setSelectedChatId(currentChatId);
143
- }
144
-
145
- // Call the original handleSendMessage without parameters
146
- return handleSendMessage();
147
- }, [
148
- selectedChatId,
149
- setSelectedChatId,
150
- currentChatId,
151
- handleSendMessage,
152
- ]);
153
-
154
- // Check for stored AI prompt on component mount (post-fork restoration)
155
- // This logic is now handled by the AutoSendAIMessage component in VizHub
156
- // to ensure proper timing and coordination with editor opening
157
- useEffect(() => {
158
- DEBUG &&
159
- console.log('AIChat: Checking for stored AI prompt');
160
- if (getStoredAIPrompt) {
161
- const storedPrompt = getStoredAIPrompt();
162
- DEBUG &&
163
- console.log(
164
- 'AIChat: Stored prompt result:',
165
- storedPrompt,
166
- );
167
- if (storedPrompt) {
168
- // Only restore the prompt and mode, but don't auto-send
169
- // The AutoSendAIMessage component will handle the sending
170
- DEBUG &&
171
- console.log(
172
- 'AIChat: Restoring prompt and mode only',
173
- );
174
- setAIChatMessage(storedPrompt.prompt);
175
- setAIChatMode(
176
- storedPrompt.modelName === 'ask' ? 'ask' : 'edit',
177
- );
178
-
179
- // Don't clear the stored prompt here - let AutoSendAIMessage handle it
180
- // Don't auto-submit here - let AutoSendAIMessage handle the timing
181
- DEBUG &&
182
- console.log(
183
- 'AIChat: Prompt restored, waiting for AutoSendAIMessage to handle sending',
184
- );
185
- } else {
186
- DEBUG &&
187
- console.log('AIChat: No stored prompt found');
188
- }
189
- }
190
- }, [getStoredAIPrompt, setAIChatMessage, setAIChatMode]);
191
-
192
- return (
193
- <div className="ai-chat-container">
194
- <div className="ai-chat-content">
195
- <div
196
- className="ai-chat-messages-container"
197
- ref={messagesContainerRef}
198
- >
199
- {isEmptyState ? (
200
- <div className="ai-chat-empty">
201
- <div className="ai-chat-empty-icon">✨</div>
202
- <h3 className="ai-chat-empty-title">
203
- Edit with AI
204
- </h3>
205
- <div className="ai-chat-empty-text">
206
- How can I help you?
207
- </div>
208
- {hasExistingChats && showPreviousChats && (
209
- <ChatList
210
- chats={existingChats}
211
- selectedChatId={selectedChatId}
212
- onSelectChat={setSelectedChatId}
213
- getChatTitle={getChatTitle}
214
- />
215
- )}
216
- {showSuggestedRequests && (
217
- <div className="ai-chat-empty-examples">
218
- {aiChatMode === 'ask' ? (
219
- <>
220
- <h4>Try asking questions like:</h4>
221
- <div className="ai-chat-suggested-prompts">
222
- <button
223
- className="ai-chat-suggested-prompt"
224
- onClick={() =>
225
- handleSendMessage(
226
- 'Explain how this works',
227
- )
228
- }
229
- >
230
- &quot;Explain how this works&quot;
231
- </button>
232
- <button
233
- className="ai-chat-suggested-prompt"
234
- onClick={() =>
235
- handleSendMessage(
236
- 'How could I change it so that the circles are bigger?',
237
- )
238
- }
239
- >
240
- &quot;How could I change it so
241
- that the circles are bigger?&quot;
242
- </button>
243
- <button
244
- className="ai-chat-suggested-prompt"
245
- onClick={() =>
246
- handleSendMessage(
247
- 'What does this function do?',
248
- )
249
- }
250
- >
251
- &quot;What does this function
252
- do?&quot;
253
- </button>
254
- <button
255
- className="ai-chat-suggested-prompt"
256
- onClick={() =>
257
- handleSendMessage(
258
- 'How can I make this more accessible?',
259
- )
260
- }
261
- >
262
- &quot;How can I make this more
263
- accessible?&quot;
264
- </button>
265
- </div>
266
- </>
267
- ) : (
268
- <>
269
- <h4>Try edit requests like these:</h4>
270
- <div className="ai-chat-suggested-prompts">
271
- <button
272
- className="ai-chat-suggested-prompt"
273
- onClick={() =>
274
- handleSendMessage(
275
- 'Change the circles to squares',
276
- )
277
- }
278
- >
279
- &quot;Change the circles to
280
- squares&quot;
281
- </button>
282
- <button
283
- className="ai-chat-suggested-prompt"
284
- onClick={() =>
285
- handleSendMessage(
286
- 'Add a button that toggles the animation',
287
- )
288
- }
289
- >
290
- &quot;Add a button that toggles
291
- the animation&quot;
292
- </button>
293
- <button
294
- className="ai-chat-suggested-prompt"
295
- onClick={() =>
296
- handleSendMessage(
297
- 'Fix the CSS so the layout is responsive',
298
- )
299
- }
300
- >
301
- &quot;Fix the CSS so the layout is
302
- responsive&quot;
303
- </button>
304
- <button
305
- className="ai-chat-suggested-prompt"
306
- onClick={() =>
307
- handleSendMessage(
308
- 'Refactor this function to use async/await',
309
- )
310
- }
311
- >
312
- &quot;Refactor this function to
313
- use async/await&quot;
314
- </button>
315
- </div>
316
- </>
317
- )}
318
- </div>
319
- )}
320
- {showSuggestedRequests && (
321
- <div className="ai-chat-empty-text">
322
- {aiChatMode === 'ask'
323
- ? 'Type your question below to get started!'
324
- : 'Type your edit request below to get started!'}
325
- </div>
326
- )}
327
- </div>
328
- ) : (
329
- <MessageList
330
- messages={messages}
331
- isLoading={false}
332
- chatId={selectedChatId || currentChatId}
333
- aiScratchpad={aiScratchpad}
334
- currentStatus={currentStatus}
335
- onNewEvent={onNewEvent}
336
- onJumpToLatest={onJumpToLatest}
337
- beforeRender={beforeRender}
338
- afterRender={afterRender}
339
- />
340
- )}
341
- {aiErrorMessage && (
342
- <div className="ai-chat-error">
343
- <div className="ai-chat-error-content">
344
- <span className="ai-chat-error-icon">
345
- ⚠️
346
- </span>
347
- <span className="ai-chat-error-message">
348
- {aiErrorMessage}
349
- </span>
350
- <button
351
- className="ai-chat-error-dismiss"
352
- onClick={() => setAIErrorMessage(null)}
353
- aria-label="Dismiss error"
354
- >
355
- ×
356
- </button>
357
- </div>
358
- </div>
359
- )}
360
- {/* Jump to Latest Button */}
361
- <JumpToLatestButton
362
- visible={showJumpButton}
363
- onClick={onJumpToLatest}
364
- />
365
- </div>
366
- </div>
367
- <div className="ai-chat-input-fixed">
368
- <ChatInput
369
- aiChatMessage={aiChatMessage}
370
- setAIChatMessage={setAIChatMessage}
371
- onSendMessage={handleSendMessageWithSelection}
372
- focused={aiChatFocused}
373
- aiChatMode={aiChatMode}
374
- setAIChatMode={setAIChatMode}
375
- navigateMessageHistoryUp={
376
- navigateMessageHistoryUp
377
- }
378
- navigateMessageHistoryDown={
379
- navigateMessageHistoryDown
380
- }
381
- resetMessageHistoryNavigation={
382
- resetMessageHistoryNavigation
383
- }
384
- />
385
- </div>
386
- </div>
387
- );
388
- };