@mobileai/react-native 0.9.10 → 0.9.11
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 +11 -0
- package/lib/module/components/AIAgent.js +513 -36
- package/lib/module/components/AIAgent.js.map +1 -1
- package/lib/module/components/AgentChatBar.js +320 -13
- package/lib/module/components/AgentChatBar.js.map +1 -1
- package/lib/module/config/endpoints.js +22 -0
- package/lib/module/config/endpoints.js.map +1 -0
- package/lib/module/core/systemPrompt.js +126 -100
- package/lib/module/core/systemPrompt.js.map +1 -1
- package/lib/module/services/AudioInputService.js +9 -0
- package/lib/module/services/AudioInputService.js.map +1 -1
- package/lib/module/services/flags/FlagService.js +1 -1
- package/lib/module/services/flags/FlagService.js.map +1 -1
- package/lib/module/services/telemetry/TelemetryService.js +39 -13
- package/lib/module/services/telemetry/TelemetryService.js.map +1 -1
- package/lib/module/services/telemetry/device.js +80 -10
- package/lib/module/services/telemetry/device.js.map +1 -1
- package/lib/module/support/EscalationSocket.js +46 -7
- package/lib/module/support/EscalationSocket.js.map +1 -1
- package/lib/module/support/SupportChatModal.js +516 -0
- package/lib/module/support/SupportChatModal.js.map +1 -0
- package/lib/module/support/TicketStore.js +93 -0
- package/lib/module/support/TicketStore.js.map +1 -0
- package/lib/module/support/escalateTool.js +39 -13
- package/lib/module/support/escalateTool.js.map +1 -1
- package/lib/module/support/index.js.map +1 -1
- package/lib/typescript/src/components/AIAgent.d.ts +24 -1
- package/lib/typescript/src/components/AIAgent.d.ts.map +1 -1
- package/lib/typescript/src/components/AgentChatBar.d.ts +24 -2
- package/lib/typescript/src/components/AgentChatBar.d.ts.map +1 -1
- package/lib/typescript/src/config/endpoints.d.ts +18 -0
- package/lib/typescript/src/config/endpoints.d.ts.map +1 -0
- package/lib/typescript/src/core/systemPrompt.d.ts +4 -13
- package/lib/typescript/src/core/systemPrompt.d.ts.map +1 -1
- package/lib/typescript/src/core/types.d.ts +1 -1
- package/lib/typescript/src/core/types.d.ts.map +1 -1
- package/lib/typescript/src/hooks/useAction.d.ts +2 -2
- package/lib/typescript/src/hooks/useAction.d.ts.map +1 -1
- package/lib/typescript/src/index.d.ts +1 -1
- package/lib/typescript/src/index.d.ts.map +1 -1
- package/lib/typescript/src/services/AudioInputService.d.ts.map +1 -1
- package/lib/typescript/src/services/telemetry/TelemetryService.d.ts.map +1 -1
- package/lib/typescript/src/services/telemetry/device.d.ts +15 -4
- package/lib/typescript/src/services/telemetry/device.d.ts.map +1 -1
- package/lib/typescript/src/support/EscalationSocket.d.ts +7 -1
- package/lib/typescript/src/support/EscalationSocket.d.ts.map +1 -1
- package/lib/typescript/src/support/SupportChatModal.d.ts +19 -0
- package/lib/typescript/src/support/SupportChatModal.d.ts.map +1 -0
- package/lib/typescript/src/support/TicketStore.d.ts +34 -0
- package/lib/typescript/src/support/TicketStore.d.ts.map +1 -0
- package/lib/typescript/src/support/escalateTool.d.ts +15 -1
- package/lib/typescript/src/support/escalateTool.d.ts.map +1 -1
- package/lib/typescript/src/support/index.d.ts +1 -1
- package/lib/typescript/src/support/index.d.ts.map +1 -1
- package/lib/typescript/src/support/types.d.ts +15 -0
- package/lib/typescript/src/support/types.d.ts.map +1 -1
- package/package.json +5 -1
- package/src/components/AIAgent.tsx +507 -36
- package/src/components/AgentChatBar.tsx +355 -9
- package/src/config/endpoints.ts +22 -0
- package/src/core/systemPrompt.ts +126 -100
- package/src/core/types.ts +1 -1
- package/src/hooks/useAction.ts +2 -2
- package/src/index.ts +1 -0
- package/src/services/AudioInputService.ts +9 -0
- package/src/services/flags/FlagService.ts +1 -1
- package/src/services/telemetry/TelemetryService.ts +40 -13
- package/src/services/telemetry/device.ts +88 -11
- package/src/support/EscalationSocket.ts +47 -8
- package/src/support/SupportChatModal.tsx +527 -0
- package/src/support/TicketStore.ts +100 -0
- package/src/support/escalateTool.ts +47 -13
- package/src/support/index.ts +1 -0
- package/src/support/types.ts +14 -0
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TicketStore — persists the active support ticket across app restarts.
|
|
3
|
+
*
|
|
4
|
+
* Uses @react-native-async-storage/async-storage as an optional peer dependency.
|
|
5
|
+
* If AsyncStorage is not installed, all methods silently no-op and the feature
|
|
6
|
+
* degrades gracefully (tickets are still shown while the app is open, just not
|
|
7
|
+
* restored after a restart).
|
|
8
|
+
*
|
|
9
|
+
* Usage:
|
|
10
|
+
* await TicketStore.save(ticketId, analyticsKey); // on escalation start
|
|
11
|
+
* const pending = await TicketStore.get(); // on AIAgent mount
|
|
12
|
+
* await TicketStore.clear(); // on modal close / ticket closed
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
const STORAGE_KEY = '@mobileai_pending_ticket';
|
|
16
|
+
|
|
17
|
+
interface PendingTicket {
|
|
18
|
+
ticketId: string;
|
|
19
|
+
analyticsKey: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** Try to load AsyncStorage at runtime. Optional peer dep — not bundled. */
|
|
23
|
+
function getAsyncStorage(): any | null {
|
|
24
|
+
try {
|
|
25
|
+
// Suppress the RN red box that AsyncStorage triggers when its native module
|
|
26
|
+
// isn't linked ("NativeModule: AsyncStorage is null").
|
|
27
|
+
const origError = console.error;
|
|
28
|
+
console.error = (...args: unknown[]) => {
|
|
29
|
+
const msg = args[0];
|
|
30
|
+
if (typeof msg === 'string' && msg.includes('AsyncStorage')) return;
|
|
31
|
+
origError.apply(console, args);
|
|
32
|
+
};
|
|
33
|
+
try {
|
|
34
|
+
const mod = require('@react-native-async-storage/async-storage');
|
|
35
|
+
const candidate = mod?.default ?? mod?.AsyncStorage ?? null;
|
|
36
|
+
if (candidate && typeof candidate.getItem === 'function') {
|
|
37
|
+
return candidate;
|
|
38
|
+
}
|
|
39
|
+
return null;
|
|
40
|
+
} finally {
|
|
41
|
+
console.error = origError;
|
|
42
|
+
}
|
|
43
|
+
} catch {
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export const TicketStore = {
|
|
49
|
+
/**
|
|
50
|
+
* Persist the active ticket so it survives an app restart.
|
|
51
|
+
*/
|
|
52
|
+
async save(ticketId: string, analyticsKey: string): Promise<void> {
|
|
53
|
+
const AS = getAsyncStorage();
|
|
54
|
+
if (!AS) {
|
|
55
|
+
console.warn(
|
|
56
|
+
'[TicketStore] @react-native-async-storage/async-storage is not installed — ' +
|
|
57
|
+
'ticket will not persist across app restarts. ' +
|
|
58
|
+
'Run: npx expo install @react-native-async-storage/async-storage'
|
|
59
|
+
);
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
try {
|
|
63
|
+
await AS.setItem(STORAGE_KEY, JSON.stringify({ ticketId, analyticsKey }));
|
|
64
|
+
console.log('[TicketStore] Ticket saved:', ticketId);
|
|
65
|
+
} catch (err) {
|
|
66
|
+
console.error('[TicketStore] Failed to save ticket:', err);
|
|
67
|
+
}
|
|
68
|
+
},
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Retrieve the persisted pending ticket, if any.
|
|
72
|
+
* Returns null if nothing is stored or AsyncStorage is unavailable.
|
|
73
|
+
*/
|
|
74
|
+
async get(): Promise<PendingTicket | null> {
|
|
75
|
+
const AS = getAsyncStorage();
|
|
76
|
+
if (!AS) return null;
|
|
77
|
+
try {
|
|
78
|
+
const raw = await AS.getItem(STORAGE_KEY);
|
|
79
|
+
if (!raw) return null;
|
|
80
|
+
return JSON.parse(raw) as PendingTicket;
|
|
81
|
+
} catch (err) {
|
|
82
|
+
console.error('[TicketStore] Failed to read ticket:', err);
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
85
|
+
},
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Remove the stored ticket (ticket closed or user dismissed modal).
|
|
89
|
+
*/
|
|
90
|
+
async clear(): Promise<void> {
|
|
91
|
+
const AS = getAsyncStorage();
|
|
92
|
+
if (!AS) return;
|
|
93
|
+
try {
|
|
94
|
+
await AS.removeItem(STORAGE_KEY);
|
|
95
|
+
console.log('[TicketStore] Pending ticket cleared');
|
|
96
|
+
} catch (err) {
|
|
97
|
+
console.error('[TicketStore] Failed to clear ticket:', err);
|
|
98
|
+
}
|
|
99
|
+
},
|
|
100
|
+
};
|
|
@@ -12,14 +12,31 @@ import type { ToolDefinition } from '../core/types';
|
|
|
12
12
|
import type { EscalationConfig, EscalationContext } from './types';
|
|
13
13
|
import { EscalationSocket } from './EscalationSocket';
|
|
14
14
|
|
|
15
|
-
|
|
15
|
+
import { ENDPOINTS } from '../config/endpoints';
|
|
16
|
+
import { getDeviceId } from '../services/telemetry/device';
|
|
17
|
+
import { logger } from '../utils/logger';
|
|
18
|
+
|
|
19
|
+
const MOBILEAI_HOST = ENDPOINTS.escalation;
|
|
16
20
|
|
|
17
21
|
export interface EscalationToolDeps {
|
|
18
22
|
config: EscalationConfig;
|
|
19
23
|
analyticsKey?: string;
|
|
20
24
|
getContext: () => Omit<EscalationContext, 'conversationSummary'>;
|
|
21
25
|
getHistory: () => Array<{ role: string; content: string }>;
|
|
22
|
-
onHumanReply?: (reply: string) => void;
|
|
26
|
+
onHumanReply?: (reply: string, ticketId?: string) => void;
|
|
27
|
+
onEscalationStarted?: (ticketId: string, socket: EscalationSocket) => void;
|
|
28
|
+
onTypingChange?: (isTyping: boolean) => void;
|
|
29
|
+
onTicketClosed?: (ticketId?: string) => void;
|
|
30
|
+
userContext?: {
|
|
31
|
+
userId?: string;
|
|
32
|
+
name?: string;
|
|
33
|
+
email?: string;
|
|
34
|
+
phone?: string;
|
|
35
|
+
plan?: string;
|
|
36
|
+
custom?: Record<string, string | number | boolean>;
|
|
37
|
+
};
|
|
38
|
+
pushToken?: string;
|
|
39
|
+
pushTokenType?: 'fcm' | 'expo' | 'apns';
|
|
23
40
|
}
|
|
24
41
|
|
|
25
42
|
export function createEscalateTool(deps: EscalationToolDeps): ToolDefinition;
|
|
@@ -44,7 +61,7 @@ export function createEscalateTool(
|
|
|
44
61
|
deps = depsOrConfig as EscalationToolDeps;
|
|
45
62
|
}
|
|
46
63
|
|
|
47
|
-
const { config, analyticsKey, getContext, getHistory, onHumanReply } = deps;
|
|
64
|
+
const { config, analyticsKey, getContext, getHistory, onHumanReply, onEscalationStarted, onTypingChange, onTicketClosed, userContext, pushToken, pushTokenType } = deps;
|
|
48
65
|
|
|
49
66
|
// Determine effective provider
|
|
50
67
|
const provider = config.provider ?? (analyticsKey ? 'mobileai' : 'custom');
|
|
@@ -72,10 +89,11 @@ export function createEscalateTool(
|
|
|
72
89
|
|
|
73
90
|
if (provider === 'mobileai') {
|
|
74
91
|
if (!analyticsKey) {
|
|
75
|
-
|
|
92
|
+
logger.warn('Escalation', 'provider=mobileai but no analyticsKey — falling back to custom');
|
|
76
93
|
} else {
|
|
77
94
|
try {
|
|
78
95
|
const history = getHistory().slice(-20); // last 20 messages for context
|
|
96
|
+
logger.info('Escalation', '★★★ Creating ticket — reason:', reason, '| deviceId:', getDeviceId());
|
|
79
97
|
const res = await fetch(`${MOBILEAI_HOST}/api/v1/escalations`, {
|
|
80
98
|
method: 'POST',
|
|
81
99
|
headers: { 'Content-Type': 'application/json' },
|
|
@@ -85,32 +103,48 @@ export function createEscalateTool(
|
|
|
85
103
|
screen: context.currentScreen,
|
|
86
104
|
history,
|
|
87
105
|
stepsBeforeEscalation: context.stepsBeforeEscalation,
|
|
106
|
+
userContext,
|
|
107
|
+
pushToken,
|
|
108
|
+
pushTokenType,
|
|
109
|
+
deviceId: getDeviceId(),
|
|
88
110
|
}),
|
|
89
111
|
});
|
|
90
112
|
|
|
91
113
|
if (res.ok) {
|
|
92
114
|
const { ticketId, wsUrl } = await res.json();
|
|
93
|
-
|
|
115
|
+
logger.info('Escalation', '★★★ Ticket created:', ticketId, '| wsUrl:', wsUrl);
|
|
94
116
|
|
|
95
117
|
// Connect WebSocket for real-time reply
|
|
96
118
|
socket?.disconnect();
|
|
97
119
|
socket = new EscalationSocket({
|
|
98
|
-
onReply: (reply) => {
|
|
99
|
-
|
|
100
|
-
onHumanReply?.(reply);
|
|
101
|
-
|
|
102
|
-
|
|
120
|
+
onReply: (reply, replyTicketId) => {
|
|
121
|
+
logger.info('Escalation', '★★★ Human reply for ticket', ticketId, ':', reply.substring(0, 80));
|
|
122
|
+
onHumanReply?.(reply, replyTicketId || ticketId);
|
|
123
|
+
},
|
|
124
|
+
onTypingChange: (v) => {
|
|
125
|
+
logger.info('Escalation', '★★★ Agent typing:', v);
|
|
126
|
+
onTypingChange?.(v);
|
|
127
|
+
},
|
|
128
|
+
onTicketClosed: (closedTicketId) => {
|
|
129
|
+
logger.info('Escalation', '★★★ Ticket closed:', ticketId);
|
|
130
|
+
onTicketClosed?.(closedTicketId || ticketId);
|
|
103
131
|
},
|
|
104
132
|
onError: (err) => {
|
|
105
|
-
|
|
133
|
+
logger.error('Escalation', '★★★ WebSocket error:', err);
|
|
106
134
|
},
|
|
107
135
|
});
|
|
108
136
|
socket.connect(wsUrl);
|
|
137
|
+
logger.info('Escalation', '★★★ WebSocket connecting...');
|
|
138
|
+
|
|
139
|
+
// Pass the socket to UI
|
|
140
|
+
logger.info('Escalation', '★★★ Calling onEscalationStarted for ticket:', ticketId);
|
|
141
|
+
onEscalationStarted?.(ticketId, socket);
|
|
142
|
+
logger.info('Escalation', '★★★ onEscalationStarted DONE');
|
|
109
143
|
} else {
|
|
110
|
-
|
|
144
|
+
logger.error('Escalation', 'Failed to create ticket:', res.status);
|
|
111
145
|
}
|
|
112
146
|
} catch (err) {
|
|
113
|
-
|
|
147
|
+
logger.error('Escalation', 'Network error:', (err as Error).message);
|
|
114
148
|
}
|
|
115
149
|
|
|
116
150
|
const message = config.escalationMessage ?? 'Connecting you to a human agent...';
|
package/src/support/index.ts
CHANGED
package/src/support/types.ts
CHANGED
|
@@ -139,3 +139,17 @@ export interface BusinessHoursConfig {
|
|
|
139
139
|
/** Message shown outside business hours */
|
|
140
140
|
offlineMessage?: string;
|
|
141
141
|
}
|
|
142
|
+
|
|
143
|
+
// ─── Support Ticket ───────────────────────────────────────
|
|
144
|
+
|
|
145
|
+
export interface SupportTicket {
|
|
146
|
+
id: string;
|
|
147
|
+
reason: string;
|
|
148
|
+
screen: string;
|
|
149
|
+
status: string;
|
|
150
|
+
history: Array<{ role: string; content: string; timestamp?: string }>;
|
|
151
|
+
createdAt: string;
|
|
152
|
+
wsUrl: string;
|
|
153
|
+
/** Number of unread messages (computed by backend = history.length - readMessageCount) */
|
|
154
|
+
unreadCount?: number;
|
|
155
|
+
}
|