@uselingu/react-native-sdk 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +279 -0
- package/lib/commonjs/components/LinguChat.js +283 -0
- package/lib/commonjs/components/LinguChat.js.map +1 -0
- package/lib/commonjs/components/LinguWebView.js +203 -0
- package/lib/commonjs/components/LinguWebView.js.map +1 -0
- package/lib/commonjs/components/MessageBubble.js +91 -0
- package/lib/commonjs/components/MessageBubble.js.map +1 -0
- package/lib/commonjs/components/NativeChatUI.js +192 -0
- package/lib/commonjs/components/NativeChatUI.js.map +1 -0
- package/lib/commonjs/components/TypingIndicator.js +92 -0
- package/lib/commonjs/components/TypingIndicator.js.map +1 -0
- package/lib/commonjs/context/LinguProvider.js +47 -0
- package/lib/commonjs/context/LinguProvider.js.map +1 -0
- package/lib/commonjs/context/index.js +17 -0
- package/lib/commonjs/context/index.js.map +1 -0
- package/lib/commonjs/hooks/useLinguChat.js +295 -0
- package/lib/commonjs/hooks/useLinguChat.js.map +1 -0
- package/lib/commonjs/index.js +129 -0
- package/lib/commonjs/index.js.map +1 -0
- package/lib/commonjs/services/LinguAPI.js +196 -0
- package/lib/commonjs/services/LinguAPI.js.map +1 -0
- package/lib/commonjs/services/SessionManager.js +131 -0
- package/lib/commonjs/services/SessionManager.js.map +1 -0
- package/lib/commonjs/services/StorageManager.js +116 -0
- package/lib/commonjs/services/StorageManager.js.map +1 -0
- package/lib/commonjs/theme/ThemeProvider.js +49 -0
- package/lib/commonjs/theme/ThemeProvider.js.map +1 -0
- package/lib/commonjs/theme/index.js +28 -0
- package/lib/commonjs/theme/index.js.map +1 -0
- package/lib/commonjs/theme/theme.js +150 -0
- package/lib/commonjs/theme/theme.js.map +1 -0
- package/lib/commonjs/types/index.js +2 -0
- package/lib/commonjs/types/index.js.map +1 -0
- package/lib/commonjs/utils/animations.js +118 -0
- package/lib/commonjs/utils/animations.js.map +1 -0
- package/lib/commonjs/utils/formatters.js +89 -0
- package/lib/commonjs/utils/formatters.js.map +1 -0
- package/lib/commonjs/utils/index.js +28 -0
- package/lib/commonjs/utils/index.js.map +1 -0
- package/lib/module/components/LinguChat.js +277 -0
- package/lib/module/components/LinguChat.js.map +1 -0
- package/lib/module/components/LinguWebView.js +195 -0
- package/lib/module/components/LinguWebView.js.map +1 -0
- package/lib/module/components/MessageBubble.js +83 -0
- package/lib/module/components/MessageBubble.js.map +1 -0
- package/lib/module/components/NativeChatUI.js +184 -0
- package/lib/module/components/NativeChatUI.js.map +1 -0
- package/lib/module/components/TypingIndicator.js +84 -0
- package/lib/module/components/TypingIndicator.js.map +1 -0
- package/lib/module/context/LinguProvider.js +38 -0
- package/lib/module/context/LinguProvider.js.map +1 -0
- package/lib/module/context/index.js +6 -0
- package/lib/module/context/index.js.map +1 -0
- package/lib/module/hooks/useLinguChat.js +288 -0
- package/lib/module/hooks/useLinguChat.js.map +1 -0
- package/lib/module/index.js +30 -0
- package/lib/module/index.js.map +1 -0
- package/lib/module/services/LinguAPI.js +190 -0
- package/lib/module/services/LinguAPI.js.map +1 -0
- package/lib/module/services/SessionManager.js +124 -0
- package/lib/module/services/SessionManager.js.map +1 -0
- package/lib/module/services/StorageManager.js +108 -0
- package/lib/module/services/StorageManager.js.map +1 -0
- package/lib/module/theme/ThemeProvider.js +40 -0
- package/lib/module/theme/ThemeProvider.js.map +1 -0
- package/lib/module/theme/index.js +7 -0
- package/lib/module/theme/index.js.map +1 -0
- package/lib/module/theme/theme.js +142 -0
- package/lib/module/theme/theme.js.map +1 -0
- package/lib/module/types/index.js +2 -0
- package/lib/module/types/index.js.map +1 -0
- package/lib/module/utils/animations.js +106 -0
- package/lib/module/utils/animations.js.map +1 -0
- package/lib/module/utils/formatters.js +78 -0
- package/lib/module/utils/formatters.js.map +1 -0
- package/lib/module/utils/index.js +7 -0
- package/lib/module/utils/index.js.map +1 -0
- package/lib/typescript/components/LinguChat.d.ts +18 -0
- package/lib/typescript/components/LinguChat.d.ts.map +1 -0
- package/lib/typescript/components/LinguWebView.d.ts +16 -0
- package/lib/typescript/components/LinguWebView.d.ts.map +1 -0
- package/lib/typescript/components/MessageBubble.d.ts +13 -0
- package/lib/typescript/components/MessageBubble.d.ts.map +1 -0
- package/lib/typescript/components/NativeChatUI.d.ts +16 -0
- package/lib/typescript/components/NativeChatUI.d.ts.map +1 -0
- package/lib/typescript/components/TypingIndicator.d.ts +7 -0
- package/lib/typescript/components/TypingIndicator.d.ts.map +1 -0
- package/lib/typescript/context/LinguProvider.d.ts +32 -0
- package/lib/typescript/context/LinguProvider.d.ts.map +1 -0
- package/lib/typescript/context/index.d.ts +5 -0
- package/lib/typescript/context/index.d.ts.map +1 -0
- package/lib/typescript/hooks/useLinguChat.d.ts +18 -0
- package/lib/typescript/hooks/useLinguChat.d.ts.map +1 -0
- package/lib/typescript/index.d.ts +19 -0
- package/lib/typescript/index.d.ts.map +1 -0
- package/lib/typescript/services/LinguAPI.d.ts +39 -0
- package/lib/typescript/services/LinguAPI.d.ts.map +1 -0
- package/lib/typescript/services/SessionManager.d.ts +46 -0
- package/lib/typescript/services/SessionManager.d.ts.map +1 -0
- package/lib/typescript/services/StorageManager.d.ts +36 -0
- package/lib/typescript/services/StorageManager.d.ts.map +1 -0
- package/lib/typescript/theme/ThemeProvider.d.ts +23 -0
- package/lib/typescript/theme/ThemeProvider.d.ts.map +1 -0
- package/lib/typescript/theme/index.d.ts +6 -0
- package/lib/typescript/theme/index.d.ts.map +1 -0
- package/lib/typescript/theme/theme.d.ts +130 -0
- package/lib/typescript/theme/theme.d.ts.map +1 -0
- package/lib/typescript/types/index.d.ts +50 -0
- package/lib/typescript/types/index.d.ts.map +1 -0
- package/lib/typescript/utils/animations.d.ts +34 -0
- package/lib/typescript/utils/animations.d.ts.map +1 -0
- package/lib/typescript/utils/formatters.d.ts +25 -0
- package/lib/typescript/utils/formatters.d.ts.map +1 -0
- package/lib/typescript/utils/index.d.ts +6 -0
- package/lib/typescript/utils/index.d.ts.map +1 -0
- package/package.json +98 -0
- package/src/components/LinguChat.tsx +309 -0
- package/src/components/LinguWebView.tsx +216 -0
- package/src/components/MessageBubble.tsx +120 -0
- package/src/components/NativeChatUI.tsx +247 -0
- package/src/components/TypingIndicator.tsx +84 -0
- package/src/context/LinguProvider.tsx +53 -0
- package/src/context/index.ts +5 -0
- package/src/hooks/useLinguChat.ts +314 -0
- package/src/index.ts +43 -0
- package/src/services/LinguAPI.ts +214 -0
- package/src/services/SessionManager.ts +131 -0
- package/src/services/StorageManager.ts +111 -0
- package/src/theme/ThemeProvider.tsx +58 -0
- package/src/theme/index.ts +6 -0
- package/src/theme/theme.ts +254 -0
- package/src/types/index.ts +59 -0
- package/src/utils/animations.ts +141 -0
- package/src/utils/formatters.ts +80 -0
- package/src/utils/index.ts +6 -0
package/src/index.ts
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lingu React Native SDK
|
|
3
|
+
* Standalone AI chat SDK with native UI and theme support
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
// Main components
|
|
7
|
+
export { LinguChat } from './components/LinguChat';
|
|
8
|
+
export { NativeChatUI } from './components/NativeChatUI';
|
|
9
|
+
export { LinguWebView } from './components/LinguWebView';
|
|
10
|
+
export { MessageBubble } from './components/MessageBubble';
|
|
11
|
+
export { TypingIndicator } from './components/TypingIndicator';
|
|
12
|
+
|
|
13
|
+
// Context and state management
|
|
14
|
+
export { LinguProvider, useLinguContext } from './context';
|
|
15
|
+
|
|
16
|
+
// Theme system
|
|
17
|
+
export { ThemeProvider, useTheme, createTheme } from './theme';
|
|
18
|
+
export type { Theme, ThemeColors } from './theme';
|
|
19
|
+
|
|
20
|
+
// Hooks
|
|
21
|
+
export { useLinguChat } from './hooks/useLinguChat';
|
|
22
|
+
|
|
23
|
+
// Services
|
|
24
|
+
export { LinguAPI } from './services/LinguAPI';
|
|
25
|
+
export { SessionManager } from './services/SessionManager';
|
|
26
|
+
export { StorageManager } from './services/StorageManager';
|
|
27
|
+
|
|
28
|
+
// Utils
|
|
29
|
+
export * from './utils';
|
|
30
|
+
|
|
31
|
+
// Types
|
|
32
|
+
export type {
|
|
33
|
+
LinguConfig,
|
|
34
|
+
ChatPosition,
|
|
35
|
+
ThemeMode,
|
|
36
|
+
Message,
|
|
37
|
+
MessageSender,
|
|
38
|
+
WidgetConfig,
|
|
39
|
+
SessionData,
|
|
40
|
+
ApiResponse,
|
|
41
|
+
ChatResponse,
|
|
42
|
+
SessionResponse,
|
|
43
|
+
} from './types';
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lingu API Client
|
|
3
|
+
* Handles all communication with the Lingu backend API
|
|
4
|
+
* Now with comprehensive logging
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import axios, { AxiosInstance, AxiosError } from 'axios';
|
|
8
|
+
import type {
|
|
9
|
+
WidgetConfig,
|
|
10
|
+
ChatResponse,
|
|
11
|
+
ApiResponse,
|
|
12
|
+
} from '../types';
|
|
13
|
+
|
|
14
|
+
const LOG_PREFIX = '[LinguAPI]';
|
|
15
|
+
|
|
16
|
+
export class LinguAPI {
|
|
17
|
+
private client: AxiosInstance;
|
|
18
|
+
private apiKey: string;
|
|
19
|
+
private sessionId: string | null = null;
|
|
20
|
+
|
|
21
|
+
constructor(apiKey: string, baseURL: string = 'https://api.uselingu.app/api') {
|
|
22
|
+
console.log(`${LOG_PREFIX} Initializing API client:`, {
|
|
23
|
+
baseURL,
|
|
24
|
+
apiKeyPrefix: apiKey.substring(0, 20) + '...',
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
this.apiKey = apiKey;
|
|
28
|
+
this.client = axios.create({
|
|
29
|
+
baseURL,
|
|
30
|
+
headers: {
|
|
31
|
+
'Content-Type': 'application/json',
|
|
32
|
+
},
|
|
33
|
+
timeout: 30000, // 30 seconds
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
// Add request interceptor for logging
|
|
37
|
+
this.client.interceptors.request.use(
|
|
38
|
+
(config) => {
|
|
39
|
+
console.log(`${LOG_PREFIX} → Request:`, {
|
|
40
|
+
method: config.method?.toUpperCase(),
|
|
41
|
+
url: config.url,
|
|
42
|
+
hasData: !!config.data,
|
|
43
|
+
});
|
|
44
|
+
return config;
|
|
45
|
+
},
|
|
46
|
+
(error) => {
|
|
47
|
+
console.error(`${LOG_PREFIX} ❌ Request setup failed:`, error);
|
|
48
|
+
return Promise.reject(error);
|
|
49
|
+
}
|
|
50
|
+
);
|
|
51
|
+
|
|
52
|
+
// Add response interceptor for error handling and logging
|
|
53
|
+
this.client.interceptors.response.use(
|
|
54
|
+
(response) => {
|
|
55
|
+
console.log(`${LOG_PREFIX} ← Response:`, {
|
|
56
|
+
status: response.status,
|
|
57
|
+
url: response.config.url,
|
|
58
|
+
dataSize: JSON.stringify(response.data).length,
|
|
59
|
+
});
|
|
60
|
+
return response;
|
|
61
|
+
},
|
|
62
|
+
(error: AxiosError) => {
|
|
63
|
+
if (error.response) {
|
|
64
|
+
console.error(`${LOG_PREFIX} ❌ HTTP Error:`, {
|
|
65
|
+
status: error.response.status,
|
|
66
|
+
statusText: error.response.statusText,
|
|
67
|
+
url: error.config?.url,
|
|
68
|
+
data: error.response.data,
|
|
69
|
+
});
|
|
70
|
+
} else if (error.request) {
|
|
71
|
+
console.error(`${LOG_PREFIX} ❌ Network Error:`, {
|
|
72
|
+
message: error.message,
|
|
73
|
+
url: error.config?.url,
|
|
74
|
+
baseURL: error.config?.baseURL,
|
|
75
|
+
});
|
|
76
|
+
} else {
|
|
77
|
+
console.error(`${LOG_PREFIX} ❌ Request Error:`, error.message);
|
|
78
|
+
}
|
|
79
|
+
return Promise.reject(error);
|
|
80
|
+
}
|
|
81
|
+
);
|
|
82
|
+
|
|
83
|
+
console.log(`${LOG_PREFIX} ✅ API client initialized successfully`);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Get widget configuration from the API
|
|
88
|
+
*/
|
|
89
|
+
async getConfig(): Promise<WidgetConfig> {
|
|
90
|
+
console.log(`${LOG_PREFIX} Fetching widget configuration...`);
|
|
91
|
+
|
|
92
|
+
try {
|
|
93
|
+
const startTime = Date.now();
|
|
94
|
+
const response = await this.client.get<ApiResponse<WidgetConfig>>(
|
|
95
|
+
'/chatbot-settings/widget/config',
|
|
96
|
+
{
|
|
97
|
+
headers: { 'X-API-Key': this.apiKey },
|
|
98
|
+
}
|
|
99
|
+
);
|
|
100
|
+
const duration = Date.now() - startTime;
|
|
101
|
+
|
|
102
|
+
console.log(`${LOG_PREFIX} ✅ Config loaded in ${duration}ms:`, {
|
|
103
|
+
theme: response.data.data.theme,
|
|
104
|
+
primaryColor: response.data.data.primaryColor,
|
|
105
|
+
brandName: response.data.data.brandName,
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
return response.data.data;
|
|
109
|
+
} catch (error) {
|
|
110
|
+
console.error(`${LOG_PREFIX} ❌ Failed to get config:`, error);
|
|
111
|
+
throw new Error('Failed to load widget configuration');
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Start a new chat session
|
|
117
|
+
* Note: Backend manages sessions automatically, we just generate a client-side ID
|
|
118
|
+
*/
|
|
119
|
+
async startSession(): Promise<string> {
|
|
120
|
+
console.log(`${LOG_PREFIX} Starting new chat session (client-side)...`);
|
|
121
|
+
|
|
122
|
+
try {
|
|
123
|
+
// Generate a unique session ID client-side
|
|
124
|
+
const sessionId = `session_${Date.now()}_${Math.random().toString(36).substring(7)}`;
|
|
125
|
+
this.sessionId = sessionId;
|
|
126
|
+
|
|
127
|
+
console.log(`${LOG_PREFIX} ✅ Session created:`, sessionId);
|
|
128
|
+
|
|
129
|
+
return this.sessionId;
|
|
130
|
+
} catch (error) {
|
|
131
|
+
console.error(`${LOG_PREFIX} ❌ Failed to start session:`, error);
|
|
132
|
+
throw new Error('Failed to start chat session');
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Send a message to the AI
|
|
138
|
+
*/
|
|
139
|
+
async sendMessage(message: string, sessionId: string): Promise<string> {
|
|
140
|
+
console.log(`${LOG_PREFIX} Sending message:`, {
|
|
141
|
+
sessionId,
|
|
142
|
+
messageLength: message.length,
|
|
143
|
+
preview: message.substring(0, 50) + (message.length > 50 ? '...' : ''),
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
try {
|
|
147
|
+
const startTime = Date.now();
|
|
148
|
+
const response = await this.client.post<ApiResponse<ChatResponse>>(
|
|
149
|
+
'/chat/widget',
|
|
150
|
+
{
|
|
151
|
+
apiKey: this.apiKey,
|
|
152
|
+
message,
|
|
153
|
+
sessionId,
|
|
154
|
+
}
|
|
155
|
+
);
|
|
156
|
+
const duration = Date.now() - startTime;
|
|
157
|
+
|
|
158
|
+
const aiResponse = response.data.data.message;
|
|
159
|
+
console.log(`${LOG_PREFIX} ✅ AI response received in ${duration}ms:`, {
|
|
160
|
+
responseLength: aiResponse.length,
|
|
161
|
+
preview: aiResponse.substring(0, 50) + (aiResponse.length > 50 ? '...' : ''),
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
return aiResponse;
|
|
165
|
+
} catch (error) {
|
|
166
|
+
console.error(`${LOG_PREFIX} ❌ Failed to send message:`, error);
|
|
167
|
+
throw new Error('Failed to send message');
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* End the current chat session
|
|
173
|
+
* Note: Backend doesn't have explicit session end, sessions timeout automatically
|
|
174
|
+
*/
|
|
175
|
+
async endSession(sessionId: string): Promise<void> {
|
|
176
|
+
console.log(`${LOG_PREFIX} Ending session (client-side only):`, sessionId);
|
|
177
|
+
|
|
178
|
+
try {
|
|
179
|
+
this.sessionId = null;
|
|
180
|
+
console.log(`${LOG_PREFIX} ✅ Session ended successfully`);
|
|
181
|
+
} catch (error) {
|
|
182
|
+
console.error(`${LOG_PREFIX} ⚠️ Failed to end session:`, error);
|
|
183
|
+
// Don't throw - session will timeout anyway
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Send heartbeat to keep session alive
|
|
189
|
+
*/
|
|
190
|
+
async sendHeartbeat(sessionId: string): Promise<void> {
|
|
191
|
+
console.log(`${LOG_PREFIX} Sending heartbeat for session:`, sessionId);
|
|
192
|
+
|
|
193
|
+
try {
|
|
194
|
+
// Send special heartbeat message
|
|
195
|
+
await this.client.post('/chat/widget', {
|
|
196
|
+
apiKey: this.apiKey,
|
|
197
|
+
message: '__HEARTBEAT__',
|
|
198
|
+
sessionId,
|
|
199
|
+
});
|
|
200
|
+
console.log(`${LOG_PREFIX} ✅ Heartbeat sent successfully`);
|
|
201
|
+
} catch (error) {
|
|
202
|
+
console.error(`${LOG_PREFIX} ⚠️ Heartbeat failed (will retry):`, error);
|
|
203
|
+
// Don't throw - will retry on next heartbeat
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Get the current session ID
|
|
209
|
+
*/
|
|
210
|
+
getSessionId(): string | null {
|
|
211
|
+
console.log(`${LOG_PREFIX} Current session ID:`, this.sessionId || 'none');
|
|
212
|
+
return this.sessionId;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Session Manager
|
|
3
|
+
* Handles session lifecycle, heartbeat, and cleanup
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { LinguAPI } from './LinguAPI';
|
|
7
|
+
|
|
8
|
+
export class SessionManager {
|
|
9
|
+
private api: LinguAPI;
|
|
10
|
+
private sessionId: string | null = null;
|
|
11
|
+
private heartbeatInterval: NodeJS.Timeout | null = null;
|
|
12
|
+
private heartbeatIntervalMs: number = 30000; // 30 seconds
|
|
13
|
+
|
|
14
|
+
private onSessionStart?: (sessionId: string) => void;
|
|
15
|
+
private onSessionEnd?: (sessionId: string) => void;
|
|
16
|
+
|
|
17
|
+
constructor(
|
|
18
|
+
api: LinguAPI,
|
|
19
|
+
callbacks?: {
|
|
20
|
+
onSessionStart?: (sessionId: string) => void;
|
|
21
|
+
onSessionEnd?: (sessionId: string) => void;
|
|
22
|
+
}
|
|
23
|
+
) {
|
|
24
|
+
this.api = api;
|
|
25
|
+
this.onSessionStart = callbacks?.onSessionStart;
|
|
26
|
+
this.onSessionEnd = callbacks?.onSessionEnd;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Start a new session
|
|
31
|
+
*/
|
|
32
|
+
async start(): Promise<string> {
|
|
33
|
+
try {
|
|
34
|
+
// End existing session if any
|
|
35
|
+
if (this.sessionId) {
|
|
36
|
+
await this.end();
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Start new session
|
|
40
|
+
this.sessionId = await this.api.startSession();
|
|
41
|
+
|
|
42
|
+
// Start heartbeat
|
|
43
|
+
this.startHeartbeat();
|
|
44
|
+
|
|
45
|
+
// Notify callback
|
|
46
|
+
this.onSessionStart?.(this.sessionId);
|
|
47
|
+
|
|
48
|
+
return this.sessionId;
|
|
49
|
+
} catch (error) {
|
|
50
|
+
console.error('[SessionManager] Failed to start session:', error);
|
|
51
|
+
throw error;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* End the current session
|
|
57
|
+
*/
|
|
58
|
+
async end(): Promise<void> {
|
|
59
|
+
if (!this.sessionId) return;
|
|
60
|
+
|
|
61
|
+
try {
|
|
62
|
+
// Stop heartbeat
|
|
63
|
+
this.stopHeartbeat();
|
|
64
|
+
|
|
65
|
+
// End session on server
|
|
66
|
+
await this.api.endSession(this.sessionId);
|
|
67
|
+
|
|
68
|
+
// Notify callback
|
|
69
|
+
this.onSessionEnd?.(this.sessionId);
|
|
70
|
+
|
|
71
|
+
this.sessionId = null;
|
|
72
|
+
} catch (error) {
|
|
73
|
+
console.error('[SessionManager] Failed to end session:', error);
|
|
74
|
+
// Still clear local session
|
|
75
|
+
this.sessionId = null;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Start sending heartbeats to keep session alive
|
|
81
|
+
*/
|
|
82
|
+
private startHeartbeat(): void {
|
|
83
|
+
if (this.heartbeatInterval) {
|
|
84
|
+
clearInterval(this.heartbeatInterval);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
this.heartbeatInterval = setInterval(() => {
|
|
88
|
+
if (this.sessionId) {
|
|
89
|
+
this.api.sendHeartbeat(this.sessionId).catch((error) => {
|
|
90
|
+
console.error('[SessionManager] Heartbeat failed:', error);
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
}, this.heartbeatIntervalMs);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Stop sending heartbeats
|
|
98
|
+
*/
|
|
99
|
+
private stopHeartbeat(): void {
|
|
100
|
+
if (this.heartbeatInterval) {
|
|
101
|
+
clearInterval(this.heartbeatInterval);
|
|
102
|
+
this.heartbeatInterval = null;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Get the current session ID
|
|
108
|
+
*/
|
|
109
|
+
getSessionId(): string | null {
|
|
110
|
+
return this.sessionId;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Check if a session is active
|
|
115
|
+
*/
|
|
116
|
+
isActive(): boolean {
|
|
117
|
+
return this.sessionId !== null;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Cleanup on unmount
|
|
122
|
+
*/
|
|
123
|
+
cleanup(): void {
|
|
124
|
+
this.stopHeartbeat();
|
|
125
|
+
if (this.sessionId) {
|
|
126
|
+
// Fire and forget - don't wait for response
|
|
127
|
+
this.api.endSession(this.sessionId).catch(() => { });
|
|
128
|
+
this.sessionId = null;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Storage Manager
|
|
3
|
+
* Handles message persistence using AsyncStorage
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import AsyncStorage from '@react-native-async-storage/async-storage';
|
|
7
|
+
import type { Message } from '../types';
|
|
8
|
+
|
|
9
|
+
const STORAGE_KEY_PREFIX = '@lingu_chat_';
|
|
10
|
+
const MAX_STORED_MESSAGES = 100; // Limit stored messages per session
|
|
11
|
+
|
|
12
|
+
export class StorageManager {
|
|
13
|
+
/**
|
|
14
|
+
* Save messages for a session
|
|
15
|
+
*/
|
|
16
|
+
static async saveMessages(sessionId: string, messages: Message[]): Promise<void> {
|
|
17
|
+
try {
|
|
18
|
+
const key = `${STORAGE_KEY_PREFIX}${sessionId}`;
|
|
19
|
+
// Keep only the last MAX_STORED_MESSAGES
|
|
20
|
+
const messagesToStore = messages.slice(-MAX_STORED_MESSAGES);
|
|
21
|
+
await AsyncStorage.setItem(key, JSON.stringify(messagesToStore));
|
|
22
|
+
} catch (error) {
|
|
23
|
+
console.error('[StorageManager] Failed to save messages:', error);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Load messages for a session
|
|
29
|
+
*/
|
|
30
|
+
static async loadMessages(sessionId: string): Promise<Message[]> {
|
|
31
|
+
try {
|
|
32
|
+
const key = `${STORAGE_KEY_PREFIX}${sessionId}`;
|
|
33
|
+
const data = await AsyncStorage.getItem(key);
|
|
34
|
+
if (data) {
|
|
35
|
+
const messages = JSON.parse(data);
|
|
36
|
+
// Convert timestamp strings back to Date objects
|
|
37
|
+
return messages.map((msg: any) => ({
|
|
38
|
+
...msg,
|
|
39
|
+
timestamp: new Date(msg.timestamp),
|
|
40
|
+
}));
|
|
41
|
+
}
|
|
42
|
+
return [];
|
|
43
|
+
} catch (error) {
|
|
44
|
+
console.error('[StorageManager] Failed to load messages:', error);
|
|
45
|
+
return [];
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Clear messages for a session
|
|
51
|
+
*/
|
|
52
|
+
static async clearMessages(sessionId: string): Promise<void> {
|
|
53
|
+
try {
|
|
54
|
+
const key = `${STORAGE_KEY_PREFIX}${sessionId}`;
|
|
55
|
+
await AsyncStorage.removeItem(key);
|
|
56
|
+
} catch (error) {
|
|
57
|
+
console.error('[StorageManager] Failed to clear messages:', error);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Get all stored session IDs
|
|
63
|
+
*/
|
|
64
|
+
static async getAllSessionIds(): Promise<string[]> {
|
|
65
|
+
try {
|
|
66
|
+
const keys = await AsyncStorage.getAllKeys();
|
|
67
|
+
return keys
|
|
68
|
+
.filter((key) => key.startsWith(STORAGE_KEY_PREFIX))
|
|
69
|
+
.map((key) => key.replace(STORAGE_KEY_PREFIX, ''));
|
|
70
|
+
} catch (error) {
|
|
71
|
+
console.error('[StorageManager] Failed to get session IDs:', error);
|
|
72
|
+
return [];
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Save current session ID
|
|
78
|
+
*/
|
|
79
|
+
static async saveCurrentSessionId(sessionId: string): Promise<void> {
|
|
80
|
+
try {
|
|
81
|
+
await AsyncStorage.setItem(`${STORAGE_KEY_PREFIX}current`, sessionId);
|
|
82
|
+
} catch (error) {
|
|
83
|
+
console.error('[StorageManager] Failed to save current session ID:', error);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Get current session ID
|
|
89
|
+
*/
|
|
90
|
+
static async getCurrentSessionId(): Promise<string | null> {
|
|
91
|
+
try {
|
|
92
|
+
return await AsyncStorage.getItem(`${STORAGE_KEY_PREFIX}current`);
|
|
93
|
+
} catch (error) {
|
|
94
|
+
console.error('[StorageManager] Failed to get current session ID:', error);
|
|
95
|
+
return null;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Clear all chat data
|
|
101
|
+
*/
|
|
102
|
+
static async clearAll(): Promise<void> {
|
|
103
|
+
try {
|
|
104
|
+
const keys = await AsyncStorage.getAllKeys();
|
|
105
|
+
const chatKeys = keys.filter((key) => key.startsWith(STORAGE_KEY_PREFIX));
|
|
106
|
+
await AsyncStorage.multiRemove(chatKeys);
|
|
107
|
+
} catch (error) {
|
|
108
|
+
console.error('[StorageManager] Failed to clear all data:', error);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Theme Provider
|
|
3
|
+
* Provides theme context to all child components
|
|
4
|
+
* Theme is derived from backend widget configuration
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import React, { createContext, useContext, useMemo, ReactNode } from 'react';
|
|
8
|
+
import { createTheme, Theme } from './theme';
|
|
9
|
+
import type { WidgetConfig } from '../types';
|
|
10
|
+
|
|
11
|
+
interface ThemeContextValue {
|
|
12
|
+
theme: Theme;
|
|
13
|
+
config: WidgetConfig | null;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const ThemeContext = createContext<ThemeContextValue | undefined>(undefined);
|
|
17
|
+
|
|
18
|
+
interface ThemeProviderProps {
|
|
19
|
+
config: WidgetConfig | null;
|
|
20
|
+
children: ReactNode;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export const ThemeProvider: React.FC<ThemeProviderProps> = ({ config, children }) => {
|
|
24
|
+
const theme = useMemo(() => {
|
|
25
|
+
if (!config) {
|
|
26
|
+
// Return default theme if config not loaded yet
|
|
27
|
+
return createTheme();
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return createTheme(
|
|
31
|
+
config.primaryColor,
|
|
32
|
+
config.backgroundColor,
|
|
33
|
+
config.textColor,
|
|
34
|
+
config.theme === 'dark' ? 'dark' : 'light'
|
|
35
|
+
);
|
|
36
|
+
}, [config]);
|
|
37
|
+
|
|
38
|
+
const value = useMemo(
|
|
39
|
+
() => ({
|
|
40
|
+
theme,
|
|
41
|
+
config,
|
|
42
|
+
}),
|
|
43
|
+
[theme, config]
|
|
44
|
+
);
|
|
45
|
+
|
|
46
|
+
return <ThemeContext.Provider value={value}>{children}</ThemeContext.Provider>;
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Hook to access theme context
|
|
51
|
+
*/
|
|
52
|
+
export const useTheme = (): ThemeContextValue => {
|
|
53
|
+
const context = useContext(ThemeContext);
|
|
54
|
+
if (context === undefined) {
|
|
55
|
+
throw new Error('useTheme must be used within a ThemeProvider');
|
|
56
|
+
}
|
|
57
|
+
return context;
|
|
58
|
+
};
|