@aslaluroba/help-center-react 2.0.2 → 2.0.5

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 (41) hide show
  1. package/dist/core/api.d.ts +4 -1
  2. package/dist/index.d.ts +3 -2
  3. package/dist/index.esm.js +994 -25294
  4. package/dist/index.esm.js.map +1 -1
  5. package/dist/index.js +995 -25295
  6. package/dist/index.js.map +1 -1
  7. package/dist/lib/config.d.ts +1 -1
  8. package/dist/lib/types.d.ts +4 -0
  9. package/dist/ui/chatbot-popup/chat-window-screen/footer.d.ts +1 -0
  10. package/dist/ui/chatbot-popup/chat-window-screen/index.d.ts +1 -1
  11. package/dist/ui/help-center.d.ts +1 -1
  12. package/dist/ui/help-popup.d.ts +9 -3
  13. package/dist/ui/review-dialog/index.d.ts +8 -0
  14. package/dist/ui/review-dialog/rating.d.ts +12 -0
  15. package/package.json +26 -5
  16. package/src/assets/icons/arrowRight.svg +1 -1
  17. package/src/assets/icons/closeCircle.svg +1 -1
  18. package/src/components/ui/agent-response/agent-response.tsx +36 -34
  19. package/src/components/ui/header.tsx +2 -3
  20. package/src/core/SignalRService.ts +25 -25
  21. package/src/core/api.ts +180 -43
  22. package/src/globals.css +0 -9
  23. package/src/index.ts +3 -2
  24. package/src/lib/config.ts +31 -25
  25. package/src/lib/types.ts +5 -0
  26. package/src/locales/ar.json +18 -1
  27. package/src/locales/en.json +26 -8
  28. package/src/ui/chatbot-popup/chat-window-screen/footer.tsx +31 -34
  29. package/src/ui/chatbot-popup/chat-window-screen/header.tsx +47 -53
  30. package/src/ui/chatbot-popup/chat-window-screen/index.tsx +178 -88
  31. package/src/ui/chatbot-popup/options-list-screen/header.tsx +24 -20
  32. package/src/ui/chatbot-popup/options-list-screen/index.tsx +24 -24
  33. package/src/ui/chatbot-popup/options-list-screen/option-card.tsx +9 -4
  34. package/src/ui/help-center.tsx +367 -141
  35. package/src/ui/help-popup.tsx +239 -165
  36. package/src/ui/review-dialog/index.tsx +106 -0
  37. package/src/ui/review-dialog/rating.tsx +78 -0
  38. package/tsconfig.json +48 -0
  39. package/postcss.config.js +0 -6
  40. package/rollup.config.js +0 -58
  41. package/tailwind.config.js +0 -174
@@ -1,6 +1,6 @@
1
1
  import { TokenResponse } from './types';
2
2
  type Config = {
3
- baseUrl: string;
3
+ baseUrl?: string;
4
4
  hubUrl?: string;
5
5
  tenantId?: string;
6
6
  apiKey?: string;
@@ -109,3 +109,7 @@ export interface Option {
109
109
  hasNestedOptions: boolean;
110
110
  order: number;
111
111
  }
112
+ export interface ReviewProps {
113
+ comment: string;
114
+ rating: number;
115
+ }
@@ -4,6 +4,7 @@ interface ChatWindowFooterProps {
4
4
  setInputMessage: (e: string) => void;
5
5
  handleKeyDown: (e: React.KeyboardEvent) => void;
6
6
  handleSendMessage: () => void;
7
+ isLoading: boolean;
7
8
  }
8
9
  declare const ChatWindowFooter: React.FC<ChatWindowFooterProps>;
9
10
  export default ChatWindowFooter;
@@ -6,5 +6,5 @@ interface ChatWindowProps {
6
6
  assistantStatus: string;
7
7
  needsAgent: boolean;
8
8
  }
9
- export declare function ChatWindow({ onSendMessage, messages, assistantStatus }: ChatWindowProps): React.JSX.Element;
9
+ export declare const ChatWindow: React.MemoExoticComponent<({ onSendMessage, messages, assistantStatus }: ChatWindowProps) => React.JSX.Element>;
10
10
  export {};
@@ -13,5 +13,5 @@ interface HelpCenterProps {
13
13
  messageLabel?: string | null;
14
14
  showHelpScreen?: boolean;
15
15
  }
16
- export declare function HelpCenter({ helpScreenId, user, showArrow, language, messageLabel, showHelpScreen }: HelpCenterProps): React.JSX.Element;
16
+ export declare function HelpCenter({ helpScreenId, user, showArrow, language, messageLabel, showHelpScreen, }: HelpCenterProps): React.JSX.Element;
17
17
  export {};
@@ -1,5 +1,5 @@
1
1
  import React from 'react';
2
- import { HelpScreenData, Message, Option } from '../lib/types';
2
+ import { HelpScreenData, Message, Option } from '@/lib/types';
3
3
  type HelpPopupProps = {
4
4
  isOpen: boolean;
5
5
  onClose: () => void;
@@ -9,7 +9,7 @@ type HelpPopupProps = {
9
9
  user: any;
10
10
  onStartChat: (option: Option) => void;
11
11
  onSendMessage: (message: string) => void;
12
- onEndChat: () => void;
12
+ onEndChat: (option?: Option) => void;
13
13
  messages: Message[];
14
14
  needsAgent: boolean;
15
15
  assistantStatus: string;
@@ -20,5 +20,11 @@ type HelpPopupProps = {
20
20
  setSelectedOption: (option: Option | null) => void;
21
21
  showHelpScreen: boolean;
22
22
  };
23
- export declare function HelpPopup({ onClose, helpScreen, status, error, onStartChat, onSendMessage, onEndChat, messages, assistantStatus, needsAgent, sessionId, isChatClosed, isSignalRConnected, selectedOption, setSelectedOption, showHelpScreen }: HelpPopupProps): React.JSX.Element;
23
+ export declare const ConfirmationModal: ({ title, message, onCancel, onConfirm, }: {
24
+ title: string;
25
+ message: string;
26
+ onCancel: () => void;
27
+ onConfirm: () => void;
28
+ }) => React.JSX.Element;
29
+ export declare function HelpPopup({ onClose, helpScreen, status, error, onStartChat, onSendMessage, onEndChat, messages, assistantStatus, needsAgent, sessionId, selectedOption, setSelectedOption, showHelpScreen, }: HelpPopupProps): React.JSX.Element;
24
30
  export {};
@@ -0,0 +1,8 @@
1
+ import { ReviewProps } from '@/lib/types';
2
+ import React from 'react';
3
+ interface ReviewDialogProps {
4
+ handleSubmit: ({ comment, rating }: ReviewProps) => void;
5
+ onClose: () => void;
6
+ }
7
+ declare const ReviewDialog: React.FC<ReviewDialogProps>;
8
+ export default ReviewDialog;
@@ -0,0 +1,12 @@
1
+ import * as React from 'react';
2
+ export interface RatingProps {
3
+ value: number;
4
+ onChange?: (value: number) => void;
5
+ max?: number;
6
+ icon?: 'star' | 'heart' | 'thumbsUp';
7
+ size?: 'sm' | 'md' | 'lg';
8
+ readOnly?: boolean;
9
+ className?: string;
10
+ style?: React.CSSProperties;
11
+ }
12
+ export declare const Rating: React.ForwardRefExoticComponent<RatingProps & React.RefAttributes<HTMLDivElement>>;
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "main": "dist/index.js",
4
4
  "module": "dist/index.esm.js",
5
5
  "types": "dist/index.d.ts",
6
- "version": "2.0.2",
6
+ "version": "2.0.5",
7
7
  "description": "BabylAI Help Center Widget for React and Next.js",
8
8
  "private": false,
9
9
  "exports": {
@@ -24,13 +24,19 @@
24
24
  "dev": "rollup -c -w",
25
25
  "test": "jest",
26
26
  "clean": "rimraf dist",
27
- "publish:lib": "npm run build && npm publish --access public"
27
+ "version:patch": "npm version patch --no-git-tag-version",
28
+ "version:minor": "npm version minor --no-git-tag-version",
29
+ "version:major": "npm version major --no-git-tag-version",
30
+ "publish": "yarn clean && yarn build && yarn version:patch && yarn publish --access public",
31
+ "publish:minor": "yarn clean && yarn build && yarn version:minor && yarn publish --access public",
32
+ "publish:major": "yarn clean && yarn build && yarn version:major && yarn publish --access public"
28
33
  },
29
34
  "files": [
30
35
  "dist",
31
36
  "src",
32
37
  "globals.css",
33
38
  "package.json",
39
+ "tsconfig.json",
34
40
  "./tailwind.config.js",
35
41
  "./postcss.config.js",
36
42
  "./rollup.config.js"
@@ -45,10 +51,14 @@
45
51
  "author": "BabylAI",
46
52
  "license": "MIT",
47
53
  "peerDependencies": {
54
+ "@tabler/icons-react": "^3.0.0",
55
+ "clsx": "^2.0.0",
48
56
  "i18next": "^23.0.0",
49
57
  "react": ">=16.8.0",
50
58
  "react-dom": ">=16.8.0",
51
- "react-i18next": "^13.0.0"
59
+ "react-i18next": "^13.0.0",
60
+ "react-markdown": "^10.0.0",
61
+ "tailwind-merge": "^3.0.0"
52
62
  },
53
63
  "dependencies": {
54
64
  "@babel/core": "^7.26.10",
@@ -61,6 +71,7 @@
61
71
  "class-variance-authority": "^0.7.1"
62
72
  },
63
73
  "devDependencies": {
74
+ "@rollup/plugin-alias": "^5.1.1",
64
75
  "@rollup/plugin-commonjs": "^21.1.0",
65
76
  "@rollup/plugin-image": "^3.0.3",
66
77
  "@rollup/plugin-json": "^6.1.0",
@@ -68,19 +79,29 @@
68
79
  "@rollup/plugin-typescript": "^8.5.0",
69
80
  "@rollup/plugin-url": "^8.0.2",
70
81
  "@svgr/rollup": "^8.1.0",
82
+ "@tabler/icons-react": "^3.34.0",
83
+ "@types/hast": "^3.0.4",
71
84
  "@types/node": "^16.18.126",
72
85
  "@types/react": "^17.0.83",
73
86
  "autoprefixer": "^10.4.14",
87
+ "clsx": "^2.1.1",
88
+ "i18next": "^25.2.1",
74
89
  "postcss": "^8.4.24",
90
+ "react-i18next": "^15.5.2",
91
+ "react-markdown": "^10.1.0",
92
+ "rimraf": "^6.0.1",
75
93
  "rollup": "^2.79.2",
76
94
  "rollup-plugin-postcss": "^4.0.2",
95
+ "tailwind-merge": "^3.3.0",
77
96
  "tailwindcss": "^3.3.2",
78
97
  "tailwindcss-animate": "^1.0.7",
79
98
  "tailwindcss-rtl": "^0.9.0",
80
99
  "tslib": "^2.8.1",
81
- "typescript": "^5.1.6"
100
+ "typescript": "^5.1.6",
101
+ "webpack": "^5.99.9"
82
102
  },
83
103
  "publishConfig": {
84
104
  "access": "public"
85
- }
105
+ },
106
+ "packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e"
86
107
  }
@@ -1,3 +1,3 @@
1
- <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 9 16">
1
+ <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 9 16" width="100%" height="100%">
2
2
  <path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="m1.5 1 6 7L6 9.75M1.5 15l2-2.333"/>
3
3
  </svg>
@@ -1,3 +1,3 @@
1
1
  <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 55 55">
2
- <path stroke="#fff" stroke-linecap="round" stroke-width="3" d="M33.857 21.146 21.148 33.854m0-12.708 12.709 12.708M14.794 5.484a25.29 25.29 0 0 1 12.709-3.4c14.037 0 25.416 11.378 25.416 25.416S41.54 52.917 27.503 52.917c-14.038 0-25.417-11.38-25.417-25.417 0-4.628 1.238-8.972 3.4-12.708"/>
2
+ <path stroke="currentcolor" stroke-linecap="round" stroke-width="3" d="M33.857 21.146 21.148 33.854m0-12.708 12.709 12.708M14.794 5.484a25.29 25.29 0 0 1 12.709-3.4c14.037 0 25.416 11.378 25.416 25.416S41.54 52.917 27.503 52.917c-14.038 0-25.417-11.38-25.417-25.417 0-4.628 1.238-8.972 3.4-12.708"/>
3
3
  </svg>
@@ -1,47 +1,49 @@
1
1
  import { useTypewriter } from '@/lib/custom-hooks/useTypewriter';
2
2
  import React from 'react';
3
3
  import Markdown from 'react-markdown';
4
+ import type { Element } from 'hast';
4
5
 
5
6
  interface AgentResponseProps {
6
- messageContent: string;
7
- senderType: number;
8
- messageId: number;
9
- onType?: () => void;
7
+ messageContent: string;
8
+ senderType: number;
9
+ messageId: number;
10
+ onType?: () => void;
10
11
  }
11
12
 
12
13
  const seenMessagesRef = new Set<number>();
13
14
 
14
15
  const AgentResponse = ({ senderType, messageContent, messageId, onType }: AgentResponseProps) => {
15
- const shouldAnimate = (senderType === 2 || senderType === 3) && !seenMessagesRef.has(messageId);
16
- const animatedText = useTypewriter(messageContent, 20, onType);
17
- const finalMessage = shouldAnimate ? animatedText : messageContent;
16
+ const shouldAnimate = (senderType === 2 || senderType === 3) && !seenMessagesRef.has(messageId);
17
+ const animatedText = useTypewriter(messageContent, 20, onType);
18
+ const finalMessage = shouldAnimate ? animatedText : messageContent;
18
19
 
19
- // Mark message as "seen" after full animation
20
- if (shouldAnimate && finalMessage === messageContent) {
21
- seenMessagesRef.add(messageId);
22
- }
20
+ // Mark message as "seen" after full animation
21
+ if (shouldAnimate && finalMessage === messageContent) {
22
+ seenMessagesRef.add(messageId);
23
+ }
23
24
 
24
- return (
25
- <div
26
- className={`babylai-rounded-2xl babylai-p-4 ${senderType === 1
27
- ? 'babylai-bg-primary-500 !babylai-text-black-white-50 babylai-max-w-[220px]'
28
- : 'babylai-bg-black-white-50'
29
- }`}
30
- >
31
- <Markdown
32
- components={{
33
- p: ({ node, ...props }) => (
34
- <p
35
- className='babylai-m-0 babylai-leading-6 babylai-text-sm babylai-font-sans babylai-break-words'
36
- {...props}
37
- />
38
- )
39
- }}
40
- >
41
- {finalMessage}
42
- </Markdown>
43
- </div>
44
- )
45
- }
25
+ return (
26
+ <div
27
+ className={`babylai-rounded-2xl babylai-p-4 ${
28
+ senderType === 1
29
+ ? 'babylai-bg-primary-500 !babylai-text-black-white-50 babylai-max-w-[220px]'
30
+ : 'babylai-bg-black-white-50'
31
+ }`}
32
+ >
33
+ <Markdown
34
+ components={{
35
+ p: ({ node, ...props }: { node?: Element; [key: string]: any }) => (
36
+ <p
37
+ className='babylai-m-0 babylai-leading-6 babylai-text-sm babylai-font-sans babylai-break-words'
38
+ {...props}
39
+ />
40
+ ),
41
+ }}
42
+ >
43
+ {finalMessage}
44
+ </Markdown>
45
+ </div>
46
+ );
47
+ };
46
48
 
47
- export default AgentResponse
49
+ export default AgentResponse;
@@ -7,11 +7,10 @@ interface HeaderProps {
7
7
  }
8
8
 
9
9
  export function Header({ onClose }: HeaderProps) {
10
-
11
10
  return (
12
11
  <header className="babylai-flex babylai-w-full babylai-justify-between babylai-items-center">
13
- <Logo className="babylai-w-12 babylai-h-12" />
14
- <CloseCircle className="babylai-w-12 babylai-h-12 babylai-cursor-pointer" onClick={onClose} />
12
+ <Logo className="babylai-w-10 babylai-h-10" />
13
+ <CloseCircle className="babylai-w-10 babylai-h-10 babylai-cursor-pointer babylai-text-white" onClick={onClose} />
15
14
  </header>
16
15
  )
17
16
  }
@@ -1,17 +1,17 @@
1
- import * as signalR from '@microsoft/signalr'
1
+ import * as signalR from '@microsoft/signalr';
2
2
 
3
3
  export class ClientSignalRService {
4
- private static connection: signalR.HubConnection | null = null
5
- private static isConnected: boolean = false
6
- private static hubUrl: string = ''
4
+ private static connection: signalR.HubConnection | null = null;
5
+ private static isConnected: boolean = false;
6
+ private static hubUrl: string = '';
7
7
 
8
8
  static initialize(hubUrl: string) {
9
- this.hubUrl = hubUrl
9
+ this.hubUrl = hubUrl;
10
10
  }
11
11
 
12
12
  static async startConnection(sessionId: string, apiKey: string, onMessageReceived: Function) {
13
13
  // Prevent multiple connections
14
- if (this.isConnected) return
14
+ if (this.isConnected) return;
15
15
 
16
16
  // Build the SignalR connection
17
17
  this.connection = new signalR.HubConnectionBuilder()
@@ -19,38 +19,38 @@ export class ClientSignalRService {
19
19
  withCredentials: true, // Ensure credentials are passed with the WebSocket request
20
20
  transport: signalR.HttpTransportType.WebSockets | signalR.HttpTransportType.LongPolling,
21
21
  headers: {
22
- Authorization: `Bearer ${apiKey}`
23
- }
22
+ Authorization: `Bearer ${apiKey}`,
23
+ },
24
24
  })
25
25
  .configureLogging(signalR.LogLevel.Information)
26
- .build()
26
+ .build();
27
27
 
28
28
  // Define callback function for receiving messages
29
29
  this.connection.on('ReceiveMessage', (message: any, senderType: number, needsAgent: boolean) => {
30
- onMessageReceived(message, senderType, needsAgent)
31
- })
30
+ onMessageReceived(message, senderType, needsAgent);
31
+ });
32
32
 
33
33
  try {
34
34
  // Start the connection
35
- await this.connection.start()
35
+ await this.connection.start();
36
36
 
37
37
  // Set the isConnected flag after the connection is fully established
38
- this.isConnected = true
38
+ this.isConnected = true;
39
39
 
40
40
  // Join the session group after connection is fully established
41
- await this.joinGroup(sessionId)
41
+ await this.joinGroup(sessionId);
42
42
  } catch (error) {
43
- this.isConnected = false // Ensure isConnected is false if connection fails
44
- throw error
43
+ this.isConnected = false; // Ensure isConnected is false if connection fails
44
+ throw error;
45
45
  }
46
46
  }
47
47
 
48
48
  static async joinGroup(sessionId: string) {
49
49
  if (this.connection) {
50
50
  try {
51
- await this.connection.invoke('JoinGroup', sessionId)
51
+ await this.connection.invoke('JoinGroup', sessionId);
52
52
  } catch (error) {
53
- throw error
53
+ throw error;
54
54
  }
55
55
  }
56
56
  }
@@ -58,9 +58,9 @@ export class ClientSignalRService {
58
58
  static async leaveGroup(sessionId: string) {
59
59
  if (this.connection && this.isConnected) {
60
60
  try {
61
- await this.connection.invoke('LeaveGroup', sessionId)
61
+ await this.connection.invoke('LeaveGroup', sessionId);
62
62
  } catch (error) {
63
- throw error
63
+ throw error;
64
64
  }
65
65
  }
66
66
  }
@@ -68,16 +68,16 @@ export class ClientSignalRService {
68
68
  static async stopConnection() {
69
69
  if (this.connection && this.isConnected) {
70
70
  try {
71
- await this.connection.stop()
72
- this.isConnected = false
73
- this.connection = null
71
+ await this.connection.stop();
72
+ this.isConnected = false;
73
+ this.connection = null;
74
74
  } catch (error) {
75
- throw error
75
+ throw error;
76
76
  }
77
77
  }
78
78
  }
79
79
 
80
80
  static isConnectionActive(): boolean {
81
- return this.isConnected
81
+ return this.isConnected;
82
82
  }
83
83
  }
package/src/core/api.ts CHANGED
@@ -1,70 +1,207 @@
1
- import { TokenResponse } from '../lib/types'
1
+ import { TokenResponse } from '../lib/types';
2
2
 
3
- let getTokenFunction: (() => Promise<TokenResponse>) | undefined = undefined
3
+ let getTokenFunction: (() => Promise<TokenResponse>) | undefined = undefined;
4
+ let baseUrl: string | null = null;
4
5
 
5
- let baseUrl: string | null = null
6
+ // Add request cache and connection optimization
7
+ const requestCache = new Map<string, { data: any; timestamp: number; ttl: number }>();
8
+ const CACHE_TTL = 30000; // 30 seconds cache for non-critical requests
9
+ const pendingRequests = new Map<string, Promise<any>>();
6
10
 
7
11
  export function initializeAPI(url: string, getToken: () => Promise<TokenResponse>) {
8
- getTokenFunction = getToken
9
- baseUrl = url
12
+ getTokenFunction = getToken;
13
+ baseUrl = url;
10
14
  }
11
15
 
12
16
  export async function getValidToken(forceRefresh = false): Promise<string> {
13
17
  if (!getTokenFunction) {
14
- throw new Error('API module not initialized. Call initializeAPI(getToken) first.')
18
+ throw new Error('API module not initialized. Call initializeAPI(getToken) first.');
15
19
  }
16
20
 
17
- let storedToken = localStorage.getItem('chatbot-token')
18
- let storedExpiry = localStorage.getItem('chatbot-token-expiry')
19
- const currentTime = Math.floor(Date.now() / 1000)
20
-
21
- if (!storedToken || !storedExpiry || currentTime >= Number(storedExpiry) || forceRefresh) {
22
- const tokenResponse = await getTokenFunction()
23
- storedToken = tokenResponse.token
24
- storedExpiry = String(currentTime + (tokenResponse.expiresIn ?? 900))
25
-
26
- localStorage.setItem('chatbot-token', storedToken)
27
- localStorage.setItem('chatbot-token-expiry', storedExpiry)
21
+ let storedToken = localStorage.getItem('chatbot-token');
22
+ let storedExpiry = localStorage.getItem('chatbot-token-expiry');
23
+ const currentTime = Math.floor(Date.now() / 1000);
24
+
25
+ // Add buffer time to prevent token expiry during request
26
+ const bufferTime = 60; // 1 minute buffer
27
+ const isTokenExpiring = storedExpiry && currentTime >= Number(storedExpiry) - bufferTime;
28
+
29
+ if (!storedToken || !storedExpiry || isTokenExpiring || forceRefresh) {
30
+ try {
31
+ const tokenResponse = await getTokenFunction();
32
+ storedToken = tokenResponse.token;
33
+ storedExpiry = String(currentTime + (tokenResponse.expiresIn ?? 900));
34
+
35
+ localStorage.setItem('chatbot-token', storedToken);
36
+ localStorage.setItem('chatbot-token-expiry', storedExpiry);
37
+ } catch (error) {
38
+ console.error('Failed to refresh token:', error);
39
+ throw error;
40
+ }
28
41
  }
29
42
 
30
- return storedToken
43
+ return storedToken;
31
44
  }
32
45
 
46
+ // Optimized fetch with retry logic and connection pooling
33
47
  async function fetchWithAuth(url: string, options: RequestInit, retry = true): Promise<Response> {
34
- const headers = new Headers(options.headers)
35
- headers.set('Authorization', `Bearer ${await getValidToken()}`)
36
- options.headers = headers
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
+ console.warn('Token expired, refreshing...');
79
+ const newToken = await getValidToken(true);
80
+ headers.set('Authorization', `Bearer ${newToken}`);
81
+ options.headers = headers;
82
+
83
+ // Retry the request with new token
84
+ return fetchWithAuth(url, options, false);
85
+ }
86
+
87
+ return response;
88
+ } catch (error) {
89
+ clearTimeout(timeoutId);
90
+
91
+ if (error instanceof Error && error.name === 'AbortError') {
92
+ throw new Error('Request timeout - please try again');
93
+ }
94
+ throw error;
95
+ }
96
+ } catch (error) {
97
+ console.error('Fetch error:', error);
98
+ throw error;
99
+ }
100
+ }
37
101
 
38
- let response = await fetch(url, options)
102
+ // Cache management functions
103
+ function getCachedResponse(cacheKey: string): any | null {
104
+ const cached = requestCache.get(cacheKey);
105
+ if (cached && Date.now() < cached.timestamp + cached.ttl) {
106
+ return cached.data;
107
+ }
39
108
 
40
- if ((response.status === 401 || response.status === 403) && retry) {
41
- const newToken = await getValidToken(true)
42
- headers.set('Authorization', `Bearer ${newToken}`)
43
- options.headers = headers
44
- response = await fetch(url, options)
109
+ if (cached) {
110
+ requestCache.delete(cacheKey);
45
111
  }
46
112
 
47
- return response
113
+ return null;
48
114
  }
49
115
 
50
- export async function apiRequest(endpoint: string, method = 'GET', body: any = null) {
51
- if (!baseUrl) throw new Error('API not initialized')
116
+ function setCachedResponse(cacheKey: string, data: any, ttl: number = CACHE_TTL): void {
117
+ requestCache.set(cacheKey, {
118
+ data,
119
+ timestamp: Date.now(),
120
+ ttl,
121
+ });
122
+ }
52
123
 
53
- const url = `${baseUrl}/${endpoint}`
54
- const options: RequestInit = {
55
- method,
56
- headers: {
57
- 'Content-Type': 'application/json'
58
- },
59
- body: body ? JSON.stringify(body) : null
60
- }
124
+ // Deduplicate concurrent requests
125
+ function getDuplicateRequest(requestKey: string): Promise<any> | null {
126
+ return pendingRequests.get(requestKey) || null;
127
+ }
61
128
 
62
- const response = await fetchWithAuth(url, options)
129
+ function setPendingRequest(requestKey: string, promise: Promise<any>): void {
130
+ pendingRequests.set(requestKey, promise);
63
131
 
64
- if (!response.ok) {
65
- const errorData = await response.json()
66
- throw new Error(errorData.message || 'API request failed')
132
+ // Clean up after request completes
133
+ promise.finally(() => {
134
+ pendingRequests.delete(requestKey);
135
+ });
136
+ }
137
+
138
+ export async function apiRequest(
139
+ endpoint: string,
140
+ method = 'GET',
141
+ body: any = null,
142
+ options: { cache?: boolean; timeout?: number } = {}
143
+ ) {
144
+ if (!baseUrl) throw new Error('API not initialized');
145
+
146
+ const url = `${baseUrl}/${endpoint}`;
147
+ const requestKey = `${method}:${endpoint}:${JSON.stringify(body)}`;
148
+
149
+ // Check for duplicate in-flight requests
150
+ const duplicateRequest = getDuplicateRequest(requestKey);
151
+ if (duplicateRequest) {
152
+ return duplicateRequest;
67
153
  }
68
154
 
69
- return response
155
+ // Check cache for GET requests (except real-time endpoints)
156
+ if (method === 'GET' && options.cache !== false && !endpoint.includes('/send-message')) {
157
+ const cached = getCachedResponse(requestKey);
158
+ if (cached) {
159
+ return Promise.resolve(cached);
160
+ }
161
+ }
162
+
163
+ const requestOptions: RequestInit = {
164
+ method,
165
+ headers: {
166
+ 'Content-Type': 'application/json',
167
+ 'Cache-Control': method === 'GET' ? 'max-age=30' : 'no-cache',
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
+ const errorData = await response.json();
181
+ errorMessage = errorData.message || errorMessage;
182
+ } catch {
183
+ errorMessage = `HTTP ${response.status}: ${response.statusText}`;
184
+ }
185
+
186
+ throw new Error(errorMessage);
187
+ }
188
+
189
+ // Cache successful GET responses
190
+ if (method === 'GET' && options.cache !== false) {
191
+ const responseData = await response.clone();
192
+ const data = await responseData.json();
193
+ setCachedResponse(requestKey, { json: () => Promise.resolve(data) });
194
+ }
195
+
196
+ return response;
197
+ } catch (error) {
198
+ console.error(`API request failed for ${endpoint}:`, error);
199
+ throw error;
200
+ }
201
+ })();
202
+
203
+ // Track pending request
204
+ setPendingRequest(requestKey, requestPromise);
205
+
206
+ return requestPromise;
70
207
  }