@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.
Files changed (100) hide show
  1. package/README.md +176 -0
  2. package/dist/components/index.d.ts +1 -0
  3. package/dist/components/shared/Button/button.d.ts +11 -0
  4. package/dist/components/shared/Button/index.d.ts +1 -0
  5. package/dist/components/shared/Card/card.d.ts +11 -0
  6. package/dist/components/shared/Card/index.d.ts +1 -0
  7. package/dist/components/shared/index.d.ts +2 -0
  8. package/dist/components/ui/agent-response/agent-response.d.ts +9 -0
  9. package/dist/components/ui/header.d.ts +6 -0
  10. package/dist/core/ApiService.d.ts +16 -0
  11. package/dist/core/SignalRService.d.ts +11 -0
  12. package/dist/core/api.d.ts +4 -0
  13. package/dist/core/token-service.d.ts +10 -0
  14. package/dist/i18n.d.ts +2 -0
  15. package/dist/index.d.ts +12 -0
  16. package/dist/index.esm.js +26076 -0
  17. package/dist/index.esm.js.map +1 -0
  18. package/dist/index.js +26110 -0
  19. package/dist/index.js.map +1 -0
  20. package/dist/lib/config.d.ts +19 -0
  21. package/dist/lib/custom-hooks/useTypewriter.d.ts +1 -0
  22. package/dist/lib/index.d.ts +1 -0
  23. package/dist/lib/types.d.ts +111 -0
  24. package/dist/lib/utils.d.ts +2 -0
  25. package/dist/ui/chatbot-popup/chat-window-screen/footer.d.ts +9 -0
  26. package/dist/ui/chatbot-popup/chat-window-screen/header.d.ts +11 -0
  27. package/dist/ui/chatbot-popup/chat-window-screen/index.d.ts +10 -0
  28. package/dist/ui/chatbot-popup/error-screen/index.d.ts +7 -0
  29. package/dist/ui/chatbot-popup/home-screen/card.d.ts +7 -0
  30. package/dist/ui/chatbot-popup/home-screen/chat-now-card.d.ts +6 -0
  31. package/dist/ui/chatbot-popup/home-screen/index.d.ts +7 -0
  32. package/dist/ui/chatbot-popup/loading-screen/index.d.ts +7 -0
  33. package/dist/ui/chatbot-popup/options-list-screen/expanded-option.d.ts +8 -0
  34. package/dist/ui/chatbot-popup/options-list-screen/header.d.ts +7 -0
  35. package/dist/ui/chatbot-popup/options-list-screen/index.d.ts +12 -0
  36. package/dist/ui/chatbot-popup/options-list-screen/option-card.d.ts +6 -0
  37. package/dist/ui/floating-message.d.ts +7 -0
  38. package/dist/ui/help-button.d.ts +6 -0
  39. package/dist/ui/help-center.d.ts +17 -0
  40. package/dist/ui/help-popup.d.ts +24 -0
  41. package/dist/useLocalTranslation.d.ts +5 -0
  42. package/package.json +86 -0
  43. package/src/assets/animatedLogo.gif +0 -0
  44. package/src/assets/icons/arrowRight.svg +3 -0
  45. package/src/assets/icons/chat.svg +4 -0
  46. package/src/assets/icons/close.svg +1 -0
  47. package/src/assets/icons/closeCircle.svg +3 -0
  48. package/src/assets/icons/closeCirclePrimary.svg +3 -0
  49. package/src/assets/icons/envelope.svg +3 -0
  50. package/src/assets/icons/seperator.svg +5 -0
  51. package/src/assets/icons/threeDots.svg +3 -0
  52. package/src/assets/icons/user.svg +3 -0
  53. package/src/assets/logo.svg +5 -0
  54. package/src/assets/logoColors.svg +5 -0
  55. package/src/assets/logo_ai.svg +14 -0
  56. package/src/assets/thinking-logo.svg +3 -0
  57. package/src/components/index.ts +1 -0
  58. package/src/components/shared/Button/button.tsx +46 -0
  59. package/src/components/shared/Button/index.ts +1 -0
  60. package/src/components/shared/Card/card.tsx +44 -0
  61. package/src/components/shared/Card/index.ts +1 -0
  62. package/src/components/shared/index.ts +2 -0
  63. package/src/components/ui/agent-response/agent-response.tsx +47 -0
  64. package/src/components/ui/agent-response/doc.md +88 -0
  65. package/src/components/ui/header.tsx +17 -0
  66. package/src/components/ui/index.ts +0 -0
  67. package/src/core/ApiService.ts +118 -0
  68. package/src/core/SignalRService.ts +83 -0
  69. package/src/core/api.ts +71 -0
  70. package/src/core/token-service.ts +35 -0
  71. package/src/globals.css +484 -0
  72. package/src/i18n.ts +17 -0
  73. package/src/index.ts +21 -0
  74. package/src/lib/config.ts +59 -0
  75. package/src/lib/custom-hooks/useTypewriter.ts +24 -0
  76. package/src/lib/index.ts +1 -0
  77. package/src/lib/types.ts +120 -0
  78. package/src/lib/utils.ts +6 -0
  79. package/src/locales/ar.json +13 -0
  80. package/src/locales/en.json +15 -0
  81. package/src/styles/tailwind.css +4 -0
  82. package/src/types/svg.d.ts +5 -0
  83. package/src/types.d.ts +9 -0
  84. package/src/ui/chatbot-popup/chat-window-screen/footer.tsx +42 -0
  85. package/src/ui/chatbot-popup/chat-window-screen/header.tsx +64 -0
  86. package/src/ui/chatbot-popup/chat-window-screen/index.tsx +103 -0
  87. package/src/ui/chatbot-popup/error-screen/index.tsx +22 -0
  88. package/src/ui/chatbot-popup/home-screen/card.tsx +34 -0
  89. package/src/ui/chatbot-popup/home-screen/chat-now-card.tsx +36 -0
  90. package/src/ui/chatbot-popup/home-screen/index.tsx +44 -0
  91. package/src/ui/chatbot-popup/loading-screen/index.tsx +33 -0
  92. package/src/ui/chatbot-popup/options-list-screen/expanded-option.tsx +38 -0
  93. package/src/ui/chatbot-popup/options-list-screen/header.tsx +38 -0
  94. package/src/ui/chatbot-popup/options-list-screen/index.tsx +59 -0
  95. package/src/ui/chatbot-popup/options-list-screen/option-card.tsx +20 -0
  96. package/src/ui/floating-message.tsx +25 -0
  97. package/src/ui/help-button.tsx +22 -0
  98. package/src/ui/help-center.tsx +303 -0
  99. package/src/ui/help-popup.tsx +264 -0
  100. 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
+ }
@@ -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
+ }