@aslaluroba/help-center-react 3.0.21 → 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,39 +1,30 @@
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;
5
5
 
6
6
  // Add request cache and connection optimization
7
- const requestCache = new Map<
8
- string,
9
- { data: any; timestamp: number; ttl: number }
10
- >();
7
+ const requestCache = new Map<string, { data: any; timestamp: number; ttl: number }>();
11
8
  const CACHE_TTL = 30000; // 30 seconds cache for non-critical requests
12
9
  const pendingRequests = new Map<string, Promise<any>>();
13
10
 
14
- export function initializeAPI(
15
- url: string,
16
- getToken: () => Promise<TokenResponse>,
17
- ) {
11
+ export function initializeAPI(url: string, getToken: () => Promise<TokenResponse>) {
18
12
  getTokenFunction = getToken;
19
13
  baseUrl = url;
20
14
  }
21
15
 
22
16
  export async function getValidToken(forceRefresh = false): Promise<string> {
23
17
  if (!getTokenFunction) {
24
- throw new Error(
25
- "API module not initialized. Call initializeAPI(getToken) first.",
26
- );
18
+ throw new Error('API module not initialized. Call initializeAPI(getToken) first.');
27
19
  }
28
20
 
29
- let storedToken = localStorage.getItem("chatbot-token");
30
- let storedExpiry = localStorage.getItem("chatbot-token-expiry");
21
+ let storedToken = localStorage.getItem('chatbot-token');
22
+ let storedExpiry = localStorage.getItem('chatbot-token-expiry');
31
23
  const currentTime = Math.floor(Date.now() / 1000);
32
24
 
33
25
  // Add buffer time to prevent token expiry during request
34
26
  const bufferTime = 60; // 1 minute buffer
35
- const isTokenExpiring =
36
- storedExpiry && currentTime >= Number(storedExpiry) - bufferTime;
27
+ const isTokenExpiring = storedExpiry && currentTime >= Number(storedExpiry) - bufferTime;
37
28
 
38
29
  if (!storedToken || !storedExpiry || isTokenExpiring || forceRefresh) {
39
30
  try {
@@ -41,10 +32,9 @@ export async function getValidToken(forceRefresh = false): Promise<string> {
41
32
  storedToken = tokenResponse.token;
42
33
  storedExpiry = String(currentTime + (tokenResponse.expiresIn ?? 900));
43
34
 
44
- localStorage.setItem("chatbot-token", storedToken);
45
- localStorage.setItem("chatbot-token-expiry", storedExpiry);
35
+ localStorage.setItem('chatbot-token', storedToken);
36
+ localStorage.setItem('chatbot-token-expiry', storedExpiry);
46
37
  } catch (error) {
47
- console.error("Failed to refresh token:", error);
48
38
  throw error;
49
39
  }
50
40
  }
@@ -53,21 +43,17 @@ export async function getValidToken(forceRefresh = false): Promise<string> {
53
43
  }
54
44
 
55
45
  // Optimized fetch with retry logic and connection pooling
56
- async function fetchWithAuth(
57
- url: string,
58
- options: RequestInit,
59
- retry = true,
60
- ): Promise<Response> {
46
+ async function fetchWithAuth(url: string, options: RequestInit, retry = true): Promise<Response> {
61
47
  const headers = new Headers(options.headers);
62
48
 
63
49
  try {
64
50
  const token = await getValidToken();
65
- headers.set("Authorization", `Bearer ${token}`);
51
+ headers.set('Authorization', `Bearer ${token}`);
66
52
 
67
53
  // Add performance optimizations
68
- headers.set("Accept", "application/json");
69
- headers.set("Accept-Encoding", "gzip, deflate, br");
70
- headers.set("Connection", "keep-alive");
54
+ headers.set('Accept', 'application/json');
55
+ headers.set('Accept-Encoding', 'gzip, deflate, br');
56
+ headers.set('Connection', 'keep-alive');
71
57
 
72
58
  options.headers = headers;
73
59
 
@@ -80,17 +66,16 @@ async function fetchWithAuth(
80
66
  ...options,
81
67
  signal: controller.signal,
82
68
  // Add HTTP/2 optimization hints
83
- cache: "no-cache",
84
- mode: "cors",
69
+ cache: 'no-cache',
70
+ mode: 'cors',
85
71
  });
86
72
 
87
73
  clearTimeout(timeoutId);
88
74
 
89
75
  // Handle 401/403 with token refresh
90
76
  if ((response.status === 401 || response.status === 403) && retry) {
91
- console.warn("Token expired, refreshing...");
92
77
  const newToken = await getValidToken(true);
93
- headers.set("Authorization", `Bearer ${newToken}`);
78
+ headers.set('Authorization', `Bearer ${newToken}`);
94
79
  options.headers = headers;
95
80
 
96
81
  // Retry the request with new token
@@ -101,13 +86,12 @@ async function fetchWithAuth(
101
86
  } catch (error) {
102
87
  clearTimeout(timeoutId);
103
88
 
104
- if (error instanceof Error && error.name === "AbortError") {
105
- throw new Error("Request timeout - please try again");
89
+ if (error instanceof Error && error.name === 'AbortError') {
90
+ throw new Error('Request timeout - please try again');
106
91
  }
107
92
  throw error;
108
93
  }
109
94
  } catch (error) {
110
- console.error("Fetch error:", error);
111
95
  throw error;
112
96
  }
113
97
  }
@@ -126,11 +110,7 @@ function getCachedResponse(cacheKey: string): any | null {
126
110
  return null;
127
111
  }
128
112
 
129
- function setCachedResponse(
130
- cacheKey: string,
131
- data: any,
132
- ttl: number = CACHE_TTL,
133
- ): void {
113
+ function setCachedResponse(cacheKey: string, data: any, ttl: number = CACHE_TTL): void {
134
114
  requestCache.set(cacheKey, {
135
115
  data,
136
116
  timestamp: Date.now(),
@@ -154,11 +134,11 @@ function setPendingRequest(requestKey: string, promise: Promise<any>): void {
154
134
 
155
135
  export async function apiRequest(
156
136
  endpoint: string,
157
- method = "GET",
137
+ method = 'GET',
158
138
  body: any = null,
159
- options: { cache?: boolean; timeout?: number; language: string },
139
+ options: { cache?: boolean; timeout?: number; language: string }
160
140
  ) {
161
- if (!baseUrl) throw new Error("API not initialized");
141
+ if (!baseUrl) throw new Error('API not initialized');
162
142
 
163
143
  const url = `${baseUrl}/${endpoint}`;
164
144
  const requestKey = `${method}:${endpoint}:${JSON.stringify(body)}`;
@@ -170,11 +150,7 @@ export async function apiRequest(
170
150
  }
171
151
 
172
152
  // Check cache for GET requests (except real-time endpoints)
173
- if (
174
- method === "GET" &&
175
- options.cache !== false &&
176
- !endpoint.includes("/send-message")
177
- ) {
153
+ if (method === 'GET' && options.cache !== false && !endpoint.includes('/send-message')) {
178
154
  const cached = getCachedResponse(requestKey);
179
155
  if (cached) {
180
156
  return Promise.resolve(cached);
@@ -184,9 +160,9 @@ export async function apiRequest(
184
160
  const requestOptions: RequestInit = {
185
161
  method,
186
162
  headers: {
187
- "Content-Type": "application/json",
188
- "Cache-Control": method === "GET" ? "max-age=30" : "no-cache",
189
- "Accept-Language": options.language,
163
+ 'Content-Type': 'application/json',
164
+ 'Cache-Control': method === 'GET' ? 'max-age=30' : 'no-cache',
165
+ 'Accept-Language': options.language,
190
166
  },
191
167
  body: body ? JSON.stringify(body) : null,
192
168
  };
@@ -196,28 +172,39 @@ export async function apiRequest(
196
172
  const response = await fetchWithAuth(url, requestOptions);
197
173
 
198
174
  if (!response.ok) {
199
- let errorMessage = "API request failed";
175
+ let errorMessage = 'API request failed';
200
176
 
201
177
  try {
202
- const errorData = await response.json();
203
- errorMessage = errorData.message || errorMessage;
204
- } catch {
205
- 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
+ }
206
191
  }
207
192
 
208
193
  throw new Error(errorMessage);
209
194
  }
210
195
 
211
196
  // Cache successful GET responses
212
- if (method === "GET" && options.cache !== false) {
197
+ // Note: We clone before caching to avoid consuming the original response body
198
+ if (method === 'GET' && options.cache !== false) {
213
199
  const responseData = response.clone();
214
200
  const data = await responseData.json();
215
201
  setCachedResponse(requestKey, { json: () => Promise.resolve(data) });
216
202
  }
217
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)
218
206
  return response;
219
207
  } catch (error) {
220
- console.error(`API request failed for ${endpoint}:`, error);
221
208
  throw error;
222
209
  }
223
210
  })();
@@ -227,3 +214,64 @@ export async function apiRequest(
227
214
 
228
215
  return requestPromise;
229
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
  }