@autobe/ui 0.30.4-dev.20260324 → 0.30.5
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/LICENSE +661 -661
- package/lib/components/AutoBeChatMain.js +5 -5
- package/lib/components/AutoBeConfigModal.js +9 -9
- package/package.json +2 -2
- package/src/components/AutoBeAssistantMessageMovie.tsx +22 -22
- package/src/components/AutoBeChatMain.tsx +394 -394
- package/src/components/AutoBeChatSidebar.tsx +414 -414
- package/src/components/AutoBeConfigButton.tsx +83 -83
- package/src/components/AutoBeConfigModal.tsx +443 -443
- package/src/components/AutoBeStatusButton.tsx +75 -75
- package/src/components/AutoBeStatusModal.tsx +486 -486
- package/src/components/AutoBeUserMessageMovie.tsx +27 -27
- package/src/components/common/ActionButton.tsx +205 -205
- package/src/components/common/ActionButtonGroup.tsx +80 -80
- package/src/components/common/AutoBeConfigInput.tsx +185 -185
- package/src/components/common/ChatBubble.tsx +119 -119
- package/src/components/common/Collapsible.tsx +95 -95
- package/src/components/common/CompactSessionIndicator.tsx +73 -73
- package/src/components/common/CompactSessionList.tsx +82 -82
- package/src/components/common/openai/OpenAIContent.tsx +53 -53
- package/src/components/common/openai/OpenAIUserAudioContent.tsx +70 -70
- package/src/components/common/openai/OpenAIUserFileContent.tsx +76 -76
- package/src/components/common/openai/OpenAIUserImageContent.tsx +34 -34
- package/src/components/common/openai/OpenAIUserTextContent.tsx +15 -15
- package/src/components/events/AutoBeCompleteEventMovie.tsx +402 -402
- package/src/components/events/AutoBeCorrectEventMovie.tsx +354 -354
- package/src/components/events/AutoBeEventGroupMovie.tsx +18 -18
- package/src/components/events/AutoBeEventMovie.tsx +154 -154
- package/src/components/events/AutoBeProgressEventMovie.tsx +207 -207
- package/src/components/events/AutoBeScenarioEventMovie.tsx +135 -135
- package/src/components/events/AutoBeStartEventMovie.tsx +82 -82
- package/src/components/events/AutoBeValidateEventMovie.tsx +249 -249
- package/src/components/events/README.md +300 -300
- package/src/components/events/common/CollapsibleEventGroup.tsx +211 -211
- package/src/components/events/common/EventCard.tsx +61 -61
- package/src/components/events/common/EventContent.tsx +31 -31
- package/src/components/events/common/EventHeader.tsx +85 -85
- package/src/components/events/common/EventIcon.tsx +82 -82
- package/src/components/events/common/ProgressBar.tsx +64 -64
- package/src/components/events/groups/CorrectEventGroup.tsx +183 -183
- package/src/components/events/groups/ValidateEventGroup.tsx +143 -143
- package/src/components/events/utils/eventGrouper.tsx +116 -116
- package/src/components/upload/AutoBeChatUploadBox.tsx +433 -433
- package/src/components/upload/AutoBeChatUploadSendButton.tsx +66 -66
- package/src/components/upload/AutoBeFileUploadBox.tsx +123 -123
- package/src/components/upload/AutoBeVoiceRecoderButton.tsx +100 -100
- package/src/context/AutoBeAgentContext.tsx +301 -301
- package/src/context/AutoBeAgentSessionList.tsx +58 -58
- package/src/context/SearchParamsContext.tsx +49 -49
- package/src/icons/Receipt.tsx +74 -74
- package/src/structure/AutoBeListener.ts +368 -368
- package/tsconfig.json +9 -9
- package/README.md +0 -261
|
@@ -1,394 +1,394 @@
|
|
|
1
|
-
import { AutoBeUserConversateContent } from "@autobe/interface";
|
|
2
|
-
import { OverlayProvider, overlay } from "overlay-kit";
|
|
3
|
-
import { RefObject, useEffect, useRef } from "react";
|
|
4
|
-
|
|
5
|
-
import { AutoBeChatUploadBox, useAutoBeAgentSessionList } from "..";
|
|
6
|
-
import { useAutoBeAgent } from "../context/AutoBeAgentContext";
|
|
7
|
-
import { useSearchParams } from "../context/SearchParamsContext";
|
|
8
|
-
import { useMediaQuery } from "../hooks";
|
|
9
|
-
import {
|
|
10
|
-
DEFAULT_CONFIG,
|
|
11
|
-
IAutoBeConfig,
|
|
12
|
-
IAutoBePartialConfig,
|
|
13
|
-
} from "../types/config";
|
|
14
|
-
import { getEncryptedSessionStorage } from "../utils/storage";
|
|
15
|
-
import AutoBeConfigButton from "./AutoBeConfigButton";
|
|
16
|
-
import AutoBeConfigModal, { IConfigField } from "./AutoBeConfigModal";
|
|
17
|
-
import AutoBeStatusButton from "./AutoBeStatusButton";
|
|
18
|
-
import AutoBeEventGroupMovie from "./events/AutoBeEventGroupMovie";
|
|
19
|
-
|
|
20
|
-
export interface IAutoBeChatMainProps {
|
|
21
|
-
isUnusedConfig?: boolean;
|
|
22
|
-
isReplay?: boolean;
|
|
23
|
-
isMobile: boolean;
|
|
24
|
-
setError: (error: Error) => void;
|
|
25
|
-
className?: string;
|
|
26
|
-
style?: React.CSSProperties;
|
|
27
|
-
configFields?: IConfigField[];
|
|
28
|
-
|
|
29
|
-
/** Additional required config fields beyond openApiKey */
|
|
30
|
-
requiredFields?: string[];
|
|
31
|
-
|
|
32
|
-
/** Custom content to render when disconnected instead of the default placeholder */
|
|
33
|
-
disconnectedContent?: React.ReactNode;
|
|
34
|
-
|
|
35
|
-
/** Hide the status info button (e.g. when a persistent status panel is shown) */
|
|
36
|
-
hideStatusButton?: boolean;
|
|
37
|
-
|
|
38
|
-
/** Disable the chat input (e.g. when required config like vendor/model is not selected) */
|
|
39
|
-
chatDisabled?: boolean;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
export const AutoBeChatMain = (props: IAutoBeChatMainProps) => {
|
|
43
|
-
const bodyContainerRef = useRef<HTMLDivElement>(null);
|
|
44
|
-
const scrollAnchorRef = useRef<HTMLDivElement>(null);
|
|
45
|
-
const { eventGroups, getAutoBeService, connectionStatus } = useAutoBeAgent();
|
|
46
|
-
const { refreshSessionList } = useAutoBeAgentSessionList();
|
|
47
|
-
const { searchParams } = useSearchParams();
|
|
48
|
-
const activeSessionId = searchParams.get("session-id");
|
|
49
|
-
const listener: RefObject<AutoBeChatUploadBox.IListener> = useRef({
|
|
50
|
-
handleDragEnter: () => {},
|
|
51
|
-
handleDragLeave: () => {},
|
|
52
|
-
handleDrop: () => {},
|
|
53
|
-
handleDragOver: () => {},
|
|
54
|
-
});
|
|
55
|
-
|
|
56
|
-
// Simplified config reader
|
|
57
|
-
const getCurrentConfig = (): IAutoBeConfig => {
|
|
58
|
-
const config: IAutoBePartialConfig = {};
|
|
59
|
-
|
|
60
|
-
props.configFields?.forEach((field) => {
|
|
61
|
-
const value = field.encrypted
|
|
62
|
-
? getEncryptedSessionStorage(field.storageKey)
|
|
63
|
-
: localStorage.getItem(field.storageKey) || "";
|
|
64
|
-
|
|
65
|
-
if (field.type === "checkbox") {
|
|
66
|
-
config[field.key] = String(value) === "true";
|
|
67
|
-
} else if (field.type === "number") {
|
|
68
|
-
config[field.key] = parseInt(String(value)) || 0;
|
|
69
|
-
} else {
|
|
70
|
-
config[field.key] = String(value);
|
|
71
|
-
}
|
|
72
|
-
});
|
|
73
|
-
|
|
74
|
-
return { ...DEFAULT_CONFIG, ...config };
|
|
75
|
-
};
|
|
76
|
-
|
|
77
|
-
// Check if required config is available
|
|
78
|
-
const hasRequiredConfig = (): boolean => {
|
|
79
|
-
const config = getCurrentConfig();
|
|
80
|
-
|
|
81
|
-
// Check additional required fields from props
|
|
82
|
-
if (props.requiredFields) {
|
|
83
|
-
for (const field of props.requiredFields) {
|
|
84
|
-
if (!config[field]) {
|
|
85
|
-
return false;
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
return true;
|
|
91
|
-
};
|
|
92
|
-
|
|
93
|
-
// Unified service connection handler
|
|
94
|
-
const conversate = async (
|
|
95
|
-
messages: AutoBeUserConversateContent[],
|
|
96
|
-
): Promise<void> => {
|
|
97
|
-
// Check if we have required config
|
|
98
|
-
if (props.isUnusedConfig === false && !hasRequiredConfig()) {
|
|
99
|
-
overlay.open(({ isOpen, close }) => (
|
|
100
|
-
<AutoBeConfigModal
|
|
101
|
-
isOpen={isOpen}
|
|
102
|
-
onClose={close}
|
|
103
|
-
title="Server Connection Required"
|
|
104
|
-
fields={props.configFields || []}
|
|
105
|
-
onSave={() => {
|
|
106
|
-
conversate(messages);
|
|
107
|
-
}}
|
|
108
|
-
/>
|
|
109
|
-
));
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
// Connect to service
|
|
113
|
-
try {
|
|
114
|
-
const config = getCurrentConfig();
|
|
115
|
-
const serviceData = await getAutoBeService(config);
|
|
116
|
-
if (messages.length !== 0) {
|
|
117
|
-
if (serviceData.listener.getEnable() !== true) {
|
|
118
|
-
await new Promise<void>((resolve) => {
|
|
119
|
-
const onEnable = async (value: boolean) => {
|
|
120
|
-
if (value === true) {
|
|
121
|
-
serviceData.listener.offEnable(onEnable);
|
|
122
|
-
resolve();
|
|
123
|
-
}
|
|
124
|
-
};
|
|
125
|
-
serviceData.listener.onEnable(onEnable);
|
|
126
|
-
});
|
|
127
|
-
}
|
|
128
|
-
await serviceData.service.conversate(messages);
|
|
129
|
-
}
|
|
130
|
-
if (eventGroups.length === 0) {
|
|
131
|
-
refreshSessionList();
|
|
132
|
-
}
|
|
133
|
-
} catch (error) {
|
|
134
|
-
console.error("Failed to connect:", error);
|
|
135
|
-
props.setError(error as Error);
|
|
136
|
-
}
|
|
137
|
-
};
|
|
138
|
-
|
|
139
|
-
// Auto-scroll when new events arrive
|
|
140
|
-
useEffect(() => {
|
|
141
|
-
if (eventGroups.length > 0) {
|
|
142
|
-
scrollAnchorRef.current?.scrollIntoView({ behavior: "smooth" });
|
|
143
|
-
}
|
|
144
|
-
}, [eventGroups.length]);
|
|
145
|
-
|
|
146
|
-
// Auto-connect: immediately when URL has session-id or on replay
|
|
147
|
-
useEffect(() => {
|
|
148
|
-
if (connectionStatus !== "disconnected") return;
|
|
149
|
-
|
|
150
|
-
if (props.isReplay === true) {
|
|
151
|
-
conversate([]);
|
|
152
|
-
return;
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
if (activeSessionId) {
|
|
156
|
-
conversate([]);
|
|
157
|
-
return;
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
if (eventGroups.length > 0 && hasRequiredConfig()) {
|
|
161
|
-
conversate([]);
|
|
162
|
-
}
|
|
163
|
-
}, [connectionStatus, activeSessionId, eventGroups.length]);
|
|
164
|
-
|
|
165
|
-
return (
|
|
166
|
-
<OverlayProvider>
|
|
167
|
-
<div
|
|
168
|
-
onDragEnter={(e) => listener.current.handleDragEnter(e)}
|
|
169
|
-
onDragLeave={(e) => listener.current.handleDragLeave(e)}
|
|
170
|
-
onDragOver={(e) => listener.current.handleDragOver(e)}
|
|
171
|
-
onDrop={(e) => listener.current.handleDrop(e)}
|
|
172
|
-
style={{
|
|
173
|
-
position: "relative",
|
|
174
|
-
overflowY: "auto",
|
|
175
|
-
margin: 0,
|
|
176
|
-
flexGrow: 1,
|
|
177
|
-
display: "flex",
|
|
178
|
-
flexDirection: "column",
|
|
179
|
-
...props.style,
|
|
180
|
-
}}
|
|
181
|
-
className={props.className}
|
|
182
|
-
ref={bodyContainerRef}
|
|
183
|
-
>
|
|
184
|
-
{/* Control Buttons & Status - Sticky position in top right */}
|
|
185
|
-
<div
|
|
186
|
-
style={{
|
|
187
|
-
position: "sticky",
|
|
188
|
-
top: "1rem",
|
|
189
|
-
zIndex: 1001,
|
|
190
|
-
display: "flex",
|
|
191
|
-
justifyContent: "flex-end",
|
|
192
|
-
alignItems: "center",
|
|
193
|
-
gap: "0.5rem",
|
|
194
|
-
marginBottom: "-3rem",
|
|
195
|
-
paddingRight: "1.5rem",
|
|
196
|
-
}}
|
|
197
|
-
>
|
|
198
|
-
{/* Connection Status Indicator */}
|
|
199
|
-
{connectionStatus === "disconnected" && (
|
|
200
|
-
<div
|
|
201
|
-
style={{
|
|
202
|
-
background: "#f8d7da",
|
|
203
|
-
color: "#721c24",
|
|
204
|
-
border: "1px solid #f5c6cb",
|
|
205
|
-
borderRadius: "50px",
|
|
206
|
-
padding: "0.4rem 0.8rem",
|
|
207
|
-
fontSize: "0.8rem",
|
|
208
|
-
fontWeight: "500",
|
|
209
|
-
display: "flex",
|
|
210
|
-
alignItems: "center",
|
|
211
|
-
gap: "0.4rem",
|
|
212
|
-
boxShadow: "0 2px 4px rgba(0, 0, 0, 0.1)",
|
|
213
|
-
}}
|
|
214
|
-
>
|
|
215
|
-
<div
|
|
216
|
-
style={{
|
|
217
|
-
width: "6px",
|
|
218
|
-
height: "6px",
|
|
219
|
-
backgroundColor: "#dc3545",
|
|
220
|
-
borderRadius: "50%",
|
|
221
|
-
animation: "pulse 2s infinite",
|
|
222
|
-
}}
|
|
223
|
-
></div>
|
|
224
|
-
Disconnected
|
|
225
|
-
</div>
|
|
226
|
-
)}
|
|
227
|
-
|
|
228
|
-
{connectionStatus === "connecting" && (
|
|
229
|
-
<div
|
|
230
|
-
style={{
|
|
231
|
-
background: "#fff3cd",
|
|
232
|
-
color: "#856404",
|
|
233
|
-
border: "1px solid #ffeaa7",
|
|
234
|
-
borderRadius: "50px",
|
|
235
|
-
padding: "0.4rem 0.8rem",
|
|
236
|
-
fontSize: "0.8rem",
|
|
237
|
-
fontWeight: "500",
|
|
238
|
-
display: "flex",
|
|
239
|
-
alignItems: "center",
|
|
240
|
-
gap: "0.4rem",
|
|
241
|
-
boxShadow: "0 2px 4px rgba(0, 0, 0, 0.1)",
|
|
242
|
-
}}
|
|
243
|
-
>
|
|
244
|
-
<div
|
|
245
|
-
style={{
|
|
246
|
-
width: "6px",
|
|
247
|
-
height: "6px",
|
|
248
|
-
backgroundColor: "#f39c12",
|
|
249
|
-
borderRadius: "50%",
|
|
250
|
-
animation: "pulse 1.5s infinite",
|
|
251
|
-
}}
|
|
252
|
-
></div>
|
|
253
|
-
Connecting...
|
|
254
|
-
</div>
|
|
255
|
-
)}
|
|
256
|
-
|
|
257
|
-
{connectionStatus === "connected" && (
|
|
258
|
-
<div
|
|
259
|
-
style={{
|
|
260
|
-
background: "#d4edda",
|
|
261
|
-
color: "#155724",
|
|
262
|
-
border: "1px solid #c3e6cb",
|
|
263
|
-
borderRadius: "50px",
|
|
264
|
-
padding: "0.4rem 0.8rem",
|
|
265
|
-
fontSize: "0.8rem",
|
|
266
|
-
fontWeight: "500",
|
|
267
|
-
display: "flex",
|
|
268
|
-
alignItems: "center",
|
|
269
|
-
gap: "0.4rem",
|
|
270
|
-
boxShadow: "0 2px 4px rgba(0, 0, 0, 0.1)",
|
|
271
|
-
}}
|
|
272
|
-
>
|
|
273
|
-
<div
|
|
274
|
-
style={{
|
|
275
|
-
width: "6px",
|
|
276
|
-
height: "6px",
|
|
277
|
-
backgroundColor: "#28a745",
|
|
278
|
-
borderRadius: "50%",
|
|
279
|
-
animation: "pulse 1.5s infinite",
|
|
280
|
-
}}
|
|
281
|
-
></div>
|
|
282
|
-
Connected
|
|
283
|
-
</div>
|
|
284
|
-
)}
|
|
285
|
-
|
|
286
|
-
<style>
|
|
287
|
-
{`
|
|
288
|
-
@keyframes pulse {
|
|
289
|
-
0%, 100% { opacity: 1; }
|
|
290
|
-
50% { opacity: 0.5; }
|
|
291
|
-
}
|
|
292
|
-
`}
|
|
293
|
-
</style>
|
|
294
|
-
|
|
295
|
-
{props.isUnusedConfig === false &&
|
|
296
|
-
props.configFields?.length != null &&
|
|
297
|
-
props.configFields.length > 0 && (
|
|
298
|
-
<AutoBeConfigButton fields={props.configFields || []} />
|
|
299
|
-
)}
|
|
300
|
-
{!props.hideStatusButton && <AutoBeStatusButton />}
|
|
301
|
-
</div>
|
|
302
|
-
<div
|
|
303
|
-
style={{
|
|
304
|
-
display: "flex",
|
|
305
|
-
flexDirection: "column",
|
|
306
|
-
maxWidth: useMediaQuery.WIDTH_MD,
|
|
307
|
-
width: "100%",
|
|
308
|
-
margin: "0 auto",
|
|
309
|
-
}}
|
|
310
|
-
>
|
|
311
|
-
<div
|
|
312
|
-
style={{
|
|
313
|
-
padding: "2rem",
|
|
314
|
-
gap: 16,
|
|
315
|
-
display: "flex",
|
|
316
|
-
flexDirection: "column",
|
|
317
|
-
}}
|
|
318
|
-
>
|
|
319
|
-
{connectionStatus === "disconnected" &&
|
|
320
|
-
eventGroups.length === 0 &&
|
|
321
|
-
(props.disconnectedContent ?? (
|
|
322
|
-
<div
|
|
323
|
-
style={{
|
|
324
|
-
display: "flex",
|
|
325
|
-
flexDirection: "column",
|
|
326
|
-
justifyContent: "center",
|
|
327
|
-
alignItems: "center",
|
|
328
|
-
padding: "3rem",
|
|
329
|
-
color: "#666",
|
|
330
|
-
textAlign: "center",
|
|
331
|
-
gap: "1rem",
|
|
332
|
-
}}
|
|
333
|
-
>
|
|
334
|
-
<div style={{ fontSize: "3rem" }}>⚙️</div>
|
|
335
|
-
<div style={{ fontSize: "1.25rem", fontWeight: "600" }}>
|
|
336
|
-
Configuration Required
|
|
337
|
-
</div>
|
|
338
|
-
<div
|
|
339
|
-
style={{
|
|
340
|
-
fontSize: "1rem",
|
|
341
|
-
maxWidth: "400px",
|
|
342
|
-
lineHeight: "1.5",
|
|
343
|
-
}}
|
|
344
|
-
>
|
|
345
|
-
Please click the settings button ⚙️ to configure your
|
|
346
|
-
server connection and API credentials, or start typing to
|
|
347
|
-
begin setup.
|
|
348
|
-
</div>
|
|
349
|
-
</div>
|
|
350
|
-
))}
|
|
351
|
-
|
|
352
|
-
{eventGroups.length > 0 && (
|
|
353
|
-
<AutoBeEventGroupMovie eventGroups={eventGroups} />
|
|
354
|
-
)}
|
|
355
|
-
</div>
|
|
356
|
-
</div>
|
|
357
|
-
|
|
358
|
-
{/*
|
|
359
|
-
* Prompt input area
|
|
360
|
-
* this flexGrow: 1 means that the prompt input area will take up the remaining space
|
|
361
|
-
* so that the upload box will be at the bottom of the screen
|
|
362
|
-
*/}
|
|
363
|
-
<div
|
|
364
|
-
style={{ flexGrow: 1, minHeight: "1rem" }}
|
|
365
|
-
ref={scrollAnchorRef}
|
|
366
|
-
></div>
|
|
367
|
-
<div
|
|
368
|
-
style={{
|
|
369
|
-
position: "sticky",
|
|
370
|
-
bottom: 16,
|
|
371
|
-
left: 0,
|
|
372
|
-
right: 0,
|
|
373
|
-
zIndex: 1000,
|
|
374
|
-
}}
|
|
375
|
-
>
|
|
376
|
-
<AutoBeChatUploadBox
|
|
377
|
-
listener={listener}
|
|
378
|
-
uploadConfig={
|
|
379
|
-
getCurrentConfig().supportAudioEnable
|
|
380
|
-
? {
|
|
381
|
-
supportAudio: true,
|
|
382
|
-
}
|
|
383
|
-
: undefined
|
|
384
|
-
}
|
|
385
|
-
conversate={conversate}
|
|
386
|
-
setError={props.setError}
|
|
387
|
-
disabled={props.chatDisabled}
|
|
388
|
-
/>
|
|
389
|
-
</div>
|
|
390
|
-
</div>
|
|
391
|
-
</OverlayProvider>
|
|
392
|
-
);
|
|
393
|
-
};
|
|
394
|
-
export default AutoBeChatMain;
|
|
1
|
+
import { AutoBeUserConversateContent } from "@autobe/interface";
|
|
2
|
+
import { OverlayProvider, overlay } from "overlay-kit";
|
|
3
|
+
import { RefObject, useEffect, useRef } from "react";
|
|
4
|
+
|
|
5
|
+
import { AutoBeChatUploadBox, useAutoBeAgentSessionList } from "..";
|
|
6
|
+
import { useAutoBeAgent } from "../context/AutoBeAgentContext";
|
|
7
|
+
import { useSearchParams } from "../context/SearchParamsContext";
|
|
8
|
+
import { useMediaQuery } from "../hooks";
|
|
9
|
+
import {
|
|
10
|
+
DEFAULT_CONFIG,
|
|
11
|
+
IAutoBeConfig,
|
|
12
|
+
IAutoBePartialConfig,
|
|
13
|
+
} from "../types/config";
|
|
14
|
+
import { getEncryptedSessionStorage } from "../utils/storage";
|
|
15
|
+
import AutoBeConfigButton from "./AutoBeConfigButton";
|
|
16
|
+
import AutoBeConfigModal, { IConfigField } from "./AutoBeConfigModal";
|
|
17
|
+
import AutoBeStatusButton from "./AutoBeStatusButton";
|
|
18
|
+
import AutoBeEventGroupMovie from "./events/AutoBeEventGroupMovie";
|
|
19
|
+
|
|
20
|
+
export interface IAutoBeChatMainProps {
|
|
21
|
+
isUnusedConfig?: boolean;
|
|
22
|
+
isReplay?: boolean;
|
|
23
|
+
isMobile: boolean;
|
|
24
|
+
setError: (error: Error) => void;
|
|
25
|
+
className?: string;
|
|
26
|
+
style?: React.CSSProperties;
|
|
27
|
+
configFields?: IConfigField[];
|
|
28
|
+
|
|
29
|
+
/** Additional required config fields beyond openApiKey */
|
|
30
|
+
requiredFields?: string[];
|
|
31
|
+
|
|
32
|
+
/** Custom content to render when disconnected instead of the default placeholder */
|
|
33
|
+
disconnectedContent?: React.ReactNode;
|
|
34
|
+
|
|
35
|
+
/** Hide the status info button (e.g. when a persistent status panel is shown) */
|
|
36
|
+
hideStatusButton?: boolean;
|
|
37
|
+
|
|
38
|
+
/** Disable the chat input (e.g. when required config like vendor/model is not selected) */
|
|
39
|
+
chatDisabled?: boolean;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export const AutoBeChatMain = (props: IAutoBeChatMainProps) => {
|
|
43
|
+
const bodyContainerRef = useRef<HTMLDivElement>(null);
|
|
44
|
+
const scrollAnchorRef = useRef<HTMLDivElement>(null);
|
|
45
|
+
const { eventGroups, getAutoBeService, connectionStatus } = useAutoBeAgent();
|
|
46
|
+
const { refreshSessionList } = useAutoBeAgentSessionList();
|
|
47
|
+
const { searchParams } = useSearchParams();
|
|
48
|
+
const activeSessionId = searchParams.get("session-id");
|
|
49
|
+
const listener: RefObject<AutoBeChatUploadBox.IListener> = useRef({
|
|
50
|
+
handleDragEnter: () => {},
|
|
51
|
+
handleDragLeave: () => {},
|
|
52
|
+
handleDrop: () => {},
|
|
53
|
+
handleDragOver: () => {},
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
// Simplified config reader
|
|
57
|
+
const getCurrentConfig = (): IAutoBeConfig => {
|
|
58
|
+
const config: IAutoBePartialConfig = {};
|
|
59
|
+
|
|
60
|
+
props.configFields?.forEach((field) => {
|
|
61
|
+
const value = field.encrypted
|
|
62
|
+
? getEncryptedSessionStorage(field.storageKey)
|
|
63
|
+
: localStorage.getItem(field.storageKey) || "";
|
|
64
|
+
|
|
65
|
+
if (field.type === "checkbox") {
|
|
66
|
+
config[field.key] = String(value) === "true";
|
|
67
|
+
} else if (field.type === "number") {
|
|
68
|
+
config[field.key] = parseInt(String(value)) || 0;
|
|
69
|
+
} else {
|
|
70
|
+
config[field.key] = String(value);
|
|
71
|
+
}
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
return { ...DEFAULT_CONFIG, ...config };
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
// Check if required config is available
|
|
78
|
+
const hasRequiredConfig = (): boolean => {
|
|
79
|
+
const config = getCurrentConfig();
|
|
80
|
+
|
|
81
|
+
// Check additional required fields from props
|
|
82
|
+
if (props.requiredFields) {
|
|
83
|
+
for (const field of props.requiredFields) {
|
|
84
|
+
if (!config[field]) {
|
|
85
|
+
return false;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return true;
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
// Unified service connection handler
|
|
94
|
+
const conversate = async (
|
|
95
|
+
messages: AutoBeUserConversateContent[],
|
|
96
|
+
): Promise<void> => {
|
|
97
|
+
// Check if we have required config
|
|
98
|
+
if (props.isUnusedConfig === false && !hasRequiredConfig()) {
|
|
99
|
+
overlay.open(({ isOpen, close }) => (
|
|
100
|
+
<AutoBeConfigModal
|
|
101
|
+
isOpen={isOpen}
|
|
102
|
+
onClose={close}
|
|
103
|
+
title="Server Connection Required"
|
|
104
|
+
fields={props.configFields || []}
|
|
105
|
+
onSave={() => {
|
|
106
|
+
conversate(messages);
|
|
107
|
+
}}
|
|
108
|
+
/>
|
|
109
|
+
));
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Connect to service
|
|
113
|
+
try {
|
|
114
|
+
const config = getCurrentConfig();
|
|
115
|
+
const serviceData = await getAutoBeService(config);
|
|
116
|
+
if (messages.length !== 0) {
|
|
117
|
+
if (serviceData.listener.getEnable() !== true) {
|
|
118
|
+
await new Promise<void>((resolve) => {
|
|
119
|
+
const onEnable = async (value: boolean) => {
|
|
120
|
+
if (value === true) {
|
|
121
|
+
serviceData.listener.offEnable(onEnable);
|
|
122
|
+
resolve();
|
|
123
|
+
}
|
|
124
|
+
};
|
|
125
|
+
serviceData.listener.onEnable(onEnable);
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
await serviceData.service.conversate(messages);
|
|
129
|
+
}
|
|
130
|
+
if (eventGroups.length === 0) {
|
|
131
|
+
refreshSessionList();
|
|
132
|
+
}
|
|
133
|
+
} catch (error) {
|
|
134
|
+
console.error("Failed to connect:", error);
|
|
135
|
+
props.setError(error as Error);
|
|
136
|
+
}
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
// Auto-scroll when new events arrive
|
|
140
|
+
useEffect(() => {
|
|
141
|
+
if (eventGroups.length > 0) {
|
|
142
|
+
scrollAnchorRef.current?.scrollIntoView({ behavior: "smooth" });
|
|
143
|
+
}
|
|
144
|
+
}, [eventGroups.length]);
|
|
145
|
+
|
|
146
|
+
// Auto-connect: immediately when URL has session-id or on replay
|
|
147
|
+
useEffect(() => {
|
|
148
|
+
if (connectionStatus !== "disconnected") return;
|
|
149
|
+
|
|
150
|
+
if (props.isReplay === true) {
|
|
151
|
+
conversate([]);
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
if (activeSessionId) {
|
|
156
|
+
conversate([]);
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
if (eventGroups.length > 0 && hasRequiredConfig()) {
|
|
161
|
+
conversate([]);
|
|
162
|
+
}
|
|
163
|
+
}, [connectionStatus, activeSessionId, eventGroups.length]);
|
|
164
|
+
|
|
165
|
+
return (
|
|
166
|
+
<OverlayProvider>
|
|
167
|
+
<div
|
|
168
|
+
onDragEnter={(e) => listener.current.handleDragEnter(e)}
|
|
169
|
+
onDragLeave={(e) => listener.current.handleDragLeave(e)}
|
|
170
|
+
onDragOver={(e) => listener.current.handleDragOver(e)}
|
|
171
|
+
onDrop={(e) => listener.current.handleDrop(e)}
|
|
172
|
+
style={{
|
|
173
|
+
position: "relative",
|
|
174
|
+
overflowY: "auto",
|
|
175
|
+
margin: 0,
|
|
176
|
+
flexGrow: 1,
|
|
177
|
+
display: "flex",
|
|
178
|
+
flexDirection: "column",
|
|
179
|
+
...props.style,
|
|
180
|
+
}}
|
|
181
|
+
className={props.className}
|
|
182
|
+
ref={bodyContainerRef}
|
|
183
|
+
>
|
|
184
|
+
{/* Control Buttons & Status - Sticky position in top right */}
|
|
185
|
+
<div
|
|
186
|
+
style={{
|
|
187
|
+
position: "sticky",
|
|
188
|
+
top: "1rem",
|
|
189
|
+
zIndex: 1001,
|
|
190
|
+
display: "flex",
|
|
191
|
+
justifyContent: "flex-end",
|
|
192
|
+
alignItems: "center",
|
|
193
|
+
gap: "0.5rem",
|
|
194
|
+
marginBottom: "-3rem",
|
|
195
|
+
paddingRight: "1.5rem",
|
|
196
|
+
}}
|
|
197
|
+
>
|
|
198
|
+
{/* Connection Status Indicator */}
|
|
199
|
+
{connectionStatus === "disconnected" && (
|
|
200
|
+
<div
|
|
201
|
+
style={{
|
|
202
|
+
background: "#f8d7da",
|
|
203
|
+
color: "#721c24",
|
|
204
|
+
border: "1px solid #f5c6cb",
|
|
205
|
+
borderRadius: "50px",
|
|
206
|
+
padding: "0.4rem 0.8rem",
|
|
207
|
+
fontSize: "0.8rem",
|
|
208
|
+
fontWeight: "500",
|
|
209
|
+
display: "flex",
|
|
210
|
+
alignItems: "center",
|
|
211
|
+
gap: "0.4rem",
|
|
212
|
+
boxShadow: "0 2px 4px rgba(0, 0, 0, 0.1)",
|
|
213
|
+
}}
|
|
214
|
+
>
|
|
215
|
+
<div
|
|
216
|
+
style={{
|
|
217
|
+
width: "6px",
|
|
218
|
+
height: "6px",
|
|
219
|
+
backgroundColor: "#dc3545",
|
|
220
|
+
borderRadius: "50%",
|
|
221
|
+
animation: "pulse 2s infinite",
|
|
222
|
+
}}
|
|
223
|
+
></div>
|
|
224
|
+
Disconnected
|
|
225
|
+
</div>
|
|
226
|
+
)}
|
|
227
|
+
|
|
228
|
+
{connectionStatus === "connecting" && (
|
|
229
|
+
<div
|
|
230
|
+
style={{
|
|
231
|
+
background: "#fff3cd",
|
|
232
|
+
color: "#856404",
|
|
233
|
+
border: "1px solid #ffeaa7",
|
|
234
|
+
borderRadius: "50px",
|
|
235
|
+
padding: "0.4rem 0.8rem",
|
|
236
|
+
fontSize: "0.8rem",
|
|
237
|
+
fontWeight: "500",
|
|
238
|
+
display: "flex",
|
|
239
|
+
alignItems: "center",
|
|
240
|
+
gap: "0.4rem",
|
|
241
|
+
boxShadow: "0 2px 4px rgba(0, 0, 0, 0.1)",
|
|
242
|
+
}}
|
|
243
|
+
>
|
|
244
|
+
<div
|
|
245
|
+
style={{
|
|
246
|
+
width: "6px",
|
|
247
|
+
height: "6px",
|
|
248
|
+
backgroundColor: "#f39c12",
|
|
249
|
+
borderRadius: "50%",
|
|
250
|
+
animation: "pulse 1.5s infinite",
|
|
251
|
+
}}
|
|
252
|
+
></div>
|
|
253
|
+
Connecting...
|
|
254
|
+
</div>
|
|
255
|
+
)}
|
|
256
|
+
|
|
257
|
+
{connectionStatus === "connected" && (
|
|
258
|
+
<div
|
|
259
|
+
style={{
|
|
260
|
+
background: "#d4edda",
|
|
261
|
+
color: "#155724",
|
|
262
|
+
border: "1px solid #c3e6cb",
|
|
263
|
+
borderRadius: "50px",
|
|
264
|
+
padding: "0.4rem 0.8rem",
|
|
265
|
+
fontSize: "0.8rem",
|
|
266
|
+
fontWeight: "500",
|
|
267
|
+
display: "flex",
|
|
268
|
+
alignItems: "center",
|
|
269
|
+
gap: "0.4rem",
|
|
270
|
+
boxShadow: "0 2px 4px rgba(0, 0, 0, 0.1)",
|
|
271
|
+
}}
|
|
272
|
+
>
|
|
273
|
+
<div
|
|
274
|
+
style={{
|
|
275
|
+
width: "6px",
|
|
276
|
+
height: "6px",
|
|
277
|
+
backgroundColor: "#28a745",
|
|
278
|
+
borderRadius: "50%",
|
|
279
|
+
animation: "pulse 1.5s infinite",
|
|
280
|
+
}}
|
|
281
|
+
></div>
|
|
282
|
+
Connected
|
|
283
|
+
</div>
|
|
284
|
+
)}
|
|
285
|
+
|
|
286
|
+
<style>
|
|
287
|
+
{`
|
|
288
|
+
@keyframes pulse {
|
|
289
|
+
0%, 100% { opacity: 1; }
|
|
290
|
+
50% { opacity: 0.5; }
|
|
291
|
+
}
|
|
292
|
+
`}
|
|
293
|
+
</style>
|
|
294
|
+
|
|
295
|
+
{props.isUnusedConfig === false &&
|
|
296
|
+
props.configFields?.length != null &&
|
|
297
|
+
props.configFields.length > 0 && (
|
|
298
|
+
<AutoBeConfigButton fields={props.configFields || []} />
|
|
299
|
+
)}
|
|
300
|
+
{!props.hideStatusButton && <AutoBeStatusButton />}
|
|
301
|
+
</div>
|
|
302
|
+
<div
|
|
303
|
+
style={{
|
|
304
|
+
display: "flex",
|
|
305
|
+
flexDirection: "column",
|
|
306
|
+
maxWidth: useMediaQuery.WIDTH_MD,
|
|
307
|
+
width: "100%",
|
|
308
|
+
margin: "0 auto",
|
|
309
|
+
}}
|
|
310
|
+
>
|
|
311
|
+
<div
|
|
312
|
+
style={{
|
|
313
|
+
padding: "2rem",
|
|
314
|
+
gap: 16,
|
|
315
|
+
display: "flex",
|
|
316
|
+
flexDirection: "column",
|
|
317
|
+
}}
|
|
318
|
+
>
|
|
319
|
+
{connectionStatus === "disconnected" &&
|
|
320
|
+
eventGroups.length === 0 &&
|
|
321
|
+
(props.disconnectedContent ?? (
|
|
322
|
+
<div
|
|
323
|
+
style={{
|
|
324
|
+
display: "flex",
|
|
325
|
+
flexDirection: "column",
|
|
326
|
+
justifyContent: "center",
|
|
327
|
+
alignItems: "center",
|
|
328
|
+
padding: "3rem",
|
|
329
|
+
color: "#666",
|
|
330
|
+
textAlign: "center",
|
|
331
|
+
gap: "1rem",
|
|
332
|
+
}}
|
|
333
|
+
>
|
|
334
|
+
<div style={{ fontSize: "3rem" }}>⚙️</div>
|
|
335
|
+
<div style={{ fontSize: "1.25rem", fontWeight: "600" }}>
|
|
336
|
+
Configuration Required
|
|
337
|
+
</div>
|
|
338
|
+
<div
|
|
339
|
+
style={{
|
|
340
|
+
fontSize: "1rem",
|
|
341
|
+
maxWidth: "400px",
|
|
342
|
+
lineHeight: "1.5",
|
|
343
|
+
}}
|
|
344
|
+
>
|
|
345
|
+
Please click the settings button ⚙️ to configure your
|
|
346
|
+
server connection and API credentials, or start typing to
|
|
347
|
+
begin setup.
|
|
348
|
+
</div>
|
|
349
|
+
</div>
|
|
350
|
+
))}
|
|
351
|
+
|
|
352
|
+
{eventGroups.length > 0 && (
|
|
353
|
+
<AutoBeEventGroupMovie eventGroups={eventGroups} />
|
|
354
|
+
)}
|
|
355
|
+
</div>
|
|
356
|
+
</div>
|
|
357
|
+
|
|
358
|
+
{/*
|
|
359
|
+
* Prompt input area
|
|
360
|
+
* this flexGrow: 1 means that the prompt input area will take up the remaining space
|
|
361
|
+
* so that the upload box will be at the bottom of the screen
|
|
362
|
+
*/}
|
|
363
|
+
<div
|
|
364
|
+
style={{ flexGrow: 1, minHeight: "1rem" }}
|
|
365
|
+
ref={scrollAnchorRef}
|
|
366
|
+
></div>
|
|
367
|
+
<div
|
|
368
|
+
style={{
|
|
369
|
+
position: "sticky",
|
|
370
|
+
bottom: 16,
|
|
371
|
+
left: 0,
|
|
372
|
+
right: 0,
|
|
373
|
+
zIndex: 1000,
|
|
374
|
+
}}
|
|
375
|
+
>
|
|
376
|
+
<AutoBeChatUploadBox
|
|
377
|
+
listener={listener}
|
|
378
|
+
uploadConfig={
|
|
379
|
+
getCurrentConfig().supportAudioEnable
|
|
380
|
+
? {
|
|
381
|
+
supportAudio: true,
|
|
382
|
+
}
|
|
383
|
+
: undefined
|
|
384
|
+
}
|
|
385
|
+
conversate={conversate}
|
|
386
|
+
setError={props.setError}
|
|
387
|
+
disabled={props.chatDisabled}
|
|
388
|
+
/>
|
|
389
|
+
</div>
|
|
390
|
+
</div>
|
|
391
|
+
</OverlayProvider>
|
|
392
|
+
);
|
|
393
|
+
};
|
|
394
|
+
export default AutoBeChatMain;
|