@aslaluroba/help-center-react 3.2.17 → 3.2.18

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 (101) hide show
  1. package/dist/components/shared/Button/button.d.ts +1 -1
  2. package/dist/components/shared/Card/card.d.ts +1 -4
  3. package/dist/components/ui/agent-response/agent-response.d.ts +2 -1
  4. package/dist/index.css +1424 -1
  5. package/dist/index.d.ts +3 -3
  6. package/dist/index.esm.js +19194 -38923
  7. package/dist/index.esm.js.map +1 -1
  8. package/dist/index.js +19198 -38927
  9. package/dist/index.js.map +1 -1
  10. package/dist/lib/LanguageContext.d.ts +1 -1
  11. package/dist/lib/custom-hooks/useAblyConnection.d.ts +25 -0
  12. package/dist/lib/custom-hooks/useActionHandler.d.ts +1 -7
  13. package/dist/lib/custom-hooks/useChatSession.d.ts +37 -0
  14. package/dist/lib/custom-hooks/useMessageQueue.d.ts +16 -0
  15. package/dist/lib/custom-hooks/useReview.d.ts +14 -0
  16. package/dist/lib/index.d.ts +1 -2
  17. package/dist/services.d.ts +9 -6
  18. package/dist/services.esm.js +1 -14348
  19. package/dist/services.esm.js.map +1 -1
  20. package/dist/services.js +19 -14344
  21. package/dist/services.js.map +1 -1
  22. package/dist/ui/chatbot-popup/chat-window-screen/footer.d.ts +1 -1
  23. package/dist/ui/chatbot-popup/chat-window-screen/in-chat-review.d.ts +1 -1
  24. package/dist/ui/chatbot-popup/chat-window-screen/index.d.ts +2 -2
  25. package/dist/ui/chatbot-popup/options-list-screen/helpscreen-list.d.ts +1 -1
  26. package/dist/ui/chatbot-popup/options-list-screen/helpscreen-option.d.ts +1 -1
  27. package/dist/ui/chatbot-popup/options-list-screen/index.d.ts +1 -1
  28. package/dist/ui/help-center.d.ts +1 -1
  29. package/dist/ui/help-popup.d.ts +4 -27
  30. package/dist/ui/review-dialog/index.d.ts +1 -1
  31. package/package.json +12 -26
  32. package/postcss.config.js +5 -0
  33. package/rollup.config.mjs +34 -0
  34. package/dist/core/AblyService.d.ts +0 -16
  35. package/dist/core/ApiService.d.ts +0 -16
  36. package/dist/core/api.d.ts +0 -10
  37. package/dist/core/token-service.d.ts +0 -10
  38. package/dist/i18n.d.ts +0 -3
  39. package/dist/lib/config.d.ts +0 -18
  40. package/dist/lib/theme-utils.d.ts +0 -10
  41. package/dist/lib/types.d.ts +0 -145
  42. package/dist/lib/utils.d.ts +0 -2
  43. package/src/assets/animatedLogo.gif +0 -0
  44. package/src/assets/logo.svg +0 -5
  45. package/src/assets/seperator.svg +0 -5
  46. package/src/components/index.ts +0 -1
  47. package/src/components/shared/Button/button.tsx +0 -38
  48. package/src/components/shared/Button/index.ts +0 -1
  49. package/src/components/shared/Card/card.tsx +0 -44
  50. package/src/components/shared/Card/index.ts +0 -1
  51. package/src/components/shared/index.ts +0 -2
  52. package/src/components/ui/agent-response/agent-response.tsx +0 -57
  53. package/src/components/ui/agent-response/doc.md +0 -88
  54. package/src/components/ui/image-attachment.tsx +0 -119
  55. package/src/components/ui/image-preview-dialog.tsx +0 -400
  56. package/src/components/ui/index.ts +0 -3
  57. package/src/core/AblyService.ts +0 -243
  58. package/src/core/ApiService.ts +0 -116
  59. package/src/core/api.ts +0 -278
  60. package/src/core/token-service.ts +0 -35
  61. package/src/globals.css +0 -268
  62. package/src/i18n.ts +0 -21
  63. package/src/index.ts +0 -19
  64. package/src/lib/LanguageContext.tsx +0 -28
  65. package/src/lib/config.ts +0 -52
  66. package/src/lib/custom-hooks/useActionHandler.ts +0 -102
  67. package/src/lib/custom-hooks/useTypewriter.ts +0 -26
  68. package/src/lib/index.ts +0 -4
  69. package/src/lib/theme-utils.ts +0 -56
  70. package/src/lib/types.ts +0 -158
  71. package/src/lib/utils.ts +0 -6
  72. package/src/locales/ar.json +0 -45
  73. package/src/locales/en.json +0 -45
  74. package/src/services.ts +0 -14
  75. package/src/types/icons.d.ts +0 -6
  76. package/src/types/svg.d.ts +0 -5
  77. package/src/types.d.ts +0 -9
  78. package/src/ui/chatbot-popup/active-chat-actions.tsx +0 -39
  79. package/src/ui/chatbot-popup/chat-window-screen/action-button.tsx +0 -37
  80. package/src/ui/chatbot-popup/chat-window-screen/footer.tsx +0 -313
  81. package/src/ui/chatbot-popup/chat-window-screen/header.tsx +0 -53
  82. package/src/ui/chatbot-popup/chat-window-screen/in-chat-review.tsx +0 -116
  83. package/src/ui/chatbot-popup/chat-window-screen/index.tsx +0 -366
  84. package/src/ui/chatbot-popup/chat-window-screen/typing-indicator.tsx +0 -31
  85. package/src/ui/chatbot-popup/error-screen/index.tsx +0 -22
  86. package/src/ui/chatbot-popup/loading-screen/index.tsx +0 -21
  87. package/src/ui/chatbot-popup/options-list-screen/company-card.tsx +0 -39
  88. package/src/ui/chatbot-popup/options-list-screen/header.tsx +0 -23
  89. package/src/ui/chatbot-popup/options-list-screen/helpscreen-intro.tsx +0 -32
  90. package/src/ui/chatbot-popup/options-list-screen/helpscreen-list.tsx +0 -57
  91. package/src/ui/chatbot-popup/options-list-screen/helpscreen-option.tsx +0 -56
  92. package/src/ui/chatbot-popup/options-list-screen/index.tsx +0 -70
  93. package/src/ui/confirmation-modal/index.tsx +0 -62
  94. package/src/ui/floating-message.tsx +0 -29
  95. package/src/ui/help-button.tsx +0 -25
  96. package/src/ui/help-center.tsx +0 -448
  97. package/src/ui/help-popup.tsx +0 -367
  98. package/src/ui/powered-by.tsx +0 -62
  99. package/src/ui/review-dialog/index.tsx +0 -149
  100. package/src/ui/review-dialog/rating.tsx +0 -79
  101. package/src/useLocalTranslation.ts +0 -15
package/src/lib/types.ts DELETED
@@ -1,158 +0,0 @@
1
- export interface HelpCenterProps extends React.HTMLAttributes<HTMLDivElement> {
2
- helpScreenId: string;
3
- primaryColor?: string;
4
- language?: 'ar' | 'en';
5
- }
6
-
7
- export interface Theme {
8
- primary: string;
9
- secondary: string;
10
- text: string;
11
- background: string;
12
- error: string;
13
- success: string;
14
- }
15
-
16
- export interface UserData {
17
- id: string;
18
- name: string;
19
- email: string;
20
- avatar?: string;
21
- }
22
-
23
- export interface Message {
24
- id: number;
25
- senderType: number; // 1: Customer, 2: Agent, 3: AI
26
- messageContent: string;
27
- sentAt: Date;
28
- isSeen: boolean;
29
- attachmentIds?: string[]; // For user-sent messages (file IDs)
30
- attachmentUrls?: string[]; // For received messages from Ably (download URLs)
31
- }
32
-
33
- export interface ChatSession {
34
- id: string;
35
- messages: Message[];
36
- participants: UserData[];
37
- status: 'active' | 'closed';
38
- createdAt: Date;
39
- updatedAt: Date;
40
- }
41
-
42
- export interface ChatSessionData {
43
- helpScreenId: string;
44
- user: UserData;
45
- initialMessage?: string;
46
- }
47
-
48
- export interface HelpCenterConfig {
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;
57
- }
58
-
59
- export interface HelpScreenOption {
60
- id: string;
61
- title: string;
62
- paragraphs?: string[];
63
- nestedOptions?: HelpScreenOption[];
64
- chatWithUs?: boolean;
65
- }
66
-
67
- export interface HelpScreen {
68
- id: string;
69
- title: string;
70
- options: HelpScreenOption[];
71
- }
72
-
73
- export interface ApiResponse<T> {
74
- data: T;
75
- success: boolean;
76
- error?: string;
77
- }
78
-
79
- export interface TokenResponse {
80
- token: string;
81
- expiresIn: number;
82
- }
83
-
84
- export interface HelpScreenData {
85
- id: string;
86
- tenantId: string;
87
- tenant: {
88
- id: string;
89
- name: string;
90
- key: string;
91
- logoUrl: string;
92
- settings: {
93
- description: string;
94
- };
95
- };
96
- title: string;
97
- description: string;
98
- options: Option[];
99
- chatWithUs: boolean;
100
- }
101
-
102
- export interface Option {
103
- id: string;
104
- helpScreenId: string;
105
- parentOptionId: string | null;
106
- parentOption: Option | null;
107
- files: any[];
108
- nestedOptions: Option[];
109
- title: string;
110
- paragraphs: string[];
111
- chatWithUs: boolean;
112
- assistantId?: string;
113
- assistant?: {
114
- id: string;
115
- tenantId: string;
116
- tenant: {
117
- id: string;
118
- name: string;
119
- key: string;
120
- };
121
- name: string;
122
- openAIAssistantId: string;
123
- greeting: string;
124
- closing: string;
125
- };
126
- hasNestedOptions: boolean;
127
- order: number;
128
- }
129
-
130
- export interface ReviewProps {
131
- comment?: string;
132
- rating: number;
133
- }
134
-
135
- export interface PresignUploadRequestDto {
136
- name: string;
137
- contentType: string;
138
- sizeBytes: number;
139
- pathData: {
140
- type: number;
141
- chatSessionId: string;
142
- };
143
- }
144
-
145
- export interface PresignUploadResponse {
146
- id: string;
147
- uploadUrl: string;
148
- path: string;
149
- expiresAt: string;
150
- }
151
-
152
- export interface PresignDownloadResponse {
153
- id: string;
154
- name: string;
155
- downloadUrl: string;
156
- contentType: string;
157
- expiresAt: string;
158
- }
package/src/lib/utils.ts DELETED
@@ -1,6 +0,0 @@
1
- import { clsx, type ClassValue } from 'clsx';
2
- import { twMerge } from 'tailwind-merge';
3
-
4
- export function cn(...inputs: ClassValue[]) {
5
- return twMerge(clsx(inputs));
6
- }
@@ -1,45 +0,0 @@
1
- {
2
- "homeSdk": {
3
- "chatTitle": "تحدث مع",
4
- "tryBabylAi": "جرّب Babyl AI مجانًا 🎉",
5
- "contactSdk": "تواصل معنا، دعنا نتحدث!",
6
- "welcomeMessage": "مرحبًا! 👋 أنا Babyl AI، هنا لمساعدتك.",
7
- "chatNow": "ابدأ المحادثة",
8
- "getstarted": "للبدء",
9
- "pickTopic": "اختر موضوعًا",
10
- "endChat": "إنهاء المحادثة",
11
- "needAssistance": "هل تحتاج مساعدة؟ لاتردد في اخباري اضغط هنا",
12
- "placeholder": "اكتب سؤالك هنا",
13
- "poweredBy": "مدعوم بواسطة",
14
-
15
- "ReviewDialog": {
16
- "title": "نحن نقدر ملاحظاتك!",
17
- "simple_title": "نود معرفة رأيك!",
18
- "simple_description": "رأيك يساعدنا على التحسين وتقديم دعم أفضل. يُرجى تقييم تجربتك مع خدمتنا وإخبارنا كيف كان أداؤنا.",
19
- "description": "رأيك يساعدنا على التحسين وتقديم دعم أفضل. يُرجى تقييم تجربتك مع خدمتنا وإخبارنا كيف كان أداؤنا.",
20
- "rating_label": "التقييم:",
21
- "comment_label": "التعليق:",
22
- "comment_placeholder": "اكتب تعليقك هنا...",
23
- "submit_button": "إرسال التقييم",
24
- "rate_button": "تقييم",
25
- "comment_error": "يجب أن يكون الحقل التعليق نصًا بحد أقصى 500 حرف.",
26
- "rating_error": "يجب أن يكون التقييم بين 1 و5.",
27
- "skip_button": "تخطي"
28
- },
29
- "InChatReview": {
30
- "title": "كيف كانت محادثتك معنا اليوم؟",
31
- "description": "نحب أن نعرف كيف كان أداؤنا - ملاحظاتك تساعدنا على جعل كل محادثة أفضل.",
32
- "follow_up": "رائع! نحن سعداء أنك استمتعت بالدردشة معنا. هل يمكنك تقييم تجربتك الإجمالية لمساعدتنا على الحفاظ على ذلك؟",
33
- "note_placeholder": "اكتب ملاحظتك",
34
- "submit_button": "تقديم"
35
- },
36
- "ConfirmationModal": {
37
- "title": "تريد المغادرة؟ 👋",
38
- "message": "لا تقلق، يمكنك العودة في أي وقت. نحن دائما هنا إذا كنت تحتاج مساعدة أو لديك أسئلة.",
39
- "confirmation_button": "إغلاق المحادثة",
40
- "cancel_button": "استمرار",
41
- "endAndStartNewChatTitle": "إنهاء وبدء محادثة جديدة",
42
- "endAndStartNewChatMessage": "هل أنت متأكد أنك تريد إنهاء هذه المحادثة وبدء محادثة جديدة؟"
43
- }
44
- }
45
- }
@@ -1,45 +0,0 @@
1
- {
2
- "homeSdk": {
3
- "chatTitle": "Chat with",
4
- "tryBabylAi": "Try Babyl AI for Free 🎉",
5
- "contactSdk": "Contact us, Let's Talk!",
6
- "welcomeMessage": "Hey there! 👋 I'm Babyl AI, here to assist you.",
7
- "chatNow": "Chat Now",
8
- "getstarted": "Get Started",
9
- "pickTopic": "Pick a Topic to",
10
- "endChat": "End Chat",
11
- "needAssistance": "Need assistance Or You want to try the Product? Click here",
12
- "placeholder": "write your message here...",
13
- "poweredBy": "Powered by",
14
-
15
- "ReviewDialog": {
16
- "title": "We’d Love Your Feedback!",
17
- "simple_title": "We'd Love Your Feedback!",
18
- "simple_description": "Your opinion helps us improve and deliver better support. Please rate your experience with our service and let us know how we did.",
19
- "description": "Your opinion helps us improve and deliver better support. Please rate your experience with our service and let us know how we did.",
20
- "rating_label": "Rating:",
21
- "comment_label": "Comment:",
22
- "comment_placeholder": "Write your comment here...",
23
- "submit_button": "Submit Review",
24
- "rate_button": "Rate",
25
- "comment_error": "The field Comment must be a string with a maximum length of 500.",
26
- "rating_error": "Rating must be between 1 and 5.",
27
- "skip_button": "Skip"
28
- },
29
- "InChatReview": {
30
- "title": "How was your conversation with us today?",
31
- "description": "We'd love to know how we did - your feedback helps us make every chat even better.",
32
- "follow_up": "Awesome! We're glad you enjoyed chatting with us. Could you rate your overall experience to help us keep it that way?",
33
- "note_placeholder": "Write Your Note",
34
- "submit_button": "Submit Review"
35
- },
36
- "ConfirmationModal": {
37
- "title": "Leaving so soon? 👋",
38
- "message": "Don't worry, you can come back anytime. We're always here if you need help or have questions.",
39
- "confirmation_button": "Close Chat",
40
- "cancel_button": "Continue",
41
- "endAndStartNewChatTitle": "End and Start New Chat",
42
- "endAndStartNewChatMessage": "Are you sure you want to end the current conversation and start a new one?"
43
- }
44
- }
45
- }
package/src/services.ts DELETED
@@ -1,14 +0,0 @@
1
- // Services export file - separate from main component exports
2
- // This prevents Fast Refresh issues with non-React components
3
-
4
- // Re-export services for backward compatibility
5
- export { ApiService } from './core/ApiService';
6
- export { ClientAblyService as AblyService } from './core/AblyService';
7
- export { TokenService } from './core/token-service';
8
-
9
- // Re-export API functions
10
- export { initializeAPI, getValidToken, apiRequest } from './core/api';
11
-
12
- // Re-export types that might be needed with services
13
- export type { TokenResponse } from './lib/types';
14
- export * from './lib/types';
@@ -1,6 +0,0 @@
1
- declare module "~icons/*" {
2
- import type { FC, SVGProps } from "react";
3
- const Icon: FC<SVGProps<SVGSVGElement> & { title?: string }>;
4
- export default Icon;
5
- }
6
-
@@ -1,5 +0,0 @@
1
- declare module "*.svg" {
2
- import type { FC, SVGProps } from "react";
3
- const SVG: FC<SVGProps<SVGSVGElement> & { title?: string }>;
4
- export default SVG;
5
- }
package/src/types.d.ts DELETED
@@ -1,9 +0,0 @@
1
- declare module '*.css' {
2
- const content: { [className: string]: string }
3
- export default content
4
- }
5
-
6
- declare module '*.gif' {
7
- const src: string
8
- export default src
9
- }
@@ -1,39 +0,0 @@
1
- import { Button } from '@/components';
2
- import { useLocalTranslation } from '@/useLocalTranslation';
3
- import React from 'react';
4
- import SolarPlain2BoldDuotone from '~icons/solar/plain-2-bold-duotone';
5
-
6
- interface ActiveChatActionsProps {
7
- onConfirm: () => void;
8
- onCancel: () => void;
9
- }
10
-
11
- export const ActiveChatActions: React.FC<ActiveChatActionsProps> = ({
12
- onConfirm,
13
- onCancel,
14
- }) => {
15
- const { t } = useLocalTranslation();
16
-
17
- return (
18
- <section className="babylai:flex babylai:justify-between babylai:gap-3 babylai:px-4 babylai:py-6 babylai:absolute babylai:left-0 babylai:right-0 babylai:bottom-11 babylai:z-20 babylai:bg-linear-to-t babylai:from-card babylai:to-transparent babylai:from-[28.32%] babylai:to-[112.59%]">
19
- <Button
20
- onClick={onConfirm}
21
- variant='secondary'
22
- >
23
- {t('homeSdk.ConfirmationModal.confirmation_button')}
24
-
25
- </Button>
26
- <Button
27
- onClick={onCancel}
28
- variant='default'
29
- >
30
- {t('homeSdk.ConfirmationModal.cancel_button')}
31
- <SolarPlain2BoldDuotone className="babylai:w-6 babylai:h-6" />
32
- </Button>
33
- </section>
34
- );
35
- };
36
-
37
- ActiveChatActions.displayName = 'ActiveChatActions';
38
-
39
- export default ActiveChatActions;
@@ -1,37 +0,0 @@
1
- import React from 'react';
2
- import { cn } from '@/lib/utils';
3
-
4
- interface ActionButtonProps {
5
- onClick: () => void;
6
- icon?: React.ReactNode;
7
- children?: React.ReactNode;
8
- ariaLabel?: string;
9
- className?: string;
10
- }
11
-
12
- const ActionButton: React.FC<ActionButtonProps> = ({
13
- onClick,
14
- icon,
15
- children,
16
- ariaLabel,
17
- className,
18
- }) => {
19
- return (
20
- <button
21
- onClick={onClick}
22
- className={cn(
23
- 'babylai:bg-card babylai:text-card-foreground babylai:h-6 babylai:w-8 babylai:rounded-full babylai:flex babylai:items-center babylai:justify-center babylai:font-bold babylai:text-base babylai:cursor-pointer babylai:border-0',
24
- className
25
- )}
26
- aria-label={ariaLabel}
27
- type="button"
28
- >
29
- {icon}
30
- {children}
31
- </button>
32
- );
33
- };
34
-
35
- ActionButton.displayName = 'ActionButton';
36
-
37
- export default ActionButton;
@@ -1,313 +0,0 @@
1
- import React, { useRef, useState, useCallback } from 'react';
2
- import axios from 'axios';
3
- import { ImagePreviewDialog } from '@/components/ui';
4
- import MaterialSymbolsCloseSmallOutlineRounded from '~icons/material-symbols/close-small-outline-rounded'
5
- import { useLocalTranslation } from '../../../useLocalTranslation';
6
- import { presignUpload } from '@/core/api';
7
- import SolarPlain2BoldDuotone from '~icons/solar/plain-2-bold-duotone'
8
- import SolarPaperclipBoldDuotone from '~icons/solar/paperclip-bold-duotone'
9
-
10
- interface SelectedFileDto {
11
- file: File;
12
- previewUrl: string;
13
- uploading: boolean;
14
- uploadedId: string | null;
15
- error: string | null;
16
- }
17
-
18
- interface ChatWindowFooterProps {
19
- inputMessage: string;
20
- setInputMessage: (e: string) => void;
21
- handleSendMessage: (attachmentIds: string[]) => void;
22
- isLoading: boolean;
23
- isChatClosed?: boolean;
24
- onEnsureSession: () => Promise<string>;
25
- sessionId?: string | null; // Pass existing sessionId to avoid creating new sessions
26
- }
27
-
28
- const ChatWindowFooter: React.FC<ChatWindowFooterProps> = (props) => {
29
- const { t, i18n } = useLocalTranslation();
30
- const fileInputRef = useRef<HTMLInputElement>(null);
31
- const [selectedFiles, setSelectedFiles] = useState<SelectedFileDto[]>([]);
32
- const [previewImage, setPreviewImage] = useState<string | null>(null);
33
- const [isSending, setIsSending] = useState(false);
34
-
35
- const handleAttachClick = useCallback(() => {
36
- fileInputRef.current?.click();
37
- }, []);
38
-
39
- const handleFileSelect = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
40
- const files = Array.from(e.target.files || []);
41
-
42
- // Validate that all files are images
43
- const imageFiles = files.filter((file) => file.type.startsWith('image/'));
44
-
45
- // Create preview URLs and add to selected files (don't upload yet)
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
- // Don't upload files immediately - wait for send button click
62
- }, []);
63
-
64
- // Removed handleUploadFiles - files are now uploaded in handleSendMessageWithAttachments
65
-
66
- const handleRemoveFile = useCallback((previewUrl: string) => {
67
- setSelectedFiles((prev) => {
68
- const fileToRemove = prev.find((f) => f.previewUrl === previewUrl);
69
- if (fileToRemove) {
70
- URL.revokeObjectURL(fileToRemove.previewUrl);
71
- }
72
- return prev.filter((f) => f.previewUrl !== previewUrl);
73
- });
74
- }, []);
75
-
76
- const handleSendMessageWithAttachments = useCallback(async () => {
77
- // Prevent sending if already loading or chat was closed from chat screen
78
- if (props.isLoading || props.isChatClosed || isSending) {
79
- return;
80
- }
81
-
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);
106
- // Mark all files as error
107
- setSelectedFiles((prev) =>
108
- prev.map((f) =>
109
- filesToUpload.some((ftl) => ftl.previewUrl === f.previewUrl)
110
- ? { ...f, error: 'Failed to initialize session', uploading: false }
111
- : f
112
- )
113
- );
114
- setIsSending(false);
115
- return;
116
- }
117
-
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 as 'ar' | 'en');
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
- );
167
- }
168
- }
169
-
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
- }
177
-
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];
181
-
182
- // Call the original send message with attachment IDs
183
- props.handleSendMessage(allAttachmentIds);
184
-
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 as 'ar' | 'en', isSending]);
194
-
195
- // Check if any files are currently uploading
196
- const hasUploadingFiles = selectedFiles.some((f) => f.uploading);
197
-
198
- // Check if there are files with errors
199
- const hasFileErrors = selectedFiles.some((f) => f.error !== null);
200
-
201
- // Allow sending if there's text OR files selected (files will be uploaded on send)
202
- const hasContentToSend = props.inputMessage.trim() !== '' || selectedFiles.length > 0;
203
- const isSendDisabled =
204
- props.isLoading || props.isChatClosed || isSending || !hasContentToSend || hasUploadingFiles || hasFileErrors;
205
- const showLoading = props.isLoading || isSending || hasUploadingFiles;
206
-
207
- const handleKeyDown = useCallback(
208
- (e: React.KeyboardEvent) => {
209
- if (e.key === 'Enter' && !e.shiftKey) {
210
- e.preventDefault();
211
- if (!isSendDisabled) {
212
- handleSendMessageWithAttachments();
213
- }
214
- }
215
- },
216
- [isSendDisabled, handleSendMessageWithAttachments]
217
- );
218
-
219
- return (
220
- <footer className='babylai:flex babylai:flex-col babylai:gap-2 babylai:mx-4 babylai:mb-2'>
221
- {selectedFiles.length > 0 && (
222
- <div className='babylai:flex babylai:gap-2 babylai:flex-wrap babylai:p-2 babylai:bg-card babylai:rounded-lg'>
223
- {selectedFiles.map((file) => (
224
- <div key={file.previewUrl} className='babylai:relative babylai:group'>
225
- <img
226
- src={file.previewUrl}
227
- alt='Preview'
228
- className='babylai:w-16 babylai:h-16 babylai:object-cover babylai:rounded-lg babylai:border babylai:border-black-white-200 babylai:cursor-pointer babylai:hover:opacity-80 babylai:transition-opacity'
229
- onClick={() => setPreviewImage(file.previewUrl)}
230
- role='button'
231
- aria-label='Click to preview image'
232
- />
233
- {file.uploading && (
234
- <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'>
235
- <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>
236
- </div>
237
- )}
238
- {file.error && (
239
- <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'>
240
- <span className='babylai:text-white babylai:text-xs'>Error</span>
241
- </div>
242
- )}
243
- <button
244
- onClick={() => handleRemoveFile(file.previewUrl)}
245
- className='babylai:border-0 babylai:p-0 babylai:absolute babylai:-top-2 babylai:-right-2 babylai:bg-destructive babylai:text-white babylai:rounded-full babylai:w-5 babylai:h-5 babylai:flex babylai:items-center babylai:justify-center babylai:cursor-pointer'
246
- type='button'
247
- aria-label='Remove image'
248
- >
249
- <MaterialSymbolsCloseSmallOutlineRounded className='babylai:w-3 babylai:h-3' />
250
- </button>
251
- </div>
252
- ))}
253
- </div>
254
- )}
255
-
256
- <div className='babylai:flex babylai:items-center babylai:gap-2 babylai:relative babylai:rounded-full babylai:bg-card babylai:py-3 babylai:px-4'>
257
- <input
258
- type='file'
259
- ref={fileInputRef}
260
- onChange={handleFileSelect}
261
- accept='image/*'
262
- multiple
263
- className='babylai:hidden'
264
- />
265
- <div className='babylai:border-e babylai:border-border babylai:pe-2'>
266
- <button
267
- onClick={handleAttachClick}
268
- disabled={props.isLoading || props.isChatClosed}
269
- className='babylai:flex babylai:items-center babylai:justify-center babylai:border-0 babylai:rounded-full babylai:w-8 babylai:h-8 babylai:cursor-pointer babylai:bg-secondary babylai:text-muted-foreground babylai:hover:text-primary-500 babylai:transition-colors babylai:disabled:opacity-50 babylai:disabled:cursor-not-allowed'
270
- type='button'
271
- aria-label='Attach image'
272
- >
273
- <SolarPaperclipBoldDuotone className='babylai:w-4 babylai:h-4' />
274
- </button>
275
- </div>
276
- <input
277
- type='text'
278
- value={props.inputMessage}
279
- onChange={(e) => props.setInputMessage(e.target.value)}
280
- onKeyDown={handleKeyDown}
281
- placeholder={t('homeSdk.placeholder')}
282
- disabled={props.isChatClosed}
283
- className='babylai:flex-1 babylai:py-2 babylai:px-2 babylai:bg-transparent babylai:outline-none babylai:text-sm babylai:border-none babylai:text-card-foreground babylai:disabled:opacity-50 babylai:disabled:cursor-not-allowed'
284
- />
285
- <button
286
- onClick={handleSendMessageWithAttachments}
287
- disabled={isSendDisabled || props.isChatClosed}
288
- className='babylai:border-0 babylai:rounded-full babylai:bg-primary-500 babylai:hover:bg-primary-600 babylai:w-8 babylai:h-8 babylai:p-0 babylai:flex babylai:items-center babylai:justify-center babylai:disabled:opacity-50 babylai:text-white babylai:cursor-pointer babylai:disabled:cursor-not-allowed'
289
- type='button'
290
- >
291
- {showLoading ? (
292
- <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>
293
- ) : (
294
- <SolarPlain2BoldDuotone className="babylai:w-4 babylai:h-4" />
295
- )}
296
- </button>
297
- </div>
298
-
299
- {/* Image Preview Dialog */}
300
- {previewImage && (
301
- <ImagePreviewDialog
302
- imageUrls={[previewImage]}
303
- initialIndex={0}
304
- isOpen={!!previewImage}
305
- onClose={() => setPreviewImage(null)}
306
- alt='Image preview'
307
- />
308
- )}
309
- </footer>
310
- );
311
- };
312
-
313
- export default ChatWindowFooter;