@aslaluroba/help-center-react 3.2.3 → 3.2.4

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/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "main": "dist/index.js",
4
4
  "module": "dist/index.esm.js",
5
5
  "types": "dist/index.d.ts",
6
- "version": "3.2.3",
6
+ "version": "3.2.4",
7
7
  "description": "BabylAI Help Center Widget for React and Next.js",
8
8
  "private": false,
9
9
  "exports": {
package/src/core/api.ts CHANGED
@@ -238,7 +238,6 @@ export async function presignUpload(
238
238
 
239
239
  return await response.json();
240
240
  }
241
-
242
241
  export async function presignDownload(fileId: string, language: string): Promise<PresignDownloadResponse> {
243
242
  try {
244
243
  const response = await apiRequest(`NewFile/${fileId}/presign-download`, 'GET', null, {
@@ -30,6 +30,7 @@ const ChatWindowFooter: React.FC<ChatWindowFooterProps> = (props) => {
30
30
  const fileInputRef = useRef<HTMLInputElement>(null);
31
31
  const [selectedFiles, setSelectedFiles] = useState<SelectedFileDto[]>([]);
32
32
  const [previewImage, setPreviewImage] = useState<string | null>(null);
33
+ const [isSending, setIsSending] = useState(false);
33
34
 
34
35
  const handleAttachClick = useCallback(() => {
35
36
  fileInputRef.current?.click();
@@ -74,119 +75,122 @@ const ChatWindowFooter: React.FC<ChatWindowFooterProps> = (props) => {
74
75
 
75
76
  const handleSendMessageWithAttachments = useCallback(async () => {
76
77
  // Prevent sending if already loading
77
- if (props.isLoading) {
78
+ if (props.isLoading || isSending) {
78
79
  return;
79
80
  }
80
81
 
81
- // Get files that need to be uploaded (those without uploadedId)
82
- const filesToUpload = selectedFiles.filter((f) => f.uploadedId === null && !f.error);
83
- const alreadyUploadedIds = selectedFiles.filter((f) => f.uploadedId !== null).map((f) => f.uploadedId as string);
84
-
85
- // Declare uploadedIds outside the if block so it's accessible later
86
- let uploadedIds: string[] = [];
87
-
88
- // If there are files to upload, upload them first
89
- if (filesToUpload.length > 0) {
90
- // Get session ID - only use existing, never create new one
91
- let sessionId: string | null = null;
92
- try {
93
- // Only use existing sessionId, never call onEnsureSession
94
- if (props.sessionId) {
95
- sessionId = props.sessionId;
96
- } else {
82
+ setIsSending(true);
83
+
84
+ try {
85
+ // Get files that need to be uploaded (those without uploadedId)
86
+ const filesToUpload = selectedFiles.filter((f) => f.uploadedId === null && !f.error);
87
+ const alreadyUploadedIds = selectedFiles.filter((f) => f.uploadedId !== null).map((f) => f.uploadedId as string);
88
+
89
+ // Declare uploadedIds outside the if block so it's accessible later
90
+ let uploadedIds: string[] = [];
91
+
92
+ // If there are files to upload, upload them first
93
+ if (filesToUpload.length > 0) {
94
+ // Get session ID - ensure session exists if needed (for image-only messages)
95
+ let sessionId: string | null = null;
96
+ try {
97
+ // Use existing sessionId if available, otherwise ensure session is created
98
+ if (props.sessionId) {
99
+ sessionId = props.sessionId;
100
+ } else {
101
+ // Ensure session exists before uploading files (allows starting chat with image only)
102
+ sessionId = await props.onEnsureSession();
103
+ }
104
+ } catch (error) {
105
+ console.error('[ChatWindowFooter] Failed to get sessionId for file upload:', error);
97
106
  // Mark all files as error
98
107
  setSelectedFiles((prev) =>
99
108
  prev.map((f) =>
100
109
  filesToUpload.some((ftl) => ftl.previewUrl === f.previewUrl)
101
- ? { ...f, error: 'No session available', uploading: false }
110
+ ? { ...f, error: 'Failed to initialize session', uploading: false }
102
111
  : f
103
112
  )
104
113
  );
114
+ setIsSending(false);
105
115
  return;
106
116
  }
107
- } catch (error) {
108
- console.error('[ChatWindowFooter] Failed to get sessionId for file upload:', error);
109
- // Mark all files as error
110
- setSelectedFiles((prev) =>
111
- prev.map((f) =>
112
- filesToUpload.some((ftl) => ftl.previewUrl === f.previewUrl)
113
- ? { ...f, error: 'Failed to initialize session', uploading: false }
114
- : f
115
- )
116
- );
117
- return;
118
- }
119
-
120
- // Upload each file and collect uploaded IDs
121
- uploadedIds = [];
122
- let hasUploadErrors = false;
123
117
 
124
- for (const fileDto of filesToUpload) {
125
- try {
126
- // Mark as uploading
127
- setSelectedFiles((prev) =>
128
- prev.map((f) => (f.previewUrl === fileDto.previewUrl ? { ...f, uploading: true, error: null } : f))
129
- );
130
-
131
- // Get presigned URL
132
- const presignResponse = await presignUpload(sessionId, fileDto.file, i18n.language);
133
-
134
- // Upload file to presigned URL using axios
135
- const uploadResponse = await axios.put(presignResponse.uploadUrl, fileDto.file, {
136
- headers: {
137
- 'Content-Type': fileDto.file.type,
138
- },
139
- onUploadProgress: () => {
140
- // Upload progress tracking (silent)
141
- },
142
- });
143
-
144
- if (uploadResponse.status !== 200 && uploadResponse.status !== 204) {
145
- throw new Error(`Upload failed with status ${uploadResponse.status}`);
118
+ // Upload each file and collect uploaded IDs
119
+ uploadedIds = [];
120
+ let hasUploadErrors = false;
121
+
122
+ for (const fileDto of filesToUpload) {
123
+ try {
124
+ // Mark as uploading
125
+ setSelectedFiles((prev) =>
126
+ prev.map((f) => (f.previewUrl === fileDto.previewUrl ? { ...f, uploading: true, error: null } : f))
127
+ );
128
+
129
+ // Get presigned URL
130
+ const presignResponse = await presignUpload(sessionId, fileDto.file, i18n.language);
131
+
132
+ // Upload file to presigned URL using axios
133
+ const uploadResponse = await axios.put(presignResponse.uploadUrl, fileDto.file, {
134
+ headers: {
135
+ 'Content-Type': fileDto.file.type,
136
+ },
137
+ onUploadProgress: () => {
138
+ // Upload progress tracking (silent)
139
+ },
140
+ });
141
+
142
+ if (uploadResponse.status !== 200 && uploadResponse.status !== 204) {
143
+ throw new Error(`Upload failed with status ${uploadResponse.status}`);
144
+ }
145
+
146
+ // Collect uploaded ID
147
+ uploadedIds.push(presignResponse.id);
148
+
149
+ // Update with uploaded ID
150
+ setSelectedFiles((prev) =>
151
+ prev.map((f) =>
152
+ f.previewUrl === fileDto.previewUrl
153
+ ? { ...f, uploading: false, uploadedId: presignResponse.id, error: null }
154
+ : f
155
+ )
156
+ );
157
+ } catch (error) {
158
+ console.error('[ChatWindowFooter] File upload failed:', error);
159
+ hasUploadErrors = true;
160
+ setSelectedFiles((prev) =>
161
+ prev.map((f) =>
162
+ f.previewUrl === fileDto.previewUrl
163
+ ? { ...f, uploading: false, error: 'Upload failed', uploadedId: null }
164
+ : f
165
+ )
166
+ );
146
167
  }
147
-
148
- // Collect uploaded ID
149
- uploadedIds.push(presignResponse.id);
150
-
151
- // Update with uploaded ID
152
- setSelectedFiles((prev) =>
153
- prev.map((f) =>
154
- f.previewUrl === fileDto.previewUrl
155
- ? { ...f, uploading: false, uploadedId: presignResponse.id, error: null }
156
- : f
157
- )
158
- );
159
- } catch (error) {
160
- console.error('[ChatWindowFooter] File upload failed:', error);
161
- hasUploadErrors = true;
162
- setSelectedFiles((prev) =>
163
- prev.map((f) =>
164
- f.previewUrl === fileDto.previewUrl
165
- ? { ...f, uploading: false, error: 'Upload failed', uploadedId: null }
166
- : f
167
- )
168
- );
169
168
  }
170
- }
171
169
 
172
- // If any uploads failed, don't send the message
173
- if (hasUploadErrors) {
174
- console.error('[ChatWindowFooter] Some files failed to upload, not sending message');
175
- return;
170
+ // If any uploads failed, don't send the message
171
+ if (hasUploadErrors) {
172
+ console.error('[ChatWindowFooter] Some files failed to upload, not sending message');
173
+ setIsSending(false);
174
+ return;
175
+ }
176
176
  }
177
- }
178
177
 
179
- // Get all successfully uploaded file IDs (already uploaded + newly uploaded)
180
- // Use uploadedIds from the upload loop instead of reading from state
181
- const allAttachmentIds = [...alreadyUploadedIds, ...uploadedIds];
178
+ // Get all successfully uploaded file IDs (already uploaded + newly uploaded)
179
+ // Use uploadedIds from the upload loop instead of reading from state
180
+ const allAttachmentIds = [...alreadyUploadedIds, ...uploadedIds];
182
181
 
183
- // Call the original send message with attachment IDs
184
- props.handleSendMessage(allAttachmentIds);
182
+ // Call the original send message with attachment IDs
183
+ props.handleSendMessage(allAttachmentIds);
185
184
 
186
- // Clear selected files and revoke URLs
187
- selectedFiles.forEach((f) => URL.revokeObjectURL(f.previewUrl));
188
- setSelectedFiles([]);
189
- }, [selectedFiles, props, i18n.language]);
185
+ // Clear selected files and revoke URLs
186
+ selectedFiles.forEach((f) => URL.revokeObjectURL(f.previewUrl));
187
+ setSelectedFiles([]);
188
+ setIsSending(false);
189
+ } catch (error) {
190
+ console.error('[ChatWindowFooter] Error sending message:', error);
191
+ setIsSending(false);
192
+ }
193
+ }, [selectedFiles, props, i18n.language, isSending]);
190
194
 
191
195
  // Check if any files are currently uploading
192
196
  const hasUploadingFiles = selectedFiles.some((f) => f.uploading);
@@ -196,7 +200,8 @@ const ChatWindowFooter: React.FC<ChatWindowFooterProps> = (props) => {
196
200
 
197
201
  // Allow sending if there's text OR files selected (files will be uploaded on send)
198
202
  const hasContentToSend = props.inputMessage.trim() !== '' || selectedFiles.length > 0;
199
- const isSendDisabled = props.isLoading || !hasContentToSend || hasUploadingFiles || hasFileErrors;
203
+ const isSendDisabled = props.isLoading || isSending || !hasContentToSend || hasUploadingFiles || hasFileErrors;
204
+ const showLoading = props.isLoading || isSending || hasUploadingFiles;
200
205
 
201
206
  const handleKeyDown = useCallback(
202
207
  (e: React.KeyboardEvent) => {
@@ -279,10 +284,16 @@ const ChatWindowFooter: React.FC<ChatWindowFooterProps> = (props) => {
279
284
  size='icon'
280
285
  onClick={handleSendMessageWithAttachments}
281
286
  disabled={isSendDisabled}
282
- className='babylai-rounded-full babylai-bg-primary-500 babylai-hover:babylai-bg-purple-600 babylai-w-8 babylai-h-8 disabled:babylai-opacity-50'
287
+ className='babylai-rounded-full babylai-bg-primary-500 babylai-hover:babylai-bg-purple-600 babylai-w-8 babylai-h-8 !babylai-p-0 babylai-flex babylai-items-center babylai-justify-center disabled:babylai-opacity-50'
283
288
  type='button'
284
289
  >
285
- <EnvelopeIcon className={`babylai-w-4 babylai-h-4 ${dir === 'rtl' ? 'babylai-rotate-270' : ''}`} />
290
+ {showLoading ? (
291
+ <div className='babylai-inline-block babylai-animate-spin babylai-rounded-full babylai-h-4 babylai-w-4 babylai-aspect-square babylai-border-2 babylai-border-white babylai-border-t-transparent babylai-box-border'></div>
292
+ ) : (
293
+ <EnvelopeIcon
294
+ className={`babylai-w-4 babylai-h-4 babylai-flex-shrink-0 ${dir === 'rtl' ? 'babylai-rotate-270' : ''}`}
295
+ />
296
+ )}
286
297
  </Button>
287
298
  </div>
288
299
 
@@ -44,7 +44,7 @@ const MessageComponent = React.memo(
44
44
  const handleImageClick = useCallback(
45
45
  (clickedIndex: number) => {
46
46
  // Use attachmentUrls if available (from Ably), otherwise use attachmentIds (user-sent)
47
- const attachments = message.attachmentUrls || message.attachmentIds || [];
47
+ const attachments = message.attachmentUrls || [];
48
48
  if (attachments.length > 0) {
49
49
  onImageClick(attachments, clickedIndex);
50
50
  }