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.
- package/dist/assets/{index-C1lTrzOZ.js → index-BMYXp1Uw.js} +114 -114
- package/dist/assets/{index-DeeOcIzw.css → index-BXyJ2hMf.css} +1 -1
- package/dist/index.html +2 -2
- package/package.json +7 -3
- package/src/client/CodeEditor/index.tsx +7 -4
- package/src/client/RunCodeWidget/index.tsx +18 -15
- package/src/client/SplitPaneResizeContext.tsx +26 -108
- package/src/client/VZCodeContext.tsx +8 -1
- package/src/client/VZRight.tsx +32 -4
- package/src/client/VZSidebar/AIChat/ChatInput.tsx +72 -2
- package/src/client/VZSidebar/AIChat/DiffView.scss +29 -0
- package/src/client/VZSidebar/AIChat/DiffView.tsx +62 -1
- package/src/client/VZSidebar/AIChat/Message.tsx +14 -1
- package/src/client/VZSidebar/AIChat/MessageList.tsx +134 -13
- package/src/client/VZSidebar/AIChat/ThinkingScratchpad.tsx +37 -0
- package/src/client/VZSidebar/AIChat/index.tsx +187 -16
- package/src/client/VZSidebar/AIChat/styles.scss +278 -1
- package/src/client/VZSidebar/aiCopyPaste.ts +6 -1
- package/src/client/VZSidebar/index.tsx +10 -2
- package/src/client/bootstrap.ts +11 -1
- package/src/client/useActions.ts +12 -0
- package/src/client/usePrettier/index.ts +1 -3
- package/src/client/vzReducer/aiChatReducer.ts +13 -0
- package/src/client/vzReducer/createInitialState.ts +1 -0
- package/src/client/vzReducer/index.ts +9 -0
- package/src/client/vzReducer/searchReducer.test.ts +44 -2
- package/src/runCode.ts +5 -17
- package/src/server/aiChatHandler/aiEditing.ts +37 -0
- package/src/server/aiChatHandler/chatOperations.ts +38 -1
- package/src/server/aiChatHandler/index.ts +20 -9
- package/src/server/aiChatHandler/llmStreaming.ts +101 -19
- package/src/server/generateAIResponse.ts +4 -0
- 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
|
|
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
|
-
|
|
30
|
-
|
|
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
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
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
|
|
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
|
-
//
|
|
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
|
-
|
|
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
|
-
<
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
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
|
};
|