@autobe/ui 0.29.2 → 0.30.0-dev.20260315
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/README.md +261 -0
- package/lib/components/AutoBeChatMain.js +5 -5
- package/lib/components/AutoBeChatMain.js.map +1 -1
- package/lib/components/AutoBeConfigModal.js +9 -9
- package/lib/components/AutoBeStatusModal.js +4 -4
- package/lib/components/AutoBeStatusModal.js.map +1 -1
- package/lib/components/AutoBeUserMessageMovie.d.ts +2 -2
- package/lib/components/common/ChatBubble.d.ts +2 -2
- package/lib/components/common/openai/OpenAIContent.d.ts +2 -2
- package/lib/components/common/openai/OpenAIContent.js.map +1 -1
- package/lib/components/common/openai/OpenAIUserAudioContent.js +1 -1
- package/lib/components/common/openai/OpenAIUserAudioContent.js.map +1 -1
- package/lib/components/common/openai/OpenAIUserFileContent.js +1 -1
- package/lib/components/common/openai/OpenAIUserFileContent.js.map +1 -1
- package/lib/components/common/openai/OpenAIUserImageContent.d.ts +2 -2
- package/lib/components/events/AutoBeCompleteEventMovie.d.ts +2 -2
- package/lib/components/events/AutoBeCompleteEventMovie.js +5 -5
- package/lib/components/events/AutoBeCompleteEventMovie.js.map +1 -1
- package/lib/components/events/AutoBeCorrectEventMovie.d.ts +2 -2
- package/lib/components/events/AutoBeCorrectEventMovie.js +4 -4
- package/lib/components/events/AutoBeCorrectEventMovie.js.map +1 -1
- package/lib/components/events/AutoBeEventMovie.js +38 -17
- package/lib/components/events/AutoBeEventMovie.js.map +1 -1
- package/lib/components/events/AutoBeProgressEventMovie.js +73 -13
- package/lib/components/events/AutoBeProgressEventMovie.js.map +1 -1
- package/lib/components/events/AutoBeScenarioEventMovie.d.ts +2 -2
- package/lib/components/events/AutoBeScenarioEventMovie.js +18 -5
- package/lib/components/events/AutoBeScenarioEventMovie.js.map +1 -1
- package/lib/components/events/AutoBeStartEventMovie.d.ts +2 -2
- package/lib/components/events/AutoBeStartEventMovie.js +2 -2
- package/lib/components/events/AutoBeStartEventMovie.js.map +1 -1
- package/lib/components/events/AutoBeValidateEventMovie.d.ts +2 -2
- package/lib/components/events/AutoBeValidateEventMovie.js +3 -11
- package/lib/components/events/AutoBeValidateEventMovie.js.map +1 -1
- package/lib/components/events/groups/CorrectEventGroup.d.ts +2 -2
- package/lib/components/events/groups/CorrectEventGroup.js +1 -1
- package/lib/components/events/groups/CorrectEventGroup.js.map +1 -1
- package/lib/components/events/groups/ValidateEventGroup.d.ts +2 -2
- package/lib/components/events/groups/ValidateEventGroup.js +1 -2
- package/lib/components/events/groups/ValidateEventGroup.js.map +1 -1
- package/lib/components/events/utils/eventGrouper.js +1 -2
- package/lib/components/events/utils/eventGrouper.js.map +1 -1
- package/lib/components/upload/AutoBeChatUploadBox.d.ts +3 -4
- package/lib/components/upload/AutoBeChatUploadBox.js +2 -1
- package/lib/components/upload/AutoBeChatUploadBox.js.map +1 -1
- package/lib/components/upload/AutoBeChatUploadSendButton.js +1 -1
- package/lib/components/upload/AutoBeChatUploadSendButton.js.map +1 -1
- package/lib/context/AutoBeAgentContext.d.ts +1 -3
- package/lib/context/AutoBeAgentContext.js +0 -4
- package/lib/context/AutoBeAgentContext.js.map +1 -1
- package/lib/hooks/useSessionStorage.d.ts +4 -0
- package/lib/hooks/useSessionStorage.js +16 -0
- package/lib/hooks/useSessionStorage.js.map +1 -0
- package/lib/index.d.ts +1 -0
- package/lib/index.js +1 -0
- package/lib/index.js.map +1 -1
- package/lib/strategy/AutoBeAgentSessionStorageStrategy.d.ts +10 -0
- package/lib/strategy/AutoBeAgentSessionStorageStrategy.js +117 -0
- package/lib/strategy/AutoBeAgentSessionStorageStrategy.js.map +1 -0
- package/lib/structure/AutoBeListener.js +91 -23
- package/lib/structure/AutoBeListener.js.map +1 -1
- package/lib/structure/AutoBeListenerState.d.ts +3 -3
- package/lib/structure/AutoBeListenerState.js +4 -4
- package/lib/structure/AutoBeListenerState.js.map +1 -1
- package/lib/structure/IAutoBeAgentSessionStorageStrategy.js +1 -1
- package/lib/structure/IAutoBeAgentSessionStorageStrategy.js.map +1 -1
- package/lib/utils/AutoBeFileUploader.d.ts +2 -2
- package/lib/utils/AutoBeFileUploader.js.map +1 -1
- package/package.json +3 -4
- 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 -484
- 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 -368
- package/src/components/events/AutoBeEventGroupMovie.tsx +18 -18
- package/src/components/events/AutoBeEventMovie.tsx +158 -139
- package/src/components/events/AutoBeProgressEventMovie.tsx +217 -157
- package/src/components/events/AutoBeScenarioEventMovie.tsx +135 -95
- package/src/components/events/AutoBeStartEventMovie.tsx +82 -82
- package/src/components/events/AutoBeValidateEventMovie.tsx +249 -286
- 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 -146
- 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 -117
- package/src/components/events/utils/index.ts +1 -1
- package/src/components/index.ts +13 -13
- package/src/components/upload/AutoBeChatUploadBox.tsx +425 -424
- 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 -258
- 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 -0
- package/src/icons/Receipt.tsx +74 -74
- package/src/index.ts +9 -8
- package/src/strategy/AutoBeAgentSessionStorageStrategy.ts +127 -0
- package/src/structure/AutoBeListener.ts +373 -304
- 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
|
@@ -1,279 +1,279 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
} from "@autobe/interface";
|
|
6
|
-
|
|
7
|
-
export namespace AutoBeFileUploader {
|
|
8
|
-
interface IConfig {
|
|
9
|
-
supportAudio?: boolean;
|
|
10
|
-
file?: (file: File) => Promise<{ id: string }>;
|
|
11
|
-
image?: (file: File) => Promise<{ url: string }>;
|
|
12
|
-
}
|
|
13
|
-
export const isValidFileExtension = (
|
|
14
|
-
filename: string,
|
|
15
|
-
supportAudio: boolean,
|
|
16
|
-
hasFileUploadAPI: boolean,
|
|
17
|
-
): boolean => {
|
|
18
|
-
const extension = filename
|
|
19
|
-
.toLowerCase()
|
|
20
|
-
.substring(filename.lastIndexOf("."));
|
|
21
|
-
const format: IFileFormat | undefined = FORMATS[extension];
|
|
22
|
-
if (format === undefined) return false;
|
|
23
|
-
else if (!supportAudio && format.category === "audio") return false;
|
|
24
|
-
|
|
25
|
-
// Without file upload API, only support images, audio (if enabled), and PDF
|
|
26
|
-
if (!hasFileUploadAPI) {
|
|
27
|
-
if (format.category === "document") return extension === ".pdf";
|
|
28
|
-
else if (format.category === "video") return false;
|
|
29
|
-
|
|
30
|
-
const allowedCategories: string[] = ["image"];
|
|
31
|
-
if (supportAudio) allowedCategories.push("audio");
|
|
32
|
-
return allowedCategories.includes(format.category);
|
|
33
|
-
}
|
|
34
|
-
return true;
|
|
35
|
-
};
|
|
36
|
-
|
|
37
|
-
export const getAcceptAttribute = (
|
|
38
|
-
supportAudio: boolean = false,
|
|
39
|
-
hasFileUploadAPI: boolean = false,
|
|
40
|
-
): string => {
|
|
41
|
-
const acceptParts = Object.values(FORMATS)
|
|
42
|
-
.filter((format) => {
|
|
43
|
-
// Audio filter
|
|
44
|
-
if (!supportAudio && format.category === "audio") return false;
|
|
45
|
-
|
|
46
|
-
// Without file upload API, only allow images, audio (if enabled), and PDF
|
|
47
|
-
if (!hasFileUploadAPI) {
|
|
48
|
-
if (format.category === "image") return true;
|
|
49
|
-
if (format.category === "audio" && supportAudio) return true;
|
|
50
|
-
if (format.category === "document" && format.extension === ".pdf")
|
|
51
|
-
return true;
|
|
52
|
-
return false;
|
|
53
|
-
}
|
|
54
|
-
return true;
|
|
55
|
-
})
|
|
56
|
-
.map((format) => format.extension);
|
|
57
|
-
return acceptParts.join(",");
|
|
58
|
-
};
|
|
59
|
-
|
|
60
|
-
export const getMimeType = (filename: string): string => {
|
|
61
|
-
const extension = filename
|
|
62
|
-
.toLowerCase()
|
|
63
|
-
.substring(filename.lastIndexOf("."));
|
|
64
|
-
const format = FORMATS[extension];
|
|
65
|
-
return format?.mimeType || "application/octet-stream";
|
|
66
|
-
};
|
|
67
|
-
export const compose = async (config: IConfig, file: File) => {
|
|
68
|
-
// Validate file extension first
|
|
69
|
-
if (
|
|
70
|
-
!isValidFileExtension(
|
|
71
|
-
file.name,
|
|
72
|
-
config.supportAudio ?? false,
|
|
73
|
-
!!config.file,
|
|
74
|
-
)
|
|
75
|
-
)
|
|
76
|
-
throw new Error(
|
|
77
|
-
`Unsupported file format: ${file.name}. ${!config.file ? "Only images, PDF, and audio files (if enabled) are supported without file upload API." : ""}`,
|
|
78
|
-
);
|
|
79
|
-
|
|
80
|
-
// Check for image files
|
|
81
|
-
const extension = file.name
|
|
82
|
-
.toLowerCase()
|
|
83
|
-
.substring(file.name.lastIndexOf("."));
|
|
84
|
-
const format = FORMATS[extension];
|
|
85
|
-
|
|
86
|
-
if (format?.category === "image")
|
|
87
|
-
return {
|
|
88
|
-
file,
|
|
89
|
-
content: await composeImageContent(config, file),
|
|
90
|
-
};
|
|
91
|
-
else if (
|
|
92
|
-
config.supportAudio &&
|
|
93
|
-
format?.category === "audio" &&
|
|
94
|
-
AUDIO_MIME_VARIANTS.includes(file.type)
|
|
95
|
-
)
|
|
96
|
-
return {
|
|
97
|
-
file,
|
|
98
|
-
content: await composeAudioContent(file),
|
|
99
|
-
};
|
|
100
|
-
return {
|
|
101
|
-
file,
|
|
102
|
-
content: await composeFileContent(config, file),
|
|
103
|
-
};
|
|
104
|
-
};
|
|
105
|
-
|
|
106
|
-
export const convertToBase64 = (file: File): Promise<string> =>
|
|
107
|
-
new Promise((resolve, reject) => {
|
|
108
|
-
const reader = new FileReader();
|
|
109
|
-
reader.onload = () => {
|
|
110
|
-
let data: string = reader.result as string;
|
|
111
|
-
|
|
112
|
-
// If browser couldn't determine MIME type properly, replace with correct one
|
|
113
|
-
if (
|
|
114
|
-
data.startsWith("data:application/octet-stream") ||
|
|
115
|
-
data.startsWith("data:;")
|
|
116
|
-
) {
|
|
117
|
-
const mimeType = getMimeType(file.name);
|
|
118
|
-
data = data.replace(/^data:[^;]*/, `data:${mimeType}`);
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
resolve(data);
|
|
122
|
-
};
|
|
123
|
-
reader.onerror = reject;
|
|
124
|
-
reader.readAsDataURL(file);
|
|
125
|
-
});
|
|
126
|
-
|
|
127
|
-
export const readAsText = (file: File): Promise<string> =>
|
|
128
|
-
new Promise((resolve, reject) => {
|
|
129
|
-
const reader = new FileReader();
|
|
130
|
-
reader.onload = () => {
|
|
131
|
-
const text = reader.result as string;
|
|
132
|
-
// Convert text to base64
|
|
133
|
-
const base64 = btoa(unescape(encodeURIComponent(text)));
|
|
134
|
-
resolve(base64);
|
|
135
|
-
};
|
|
136
|
-
reader.onerror = reject;
|
|
137
|
-
reader.readAsText(file);
|
|
138
|
-
});
|
|
139
|
-
|
|
140
|
-
const composeImageContent = async (
|
|
141
|
-
config: IConfig,
|
|
142
|
-
file: File,
|
|
143
|
-
): Promise<
|
|
144
|
-
type: "image",
|
|
145
|
-
image: config.image
|
|
146
|
-
? {
|
|
147
|
-
type: "url",
|
|
148
|
-
url: await config.image(file).then((res) => res.url),
|
|
149
|
-
}
|
|
150
|
-
: {
|
|
151
|
-
type: "base64",
|
|
152
|
-
data: await convertToBase64(file),
|
|
153
|
-
},
|
|
154
|
-
});
|
|
155
|
-
|
|
156
|
-
const composeAudioContent = async (
|
|
157
|
-
file: File,
|
|
158
|
-
): Promise<AutoBeUserMessageAudioContent> => ({
|
|
159
|
-
type: "audio",
|
|
160
|
-
data: (await convertToBase64(file)).split(",")[1]!,
|
|
161
|
-
format: file.type.includes("wav") ? "wav" : "mp3",
|
|
162
|
-
});
|
|
163
|
-
|
|
164
|
-
const composeFileContent = async (
|
|
165
|
-
config: IConfig,
|
|
166
|
-
file: File,
|
|
167
|
-
): Promise<AutoBeUserMessageFileContent> => {
|
|
168
|
-
// Get MIME type for the file
|
|
169
|
-
const mimeType = getMimeType(file.name);
|
|
170
|
-
|
|
171
|
-
// If file upload API is available, use it
|
|
172
|
-
if (config.file) {
|
|
173
|
-
return {
|
|
174
|
-
type: "file",
|
|
175
|
-
file: {
|
|
176
|
-
type: "id",
|
|
177
|
-
id: await config.file(file).then((res) => res.id),
|
|
178
|
-
} satisfies AutoBeUserMessageFileContent.IId,
|
|
179
|
-
};
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
// If MIME type starts with text/, read as text and encode to base64 without data URL
|
|
183
|
-
if (mimeType.startsWith("text/")) {
|
|
184
|
-
return {
|
|
185
|
-
type: "file",
|
|
186
|
-
file: {
|
|
187
|
-
type: "base64",
|
|
188
|
-
name: file.name,
|
|
189
|
-
data: await readAsText(file),
|
|
190
|
-
} satisfies AutoBeUserMessageFileContent.IBase64,
|
|
191
|
-
};
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
// For other files, use data URL format
|
|
195
|
-
return {
|
|
196
|
-
type: "file",
|
|
197
|
-
file: {
|
|
198
|
-
type: "base64",
|
|
199
|
-
name: file.name,
|
|
200
|
-
data: await convertToBase64(file),
|
|
201
|
-
} satisfies AutoBeUserMessageFileContent.IBase64,
|
|
202
|
-
};
|
|
203
|
-
};
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
interface IFileFormat {
|
|
207
|
-
extension: string;
|
|
208
|
-
mimeType: string;
|
|
209
|
-
category: "image" | "audio" | "video" | "document";
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
const FORMATS: Record<string, IFileFormat> = {
|
|
213
|
-
// Images
|
|
214
|
-
".png": { extension: ".png", mimeType: "image/png", category: "image" },
|
|
215
|
-
".jpg": { extension: ".jpg", mimeType: "image/jpeg", category: "image" },
|
|
216
|
-
".jpeg": { extension: ".jpeg", mimeType: "image/jpeg", category: "image" },
|
|
217
|
-
".gif": { extension: ".gif", mimeType: "image/gif", category: "image" },
|
|
218
|
-
".webp": { extension: ".webp", mimeType: "image/webp", category: "image" },
|
|
219
|
-
|
|
220
|
-
// Audio
|
|
221
|
-
".mp3": { extension: ".mp3", mimeType: "audio/mpeg", category: "audio" },
|
|
222
|
-
".wav": { extension: ".wav", mimeType: "audio/wav", category: "audio" },
|
|
223
|
-
|
|
224
|
-
// Video
|
|
225
|
-
".mp4": { extension: ".mp4", mimeType: "video/mp4", category: "video" },
|
|
226
|
-
".mpeg": { extension: ".mpeg", mimeType: "video/mpeg", category: "video" },
|
|
227
|
-
".mov": { extension: ".mov", mimeType: "video/quicktime", category: "video" },
|
|
228
|
-
".avi": { extension: ".avi", mimeType: "video/x-msvideo", category: "video" },
|
|
229
|
-
".webm": { extension: ".webm", mimeType: "video/webm", category: "video" },
|
|
230
|
-
".flv": { extension: ".flv", mimeType: "video/x-flv", category: "video" },
|
|
231
|
-
".mkv": {
|
|
232
|
-
extension: ".mkv",
|
|
233
|
-
mimeType: "video/x-matroska",
|
|
234
|
-
category: "video",
|
|
235
|
-
},
|
|
236
|
-
".wmv": { extension: ".wmv", mimeType: "video/x-ms-wmv", category: "video" },
|
|
237
|
-
|
|
238
|
-
// Documents
|
|
239
|
-
".pdf": {
|
|
240
|
-
extension: ".pdf",
|
|
241
|
-
mimeType: "application/pdf",
|
|
242
|
-
category: "document",
|
|
243
|
-
},
|
|
244
|
-
".txt": { extension: ".txt", mimeType: "text/plain", category: "document" },
|
|
245
|
-
".md": { extension: ".md", mimeType: "text/plain", category: "document" },
|
|
246
|
-
".docx": {
|
|
247
|
-
extension: ".docx",
|
|
248
|
-
mimeType:
|
|
249
|
-
"application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
|
250
|
-
category: "document",
|
|
251
|
-
},
|
|
252
|
-
".html": { extension: ".html", mimeType: "text/html", category: "document" },
|
|
253
|
-
".json": {
|
|
254
|
-
extension: ".json",
|
|
255
|
-
mimeType: "application/json",
|
|
256
|
-
category: "document",
|
|
257
|
-
},
|
|
258
|
-
".csv": { extension: ".csv", mimeType: "text/csv", category: "document" },
|
|
259
|
-
".xml": {
|
|
260
|
-
extension: ".xml",
|
|
261
|
-
mimeType: "application/xml",
|
|
262
|
-
category: "document",
|
|
263
|
-
},
|
|
264
|
-
".rtf": {
|
|
265
|
-
extension: ".rtf",
|
|
266
|
-
mimeType: "application/rtf",
|
|
267
|
-
category: "document",
|
|
268
|
-
},
|
|
269
|
-
};
|
|
270
|
-
|
|
271
|
-
// Alternative MIME types for audio files that browsers might use
|
|
272
|
-
const AUDIO_MIME_VARIANTS = [
|
|
273
|
-
"audio/mpeg",
|
|
274
|
-
"audio/mp3",
|
|
275
|
-
"audio/wav",
|
|
276
|
-
"audio/x-wav",
|
|
277
|
-
"audio/wave",
|
|
278
|
-
"audio/x-wave",
|
|
279
|
-
];
|
|
1
|
+
import {
|
|
2
|
+
AutoBeUserImageConversateContent,
|
|
3
|
+
AutoBeUserMessageAudioContent,
|
|
4
|
+
AutoBeUserMessageFileContent,
|
|
5
|
+
} from "@autobe/interface";
|
|
6
|
+
|
|
7
|
+
export namespace AutoBeFileUploader {
|
|
8
|
+
interface IConfig {
|
|
9
|
+
supportAudio?: boolean;
|
|
10
|
+
file?: (file: File) => Promise<{ id: string }>;
|
|
11
|
+
image?: (file: File) => Promise<{ url: string }>;
|
|
12
|
+
}
|
|
13
|
+
export const isValidFileExtension = (
|
|
14
|
+
filename: string,
|
|
15
|
+
supportAudio: boolean,
|
|
16
|
+
hasFileUploadAPI: boolean,
|
|
17
|
+
): boolean => {
|
|
18
|
+
const extension = filename
|
|
19
|
+
.toLowerCase()
|
|
20
|
+
.substring(filename.lastIndexOf("."));
|
|
21
|
+
const format: IFileFormat | undefined = FORMATS[extension];
|
|
22
|
+
if (format === undefined) return false;
|
|
23
|
+
else if (!supportAudio && format.category === "audio") return false;
|
|
24
|
+
|
|
25
|
+
// Without file upload API, only support images, audio (if enabled), and PDF
|
|
26
|
+
if (!hasFileUploadAPI) {
|
|
27
|
+
if (format.category === "document") return extension === ".pdf";
|
|
28
|
+
else if (format.category === "video") return false;
|
|
29
|
+
|
|
30
|
+
const allowedCategories: string[] = ["image"];
|
|
31
|
+
if (supportAudio) allowedCategories.push("audio");
|
|
32
|
+
return allowedCategories.includes(format.category);
|
|
33
|
+
}
|
|
34
|
+
return true;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
export const getAcceptAttribute = (
|
|
38
|
+
supportAudio: boolean = false,
|
|
39
|
+
hasFileUploadAPI: boolean = false,
|
|
40
|
+
): string => {
|
|
41
|
+
const acceptParts = Object.values(FORMATS)
|
|
42
|
+
.filter((format) => {
|
|
43
|
+
// Audio filter
|
|
44
|
+
if (!supportAudio && format.category === "audio") return false;
|
|
45
|
+
|
|
46
|
+
// Without file upload API, only allow images, audio (if enabled), and PDF
|
|
47
|
+
if (!hasFileUploadAPI) {
|
|
48
|
+
if (format.category === "image") return true;
|
|
49
|
+
if (format.category === "audio" && supportAudio) return true;
|
|
50
|
+
if (format.category === "document" && format.extension === ".pdf")
|
|
51
|
+
return true;
|
|
52
|
+
return false;
|
|
53
|
+
}
|
|
54
|
+
return true;
|
|
55
|
+
})
|
|
56
|
+
.map((format) => format.extension);
|
|
57
|
+
return acceptParts.join(",");
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
export const getMimeType = (filename: string): string => {
|
|
61
|
+
const extension = filename
|
|
62
|
+
.toLowerCase()
|
|
63
|
+
.substring(filename.lastIndexOf("."));
|
|
64
|
+
const format = FORMATS[extension];
|
|
65
|
+
return format?.mimeType || "application/octet-stream";
|
|
66
|
+
};
|
|
67
|
+
export const compose = async (config: IConfig, file: File) => {
|
|
68
|
+
// Validate file extension first
|
|
69
|
+
if (
|
|
70
|
+
!isValidFileExtension(
|
|
71
|
+
file.name,
|
|
72
|
+
config.supportAudio ?? false,
|
|
73
|
+
!!config.file,
|
|
74
|
+
)
|
|
75
|
+
)
|
|
76
|
+
throw new Error(
|
|
77
|
+
`Unsupported file format: ${file.name}. ${!config.file ? "Only images, PDF, and audio files (if enabled) are supported without file upload API." : ""}`,
|
|
78
|
+
);
|
|
79
|
+
|
|
80
|
+
// Check for image files
|
|
81
|
+
const extension = file.name
|
|
82
|
+
.toLowerCase()
|
|
83
|
+
.substring(file.name.lastIndexOf("."));
|
|
84
|
+
const format = FORMATS[extension];
|
|
85
|
+
|
|
86
|
+
if (format?.category === "image")
|
|
87
|
+
return {
|
|
88
|
+
file,
|
|
89
|
+
content: await composeImageContent(config, file),
|
|
90
|
+
};
|
|
91
|
+
else if (
|
|
92
|
+
config.supportAudio &&
|
|
93
|
+
format?.category === "audio" &&
|
|
94
|
+
AUDIO_MIME_VARIANTS.includes(file.type)
|
|
95
|
+
)
|
|
96
|
+
return {
|
|
97
|
+
file,
|
|
98
|
+
content: await composeAudioContent(file),
|
|
99
|
+
};
|
|
100
|
+
return {
|
|
101
|
+
file,
|
|
102
|
+
content: await composeFileContent(config, file),
|
|
103
|
+
};
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
export const convertToBase64 = (file: File): Promise<string> =>
|
|
107
|
+
new Promise((resolve, reject) => {
|
|
108
|
+
const reader = new FileReader();
|
|
109
|
+
reader.onload = () => {
|
|
110
|
+
let data: string = reader.result as string;
|
|
111
|
+
|
|
112
|
+
// If browser couldn't determine MIME type properly, replace with correct one
|
|
113
|
+
if (
|
|
114
|
+
data.startsWith("data:application/octet-stream") ||
|
|
115
|
+
data.startsWith("data:;")
|
|
116
|
+
) {
|
|
117
|
+
const mimeType = getMimeType(file.name);
|
|
118
|
+
data = data.replace(/^data:[^;]*/, `data:${mimeType}`);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
resolve(data);
|
|
122
|
+
};
|
|
123
|
+
reader.onerror = reject;
|
|
124
|
+
reader.readAsDataURL(file);
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
export const readAsText = (file: File): Promise<string> =>
|
|
128
|
+
new Promise((resolve, reject) => {
|
|
129
|
+
const reader = new FileReader();
|
|
130
|
+
reader.onload = () => {
|
|
131
|
+
const text = reader.result as string;
|
|
132
|
+
// Convert text to base64
|
|
133
|
+
const base64 = btoa(unescape(encodeURIComponent(text)));
|
|
134
|
+
resolve(base64);
|
|
135
|
+
};
|
|
136
|
+
reader.onerror = reject;
|
|
137
|
+
reader.readAsText(file);
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
const composeImageContent = async (
|
|
141
|
+
config: IConfig,
|
|
142
|
+
file: File,
|
|
143
|
+
): Promise<AutoBeUserImageConversateContent> => ({
|
|
144
|
+
type: "image",
|
|
145
|
+
image: config.image
|
|
146
|
+
? {
|
|
147
|
+
type: "url",
|
|
148
|
+
url: await config.image(file).then((res) => res.url),
|
|
149
|
+
}
|
|
150
|
+
: {
|
|
151
|
+
type: "base64",
|
|
152
|
+
data: await convertToBase64(file),
|
|
153
|
+
},
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
const composeAudioContent = async (
|
|
157
|
+
file: File,
|
|
158
|
+
): Promise<AutoBeUserMessageAudioContent> => ({
|
|
159
|
+
type: "audio",
|
|
160
|
+
data: (await convertToBase64(file)).split(",")[1]!,
|
|
161
|
+
format: file.type.includes("wav") ? "wav" : "mp3",
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
const composeFileContent = async (
|
|
165
|
+
config: IConfig,
|
|
166
|
+
file: File,
|
|
167
|
+
): Promise<AutoBeUserMessageFileContent> => {
|
|
168
|
+
// Get MIME type for the file
|
|
169
|
+
const mimeType = getMimeType(file.name);
|
|
170
|
+
|
|
171
|
+
// If file upload API is available, use it
|
|
172
|
+
if (config.file) {
|
|
173
|
+
return {
|
|
174
|
+
type: "file",
|
|
175
|
+
file: {
|
|
176
|
+
type: "id",
|
|
177
|
+
id: await config.file(file).then((res) => res.id),
|
|
178
|
+
} satisfies AutoBeUserMessageFileContent.IId,
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// If MIME type starts with text/, read as text and encode to base64 without data URL
|
|
183
|
+
if (mimeType.startsWith("text/")) {
|
|
184
|
+
return {
|
|
185
|
+
type: "file",
|
|
186
|
+
file: {
|
|
187
|
+
type: "base64",
|
|
188
|
+
name: file.name,
|
|
189
|
+
data: await readAsText(file),
|
|
190
|
+
} satisfies AutoBeUserMessageFileContent.IBase64,
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// For other files, use data URL format
|
|
195
|
+
return {
|
|
196
|
+
type: "file",
|
|
197
|
+
file: {
|
|
198
|
+
type: "base64",
|
|
199
|
+
name: file.name,
|
|
200
|
+
data: await convertToBase64(file),
|
|
201
|
+
} satisfies AutoBeUserMessageFileContent.IBase64,
|
|
202
|
+
};
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
interface IFileFormat {
|
|
207
|
+
extension: string;
|
|
208
|
+
mimeType: string;
|
|
209
|
+
category: "image" | "audio" | "video" | "document";
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
const FORMATS: Record<string, IFileFormat> = {
|
|
213
|
+
// Images
|
|
214
|
+
".png": { extension: ".png", mimeType: "image/png", category: "image" },
|
|
215
|
+
".jpg": { extension: ".jpg", mimeType: "image/jpeg", category: "image" },
|
|
216
|
+
".jpeg": { extension: ".jpeg", mimeType: "image/jpeg", category: "image" },
|
|
217
|
+
".gif": { extension: ".gif", mimeType: "image/gif", category: "image" },
|
|
218
|
+
".webp": { extension: ".webp", mimeType: "image/webp", category: "image" },
|
|
219
|
+
|
|
220
|
+
// Audio
|
|
221
|
+
".mp3": { extension: ".mp3", mimeType: "audio/mpeg", category: "audio" },
|
|
222
|
+
".wav": { extension: ".wav", mimeType: "audio/wav", category: "audio" },
|
|
223
|
+
|
|
224
|
+
// Video
|
|
225
|
+
".mp4": { extension: ".mp4", mimeType: "video/mp4", category: "video" },
|
|
226
|
+
".mpeg": { extension: ".mpeg", mimeType: "video/mpeg", category: "video" },
|
|
227
|
+
".mov": { extension: ".mov", mimeType: "video/quicktime", category: "video" },
|
|
228
|
+
".avi": { extension: ".avi", mimeType: "video/x-msvideo", category: "video" },
|
|
229
|
+
".webm": { extension: ".webm", mimeType: "video/webm", category: "video" },
|
|
230
|
+
".flv": { extension: ".flv", mimeType: "video/x-flv", category: "video" },
|
|
231
|
+
".mkv": {
|
|
232
|
+
extension: ".mkv",
|
|
233
|
+
mimeType: "video/x-matroska",
|
|
234
|
+
category: "video",
|
|
235
|
+
},
|
|
236
|
+
".wmv": { extension: ".wmv", mimeType: "video/x-ms-wmv", category: "video" },
|
|
237
|
+
|
|
238
|
+
// Documents
|
|
239
|
+
".pdf": {
|
|
240
|
+
extension: ".pdf",
|
|
241
|
+
mimeType: "application/pdf",
|
|
242
|
+
category: "document",
|
|
243
|
+
},
|
|
244
|
+
".txt": { extension: ".txt", mimeType: "text/plain", category: "document" },
|
|
245
|
+
".md": { extension: ".md", mimeType: "text/plain", category: "document" },
|
|
246
|
+
".docx": {
|
|
247
|
+
extension: ".docx",
|
|
248
|
+
mimeType:
|
|
249
|
+
"application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
|
250
|
+
category: "document",
|
|
251
|
+
},
|
|
252
|
+
".html": { extension: ".html", mimeType: "text/html", category: "document" },
|
|
253
|
+
".json": {
|
|
254
|
+
extension: ".json",
|
|
255
|
+
mimeType: "application/json",
|
|
256
|
+
category: "document",
|
|
257
|
+
},
|
|
258
|
+
".csv": { extension: ".csv", mimeType: "text/csv", category: "document" },
|
|
259
|
+
".xml": {
|
|
260
|
+
extension: ".xml",
|
|
261
|
+
mimeType: "application/xml",
|
|
262
|
+
category: "document",
|
|
263
|
+
},
|
|
264
|
+
".rtf": {
|
|
265
|
+
extension: ".rtf",
|
|
266
|
+
mimeType: "application/rtf",
|
|
267
|
+
category: "document",
|
|
268
|
+
},
|
|
269
|
+
};
|
|
270
|
+
|
|
271
|
+
// Alternative MIME types for audio files that browsers might use
|
|
272
|
+
const AUDIO_MIME_VARIANTS = [
|
|
273
|
+
"audio/mpeg",
|
|
274
|
+
"audio/mp3",
|
|
275
|
+
"audio/wav",
|
|
276
|
+
"audio/x-wav",
|
|
277
|
+
"audio/wave",
|
|
278
|
+
"audio/x-wave",
|
|
279
|
+
];
|