@aslaluroba/help-center-react 2.0.4 → 2.0.6
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/core/api.d.ts +4 -1
- package/dist/index.d.ts +3 -2
- package/dist/index.esm.js +994 -25294
- package/dist/index.esm.js.map +1 -1
- package/dist/index.js +995 -25295
- package/dist/index.js.map +1 -1
- package/dist/lib/config.d.ts +1 -1
- package/dist/lib/types.d.ts +4 -0
- package/dist/ui/chatbot-popup/chat-window-screen/footer.d.ts +1 -0
- package/dist/ui/chatbot-popup/chat-window-screen/index.d.ts +1 -1
- package/dist/ui/help-center.d.ts +1 -1
- package/dist/ui/help-popup.d.ts +9 -3
- package/dist/ui/review-dialog/index.d.ts +8 -0
- package/dist/ui/review-dialog/rating.d.ts +12 -0
- package/package.json +26 -5
- package/src/assets/icons/arrowRight.svg +1 -1
- package/src/assets/icons/closeCircle.svg +1 -1
- package/src/components/ui/agent-response/agent-response.tsx +36 -34
- package/src/components/ui/header.tsx +2 -3
- package/src/core/SignalRService.ts +25 -25
- package/src/core/api.ts +180 -44
- package/src/globals.css +0 -9
- package/src/index.ts +3 -2
- package/src/lib/config.ts +25 -25
- package/src/lib/types.ts +5 -0
- package/src/locales/ar.json +18 -1
- package/src/locales/en.json +26 -8
- package/src/ui/chatbot-popup/chat-window-screen/footer.tsx +31 -33
- package/src/ui/chatbot-popup/chat-window-screen/header.tsx +47 -53
- package/src/ui/chatbot-popup/chat-window-screen/index.tsx +182 -88
- package/src/ui/chatbot-popup/options-list-screen/header.tsx +24 -20
- package/src/ui/chatbot-popup/options-list-screen/index.tsx +24 -24
- package/src/ui/chatbot-popup/options-list-screen/option-card.tsx +9 -4
- package/src/ui/help-center.tsx +189 -159
- package/src/ui/help-popup.tsx +241 -165
- package/src/ui/review-dialog/index.tsx +106 -0
- package/src/ui/review-dialog/rating.tsx +78 -0
- package/tsconfig.json +48 -0
package/dist/lib/config.d.ts
CHANGED
package/dist/lib/types.d.ts
CHANGED
|
@@ -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
|
|
9
|
+
export declare const ChatWindow: React.MemoExoticComponent<({ onSendMessage, messages, assistantStatus }: ChatWindowProps) => React.JSX.Element>;
|
|
10
10
|
export {};
|
package/dist/ui/help-center.d.ts
CHANGED
|
@@ -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 {};
|
package/dist/ui/help-popup.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
|
-
import { HelpScreenData, Message, Option } from '
|
|
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
|
|
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.
|
|
6
|
+
"version": "2.0.6",
|
|
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
|
-
"
|
|
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="
|
|
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
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
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
|
-
|
|
16
|
-
|
|
17
|
-
|
|
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
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
20
|
+
// Mark message as "seen" after full animation
|
|
21
|
+
if (shouldAnimate && finalMessage === messageContent) {
|
|
22
|
+
seenMessagesRef.add(messageId);
|
|
23
|
+
}
|
|
23
24
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
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-
|
|
14
|
-
<CloseCircle className="babylai-w-
|
|
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,71 +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
|
-
|
|
6
|
-
|
|
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>>();
|
|
7
10
|
|
|
8
11
|
export function initializeAPI(url: string, getToken: () => Promise<TokenResponse>) {
|
|
9
|
-
getTokenFunction = getToken
|
|
10
|
-
baseUrl = url
|
|
12
|
+
getTokenFunction = getToken;
|
|
13
|
+
baseUrl = url;
|
|
11
14
|
}
|
|
12
15
|
|
|
13
16
|
export async function getValidToken(forceRefresh = false): Promise<string> {
|
|
14
17
|
if (!getTokenFunction) {
|
|
15
|
-
throw new Error('API module not initialized. Call initializeAPI(getToken) first.')
|
|
18
|
+
throw new Error('API module not initialized. Call initializeAPI(getToken) first.');
|
|
16
19
|
}
|
|
17
20
|
|
|
18
|
-
let storedToken = localStorage.getItem('chatbot-token')
|
|
19
|
-
let storedExpiry = localStorage.getItem('chatbot-token-expiry')
|
|
20
|
-
const currentTime = Math.floor(Date.now() / 1000)
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
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
|
+
}
|
|
29
41
|
}
|
|
30
42
|
|
|
31
|
-
return storedToken
|
|
43
|
+
return storedToken;
|
|
32
44
|
}
|
|
33
45
|
|
|
46
|
+
// Optimized fetch with retry logic and connection pooling
|
|
34
47
|
async function fetchWithAuth(url: string, options: RequestInit, retry = true): Promise<Response> {
|
|
35
|
-
const headers = new Headers(options.headers)
|
|
36
|
-
|
|
37
|
-
|
|
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
|
+
}
|
|
38
101
|
|
|
39
|
-
|
|
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
|
+
}
|
|
40
108
|
|
|
41
|
-
if (
|
|
42
|
-
|
|
43
|
-
headers.set('Authorization', `Bearer ${newToken}`)
|
|
44
|
-
options.headers = headers
|
|
45
|
-
response = await fetch(url, options)
|
|
109
|
+
if (cached) {
|
|
110
|
+
requestCache.delete(cacheKey);
|
|
46
111
|
}
|
|
47
112
|
|
|
48
|
-
return
|
|
113
|
+
return null;
|
|
49
114
|
}
|
|
50
115
|
|
|
51
|
-
|
|
52
|
-
|
|
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
|
+
}
|
|
53
123
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
'Content-Type': 'application/json'
|
|
59
|
-
},
|
|
60
|
-
body: body ? JSON.stringify(body) : null
|
|
61
|
-
}
|
|
124
|
+
// Deduplicate concurrent requests
|
|
125
|
+
function getDuplicateRequest(requestKey: string): Promise<any> | null {
|
|
126
|
+
return pendingRequests.get(requestKey) || null;
|
|
127
|
+
}
|
|
62
128
|
|
|
63
|
-
|
|
129
|
+
function setPendingRequest(requestKey: string, promise: Promise<any>): void {
|
|
130
|
+
pendingRequests.set(requestKey, promise);
|
|
64
131
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
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;
|
|
68
153
|
}
|
|
69
154
|
|
|
70
|
-
|
|
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;
|
|
71
207
|
}
|