@autobe/ui 0.30.0-dev.20260315 → 0.30.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/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 +376 -376
- 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/index.ts +8 -8
- 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/common/openai/index.ts +5 -5
- 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 +158 -158
- package/src/components/events/AutoBeProgressEventMovie.tsx +217 -217
- 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/common/index.ts +13 -13
- package/src/components/events/groups/CorrectEventGroup.tsx +183 -183
- package/src/components/events/groups/ValidateEventGroup.tsx +143 -143
- package/src/components/events/groups/index.ts +8 -8
- package/src/components/events/index.ts +16 -16
- package/src/components/events/utils/eventGrouper.tsx +116 -116
- package/src/components/events/utils/index.ts +1 -1
- package/src/components/index.ts +13 -13
- package/src/components/upload/AutoBeChatUploadBox.tsx +425 -425
- package/src/components/upload/AutoBeChatUploadSendButton.tsx +66 -66
- package/src/components/upload/AutoBeFileUploadBox.tsx +123 -123
- package/src/components/upload/AutoBeUploadConfig.ts +5 -5
- package/src/components/upload/AutoBeVoiceRecoderButton.tsx +100 -100
- package/src/components/upload/index.ts +5 -5
- package/src/constant/color.ts +28 -28
- package/src/context/AutoBeAgentContext.tsx +245 -245
- package/src/context/AutoBeAgentSessionList.tsx +58 -58
- package/src/context/SearchParamsContext.tsx +49 -49
- package/src/hooks/index.ts +3 -3
- package/src/hooks/useEscapeKey.ts +24 -24
- package/src/hooks/useIsomorphicLayoutEffect.ts +8 -8
- package/src/hooks/useMediaQuery.ts +73 -73
- package/src/hooks/useSessionStorage.ts +10 -10
- package/src/icons/Receipt.tsx +74 -74
- package/src/index.ts +9 -9
- package/src/strategy/AutoBeAgentSessionStorageStrategy.ts +127 -127
- package/src/structure/AutoBeListener.ts +373 -373
- package/src/structure/AutoBeListenerState.ts +53 -53
- package/src/structure/IAutoBeAgentSessionStorageStrategy.ts +87 -87
- package/src/structure/IAutoBeEventGroup.ts +6 -6
- package/src/structure/index.ts +4 -4
- package/src/types/config.ts +44 -44
- package/src/types/index.ts +1 -1
- package/src/utils/AutoBeFileUploader.ts +279 -279
- package/src/utils/AutoBeVoiceRecorder.ts +95 -95
- package/src/utils/__tests__/crypto.test.ts +286 -286
- package/src/utils/__tests__/storage.test.ts +229 -229
- package/src/utils/crypto.ts +95 -95
- package/src/utils/index.ts +6 -6
- package/src/utils/number.ts +17 -17
- package/src/utils/storage.ts +96 -96
- package/src/utils/time.ts +14 -14
- package/tsconfig.json +9 -9
- package/vitest.config.ts +15 -15
- package/README.md +0 -261
|
@@ -1,443 +1,443 @@
|
|
|
1
|
-
import { CSSProperties, useState } from "react";
|
|
2
|
-
|
|
3
|
-
import {
|
|
4
|
-
getEncryptedSessionStorage,
|
|
5
|
-
setEncryptedSessionStorage,
|
|
6
|
-
} from "../utils/storage";
|
|
7
|
-
import AutoBeConfigInput from "./common/AutoBeConfigInput";
|
|
8
|
-
|
|
9
|
-
/** Generic config field definition */
|
|
10
|
-
export interface IConfigField {
|
|
11
|
-
key: string;
|
|
12
|
-
label: string;
|
|
13
|
-
type: "text" | "number" | "checkbox" | "list";
|
|
14
|
-
placeholder?: string;
|
|
15
|
-
default?: string | number | boolean;
|
|
16
|
-
suggestions?: string[];
|
|
17
|
-
required?: boolean;
|
|
18
|
-
min?: number;
|
|
19
|
-
max?: number;
|
|
20
|
-
storageKey: string;
|
|
21
|
-
encrypted?: boolean; // Use sessionStorage with encryption
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
export interface IAutoBeConfigModalProps {
|
|
25
|
-
isOpen: boolean;
|
|
26
|
-
onClose: () => void;
|
|
27
|
-
title?: string;
|
|
28
|
-
fields: IConfigField[];
|
|
29
|
-
onSave?: (config: Record<string, unknown>) => void;
|
|
30
|
-
onReset?: () => void;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
/**
|
|
34
|
-
* Generic configuration modal component Receives config field definitions from
|
|
35
|
-
* props for maximum flexibility
|
|
36
|
-
*/
|
|
37
|
-
export const AutoBeConfigModal = (props: IAutoBeConfigModalProps) => {
|
|
38
|
-
// Get stored values for all fields
|
|
39
|
-
const getStoredValue = (field: IConfigField) => {
|
|
40
|
-
if (typeof window === "undefined") {
|
|
41
|
-
if (field.type === "checkbox") return false;
|
|
42
|
-
if (field.type === "number") return 0;
|
|
43
|
-
return "";
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
const stored = field.encrypted
|
|
47
|
-
? getEncryptedSessionStorage(field.storageKey)
|
|
48
|
-
: localStorage.getItem(field.storageKey);
|
|
49
|
-
|
|
50
|
-
if (stored !== null && stored !== "") {
|
|
51
|
-
if (field.type === "checkbox") {
|
|
52
|
-
return stored === "true";
|
|
53
|
-
}
|
|
54
|
-
if (field.type === "number") {
|
|
55
|
-
return parseInt(stored, 10) || 0;
|
|
56
|
-
}
|
|
57
|
-
return stored;
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
if (field.default !== undefined) return field.default;
|
|
61
|
-
// Return default values based on type
|
|
62
|
-
if (field.type === "checkbox") return false;
|
|
63
|
-
if (field.type === "number") return 0;
|
|
64
|
-
};
|
|
65
|
-
|
|
66
|
-
// Initialize config state from stored values
|
|
67
|
-
const [config, setConfig] = useState<Record<string, unknown>>(() => {
|
|
68
|
-
const initialConfig: Record<string, unknown> = {};
|
|
69
|
-
props.fields.forEach((field) => {
|
|
70
|
-
initialConfig[field.key] = getStoredValue(field) ?? undefined;
|
|
71
|
-
});
|
|
72
|
-
return initialConfig;
|
|
73
|
-
});
|
|
74
|
-
|
|
75
|
-
const updateConfig = (key: string, value: unknown) => {
|
|
76
|
-
setConfig((prev) => ({ ...prev, [key]: value }));
|
|
77
|
-
};
|
|
78
|
-
|
|
79
|
-
const handleSave = () => {
|
|
80
|
-
// Save all config values to their respective storage
|
|
81
|
-
props.fields.forEach((field) => {
|
|
82
|
-
const value = config[field.key];
|
|
83
|
-
const stringValue = String(value);
|
|
84
|
-
|
|
85
|
-
if (field.encrypted) {
|
|
86
|
-
setEncryptedSessionStorage(field.storageKey, stringValue);
|
|
87
|
-
} else {
|
|
88
|
-
localStorage.setItem(field.storageKey, stringValue);
|
|
89
|
-
}
|
|
90
|
-
});
|
|
91
|
-
|
|
92
|
-
console.log(config);
|
|
93
|
-
// Call optional external save handler
|
|
94
|
-
props.onSave?.(config);
|
|
95
|
-
props.onClose();
|
|
96
|
-
};
|
|
97
|
-
|
|
98
|
-
if (!props.isOpen) return null;
|
|
99
|
-
|
|
100
|
-
return (
|
|
101
|
-
<div
|
|
102
|
-
style={{
|
|
103
|
-
position: "fixed",
|
|
104
|
-
top: 0,
|
|
105
|
-
left: 0,
|
|
106
|
-
right: 0,
|
|
107
|
-
bottom: 0,
|
|
108
|
-
backgroundColor: "rgba(0, 0, 0, 0.5)",
|
|
109
|
-
display: "flex",
|
|
110
|
-
alignItems: "center",
|
|
111
|
-
justifyContent: "center",
|
|
112
|
-
zIndex: 1002,
|
|
113
|
-
}}
|
|
114
|
-
onClick={props.onClose}
|
|
115
|
-
>
|
|
116
|
-
<div
|
|
117
|
-
style={{
|
|
118
|
-
backgroundColor: "white",
|
|
119
|
-
borderRadius: "1rem",
|
|
120
|
-
padding: "1.25rem",
|
|
121
|
-
width: "90%",
|
|
122
|
-
maxWidth: "500px",
|
|
123
|
-
maxHeight: "90vh",
|
|
124
|
-
overflow: "visible",
|
|
125
|
-
boxShadow:
|
|
126
|
-
"0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04)",
|
|
127
|
-
}}
|
|
128
|
-
onClick={(e) => e.stopPropagation()}
|
|
129
|
-
>
|
|
130
|
-
{/* Header with title and X button */}
|
|
131
|
-
<div style={{ position: "relative", marginBottom: "1.25rem" }}>
|
|
132
|
-
<h2
|
|
133
|
-
style={{
|
|
134
|
-
margin: "0",
|
|
135
|
-
fontSize: "1.125rem",
|
|
136
|
-
fontWeight: "600",
|
|
137
|
-
color: "#1a1a1a",
|
|
138
|
-
textAlign: "center",
|
|
139
|
-
}}
|
|
140
|
-
>
|
|
141
|
-
{props.title || "Configuration"}
|
|
142
|
-
</h2>
|
|
143
|
-
|
|
144
|
-
{/* X Close Button */}
|
|
145
|
-
<button
|
|
146
|
-
onClick={props.onClose}
|
|
147
|
-
style={{
|
|
148
|
-
position: "absolute",
|
|
149
|
-
top: "50%",
|
|
150
|
-
right: "0",
|
|
151
|
-
transform: "translateY(-50%)",
|
|
152
|
-
background: "none",
|
|
153
|
-
border: "none",
|
|
154
|
-
cursor: "pointer",
|
|
155
|
-
padding: "0.25rem",
|
|
156
|
-
borderRadius: "50%",
|
|
157
|
-
width: "1.75rem",
|
|
158
|
-
height: "1.75rem",
|
|
159
|
-
display: "flex",
|
|
160
|
-
alignItems: "center",
|
|
161
|
-
justifyContent: "center",
|
|
162
|
-
fontSize: "1rem",
|
|
163
|
-
color: "#6b7280",
|
|
164
|
-
transition: "all 0.2s",
|
|
165
|
-
}}
|
|
166
|
-
className="modal-close-button"
|
|
167
|
-
>
|
|
168
|
-
✖️
|
|
169
|
-
</button>
|
|
170
|
-
|
|
171
|
-
<style>
|
|
172
|
-
{`
|
|
173
|
-
.modal-close-button:hover {
|
|
174
|
-
background-color: #f3f4f6 !important;
|
|
175
|
-
color: #374151 !important;
|
|
176
|
-
}
|
|
177
|
-
`}
|
|
178
|
-
</style>
|
|
179
|
-
</div>
|
|
180
|
-
|
|
181
|
-
<div style={{ display: "flex", flexDirection: "column", gap: "1rem" }}>
|
|
182
|
-
{props.fields.map((field) => (
|
|
183
|
-
<div key={field.key}>
|
|
184
|
-
{field.type === "checkbox" ? (
|
|
185
|
-
<div style={{ marginBottom: "0.25rem" }}>
|
|
186
|
-
<label
|
|
187
|
-
style={{
|
|
188
|
-
display: "flex",
|
|
189
|
-
alignItems: "center",
|
|
190
|
-
gap: "0.5rem",
|
|
191
|
-
fontSize: "1rem",
|
|
192
|
-
fontWeight: "500",
|
|
193
|
-
color: "#374151",
|
|
194
|
-
marginBottom: "0.5rem",
|
|
195
|
-
}}
|
|
196
|
-
>
|
|
197
|
-
{field.label}
|
|
198
|
-
</label>
|
|
199
|
-
<div
|
|
200
|
-
style={{
|
|
201
|
-
display: "flex",
|
|
202
|
-
alignItems: "center",
|
|
203
|
-
gap: "0.5rem",
|
|
204
|
-
padding: "0.75rem 1rem",
|
|
205
|
-
border: "2px solid #e5e7eb",
|
|
206
|
-
borderRadius: "0.5rem",
|
|
207
|
-
backgroundColor: "#f9fafb",
|
|
208
|
-
transition: "all 0.2s",
|
|
209
|
-
}}
|
|
210
|
-
>
|
|
211
|
-
<input
|
|
212
|
-
type="checkbox"
|
|
213
|
-
checked={Boolean(config[field.key])}
|
|
214
|
-
onChange={(e) =>
|
|
215
|
-
updateConfig(field.key, e.target.checked)
|
|
216
|
-
}
|
|
217
|
-
style={{
|
|
218
|
-
width: "1rem",
|
|
219
|
-
height: "1rem",
|
|
220
|
-
cursor: "pointer",
|
|
221
|
-
}}
|
|
222
|
-
/>
|
|
223
|
-
<span style={{ fontSize: "0.875rem", color: "#6b7280" }}>
|
|
224
|
-
{field.placeholder ||
|
|
225
|
-
`Enable ${field.label.toLowerCase()}`}
|
|
226
|
-
</span>
|
|
227
|
-
</div>
|
|
228
|
-
</div>
|
|
229
|
-
) : (
|
|
230
|
-
<AutoBeConfigInput
|
|
231
|
-
label={field.label}
|
|
232
|
-
type={field.type}
|
|
233
|
-
value={String(config[field.key] || field.default || "")}
|
|
234
|
-
onChange={(value) => {
|
|
235
|
-
const finalValue =
|
|
236
|
-
field.type === "number" ? parseInt(value) || 0 : value;
|
|
237
|
-
updateConfig(field.key, finalValue);
|
|
238
|
-
}}
|
|
239
|
-
placeholder={field.placeholder}
|
|
240
|
-
suggestions={field.suggestions?.map((value) => ({ value }))}
|
|
241
|
-
min={field.min}
|
|
242
|
-
max={field.max}
|
|
243
|
-
required={field.required}
|
|
244
|
-
/>
|
|
245
|
-
)}
|
|
246
|
-
</div>
|
|
247
|
-
))}
|
|
248
|
-
</div>
|
|
249
|
-
|
|
250
|
-
<div
|
|
251
|
-
style={{
|
|
252
|
-
display: "flex",
|
|
253
|
-
gap: "1rem",
|
|
254
|
-
marginTop: "1.25rem",
|
|
255
|
-
justifyContent: "flex-end",
|
|
256
|
-
}}
|
|
257
|
-
>
|
|
258
|
-
<ConfigButton variant="secondary" onClick={props.onClose}>
|
|
259
|
-
Cancel
|
|
260
|
-
</ConfigButton>
|
|
261
|
-
|
|
262
|
-
<ConfigButton variant="primary" onClick={handleSave}>
|
|
263
|
-
Save
|
|
264
|
-
</ConfigButton>
|
|
265
|
-
</div>
|
|
266
|
-
</div>
|
|
267
|
-
</div>
|
|
268
|
-
);
|
|
269
|
-
};
|
|
270
|
-
|
|
271
|
-
/** Reusable button component for config modal */
|
|
272
|
-
interface IConfigButtonProps {
|
|
273
|
-
variant: "primary" | "secondary";
|
|
274
|
-
onClick: () => void;
|
|
275
|
-
children: React.ReactNode;
|
|
276
|
-
disabled?: boolean;
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
const ConfigButton = ({
|
|
280
|
-
variant,
|
|
281
|
-
onClick,
|
|
282
|
-
children,
|
|
283
|
-
disabled,
|
|
284
|
-
}: IConfigButtonProps) => {
|
|
285
|
-
const baseStyles: CSSProperties = {
|
|
286
|
-
padding: "0.5rem 1.25rem",
|
|
287
|
-
fontSize: "0.875rem",
|
|
288
|
-
fontWeight: "500",
|
|
289
|
-
borderRadius: "0.5rem",
|
|
290
|
-
cursor: disabled ? "not-allowed" : "pointer",
|
|
291
|
-
transition: "all 0.2s",
|
|
292
|
-
border: "none",
|
|
293
|
-
outline: "none",
|
|
294
|
-
opacity: disabled ? 0.5 : 1,
|
|
295
|
-
};
|
|
296
|
-
|
|
297
|
-
const variantStyles: Record<"primary" | "secondary", CSSProperties> = {
|
|
298
|
-
primary: {
|
|
299
|
-
color: "white",
|
|
300
|
-
backgroundColor: "#3b82f6",
|
|
301
|
-
border: "1px solid #3b82f6",
|
|
302
|
-
},
|
|
303
|
-
secondary: {
|
|
304
|
-
color: "#374151",
|
|
305
|
-
backgroundColor: "#f9fafb",
|
|
306
|
-
border: "1px solid #d1d5db",
|
|
307
|
-
},
|
|
308
|
-
};
|
|
309
|
-
|
|
310
|
-
return (
|
|
311
|
-
<>
|
|
312
|
-
<style>
|
|
313
|
-
{`
|
|
314
|
-
.config-button-${variant}:hover:not(:disabled) {
|
|
315
|
-
${
|
|
316
|
-
variant === "primary"
|
|
317
|
-
? "background-color: #2563eb !important; border-color: #2563eb !important;"
|
|
318
|
-
: "background-color: #f3f4f6 !important; border-color: #9ca3af !important;"
|
|
319
|
-
}
|
|
320
|
-
}
|
|
321
|
-
`}
|
|
322
|
-
</style>
|
|
323
|
-
<button
|
|
324
|
-
className={`config-button-${variant}`}
|
|
325
|
-
style={{
|
|
326
|
-
...baseStyles,
|
|
327
|
-
...variantStyles[variant],
|
|
328
|
-
}}
|
|
329
|
-
onClick={onClick}
|
|
330
|
-
disabled={disabled}
|
|
331
|
-
>
|
|
332
|
-
{children}
|
|
333
|
-
</button>
|
|
334
|
-
</>
|
|
335
|
-
);
|
|
336
|
-
};
|
|
337
|
-
|
|
338
|
-
/** All available AutoBE configuration fields */
|
|
339
|
-
const ALL_CONFIG_FIELDS: Record<string, IConfigField> = {
|
|
340
|
-
locale: {
|
|
341
|
-
key: "locale",
|
|
342
|
-
label: "Language",
|
|
343
|
-
type: "text",
|
|
344
|
-
storageKey: "autobe_locale",
|
|
345
|
-
placeholder: "en",
|
|
346
|
-
default: "en",
|
|
347
|
-
suggestions: ["en", "ko", "ja", "zh", "es", "fr", "de"],
|
|
348
|
-
},
|
|
349
|
-
schemaModel: {
|
|
350
|
-
key: "schemaModel",
|
|
351
|
-
label: "Schema Model",
|
|
352
|
-
type: "text",
|
|
353
|
-
storageKey: "autobe_schema_model",
|
|
354
|
-
placeholder: "chatgpt",
|
|
355
|
-
default: "chatgpt",
|
|
356
|
-
suggestions: ["chatgpt", "claude"],
|
|
357
|
-
},
|
|
358
|
-
aiModel: {
|
|
359
|
-
key: "aiModel",
|
|
360
|
-
label: "AI Model",
|
|
361
|
-
type: "text",
|
|
362
|
-
storageKey: "autobe_ai_model",
|
|
363
|
-
placeholder: "gpt-4.1",
|
|
364
|
-
default: "gpt-4.1",
|
|
365
|
-
suggestions: [
|
|
366
|
-
"gpt-4.1",
|
|
367
|
-
"gpt-4.1-mini",
|
|
368
|
-
"qwen/qwen3-235b-a22b-2507",
|
|
369
|
-
"qwen/qwen3-next-80b-a3b-instruct",
|
|
370
|
-
],
|
|
371
|
-
},
|
|
372
|
-
openApiKey: {
|
|
373
|
-
key: "openApiKey",
|
|
374
|
-
label: "OpenAI API Key",
|
|
375
|
-
type: "text",
|
|
376
|
-
storageKey: "autobe_openapi_key_encrypted",
|
|
377
|
-
placeholder: "sk-...",
|
|
378
|
-
encrypted: true,
|
|
379
|
-
required: true,
|
|
380
|
-
},
|
|
381
|
-
baseUrl: {
|
|
382
|
-
key: "baseUrl",
|
|
383
|
-
label: "Base URL",
|
|
384
|
-
type: "text",
|
|
385
|
-
storageKey: "autobe_base_url",
|
|
386
|
-
placeholder: "https://api.openai.com/v1",
|
|
387
|
-
suggestions: ["https://api.openai.com/v1", "https://openrouter.ai/api/v1"],
|
|
388
|
-
},
|
|
389
|
-
semaphore: {
|
|
390
|
-
key: "semaphore",
|
|
391
|
-
label: "Concurrency Limit",
|
|
392
|
-
type: "number",
|
|
393
|
-
storageKey: "autobe_semaphore",
|
|
394
|
-
placeholder: "16",
|
|
395
|
-
default: 16,
|
|
396
|
-
min: 1,
|
|
397
|
-
max: 100,
|
|
398
|
-
},
|
|
399
|
-
supportAudioEnable: {
|
|
400
|
-
key: "supportAudioEnable",
|
|
401
|
-
label: "Audio Support",
|
|
402
|
-
type: "checkbox",
|
|
403
|
-
storageKey: "autobe_support_audio",
|
|
404
|
-
placeholder: "Not STT, just bypass to LLM",
|
|
405
|
-
},
|
|
406
|
-
};
|
|
407
|
-
|
|
408
|
-
/** Available config field keys */
|
|
409
|
-
export type AutoBeConfigKey = keyof typeof ALL_CONFIG_FIELDS;
|
|
410
|
-
|
|
411
|
-
/**
|
|
412
|
-
* Create AutoBE configuration fields with additional keys and optional
|
|
413
|
-
* extensions
|
|
414
|
-
*
|
|
415
|
-
* @example
|
|
416
|
-
* ```typescript
|
|
417
|
-
* // Get default fields only
|
|
418
|
-
* createAutoBeConfigFields()
|
|
419
|
-
*
|
|
420
|
-
* // Add serverUrl to default fields
|
|
421
|
-
* createAutoBeConfigFields(['serverUrl'])
|
|
422
|
-
*
|
|
423
|
-
* // Add multiple keys to default fields
|
|
424
|
-
* createAutoBeConfigFields(['serverUrl', 'customKey'])
|
|
425
|
-
*
|
|
426
|
-
* // Add keys + custom extensions
|
|
427
|
-
* createAutoBeConfigFields(['serverUrl'], [
|
|
428
|
-
* { key: 'customField', label: 'Custom', type: 'text', storageKey: 'custom' }
|
|
429
|
-
* ])
|
|
430
|
-
* ```;
|
|
431
|
-
*
|
|
432
|
-
* @param additionalKeys - Array of additional config keys to add to default
|
|
433
|
-
* fields
|
|
434
|
-
* @param extensions - Additional custom config fields to append
|
|
435
|
-
* @returns Array of default + additional + extended config fields
|
|
436
|
-
*/
|
|
437
|
-
export const createAutoBeConfigFields = (
|
|
438
|
-
...extensions: IConfigField[]
|
|
439
|
-
): IConfigField[] => {
|
|
440
|
-
return [...Object.values(ALL_CONFIG_FIELDS), ...extensions];
|
|
441
|
-
};
|
|
442
|
-
|
|
443
|
-
export default AutoBeConfigModal;
|
|
1
|
+
import { CSSProperties, useState } from "react";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
getEncryptedSessionStorage,
|
|
5
|
+
setEncryptedSessionStorage,
|
|
6
|
+
} from "../utils/storage";
|
|
7
|
+
import AutoBeConfigInput from "./common/AutoBeConfigInput";
|
|
8
|
+
|
|
9
|
+
/** Generic config field definition */
|
|
10
|
+
export interface IConfigField {
|
|
11
|
+
key: string;
|
|
12
|
+
label: string;
|
|
13
|
+
type: "text" | "number" | "checkbox" | "list";
|
|
14
|
+
placeholder?: string;
|
|
15
|
+
default?: string | number | boolean;
|
|
16
|
+
suggestions?: string[];
|
|
17
|
+
required?: boolean;
|
|
18
|
+
min?: number;
|
|
19
|
+
max?: number;
|
|
20
|
+
storageKey: string;
|
|
21
|
+
encrypted?: boolean; // Use sessionStorage with encryption
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface IAutoBeConfigModalProps {
|
|
25
|
+
isOpen: boolean;
|
|
26
|
+
onClose: () => void;
|
|
27
|
+
title?: string;
|
|
28
|
+
fields: IConfigField[];
|
|
29
|
+
onSave?: (config: Record<string, unknown>) => void;
|
|
30
|
+
onReset?: () => void;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Generic configuration modal component Receives config field definitions from
|
|
35
|
+
* props for maximum flexibility
|
|
36
|
+
*/
|
|
37
|
+
export const AutoBeConfigModal = (props: IAutoBeConfigModalProps) => {
|
|
38
|
+
// Get stored values for all fields
|
|
39
|
+
const getStoredValue = (field: IConfigField) => {
|
|
40
|
+
if (typeof window === "undefined") {
|
|
41
|
+
if (field.type === "checkbox") return false;
|
|
42
|
+
if (field.type === "number") return 0;
|
|
43
|
+
return "";
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const stored = field.encrypted
|
|
47
|
+
? getEncryptedSessionStorage(field.storageKey)
|
|
48
|
+
: localStorage.getItem(field.storageKey);
|
|
49
|
+
|
|
50
|
+
if (stored !== null && stored !== "") {
|
|
51
|
+
if (field.type === "checkbox") {
|
|
52
|
+
return stored === "true";
|
|
53
|
+
}
|
|
54
|
+
if (field.type === "number") {
|
|
55
|
+
return parseInt(stored, 10) || 0;
|
|
56
|
+
}
|
|
57
|
+
return stored;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (field.default !== undefined) return field.default;
|
|
61
|
+
// Return default values based on type
|
|
62
|
+
if (field.type === "checkbox") return false;
|
|
63
|
+
if (field.type === "number") return 0;
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
// Initialize config state from stored values
|
|
67
|
+
const [config, setConfig] = useState<Record<string, unknown>>(() => {
|
|
68
|
+
const initialConfig: Record<string, unknown> = {};
|
|
69
|
+
props.fields.forEach((field) => {
|
|
70
|
+
initialConfig[field.key] = getStoredValue(field) ?? undefined;
|
|
71
|
+
});
|
|
72
|
+
return initialConfig;
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
const updateConfig = (key: string, value: unknown) => {
|
|
76
|
+
setConfig((prev) => ({ ...prev, [key]: value }));
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
const handleSave = () => {
|
|
80
|
+
// Save all config values to their respective storage
|
|
81
|
+
props.fields.forEach((field) => {
|
|
82
|
+
const value = config[field.key];
|
|
83
|
+
const stringValue = String(value);
|
|
84
|
+
|
|
85
|
+
if (field.encrypted) {
|
|
86
|
+
setEncryptedSessionStorage(field.storageKey, stringValue);
|
|
87
|
+
} else {
|
|
88
|
+
localStorage.setItem(field.storageKey, stringValue);
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
console.log(config);
|
|
93
|
+
// Call optional external save handler
|
|
94
|
+
props.onSave?.(config);
|
|
95
|
+
props.onClose();
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
if (!props.isOpen) return null;
|
|
99
|
+
|
|
100
|
+
return (
|
|
101
|
+
<div
|
|
102
|
+
style={{
|
|
103
|
+
position: "fixed",
|
|
104
|
+
top: 0,
|
|
105
|
+
left: 0,
|
|
106
|
+
right: 0,
|
|
107
|
+
bottom: 0,
|
|
108
|
+
backgroundColor: "rgba(0, 0, 0, 0.5)",
|
|
109
|
+
display: "flex",
|
|
110
|
+
alignItems: "center",
|
|
111
|
+
justifyContent: "center",
|
|
112
|
+
zIndex: 1002,
|
|
113
|
+
}}
|
|
114
|
+
onClick={props.onClose}
|
|
115
|
+
>
|
|
116
|
+
<div
|
|
117
|
+
style={{
|
|
118
|
+
backgroundColor: "white",
|
|
119
|
+
borderRadius: "1rem",
|
|
120
|
+
padding: "1.25rem",
|
|
121
|
+
width: "90%",
|
|
122
|
+
maxWidth: "500px",
|
|
123
|
+
maxHeight: "90vh",
|
|
124
|
+
overflow: "visible",
|
|
125
|
+
boxShadow:
|
|
126
|
+
"0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04)",
|
|
127
|
+
}}
|
|
128
|
+
onClick={(e) => e.stopPropagation()}
|
|
129
|
+
>
|
|
130
|
+
{/* Header with title and X button */}
|
|
131
|
+
<div style={{ position: "relative", marginBottom: "1.25rem" }}>
|
|
132
|
+
<h2
|
|
133
|
+
style={{
|
|
134
|
+
margin: "0",
|
|
135
|
+
fontSize: "1.125rem",
|
|
136
|
+
fontWeight: "600",
|
|
137
|
+
color: "#1a1a1a",
|
|
138
|
+
textAlign: "center",
|
|
139
|
+
}}
|
|
140
|
+
>
|
|
141
|
+
{props.title || "Configuration"}
|
|
142
|
+
</h2>
|
|
143
|
+
|
|
144
|
+
{/* X Close Button */}
|
|
145
|
+
<button
|
|
146
|
+
onClick={props.onClose}
|
|
147
|
+
style={{
|
|
148
|
+
position: "absolute",
|
|
149
|
+
top: "50%",
|
|
150
|
+
right: "0",
|
|
151
|
+
transform: "translateY(-50%)",
|
|
152
|
+
background: "none",
|
|
153
|
+
border: "none",
|
|
154
|
+
cursor: "pointer",
|
|
155
|
+
padding: "0.25rem",
|
|
156
|
+
borderRadius: "50%",
|
|
157
|
+
width: "1.75rem",
|
|
158
|
+
height: "1.75rem",
|
|
159
|
+
display: "flex",
|
|
160
|
+
alignItems: "center",
|
|
161
|
+
justifyContent: "center",
|
|
162
|
+
fontSize: "1rem",
|
|
163
|
+
color: "#6b7280",
|
|
164
|
+
transition: "all 0.2s",
|
|
165
|
+
}}
|
|
166
|
+
className="modal-close-button"
|
|
167
|
+
>
|
|
168
|
+
✖️
|
|
169
|
+
</button>
|
|
170
|
+
|
|
171
|
+
<style>
|
|
172
|
+
{`
|
|
173
|
+
.modal-close-button:hover {
|
|
174
|
+
background-color: #f3f4f6 !important;
|
|
175
|
+
color: #374151 !important;
|
|
176
|
+
}
|
|
177
|
+
`}
|
|
178
|
+
</style>
|
|
179
|
+
</div>
|
|
180
|
+
|
|
181
|
+
<div style={{ display: "flex", flexDirection: "column", gap: "1rem" }}>
|
|
182
|
+
{props.fields.map((field) => (
|
|
183
|
+
<div key={field.key}>
|
|
184
|
+
{field.type === "checkbox" ? (
|
|
185
|
+
<div style={{ marginBottom: "0.25rem" }}>
|
|
186
|
+
<label
|
|
187
|
+
style={{
|
|
188
|
+
display: "flex",
|
|
189
|
+
alignItems: "center",
|
|
190
|
+
gap: "0.5rem",
|
|
191
|
+
fontSize: "1rem",
|
|
192
|
+
fontWeight: "500",
|
|
193
|
+
color: "#374151",
|
|
194
|
+
marginBottom: "0.5rem",
|
|
195
|
+
}}
|
|
196
|
+
>
|
|
197
|
+
{field.label}
|
|
198
|
+
</label>
|
|
199
|
+
<div
|
|
200
|
+
style={{
|
|
201
|
+
display: "flex",
|
|
202
|
+
alignItems: "center",
|
|
203
|
+
gap: "0.5rem",
|
|
204
|
+
padding: "0.75rem 1rem",
|
|
205
|
+
border: "2px solid #e5e7eb",
|
|
206
|
+
borderRadius: "0.5rem",
|
|
207
|
+
backgroundColor: "#f9fafb",
|
|
208
|
+
transition: "all 0.2s",
|
|
209
|
+
}}
|
|
210
|
+
>
|
|
211
|
+
<input
|
|
212
|
+
type="checkbox"
|
|
213
|
+
checked={Boolean(config[field.key])}
|
|
214
|
+
onChange={(e) =>
|
|
215
|
+
updateConfig(field.key, e.target.checked)
|
|
216
|
+
}
|
|
217
|
+
style={{
|
|
218
|
+
width: "1rem",
|
|
219
|
+
height: "1rem",
|
|
220
|
+
cursor: "pointer",
|
|
221
|
+
}}
|
|
222
|
+
/>
|
|
223
|
+
<span style={{ fontSize: "0.875rem", color: "#6b7280" }}>
|
|
224
|
+
{field.placeholder ||
|
|
225
|
+
`Enable ${field.label.toLowerCase()}`}
|
|
226
|
+
</span>
|
|
227
|
+
</div>
|
|
228
|
+
</div>
|
|
229
|
+
) : (
|
|
230
|
+
<AutoBeConfigInput
|
|
231
|
+
label={field.label}
|
|
232
|
+
type={field.type}
|
|
233
|
+
value={String(config[field.key] || field.default || "")}
|
|
234
|
+
onChange={(value) => {
|
|
235
|
+
const finalValue =
|
|
236
|
+
field.type === "number" ? parseInt(value) || 0 : value;
|
|
237
|
+
updateConfig(field.key, finalValue);
|
|
238
|
+
}}
|
|
239
|
+
placeholder={field.placeholder}
|
|
240
|
+
suggestions={field.suggestions?.map((value) => ({ value }))}
|
|
241
|
+
min={field.min}
|
|
242
|
+
max={field.max}
|
|
243
|
+
required={field.required}
|
|
244
|
+
/>
|
|
245
|
+
)}
|
|
246
|
+
</div>
|
|
247
|
+
))}
|
|
248
|
+
</div>
|
|
249
|
+
|
|
250
|
+
<div
|
|
251
|
+
style={{
|
|
252
|
+
display: "flex",
|
|
253
|
+
gap: "1rem",
|
|
254
|
+
marginTop: "1.25rem",
|
|
255
|
+
justifyContent: "flex-end",
|
|
256
|
+
}}
|
|
257
|
+
>
|
|
258
|
+
<ConfigButton variant="secondary" onClick={props.onClose}>
|
|
259
|
+
Cancel
|
|
260
|
+
</ConfigButton>
|
|
261
|
+
|
|
262
|
+
<ConfigButton variant="primary" onClick={handleSave}>
|
|
263
|
+
Save
|
|
264
|
+
</ConfigButton>
|
|
265
|
+
</div>
|
|
266
|
+
</div>
|
|
267
|
+
</div>
|
|
268
|
+
);
|
|
269
|
+
};
|
|
270
|
+
|
|
271
|
+
/** Reusable button component for config modal */
|
|
272
|
+
interface IConfigButtonProps {
|
|
273
|
+
variant: "primary" | "secondary";
|
|
274
|
+
onClick: () => void;
|
|
275
|
+
children: React.ReactNode;
|
|
276
|
+
disabled?: boolean;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
const ConfigButton = ({
|
|
280
|
+
variant,
|
|
281
|
+
onClick,
|
|
282
|
+
children,
|
|
283
|
+
disabled,
|
|
284
|
+
}: IConfigButtonProps) => {
|
|
285
|
+
const baseStyles: CSSProperties = {
|
|
286
|
+
padding: "0.5rem 1.25rem",
|
|
287
|
+
fontSize: "0.875rem",
|
|
288
|
+
fontWeight: "500",
|
|
289
|
+
borderRadius: "0.5rem",
|
|
290
|
+
cursor: disabled ? "not-allowed" : "pointer",
|
|
291
|
+
transition: "all 0.2s",
|
|
292
|
+
border: "none",
|
|
293
|
+
outline: "none",
|
|
294
|
+
opacity: disabled ? 0.5 : 1,
|
|
295
|
+
};
|
|
296
|
+
|
|
297
|
+
const variantStyles: Record<"primary" | "secondary", CSSProperties> = {
|
|
298
|
+
primary: {
|
|
299
|
+
color: "white",
|
|
300
|
+
backgroundColor: "#3b82f6",
|
|
301
|
+
border: "1px solid #3b82f6",
|
|
302
|
+
},
|
|
303
|
+
secondary: {
|
|
304
|
+
color: "#374151",
|
|
305
|
+
backgroundColor: "#f9fafb",
|
|
306
|
+
border: "1px solid #d1d5db",
|
|
307
|
+
},
|
|
308
|
+
};
|
|
309
|
+
|
|
310
|
+
return (
|
|
311
|
+
<>
|
|
312
|
+
<style>
|
|
313
|
+
{`
|
|
314
|
+
.config-button-${variant}:hover:not(:disabled) {
|
|
315
|
+
${
|
|
316
|
+
variant === "primary"
|
|
317
|
+
? "background-color: #2563eb !important; border-color: #2563eb !important;"
|
|
318
|
+
: "background-color: #f3f4f6 !important; border-color: #9ca3af !important;"
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
`}
|
|
322
|
+
</style>
|
|
323
|
+
<button
|
|
324
|
+
className={`config-button-${variant}`}
|
|
325
|
+
style={{
|
|
326
|
+
...baseStyles,
|
|
327
|
+
...variantStyles[variant],
|
|
328
|
+
}}
|
|
329
|
+
onClick={onClick}
|
|
330
|
+
disabled={disabled}
|
|
331
|
+
>
|
|
332
|
+
{children}
|
|
333
|
+
</button>
|
|
334
|
+
</>
|
|
335
|
+
);
|
|
336
|
+
};
|
|
337
|
+
|
|
338
|
+
/** All available AutoBE configuration fields */
|
|
339
|
+
const ALL_CONFIG_FIELDS: Record<string, IConfigField> = {
|
|
340
|
+
locale: {
|
|
341
|
+
key: "locale",
|
|
342
|
+
label: "Language",
|
|
343
|
+
type: "text",
|
|
344
|
+
storageKey: "autobe_locale",
|
|
345
|
+
placeholder: "en",
|
|
346
|
+
default: "en",
|
|
347
|
+
suggestions: ["en", "ko", "ja", "zh", "es", "fr", "de"],
|
|
348
|
+
},
|
|
349
|
+
schemaModel: {
|
|
350
|
+
key: "schemaModel",
|
|
351
|
+
label: "Schema Model",
|
|
352
|
+
type: "text",
|
|
353
|
+
storageKey: "autobe_schema_model",
|
|
354
|
+
placeholder: "chatgpt",
|
|
355
|
+
default: "chatgpt",
|
|
356
|
+
suggestions: ["chatgpt", "claude"],
|
|
357
|
+
},
|
|
358
|
+
aiModel: {
|
|
359
|
+
key: "aiModel",
|
|
360
|
+
label: "AI Model",
|
|
361
|
+
type: "text",
|
|
362
|
+
storageKey: "autobe_ai_model",
|
|
363
|
+
placeholder: "gpt-4.1",
|
|
364
|
+
default: "gpt-4.1",
|
|
365
|
+
suggestions: [
|
|
366
|
+
"gpt-4.1",
|
|
367
|
+
"gpt-4.1-mini",
|
|
368
|
+
"qwen/qwen3-235b-a22b-2507",
|
|
369
|
+
"qwen/qwen3-next-80b-a3b-instruct",
|
|
370
|
+
],
|
|
371
|
+
},
|
|
372
|
+
openApiKey: {
|
|
373
|
+
key: "openApiKey",
|
|
374
|
+
label: "OpenAI API Key",
|
|
375
|
+
type: "text",
|
|
376
|
+
storageKey: "autobe_openapi_key_encrypted",
|
|
377
|
+
placeholder: "sk-...",
|
|
378
|
+
encrypted: true,
|
|
379
|
+
required: true,
|
|
380
|
+
},
|
|
381
|
+
baseUrl: {
|
|
382
|
+
key: "baseUrl",
|
|
383
|
+
label: "Base URL",
|
|
384
|
+
type: "text",
|
|
385
|
+
storageKey: "autobe_base_url",
|
|
386
|
+
placeholder: "https://api.openai.com/v1",
|
|
387
|
+
suggestions: ["https://api.openai.com/v1", "https://openrouter.ai/api/v1"],
|
|
388
|
+
},
|
|
389
|
+
semaphore: {
|
|
390
|
+
key: "semaphore",
|
|
391
|
+
label: "Concurrency Limit",
|
|
392
|
+
type: "number",
|
|
393
|
+
storageKey: "autobe_semaphore",
|
|
394
|
+
placeholder: "16",
|
|
395
|
+
default: 16,
|
|
396
|
+
min: 1,
|
|
397
|
+
max: 100,
|
|
398
|
+
},
|
|
399
|
+
supportAudioEnable: {
|
|
400
|
+
key: "supportAudioEnable",
|
|
401
|
+
label: "Audio Support",
|
|
402
|
+
type: "checkbox",
|
|
403
|
+
storageKey: "autobe_support_audio",
|
|
404
|
+
placeholder: "Not STT, just bypass to LLM",
|
|
405
|
+
},
|
|
406
|
+
};
|
|
407
|
+
|
|
408
|
+
/** Available config field keys */
|
|
409
|
+
export type AutoBeConfigKey = keyof typeof ALL_CONFIG_FIELDS;
|
|
410
|
+
|
|
411
|
+
/**
|
|
412
|
+
* Create AutoBE configuration fields with additional keys and optional
|
|
413
|
+
* extensions
|
|
414
|
+
*
|
|
415
|
+
* @example
|
|
416
|
+
* ```typescript
|
|
417
|
+
* // Get default fields only
|
|
418
|
+
* createAutoBeConfigFields()
|
|
419
|
+
*
|
|
420
|
+
* // Add serverUrl to default fields
|
|
421
|
+
* createAutoBeConfigFields(['serverUrl'])
|
|
422
|
+
*
|
|
423
|
+
* // Add multiple keys to default fields
|
|
424
|
+
* createAutoBeConfigFields(['serverUrl', 'customKey'])
|
|
425
|
+
*
|
|
426
|
+
* // Add keys + custom extensions
|
|
427
|
+
* createAutoBeConfigFields(['serverUrl'], [
|
|
428
|
+
* { key: 'customField', label: 'Custom', type: 'text', storageKey: 'custom' }
|
|
429
|
+
* ])
|
|
430
|
+
* ```;
|
|
431
|
+
*
|
|
432
|
+
* @param additionalKeys - Array of additional config keys to add to default
|
|
433
|
+
* fields
|
|
434
|
+
* @param extensions - Additional custom config fields to append
|
|
435
|
+
* @returns Array of default + additional + extended config fields
|
|
436
|
+
*/
|
|
437
|
+
export const createAutoBeConfigFields = (
|
|
438
|
+
...extensions: IConfigField[]
|
|
439
|
+
): IConfigField[] => {
|
|
440
|
+
return [...Object.values(ALL_CONFIG_FIELDS), ...extensions];
|
|
441
|
+
};
|
|
442
|
+
|
|
443
|
+
export default AutoBeConfigModal;
|