@autobe/ui 0.19.1 → 0.21.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.
Files changed (177) hide show
  1. package/lib/AutoBeChatMain.d.ts +16 -0
  2. package/lib/AutoBeChatMain.js +51 -0
  3. package/lib/AutoBeChatMain.js.map +1 -0
  4. package/lib/banner/AutoBeAgentInformation.d.ts +15 -0
  5. package/lib/banner/AutoBeAgentInformation.js +41 -0
  6. package/lib/banner/AutoBeAgentInformation.js.map +1 -0
  7. package/lib/banner/AutoBeChatBanner.d.ts +15 -0
  8. package/lib/banner/AutoBeChatBanner.js +29 -0
  9. package/lib/banner/AutoBeChatBanner.js.map +1 -0
  10. package/lib/banner/AutoBeChatState.d.ts +6 -0
  11. package/lib/banner/AutoBeChatState.js +80 -0
  12. package/lib/banner/AutoBeChatState.js.map +1 -0
  13. package/lib/banner/AutoBeTokenUsage.d.ts +14 -0
  14. package/lib/banner/AutoBeTokenUsage.js +60 -0
  15. package/lib/banner/AutoBeTokenUsage.js.map +1 -0
  16. package/lib/banner/index.d.ts +4 -0
  17. package/lib/banner/index.js +21 -0
  18. package/lib/banner/index.js.map +1 -0
  19. package/lib/common/Collapsible.d.ts +15 -0
  20. package/lib/common/Collapsible.js +45 -0
  21. package/lib/common/Collapsible.js.map +1 -0
  22. package/lib/common/index.d.ts +2 -0
  23. package/lib/common/index.js +19 -0
  24. package/lib/common/index.js.map +1 -0
  25. package/lib/constant/color.d.ts +18 -0
  26. package/lib/constant/color.js +25 -0
  27. package/lib/constant/color.js.map +1 -0
  28. package/lib/events/AutoBeCompleteEventMovie.d.ts +7 -0
  29. package/lib/events/AutoBeCompleteEventMovie.js +210 -0
  30. package/lib/events/AutoBeCompleteEventMovie.js.map +1 -0
  31. package/lib/events/AutoBeEventMovie.d.ts +8 -0
  32. package/lib/events/AutoBeEventMovie.js +84 -0
  33. package/lib/events/AutoBeEventMovie.js.map +1 -0
  34. package/lib/events/AutoBeProgressEventMovie.js +2 -61
  35. package/lib/events/AutoBeProgressEventMovie.js.map +1 -1
  36. package/lib/events/AutoBeScenarioEventMovie.js +2 -44
  37. package/lib/events/AutoBeScenarioEventMovie.js.map +1 -1
  38. package/lib/events/AutoBeValidateEventMovie.d.ts +6 -0
  39. package/lib/events/AutoBeValidateEventMovie.js +115 -0
  40. package/lib/events/AutoBeValidateEventMovie.js.map +1 -0
  41. package/lib/events/common/CollapsibleEventGroup.d.ts +28 -0
  42. package/lib/events/common/CollapsibleEventGroup.js +89 -0
  43. package/lib/events/common/CollapsibleEventGroup.js.map +1 -0
  44. package/lib/events/common/EventCard.d.ts +13 -0
  45. package/lib/events/common/EventCard.js +43 -0
  46. package/lib/events/common/EventCard.js.map +1 -0
  47. package/lib/events/common/EventContent.d.ts +11 -0
  48. package/lib/events/common/EventContent.js +14 -0
  49. package/lib/events/common/EventContent.js.map +1 -0
  50. package/lib/events/common/EventHeader.d.ts +15 -0
  51. package/lib/events/common/EventHeader.js +41 -0
  52. package/lib/events/common/EventHeader.js.map +1 -0
  53. package/lib/events/common/EventIcon.d.ts +11 -0
  54. package/lib/events/common/EventIcon.js +50 -0
  55. package/lib/events/common/EventIcon.js.map +1 -0
  56. package/lib/events/common/ProgressBar.d.ts +14 -0
  57. package/lib/events/common/ProgressBar.js +33 -0
  58. package/lib/events/common/ProgressBar.js.map +1 -0
  59. package/lib/events/common/index.d.ts +6 -0
  60. package/lib/events/common/index.js +16 -0
  61. package/lib/events/common/index.js.map +1 -0
  62. package/lib/events/groups/ValidateEventGroup.d.ts +12 -0
  63. package/lib/events/groups/ValidateEventGroup.js +78 -0
  64. package/lib/events/groups/ValidateEventGroup.js.map +1 -0
  65. package/lib/events/groups/index.d.ts +1 -0
  66. package/lib/events/groups/index.js +6 -0
  67. package/lib/events/groups/index.js.map +1 -0
  68. package/lib/events/index.d.ts +6 -0
  69. package/lib/events/index.js +27 -1
  70. package/lib/events/index.js.map +1 -1
  71. package/lib/events/utils/eventGrouper.d.ts +20 -0
  72. package/lib/events/utils/eventGrouper.js +74 -0
  73. package/lib/events/utils/eventGrouper.js.map +1 -0
  74. package/lib/events/utils/index.d.ts +1 -0
  75. package/lib/events/utils/index.js +6 -0
  76. package/lib/events/utils/index.js.map +1 -0
  77. package/lib/hooks/index.d.ts +2 -0
  78. package/lib/hooks/index.js +19 -0
  79. package/lib/hooks/index.js.map +1 -0
  80. package/lib/hooks/useIsomorphicLayoutEffect.d.ts +6 -0
  81. package/lib/hooks/useIsomorphicLayoutEffect.js +10 -0
  82. package/lib/hooks/useIsomorphicLayoutEffect.js.map +1 -0
  83. package/lib/hooks/useMediaQuery.d.ts +11 -0
  84. package/lib/hooks/useMediaQuery.js +52 -0
  85. package/lib/hooks/useMediaQuery.js.map +1 -0
  86. package/lib/index.d.ts +5 -0
  87. package/lib/index.js +8 -1
  88. package/lib/index.js.map +1 -1
  89. package/lib/structure/AutoBeListener.d.ts +17 -0
  90. package/lib/structure/AutoBeListener.js +250 -0
  91. package/lib/structure/AutoBeListener.js.map +1 -0
  92. package/lib/structure/AutoBeListenerState.d.ts +14 -0
  93. package/lib/structure/AutoBeListenerState.js +39 -0
  94. package/lib/structure/AutoBeListenerState.js.map +1 -0
  95. package/lib/structure/IAutoBeEventGroup.d.ts +5 -0
  96. package/lib/structure/IAutoBeEventGroup.js +3 -0
  97. package/lib/structure/IAutoBeEventGroup.js.map +1 -0
  98. package/lib/structure/index.d.ts +3 -0
  99. package/lib/structure/index.js +20 -0
  100. package/lib/structure/index.js.map +1 -0
  101. package/lib/upload/AutoBeChatUploadBox.d.ts +31 -0
  102. package/lib/upload/AutoBeChatUploadBox.js +221 -0
  103. package/lib/upload/AutoBeChatUploadBox.js.map +1 -0
  104. package/lib/upload/AutoBeChatUploadSendButton.d.ts +15 -0
  105. package/lib/upload/AutoBeChatUploadSendButton.js +38 -0
  106. package/lib/upload/AutoBeChatUploadSendButton.js.map +1 -0
  107. package/lib/upload/AutoBeFileUploadBox.d.ts +8 -0
  108. package/lib/upload/AutoBeFileUploadBox.js +68 -0
  109. package/lib/upload/AutoBeFileUploadBox.js.map +1 -0
  110. package/lib/upload/AutoBeUploadConfig.d.ts +9 -0
  111. package/lib/upload/AutoBeUploadConfig.js +3 -0
  112. package/lib/upload/AutoBeUploadConfig.js.map +1 -0
  113. package/lib/upload/AutoBeVoiceRecoderButton.d.ts +11 -0
  114. package/lib/upload/AutoBeVoiceRecoderButton.js +58 -0
  115. package/lib/upload/AutoBeVoiceRecoderButton.js.map +1 -0
  116. package/lib/upload/index.d.ts +5 -0
  117. package/lib/upload/index.js +22 -0
  118. package/lib/upload/index.js.map +1 -0
  119. package/lib/utils/AutoBeFileUploader.d.ts +28 -0
  120. package/lib/utils/AutoBeFileUploader.js +237 -0
  121. package/lib/utils/AutoBeFileUploader.js.map +1 -0
  122. package/lib/utils/AutoBeVoiceRecorder.d.ts +7 -0
  123. package/lib/utils/AutoBeVoiceRecorder.js +94 -0
  124. package/lib/utils/AutoBeVoiceRecorder.js.map +1 -0
  125. package/lib/utils/index.d.ts +4 -0
  126. package/lib/utils/index.js +21 -0
  127. package/lib/utils/index.js.map +1 -0
  128. package/lib/utils/number.d.ts +1 -0
  129. package/lib/utils/number.js +20 -0
  130. package/lib/utils/number.js.map +1 -0
  131. package/package.json +13 -2
  132. package/src/AutoBeChatMain.tsx +123 -0
  133. package/src/banner/AutoBeAgentInformation.tsx +102 -0
  134. package/src/banner/AutoBeChatBanner.tsx +72 -0
  135. package/src/banner/AutoBeChatState.tsx +152 -0
  136. package/src/banner/AutoBeTokenUsage.tsx +152 -0
  137. package/src/banner/index.ts +4 -0
  138. package/src/common/Collapsible.tsx +95 -0
  139. package/src/common/index.ts +2 -0
  140. package/src/constant/color.ts +24 -0
  141. package/src/events/AutoBeCompleteEventMovie.tsx +402 -0
  142. package/src/events/AutoBeEventMovie.tsx +114 -0
  143. package/src/events/AutoBeProgressEventMovie.tsx +12 -125
  144. package/src/events/AutoBeScenarioEventMovie.tsx +5 -93
  145. package/src/events/AutoBeValidateEventMovie.tsx +326 -0
  146. package/src/events/README.md +300 -0
  147. package/src/events/common/CollapsibleEventGroup.tsx +220 -0
  148. package/src/events/common/EventCard.tsx +61 -0
  149. package/src/events/common/EventContent.tsx +31 -0
  150. package/src/events/common/EventHeader.tsx +85 -0
  151. package/src/events/common/EventIcon.tsx +82 -0
  152. package/src/events/common/ProgressBar.tsx +63 -0
  153. package/src/events/common/index.ts +13 -0
  154. package/src/events/groups/ValidateEventGroup.tsx +150 -0
  155. package/src/events/groups/index.ts +4 -0
  156. package/src/events/index.ts +12 -0
  157. package/src/events/utils/eventGrouper.tsx +118 -0
  158. package/src/events/utils/index.ts +1 -0
  159. package/src/hooks/index.ts +2 -0
  160. package/src/hooks/useIsomorphicLayoutEffect.ts +8 -0
  161. package/src/hooks/useMediaQuery.ts +68 -0
  162. package/src/index.ts +5 -0
  163. package/src/structure/AutoBeListener.ts +263 -0
  164. package/src/structure/AutoBeListenerState.ts +53 -0
  165. package/src/structure/IAutoBeEventGroup.ts +6 -0
  166. package/src/structure/index.ts +3 -0
  167. package/src/upload/AutoBeChatUploadBox.tsx +372 -0
  168. package/src/upload/AutoBeChatUploadSendButton.tsx +66 -0
  169. package/src/upload/AutoBeFileUploadBox.tsx +123 -0
  170. package/src/upload/AutoBeUploadConfig.ts +5 -0
  171. package/src/upload/AutoBeVoiceRecoderButton.tsx +100 -0
  172. package/src/upload/index.ts +5 -0
  173. package/src/utils/AutoBeFileUploader.ts +279 -0
  174. package/src/utils/AutoBeVoiceRecorder.ts +95 -0
  175. package/src/utils/index.ts +4 -0
  176. package/src/utils/number.ts +17 -0
  177. package/tsconfig.json +2 -1
@@ -0,0 +1,66 @@
1
+ /** Props interface for AutoBeChatUploadSendButton component */
2
+ interface IAutoBeChatUploadSendButtonProps {
3
+ /** Function to trigger conversation */
4
+ onClick?: (event: React.MouseEvent<HTMLButtonElement>) => Promise<void>;
5
+ /** Whether the button is enabled */
6
+ enabled: boolean;
7
+ }
8
+
9
+ /**
10
+ * Chat upload send button component for triggering conversations
11
+ *
12
+ * @param props - Component props
13
+ * @returns JSX element representing the send button
14
+ */
15
+ export const AutoBeChatUploadSendButton = (
16
+ props: IAutoBeChatUploadSendButtonProps,
17
+ ) => {
18
+ const baseStyles: React.CSSProperties = {
19
+ padding: "6px",
20
+ border: "none",
21
+ borderRadius: "50%", // 4px에서 50%로 변경하여 둥글게
22
+ backgroundColor: props.enabled ? "#1976d2" : "#e0e0e0",
23
+ color: props.enabled ? "#ffffff" : "#9e9e9e",
24
+ cursor: props.enabled ? "pointer" : "not-allowed",
25
+ display: "inline-flex",
26
+ alignItems: "center",
27
+ justifyContent: "center",
28
+ transition: "background-color 0.3s ease",
29
+ outline: "none",
30
+ width: "32px",
31
+ height: "32px",
32
+ };
33
+
34
+ const hoverStyles: React.CSSProperties = {
35
+ ...baseStyles,
36
+ backgroundColor: props.enabled ? "#1565c0" : "#e0e0e0",
37
+ };
38
+
39
+ return (
40
+ <button
41
+ style={baseStyles}
42
+ onClick={(e) => void props.onClick?.(e)}
43
+ disabled={!props.enabled}
44
+ onMouseEnter={(e) => {
45
+ if (props.enabled) {
46
+ Object.assign(e.currentTarget.style, hoverStyles);
47
+ }
48
+ }}
49
+ onMouseLeave={(e) => {
50
+ Object.assign(e.currentTarget.style, baseStyles);
51
+ }}
52
+ >
53
+ <svg
54
+ width="16"
55
+ height="16"
56
+ viewBox="0 0 24 24"
57
+ fill="currentColor"
58
+ style={{ display: "block" }}
59
+ >
60
+ <path d="M4 12l1.41 1.41L11 7.83V20h2V7.83l5.59 5.58L20 12l-8-8-8 8z" />
61
+ </svg>
62
+ </button>
63
+ );
64
+ };
65
+
66
+ export default AutoBeChatUploadSendButton;
@@ -0,0 +1,123 @@
1
+ import { ReactNode, useState } from "react";
2
+
3
+ export interface IAutoBeFileUploadBoxProps {
4
+ extensionError: ReactNode | null;
5
+ onClick: () => void;
6
+ enabled: boolean;
7
+ }
8
+
9
+ export const AutoBeFileUploadBox = (props: IAutoBeFileUploadBoxProps) => {
10
+ const [isHovered, setIsHovered] = useState(false);
11
+
12
+ const hasError = !!props.extensionError;
13
+
14
+ const getButtonStyles = () => {
15
+ const baseStyles = {
16
+ display: "inline-flex",
17
+ alignItems: "center",
18
+ justifyContent: "center",
19
+ width: "40px",
20
+ height: "40px",
21
+ padding: "0",
22
+ border: "1px solid",
23
+ borderRadius: "50%",
24
+ cursor: props.enabled ? "pointer" : "not-allowed",
25
+ backgroundColor: hasError ? "#fef2f2" : "transparent",
26
+ borderColor: hasError ? "#f87171" : isHovered ? "#3b82f6" : "#d1d5db",
27
+ opacity: props.enabled ? 1 : 0.5,
28
+ transition: "all 0.3s ease",
29
+ position: "relative" as const,
30
+ };
31
+
32
+ if (isHovered && props.enabled) {
33
+ return {
34
+ ...baseStyles,
35
+ backgroundColor: hasError ? "#fef2f2" : "#f3f4f6",
36
+ borderColor: hasError ? "#f87171" : "#3b82f6",
37
+ };
38
+ }
39
+
40
+ return baseStyles;
41
+ };
42
+
43
+ const iconColor = hasError ? "#ef4444" : "#6b7280";
44
+
45
+ return (
46
+ <div style={{ position: "relative", display: "inline-block" }}>
47
+ <button
48
+ onClick={props.onClick}
49
+ disabled={!props.enabled}
50
+ style={getButtonStyles()}
51
+ onMouseEnter={() => {
52
+ setIsHovered(true);
53
+ }}
54
+ onMouseLeave={() => {
55
+ setIsHovered(false);
56
+ }}
57
+ >
58
+ {hasError ? (
59
+ // Error Icon (SVG)
60
+ <svg
61
+ width="20"
62
+ height="20"
63
+ viewBox="0 0 24 24"
64
+ fill="currentColor"
65
+ style={{ color: iconColor }}
66
+ >
67
+ <path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm1 15h-2v-2h2v2zm0-4h-2V7h2v6z" />
68
+ </svg>
69
+ ) : (
70
+ // Add Icon (SVG)
71
+ <svg
72
+ width="20"
73
+ height="20"
74
+ viewBox="0 0 24 24"
75
+ fill="currentColor"
76
+ style={{ color: iconColor }}
77
+ >
78
+ <path d="M19 13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z" />
79
+ </svg>
80
+ )}
81
+ </button>
82
+
83
+ {/* Custom Tooltip - Always show when there's an error */}
84
+ {hasError && (
85
+ <div
86
+ style={{
87
+ position: "absolute",
88
+ bottom: "100%",
89
+ left: "50%",
90
+ transform: "translateX(-50%)",
91
+ marginBottom: "8px",
92
+ padding: "8px 12px",
93
+ backgroundColor: "#ef4444",
94
+ color: "#ffffff",
95
+ borderRadius: "6px",
96
+ fontSize: "14px",
97
+ whiteSpace: "nowrap",
98
+ zIndex: 1000,
99
+ boxShadow: "0 2px 8px rgba(0, 0, 0, 0.15)",
100
+ }}
101
+ >
102
+ {props.extensionError}
103
+ {/* Tooltip Arrow */}
104
+ <div
105
+ style={{
106
+ position: "absolute",
107
+ top: "100%",
108
+ left: "50%",
109
+ transform: "translateX(-50%)",
110
+ width: 0,
111
+ height: 0,
112
+ borderLeft: "6px solid transparent",
113
+ borderRight: "6px solid transparent",
114
+ borderTop: "6px solid #ef4444",
115
+ }}
116
+ />
117
+ </div>
118
+ )}
119
+ </div>
120
+ );
121
+ };
122
+
123
+ export default AutoBeFileUploadBox;
@@ -0,0 +1,5 @@
1
+ export interface IAutoBeUploadConfig {
2
+ supportAudio?: boolean;
3
+ file?: (file: File) => Promise<{ id: string }>;
4
+ image?: (file: File) => Promise<{ url: string }>;
5
+ }
@@ -0,0 +1,100 @@
1
+ import { AutoBeUserMessageAudioContent } from "@autobe/interface";
2
+ import { useState } from "react";
3
+
4
+ import { AutoBeVoiceRecorder } from "../utils/AutoBeVoiceRecorder";
5
+
6
+ export const AutoBeVoiceRecoderButton = (
7
+ props: AutoBeVoiceRecoderButton.IProps,
8
+ ) => {
9
+ const [isRecording, setIsRecording] = useState(false);
10
+ const [mediaRecorder, setMediaRecorder] = useState<MediaRecorder | null>(
11
+ null,
12
+ );
13
+
14
+ const startRecording = async () => {
15
+ const recorder = await AutoBeVoiceRecorder.start(props.onComplete);
16
+ recorder.start();
17
+ setMediaRecorder(recorder);
18
+ setIsRecording(true);
19
+ };
20
+
21
+ const stopRecording = () => {
22
+ if (mediaRecorder && mediaRecorder.state !== "inactive") {
23
+ mediaRecorder.stop();
24
+ setIsRecording(false);
25
+ }
26
+ };
27
+
28
+ const baseStyles: React.CSSProperties = {
29
+ padding: "6px",
30
+ border: "1px solid",
31
+ borderColor: isRecording ? "#f44336" : "#e0e0e0",
32
+ borderRadius: "4px",
33
+ backgroundColor: isRecording ? "#ffebee" : "transparent",
34
+ color: isRecording ? "#f44336" : "#1976d2",
35
+ cursor: props.enabled ? "pointer" : "not-allowed",
36
+ display: "inline-flex",
37
+ alignItems: "center",
38
+ justifyContent: "center",
39
+ transition: "all 0.3s ease",
40
+ outline: "none",
41
+ width: "32px",
42
+ height: "32px",
43
+ opacity: props.enabled ? 1 : 0.5,
44
+ };
45
+
46
+ const hoverStyles: React.CSSProperties = {
47
+ ...baseStyles,
48
+ backgroundColor: isRecording ? "#f44336" : "#f5f5f5",
49
+ borderColor: isRecording ? "#d32f2f" : "#1976d2",
50
+ color: isRecording ? "#ffffff" : "#1976d2",
51
+ };
52
+
53
+ return (
54
+ <button
55
+ style={baseStyles}
56
+ onClick={isRecording ? stopRecording : () => void startRecording()}
57
+ disabled={!props.enabled}
58
+ onMouseEnter={(e) => {
59
+ if (props.enabled) {
60
+ Object.assign(e.currentTarget.style, hoverStyles);
61
+ }
62
+ }}
63
+ onMouseLeave={(e) => {
64
+ Object.assign(e.currentTarget.style, baseStyles);
65
+ }}
66
+ >
67
+ {isRecording ? (
68
+ <svg
69
+ width="16"
70
+ height="16"
71
+ viewBox="0 0 24 24"
72
+ fill="currentColor"
73
+ style={{ display: "block" }}
74
+ >
75
+ <path d="M6 6h12v12H6z" />
76
+ </svg>
77
+ ) : (
78
+ <svg
79
+ width="16"
80
+ height="16"
81
+ viewBox="0 0 24 24"
82
+ fill="currentColor"
83
+ style={{ display: "block" }}
84
+ >
85
+ <path d="M12 2C13.1 2 14 2.9 14 4V10C14 11.1 13.1 12 12 12C10.9 12 10 11.1 10 10V4C10 2.9 10.9 2 12 2ZM19 10V12C19 15.866 15.866 19 12 19C8.134 19 5 15.866 5 12V10H7V12C7 14.761 9.239 17 12 17C14.761 17 17 14.761 17 12V10H19ZM11 20V22H13V20H16V22H8V20H11Z" />
86
+ </svg>
87
+ )}
88
+ </button>
89
+ );
90
+ };
91
+
92
+ export namespace AutoBeVoiceRecoderButton {
93
+ export interface IProps {
94
+ enabled: boolean;
95
+ onComplete: (content: {
96
+ file: File;
97
+ content: AutoBeUserMessageAudioContent;
98
+ }) => void;
99
+ }
100
+ }
@@ -0,0 +1,5 @@
1
+ export * from "./AutoBeFileUploadBox";
2
+ export * from "./AutoBeVoiceRecoderButton";
3
+ export * from "./AutoBeChatUploadBox";
4
+ export * from "./AutoBeChatUploadSendButton";
5
+ export * from "./AutoBeUploadConfig";
@@ -0,0 +1,279 @@
1
+ import {
2
+ AutoBeUserMessageAudioContent,
3
+ AutoBeUserMessageFileContent,
4
+ AutoBeUserMessageImageContent,
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<AutoBeUserMessageImageContent> => ({
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
+ ];
@@ -0,0 +1,95 @@
1
+ import { AutoBeUserMessageAudioContent } from "@autobe/interface";
2
+
3
+ import { AutoBeFileUploader } from "./AutoBeFileUploader";
4
+
5
+ export namespace AutoBeVoiceRecorder {
6
+ export const start = async (
7
+ onComplete: (content: {
8
+ file: File;
9
+ content: AutoBeUserMessageAudioContent;
10
+ }) => void,
11
+ ): Promise<MediaRecorder> => {
12
+ const stream: MediaStream = await navigator.mediaDevices.getUserMedia({
13
+ audio: true,
14
+ });
15
+ const recorder: MediaRecorder = new MediaRecorder(stream);
16
+ const chunks: Blob[] = [];
17
+
18
+ recorder.ondataavailable = (event) => {
19
+ if (event.data.size > 0) chunks.push(event.data);
20
+ };
21
+ recorder.onstop = async () => {
22
+ try {
23
+ const audioBlob = new Blob(chunks, { type: "audio/webm" });
24
+ const wavBlob = await convertToWav(audioBlob);
25
+ const audioFile = new File([wavBlob], `recording-${Date.now()}.wav`, {
26
+ type: "audio/wav",
27
+ });
28
+
29
+ const base64 = await AutoBeFileUploader.convertToBase64(audioFile);
30
+ const content: AutoBeUserMessageAudioContent = {
31
+ type: "audio",
32
+ data: base64,
33
+ format: "wav",
34
+ };
35
+ onComplete({
36
+ file: audioFile,
37
+ content,
38
+ });
39
+ } finally {
40
+ stream.getTracks().forEach((track) => track.stop());
41
+ }
42
+ };
43
+ return recorder;
44
+ };
45
+ }
46
+
47
+ const convertToWav = async (audioBlob: Blob): Promise<Blob> => {
48
+ const audioContext = new AudioContext();
49
+ const arrayBuffer = await audioBlob.arrayBuffer();
50
+ const audioBuffer = await audioContext.decodeAudioData(arrayBuffer);
51
+
52
+ // Create WAV file
53
+ const length = audioBuffer.length;
54
+ const sampleRate = audioBuffer.sampleRate;
55
+ const numberOfChannels = audioBuffer.numberOfChannels;
56
+
57
+ // Calculate WAV file size
58
+ const wavLength = 44 + length * numberOfChannels * 2;
59
+ const buffer = new ArrayBuffer(wavLength);
60
+ const view = new DataView(buffer);
61
+
62
+ // WAV file header
63
+ const writeString = (offset: number, string: string) => {
64
+ for (let i = 0; i < string.length; i++) {
65
+ view.setUint8(offset + i, string.charCodeAt(i));
66
+ }
67
+ };
68
+
69
+ writeString(0, "RIFF");
70
+ view.setUint32(4, wavLength - 8, true);
71
+ writeString(8, "WAVE");
72
+ writeString(12, "fmt ");
73
+ view.setUint32(16, 16, true); // fmt chunk size
74
+ view.setUint16(20, 1, true); // PCM format
75
+ view.setUint16(22, numberOfChannels, true);
76
+ view.setUint32(24, sampleRate, true);
77
+ view.setUint32(28, sampleRate * numberOfChannels * 2, true); // byte rate
78
+ view.setUint16(32, numberOfChannels * 2, true); // block align
79
+ view.setUint16(34, 16, true); // bits per sample
80
+ writeString(36, "data");
81
+ view.setUint32(40, length * numberOfChannels * 2, true);
82
+
83
+ // Write audio data
84
+ let offset = 44;
85
+ for (let i = 0; i < length; i++) {
86
+ for (let channel = 0; channel < numberOfChannels; channel++) {
87
+ const sample = audioBuffer.getChannelData(channel)[i]!;
88
+ const value = Math.max(-1, Math.min(1, sample));
89
+ view.setInt16(offset, value * 0x7fff, true);
90
+ offset += 2;
91
+ }
92
+ }
93
+
94
+ return new Blob([buffer], { type: "audio/wav" });
95
+ };
@@ -0,0 +1,4 @@
1
+ export * from "./AutoBeFileUploader";
2
+ export * from "./AutoBeVoiceRecorder";
3
+ export * from "./time";
4
+ export * from "./number";
@@ -0,0 +1,17 @@
1
+ export const toCompactNumberFormat = (value: number) => {
2
+ const units = [
3
+ { value: 1_000_000_000_000_000, symbol: "Q" },
4
+ { value: 1_000_000_000_000, symbol: "T" },
5
+ { value: 1_000_000_000, symbol: "B" },
6
+ { value: 1_000_000, symbol: "M" },
7
+ { value: 1_000, symbol: "K" },
8
+ ];
9
+
10
+ for (const unit of units) {
11
+ if (value >= unit.value) {
12
+ return (value / unit.value).toFixed(1) + unit.symbol;
13
+ }
14
+ }
15
+
16
+ return value.toString();
17
+ };