@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.
- package/dist/components/shared/Button/button.d.ts +1 -1
- package/dist/components/shared/Card/card.d.ts +1 -4
- package/dist/components/ui/agent-response/agent-response.d.ts +2 -1
- package/dist/index.css +1424 -1
- package/dist/index.d.ts +3 -3
- package/dist/index.esm.js +19194 -38923
- package/dist/index.esm.js.map +1 -1
- package/dist/index.js +19198 -38927
- package/dist/index.js.map +1 -1
- package/dist/lib/LanguageContext.d.ts +1 -1
- package/dist/lib/custom-hooks/useAblyConnection.d.ts +25 -0
- package/dist/lib/custom-hooks/useActionHandler.d.ts +1 -7
- package/dist/lib/custom-hooks/useChatSession.d.ts +37 -0
- package/dist/lib/custom-hooks/useMessageQueue.d.ts +16 -0
- package/dist/lib/custom-hooks/useReview.d.ts +14 -0
- package/dist/lib/index.d.ts +1 -2
- package/dist/services.d.ts +9 -6
- package/dist/services.esm.js +1 -14348
- package/dist/services.esm.js.map +1 -1
- package/dist/services.js +19 -14344
- package/dist/services.js.map +1 -1
- package/dist/ui/chatbot-popup/chat-window-screen/footer.d.ts +1 -1
- package/dist/ui/chatbot-popup/chat-window-screen/in-chat-review.d.ts +1 -1
- package/dist/ui/chatbot-popup/chat-window-screen/index.d.ts +2 -2
- package/dist/ui/chatbot-popup/options-list-screen/helpscreen-list.d.ts +1 -1
- package/dist/ui/chatbot-popup/options-list-screen/helpscreen-option.d.ts +1 -1
- package/dist/ui/chatbot-popup/options-list-screen/index.d.ts +1 -1
- package/dist/ui/help-center.d.ts +1 -1
- package/dist/ui/help-popup.d.ts +4 -27
- package/dist/ui/review-dialog/index.d.ts +1 -1
- package/package.json +12 -26
- package/postcss.config.js +5 -0
- package/rollup.config.mjs +34 -0
- package/dist/core/AblyService.d.ts +0 -16
- package/dist/core/ApiService.d.ts +0 -16
- package/dist/core/api.d.ts +0 -10
- package/dist/core/token-service.d.ts +0 -10
- package/dist/i18n.d.ts +0 -3
- package/dist/lib/config.d.ts +0 -18
- package/dist/lib/theme-utils.d.ts +0 -10
- package/dist/lib/types.d.ts +0 -145
- package/dist/lib/utils.d.ts +0 -2
- package/src/assets/animatedLogo.gif +0 -0
- package/src/assets/logo.svg +0 -5
- package/src/assets/seperator.svg +0 -5
- package/src/components/index.ts +0 -1
- package/src/components/shared/Button/button.tsx +0 -38
- package/src/components/shared/Button/index.ts +0 -1
- package/src/components/shared/Card/card.tsx +0 -44
- package/src/components/shared/Card/index.ts +0 -1
- package/src/components/shared/index.ts +0 -2
- package/src/components/ui/agent-response/agent-response.tsx +0 -57
- package/src/components/ui/agent-response/doc.md +0 -88
- package/src/components/ui/image-attachment.tsx +0 -119
- package/src/components/ui/image-preview-dialog.tsx +0 -400
- package/src/components/ui/index.ts +0 -3
- package/src/core/AblyService.ts +0 -243
- package/src/core/ApiService.ts +0 -116
- package/src/core/api.ts +0 -278
- package/src/core/token-service.ts +0 -35
- package/src/globals.css +0 -268
- package/src/i18n.ts +0 -21
- package/src/index.ts +0 -19
- package/src/lib/LanguageContext.tsx +0 -28
- package/src/lib/config.ts +0 -52
- package/src/lib/custom-hooks/useActionHandler.ts +0 -102
- package/src/lib/custom-hooks/useTypewriter.ts +0 -26
- package/src/lib/index.ts +0 -4
- package/src/lib/theme-utils.ts +0 -56
- package/src/lib/types.ts +0 -158
- package/src/lib/utils.ts +0 -6
- package/src/locales/ar.json +0 -45
- package/src/locales/en.json +0 -45
- package/src/services.ts +0 -14
- package/src/types/icons.d.ts +0 -6
- package/src/types/svg.d.ts +0 -5
- package/src/types.d.ts +0 -9
- package/src/ui/chatbot-popup/active-chat-actions.tsx +0 -39
- package/src/ui/chatbot-popup/chat-window-screen/action-button.tsx +0 -37
- package/src/ui/chatbot-popup/chat-window-screen/footer.tsx +0 -313
- package/src/ui/chatbot-popup/chat-window-screen/header.tsx +0 -53
- package/src/ui/chatbot-popup/chat-window-screen/in-chat-review.tsx +0 -116
- package/src/ui/chatbot-popup/chat-window-screen/index.tsx +0 -366
- package/src/ui/chatbot-popup/chat-window-screen/typing-indicator.tsx +0 -31
- package/src/ui/chatbot-popup/error-screen/index.tsx +0 -22
- package/src/ui/chatbot-popup/loading-screen/index.tsx +0 -21
- package/src/ui/chatbot-popup/options-list-screen/company-card.tsx +0 -39
- package/src/ui/chatbot-popup/options-list-screen/header.tsx +0 -23
- package/src/ui/chatbot-popup/options-list-screen/helpscreen-intro.tsx +0 -32
- package/src/ui/chatbot-popup/options-list-screen/helpscreen-list.tsx +0 -57
- package/src/ui/chatbot-popup/options-list-screen/helpscreen-option.tsx +0 -56
- package/src/ui/chatbot-popup/options-list-screen/index.tsx +0 -70
- package/src/ui/confirmation-modal/index.tsx +0 -62
- package/src/ui/floating-message.tsx +0 -29
- package/src/ui/help-button.tsx +0 -25
- package/src/ui/help-center.tsx +0 -448
- package/src/ui/help-popup.tsx +0 -367
- package/src/ui/powered-by.tsx +0 -62
- package/src/ui/review-dialog/index.tsx +0 -149
- package/src/ui/review-dialog/rating.tsx +0 -79
- package/src/useLocalTranslation.ts +0 -15
package/src/core/AblyService.ts
DELETED
|
@@ -1,243 +0,0 @@
|
|
|
1
|
-
import * as Ably from 'ably';
|
|
2
|
-
|
|
3
|
-
type ActionHandlerCallback = (actionType: string | undefined | null, messageData: any) => void | Promise<void>;
|
|
4
|
-
|
|
5
|
-
export class ClientAblyService {
|
|
6
|
-
private static client: Ably.Realtime | null = null;
|
|
7
|
-
private static channel: Ably.RealtimeChannel | null = null;
|
|
8
|
-
private static isConnected: boolean = false;
|
|
9
|
-
private static sessionId: string | null = null;
|
|
10
|
-
private static messageUnsubscribe: (() => void) | null = null;
|
|
11
|
-
private static onActionReceived: ActionHandlerCallback | null = null;
|
|
12
|
-
|
|
13
|
-
static async startConnection(
|
|
14
|
-
sessionId: string,
|
|
15
|
-
ablyToken: string,
|
|
16
|
-
onMessageReceived: Function,
|
|
17
|
-
tenantId: string,
|
|
18
|
-
onActionReceived?: ActionHandlerCallback
|
|
19
|
-
) {
|
|
20
|
-
// Prevent multiple connections
|
|
21
|
-
if (this.isConnected && this.sessionId === sessionId) {
|
|
22
|
-
return;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
// Close existing connection if connecting to a different session
|
|
26
|
-
if (this.isConnected && this.sessionId !== sessionId) {
|
|
27
|
-
await this.stopConnection();
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
try {
|
|
31
|
-
// Initialize Ably client with the token
|
|
32
|
-
this.client = new Ably.Realtime({
|
|
33
|
-
authUrl: undefined,
|
|
34
|
-
token: ablyToken,
|
|
35
|
-
autoConnect: true,
|
|
36
|
-
});
|
|
37
|
-
|
|
38
|
-
this.client.connection.on('failed', (stateChange) => {
|
|
39
|
-
console.error('[AblyService] Connection state: failed', {
|
|
40
|
-
reason: stateChange.reason?.message,
|
|
41
|
-
error: stateChange.reason,
|
|
42
|
-
});
|
|
43
|
-
});
|
|
44
|
-
|
|
45
|
-
// Wait for connection to be established
|
|
46
|
-
await new Promise<void>((resolve, reject) => {
|
|
47
|
-
if (!this.client) {
|
|
48
|
-
const error = new Error('Failed to initialize Ably client');
|
|
49
|
-
console.error('[AblyService]', error);
|
|
50
|
-
reject(error);
|
|
51
|
-
return;
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
this.client.connection.once('connected', () => {
|
|
55
|
-
this.isConnected = true;
|
|
56
|
-
this.sessionId = sessionId;
|
|
57
|
-
resolve();
|
|
58
|
-
});
|
|
59
|
-
|
|
60
|
-
this.client.connection.once('failed', (stateChange) => {
|
|
61
|
-
const error = new Error(`Ably connection failed: ${stateChange.reason?.message || 'Unknown error'}`);
|
|
62
|
-
console.error('[AblyService] Connection failed', {
|
|
63
|
-
reason: stateChange.reason?.message,
|
|
64
|
-
error: stateChange.reason,
|
|
65
|
-
});
|
|
66
|
-
reject(error);
|
|
67
|
-
});
|
|
68
|
-
|
|
69
|
-
this.client.connection.once('disconnected', (stateChange) => {
|
|
70
|
-
const error = new Error(`Ably connection disconnected: ${stateChange.reason?.message || 'Unknown error'}`);
|
|
71
|
-
console.error('[AblyService] Connection disconnected', { reason: stateChange.reason?.message });
|
|
72
|
-
reject(error);
|
|
73
|
-
});
|
|
74
|
-
|
|
75
|
-
// Set a timeout for connection
|
|
76
|
-
setTimeout(() => {
|
|
77
|
-
if (!this.isConnected) {
|
|
78
|
-
const error = new Error('Ably connection timeout');
|
|
79
|
-
console.error('[AblyService] Connection timeout after 10 seconds');
|
|
80
|
-
reject(error);
|
|
81
|
-
}
|
|
82
|
-
}, 10000);
|
|
83
|
-
});
|
|
84
|
-
|
|
85
|
-
// Store optional action handler for this connection
|
|
86
|
-
this.onActionReceived = onActionReceived ?? null;
|
|
87
|
-
|
|
88
|
-
// Subscribe to the session room
|
|
89
|
-
await this.joinChannel(sessionId, onMessageReceived, tenantId);
|
|
90
|
-
} catch (error) {
|
|
91
|
-
console.error('[AblyService] Error in startConnection', { error, sessionId });
|
|
92
|
-
this.isConnected = false;
|
|
93
|
-
this.sessionId = null;
|
|
94
|
-
throw error;
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
private static async joinChannel(sessionId: string, onMessageReceived: Function, tenantId: string) {
|
|
99
|
-
if (!this.client) {
|
|
100
|
-
const error = new Error('Chat client not initialized');
|
|
101
|
-
console.error('[AblyService] joinChannel error:', error);
|
|
102
|
-
throw error;
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
const roomName = `session:${tenantId}:${sessionId}`;
|
|
106
|
-
|
|
107
|
-
// Set up raw channel subscription for server messages
|
|
108
|
-
if (this.client) {
|
|
109
|
-
this.channel = this.client.channels.get(roomName);
|
|
110
|
-
|
|
111
|
-
this.channel.on('failed', (stateChange) => {
|
|
112
|
-
console.error('[AblyService] Channel failed', {
|
|
113
|
-
roomName,
|
|
114
|
-
reason: stateChange.reason?.message,
|
|
115
|
-
error: stateChange.reason,
|
|
116
|
-
});
|
|
117
|
-
});
|
|
118
|
-
|
|
119
|
-
// Subscribe to assistant/system responses
|
|
120
|
-
this.channel.subscribe('ReceiveMessage', (message) => {
|
|
121
|
-
try {
|
|
122
|
-
// Ensure messageContent is always a string (default to empty string if undefined)
|
|
123
|
-
const rawData = message.data;
|
|
124
|
-
|
|
125
|
-
const messageContent =
|
|
126
|
-
typeof rawData === 'string' ? rawData : rawData?.content ?? rawData?.message ?? '';
|
|
127
|
-
const senderType = rawData?.senderType || 3; // Assistant
|
|
128
|
-
const needsAgent = rawData?.needsAgent || rawData?.actionType == 'needs_agent' || false;
|
|
129
|
-
const attachments = rawData?.attachments || [];
|
|
130
|
-
const actionType = (rawData && typeof rawData.actionType === 'string' ? rawData.actionType : '') as
|
|
131
|
-
| string
|
|
132
|
-
| undefined
|
|
133
|
-
| null;
|
|
134
|
-
|
|
135
|
-
// Extract downloadUrl from attachments (Ably now returns downloadUrl directly)
|
|
136
|
-
// Attachments can be: strings (URLs), objects with downloadUrl, url, or id only
|
|
137
|
-
const attachmentUrls: string[] = [];
|
|
138
|
-
const attachmentIdsFromAttachments: string[] = [];
|
|
139
|
-
for (const attachment of attachments) {
|
|
140
|
-
if (typeof attachment === 'string') {
|
|
141
|
-
if (attachment.startsWith('http://') || attachment.startsWith('https://')) {
|
|
142
|
-
attachmentUrls.push(attachment);
|
|
143
|
-
} else {
|
|
144
|
-
attachmentIdsFromAttachments.push(attachment);
|
|
145
|
-
}
|
|
146
|
-
} else if (attachment?.downloadUrl) {
|
|
147
|
-
attachmentUrls.push(attachment.downloadUrl);
|
|
148
|
-
} else if (attachment?.url) {
|
|
149
|
-
attachmentUrls.push(attachment.url);
|
|
150
|
-
} else if (attachment?.id && typeof attachment.id === 'string') {
|
|
151
|
-
attachmentIdsFromAttachments.push(attachment.id);
|
|
152
|
-
}
|
|
153
|
-
}
|
|
154
|
-
const attachmentIds =
|
|
155
|
-
attachmentIdsFromAttachments.length > 0
|
|
156
|
-
? attachmentIdsFromAttachments
|
|
157
|
-
: (rawData?.attachmentIds as string[] | undefined) || [];
|
|
158
|
-
|
|
159
|
-
// Invoke optional action handler first (non-blocking for message processing)
|
|
160
|
-
if (this.onActionReceived && actionType !== undefined) {
|
|
161
|
-
try {
|
|
162
|
-
void this.onActionReceived(actionType, rawData);
|
|
163
|
-
} catch (actionError) {
|
|
164
|
-
console.error('[AblyService] Error in action handler callback', {
|
|
165
|
-
error: actionError,
|
|
166
|
-
actionType,
|
|
167
|
-
rawData,
|
|
168
|
-
});
|
|
169
|
-
}
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
onMessageReceived(messageContent, senderType, needsAgent, attachmentUrls, attachmentIds);
|
|
173
|
-
} catch (error) {
|
|
174
|
-
console.error('[AblyService] Error processing message', { error, message });
|
|
175
|
-
}
|
|
176
|
-
});
|
|
177
|
-
|
|
178
|
-
await this.channel.attach();
|
|
179
|
-
}
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
static async stopConnection() {
|
|
183
|
-
try {
|
|
184
|
-
// Unsubscribe from room messages
|
|
185
|
-
if (this.messageUnsubscribe) {
|
|
186
|
-
this.messageUnsubscribe();
|
|
187
|
-
this.messageUnsubscribe = null;
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
// Unsubscribe and detach from raw channel
|
|
191
|
-
if (this.channel) {
|
|
192
|
-
this.channel.unsubscribe();
|
|
193
|
-
await this.channel.detach();
|
|
194
|
-
this.channel = null;
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
// Close Ably connection
|
|
198
|
-
if (this.client) {
|
|
199
|
-
this.client.close();
|
|
200
|
-
this.client = null;
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
this.isConnected = false;
|
|
204
|
-
this.sessionId = null;
|
|
205
|
-
this.onActionReceived = null;
|
|
206
|
-
} catch (error) {
|
|
207
|
-
console.error('[AblyService] Error in stopConnection', { error });
|
|
208
|
-
// Reset state even if there's an error
|
|
209
|
-
this.isConnected = false;
|
|
210
|
-
this.sessionId = null;
|
|
211
|
-
this.client = null;
|
|
212
|
-
this.channel = null;
|
|
213
|
-
this.messageUnsubscribe = null;
|
|
214
|
-
}
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
static isConnectionActive(): boolean {
|
|
218
|
-
return this.isConnected && this.client?.connection.state === 'connected';
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
static getConnectionState(): string {
|
|
222
|
-
return this.client?.connection.state || 'disconnected';
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
// Method to manually send a message (if needed for debugging or direct messaging)
|
|
226
|
-
static async sendMessage(messageContent: string, senderType: number = 1) {
|
|
227
|
-
if (!this.channel || !this.isConnected) {
|
|
228
|
-
throw new Error('Connection not active');
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
try {
|
|
232
|
-
await this.channel.publish('message', {
|
|
233
|
-
text: messageContent,
|
|
234
|
-
metadata: {
|
|
235
|
-
senderType,
|
|
236
|
-
sentAt: new Date().toISOString(),
|
|
237
|
-
},
|
|
238
|
-
});
|
|
239
|
-
} catch (error) {
|
|
240
|
-
throw error;
|
|
241
|
-
}
|
|
242
|
-
}
|
|
243
|
-
}
|
package/src/core/ApiService.ts
DELETED
|
@@ -1,116 +0,0 @@
|
|
|
1
|
-
import axios, { AxiosInstance, AxiosRequestConfig } from 'axios';
|
|
2
|
-
import { HelpCenterConfig } from '../lib/types';
|
|
3
|
-
|
|
4
|
-
export class ApiService {
|
|
5
|
-
private axiosInstance: AxiosInstance;
|
|
6
|
-
private config: HelpCenterConfig;
|
|
7
|
-
private tokenExpiryTime: number = 0;
|
|
8
|
-
private currentToken: string | null = null;
|
|
9
|
-
|
|
10
|
-
constructor(config: HelpCenterConfig) {
|
|
11
|
-
this.config = config;
|
|
12
|
-
this.axiosInstance = axios.create({
|
|
13
|
-
baseURL: config.baseUrl,
|
|
14
|
-
headers: {
|
|
15
|
-
'Content-Type': 'application/json',
|
|
16
|
-
},
|
|
17
|
-
});
|
|
18
|
-
|
|
19
|
-
this.setupInterceptors();
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
private setupInterceptors() {
|
|
23
|
-
this.axiosInstance.interceptors.request.use(
|
|
24
|
-
async (config) => {
|
|
25
|
-
const token = await this.getValidToken();
|
|
26
|
-
if (token && config.headers) {
|
|
27
|
-
config.headers.Authorization = `Bearer ${token}`;
|
|
28
|
-
}
|
|
29
|
-
return config;
|
|
30
|
-
},
|
|
31
|
-
(error) => Promise.reject(error)
|
|
32
|
-
);
|
|
33
|
-
|
|
34
|
-
this.axiosInstance.interceptors.response.use(
|
|
35
|
-
(response) => response,
|
|
36
|
-
async (error) => {
|
|
37
|
-
if (error.response?.status === 401) {
|
|
38
|
-
// Token might be expired, try to refresh
|
|
39
|
-
const token = await this.getValidToken(true);
|
|
40
|
-
if (token && error.config) {
|
|
41
|
-
error.config.headers.Authorization = `Bearer ${token}`;
|
|
42
|
-
return this.axiosInstance.request(error.config);
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
return Promise.reject(error);
|
|
46
|
-
}
|
|
47
|
-
);
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
private async getValidToken(forceRefresh = false): Promise<string> {
|
|
51
|
-
const currentTime = Math.floor(Date.now() / 1000);
|
|
52
|
-
|
|
53
|
-
if (forceRefresh || !this.currentToken || currentTime >= this.tokenExpiryTime) {
|
|
54
|
-
try {
|
|
55
|
-
const response = await this.config.getToken();
|
|
56
|
-
if (!response || !response.token || !response.expiresIn) {
|
|
57
|
-
throw new Error('Invalid token response');
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
this.currentToken = response.token;
|
|
61
|
-
this.tokenExpiryTime = currentTime + response.expiresIn;
|
|
62
|
-
return this.currentToken;
|
|
63
|
-
} catch (error) {
|
|
64
|
-
throw error;
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
return this.currentToken;
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
async get<T>(endpoint: string, config?: AxiosRequestConfig): Promise<T> {
|
|
72
|
-
try {
|
|
73
|
-
const response = await this.axiosInstance.get<T>(endpoint, config);
|
|
74
|
-
return response.data;
|
|
75
|
-
} catch (error) {
|
|
76
|
-
this.handleError(error);
|
|
77
|
-
throw error;
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
async post<T>(endpoint: string, data?: unknown, config?: AxiosRequestConfig): Promise<T> {
|
|
82
|
-
try {
|
|
83
|
-
const response = await this.axiosInstance.post<T>(endpoint, data, config);
|
|
84
|
-
return response.data;
|
|
85
|
-
} catch (error) {
|
|
86
|
-
this.handleError(error);
|
|
87
|
-
throw error;
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
async put<T>(endpoint: string, data?: unknown, config?: AxiosRequestConfig): Promise<T> {
|
|
92
|
-
try {
|
|
93
|
-
const response = await this.axiosInstance.put<T>(endpoint, data, config);
|
|
94
|
-
return response.data;
|
|
95
|
-
} catch (error) {
|
|
96
|
-
this.handleError(error);
|
|
97
|
-
throw error;
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
async delete<T>(endpoint: string, config?: AxiosRequestConfig): Promise<T> {
|
|
102
|
-
try {
|
|
103
|
-
const response = await this.axiosInstance.delete<T>(endpoint, config);
|
|
104
|
-
return response.data;
|
|
105
|
-
} catch (error) {
|
|
106
|
-
this.handleError(error);
|
|
107
|
-
throw error;
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
private handleError(error: unknown) {
|
|
112
|
-
if (this.config.onError) {
|
|
113
|
-
this.config.onError(error instanceof Error ? error : new Error('Unknown error occurred'));
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
}
|
package/src/core/api.ts
DELETED
|
@@ -1,278 +0,0 @@
|
|
|
1
|
-
import { defaultLanguage } from '@/i18n';
|
|
2
|
-
import { TokenResponse, PresignUploadRequestDto, PresignUploadResponse, PresignDownloadResponse } from '@/lib/types';
|
|
3
|
-
|
|
4
|
-
let getTokenFunction: (() => Promise<TokenResponse>) | undefined = undefined;
|
|
5
|
-
let baseUrl: string | null = null;
|
|
6
|
-
|
|
7
|
-
// Add request cache and connection optimization
|
|
8
|
-
const requestCache = new Map<string, { data: any; timestamp: number; ttl: number }>();
|
|
9
|
-
const CACHE_TTL = 30000; // 30 seconds cache for non-critical requests
|
|
10
|
-
const pendingRequests = new Map<string, Promise<any>>();
|
|
11
|
-
|
|
12
|
-
export function initializeAPI(url: string, getToken: () => Promise<TokenResponse>) {
|
|
13
|
-
getTokenFunction = getToken;
|
|
14
|
-
baseUrl = url;
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
export async function getValidToken(forceRefresh = false): Promise<string> {
|
|
18
|
-
if (!getTokenFunction) {
|
|
19
|
-
throw new Error('API module not initialized. Call initializeAPI(getToken) first.');
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
let storedToken = localStorage.getItem('chatbot-token');
|
|
23
|
-
let storedExpiry = localStorage.getItem('chatbot-token-expiry');
|
|
24
|
-
const currentTime = Math.floor(Date.now() / 1000);
|
|
25
|
-
|
|
26
|
-
// Add buffer time to prevent token expiry during request
|
|
27
|
-
const bufferTime = 60; // 1 minute buffer
|
|
28
|
-
const isTokenExpiring = storedExpiry && currentTime >= Number(storedExpiry) - bufferTime;
|
|
29
|
-
|
|
30
|
-
if (!storedToken || !storedExpiry || isTokenExpiring || forceRefresh) {
|
|
31
|
-
try {
|
|
32
|
-
const tokenResponse = await getTokenFunction();
|
|
33
|
-
storedToken = tokenResponse.token;
|
|
34
|
-
storedExpiry = String(currentTime + (tokenResponse.expiresIn ?? 900));
|
|
35
|
-
|
|
36
|
-
localStorage.setItem('chatbot-token', storedToken);
|
|
37
|
-
localStorage.setItem('chatbot-token-expiry', storedExpiry);
|
|
38
|
-
} catch (error) {
|
|
39
|
-
throw error;
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
return storedToken;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
// Optimized fetch with retry logic and connection pooling
|
|
47
|
-
async function fetchWithAuth(url: string, options: RequestInit, retry = true): Promise<Response> {
|
|
48
|
-
const headers = new Headers(options.headers);
|
|
49
|
-
|
|
50
|
-
try {
|
|
51
|
-
const token = await getValidToken();
|
|
52
|
-
headers.set('Authorization', `Bearer ${token}`);
|
|
53
|
-
|
|
54
|
-
// Add performance optimizations
|
|
55
|
-
headers.set('Accept', 'application/json');
|
|
56
|
-
headers.set('Accept-Encoding', 'gzip, deflate, br');
|
|
57
|
-
headers.set('Connection', 'keep-alive');
|
|
58
|
-
|
|
59
|
-
options.headers = headers;
|
|
60
|
-
|
|
61
|
-
// Add timeout to prevent hanging requests
|
|
62
|
-
const controller = new AbortController();
|
|
63
|
-
const timeoutId = setTimeout(() => controller.abort(), 30000); // 30 second timeout
|
|
64
|
-
|
|
65
|
-
try {
|
|
66
|
-
const response = await fetch(url, {
|
|
67
|
-
...options,
|
|
68
|
-
signal: controller.signal,
|
|
69
|
-
// Add HTTP/2 optimization hints
|
|
70
|
-
cache: 'no-cache',
|
|
71
|
-
mode: 'cors',
|
|
72
|
-
});
|
|
73
|
-
|
|
74
|
-
clearTimeout(timeoutId);
|
|
75
|
-
|
|
76
|
-
// Handle 401/403 with token refresh
|
|
77
|
-
if ((response.status === 401 || response.status === 403) && retry) {
|
|
78
|
-
const newToken = await getValidToken(true);
|
|
79
|
-
headers.set('Authorization', `Bearer ${newToken}`);
|
|
80
|
-
options.headers = headers;
|
|
81
|
-
|
|
82
|
-
// Retry the request with new token
|
|
83
|
-
return fetchWithAuth(url, options, false);
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
return response;
|
|
87
|
-
} catch (error) {
|
|
88
|
-
clearTimeout(timeoutId);
|
|
89
|
-
|
|
90
|
-
if (error instanceof Error && error.name === 'AbortError') {
|
|
91
|
-
throw new Error('Request timeout - please try again');
|
|
92
|
-
}
|
|
93
|
-
throw error;
|
|
94
|
-
}
|
|
95
|
-
} catch (error) {
|
|
96
|
-
throw error;
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
// Cache management functions
|
|
101
|
-
function getCachedResponse(cacheKey: string): any | null {
|
|
102
|
-
const cached = requestCache.get(cacheKey);
|
|
103
|
-
if (cached && Date.now() < cached.timestamp + cached.ttl) {
|
|
104
|
-
return cached.data;
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
if (cached) {
|
|
108
|
-
requestCache.delete(cacheKey);
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
return null;
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
function setCachedResponse(cacheKey: string, data: any, ttl: number = CACHE_TTL): void {
|
|
115
|
-
requestCache.set(cacheKey, {
|
|
116
|
-
data,
|
|
117
|
-
timestamp: Date.now(),
|
|
118
|
-
ttl,
|
|
119
|
-
});
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
// Deduplicate concurrent requests
|
|
123
|
-
function getDuplicateRequest(requestKey: string): Promise<any> | null {
|
|
124
|
-
return pendingRequests.get(requestKey) || null;
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
function setPendingRequest(requestKey: string, promise: Promise<any>): void {
|
|
128
|
-
pendingRequests.set(requestKey, promise);
|
|
129
|
-
|
|
130
|
-
// Clean up after request completes
|
|
131
|
-
promise.finally(() => {
|
|
132
|
-
pendingRequests.delete(requestKey);
|
|
133
|
-
});
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
export async function apiRequest(
|
|
137
|
-
endpoint: string,
|
|
138
|
-
method = 'GET',
|
|
139
|
-
body: any = null,
|
|
140
|
-
options: { cache?: boolean; timeout?: number; language?: 'ar' | 'en' }
|
|
141
|
-
) {
|
|
142
|
-
if (!baseUrl) throw new Error('API not initialized');
|
|
143
|
-
|
|
144
|
-
const url = `${baseUrl}/${endpoint}`;
|
|
145
|
-
const requestKey = `${method}:${endpoint}:${JSON.stringify(body)}`;
|
|
146
|
-
|
|
147
|
-
// Check for duplicate in-flight requests (skip for GET: callers receive Response and call .json();
|
|
148
|
-
// sharing the same Response causes "body already consumed" when the second caller parses)
|
|
149
|
-
const duplicateRequest = getDuplicateRequest(requestKey);
|
|
150
|
-
if (duplicateRequest && method !== 'GET') {
|
|
151
|
-
return duplicateRequest;
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
// Check cache for GET requests (except real-time endpoints)
|
|
155
|
-
if (method === 'GET' && options.cache !== false && !endpoint.includes('/send-message')) {
|
|
156
|
-
const cached = getCachedResponse(requestKey);
|
|
157
|
-
if (cached) {
|
|
158
|
-
return Promise.resolve(cached);
|
|
159
|
-
}
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
const requestOptions: RequestInit = {
|
|
163
|
-
method,
|
|
164
|
-
headers: {
|
|
165
|
-
'Content-Type': 'application/json',
|
|
166
|
-
'Cache-Control': method === 'GET' ? 'max-age=30' : 'no-cache',
|
|
167
|
-
'Accept-Language': options.language || defaultLanguage,
|
|
168
|
-
},
|
|
169
|
-
body: body ? JSON.stringify(body) : null,
|
|
170
|
-
};
|
|
171
|
-
|
|
172
|
-
const requestPromise = (async () => {
|
|
173
|
-
try {
|
|
174
|
-
const response = await fetchWithAuth(url, requestOptions);
|
|
175
|
-
|
|
176
|
-
if (!response.ok) {
|
|
177
|
-
let errorMessage = 'API request failed';
|
|
178
|
-
|
|
179
|
-
try {
|
|
180
|
-
// Clone response before reading to avoid consuming the body
|
|
181
|
-
const errorResponse = response.clone();
|
|
182
|
-
const errorData = await errorResponse.json();
|
|
183
|
-
errorMessage = errorData.message || errorData.error || errorMessage;
|
|
184
|
-
} catch (parseError) {
|
|
185
|
-
// If JSON parsing fails, try to get text
|
|
186
|
-
try {
|
|
187
|
-
const errorResponse = response.clone();
|
|
188
|
-
const errorText = await errorResponse.text();
|
|
189
|
-
errorMessage = errorText || `HTTP ${response.status}: ${response.statusText}`;
|
|
190
|
-
} catch {
|
|
191
|
-
errorMessage = `HTTP ${response.status}: ${response.statusText}`;
|
|
192
|
-
}
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
throw new Error(errorMessage);
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
// Cache successful GET responses
|
|
199
|
-
// Note: We clone before caching to avoid consuming the original response body
|
|
200
|
-
if (method === 'GET' && options.cache !== false) {
|
|
201
|
-
const responseData = response.clone();
|
|
202
|
-
const data = await responseData.json();
|
|
203
|
-
setCachedResponse(requestKey, { json: () => Promise.resolve(data) });
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
// Return the original response - it's body hasn't been consumed yet
|
|
207
|
-
// (we only cloned for caching, and the clone was consumed)
|
|
208
|
-
return response;
|
|
209
|
-
} catch (error) {
|
|
210
|
-
throw error;
|
|
211
|
-
}
|
|
212
|
-
})();
|
|
213
|
-
|
|
214
|
-
// Track pending request
|
|
215
|
-
setPendingRequest(requestKey, requestPromise);
|
|
216
|
-
|
|
217
|
-
return requestPromise;
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
export async function presignUpload(
|
|
221
|
-
chatSessionId: string,
|
|
222
|
-
file: File,
|
|
223
|
-
language: 'ar' | 'en' = defaultLanguage
|
|
224
|
-
): Promise<PresignUploadResponse> {
|
|
225
|
-
const requestBody: PresignUploadRequestDto = {
|
|
226
|
-
name: file.name,
|
|
227
|
-
contentType: file.type,
|
|
228
|
-
sizeBytes: file.size,
|
|
229
|
-
pathData: {
|
|
230
|
-
type: 1,
|
|
231
|
-
chatSessionId: chatSessionId,
|
|
232
|
-
},
|
|
233
|
-
};
|
|
234
|
-
|
|
235
|
-
const response = await apiRequest('NewFile/presign-upload', 'POST', requestBody, { language });
|
|
236
|
-
|
|
237
|
-
if (!response.ok) {
|
|
238
|
-
throw new Error('Failed to get presigned upload URL');
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
return await response.json();
|
|
242
|
-
}
|
|
243
|
-
export async function presignDownload(fileId: string, language: 'ar' | 'en' = defaultLanguage): Promise<PresignDownloadResponse> {
|
|
244
|
-
try {
|
|
245
|
-
const response = await apiRequest(`NewFile/${fileId}/presign-download`, 'GET', null, {
|
|
246
|
-
language,
|
|
247
|
-
cache: false, // Don't cache presigned URLs as they have expiration times
|
|
248
|
-
});
|
|
249
|
-
|
|
250
|
-
// If response is not ok, apiRequest would have thrown an error already
|
|
251
|
-
// So we can safely read the JSON here
|
|
252
|
-
try {
|
|
253
|
-
return await response.json();
|
|
254
|
-
} catch (jsonError) {
|
|
255
|
-
// If JSON parsing fails, the response body might have been consumed
|
|
256
|
-
// or the response might not be valid JSON
|
|
257
|
-
if (jsonError instanceof Error) {
|
|
258
|
-
if (jsonError.message.includes('already read')) {
|
|
259
|
-
throw new Error(`Failed to parse response for file ${fileId}: Response body was already consumed`);
|
|
260
|
-
}
|
|
261
|
-
throw new Error(`Failed to parse response for file ${fileId}: ${jsonError.message}`);
|
|
262
|
-
}
|
|
263
|
-
throw new Error(`Failed to parse response for file ${fileId}`);
|
|
264
|
-
}
|
|
265
|
-
} catch (error) {
|
|
266
|
-
// Handle all types of errors
|
|
267
|
-
if (error instanceof Error) {
|
|
268
|
-
// If it's already a descriptive error from apiRequest, re-throw it
|
|
269
|
-
if (error.message.includes('API request failed') || error.message.includes('HTTP')) {
|
|
270
|
-
throw new Error(`Failed to get presigned download URL for file ${fileId}: ${error.message}`);
|
|
271
|
-
}
|
|
272
|
-
// Otherwise, re-throw with context
|
|
273
|
-
throw error;
|
|
274
|
-
}
|
|
275
|
-
// Handle non-Error types
|
|
276
|
-
throw new Error(`Failed to get presigned download URL for file ${fileId}: ${String(error)}`);
|
|
277
|
-
}
|
|
278
|
-
}
|
|
@@ -1,35 +0,0 @@
|
|
|
1
|
-
type TokenResponse = {
|
|
2
|
-
token: string
|
|
3
|
-
expiresIn: number
|
|
4
|
-
}
|
|
5
|
-
|
|
6
|
-
export class TokenService {
|
|
7
|
-
private baseUrl: string
|
|
8
|
-
|
|
9
|
-
constructor(baseUrl: string) {
|
|
10
|
-
this.baseUrl = baseUrl
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
async getToken(): Promise<TokenResponse> {
|
|
14
|
-
try {
|
|
15
|
-
const response = await fetch(`${this.baseUrl}/Auth/client/get-babylai-token`, {
|
|
16
|
-
method: 'POST',
|
|
17
|
-
headers: {
|
|
18
|
-
'Content-Type': 'application/json'
|
|
19
|
-
}
|
|
20
|
-
})
|
|
21
|
-
|
|
22
|
-
if (!response.ok) {
|
|
23
|
-
throw new Error('Failed to fetch token')
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
const data = await response.json()
|
|
27
|
-
return {
|
|
28
|
-
token: data.token,
|
|
29
|
-
expiresIn: data.expiresIn || 3600 // Default to 1 hour if not provided
|
|
30
|
-
}
|
|
31
|
-
} catch (error) {
|
|
32
|
-
throw new Error('Failed to get authentication token')
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
}
|