@aslaluroba/help-center-react 1.0.1
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/README.md +176 -0
- package/dist/components/index.d.ts +1 -0
- package/dist/components/shared/Button/button.d.ts +11 -0
- package/dist/components/shared/Button/index.d.ts +1 -0
- package/dist/components/shared/Card/card.d.ts +11 -0
- package/dist/components/shared/Card/index.d.ts +1 -0
- package/dist/components/shared/index.d.ts +2 -0
- package/dist/components/ui/agent-response/agent-response.d.ts +9 -0
- package/dist/components/ui/header.d.ts +6 -0
- package/dist/core/ApiService.d.ts +16 -0
- package/dist/core/SignalRService.d.ts +11 -0
- package/dist/core/api.d.ts +4 -0
- package/dist/core/token-service.d.ts +10 -0
- package/dist/i18n.d.ts +2 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.esm.js +26076 -0
- package/dist/index.esm.js.map +1 -0
- package/dist/index.js +26110 -0
- package/dist/index.js.map +1 -0
- package/dist/lib/config.d.ts +19 -0
- package/dist/lib/custom-hooks/useTypewriter.d.ts +1 -0
- package/dist/lib/index.d.ts +1 -0
- package/dist/lib/types.d.ts +111 -0
- package/dist/lib/utils.d.ts +2 -0
- package/dist/ui/chatbot-popup/chat-window-screen/footer.d.ts +9 -0
- package/dist/ui/chatbot-popup/chat-window-screen/header.d.ts +11 -0
- package/dist/ui/chatbot-popup/chat-window-screen/index.d.ts +10 -0
- package/dist/ui/chatbot-popup/error-screen/index.d.ts +7 -0
- package/dist/ui/chatbot-popup/home-screen/card.d.ts +7 -0
- package/dist/ui/chatbot-popup/home-screen/chat-now-card.d.ts +6 -0
- package/dist/ui/chatbot-popup/home-screen/index.d.ts +7 -0
- package/dist/ui/chatbot-popup/loading-screen/index.d.ts +7 -0
- package/dist/ui/chatbot-popup/options-list-screen/expanded-option.d.ts +8 -0
- package/dist/ui/chatbot-popup/options-list-screen/header.d.ts +7 -0
- package/dist/ui/chatbot-popup/options-list-screen/index.d.ts +12 -0
- package/dist/ui/chatbot-popup/options-list-screen/option-card.d.ts +6 -0
- package/dist/ui/floating-message.d.ts +7 -0
- package/dist/ui/help-button.d.ts +6 -0
- package/dist/ui/help-center.d.ts +17 -0
- package/dist/ui/help-popup.d.ts +24 -0
- package/dist/useLocalTranslation.d.ts +5 -0
- package/package.json +86 -0
- package/src/assets/animatedLogo.gif +0 -0
- package/src/assets/icons/arrowRight.svg +3 -0
- package/src/assets/icons/chat.svg +4 -0
- package/src/assets/icons/close.svg +1 -0
- package/src/assets/icons/closeCircle.svg +3 -0
- package/src/assets/icons/closeCirclePrimary.svg +3 -0
- package/src/assets/icons/envelope.svg +3 -0
- package/src/assets/icons/seperator.svg +5 -0
- package/src/assets/icons/threeDots.svg +3 -0
- package/src/assets/icons/user.svg +3 -0
- package/src/assets/logo.svg +5 -0
- package/src/assets/logoColors.svg +5 -0
- package/src/assets/logo_ai.svg +14 -0
- package/src/assets/thinking-logo.svg +3 -0
- package/src/components/index.ts +1 -0
- package/src/components/shared/Button/button.tsx +46 -0
- package/src/components/shared/Button/index.ts +1 -0
- package/src/components/shared/Card/card.tsx +44 -0
- package/src/components/shared/Card/index.ts +1 -0
- package/src/components/shared/index.ts +2 -0
- package/src/components/ui/agent-response/agent-response.tsx +47 -0
- package/src/components/ui/agent-response/doc.md +88 -0
- package/src/components/ui/header.tsx +17 -0
- package/src/components/ui/index.ts +0 -0
- package/src/core/ApiService.ts +118 -0
- package/src/core/SignalRService.ts +83 -0
- package/src/core/api.ts +71 -0
- package/src/core/token-service.ts +35 -0
- package/src/globals.css +484 -0
- package/src/i18n.ts +17 -0
- package/src/index.ts +21 -0
- package/src/lib/config.ts +59 -0
- package/src/lib/custom-hooks/useTypewriter.ts +24 -0
- package/src/lib/index.ts +1 -0
- package/src/lib/types.ts +120 -0
- package/src/lib/utils.ts +6 -0
- package/src/locales/ar.json +13 -0
- package/src/locales/en.json +15 -0
- package/src/styles/tailwind.css +4 -0
- package/src/types/svg.d.ts +5 -0
- package/src/types.d.ts +9 -0
- package/src/ui/chatbot-popup/chat-window-screen/footer.tsx +42 -0
- package/src/ui/chatbot-popup/chat-window-screen/header.tsx +64 -0
- package/src/ui/chatbot-popup/chat-window-screen/index.tsx +103 -0
- package/src/ui/chatbot-popup/error-screen/index.tsx +22 -0
- package/src/ui/chatbot-popup/home-screen/card.tsx +34 -0
- package/src/ui/chatbot-popup/home-screen/chat-now-card.tsx +36 -0
- package/src/ui/chatbot-popup/home-screen/index.tsx +44 -0
- package/src/ui/chatbot-popup/loading-screen/index.tsx +33 -0
- package/src/ui/chatbot-popup/options-list-screen/expanded-option.tsx +38 -0
- package/src/ui/chatbot-popup/options-list-screen/header.tsx +38 -0
- package/src/ui/chatbot-popup/options-list-screen/index.tsx +59 -0
- package/src/ui/chatbot-popup/options-list-screen/option-card.tsx +20 -0
- package/src/ui/floating-message.tsx +25 -0
- package/src/ui/help-button.tsx +22 -0
- package/src/ui/help-center.tsx +303 -0
- package/src/ui/help-popup.tsx +264 -0
- package/src/useLocalTranslation.ts +14 -0
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { useTypewriter } from '@/lib/custom-hooks/useTypewriter';
|
|
2
|
+
import React from 'react';
|
|
3
|
+
import Markdown from 'react-markdown';
|
|
4
|
+
|
|
5
|
+
interface AgentResponseProps {
|
|
6
|
+
messageContent: string;
|
|
7
|
+
senderType: number;
|
|
8
|
+
messageId: number;
|
|
9
|
+
onType?: () => void;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const seenMessagesRef = new Set<number>();
|
|
13
|
+
|
|
14
|
+
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;
|
|
18
|
+
|
|
19
|
+
// Mark message as "seen" after full animation
|
|
20
|
+
if (shouldAnimate && finalMessage === messageContent) {
|
|
21
|
+
seenMessagesRef.add(messageId);
|
|
22
|
+
}
|
|
23
|
+
|
|
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
|
+
}
|
|
46
|
+
|
|
47
|
+
export default AgentResponse
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
# AgentResponse Component
|
|
2
|
+
|
|
3
|
+
## Overview
|
|
4
|
+
|
|
5
|
+
The `AgentResponse` component renders a chat message in a conversational interface, supporting user and agent messages with optional typewriter animation for agent responses. It uses `react-markdown` to render the message content, allowing Markdown formatting, and applies conditional styling based on the sender type.
|
|
6
|
+
|
|
7
|
+
## Props
|
|
8
|
+
|
|
9
|
+
| Prop Name | Type | Required | Description |
|
|
10
|
+
|------------------|--------|----------|-----------------------------------------------------------------------------|
|
|
11
|
+
| `messageContent` | string | Yes | The content of the message to display, supports Markdown formatting. |
|
|
12
|
+
| `senderType` | number | Yes | The type of sender: `1` for user, `2` or `3` for agents (triggers animation). |
|
|
13
|
+
| `messageId` | number | Yes | A unique identifier for the message, used to track seen messages. |
|
|
14
|
+
| `onType` | () => void | No | A Callback function, used to trigger scroll on typing. |
|
|
15
|
+
|
|
16
|
+
## Features
|
|
17
|
+
|
|
18
|
+
- **Conditional Styling**:
|
|
19
|
+
- User messages (`senderType === 1`): Rendered with a primary background (`babylai-bg-primary-500`), white text (`!babylai-text-black-white-50`), and aligned to the right (`babylai-self-end`).
|
|
20
|
+
- Agent messages (`senderType === 2` or `3`): Rendered with a neutral background (`babylai-bg-black-white-50`).
|
|
21
|
+
- **Typewriter Animation**:
|
|
22
|
+
- Applied to agent messages (`senderType === 2` or `3`) on first render (when `messageId` is not in `seenMessagesRef`).
|
|
23
|
+
- Uses the `useTypewriter` hook with a 20ms delay per character.
|
|
24
|
+
- Animation is skipped for subsequent renders of the same `messageId`.
|
|
25
|
+
- **Markdown Support**:
|
|
26
|
+
- Renders `messageContent` using `react-markdown`.
|
|
27
|
+
- Customizes `<p>` tags with classes for consistent styling (`babylai-m-0`, `babylai-leading-6`, `babylai-text-sm`, `babylai-text-right`, `babylai-font-sans`).
|
|
28
|
+
- **Seen Message Tracking**:
|
|
29
|
+
- Maintains a global `seenMessagesRef` Set to track which `messageId`s have been fully animated.
|
|
30
|
+
- Adds `messageId` to `seenMessagesRef` when animation completes (`animatedText === messageContent`).
|
|
31
|
+
|
|
32
|
+
## Usage
|
|
33
|
+
|
|
34
|
+
### Example
|
|
35
|
+
|
|
36
|
+
```tsx
|
|
37
|
+
import { AgentResponse } from './agent-response';
|
|
38
|
+
|
|
39
|
+
function Chat() {
|
|
40
|
+
return (
|
|
41
|
+
<div>
|
|
42
|
+
{/* User message */}
|
|
43
|
+
<AgentResponse
|
|
44
|
+
senderType={1}
|
|
45
|
+
messageContent="Hello, how can I help?"
|
|
46
|
+
messageId={1}
|
|
47
|
+
/>
|
|
48
|
+
{/* Agent message with animation */}
|
|
49
|
+
<AgentResponse
|
|
50
|
+
senderType={2}
|
|
51
|
+
messageContent="Hi! I'm here to assist."
|
|
52
|
+
messageId={2}
|
|
53
|
+
/>
|
|
54
|
+
</div>
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
### Styling
|
|
60
|
+
|
|
61
|
+
The component uses the following Tailwind/custom classes:
|
|
62
|
+
- Container: `babylai-max-w-[80%] babylai-rounded-2xl babylai-p-4 babylai-text-right`
|
|
63
|
+
- User-specific: `babylai-bg-primary-500 !babylai-text-black-white-50 babylai-self-end`
|
|
64
|
+
- Agent-specific: `babylai-bg-black-white-50`
|
|
65
|
+
- Paragraph: `babylai-m-0 babylai-leading-6 babylai-text-sm babylai-text-right babylai-font-sans`
|
|
66
|
+
|
|
67
|
+
Ensure these classes are defined in your CSS (e.g., Tailwind configuration).
|
|
68
|
+
|
|
69
|
+
## Notes
|
|
70
|
+
|
|
71
|
+
- **Animation**: The typewriter effect is only applied to agent messages (`senderType` 2 or 3) on their first render. Once the animation completes, the `messageId` is marked as seen, and subsequent renders use the full message.
|
|
72
|
+
- **Markdown**: Supports basic Markdown (e.g., bold, italic). Complex Markdown features depend on `react-markdown` capabilities.
|
|
73
|
+
- **Performance**: The `seenMessagesRef` is a global `Set`, so ensure `messageId`s are unique to avoid conflicts across instances.
|
|
74
|
+
|
|
75
|
+
## Dependencies
|
|
76
|
+
|
|
77
|
+
- `react-markdown`: For rendering Markdown content.
|
|
78
|
+
- `@/lib/custom-hooks/useTypewriter`: Custom hook for typewriter animation.
|
|
79
|
+
|
|
80
|
+
## Testing
|
|
81
|
+
|
|
82
|
+
The component is tested with Jest and React Testing Library, covering:
|
|
83
|
+
- Rendering and styling for user (`senderType=1`) and agent (`senderType=2/3`) messages.
|
|
84
|
+
- Animation behavior for unseen and seen messages.
|
|
85
|
+
- `seenMessagesRef` updates when animation completes.
|
|
86
|
+
- Markdown rendering with custom paragraph styling.
|
|
87
|
+
|
|
88
|
+
See `agent-response.test.tsx` for the full test suite.
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
import CloseCircle from '../../assets/icons/closeCircle.svg'
|
|
3
|
+
import Logo from '../../assets/logo.svg'
|
|
4
|
+
|
|
5
|
+
interface HeaderProps {
|
|
6
|
+
onClose: () => void
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function Header({ onClose }: HeaderProps) {
|
|
10
|
+
|
|
11
|
+
return (
|
|
12
|
+
<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} />
|
|
15
|
+
</header>
|
|
16
|
+
)
|
|
17
|
+
}
|
|
File without changes
|
|
@@ -0,0 +1,118 @@
|
|
|
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
|
+
console.error('Error getting token:', error);
|
|
65
|
+
throw error;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return this.currentToken;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
async get<T>(endpoint: string, config?: AxiosRequestConfig): Promise<T> {
|
|
73
|
+
try {
|
|
74
|
+
const response = await this.axiosInstance.get<T>(endpoint, config);
|
|
75
|
+
return response.data;
|
|
76
|
+
} catch (error) {
|
|
77
|
+
this.handleError(error);
|
|
78
|
+
throw error;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
async post<T>(endpoint: string, data?: unknown, config?: AxiosRequestConfig): Promise<T> {
|
|
83
|
+
try {
|
|
84
|
+
const response = await this.axiosInstance.post<T>(endpoint, data, config);
|
|
85
|
+
return response.data;
|
|
86
|
+
} catch (error) {
|
|
87
|
+
this.handleError(error);
|
|
88
|
+
throw error;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
async put<T>(endpoint: string, data?: unknown, config?: AxiosRequestConfig): Promise<T> {
|
|
93
|
+
try {
|
|
94
|
+
const response = await this.axiosInstance.put<T>(endpoint, data, config);
|
|
95
|
+
return response.data;
|
|
96
|
+
} catch (error) {
|
|
97
|
+
this.handleError(error);
|
|
98
|
+
throw error;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
async delete<T>(endpoint: string, config?: AxiosRequestConfig): Promise<T> {
|
|
103
|
+
try {
|
|
104
|
+
const response = await this.axiosInstance.delete<T>(endpoint, config);
|
|
105
|
+
return response.data;
|
|
106
|
+
} catch (error) {
|
|
107
|
+
this.handleError(error);
|
|
108
|
+
throw error;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
private handleError(error: unknown) {
|
|
113
|
+
if (this.config.onError) {
|
|
114
|
+
this.config.onError(error instanceof Error ? error : new Error('Unknown error occurred'));
|
|
115
|
+
}
|
|
116
|
+
console.error('API Error:', error);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import * as signalR from '@microsoft/signalr'
|
|
2
|
+
|
|
3
|
+
export class ClientSignalRService {
|
|
4
|
+
private static connection: signalR.HubConnection | null = null
|
|
5
|
+
private static isConnected: boolean = false
|
|
6
|
+
private static hubUrl: string = ''
|
|
7
|
+
|
|
8
|
+
static initialize(hubUrl: string) {
|
|
9
|
+
this.hubUrl = hubUrl
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
static async startConnection(sessionId: string, apiKey: string, onMessageReceived: Function) {
|
|
13
|
+
// Prevent multiple connections
|
|
14
|
+
if (this.isConnected) return
|
|
15
|
+
|
|
16
|
+
// Build the SignalR connection
|
|
17
|
+
this.connection = new signalR.HubConnectionBuilder()
|
|
18
|
+
.withUrl(`${this.hubUrl}/clientHub?access_token=${encodeURIComponent(apiKey)}`, {
|
|
19
|
+
withCredentials: true, // Ensure credentials are passed with the WebSocket request
|
|
20
|
+
transport: signalR.HttpTransportType.WebSockets | signalR.HttpTransportType.LongPolling,
|
|
21
|
+
headers: {
|
|
22
|
+
Authorization: `Bearer ${apiKey}`
|
|
23
|
+
}
|
|
24
|
+
})
|
|
25
|
+
.configureLogging(signalR.LogLevel.Information)
|
|
26
|
+
.build()
|
|
27
|
+
|
|
28
|
+
// Define callback function for receiving messages
|
|
29
|
+
this.connection.on('ReceiveMessage', (message: any, senderType: number, needsAgent: boolean) => {
|
|
30
|
+
onMessageReceived(message, senderType, needsAgent)
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
try {
|
|
34
|
+
// Start the connection
|
|
35
|
+
await this.connection.start()
|
|
36
|
+
|
|
37
|
+
// Set the isConnected flag after the connection is fully established
|
|
38
|
+
this.isConnected = true
|
|
39
|
+
|
|
40
|
+
// Join the session group after connection is fully established
|
|
41
|
+
await this.joinGroup(sessionId)
|
|
42
|
+
} catch (error) {
|
|
43
|
+
this.isConnected = false // Ensure isConnected is false if connection fails
|
|
44
|
+
throw error
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
static async joinGroup(sessionId: string) {
|
|
49
|
+
if (this.connection) {
|
|
50
|
+
try {
|
|
51
|
+
await this.connection.invoke('JoinGroup', sessionId)
|
|
52
|
+
} catch (error) {
|
|
53
|
+
throw error
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
static async leaveGroup(sessionId: string) {
|
|
59
|
+
if (this.connection && this.isConnected) {
|
|
60
|
+
try {
|
|
61
|
+
await this.connection.invoke('LeaveGroup', sessionId)
|
|
62
|
+
} catch (error) {
|
|
63
|
+
throw error
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
static async stopConnection() {
|
|
69
|
+
if (this.connection && this.isConnected) {
|
|
70
|
+
try {
|
|
71
|
+
await this.connection.stop()
|
|
72
|
+
this.isConnected = false
|
|
73
|
+
this.connection = null
|
|
74
|
+
} catch (error) {
|
|
75
|
+
throw error
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
static isConnectionActive(): boolean {
|
|
81
|
+
return this.isConnected
|
|
82
|
+
}
|
|
83
|
+
}
|
package/src/core/api.ts
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { TokenResponse } from '../lib/types'
|
|
2
|
+
|
|
3
|
+
let getTokenFunction: (() => Promise<TokenResponse>) | undefined = undefined
|
|
4
|
+
|
|
5
|
+
console.log('🚀 ~ getTokenFunction:', getTokenFunction)
|
|
6
|
+
let baseUrl: string | null = null
|
|
7
|
+
|
|
8
|
+
export function initializeAPI(url: string, getToken: () => Promise<TokenResponse>) {
|
|
9
|
+
getTokenFunction = getToken
|
|
10
|
+
baseUrl = url
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export async function getValidToken(forceRefresh = false): Promise<string> {
|
|
14
|
+
if (!getTokenFunction) {
|
|
15
|
+
throw new Error('API module not initialized. Call initializeAPI(getToken) first.')
|
|
16
|
+
}
|
|
17
|
+
|
|
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
|
+
if (!storedToken || !storedExpiry || currentTime >= Number(storedExpiry) || forceRefresh) {
|
|
23
|
+
const tokenResponse = await getTokenFunction()
|
|
24
|
+
storedToken = tokenResponse.token
|
|
25
|
+
storedExpiry = String(currentTime + (tokenResponse.expiresIn ?? 900))
|
|
26
|
+
|
|
27
|
+
localStorage.setItem('chatbot-token', storedToken)
|
|
28
|
+
localStorage.setItem('chatbot-token-expiry', storedExpiry)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return storedToken
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
async function fetchWithAuth(url: string, options: RequestInit, retry = true): Promise<Response> {
|
|
35
|
+
const headers = new Headers(options.headers)
|
|
36
|
+
headers.set('Authorization', `Bearer ${await getValidToken()}`)
|
|
37
|
+
options.headers = headers
|
|
38
|
+
|
|
39
|
+
let response = await fetch(url, options)
|
|
40
|
+
|
|
41
|
+
if ((response.status === 401 || response.status === 403) && retry) {
|
|
42
|
+
const newToken = await getValidToken(true)
|
|
43
|
+
headers.set('Authorization', `Bearer ${newToken}`)
|
|
44
|
+
options.headers = headers
|
|
45
|
+
response = await fetch(url, options)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return response
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export async function apiRequest(endpoint: string, method = 'GET', body: any = null) {
|
|
52
|
+
if (!baseUrl) throw new Error('API not initialized')
|
|
53
|
+
|
|
54
|
+
const url = `${baseUrl}/${endpoint}`
|
|
55
|
+
const options: RequestInit = {
|
|
56
|
+
method,
|
|
57
|
+
headers: {
|
|
58
|
+
'Content-Type': 'application/json'
|
|
59
|
+
},
|
|
60
|
+
body: body ? JSON.stringify(body) : null
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const response = await fetchWithAuth(url, options)
|
|
64
|
+
|
|
65
|
+
if (!response.ok) {
|
|
66
|
+
const errorData = await response.json()
|
|
67
|
+
throw new Error(errorData.message || 'API request failed')
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return response
|
|
71
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
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
|
+
}
|