@mobileai/react-native 0.9.10 → 0.9.12

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 (86) hide show
  1. package/README.md +11 -0
  2. package/lib/module/components/AIAgent.js +635 -39
  3. package/lib/module/components/AIAgent.js.map +1 -1
  4. package/lib/module/components/AgentChatBar.js +309 -13
  5. package/lib/module/components/AgentChatBar.js.map +1 -1
  6. package/lib/module/config/endpoints.js +22 -0
  7. package/lib/module/config/endpoints.js.map +1 -0
  8. package/lib/module/core/systemPrompt.js +126 -100
  9. package/lib/module/core/systemPrompt.js.map +1 -1
  10. package/lib/module/services/AudioInputService.js +9 -0
  11. package/lib/module/services/AudioInputService.js.map +1 -1
  12. package/lib/module/services/flags/FlagService.js +1 -1
  13. package/lib/module/services/flags/FlagService.js.map +1 -1
  14. package/lib/module/services/telemetry/TelemetryService.js +44 -15
  15. package/lib/module/services/telemetry/TelemetryService.js.map +1 -1
  16. package/lib/module/services/telemetry/device.js +80 -10
  17. package/lib/module/services/telemetry/device.js.map +1 -1
  18. package/lib/module/services/telemetry/deviceMetadata.js +10 -0
  19. package/lib/module/services/telemetry/deviceMetadata.js.map +1 -0
  20. package/lib/module/support/EscalationEventSource.js +168 -0
  21. package/lib/module/support/EscalationEventSource.js.map +1 -0
  22. package/lib/module/support/EscalationSocket.js +46 -7
  23. package/lib/module/support/EscalationSocket.js.map +1 -1
  24. package/lib/module/support/SupportChatModal.js +544 -0
  25. package/lib/module/support/SupportChatModal.js.map +1 -0
  26. package/lib/module/support/TicketStore.js +93 -0
  27. package/lib/module/support/TicketStore.js.map +1 -0
  28. package/lib/module/support/escalateTool.js +45 -13
  29. package/lib/module/support/escalateTool.js.map +1 -1
  30. package/lib/module/support/index.js +2 -0
  31. package/lib/module/support/index.js.map +1 -1
  32. package/lib/typescript/src/components/AIAgent.d.ts +24 -1
  33. package/lib/typescript/src/components/AIAgent.d.ts.map +1 -1
  34. package/lib/typescript/src/components/AgentChatBar.d.ts +24 -2
  35. package/lib/typescript/src/components/AgentChatBar.d.ts.map +1 -1
  36. package/lib/typescript/src/config/endpoints.d.ts +18 -0
  37. package/lib/typescript/src/config/endpoints.d.ts.map +1 -0
  38. package/lib/typescript/src/core/systemPrompt.d.ts +4 -13
  39. package/lib/typescript/src/core/systemPrompt.d.ts.map +1 -1
  40. package/lib/typescript/src/core/types.d.ts +1 -1
  41. package/lib/typescript/src/core/types.d.ts.map +1 -1
  42. package/lib/typescript/src/hooks/useAction.d.ts +2 -2
  43. package/lib/typescript/src/hooks/useAction.d.ts.map +1 -1
  44. package/lib/typescript/src/index.d.ts +1 -1
  45. package/lib/typescript/src/index.d.ts.map +1 -1
  46. package/lib/typescript/src/services/AudioInputService.d.ts.map +1 -1
  47. package/lib/typescript/src/services/telemetry/TelemetryService.d.ts +2 -1
  48. package/lib/typescript/src/services/telemetry/TelemetryService.d.ts.map +1 -1
  49. package/lib/typescript/src/services/telemetry/device.d.ts +15 -4
  50. package/lib/typescript/src/services/telemetry/device.d.ts.map +1 -1
  51. package/lib/typescript/src/services/telemetry/deviceMetadata.d.ts +6 -0
  52. package/lib/typescript/src/services/telemetry/deviceMetadata.d.ts.map +1 -0
  53. package/lib/typescript/src/support/EscalationEventSource.d.ts +38 -0
  54. package/lib/typescript/src/support/EscalationEventSource.d.ts.map +1 -0
  55. package/lib/typescript/src/support/EscalationSocket.d.ts +7 -1
  56. package/lib/typescript/src/support/EscalationSocket.d.ts.map +1 -1
  57. package/lib/typescript/src/support/SupportChatModal.d.ts +21 -0
  58. package/lib/typescript/src/support/SupportChatModal.d.ts.map +1 -0
  59. package/lib/typescript/src/support/TicketStore.d.ts +34 -0
  60. package/lib/typescript/src/support/TicketStore.d.ts.map +1 -0
  61. package/lib/typescript/src/support/escalateTool.d.ts +16 -1
  62. package/lib/typescript/src/support/escalateTool.d.ts.map +1 -1
  63. package/lib/typescript/src/support/index.d.ts +2 -1
  64. package/lib/typescript/src/support/index.d.ts.map +1 -1
  65. package/lib/typescript/src/support/types.d.ts +15 -0
  66. package/lib/typescript/src/support/types.d.ts.map +1 -1
  67. package/package.json +5 -1
  68. package/src/components/AIAgent.tsx +622 -38
  69. package/src/components/AgentChatBar.tsx +348 -9
  70. package/src/config/endpoints.ts +22 -0
  71. package/src/core/systemPrompt.ts +126 -100
  72. package/src/core/types.ts +1 -1
  73. package/src/hooks/useAction.ts +2 -2
  74. package/src/index.ts +1 -0
  75. package/src/services/AudioInputService.ts +9 -0
  76. package/src/services/flags/FlagService.ts +1 -1
  77. package/src/services/telemetry/TelemetryService.ts +46 -14
  78. package/src/services/telemetry/device.ts +88 -11
  79. package/src/services/telemetry/deviceMetadata.ts +13 -0
  80. package/src/support/EscalationEventSource.ts +190 -0
  81. package/src/support/EscalationSocket.ts +47 -8
  82. package/src/support/SupportChatModal.tsx +563 -0
  83. package/src/support/TicketStore.ts +100 -0
  84. package/src/support/escalateTool.ts +53 -13
  85. package/src/support/index.ts +2 -0
  86. package/src/support/types.ts +14 -0
@@ -1,16 +1,93 @@
1
- import { Platform } from 'react-native';
1
+ /**
2
+ * Persistent device ID — a UUID generated on first launch and stored in AsyncStorage.
3
+ * Unique per app install, survives across sessions.
4
+ *
5
+ * AsyncStorage is an optional peer dependency — if not installed, the ID
6
+ * persists only in memory for the current session.
7
+ */
8
+
9
+ const STORAGE_KEY = '@mobileai:device_id';
10
+
11
+ let _cachedId: string | null = null;
12
+ let _storageLoaded = false;
13
+ let _storage: any = null;
14
+
15
+ function loadStorage(): any {
16
+ if (_storageLoaded) return _storage;
17
+ _storageLoaded = true;
18
+ try {
19
+ // Suppress the RN red box that AsyncStorage triggers when its native module
20
+ // isn't linked ("NativeModule: AsyncStorage is null").
21
+ const origError = console.error;
22
+ console.error = (...args: unknown[]) => {
23
+ const msg = args[0];
24
+ if (typeof msg === 'string' && msg.includes('AsyncStorage')) return;
25
+ origError.apply(console, args);
26
+ };
27
+ try {
28
+ const mod = require('@react-native-async-storage/async-storage');
29
+ const candidate = mod.default ?? mod;
30
+ if (candidate && typeof candidate.getItem === 'function') {
31
+ _storage = candidate;
32
+ }
33
+ } finally {
34
+ console.error = origError;
35
+ }
36
+ } catch {
37
+ // Not installed — device ID won't persist across restarts
38
+ }
39
+ return _storage;
40
+ }
41
+
42
+ function generateUUID(): string {
43
+ if (typeof crypto !== 'undefined' && crypto.randomUUID) {
44
+ return crypto.randomUUID();
45
+ }
46
+ return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => {
47
+ const r = (Math.random() * 16) | 0;
48
+ const v = c === 'x' ? r : (r & 0x3) | 0x8;
49
+ return v.toString(16);
50
+ });
51
+ }
2
52
 
3
53
  /**
4
- * Deterministic device ID based on platform info.
5
- * This ensures analytics and flags remain consistent across sessions
6
- * without collecting PII hardware identifiers.
54
+ * Returns the persistent device ID synchronously (from cache).
55
+ * Returns null if not yet initialized.
7
56
  */
8
- export function getDeviceId(): string {
9
- const raw = `${Platform.OS}_${Platform.Version}`;
10
- let hash = 0;
11
- for (let i = 0; i < raw.length; i++) {
12
- hash = (hash << 5) - hash + raw.charCodeAt(i);
13
- hash |= 0;
57
+ export function getDeviceId(): string | null {
58
+ return _cachedId;
59
+ }
60
+
61
+ /**
62
+ * Initializes or retrieves the persistent device ID.
63
+ * Call once on app startup. Subsequent getDeviceId() calls are synchronous.
64
+ */
65
+ export async function initDeviceId(): Promise<string> {
66
+ if (_cachedId) return _cachedId;
67
+
68
+ const storage = loadStorage();
69
+ if (storage) {
70
+ try {
71
+ const stored: string | null = await storage.getItem(STORAGE_KEY);
72
+ if (stored) {
73
+ _cachedId = stored;
74
+ return stored;
75
+ }
76
+ } catch {
77
+ // Storage read failed — continue with new ID
78
+ }
14
79
  }
15
- return `dev_${Math.abs(hash).toString(36)}`;
80
+
81
+ const newId = generateUUID();
82
+ _cachedId = newId;
83
+
84
+ if (storage) {
85
+ try {
86
+ await storage.setItem(STORAGE_KEY, newId);
87
+ } catch {
88
+ // Storage write failed — ID works for this session only
89
+ }
90
+ }
91
+
92
+ return newId;
16
93
  }
@@ -0,0 +1,13 @@
1
+ import { Platform } from 'react-native';
2
+
3
+ export interface DeviceMetadata {
4
+ platform: string;
5
+ osVersion: string;
6
+ }
7
+
8
+ export function getDeviceMetadata(): DeviceMetadata {
9
+ return {
10
+ platform: Platform.OS === 'ios' ? 'iOS' : Platform.OS === 'android' ? 'Android' : Platform.OS,
11
+ osVersion: String(Platform.Version),
12
+ };
13
+ }
@@ -0,0 +1,190 @@
1
+ /**
2
+ * EscalationEventSource — SSE client using fetch + ReadableStream.
3
+ *
4
+ * Uses only the fetch API (available in all React Native runtimes)
5
+ * to consume Server-Sent Events — no EventSource polyfill needed.
6
+ * Provides a reliable, auto-reconnecting channel for server-push
7
+ * events like `ticket_closed` that complements the bidirectional
8
+ * WebSocket used for chat.
9
+ *
10
+ * Lifecycle:
11
+ * 1. SDK calls connect() → fetch with streaming response
12
+ * 2. Server holds connection open, pushes `ticket_closed` when agent resolves
13
+ * 3. On disconnect, auto-reconnects with exponential backoff (max 5 attempts)
14
+ * 4. If ticket is already closed, server responds immediately with the event
15
+ */
16
+
17
+ import { logger } from '../utils/logger';
18
+
19
+ export interface EscalationEventSourceOptions {
20
+ url: string;
21
+ onTicketClosed?: (ticketId: string) => void;
22
+ onConnected?: (ticketId: string) => void;
23
+ onError?: (error: Error) => void;
24
+ }
25
+
26
+ export class EscalationEventSource {
27
+ private abortController: AbortController | null = null;
28
+ private intentionalClose = false;
29
+ private reconnectAttempts = 0;
30
+ private reconnectTimer: ReturnType<typeof setTimeout> | null = null;
31
+ private readonly maxReconnectAttempts = 5;
32
+ private readonly options: EscalationEventSourceOptions;
33
+
34
+ constructor(options: EscalationEventSourceOptions) {
35
+ this.options = options;
36
+ }
37
+
38
+ connect(): void {
39
+ this.intentionalClose = false;
40
+ this.reconnectAttempts = 0;
41
+ this.openConnection();
42
+ }
43
+
44
+ disconnect(): void {
45
+ this.intentionalClose = true;
46
+ if (this.reconnectTimer) {
47
+ clearTimeout(this.reconnectTimer);
48
+ this.reconnectTimer = null;
49
+ }
50
+ if (this.abortController) {
51
+ this.abortController.abort();
52
+ this.abortController = null;
53
+ }
54
+ }
55
+
56
+ private async openConnection(): Promise<void> {
57
+ if (this.intentionalClose) return;
58
+
59
+ this.abortController = new AbortController();
60
+
61
+ try {
62
+ const response = await fetch(this.options.url, {
63
+ signal: this.abortController.signal,
64
+ headers: { Accept: 'text/event-stream' },
65
+ });
66
+
67
+ if (!response.ok) {
68
+ logger.warn('EscalationSSE', 'Non-OK response:', response.status);
69
+ this.scheduleReconnect();
70
+ return;
71
+ }
72
+
73
+ if (!response.body) {
74
+ logger.warn('EscalationSSE', 'No readable body — falling back to reading full response');
75
+ await this.readFullResponse(response);
76
+ return;
77
+ }
78
+
79
+ this.reconnectAttempts = 0;
80
+ await this.readStream(response.body);
81
+ } catch (err) {
82
+ if (this.intentionalClose) return;
83
+ if ((err as Error).name === 'AbortError') return;
84
+ logger.warn('EscalationSSE', 'Connection error:', (err as Error).message);
85
+ this.options.onError?.(err as Error);
86
+ this.scheduleReconnect();
87
+ }
88
+ }
89
+
90
+ private async readStream(body: ReadableStream<Uint8Array>): Promise<void> {
91
+ const reader = body.getReader();
92
+ const decoder = new TextDecoder();
93
+ let buffer = '';
94
+
95
+ try {
96
+ while (true) {
97
+ const { done, value } = await reader.read();
98
+ if (done) break;
99
+
100
+ buffer += decoder.decode(value, { stream: true });
101
+ const lines = buffer.split('\n');
102
+ buffer = lines.pop()!;
103
+
104
+ let currentEvent = '';
105
+ let currentData = '';
106
+
107
+ for (const line of lines) {
108
+ if (line.startsWith('event: ')) {
109
+ currentEvent = line.slice(7).trim();
110
+ } else if (line.startsWith('data: ')) {
111
+ currentData = line.slice(6).trim();
112
+ } else if (line === '' && currentEvent && currentData) {
113
+ this.handleEvent(currentEvent, currentData);
114
+ currentEvent = '';
115
+ currentData = '';
116
+ }
117
+ }
118
+ }
119
+ } catch (err) {
120
+ if (this.intentionalClose) return;
121
+ if ((err as Error).name === 'AbortError') return;
122
+ logger.warn('EscalationSSE', 'Stream read error:', (err as Error).message);
123
+ }
124
+
125
+ if (!this.intentionalClose) {
126
+ this.scheduleReconnect();
127
+ }
128
+ }
129
+
130
+ private async readFullResponse(response: Response): Promise<void> {
131
+ try {
132
+ const text = await response.text();
133
+ let currentEvent = '';
134
+ let currentData = '';
135
+
136
+ for (const line of text.split('\n')) {
137
+ if (line.startsWith('event: ')) {
138
+ currentEvent = line.slice(7).trim();
139
+ } else if (line.startsWith('data: ')) {
140
+ currentData = line.slice(6).trim();
141
+ } else if (line === '' && currentEvent && currentData) {
142
+ this.handleEvent(currentEvent, currentData);
143
+ currentEvent = '';
144
+ currentData = '';
145
+ }
146
+ }
147
+ } catch (err) {
148
+ if (this.intentionalClose) return;
149
+ logger.warn('EscalationSSE', 'Full response read error:', (err as Error).message);
150
+ }
151
+
152
+ if (!this.intentionalClose) {
153
+ this.scheduleReconnect();
154
+ }
155
+ }
156
+
157
+ private handleEvent(event: string, data: string): void {
158
+ try {
159
+ const parsed = JSON.parse(data);
160
+
161
+ if (event === 'connected') {
162
+ logger.info('EscalationSSE', 'Connected for ticket:', parsed.ticketId);
163
+ this.options.onConnected?.(parsed.ticketId);
164
+ } else if (event === 'ticket_closed') {
165
+ logger.info('EscalationSSE', 'Ticket closed event:', parsed.ticketId);
166
+ this.options.onTicketClosed?.(parsed.ticketId);
167
+ this.intentionalClose = true;
168
+ this.abortController?.abort();
169
+ }
170
+ } catch {
171
+ // ignore parse error
172
+ }
173
+ }
174
+
175
+ private scheduleReconnect(): void {
176
+ if (this.intentionalClose) return;
177
+ if (this.reconnectAttempts >= this.maxReconnectAttempts) {
178
+ logger.warn('EscalationSSE', 'Max reconnect attempts reached — giving up');
179
+ return;
180
+ }
181
+
182
+ const delay = Math.min(1000 * 2 ** this.reconnectAttempts, 16_000);
183
+ this.reconnectAttempts++;
184
+ logger.info('EscalationSSE', `Reconnecting in ${delay}ms (attempt ${this.reconnectAttempts}/${this.maxReconnectAttempts})`);
185
+
186
+ this.reconnectTimer = setTimeout(() => {
187
+ this.openConnection();
188
+ }, delay);
189
+ }
190
+ }
@@ -14,11 +14,13 @@
14
14
  * - Auto-reconnect on unexpected close (max 3 attempts, exponential backoff)
15
15
  */
16
16
 
17
- export type SocketReplyHandler = (reply: string) => void;
17
+ export type SocketReplyHandler = (reply: string, ticketId?: string) => void;
18
18
 
19
19
  interface EscalationSocketOptions {
20
20
  onReply: SocketReplyHandler;
21
21
  onError?: (error: Event) => void;
22
+ onTypingChange?: (isTyping: boolean) => void;
23
+ onTicketClosed?: (ticketId?: string) => void;
22
24
  maxReconnectAttempts?: number;
23
25
  }
24
26
 
@@ -31,11 +33,15 @@ export class EscalationSocket {
31
33
 
32
34
  private readonly onReply: SocketReplyHandler;
33
35
  private readonly onError?: (error: Event) => void;
36
+ private readonly onTypingChange?: (isTyping: boolean) => void;
37
+ private readonly onTicketClosed?: (ticketId?: string) => void;
34
38
  private readonly maxReconnectAttempts: number;
35
39
 
36
40
  constructor(options: EscalationSocketOptions) {
37
41
  this.onReply = options.onReply;
38
42
  this.onError = options.onError;
43
+ this.onTypingChange = options.onTypingChange;
44
+ this.onTicketClosed = options.onTicketClosed;
39
45
  this.maxReconnectAttempts = options.maxReconnectAttempts ?? 3;
40
46
  }
41
47
 
@@ -45,6 +51,22 @@ export class EscalationSocket {
45
51
  this.openConnection();
46
52
  }
47
53
 
54
+ sendText(text: string): boolean {
55
+ if (this.ws?.readyState === 1) { // WebSocket.OPEN
56
+ this.ws.send(JSON.stringify({ type: 'user_message', content: text }));
57
+ return true;
58
+ }
59
+ return false;
60
+ }
61
+
62
+ sendTypingStatus(isTyping: boolean): boolean {
63
+ if (this.ws?.readyState === 1) {
64
+ this.ws.send(JSON.stringify({ type: isTyping ? 'typing_start' : 'typing_stop' }));
65
+ return true;
66
+ }
67
+ return false;
68
+ }
69
+
48
70
  disconnect(): void {
49
71
  this.intentionalClose = true;
50
72
  if (this.reconnectTimer) {
@@ -68,16 +90,33 @@ export class EscalationSocket {
68
90
  }
69
91
 
70
92
  this.ws.onopen = () => {
71
- console.log('[EscalationSocket] Connected:', this.wsUrl);
93
+ console.log('[EscalationSocket] Connected to:', this.wsUrl);
72
94
  this.reconnectAttempts = 0;
73
95
  };
74
96
 
75
97
  this.ws.onmessage = (event) => {
76
98
  try {
77
- const msg = JSON.parse(String(event.data));
78
- if (msg.type === 'ping') return; // heartbeat — ignore
99
+ const rawData = String(event.data);
100
+ console.log('[EscalationSocket] Message received:', rawData);
101
+ const msg = JSON.parse(rawData);
102
+ if (msg.type === 'ping') {
103
+ console.log('[EscalationSocket] Heartbeat ping received');
104
+ return;
105
+ }
79
106
  if (msg.type === 'reply' && msg.reply) {
80
- this.onReply(msg.reply);
107
+ console.log('[EscalationSocket] Human reply received:', msg.reply);
108
+ this.onTypingChange?.(false);
109
+ this.onReply(msg.reply, msg.ticketId);
110
+ } else if (msg.type === 'typing_start') {
111
+ this.onTypingChange?.(true);
112
+ } else if (msg.type === 'typing_stop') {
113
+ this.onTypingChange?.(false);
114
+ } else if (msg.type === 'ticket_closed') {
115
+ console.log('[EscalationSocket] Ticket closed by agent');
116
+ this.onTypingChange?.(false);
117
+ this.onTicketClosed?.(msg.ticketId);
118
+ this.intentionalClose = true;
119
+ this.ws?.close();
81
120
  }
82
121
  } catch {
83
122
  // Non-JSON message — ignore
@@ -85,13 +124,13 @@ export class EscalationSocket {
85
124
  };
86
125
 
87
126
  this.ws.onerror = (event) => {
88
- console.error('[EscalationSocket] Error:', event);
127
+ console.error('[EscalationSocket] ❌ WebSocket error. URL was:', this.wsUrl, event);
89
128
  this.onError?.(event);
90
129
  };
91
130
 
92
- this.ws.onclose = () => {
131
+ this.ws.onclose = (event) => {
132
+ console.warn(`[EscalationSocket] Connection closed. Code=${event.code} Reason="${event.reason}" Intentional=${this.intentionalClose}`);
93
133
  if (this.intentionalClose) return;
94
- console.warn('[EscalationSocket] Connection closed unexpectedly');
95
134
  this.scheduleReconnect();
96
135
  };
97
136
  }