@aslaluroba/help-center-react 3.0.20 → 3.2.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/src/core/api.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { TokenResponse } from '../lib/types';
1
+ import { TokenResponse, PresignUploadRequestDto, PresignUploadResponse, PresignDownloadResponse } from '../lib/types';
2
2
 
3
3
  let getTokenFunction: (() => Promise<TokenResponse>) | undefined = undefined;
4
4
  let baseUrl: string | null = null;
@@ -35,7 +35,6 @@ export async function getValidToken(forceRefresh = false): Promise<string> {
35
35
  localStorage.setItem('chatbot-token', storedToken);
36
36
  localStorage.setItem('chatbot-token-expiry', storedExpiry);
37
37
  } catch (error) {
38
- console.error('Failed to refresh token:', error);
39
38
  throw error;
40
39
  }
41
40
  }
@@ -75,7 +74,6 @@ async function fetchWithAuth(url: string, options: RequestInit, retry = true): P
75
74
 
76
75
  // Handle 401/403 with token refresh
77
76
  if ((response.status === 401 || response.status === 403) && retry) {
78
- console.warn('Token expired, refreshing...');
79
77
  const newToken = await getValidToken(true);
80
78
  headers.set('Authorization', `Bearer ${newToken}`);
81
79
  options.headers = headers;
@@ -94,7 +92,6 @@ async function fetchWithAuth(url: string, options: RequestInit, retry = true): P
94
92
  throw error;
95
93
  }
96
94
  } catch (error) {
97
- console.error('Fetch error:', error);
98
95
  throw error;
99
96
  }
100
97
  }
@@ -139,7 +136,7 @@ export async function apiRequest(
139
136
  endpoint: string,
140
137
  method = 'GET',
141
138
  body: any = null,
142
- options: { cache?: boolean; timeout?: number } = {}
139
+ options: { cache?: boolean; timeout?: number; language: string }
143
140
  ) {
144
141
  if (!baseUrl) throw new Error('API not initialized');
145
142
 
@@ -165,6 +162,7 @@ export async function apiRequest(
165
162
  headers: {
166
163
  'Content-Type': 'application/json',
167
164
  'Cache-Control': method === 'GET' ? 'max-age=30' : 'no-cache',
165
+ 'Accept-Language': options.language,
168
166
  },
169
167
  body: body ? JSON.stringify(body) : null,
170
168
  };
@@ -177,25 +175,36 @@ export async function apiRequest(
177
175
  let errorMessage = 'API request failed';
178
176
 
179
177
  try {
180
- const errorData = await response.json();
181
- errorMessage = errorData.message || errorMessage;
182
- } catch {
183
- errorMessage = `HTTP ${response.status}: ${response.statusText}`;
178
+ // Clone response before reading to avoid consuming the body
179
+ const errorResponse = response.clone();
180
+ const errorData = await errorResponse.json();
181
+ errorMessage = errorData.message || errorData.error || errorMessage;
182
+ } catch (parseError) {
183
+ // If JSON parsing fails, try to get text
184
+ try {
185
+ const errorResponse = response.clone();
186
+ const errorText = await errorResponse.text();
187
+ errorMessage = errorText || `HTTP ${response.status}: ${response.statusText}`;
188
+ } catch {
189
+ errorMessage = `HTTP ${response.status}: ${response.statusText}`;
190
+ }
184
191
  }
185
192
 
186
193
  throw new Error(errorMessage);
187
194
  }
188
195
 
189
196
  // Cache successful GET responses
197
+ // Note: We clone before caching to avoid consuming the original response body
190
198
  if (method === 'GET' && options.cache !== false) {
191
- const responseData = await response.clone();
199
+ const responseData = response.clone();
192
200
  const data = await responseData.json();
193
201
  setCachedResponse(requestKey, { json: () => Promise.resolve(data) });
194
202
  }
195
203
 
204
+ // Return the original response - it's body hasn't been consumed yet
205
+ // (we only cloned for caching, and the clone was consumed)
196
206
  return response;
197
207
  } catch (error) {
198
- console.error(`API request failed for ${endpoint}:`, error);
199
208
  throw error;
200
209
  }
201
210
  })();
@@ -205,3 +214,64 @@ export async function apiRequest(
205
214
 
206
215
  return requestPromise;
207
216
  }
217
+
218
+ export async function presignUpload(
219
+ chatSessionId: string,
220
+ file: File,
221
+ language: string
222
+ ): Promise<PresignUploadResponse> {
223
+ const requestBody: PresignUploadRequestDto = {
224
+ name: file.name,
225
+ contentType: file.type,
226
+ sizeBytes: file.size,
227
+ pathData: {
228
+ type: 1,
229
+ chatSessionId: chatSessionId,
230
+ },
231
+ };
232
+
233
+ const response = await apiRequest('NewFile/presign-upload', 'POST', requestBody, { language });
234
+
235
+ if (!response.ok) {
236
+ throw new Error('Failed to get presigned upload URL');
237
+ }
238
+
239
+ return await response.json();
240
+ }
241
+
242
+ export async function presignDownload(fileId: string, language: string): Promise<PresignDownloadResponse> {
243
+ try {
244
+ const response = await apiRequest(`NewFile/${fileId}/presign-download`, 'GET', null, {
245
+ language,
246
+ cache: false, // Don't cache presigned URLs as they have expiration times
247
+ });
248
+
249
+ // If response is not ok, apiRequest would have thrown an error already
250
+ // So we can safely read the JSON here
251
+ try {
252
+ return await response.json();
253
+ } catch (jsonError) {
254
+ // If JSON parsing fails, the response body might have been consumed
255
+ // or the response might not be valid JSON
256
+ if (jsonError instanceof Error) {
257
+ if (jsonError.message.includes('already read')) {
258
+ throw new Error(`Failed to parse response for file ${fileId}: Response body was already consumed`);
259
+ }
260
+ throw new Error(`Failed to parse response for file ${fileId}: ${jsonError.message}`);
261
+ }
262
+ throw new Error(`Failed to parse response for file ${fileId}`);
263
+ }
264
+ } catch (error) {
265
+ // Handle all types of errors
266
+ if (error instanceof Error) {
267
+ // If it's already a descriptive error from apiRequest, re-throw it
268
+ if (error.message.includes('API request failed') || error.message.includes('HTTP')) {
269
+ throw new Error(`Failed to get presigned download URL for file ${fileId}: ${error.message}`);
270
+ }
271
+ // Otherwise, re-throw with context
272
+ throw error;
273
+ }
274
+ // Handle non-Error types
275
+ throw new Error(`Failed to get presigned download URL for file ${fileId}: ${String(error)}`);
276
+ }
277
+ }
package/src/lib/types.ts CHANGED
@@ -1,128 +1,154 @@
1
1
  export interface HelpCenterProps extends React.HTMLAttributes<HTMLDivElement> {
2
- helpScreenId: string
3
- primaryColor?: string
4
- secondaryColor?: string
5
- logoUrl?: string
2
+ helpScreenId: string;
3
+ primaryColor?: string;
4
+ secondaryColor?: string;
5
+ logoUrl?: string;
6
6
  }
7
7
 
8
8
  export interface Theme {
9
- primary: string
10
- secondary: string
11
- text: string
12
- background: string
13
- error: string
14
- success: string
9
+ primary: string;
10
+ secondary: string;
11
+ text: string;
12
+ background: string;
13
+ error: string;
14
+ success: string;
15
15
  }
16
16
 
17
17
  export interface UserData {
18
- id: string
19
- name: string
20
- email: string
21
- avatar?: string
18
+ id: string;
19
+ name: string;
20
+ email: string;
21
+ avatar?: string;
22
22
  }
23
23
 
24
24
  export interface Message {
25
- id: number
26
- senderType: number // 1: Customer, 2: Agent, 3: AI
27
- messageContent: string
28
- sentAt: Date
29
- isSeen: boolean
25
+ id: number;
26
+ senderType: number; // 1: Customer, 2: Agent, 3: AI
27
+ messageContent: string;
28
+ sentAt: Date;
29
+ isSeen: boolean;
30
+ attachmentIds?: string[];
30
31
  }
31
32
 
32
33
  export interface ChatSession {
33
- id: string
34
- messages: Message[]
35
- participants: UserData[]
36
- status: 'active' | 'closed'
37
- createdAt: Date
38
- updatedAt: Date
34
+ id: string;
35
+ messages: Message[];
36
+ participants: UserData[];
37
+ status: 'active' | 'closed';
38
+ createdAt: Date;
39
+ updatedAt: Date;
39
40
  }
40
41
 
41
42
  export interface ChatSessionData {
42
- helpScreenId: string
43
- user: UserData
44
- initialMessage?: string
43
+ helpScreenId: string;
44
+ user: UserData;
45
+ initialMessage?: string;
45
46
  }
46
47
 
47
48
  export interface HelpCenterConfig {
48
- baseUrl: string
49
- getToken: () => Promise<{ token: string; expiresIn: number }>
50
- helpScreenId: string
51
- user?: UserData
52
- theme?: Partial<Theme>
53
- onMessageReceived?: (message: Message) => void
54
- onSessionClosed?: () => void
55
- onError?: (error: Error) => void
49
+ baseUrl: string;
50
+ getToken: () => Promise<{ token: string; expiresIn: number }>;
51
+ helpScreenId: string;
52
+ user?: UserData;
53
+ theme?: Partial<Theme>;
54
+ onMessageReceived?: (message: Message) => void;
55
+ onSessionClosed?: () => void;
56
+ onError?: (error: Error) => void;
56
57
  }
57
58
 
58
59
  export interface HelpScreenOption {
59
- id: string
60
- title: string
61
- paragraphs?: string[]
62
- nestedOptions?: HelpScreenOption[]
63
- chatWithUs?: boolean
60
+ id: string;
61
+ title: string;
62
+ paragraphs?: string[];
63
+ nestedOptions?: HelpScreenOption[];
64
+ chatWithUs?: boolean;
64
65
  }
65
66
 
66
67
  export interface HelpScreen {
67
- id: string
68
- title: string
69
- options: HelpScreenOption[]
68
+ id: string;
69
+ title: string;
70
+ options: HelpScreenOption[];
70
71
  }
71
72
 
72
73
  export interface ApiResponse<T> {
73
- data: T
74
- success: boolean
75
- error?: string
74
+ data: T;
75
+ success: boolean;
76
+ error?: string;
76
77
  }
77
78
 
78
79
  export interface TokenResponse {
79
- token: string
80
- expiresIn: number
80
+ token: string;
81
+ expiresIn: number;
81
82
  }
82
83
 
83
84
  export interface HelpScreenData {
84
- id: string
85
- tenantId: string
85
+ id: string;
86
+ tenantId: string;
86
87
  tenant: {
87
- id: string
88
- name: string
89
- key: string
90
- }
91
- title: string
92
- description: string
93
- options: Option[]
94
- chatWithUs: boolean
88
+ id: string;
89
+ name: string;
90
+ key: string;
91
+ };
92
+ title: string;
93
+ description: string;
94
+ options: Option[];
95
+ chatWithUs: boolean;
95
96
  }
96
97
 
97
98
  export interface Option {
98
- id: string
99
- helpScreenId: string
100
- parentOptionId: string | null
101
- parentOption: Option | null
102
- files: any[]
103
- nestedOptions: Option[]
104
- title: string
105
- paragraphs: string[]
106
- chatWithUs: boolean
107
- assistantId?: string
99
+ id: string;
100
+ helpScreenId: string;
101
+ parentOptionId: string | null;
102
+ parentOption: Option | null;
103
+ files: any[];
104
+ nestedOptions: Option[];
105
+ title: string;
106
+ paragraphs: string[];
107
+ chatWithUs: boolean;
108
+ assistantId?: string;
108
109
  assistant?: {
109
- id: string
110
- tenantId: string
110
+ id: string;
111
+ tenantId: string;
111
112
  tenant: {
112
- id: string
113
- name: string
114
- key: string
115
- }
116
- name: string
117
- openAIAssistantId: string
118
- greeting: string
119
- closing: string
120
- }
121
- hasNestedOptions: boolean
122
- order: number
113
+ id: string;
114
+ name: string;
115
+ key: string;
116
+ };
117
+ name: string;
118
+ openAIAssistantId: string;
119
+ greeting: string;
120
+ closing: string;
121
+ };
122
+ hasNestedOptions: boolean;
123
+ order: number;
123
124
  }
124
125
 
125
126
  export interface ReviewProps {
126
- comment: string
127
- rating: number
127
+ comment: string;
128
+ rating: number;
129
+ }
130
+
131
+ export interface PresignUploadRequestDto {
132
+ name: string;
133
+ contentType: string;
134
+ sizeBytes: number;
135
+ pathData: {
136
+ type: number;
137
+ chatSessionId: string;
138
+ };
139
+ }
140
+
141
+ export interface PresignUploadResponse {
142
+ id: string;
143
+ uploadUrl: string;
144
+ path: string;
145
+ expiresAt: string;
146
+ }
147
+
148
+ export interface PresignDownloadResponse {
149
+ id: string;
150
+ name: string;
151
+ downloadUrl: string;
152
+ contentType: string;
153
+ expiresAt: string;
128
154
  }
@@ -1,37 +1,274 @@
1
+ import React, { useRef, useState, useCallback } from 'react';
2
+ import axios from 'axios';
1
3
  import { Button } from '@/components';
4
+ import { ImagePreviewDialog } from '@/components/ui';
2
5
  import EnvelopeIcon from './../../../assets/icons/envelope.svg';
6
+ import PaperclipIcon from './../../../assets/icons/paperclip.svg';
7
+ import XIcon from './../../../assets/icons/x.svg';
3
8
  import { useLocalTranslation } from '../../../useLocalTranslation';
9
+ import { presignUpload } from '@/core/api';
10
+
11
+ interface SelectedFileDto {
12
+ file: File;
13
+ previewUrl: string;
14
+ uploading: boolean;
15
+ uploadedId: string | null;
16
+ error: string | null;
17
+ }
4
18
 
5
19
  interface ChatWindowFooterProps {
6
20
  inputMessage: string;
7
21
  setInputMessage: (e: string) => void;
8
- handleKeyDown: (e: React.KeyboardEvent) => void;
9
- handleSendMessage: () => void;
22
+ handleSendMessage: (attachmentIds: string[]) => void;
10
23
  isLoading: boolean;
24
+ onEnsureSession: () => Promise<string>;
11
25
  }
12
26
 
13
27
  const ChatWindowFooter: React.FC<ChatWindowFooterProps> = (props) => {
14
- const { t, dir } = useLocalTranslation();
15
-
28
+ const { t, dir, i18n } = useLocalTranslation();
29
+ const fileInputRef = useRef<HTMLInputElement>(null);
30
+ const [selectedFiles, setSelectedFiles] = useState<SelectedFileDto[]>([]);
31
+ const [previewImage, setPreviewImage] = useState<string | null>(null);
32
+
33
+ const handleAttachClick = useCallback(() => {
34
+ fileInputRef.current?.click();
35
+ }, []);
36
+
37
+ const handleFileSelect = useCallback(async (e: React.ChangeEvent<HTMLInputElement>) => {
38
+ const files = Array.from(e.target.files || []);
39
+
40
+ // Validate that all files are images
41
+ const imageFiles = files.filter((file) => file.type.startsWith('image/'));
42
+
43
+ // Only image files are allowed
44
+
45
+ // Create preview URLs and add to selected files
46
+ const newFiles: SelectedFileDto[] = imageFiles.map((file) => ({
47
+ file,
48
+ previewUrl: URL.createObjectURL(file),
49
+ uploading: false,
50
+ uploadedId: null,
51
+ error: null,
52
+ }));
53
+
54
+ setSelectedFiles((prev) => [...prev, ...newFiles]);
55
+
56
+ // Clear the input
57
+ if (fileInputRef.current) {
58
+ fileInputRef.current.value = '';
59
+ }
60
+
61
+ // Start uploading immediately
62
+ await handleUploadFiles(newFiles);
63
+ }, []);
64
+
65
+ const handleUploadFiles = useCallback(
66
+ async (filesToUpload: SelectedFileDto[]) => {
67
+ // Get session ID
68
+ let sessionId: string;
69
+ try {
70
+ sessionId = await props.onEnsureSession();
71
+ } catch (error) {
72
+ // Mark all files as error
73
+ setSelectedFiles((prev) =>
74
+ prev.map((f) =>
75
+ filesToUpload.some((ftl) => ftl.previewUrl === f.previewUrl)
76
+ ? { ...f, error: 'Failed to initialize session', uploading: false }
77
+ : f
78
+ )
79
+ );
80
+ return;
81
+ }
82
+
83
+ // Upload each file
84
+ for (const fileDto of filesToUpload) {
85
+ try {
86
+ // Mark as uploading
87
+ setSelectedFiles((prev) =>
88
+ prev.map((f) => (f.previewUrl === fileDto.previewUrl ? { ...f, uploading: true, error: null } : f))
89
+ );
90
+
91
+ // Get presigned URL
92
+ const presignResponse = await presignUpload(sessionId, fileDto.file, i18n.language);
93
+
94
+ // Upload file to presigned URL using axios
95
+ // Important: Content-Type must match the file type (e.g., 'image/png'), not 'multipart/form-data'
96
+ const uploadResponse = await axios.put(presignResponse.uploadUrl, fileDto.file, {
97
+ headers: {
98
+ 'Content-Type': fileDto.file.type,
99
+ },
100
+ onUploadProgress: () => {
101
+ // Upload progress tracking (silent)
102
+ },
103
+ });
104
+
105
+ if (uploadResponse.status !== 200 && uploadResponse.status !== 204) {
106
+ throw new Error(`Upload failed with status ${uploadResponse.status}`);
107
+ }
108
+
109
+ // Update with uploaded ID
110
+ setSelectedFiles((prev) =>
111
+ prev.map((f) =>
112
+ f.previewUrl === fileDto.previewUrl
113
+ ? { ...f, uploading: false, uploadedId: presignResponse.id, error: null }
114
+ : f
115
+ )
116
+ );
117
+ } catch (error) {
118
+ setSelectedFiles((prev) =>
119
+ prev.map((f) =>
120
+ f.previewUrl === fileDto.previewUrl
121
+ ? { ...f, uploading: false, error: 'Upload failed', uploadedId: null }
122
+ : f
123
+ )
124
+ );
125
+ }
126
+ }
127
+ },
128
+ [props.onEnsureSession, i18n.language]
129
+ );
130
+
131
+ const handleRemoveFile = useCallback((previewUrl: string) => {
132
+ setSelectedFiles((prev) => {
133
+ const fileToRemove = prev.find((f) => f.previewUrl === previewUrl);
134
+ if (fileToRemove) {
135
+ URL.revokeObjectURL(fileToRemove.previewUrl);
136
+ }
137
+ return prev.filter((f) => f.previewUrl !== previewUrl);
138
+ });
139
+ }, []);
140
+
141
+ const handleSendMessageWithAttachments = useCallback(() => {
142
+ // Only allow sending if all files have finished uploading (either successfully or with error)
143
+ const hasUploadingFiles = selectedFiles.some((f) => f.uploading);
144
+ if (hasUploadingFiles) {
145
+ return; // Prevent sending if any files are still uploading
146
+ }
147
+
148
+ // Get all successfully uploaded file IDs
149
+ const attachmentIds = selectedFiles.filter((f) => f.uploadedId !== null).map((f) => f.uploadedId as string);
150
+
151
+ // Call the original send message with attachment IDs
152
+ props.handleSendMessage(attachmentIds);
153
+
154
+ // Clear selected files and revoke URLs
155
+ selectedFiles.forEach((f) => URL.revokeObjectURL(f.previewUrl));
156
+ setSelectedFiles([]);
157
+ }, [selectedFiles, props]);
158
+
159
+ // Check if any files are currently uploading
160
+ const hasUploadingFiles = selectedFiles.some((f) => f.uploading);
161
+
162
+ // Check if there are files that haven't finished (no uploadedId, no error, not uploading)
163
+ // This shouldn't happen in normal flow, but we check for safety
164
+ const hasPendingFiles = selectedFiles.some((f) => !f.uploading && f.uploadedId === null && f.error === null);
165
+
166
+ // Check if all files have errors (no successful uploads)
167
+ const hasSuccessfulUploads = selectedFiles.some((f) => f.uploadedId !== null);
168
+ const allFilesHaveErrors =
169
+ selectedFiles.length > 0 && !hasSuccessfulUploads && !hasUploadingFiles && !hasPendingFiles;
170
+
171
+ const isSendDisabled =
172
+ props.isLoading || props.inputMessage.trim() === '' || hasUploadingFiles || hasPendingFiles || allFilesHaveErrors;
173
+
174
+ const handleKeyDown = useCallback(
175
+ (e: React.KeyboardEvent) => {
176
+ if (e.key === 'Enter' && !e.shiftKey) {
177
+ e.preventDefault();
178
+ if (!isSendDisabled) {
179
+ handleSendMessageWithAttachments();
180
+ }
181
+ }
182
+ },
183
+ [isSendDisabled, handleSendMessageWithAttachments]
184
+ );
185
+
16
186
  return (
17
- <footer className='babylai-flex babylai-items-center babylai-gap-2 babylai-relative babylai-rounded-full babylai-bg-white dark:!babylai-bg-storm-dust-900 babylai-mx-4 md:babylai-mx-6 md:babylai-py-3 md:babylai-px-4'>
18
- <input
19
- type='text'
20
- value={props.inputMessage}
21
- onChange={(e) => props.setInputMessage(e.target.value)}
22
- onKeyDown={props.handleKeyDown}
23
- placeholder={t('homeSdk.placeholder')}
24
- className='babylai-flex-1 babylai-py-2 babylai-px-4 babylai-bg-transparent babylai-outline-none babylai-text-sm dark:babylai-text-white babylai-border-none'
25
- />
26
- <Button
27
- variant='default'
28
- size='icon'
29
- onClick={props.handleSendMessage}
30
- disabled={props?.isLoading}
31
- className='babylai-rounded-full babylai-bg-primary-500 babylai-hover:babylai-bg-purple-600 babylai-w-8 babylai-h-8 disabled:babylai-opacity-50'
32
- >
33
- <EnvelopeIcon className={`babylai-w-4 babylai-h-4 ${dir === 'rtl' ? 'babylai-rotate-270' : ''}`} />
34
- </Button>
187
+ <footer className='babylai-flex babylai-flex-col babylai-gap-2 babylai-mx-4 md:babylai-mx-6'>
188
+ {selectedFiles.length > 0 && (
189
+ <div className='babylai-flex babylai-gap-2 babylai-flex-wrap babylai-p-2 babylai-bg-white dark:!babylai-bg-storm-dust-900 babylai-rounded-lg'>
190
+ {selectedFiles.map((file) => (
191
+ <div key={file.previewUrl} className='babylai-relative babylai-group'>
192
+ <img
193
+ src={file.previewUrl}
194
+ alt='Preview'
195
+ className='babylai-w-16 babylai-h-16 babylai-object-cover babylai-rounded-lg babylai-border babylai-border-black-white-200 babylai-cursor-pointer hover:babylai-opacity-80 babylai-transition-opacity'
196
+ onClick={() => setPreviewImage(file.previewUrl)}
197
+ role='button'
198
+ aria-label='Click to preview image'
199
+ />
200
+ {file.uploading && (
201
+ <div className='babylai-absolute babylai-inset-0 babylai-flex babylai-items-center babylai-justify-center babylai-bg-black babylai-bg-opacity-50 babylai-rounded-lg'>
202
+ <div className='babylai-animate-spin babylai-rounded-full babylai-h-6 babylai-w-6 babylai-border-2 babylai-border-white babylai-border-t-transparent'></div>
203
+ </div>
204
+ )}
205
+ {file.error && (
206
+ <div className='babylai-absolute babylai-inset-0 babylai-flex babylai-items-center babylai-justify-center babylai-bg-red-500 babylai-bg-opacity-70 babylai-rounded-lg'>
207
+ <span className='babylai-text-white babylai-text-xs'>Error</span>
208
+ </div>
209
+ )}
210
+ <button
211
+ onClick={() => handleRemoveFile(file.previewUrl)}
212
+ className='babylai-absolute -babylai-top-2 -babylai-right-2 babylai-bg-red-500 babylai-text-white babylai-rounded-full babylai-w-5 babylai-h-5 babylai-flex babylai-items-center babylai-justify-center babylai-opacity-0 group-hover:babylai-opacity-100 babylai-transition-opacity'
213
+ type='button'
214
+ aria-label='Remove image'
215
+ >
216
+ <XIcon className='babylai-w-3 babylai-h-3' />
217
+ </button>
218
+ </div>
219
+ ))}
220
+ </div>
221
+ )}
222
+
223
+ <div className='babylai-flex babylai-items-center babylai-gap-2 babylai-relative babylai-rounded-full babylai-bg-white dark:!babylai-bg-storm-dust-900 babylai-py-1 babylai-px-2 md:babylai-py-3 md:babylai-px-4'>
224
+ <input
225
+ type='file'
226
+ ref={fileInputRef}
227
+ onChange={handleFileSelect}
228
+ accept='image/*'
229
+ multiple
230
+ className='babylai-hidden'
231
+ />
232
+ <Button
233
+ variant='ghost'
234
+ size='icon'
235
+ onClick={handleAttachClick}
236
+ disabled={props.isLoading}
237
+ className='babylai-rounded-full babylai-w-8 babylai-h-8 babylai-text-black-white-500 hover:babylai-text-primary-500 hover:babylai-bg-transparent'
238
+ type='button'
239
+ >
240
+ <PaperclipIcon className='babylai-w-4 babylai-h-4' />
241
+ </Button>
242
+ <input
243
+ type='text'
244
+ value={props.inputMessage}
245
+ onChange={(e) => props.setInputMessage(e.target.value)}
246
+ onKeyDown={handleKeyDown}
247
+ placeholder={t('homeSdk.placeholder')}
248
+ className='babylai-flex-1 babylai-py-2 babylai-px-2 babylai-bg-transparent babylai-outline-none babylai-text-sm dark:babylai-text-white babylai-border-none'
249
+ />
250
+ <Button
251
+ variant='default'
252
+ size='icon'
253
+ onClick={handleSendMessageWithAttachments}
254
+ disabled={isSendDisabled}
255
+ className='babylai-rounded-full babylai-bg-primary-500 babylai-hover:babylai-bg-purple-600 babylai-w-8 babylai-h-8 disabled:babylai-opacity-50'
256
+ type='button'
257
+ >
258
+ <EnvelopeIcon className={`babylai-w-4 babylai-h-4 ${dir === 'rtl' ? 'babylai-rotate-270' : ''}`} />
259
+ </Button>
260
+ </div>
261
+
262
+ {/* Image Preview Dialog */}
263
+ {previewImage && (
264
+ <ImagePreviewDialog
265
+ imageUrls={[previewImage]}
266
+ initialIndex={0}
267
+ isOpen={!!previewImage}
268
+ onClose={() => setPreviewImage(null)}
269
+ alt='Image preview'
270
+ />
271
+ )}
35
272
  </footer>
36
273
  );
37
274
  };