react-native-srschat 0.1.43 → 0.1.45

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 (32) hide show
  1. package/README.md +18 -0
  2. package/lib/commonjs/components/productCard.js +34 -5
  3. package/lib/commonjs/components/productCard.js.map +1 -1
  4. package/lib/commonjs/contexts/AppContext.js +78 -31
  5. package/lib/commonjs/contexts/AppContext.js.map +1 -1
  6. package/lib/commonjs/hooks/Stream.js +2 -2
  7. package/lib/commonjs/hooks/Stream.js.map +1 -1
  8. package/lib/commonjs/utils/audioRecorder.js +49 -21
  9. package/lib/commonjs/utils/audioRecorder.js.map +1 -1
  10. package/lib/commonjs/utils/storage.js +74 -0
  11. package/lib/commonjs/utils/storage.js.map +1 -0
  12. package/lib/module/components/productCard.js +34 -5
  13. package/lib/module/components/productCard.js.map +1 -1
  14. package/lib/module/contexts/AppContext.js +77 -31
  15. package/lib/module/contexts/AppContext.js.map +1 -1
  16. package/lib/module/hooks/Stream.js +2 -2
  17. package/lib/module/hooks/Stream.js.map +1 -1
  18. package/lib/module/utils/audioRecorder.js +50 -22
  19. package/lib/module/utils/audioRecorder.js.map +1 -1
  20. package/lib/module/utils/storage.js +65 -0
  21. package/lib/module/utils/storage.js.map +1 -0
  22. package/lib/typescript/components/productCard.d.ts.map +1 -1
  23. package/lib/typescript/contexts/AppContext.d.ts.map +1 -1
  24. package/lib/typescript/utils/audioRecorder.d.ts.map +1 -1
  25. package/lib/typescript/utils/storage.d.ts +28 -0
  26. package/lib/typescript/utils/storage.d.ts.map +1 -0
  27. package/package.json +5 -5
  28. package/src/components/productCard.js +34 -4
  29. package/src/contexts/AppContext.js +94 -35
  30. package/src/hooks/Stream.js +2 -2
  31. package/src/utils/audioRecorder.js +52 -27
  32. package/src/utils/storage.ts +86 -0
@@ -1,12 +1,12 @@
1
1
  import React, {createContext, useContext, useState, useEffect, useMemo } from "react";
2
2
  import uuid from 'react-native-uuid';
3
3
  import axios from "axios";
4
- import useAsyncStorage from '../hooks/useAsyncStorage';
4
+ // import useAsyncStorage from '../hooks/useAsyncStorage';
5
+ import { loadChat, updateChat, defaultState } from '../utils/storage';
5
6
 
6
7
  export const AppContext = createContext();
7
8
 
8
9
  export const AppProvider = ({ data, onProductCardClick, onAddToCartClick, uiConfig = {}, children }) => {
9
-
10
10
  const theme = {
11
11
  userMessage: '#003764',
12
12
  botMessage: '#003764',
@@ -16,6 +16,9 @@ export const AppProvider = ({ data, onProductCardClick, onAddToCartClick, uiConf
16
16
  inlineButtonColor: '#dbd4c8',
17
17
  primaryColor: '#161616'
18
18
  };
19
+
20
+ const TRACK_CLICK_URL = "https://srs-external-agent-logging-586731320826.us-central1.run.app/track-click"
21
+ const ADD_TO_CART_URL = "https://srs-external-agent-logging-586731320826.us-central1.run.app/add-to-cart"
19
22
 
20
23
  // Backend URLs
21
24
  const BASE_URL = data.env === "stage"
@@ -28,31 +31,24 @@ export const AppProvider = ({ data, onProductCardClick, onAddToCartClick, uiConf
28
31
  const defaultMessage = [
29
32
  {type: "ai", text: "Hi there šŸ‘‹ I’m Poseidon, your Heritage Pool+ AI Agent. I can help you during your online visit with Product and Account information. How can I help you today?"}
30
33
  ]
31
-
32
34
  const maintenanceMessage = [
33
- { type: "ai", text: "Hi there šŸ‘‹ I’m Poseidon, your Heritage Pool+ AI Agent. I'm currently undergoing maintenance to improve my services. Thank you for your patience and understanding!",},
35
+ { type: "ai", text: "Hi there šŸ‘‹ I'm Poseidon, your Heritage Pool+ AI Agent. I'm currently undergoing maintenance to improve my services. Thank you for your patience and understanding!",},
34
36
  ];
35
37
 
38
+
39
+ // user data variables
40
+ const customerToken = data.customer_token;
41
+
42
+
36
43
  // Base Variables
37
44
  const [showModal, setShowModal] = useState("Icon");
38
45
  const [input, setInput] = useState('');
39
- const [messages, setMessages] = useState(defaultMessage);
46
+ const [messages, setMessages] = useState(defaultState.messages);
40
47
  const [conversationStartTime, setConversationStartTime] = useState(null);
41
48
  const [lastUserMessage, setLastUserMessage] = useState("");
42
49
  const [lastMessageId, setLastMessageId] = useState("");
43
50
  const [sessionId, setSessionId] = useState(null);
44
51
 
45
- useEffect(() => {
46
- const newSessionId = uuid.v4(); // Generate UUID v4
47
- setSessionId(newSessionId);
48
- }, []);
49
-
50
- useEffect(() => {
51
- if (showModal == "Off") {
52
- setShowModal("Icon")
53
- }
54
- }, [uiConfig.showIcon]);
55
-
56
52
  // Message UI
57
53
  const [typingIndicator, setTypingIndicator] = useState(false);
58
54
  const [ghostMessage, setGhostMessage] = useState(false);
@@ -61,6 +57,69 @@ export const AppProvider = ({ data, onProductCardClick, onAddToCartClick, uiConf
61
57
  const [disclaimer, setDisclaimer] = useState(false);
62
58
  const [startStreaming, setStartStreaming] = useState(false);
63
59
 
60
+ // Load persisted state on mount
61
+ useEffect(() => {
62
+ if (!customerToken) return;
63
+
64
+ const init = async () => {
65
+ try {
66
+ const cachedState = await loadChat(customerToken);
67
+
68
+ // Update all stateful values
69
+ setTypingIndicator(cachedState.typingIndicator);
70
+ setGhostMessage(cachedState.ghostMessage);
71
+ setGhostCard(cachedState.ghostCard);
72
+ setStopActivated(cachedState.stopActivated);
73
+ setDisclaimer(cachedState.disclaimer);
74
+ setStartStreaming(cachedState.startStreaming);
75
+ setMessages(cachedState.messages);
76
+ setShowModal(cachedState.showModal);
77
+ } catch (error) {
78
+ console.error('Error loading chat state:', error);
79
+ }
80
+ };
81
+
82
+ init();
83
+ }, [customerToken]);
84
+
85
+ // Persist state changes
86
+ useEffect(() => {
87
+ if (!customerToken) return;
88
+
89
+ const persistState = async () => {
90
+ const currentState = {
91
+ typingIndicator,
92
+ ghostMessage,
93
+ ghostCard,
94
+ stopActivated,
95
+ disclaimer,
96
+ startStreaming,
97
+ messages,
98
+ showModal
99
+ };
100
+
101
+ await updateChat(customerToken, currentState);
102
+ };
103
+
104
+ persistState();
105
+ }, [
106
+ customerToken,
107
+ typingIndicator,
108
+ ghostMessage,
109
+ ghostCard,
110
+ stopActivated,
111
+ disclaimer,
112
+ startStreaming,
113
+ messages,
114
+ showModal
115
+ ]);
116
+
117
+ useEffect(() => {
118
+ if (showModal == "Off") {
119
+ setShowModal("Icon")
120
+ }
121
+ }, [uiConfig.showIcon]);
122
+
64
123
  const stopGenerating = () => {
65
124
  setTypingIndicator(false);
66
125
  setStopActivated(true);
@@ -138,26 +197,26 @@ export const AppProvider = ({ data, onProductCardClick, onAddToCartClick, uiConf
138
197
  setShowModal("Welcome");
139
198
  }
140
199
  if (messages.length > 1) {
141
- if (maintenance) {
142
- setMessages(maintenanceMessage);
143
- } else {
144
- setMessages(defaultMessage);
145
- }
146
- const newSessionId = uuid.v4();
147
- setSessionId(newSessionId);
200
+ const newState = {
201
+ ...defaultState,
202
+ messages: maintenance ? maintenanceMessage : defaultMessage,
203
+ showModal: maintenance ? "ChatWindow" : "Welcome"
204
+ };
205
+
206
+ // Update all state variables
207
+ setMessages(newState.messages);
148
208
  setTypingIndicator(false);
149
209
  setGhostMessage(false);
150
- /* try {
151
- const response = await axios.post(API_PREFIX + BASE_URL + "/clear", {
152
- customer_code: data.customer_code,
153
- session_id: data.session_id,
154
- message_id: lastMessageId,
155
- user_UUID: data.user_UUID,
156
- });
157
- console.log(response);
158
- } catch (error) {
159
- console.error("Error in Clear:", error);
160
- } */
210
+ setShowModal(newState.showModal);
211
+
212
+ // Generate new session
213
+ const newSessionId = uuid.v4();
214
+ setSessionId(newSessionId);
215
+
216
+ // Clear persisted state
217
+ if (customerToken) {
218
+ await updateChat(customerToken, newState);
219
+ }
161
220
  }
162
221
  };
163
222
 
@@ -267,7 +326,7 @@ export const AppProvider = ({ data, onProductCardClick, onAddToCartClick, uiConf
267
326
  startStreaming, setStartStreaming, maintenance, setMaintenance, feedback, setFeedback, handleFeedback, feedbackOpen, setFeedbackOpen,
268
327
  writeFeedback, setWriteFeedback, writeAnswer, setWriteAnswer, BASE_URL, lastMessageId, setLastMessageId,
269
328
  onProductCardClick, onAddToCartClick, data, sessionId, setSessionId, handleWrittenFeedback, switchFeedbackOpen, confirmDisclaimer,
270
- formatChatHistory, uiConfig, handleVoiceSend
329
+ formatChatHistory, uiConfig, handleVoiceSend, TRACK_CLICK_URL, ADD_TO_CART_URL
271
330
  }}
272
331
  >
273
332
  {children}
@@ -52,8 +52,8 @@ export function useWebSocketMessage() {
52
52
  user_query: lastUserMessage,
53
53
  session_id: String(sessionId),
54
54
  conversation_start_time: conversationStartTime,
55
- /* customer_name: "Cristin Connerney",
56
- user_UUID: "cristin.connerney@srsdistribution.com", */
55
+ // /* customer_name: "Cristin Connerney",
56
+ user_UUID: data.user_id || "mobile_user_unspecified",
57
57
  device: "mobile",
58
58
  window_location: "mobile"
59
59
  };
@@ -1,7 +1,7 @@
1
1
  // audioRecorder.js
2
2
 
3
- import { Platform, PermissionsAndroid } from 'react-native';
4
- import Voice from '@react-native-community/voice';
3
+ import { Platform } from 'react-native';
4
+ import Voice from '@react-native-voice/voice';
5
5
  import { check, PERMISSIONS, request, RESULTS } from 'react-native-permissions';
6
6
  import useAsyncStorage from '../hooks/useAsyncStorage';
7
7
 
@@ -30,6 +30,12 @@ export async function initVoice(onResult) {
30
30
  resultCallback = onResult;
31
31
  finalResult = '';
32
32
 
33
+ // Check if Voice module is available
34
+ if (!Voice) {
35
+ console.error('Voice module is not available');
36
+ return false;
37
+ }
38
+
33
39
  // First check if speech recognition is available
34
40
  const isAvailable = await Voice.isAvailable();
35
41
  if (!isAvailable) {
@@ -72,7 +78,7 @@ export async function initVoice(onResult) {
72
78
  };
73
79
 
74
80
  Voice.onSpeechError = async (e) => {
75
- console.error('onSpeechError: ', e);
81
+ // console.error('onSpeechError: ', e);
76
82
 
77
83
  if (silenceTimer) {
78
84
  clearTimeout(silenceTimer);
@@ -86,13 +92,13 @@ export async function initVoice(onResult) {
86
92
  await cleanupVoiceSession();
87
93
 
88
94
  // Only send error to callback if it's not a "No speech detected" error
89
- if (!isNoSpeechError) {
90
- resultCallback(null, e.error?.message || 'Speech recognition error');
91
- } else {
92
- console.log('No speech detected, ignoring error');
93
- // Optionally, call the callback with null parameters or a special indicator
94
- resultCallback(null, null); // This won't trigger an error alert in the component
95
- }
95
+ // if (!isNoSpeechError) {
96
+ // resultCallback(null, e.error?.message || 'Speech recognition error');
97
+ // } else {
98
+ // console.log('No speech detected, ignoring error');
99
+ // // Optionally, call the callback with null parameters or a special indicator
100
+ // resultCallback(null, null); // This won't trigger an error alert in the component
101
+ // }
96
102
  };
97
103
 
98
104
  Voice.onSpeechResults = (e) => {
@@ -164,6 +170,12 @@ const cleanupVoiceSession = async () => {
164
170
  }
165
171
 
166
172
  try {
173
+ // Check if Voice module is available
174
+ if (!Voice) {
175
+ console.log('Voice module not available during cleanup');
176
+ return;
177
+ }
178
+
167
179
  // First try to stop if still recognizing
168
180
  const isRecognizing = await Voice.isRecognizing();
169
181
  if (isRecognizing) {
@@ -189,7 +201,9 @@ const cleanupVoiceSession = async () => {
189
201
  console.error('Error in cleanupVoiceSession:', error);
190
202
  // Final attempt to destroy on error
191
203
  try {
192
- await Voice.destroy();
204
+ if (Voice) {
205
+ await Voice.destroy();
206
+ }
193
207
  } catch (e) {
194
208
  console.error('Final destroy attempt failed:', e);
195
209
  }
@@ -200,6 +214,12 @@ const cleanupVoiceSession = async () => {
200
214
 
201
215
  export async function startRecording() {
202
216
  try {
217
+ // Check if Voice module is available
218
+ if (!Voice) {
219
+ console.error('Voice module is not available');
220
+ return false;
221
+ }
222
+
203
223
  // Ensure cleanup of any existing session
204
224
  await cleanupVoiceSession();
205
225
 
@@ -221,7 +241,7 @@ export async function startRecording() {
221
241
 
222
242
  export async function stopRecording() {
223
243
  try {
224
- if (!isCurrentlyRecording) return;
244
+ if (!isCurrentlyRecording || !Voice) return;
225
245
 
226
246
  // Set this first to prevent race conditions
227
247
  isCurrentlyRecording = false;
@@ -259,13 +279,13 @@ export async function stopRecording() {
259
279
 
260
280
  export async function cancelRecording() {
261
281
  try {
282
+ if (!Voice) return;
262
283
  await Voice.cancel();
263
284
  await cleanupVoiceSession();
264
285
  } catch (error) {
265
286
  console.error('Error canceling voice recognition:', error);
266
287
  await cleanupVoiceSession();
267
288
  }
268
-
269
289
  }
270
290
 
271
291
  export async function requestAudioPermission() {
@@ -299,24 +319,22 @@ export async function requestAudioPermission() {
299
319
 
300
320
  async function requestAndroidPermission() {
301
321
  try {
302
- // Check available speech recognition services on Android
322
+
323
+
324
+ // Request microphone permission
325
+ const micPermission = await request(PERMISSIONS.ANDROID.RECORD_AUDIO);
326
+ if (micPermission !== RESULTS.GRANTED) {
327
+ console.log('Microphone permission denied');
328
+ return false;
329
+ }
330
+
331
+ // Skip checking speech recognition services which is causing errors
303
332
  const services = await Voice.getSpeechRecognitionServices();
304
333
  if (!services || services.length === 0) {
305
334
  console.error('No speech recognition services available');
306
335
  return false;
307
336
  }
308
-
309
- const granted = await PermissionsAndroid.request(
310
- PermissionsAndroid.PERMISSIONS.RECORD_AUDIO,
311
- {
312
- title: 'Microphone Permission',
313
- message: 'This app needs access to your microphone for voice recognition.',
314
- buttonPositive: 'OK',
315
- buttonNegative: 'Cancel',
316
- }
317
- );
318
-
319
- return granted === PermissionsAndroid.RESULTS.GRANTED;
337
+ return true;
320
338
  } catch (error) {
321
339
  console.error('Error requesting Android permission:', error);
322
340
  return false;
@@ -355,13 +373,20 @@ export function resetStoredPermission() {
355
373
  }
356
374
 
357
375
  export function cleanup() {
376
+ if (!Voice) {
377
+ console.log('Voice module not available during cleanup');
378
+ return;
379
+ }
380
+
358
381
  Voice.destroy().then(() => {
359
382
  Voice.removeAllListeners();
360
383
  cleanupVoiceSession();
361
384
  }).catch(error => {
362
385
  console.error('Error in cleanup:', error);
363
386
  // Try one more time
364
- Voice.destroy().catch(e => console.error('Final cleanup attempt failed:', e));
387
+ if (Voice) {
388
+ Voice.destroy().catch(e => console.error('Final cleanup attempt failed:', e));
389
+ }
365
390
  });
366
391
  }
367
392
 
@@ -0,0 +1,86 @@
1
+ import AsyncStorage from '@react-native-async-storage/async-storage';
2
+
3
+ export interface ChatMessage {
4
+ type: string;
5
+ text: string | string[];
6
+ form?: boolean;
7
+ }
8
+
9
+ export interface ChatState {
10
+ typingIndicator: boolean;
11
+ ghostMessage: boolean;
12
+ ghostCard: boolean;
13
+ stopActivated: boolean;
14
+ disclaimer: boolean;
15
+ startStreaming: boolean;
16
+ messages: ChatMessage[];
17
+ showIcon: boolean;
18
+ toggleChat: boolean;
19
+ }
20
+
21
+ // Module-level singleton to cache chat states by token
22
+ const chatStore: Record<string, ChatState> = {};
23
+
24
+ const getStorageKey = (token: string) => `srschat_${token}`;
25
+
26
+ export const defaultState: ChatState = {
27
+ typingIndicator: false,
28
+ ghostMessage: false,
29
+ ghostCard: false,
30
+ stopActivated: false,
31
+ disclaimer: false,
32
+ startStreaming: false,
33
+ messages: [{
34
+ type: "ai",
35
+ text: "Hi there šŸ‘‹ I'm Poseidon, your Heritage Pool+ AI Agent. I can help you during your online visit with Product and Account information. How can I help you today?"
36
+ }],
37
+ showIcon: true,
38
+ toggleChat: false
39
+ };
40
+
41
+ /**
42
+ * Loads chat state for a specific customer token
43
+ * Returns cached state if available, otherwise loads from AsyncStorage
44
+ */
45
+ export const loadChat = async (token: string): Promise<ChatState> => {
46
+ // Return from in-memory cache if available
47
+ if (chatStore[token]) {
48
+ return chatStore[token];
49
+ }
50
+
51
+ // Otherwise load from AsyncStorage
52
+ try {
53
+ const key = getStorageKey(token);
54
+ const storedData = await AsyncStorage.getItem(key);
55
+
56
+ if (storedData) {
57
+ const parsedData = JSON.parse(storedData) as ChatState;
58
+ // Cache in memory for future access
59
+ chatStore[token] = parsedData;
60
+ return parsedData;
61
+ }
62
+ } catch (error) {
63
+ console.error('Error loading chat state:', error);
64
+ }
65
+
66
+ // Return default state if nothing found
67
+ return defaultState;
68
+ };
69
+
70
+ /**
71
+ * Updates chat state for a specific customer token
72
+ * Updates both in-memory cache and persists to AsyncStorage
73
+ */
74
+ export const updateChat = async (token: string, next: ChatState): Promise<void> => {
75
+ try {
76
+ const key = getStorageKey(token);
77
+
78
+ // Update in-memory cache
79
+ chatStore[token] = next;
80
+
81
+ // Persist to AsyncStorage
82
+ await AsyncStorage.setItem(key, JSON.stringify(next));
83
+ } catch (error) {
84
+ console.error('Error updating chat state:', error);
85
+ }
86
+ };