@mobileai/react-native 0.9.17 → 0.9.18

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 (213) hide show
  1. package/package.json +2 -5
  2. package/lib/module/__cli_tmp__.js.map +0 -1
  3. package/lib/module/components/AIAgent.js.map +0 -1
  4. package/lib/module/components/AIZone.js.map +0 -1
  5. package/lib/module/components/AgentChatBar.js.map +0 -1
  6. package/lib/module/components/AgentErrorBoundary.js.map +0 -1
  7. package/lib/module/components/AgentOverlay.js.map +0 -1
  8. package/lib/module/components/DiscoveryTooltip.js.map +0 -1
  9. package/lib/module/components/HighlightOverlay.js.map +0 -1
  10. package/lib/module/components/Icons.js.map +0 -1
  11. package/lib/module/components/ProactiveHint.js.map +0 -1
  12. package/lib/module/components/cards/InfoCard.js.map +0 -1
  13. package/lib/module/components/cards/ReviewSummary.js.map +0 -1
  14. package/lib/module/config/endpoints.js.map +0 -1
  15. package/lib/module/core/ActionRegistry.js.map +0 -1
  16. package/lib/module/core/AgentRuntime.js.map +0 -1
  17. package/lib/module/core/FiberTreeWalker.js.map +0 -1
  18. package/lib/module/core/IdleDetector.js.map +0 -1
  19. package/lib/module/core/MCPBridge.js.map +0 -1
  20. package/lib/module/core/ScreenDehydrator.js.map +0 -1
  21. package/lib/module/core/ZoneRegistry.js.map +0 -1
  22. package/lib/module/core/systemPrompt.js.map +0 -1
  23. package/lib/module/core/types.js.map +0 -1
  24. package/lib/module/hooks/useAction.js.map +0 -1
  25. package/lib/module/index.js.map +0 -1
  26. package/lib/module/plugin/withAppIntents.js.map +0 -1
  27. package/lib/module/providers/GeminiProvider.js.map +0 -1
  28. package/lib/module/providers/OpenAIProvider.js.map +0 -1
  29. package/lib/module/providers/ProviderFactory.js.map +0 -1
  30. package/lib/module/services/AudioInputService.js.map +0 -1
  31. package/lib/module/services/AudioOutputService.js.map +0 -1
  32. package/lib/module/services/KnowledgeBaseService.js.map +0 -1
  33. package/lib/module/services/VoiceService.js.map +0 -1
  34. package/lib/module/services/flags/FlagService.js.map +0 -1
  35. package/lib/module/services/telemetry/MobileAI.js.map +0 -1
  36. package/lib/module/services/telemetry/PiiScrubber.js.map +0 -1
  37. package/lib/module/services/telemetry/TelemetryService.js.map +0 -1
  38. package/lib/module/services/telemetry/TouchAutoCapture.js.map +0 -1
  39. package/lib/module/services/telemetry/device.js.map +0 -1
  40. package/lib/module/services/telemetry/deviceMetadata.js.map +0 -1
  41. package/lib/module/services/telemetry/index.js.map +0 -1
  42. package/lib/module/services/telemetry/types.js.map +0 -1
  43. package/lib/module/support/CSATSurvey.js.map +0 -1
  44. package/lib/module/support/EscalationEventSource.js.map +0 -1
  45. package/lib/module/support/EscalationSocket.js.map +0 -1
  46. package/lib/module/support/SupportChatModal.js.map +0 -1
  47. package/lib/module/support/SupportGreeting.js.map +0 -1
  48. package/lib/module/support/TicketStore.js.map +0 -1
  49. package/lib/module/support/escalateTool.js.map +0 -1
  50. package/lib/module/support/index.js.map +0 -1
  51. package/lib/module/support/supportPrompt.js.map +0 -1
  52. package/lib/module/support/types.js.map +0 -1
  53. package/lib/module/tools/datePickerTool.js.map +0 -1
  54. package/lib/module/tools/guideTool.js.map +0 -1
  55. package/lib/module/tools/index.js.map +0 -1
  56. package/lib/module/tools/keyboardTool.js.map +0 -1
  57. package/lib/module/tools/longPressTool.js.map +0 -1
  58. package/lib/module/tools/pickerTool.js.map +0 -1
  59. package/lib/module/tools/restoreTool.js.map +0 -1
  60. package/lib/module/tools/scrollTool.js.map +0 -1
  61. package/lib/module/tools/simplifyTool.js.map +0 -1
  62. package/lib/module/tools/sliderTool.js.map +0 -1
  63. package/lib/module/tools/tapTool.js.map +0 -1
  64. package/lib/module/tools/typeTool.js.map +0 -1
  65. package/lib/module/tools/types.js.map +0 -1
  66. package/lib/module/types/jsx.d.js.map +0 -1
  67. package/lib/module/utils/audioUtils.js.map +0 -1
  68. package/lib/module/utils/logger.js.map +0 -1
  69. package/lib/typescript/babel.config.d.ts.map +0 -1
  70. package/lib/typescript/bin/generate-map.d.cts.map +0 -1
  71. package/lib/typescript/eslint.config.d.mts.map +0 -1
  72. package/lib/typescript/generate-map.d.ts.map +0 -1
  73. package/lib/typescript/src/__cli_tmp__.d.ts.map +0 -1
  74. package/lib/typescript/src/components/AIAgent.d.ts.map +0 -1
  75. package/lib/typescript/src/components/AIZone.d.ts.map +0 -1
  76. package/lib/typescript/src/components/AgentChatBar.d.ts.map +0 -1
  77. package/lib/typescript/src/components/AgentErrorBoundary.d.ts.map +0 -1
  78. package/lib/typescript/src/components/AgentOverlay.d.ts.map +0 -1
  79. package/lib/typescript/src/components/DiscoveryTooltip.d.ts.map +0 -1
  80. package/lib/typescript/src/components/HighlightOverlay.d.ts.map +0 -1
  81. package/lib/typescript/src/components/Icons.d.ts.map +0 -1
  82. package/lib/typescript/src/components/ProactiveHint.d.ts.map +0 -1
  83. package/lib/typescript/src/components/cards/InfoCard.d.ts.map +0 -1
  84. package/lib/typescript/src/components/cards/ReviewSummary.d.ts.map +0 -1
  85. package/lib/typescript/src/config/endpoints.d.ts.map +0 -1
  86. package/lib/typescript/src/core/ActionRegistry.d.ts.map +0 -1
  87. package/lib/typescript/src/core/AgentRuntime.d.ts.map +0 -1
  88. package/lib/typescript/src/core/FiberTreeWalker.d.ts.map +0 -1
  89. package/lib/typescript/src/core/IdleDetector.d.ts.map +0 -1
  90. package/lib/typescript/src/core/MCPBridge.d.ts.map +0 -1
  91. package/lib/typescript/src/core/ScreenDehydrator.d.ts.map +0 -1
  92. package/lib/typescript/src/core/ZoneRegistry.d.ts.map +0 -1
  93. package/lib/typescript/src/core/systemPrompt.d.ts.map +0 -1
  94. package/lib/typescript/src/core/types.d.ts.map +0 -1
  95. package/lib/typescript/src/hooks/useAction.d.ts.map +0 -1
  96. package/lib/typescript/src/index.d.ts.map +0 -1
  97. package/lib/typescript/src/plugin/withAppIntents.d.ts.map +0 -1
  98. package/lib/typescript/src/providers/GeminiProvider.d.ts.map +0 -1
  99. package/lib/typescript/src/providers/OpenAIProvider.d.ts.map +0 -1
  100. package/lib/typescript/src/providers/ProviderFactory.d.ts.map +0 -1
  101. package/lib/typescript/src/services/AudioInputService.d.ts.map +0 -1
  102. package/lib/typescript/src/services/AudioOutputService.d.ts.map +0 -1
  103. package/lib/typescript/src/services/KnowledgeBaseService.d.ts.map +0 -1
  104. package/lib/typescript/src/services/VoiceService.d.ts.map +0 -1
  105. package/lib/typescript/src/services/flags/FlagService.d.ts.map +0 -1
  106. package/lib/typescript/src/services/telemetry/MobileAI.d.ts.map +0 -1
  107. package/lib/typescript/src/services/telemetry/PiiScrubber.d.ts.map +0 -1
  108. package/lib/typescript/src/services/telemetry/TelemetryService.d.ts.map +0 -1
  109. package/lib/typescript/src/services/telemetry/TouchAutoCapture.d.ts.map +0 -1
  110. package/lib/typescript/src/services/telemetry/device.d.ts.map +0 -1
  111. package/lib/typescript/src/services/telemetry/deviceMetadata.d.ts.map +0 -1
  112. package/lib/typescript/src/services/telemetry/index.d.ts.map +0 -1
  113. package/lib/typescript/src/services/telemetry/types.d.ts.map +0 -1
  114. package/lib/typescript/src/support/CSATSurvey.d.ts.map +0 -1
  115. package/lib/typescript/src/support/EscalationEventSource.d.ts.map +0 -1
  116. package/lib/typescript/src/support/EscalationSocket.d.ts.map +0 -1
  117. package/lib/typescript/src/support/SupportChatModal.d.ts.map +0 -1
  118. package/lib/typescript/src/support/SupportGreeting.d.ts.map +0 -1
  119. package/lib/typescript/src/support/TicketStore.d.ts.map +0 -1
  120. package/lib/typescript/src/support/escalateTool.d.ts.map +0 -1
  121. package/lib/typescript/src/support/index.d.ts.map +0 -1
  122. package/lib/typescript/src/support/supportPrompt.d.ts.map +0 -1
  123. package/lib/typescript/src/support/types.d.ts.map +0 -1
  124. package/lib/typescript/src/tools/datePickerTool.d.ts.map +0 -1
  125. package/lib/typescript/src/tools/guideTool.d.ts.map +0 -1
  126. package/lib/typescript/src/tools/index.d.ts.map +0 -1
  127. package/lib/typescript/src/tools/keyboardTool.d.ts.map +0 -1
  128. package/lib/typescript/src/tools/longPressTool.d.ts.map +0 -1
  129. package/lib/typescript/src/tools/pickerTool.d.ts.map +0 -1
  130. package/lib/typescript/src/tools/restoreTool.d.ts.map +0 -1
  131. package/lib/typescript/src/tools/scrollTool.d.ts.map +0 -1
  132. package/lib/typescript/src/tools/simplifyTool.d.ts.map +0 -1
  133. package/lib/typescript/src/tools/sliderTool.d.ts.map +0 -1
  134. package/lib/typescript/src/tools/tapTool.d.ts.map +0 -1
  135. package/lib/typescript/src/tools/typeTool.d.ts.map +0 -1
  136. package/lib/typescript/src/tools/types.d.ts.map +0 -1
  137. package/lib/typescript/src/utils/audioUtils.d.ts.map +0 -1
  138. package/lib/typescript/src/utils/logger.d.ts.map +0 -1
  139. package/src/__cli_tmp__.tsx +0 -9
  140. package/src/cli/analyzers/chain-analyzer.ts +0 -183
  141. package/src/cli/extractors/ai-extractor.ts +0 -6
  142. package/src/cli/extractors/ast-extractor.ts +0 -551
  143. package/src/cli/generate-intents.ts +0 -140
  144. package/src/cli/generate-map.ts +0 -121
  145. package/src/cli/generate-swift.ts +0 -116
  146. package/src/cli/scanners/expo-scanner.ts +0 -203
  147. package/src/cli/scanners/rn-scanner.ts +0 -445
  148. package/src/components/AIAgent.tsx +0 -1716
  149. package/src/components/AIZone.tsx +0 -147
  150. package/src/components/AgentChatBar.tsx +0 -1143
  151. package/src/components/AgentErrorBoundary.tsx +0 -78
  152. package/src/components/AgentOverlay.tsx +0 -73
  153. package/src/components/DiscoveryTooltip.tsx +0 -148
  154. package/src/components/HighlightOverlay.tsx +0 -136
  155. package/src/components/Icons.tsx +0 -253
  156. package/src/components/ProactiveHint.tsx +0 -145
  157. package/src/components/cards/InfoCard.tsx +0 -58
  158. package/src/components/cards/ReviewSummary.tsx +0 -76
  159. package/src/config/endpoints.ts +0 -22
  160. package/src/core/ActionRegistry.ts +0 -105
  161. package/src/core/AgentRuntime.ts +0 -1471
  162. package/src/core/FiberTreeWalker.ts +0 -930
  163. package/src/core/IdleDetector.ts +0 -72
  164. package/src/core/MCPBridge.ts +0 -163
  165. package/src/core/ScreenDehydrator.ts +0 -53
  166. package/src/core/ZoneRegistry.ts +0 -44
  167. package/src/core/systemPrompt.ts +0 -431
  168. package/src/core/types.ts +0 -521
  169. package/src/hooks/useAction.ts +0 -182
  170. package/src/index.ts +0 -83
  171. package/src/plugin/withAppIntents.ts +0 -98
  172. package/src/providers/GeminiProvider.ts +0 -357
  173. package/src/providers/OpenAIProvider.ts +0 -379
  174. package/src/providers/ProviderFactory.ts +0 -36
  175. package/src/services/AudioInputService.ts +0 -226
  176. package/src/services/AudioOutputService.ts +0 -236
  177. package/src/services/KnowledgeBaseService.ts +0 -156
  178. package/src/services/VoiceService.ts +0 -451
  179. package/src/services/flags/FlagService.ts +0 -137
  180. package/src/services/telemetry/MobileAI.ts +0 -66
  181. package/src/services/telemetry/PiiScrubber.ts +0 -17
  182. package/src/services/telemetry/TelemetryService.ts +0 -323
  183. package/src/services/telemetry/TouchAutoCapture.ts +0 -165
  184. package/src/services/telemetry/device.ts +0 -93
  185. package/src/services/telemetry/deviceMetadata.ts +0 -13
  186. package/src/services/telemetry/index.ts +0 -13
  187. package/src/services/telemetry/types.ts +0 -75
  188. package/src/support/CSATSurvey.tsx +0 -304
  189. package/src/support/EscalationEventSource.ts +0 -190
  190. package/src/support/EscalationSocket.ts +0 -152
  191. package/src/support/SupportChatModal.tsx +0 -563
  192. package/src/support/SupportGreeting.tsx +0 -161
  193. package/src/support/TicketStore.ts +0 -100
  194. package/src/support/escalateTool.ts +0 -174
  195. package/src/support/index.ts +0 -29
  196. package/src/support/supportPrompt.ts +0 -55
  197. package/src/support/types.ts +0 -155
  198. package/src/tools/datePickerTool.ts +0 -60
  199. package/src/tools/guideTool.ts +0 -76
  200. package/src/tools/index.ts +0 -20
  201. package/src/tools/keyboardTool.ts +0 -30
  202. package/src/tools/longPressTool.ts +0 -61
  203. package/src/tools/pickerTool.ts +0 -115
  204. package/src/tools/restoreTool.ts +0 -33
  205. package/src/tools/scrollTool.ts +0 -156
  206. package/src/tools/simplifyTool.ts +0 -33
  207. package/src/tools/sliderTool.ts +0 -65
  208. package/src/tools/tapTool.ts +0 -93
  209. package/src/tools/typeTool.ts +0 -113
  210. package/src/tools/types.ts +0 -58
  211. package/src/types/jsx.d.ts +0 -20
  212. package/src/utils/audioUtils.ts +0 -54
  213. package/src/utils/logger.ts +0 -38
@@ -1,304 +0,0 @@
1
- /**
2
- * CSAT Survey — Customer Satisfaction component.
3
- *
4
- * Shown after a support conversation ends (or after idle timeout).
5
- * Supports three rating types: emoji, stars, thumbs.
6
- */
7
-
8
-
9
- import { useState } from 'react';
10
- import {
11
- View,
12
- Text,
13
- TouchableOpacity,
14
- TextInput,
15
- StyleSheet,
16
- } from 'react-native';
17
- import type { CSATConfig, CSATRating } from './types';
18
-
19
- interface CSATSurveyProps {
20
- config: CSATConfig;
21
- metadata: CSATRating['metadata'];
22
- onDismiss: () => void;
23
- theme?: {
24
- primaryColor?: string;
25
- textColor?: string;
26
- backgroundColor?: string;
27
- };
28
- }
29
-
30
- const EMOJI_OPTIONS = [
31
- { emoji: '😡', label: 'Terrible', score: 1 },
32
- { emoji: '😞', label: 'Bad', score: 2 },
33
- { emoji: '😐', label: 'Okay', score: 3 },
34
- { emoji: '😊', label: 'Good', score: 4 },
35
- { emoji: '🤩', label: 'Amazing', score: 5 },
36
- ];
37
-
38
- const STAR_COUNT = 5;
39
-
40
- export function CSATSurvey({
41
- config,
42
- metadata,
43
- onDismiss,
44
- theme,
45
- }: CSATSurveyProps) {
46
- const [selectedScore, setSelectedScore] = useState<number | null>(null);
47
- const [feedback, setFeedback] = useState('');
48
- const [submitted, setSubmitted] = useState(false);
49
-
50
- const primary = theme?.primaryColor ?? '#8b5cf6';
51
- const textColor = theme?.textColor ?? '#ffffff';
52
- const bgColor = theme?.backgroundColor ?? 'rgba(26, 26, 46, 0.98)';
53
- const ratingType = config.ratingType ?? 'emoji';
54
- const question = config.question ?? 'How was your experience?';
55
-
56
- const handleSubmit = () => {
57
- if (selectedScore === null) return;
58
-
59
- const rating: CSATRating = {
60
- score: selectedScore,
61
- feedback: feedback.trim() || undefined,
62
- metadata,
63
- };
64
-
65
- config.onSubmit(rating);
66
- setSubmitted(true);
67
-
68
- // Auto-dismiss after 1.5s
69
- setTimeout(onDismiss, 1500);
70
- };
71
-
72
- if (submitted) {
73
- return (
74
- <View style={[styles.container, { backgroundColor: bgColor }]}>
75
- <Text style={[styles.thankYou, { color: textColor }]}>
76
- Thank you for your feedback! 🙏
77
- </Text>
78
- </View>
79
- );
80
- }
81
-
82
- return (
83
- <View style={[styles.container, { backgroundColor: bgColor }]}>
84
- {/* Question */}
85
- <Text style={[styles.question, { color: textColor }]}>{question}</Text>
86
-
87
- {/* Rating selector */}
88
- <View style={styles.ratingContainer}>
89
- {ratingType === 'emoji' && (
90
- <View style={styles.emojiRow}>
91
- {EMOJI_OPTIONS.map((opt) => (
92
- <TouchableOpacity
93
- key={opt.score}
94
- onPress={() => setSelectedScore(opt.score)}
95
- style={[
96
- styles.emojiButton,
97
- selectedScore === opt.score && {
98
- backgroundColor: `${primary}30`,
99
- borderColor: primary,
100
- },
101
- ]}
102
- activeOpacity={0.7}
103
- >
104
- <Text style={styles.emoji}>{opt.emoji}</Text>
105
- <Text
106
- style={[
107
- styles.emojiLabel,
108
- { color: selectedScore === opt.score ? primary : '#71717a' },
109
- ]}
110
- >
111
- {opt.label}
112
- </Text>
113
- </TouchableOpacity>
114
- ))}
115
- </View>
116
- )}
117
-
118
- {ratingType === 'stars' && (
119
- <View style={styles.starsRow}>
120
- {Array.from({ length: STAR_COUNT }, (_, i) => i + 1).map(
121
- (star) => (
122
- <TouchableOpacity
123
- key={star}
124
- onPress={() => setSelectedScore(star)}
125
- activeOpacity={0.7}
126
- >
127
- <Text
128
- style={[
129
- styles.star,
130
- {
131
- color:
132
- selectedScore !== null && star <= selectedScore
133
- ? '#fbbf24'
134
- : '#52525b',
135
- },
136
- ]}
137
- >
138
-
139
- </Text>
140
- </TouchableOpacity>
141
- )
142
- )}
143
- </View>
144
- )}
145
-
146
- {ratingType === 'thumbs' && (
147
- <View style={styles.thumbsRow}>
148
- <TouchableOpacity
149
- onPress={() => setSelectedScore(0)}
150
- style={[
151
- styles.thumbButton,
152
- selectedScore === 0 && {
153
- backgroundColor: '#ef444430',
154
- borderColor: '#ef4444',
155
- },
156
- ]}
157
- activeOpacity={0.7}
158
- >
159
- <Text style={styles.thumbEmoji}>👎</Text>
160
- </TouchableOpacity>
161
- <TouchableOpacity
162
- onPress={() => setSelectedScore(1)}
163
- style={[
164
- styles.thumbButton,
165
- selectedScore === 1 && {
166
- backgroundColor: '#22c55e30',
167
- borderColor: '#22c55e',
168
- },
169
- ]}
170
- activeOpacity={0.7}
171
- >
172
- <Text style={styles.thumbEmoji}>👍</Text>
173
- </TouchableOpacity>
174
- </View>
175
- )}
176
- </View>
177
-
178
- {/* Optional feedback text */}
179
- {selectedScore !== null && (
180
- <TextInput
181
- style={[styles.feedbackInput, { color: textColor }]}
182
- placeholder="Any additional feedback? (optional)"
183
- placeholderTextColor="#52525b"
184
- value={feedback}
185
- onChangeText={setFeedback}
186
- multiline
187
- maxLength={500}
188
- />
189
- )}
190
-
191
- {/* Actions */}
192
- <View style={styles.actions}>
193
- <TouchableOpacity onPress={onDismiss} activeOpacity={0.7}>
194
- <Text style={styles.dismissText}>Skip</Text>
195
- </TouchableOpacity>
196
- {selectedScore !== null && (
197
- <TouchableOpacity
198
- style={[styles.submitButton, { backgroundColor: primary }]}
199
- onPress={handleSubmit}
200
- activeOpacity={0.7}
201
- >
202
- <Text style={[styles.submitText, { color: textColor }]}>
203
- Submit
204
- </Text>
205
- </TouchableOpacity>
206
- )}
207
- </View>
208
- </View>
209
- );
210
- }
211
-
212
- const styles = StyleSheet.create({
213
- container: {
214
- borderRadius: 16,
215
- padding: 20,
216
- margin: 12,
217
- },
218
- question: {
219
- fontSize: 16,
220
- fontWeight: '600',
221
- textAlign: 'center',
222
- marginBottom: 16,
223
- },
224
- thankYou: {
225
- fontSize: 16,
226
- fontWeight: '500',
227
- textAlign: 'center',
228
- paddingVertical: 12,
229
- },
230
- ratingContainer: {
231
- marginBottom: 12,
232
- },
233
- emojiRow: {
234
- flexDirection: 'row',
235
- justifyContent: 'center',
236
- gap: 8,
237
- },
238
- emojiButton: {
239
- alignItems: 'center',
240
- paddingHorizontal: 10,
241
- paddingVertical: 8,
242
- borderRadius: 12,
243
- borderWidth: 1,
244
- borderColor: 'transparent',
245
- },
246
- emoji: {
247
- fontSize: 28,
248
- },
249
- emojiLabel: {
250
- fontSize: 10,
251
- marginTop: 4,
252
- fontWeight: '500',
253
- },
254
- starsRow: {
255
- flexDirection: 'row',
256
- justifyContent: 'center',
257
- gap: 8,
258
- },
259
- star: {
260
- fontSize: 36,
261
- },
262
- thumbsRow: {
263
- flexDirection: 'row',
264
- justifyContent: 'center',
265
- gap: 20,
266
- },
267
- thumbButton: {
268
- padding: 12,
269
- borderRadius: 16,
270
- borderWidth: 1,
271
- borderColor: 'transparent',
272
- },
273
- thumbEmoji: {
274
- fontSize: 36,
275
- },
276
- feedbackInput: {
277
- borderWidth: 1,
278
- borderColor: '#3f3f46',
279
- borderRadius: 12,
280
- padding: 12,
281
- fontSize: 14,
282
- minHeight: 60,
283
- textAlignVertical: 'top',
284
- marginBottom: 12,
285
- },
286
- actions: {
287
- flexDirection: 'row',
288
- justifyContent: 'space-between',
289
- alignItems: 'center',
290
- },
291
- dismissText: {
292
- color: '#71717a',
293
- fontSize: 14,
294
- },
295
- submitButton: {
296
- paddingHorizontal: 20,
297
- paddingVertical: 10,
298
- borderRadius: 10,
299
- },
300
- submitText: {
301
- fontSize: 14,
302
- fontWeight: '600',
303
- },
304
- });
@@ -1,190 +0,0 @@
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
- }
@@ -1,152 +0,0 @@
1
- /**
2
- * EscalationSocket — manages a WebSocket connection to the MobileAI platform
3
- * for receiving real-time replies from human support agents.
4
- *
5
- * Lifecycle:
6
- * 1. SDK calls escalate_to_human → POST /api/v1/escalations → gets { ticketId, wsUrl }
7
- * 2. EscalationSocket.connect(wsUrl) opens a WS connection
8
- * 3. Platform pushes { type: 'reply', ticketId, reply } when agent responds
9
- * 4. onReply callback fires → shown in chat UI as "👤 Human Agent: <reply>"
10
- * 5. disconnect() on chat close / unmount
11
- *
12
- * Handles:
13
- * - Server heartbeat pings (type: 'ping') — acknowledged silently
14
- * - Auto-reconnect on unexpected close (max 3 attempts, exponential backoff)
15
- */
16
-
17
- export type SocketReplyHandler = (reply: string, ticketId?: string) => void;
18
-
19
- interface EscalationSocketOptions {
20
- onReply: SocketReplyHandler;
21
- onError?: (error: Event) => void;
22
- onTypingChange?: (isTyping: boolean) => void;
23
- onTicketClosed?: (ticketId?: string) => void;
24
- maxReconnectAttempts?: number;
25
- }
26
-
27
- export class EscalationSocket {
28
- private ws: WebSocket | null = null;
29
- private wsUrl: string | null = null;
30
- private reconnectAttempts = 0;
31
- private reconnectTimer: ReturnType<typeof setTimeout> | null = null;
32
- private intentionalClose = false;
33
-
34
- private readonly onReply: SocketReplyHandler;
35
- private readonly onError?: (error: Event) => void;
36
- private readonly onTypingChange?: (isTyping: boolean) => void;
37
- private readonly onTicketClosed?: (ticketId?: string) => void;
38
- private readonly maxReconnectAttempts: number;
39
-
40
- constructor(options: EscalationSocketOptions) {
41
- this.onReply = options.onReply;
42
- this.onError = options.onError;
43
- this.onTypingChange = options.onTypingChange;
44
- this.onTicketClosed = options.onTicketClosed;
45
- this.maxReconnectAttempts = options.maxReconnectAttempts ?? 3;
46
- }
47
-
48
- connect(wsUrl: string): void {
49
- this.wsUrl = wsUrl;
50
- this.intentionalClose = false;
51
- this.openConnection();
52
- }
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
-
70
- disconnect(): void {
71
- this.intentionalClose = true;
72
- if (this.reconnectTimer) {
73
- clearTimeout(this.reconnectTimer);
74
- this.reconnectTimer = null;
75
- }
76
- if (this.ws) {
77
- this.ws.close();
78
- this.ws = null;
79
- }
80
- }
81
-
82
- private openConnection(): void {
83
- if (!this.wsUrl) return;
84
-
85
- try {
86
- this.ws = new WebSocket(this.wsUrl);
87
- } catch (err) {
88
- console.error('[EscalationSocket] Failed to open WebSocket:', err);
89
- return;
90
- }
91
-
92
- this.ws.onopen = () => {
93
- console.log('[EscalationSocket] ✅ Connected to:', this.wsUrl);
94
- this.reconnectAttempts = 0;
95
- };
96
-
97
- this.ws.onmessage = (event) => {
98
- try {
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
- }
106
- if (msg.type === 'reply' && 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();
120
- }
121
- } catch {
122
- // Non-JSON message — ignore
123
- }
124
- };
125
-
126
- this.ws.onerror = (event) => {
127
- console.error('[EscalationSocket] ❌ WebSocket error. URL was:', this.wsUrl, event);
128
- this.onError?.(event);
129
- };
130
-
131
- this.ws.onclose = (event) => {
132
- console.warn(`[EscalationSocket] Connection closed. Code=${event.code} Reason="${event.reason}" Intentional=${this.intentionalClose}`);
133
- if (this.intentionalClose) return;
134
- this.scheduleReconnect();
135
- };
136
- }
137
-
138
- private scheduleReconnect(): void {
139
- if (this.reconnectAttempts >= this.maxReconnectAttempts) {
140
- console.warn('[EscalationSocket] Max reconnect attempts reached — giving up');
141
- return;
142
- }
143
- const delay = Math.min(1000 * 2 ** this.reconnectAttempts, 16_000);
144
- this.reconnectAttempts++;
145
- console.log(
146
- `[EscalationSocket] Reconnecting in ${delay}ms (attempt ${this.reconnectAttempts}/${this.maxReconnectAttempts})`
147
- );
148
- this.reconnectTimer = setTimeout(() => {
149
- this.openConnection();
150
- }, delay);
151
- }
152
- }