vzcode 1.55.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/assets/{index-GFaZu2lY.css → index-CE_xP06t.css} +1 -1
- package/dist/assets/{index-DILO4YFE.js → index-DqIc-0Gp.js} +21 -20
- package/dist/index.html +2 -2
- package/package.json +1 -1
- package/src/client/VZSidebar/AIChat/ChatInput.tsx +24 -21
- package/src/client/VZSidebar/AIChat/DiffView.scss +8 -0
- package/src/client/VZSidebar/AIChat/DiffView.tsx +105 -1
- package/src/client/VZSidebar/AIChat/StreamingMessage.tsx +1 -1
- package/src/client/VZSidebar/AIChat/index.tsx +161 -154
- package/src/client/VZSidebar/AIChat/styles.scss +60 -12
- package/src/server/aiChatHandler/llmStreaming.ts +2 -2
- package/src/utils/fileDiff.ts +3 -0
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-
|
|
24
|
-
<link rel="stylesheet" crossorigin href="/assets/index-
|
|
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
|
@@ -50,6 +50,10 @@ const ChatInputComponent = ({
|
|
|
50
50
|
[onSendMessage],
|
|
51
51
|
);
|
|
52
52
|
|
|
53
|
+
const handleSendClick = useCallback(() => {
|
|
54
|
+
onSendMessage();
|
|
55
|
+
}, [onSendMessage]);
|
|
56
|
+
|
|
53
57
|
const handleChange = useCallback(
|
|
54
58
|
(event: React.ChangeEvent<HTMLTextAreaElement>) => {
|
|
55
59
|
setAIChatMessage(event.target.value);
|
|
@@ -108,7 +112,7 @@ const ChatInputComponent = ({
|
|
|
108
112
|
<Form.Group className="ai-chat-input-group">
|
|
109
113
|
<Form.Control
|
|
110
114
|
as="textarea"
|
|
111
|
-
rows={
|
|
115
|
+
rows={5}
|
|
112
116
|
value={aiChatMessage}
|
|
113
117
|
onChange={handleChange}
|
|
114
118
|
onKeyDown={handleKeyDown}
|
|
@@ -122,26 +126,25 @@ const ChatInputComponent = ({
|
|
|
122
126
|
disabled={isLoading}
|
|
123
127
|
aria-label="Chat message input"
|
|
124
128
|
/>
|
|
125
|
-
|
|
126
|
-
<
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
</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>
|
|
145
148
|
</Form.Group>
|
|
146
149
|
</div>
|
|
147
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
|
|
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>
|
|
@@ -68,13 +68,15 @@ export const AIChat = () => {
|
|
|
68
68
|
aiChatMessage,
|
|
69
69
|
);
|
|
70
70
|
const messageContent = messageToSend || aiChatMessage;
|
|
71
|
+
|
|
71
72
|
if (
|
|
72
73
|
!messageContent ||
|
|
73
74
|
typeof messageContent !== 'string' ||
|
|
74
75
|
!messageContent.trim() ||
|
|
75
76
|
isLoading
|
|
76
|
-
)
|
|
77
|
+
) {
|
|
77
78
|
return;
|
|
79
|
+
}
|
|
78
80
|
|
|
79
81
|
const currentPrompt = aiChatMessage.trim();
|
|
80
82
|
setAIChatMessage('');
|
|
@@ -144,9 +146,15 @@ export const AIChat = () => {
|
|
|
144
146
|
// The backend handles all ShareDB operations for successful responses
|
|
145
147
|
} catch (error) {
|
|
146
148
|
console.error('Error getting AI response:', error);
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
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
|
+
// );
|
|
150
158
|
} finally {
|
|
151
159
|
setIsLoading(false);
|
|
152
160
|
}
|
|
@@ -212,159 +220,158 @@ export const AIChat = () => {
|
|
|
212
220
|
|
|
213
221
|
return (
|
|
214
222
|
<div className="ai-chat-container">
|
|
215
|
-
<div
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
{isEmptyState ? (
|
|
224
|
-
<div className="ai-chat-empty">
|
|
225
|
-
<div className="ai-chat-empty-icon">✨</div>
|
|
226
|
-
<h3 className="ai-chat-empty-title">
|
|
227
|
-
Edit with AI
|
|
228
|
-
</h3>
|
|
229
|
-
<div className="ai-chat-empty-text">
|
|
230
|
-
How can I help you?
|
|
231
|
-
</div>
|
|
232
|
-
{showSuggestedRequests && (
|
|
233
|
-
<div className="ai-chat-empty-examples">
|
|
234
|
-
{aiChatMode === 'ask' ? (
|
|
235
|
-
<>
|
|
236
|
-
<h4>Try asking questions like:</h4>
|
|
237
|
-
<div className="ai-chat-suggested-prompts">
|
|
238
|
-
<button
|
|
239
|
-
className="ai-chat-suggested-prompt"
|
|
240
|
-
onClick={() =>
|
|
241
|
-
handleSendMessage(
|
|
242
|
-
'Explain how this works',
|
|
243
|
-
)
|
|
244
|
-
}
|
|
245
|
-
>
|
|
246
|
-
"Explain how this works"
|
|
247
|
-
</button>
|
|
248
|
-
<button
|
|
249
|
-
className="ai-chat-suggested-prompt"
|
|
250
|
-
onClick={() =>
|
|
251
|
-
handleSendMessage(
|
|
252
|
-
'How could I change it so that the circles are bigger?',
|
|
253
|
-
)
|
|
254
|
-
}
|
|
255
|
-
>
|
|
256
|
-
"How could I change it so that the
|
|
257
|
-
circles are bigger?"
|
|
258
|
-
</button>
|
|
259
|
-
<button
|
|
260
|
-
className="ai-chat-suggested-prompt"
|
|
261
|
-
onClick={() =>
|
|
262
|
-
handleSendMessage(
|
|
263
|
-
'What does this function do?',
|
|
264
|
-
)
|
|
265
|
-
}
|
|
266
|
-
>
|
|
267
|
-
"What does this function do?"
|
|
268
|
-
</button>
|
|
269
|
-
<button
|
|
270
|
-
className="ai-chat-suggested-prompt"
|
|
271
|
-
onClick={() =>
|
|
272
|
-
handleSendMessage(
|
|
273
|
-
'How can I make this more accessible?',
|
|
274
|
-
)
|
|
275
|
-
}
|
|
276
|
-
>
|
|
277
|
-
"How can I make this more
|
|
278
|
-
accessible?"
|
|
279
|
-
</button>
|
|
280
|
-
</div>
|
|
281
|
-
</>
|
|
282
|
-
) : (
|
|
283
|
-
<>
|
|
284
|
-
<h4>Try edit requests like these:</h4>
|
|
285
|
-
<div className="ai-chat-suggested-prompts">
|
|
286
|
-
<button
|
|
287
|
-
className="ai-chat-suggested-prompt"
|
|
288
|
-
onClick={() =>
|
|
289
|
-
handleSendMessage(
|
|
290
|
-
'Change the circles to squares',
|
|
291
|
-
)
|
|
292
|
-
}
|
|
293
|
-
>
|
|
294
|
-
"Change the circles to squares"
|
|
295
|
-
</button>
|
|
296
|
-
<button
|
|
297
|
-
className="ai-chat-suggested-prompt"
|
|
298
|
-
onClick={() =>
|
|
299
|
-
handleSendMessage(
|
|
300
|
-
'Add a button that toggles the animation',
|
|
301
|
-
)
|
|
302
|
-
}
|
|
303
|
-
>
|
|
304
|
-
"Add a button that toggles the
|
|
305
|
-
animation"
|
|
306
|
-
</button>
|
|
307
|
-
<button
|
|
308
|
-
className="ai-chat-suggested-prompt"
|
|
309
|
-
onClick={() =>
|
|
310
|
-
handleSendMessage(
|
|
311
|
-
'Fix the CSS so the layout is responsive',
|
|
312
|
-
)
|
|
313
|
-
}
|
|
314
|
-
>
|
|
315
|
-
"Fix the CSS so the layout is
|
|
316
|
-
responsive"
|
|
317
|
-
</button>
|
|
318
|
-
<button
|
|
319
|
-
className="ai-chat-suggested-prompt"
|
|
320
|
-
onClick={() =>
|
|
321
|
-
handleSendMessage(
|
|
322
|
-
'Refactor this function to use async/await',
|
|
323
|
-
)
|
|
324
|
-
}
|
|
325
|
-
>
|
|
326
|
-
"Refactor this function to use
|
|
327
|
-
async/await"
|
|
328
|
-
</button>
|
|
329
|
-
</div>
|
|
330
|
-
</>
|
|
331
|
-
)}
|
|
332
|
-
</div>
|
|
333
|
-
)}
|
|
334
|
-
{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>
|
|
335
231
|
<div className="ai-chat-empty-text">
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
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 && (
|
|
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
|
+
)}
|
|
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">
|
|
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>
|
|
339
369
|
</div>
|
|
340
|
-
)}
|
|
341
|
-
</div>
|
|
342
|
-
) : (
|
|
343
|
-
<MessageList
|
|
344
|
-
messages={messages}
|
|
345
|
-
aiStatus={aiStatus}
|
|
346
|
-
isLoading={isLoading}
|
|
347
|
-
chatId={currentChatId}
|
|
348
|
-
aiScratchpad={aiScratchpad}
|
|
349
|
-
/>
|
|
350
|
-
)}
|
|
351
|
-
{errorMessage && (
|
|
352
|
-
<div className="ai-chat-error">
|
|
353
|
-
<div className="ai-chat-error-content">
|
|
354
|
-
<span className="ai-chat-error-icon">⚠️</span>
|
|
355
|
-
<span className="ai-chat-error-message">
|
|
356
|
-
{errorMessage}
|
|
357
|
-
</span>
|
|
358
|
-
<button
|
|
359
|
-
className="ai-chat-error-dismiss"
|
|
360
|
-
onClick={() => setErrorMessage(null)}
|
|
361
|
-
aria-label="Dismiss error"
|
|
362
|
-
>
|
|
363
|
-
×
|
|
364
|
-
</button>
|
|
365
370
|
</div>
|
|
366
|
-
|
|
367
|
-
|
|
371
|
+
)}
|
|
372
|
+
</div>
|
|
373
|
+
</div>
|
|
374
|
+
<div className="ai-chat-input-fixed">
|
|
368
375
|
<ChatInput
|
|
369
376
|
aiChatMessage={aiChatMessage}
|
|
370
377
|
setAIChatMessage={setAIChatMessage}
|