@qafka/react-native 2.0.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.
Files changed (178) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/CONTRIBUTING.md +92 -0
  3. package/LICENSE +22 -0
  4. package/README.md +109 -0
  5. package/SECURITY.md +67 -0
  6. package/android/build.gradle +35 -0
  7. package/android/src/main/AndroidManifest.xml +2 -0
  8. package/android/src/main/java/com/qafka/attestation/QafkaAttestationModule.kt +92 -0
  9. package/android/src/main/java/com/qafka/attestation/QafkaAttestationPackage.kt +22 -0
  10. package/android/src/main/java/com/qafka/audio/QafkaAudioModule.kt +290 -0
  11. package/android/src/main/java/com/qafka/clipboard/QafkaClipboardModule.kt +28 -0
  12. package/android/src/main/java/com/qafka/storage/QafkaStorageModule.kt +80 -0
  13. package/app.plugin.js +1 -0
  14. package/dist/QafkaSDK.d.ts +174 -0
  15. package/dist/QafkaSDK.js +461 -0
  16. package/dist/cards/bindings/resolveFieldName.d.ts +25 -0
  17. package/dist/cards/bindings/resolveFieldName.js +82 -0
  18. package/dist/cards/cta/CardContext.d.ts +16 -0
  19. package/dist/cards/cta/CardContext.js +58 -0
  20. package/dist/cards/cta/dispatcher.d.ts +7 -0
  21. package/dist/cards/cta/dispatcher.js +90 -0
  22. package/dist/cards/cta/types.d.ts +66 -0
  23. package/dist/cards/cta/types.js +2 -0
  24. package/dist/cards/index.d.ts +20 -0
  25. package/dist/cards/index.js +34 -0
  26. package/dist/cards/primitives/QButton.d.ts +10 -0
  27. package/dist/cards/primitives/QButton.js +115 -0
  28. package/dist/cards/primitives/QDivider.d.ts +7 -0
  29. package/dist/cards/primitives/QDivider.js +17 -0
  30. package/dist/cards/primitives/QIcon.d.ts +13 -0
  31. package/dist/cards/primitives/QIcon.js +26 -0
  32. package/dist/cards/primitives/QImage.d.ts +9 -0
  33. package/dist/cards/primitives/QImage.js +22 -0
  34. package/dist/cards/primitives/QText.d.ts +9 -0
  35. package/dist/cards/primitives/QText.js +30 -0
  36. package/dist/cards/primitives/QView.d.ts +8 -0
  37. package/dist/cards/primitives/QView.js +19 -0
  38. package/dist/cards/renderer/CardRenderer.d.ts +19 -0
  39. package/dist/cards/renderer/CardRenderer.js +64 -0
  40. package/dist/cards/renderer/renderNode.d.ts +13 -0
  41. package/dist/cards/renderer/renderNode.js +42 -0
  42. package/dist/cards/types.d.ts +110 -0
  43. package/dist/cards/types.js +6 -0
  44. package/dist/components/ActionResultBadge.d.ts +12 -0
  45. package/dist/components/ActionResultBadge.js +58 -0
  46. package/dist/components/ChatPage.d.ts +44 -0
  47. package/dist/components/ChatPage.js +84 -0
  48. package/dist/components/DataChip.d.ts +8 -0
  49. package/dist/components/DataChip.js +80 -0
  50. package/dist/components/DataChipList.d.ts +13 -0
  51. package/dist/components/DataChipList.js +21 -0
  52. package/dist/components/FloatingButton.d.ts +11 -0
  53. package/dist/components/FloatingButton.js +162 -0
  54. package/dist/components/InputArea.d.ts +57 -0
  55. package/dist/components/InputArea.js +142 -0
  56. package/dist/components/MarkdownText.d.ts +15 -0
  57. package/dist/components/MarkdownText.js +283 -0
  58. package/dist/components/MessageBubble.d.ts +134 -0
  59. package/dist/components/MessageBubble.js +384 -0
  60. package/dist/components/NavigationSuggestion.d.ts +11 -0
  61. package/dist/components/NavigationSuggestion.js +109 -0
  62. package/dist/components/Qafka.d.ts +39 -0
  63. package/dist/components/Qafka.handlers.d.ts +21 -0
  64. package/dist/components/Qafka.handlers.js +54 -0
  65. package/dist/components/Qafka.js +493 -0
  66. package/dist/components/Qafka.styles.d.ts +19 -0
  67. package/dist/components/Qafka.styles.js +101 -0
  68. package/dist/components/Qafka.types.d.ts +744 -0
  69. package/dist/components/Qafka.types.js +2 -0
  70. package/dist/components/Qafka.utils.d.ts +7 -0
  71. package/dist/components/Qafka.utils.js +34 -0
  72. package/dist/components/QafkaProvider.d.ts +12 -0
  73. package/dist/components/QafkaProvider.js +87 -0
  74. package/dist/components/QuickReplies.d.ts +14 -0
  75. package/dist/components/QuickReplies.js +48 -0
  76. package/dist/components/StepProgressIndicator.d.ts +12 -0
  77. package/dist/components/StepProgressIndicator.js +48 -0
  78. package/dist/components/SuggestionButton.d.ts +42 -0
  79. package/dist/components/SuggestionButton.js +67 -0
  80. package/dist/components/ToolStatusPill.d.ts +20 -0
  81. package/dist/components/ToolStatusPill.js +43 -0
  82. package/dist/components/TypingIndicator.d.ts +28 -0
  83. package/dist/components/TypingIndicator.js +109 -0
  84. package/dist/components/VoicePage.d.ts +48 -0
  85. package/dist/components/VoicePage.js +683 -0
  86. package/dist/components/defaults/DefaultCard.d.ts +14 -0
  87. package/dist/components/defaults/DefaultCard.js +156 -0
  88. package/dist/components/defaults/DefaultDetail.d.ts +14 -0
  89. package/dist/components/defaults/DefaultDetail.js +138 -0
  90. package/dist/components/defaults/DefaultList.d.ts +12 -0
  91. package/dist/components/defaults/DefaultList.js +98 -0
  92. package/dist/components/defaults/DefaultTable.d.ts +14 -0
  93. package/dist/components/defaults/DefaultTable.js +204 -0
  94. package/dist/components/defaults/index.d.ts +14 -0
  95. package/dist/components/defaults/index.js +25 -0
  96. package/dist/components/index.d.ts +22 -0
  97. package/dist/components/index.js +36 -0
  98. package/dist/constants.d.ts +10 -0
  99. package/dist/constants.js +13 -0
  100. package/dist/hooks/useChatMessages.d.ts +72 -0
  101. package/dist/hooks/useChatMessages.js +505 -0
  102. package/dist/hooks/useContextManager.d.ts +12 -0
  103. package/dist/hooks/useContextManager.js +46 -0
  104. package/dist/hooks/useProjectTheme.d.ts +19 -0
  105. package/dist/hooks/useProjectTheme.js +163 -0
  106. package/dist/hooks/useSDK.d.ts +31 -0
  107. package/dist/hooks/useSDK.js +103 -0
  108. package/dist/hooks/useVoiceChat.d.ts +110 -0
  109. package/dist/hooks/useVoiceChat.js +436 -0
  110. package/dist/index.d.ts +13 -0
  111. package/dist/index.js +59 -0
  112. package/dist/native/QafkaAttestation.d.ts +23 -0
  113. package/dist/native/QafkaAttestation.js +70 -0
  114. package/dist/native/QafkaAudio.d.ts +14 -0
  115. package/dist/native/QafkaAudio.js +31 -0
  116. package/dist/native/QafkaClipboard.d.ts +11 -0
  117. package/dist/native/QafkaClipboard.js +14 -0
  118. package/dist/native/QafkaStorage.d.ts +15 -0
  119. package/dist/native/QafkaStorage.js +12 -0
  120. package/dist/resolve-project-config.d.ts +35 -0
  121. package/dist/resolve-project-config.js +41 -0
  122. package/dist/runtime-config-loader.d.ts +37 -0
  123. package/dist/runtime-config-loader.js +53 -0
  124. package/dist/services/AttestationManager.d.ts +38 -0
  125. package/dist/services/AttestationManager.js +296 -0
  126. package/dist/services/BackendService.d.ts +156 -0
  127. package/dist/services/BackendService.js +755 -0
  128. package/dist/services/ConversationManager.d.ts +43 -0
  129. package/dist/services/ConversationManager.js +96 -0
  130. package/dist/services/NavigationHandler.d.ts +29 -0
  131. package/dist/services/NavigationHandler.js +70 -0
  132. package/dist/services/RealtimeService.d.ts +83 -0
  133. package/dist/services/RealtimeService.js +203 -0
  134. package/dist/services/storage.d.ts +11 -0
  135. package/dist/services/storage.js +15 -0
  136. package/dist/services/storageCore.d.ts +17 -0
  137. package/dist/services/storageCore.js +46 -0
  138. package/dist/themes/dark.d.ts +5 -0
  139. package/dist/themes/dark.js +129 -0
  140. package/dist/themes/index.d.ts +12 -0
  141. package/dist/themes/index.js +33 -0
  142. package/dist/themes/light.d.ts +5 -0
  143. package/dist/themes/light.js +129 -0
  144. package/dist/themes/types.d.ts +155 -0
  145. package/dist/themes/types.js +5 -0
  146. package/dist/types/chat.d.ts +126 -0
  147. package/dist/types/chat.js +5 -0
  148. package/dist/types/components.d.ts +56 -0
  149. package/dist/types/components.js +16 -0
  150. package/dist/types/external-navigation.d.ts +19 -0
  151. package/dist/types/external-navigation.js +8 -0
  152. package/dist/types/index.d.ts +9 -0
  153. package/dist/types/index.js +25 -0
  154. package/dist/types/navigation.d.ts +86 -0
  155. package/dist/types/navigation.js +5 -0
  156. package/dist/types/sdk.d.ts +36 -0
  157. package/dist/types/sdk.js +5 -0
  158. package/dist/utils/deepMerge.d.ts +46 -0
  159. package/dist/utils/deepMerge.js +70 -0
  160. package/dist/utils/fontUtils.d.ts +8 -0
  161. package/dist/utils/fontUtils.js +16 -0
  162. package/dist/validate-end-user.d.ts +18 -0
  163. package/dist/validate-end-user.js +74 -0
  164. package/expo-plugin/withQafkaAttestation.js +57 -0
  165. package/ios/QafkaAttestation.m +25 -0
  166. package/ios/QafkaAttestation.swift +128 -0
  167. package/ios/QafkaAudio.m +23 -0
  168. package/ios/QafkaAudio.swift +519 -0
  169. package/ios/QafkaClipboard.m +10 -0
  170. package/ios/QafkaClipboard.swift +21 -0
  171. package/ios/QafkaReactImports.h +2 -0
  172. package/ios/QafkaStorage.m +26 -0
  173. package/ios/QafkaStorage.swift +118 -0
  174. package/package.json +82 -0
  175. package/qafka.config.d.ts +9 -0
  176. package/qafka.config.js +9 -0
  177. package/react-native-qafka.podspec +28 -0
  178. package/react-native.config.js +14 -0
@@ -0,0 +1,683 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.VoicePage = VoicePage;
37
+ const react_1 = __importStar(require("react"));
38
+ const react_native_1 = require("react-native");
39
+ const react_native_safe_area_context_1 = require("react-native-safe-area-context");
40
+ const ToolStatusPill_1 = require("./ToolStatusPill");
41
+ const DataChipList_1 = require("./DataChipList");
42
+ const { width: SCREEN_WIDTH } = react_native_1.Dimensions.get('window');
43
+ const STATE_LABELS = {
44
+ idle: 'Waiting...', connecting: 'Connecting...', listening: 'Listening...',
45
+ thinking: 'Thinking...', speaking: 'Speaking...',
46
+ };
47
+ const STATE_COLORS = {
48
+ idle: '#9ca3af', connecting: '#f59e0b', listening: '#22c55e',
49
+ thinking: '#3b82f6', speaking: '#8b5cf6',
50
+ };
51
+ // Numeric mapping for smooth state→color interpolation via Animated.
52
+ const STATE_NUM = {
53
+ idle: 0, connecting: 1, listening: 2, thinking: 3, speaking: 4,
54
+ };
55
+ const STATE_COLOR_INPUT = [0, 1, 2, 3, 4];
56
+ const STATE_COLOR_OUTPUT = ['#9ca3af', '#f59e0b', '#22c55e', '#3b82f6', '#8b5cf6'];
57
+ // Anchor box (rings expand inside this). Sized so a 2.8x ring still fits
58
+ // inside the screen and stays clipped by the consumer's voice slot. Compact
59
+ // mode in this file wraps the whole indicator with overflow:hidden, so
60
+ // over-expansion outside the anchor is harmless.
61
+ const ANCHOR = 200;
62
+ const CORE = 70;
63
+ const RING_BASE = CORE; // rings start at core size, scale outward
64
+ const BASELINE_CYCLE_MS = 2400;
65
+ const BASELINE_PHASE_MS = 800;
66
+ const SPIKE_DURATION_MS = 700;
67
+ const SPIKE_THRESHOLD = 0.3;
68
+ const SPIKE_MIN_INTERVAL_MS = 150;
69
+ // --- Default components (used when customer doesn't provide custom ones) ---
70
+ function DefaultVoiceIndicator({ state, amplitude, theme, isMuted }) {
71
+ // Smoothed amplitude (drives ring opacity factor + core scale).
72
+ const amplitudeAnim = (0, react_1.useRef)(new react_native_1.Animated.Value(0)).current;
73
+ (0, react_1.useEffect)(() => {
74
+ react_native_1.Animated.timing(amplitudeAnim, {
75
+ toValue: amplitude,
76
+ duration: 90,
77
+ useNativeDriver: true,
78
+ }).start();
79
+ }, [amplitude, amplitudeAnim]);
80
+ // State color interpolation. JS-driven (backgroundColor is not native-drivable),
81
+ // one-shot per state change, so perf impact is negligible.
82
+ const stateColorAnim = (0, react_1.useRef)(new react_native_1.Animated.Value(STATE_NUM[state])).current;
83
+ (0, react_1.useEffect)(() => {
84
+ react_native_1.Animated.timing(stateColorAnim, {
85
+ toValue: STATE_NUM[state],
86
+ duration: 500,
87
+ easing: react_native_1.Easing.inOut(react_native_1.Easing.cubic),
88
+ useNativeDriver: false,
89
+ }).start();
90
+ }, [state, stateColorAnim]);
91
+ const stateColor = stateColorAnim.interpolate({
92
+ inputRange: STATE_COLOR_INPUT,
93
+ outputRange: STATE_COLOR_OUTPUT,
94
+ });
95
+ // Baseline rings — 3 continuous loops, staggered to feel like a breathing wave.
96
+ const baselineProgress = (0, react_1.useRef)([
97
+ new react_native_1.Animated.Value(0),
98
+ new react_native_1.Animated.Value(0),
99
+ new react_native_1.Animated.Value(0),
100
+ ]).current;
101
+ (0, react_1.useEffect)(() => {
102
+ const timers = [];
103
+ const anims = [];
104
+ baselineProgress.forEach((p, i) => {
105
+ const timer = setTimeout(() => {
106
+ const anim = react_native_1.Animated.loop(react_native_1.Animated.timing(p, {
107
+ toValue: 1,
108
+ duration: BASELINE_CYCLE_MS,
109
+ easing: react_native_1.Easing.linear,
110
+ useNativeDriver: true,
111
+ }));
112
+ anims.push(anim);
113
+ anim.start();
114
+ }, i * BASELINE_PHASE_MS);
115
+ timers.push(timer);
116
+ });
117
+ return () => {
118
+ timers.forEach(clearTimeout);
119
+ anims.forEach((a) => a.stop());
120
+ baselineProgress.forEach((p) => p.stopAnimation());
121
+ };
122
+ }, [baselineProgress]);
123
+ // Spike rings — round-robin pool, emitted on amplitude rising edge.
124
+ // Gives a "voice → ripple" feel for both mic input and AI TTS playback,
125
+ // since native amplitude stream covers both.
126
+ const spikeProgress = (0, react_1.useRef)([
127
+ new react_native_1.Animated.Value(0),
128
+ new react_native_1.Animated.Value(0),
129
+ new react_native_1.Animated.Value(0),
130
+ ]).current;
131
+ const spikeIndex = (0, react_1.useRef)(0);
132
+ const lastAmplitude = (0, react_1.useRef)(0);
133
+ const lastSpikeMs = (0, react_1.useRef)(0);
134
+ (0, react_1.useEffect)(() => {
135
+ const now = Date.now();
136
+ if (amplitude > SPIKE_THRESHOLD &&
137
+ lastAmplitude.current <= SPIKE_THRESHOLD &&
138
+ now - lastSpikeMs.current > SPIKE_MIN_INTERVAL_MS) {
139
+ const idx = spikeIndex.current % spikeProgress.length;
140
+ spikeIndex.current += 1;
141
+ lastSpikeMs.current = now;
142
+ const p = spikeProgress[idx];
143
+ p.setValue(0);
144
+ react_native_1.Animated.timing(p, {
145
+ toValue: 1,
146
+ duration: SPIKE_DURATION_MS,
147
+ easing: react_native_1.Easing.out(react_native_1.Easing.cubic),
148
+ useNativeDriver: true,
149
+ }).start();
150
+ }
151
+ lastAmplitude.current = amplitude;
152
+ }, [amplitude, spikeProgress]);
153
+ // Core orb scale: amplitude-driven during active states, settles at 1 otherwise.
154
+ const coreScale = (0, react_1.useRef)(new react_native_1.Animated.Value(1)).current;
155
+ (0, react_1.useEffect)(() => {
156
+ const isActive = state === 'listening' || state === 'speaking';
157
+ const target = isActive ? 1 + amplitude * 0.5 : 1;
158
+ react_native_1.Animated.spring(coreScale, {
159
+ toValue: target,
160
+ damping: 14,
161
+ stiffness: 200,
162
+ useNativeDriver: true,
163
+ }).start();
164
+ }, [amplitude, state, coreScale]);
165
+ // Amplitude → opacity factor for baseline rings. Silence keeps a low visible
166
+ // baseline so the orb is never dead; loud voice amplifies up to ~0.45 peak.
167
+ const ringAmpFactor = amplitudeAnim.interpolate({
168
+ inputRange: [0, 1],
169
+ outputRange: [0.15, 0.45],
170
+ extrapolate: 'clamp',
171
+ });
172
+ // Native + JS driver isolation: outer Animated.View runs transform/opacity
173
+ // on native driver; inner Animated.View runs backgroundColor on JS driver.
174
+ // Mixing both on a single node throws "moved to native" at runtime.
175
+ const renderBaselineRing = (i) => {
176
+ const progress = baselineProgress[i];
177
+ const scale = progress.interpolate({
178
+ inputRange: [0, 1],
179
+ outputRange: [0.4, 2.8],
180
+ });
181
+ const envelope = progress.interpolate({
182
+ inputRange: [0, 0.12, 1],
183
+ outputRange: [0, 1, 0],
184
+ });
185
+ const opacity = react_native_1.Animated.multiply(envelope, ringAmpFactor);
186
+ return (<react_native_1.Animated.View key={`b${i}`} pointerEvents="none" style={[ringStyles.ring, { transform: [{ scale }], opacity }]}>
187
+ <react_native_1.Animated.View style={[ringStyles.ringFill, { backgroundColor: stateColor }]}/>
188
+ </react_native_1.Animated.View>);
189
+ };
190
+ const renderSpikeRing = (i) => {
191
+ const progress = spikeProgress[i];
192
+ const scale = progress.interpolate({
193
+ inputRange: [0, 1],
194
+ outputRange: [0.5, 3.4],
195
+ });
196
+ const opacity = progress.interpolate({
197
+ inputRange: [0, 0.08, 1],
198
+ outputRange: [0, 0.55, 0],
199
+ });
200
+ return (<react_native_1.Animated.View key={`s${i}`} pointerEvents="none" style={[ringStyles.ring, { transform: [{ scale }], opacity }]}>
201
+ <react_native_1.Animated.View style={[ringStyles.ringFill, { backgroundColor: stateColor }]}/>
202
+ </react_native_1.Animated.View>);
203
+ };
204
+ return (<react_native_1.View style={[ringStyles.anchor, isMuted ? { opacity: 0.45 } : null]}>
205
+ {[0, 1, 2].map(renderBaselineRing)}
206
+ {[0, 1, 2].map(renderSpikeRing)}
207
+ <react_native_1.Animated.View pointerEvents="none" style={[ringStyles.core, { transform: [{ scale: coreScale }] }]}>
208
+ <react_native_1.Animated.View style={[ringStyles.coreFill, { backgroundColor: stateColor }]}/>
209
+ <react_native_1.View style={ringStyles.highlight}/>
210
+ </react_native_1.Animated.View>
211
+ </react_native_1.View>);
212
+ }
213
+ function DefaultVoiceBackground({ children, theme, state }) {
214
+ // Slow breathing tint overlay — state-colored, very low alpha. Makes the
215
+ // whole screen "alive" without competing with the orb.
216
+ const breathAnim = (0, react_1.useRef)(new react_native_1.Animated.Value(0)).current;
217
+ (0, react_1.useEffect)(() => {
218
+ const loop = react_native_1.Animated.loop(react_native_1.Animated.sequence([
219
+ react_native_1.Animated.timing(breathAnim, {
220
+ toValue: 1,
221
+ duration: 3500,
222
+ easing: react_native_1.Easing.inOut(react_native_1.Easing.sin),
223
+ useNativeDriver: true,
224
+ }),
225
+ react_native_1.Animated.timing(breathAnim, {
226
+ toValue: 0,
227
+ duration: 3500,
228
+ easing: react_native_1.Easing.inOut(react_native_1.Easing.sin),
229
+ useNativeDriver: true,
230
+ }),
231
+ ]));
232
+ loop.start();
233
+ return () => loop.stop();
234
+ }, [breathAnim]);
235
+ const stateColorAnim = (0, react_1.useRef)(new react_native_1.Animated.Value(STATE_NUM[state])).current;
236
+ (0, react_1.useEffect)(() => {
237
+ react_native_1.Animated.timing(stateColorAnim, {
238
+ toValue: STATE_NUM[state],
239
+ duration: 600,
240
+ easing: react_native_1.Easing.inOut(react_native_1.Easing.cubic),
241
+ useNativeDriver: false,
242
+ }).start();
243
+ }, [state, stateColorAnim]);
244
+ const tintColor = stateColorAnim.interpolate({
245
+ inputRange: STATE_COLOR_INPUT,
246
+ outputRange: STATE_COLOR_OUTPUT,
247
+ });
248
+ const tintOpacity = breathAnim.interpolate({
249
+ inputRange: [0, 1],
250
+ outputRange: [0.025, 0.08],
251
+ });
252
+ return (<react_native_1.View style={[styles.container, { width: SCREEN_WIDTH, backgroundColor: theme.colors.background }]}>
253
+ {/* Outer drives opacity on native; inner drives backgroundColor on JS.
254
+ Splitting the nodes avoids the "JS-driven anim on native node" crash. */}
255
+ <react_native_1.Animated.View pointerEvents="none" style={[styles.absoluteFill, { opacity: tintOpacity }]}>
256
+ <react_native_1.Animated.View style={[styles.absoluteFill, { backgroundColor: tintColor }]}/>
257
+ </react_native_1.Animated.View>
258
+ {children}
259
+ </react_native_1.View>);
260
+ }
261
+ function DefaultVoiceTranscript({ transcript, userTranscript, state, theme, mode = 'centered', history, }) {
262
+ // Transcripts render on the page background, NOT inside chat bubbles, so
263
+ // theme.colors.text / textSecondary are the correct fields. The earlier
264
+ // implementation read bubble-internal colors (userBubbleText/aiBubbleText)
265
+ // which are typically white-on-colored — invisible on a light page bg.
266
+ const primaryTextColor = theme.colors.text ?? '#111';
267
+ const secondaryTextColor = theme.colors.textSecondary ?? '#666';
268
+ if (mode === 'chat') {
269
+ return (<ChatTranscript history={history ?? []} liveAiText={transcript} state={state} primaryTextColor={primaryTextColor} secondaryTextColor={secondaryTextColor}/>);
270
+ }
271
+ return (<>
272
+ {transcript ? (<react_native_1.View style={styles.transcriptContainer}>
273
+ <react_native_1.Text style={[styles.aiTranscript, { color: primaryTextColor }]}>{transcript}</react_native_1.Text>
274
+ </react_native_1.View>) : null}
275
+ <react_native_1.Text style={[styles.stateLabel, { color: STATE_COLORS[state] }]}>{STATE_LABELS[state]}</react_native_1.Text>
276
+ </>);
277
+ }
278
+ /**
279
+ * Default user-controlled mute toggle. View-only glyph (no SVG/Skia dep)
280
+ * to keep the SDK's peer-dep footprint lean. Active mute = red fill +
281
+ * diagonal slash drawn with a rotated 1px-thick View.
282
+ */
283
+ function DefaultVoiceMuteButton({ isMuted, onToggle, theme, }) {
284
+ const bg = isMuted
285
+ ? '#ef4444'
286
+ : theme?.colors?.surface ?? 'rgba(0,0,0,0.06)';
287
+ const fg = isMuted ? '#fff' : theme?.colors?.text ?? '#111';
288
+ return (<react_native_1.Pressable onPress={onToggle} hitSlop={8} accessibilityRole="button" accessibilityLabel={isMuted ? 'Unmute microphone' : 'Mute microphone'} accessibilityState={{ selected: isMuted }} style={({ pressed }) => [
289
+ muteStyles.button,
290
+ { backgroundColor: bg, opacity: pressed ? 0.75 : 1 },
291
+ ]}>
292
+ <MicGlyph color={fg}/>
293
+ {isMuted ? <react_native_1.View style={[muteStyles.slash, { backgroundColor: fg }]}/> : null}
294
+ </react_native_1.Pressable>);
295
+ }
296
+ /**
297
+ * Minimal microphone glyph built with stock Views — capsule body, stand
298
+ * post, and base bar. No external icon font, no SVG, no Skia.
299
+ */
300
+ function MicGlyph({ color }) {
301
+ return (<react_native_1.View style={micGlyph.wrapper} pointerEvents="none">
302
+ <react_native_1.View style={[micGlyph.capsule, { backgroundColor: color }]}/>
303
+ <react_native_1.View style={[micGlyph.post, { backgroundColor: color }]}/>
304
+ <react_native_1.View style={[micGlyph.base, { backgroundColor: color }]}/>
305
+ </react_native_1.View>);
306
+ }
307
+ const muteStyles = react_native_1.StyleSheet.create({
308
+ button: {
309
+ width: 44,
310
+ height: 44,
311
+ borderRadius: 22,
312
+ alignItems: 'center',
313
+ justifyContent: 'center',
314
+ },
315
+ slash: {
316
+ position: 'absolute',
317
+ width: 28,
318
+ height: 2,
319
+ borderRadius: 1,
320
+ transform: [{ rotate: '45deg' }],
321
+ },
322
+ });
323
+ const micGlyph = react_native_1.StyleSheet.create({
324
+ wrapper: {
325
+ width: 22,
326
+ height: 22,
327
+ alignItems: 'center',
328
+ justifyContent: 'center',
329
+ },
330
+ capsule: {
331
+ width: 8,
332
+ height: 13,
333
+ borderRadius: 4,
334
+ },
335
+ post: {
336
+ width: 2,
337
+ height: 4,
338
+ marginTop: 1,
339
+ },
340
+ base: {
341
+ width: 10,
342
+ height: 2,
343
+ borderRadius: 1,
344
+ marginTop: 1,
345
+ },
346
+ });
347
+ /**
348
+ * Chat-style transcript. Deliberately lighter than text-mode chat:
349
+ * - No bubble backgrounds — just text on the page
350
+ * - User right-aligned italic + textSecondary; AI left-aligned + text
351
+ * - Older turns fade by opacity to feel ephemeral
352
+ * - Auto-scrolls to bottom on new content
353
+ * - Live streaming AI text appears at the bottom as a partial line
354
+ */
355
+ function ChatTranscript({ history, liveAiText, state, primaryTextColor, secondaryTextColor, }) {
356
+ const scrollRef = (0, react_1.useRef)(null);
357
+ const lastTurnsToShow = 8;
358
+ const visible = history.slice(-lastTurnsToShow);
359
+ (0, react_1.useEffect)(() => {
360
+ // Defer until layout commits so contentSize is accurate.
361
+ const t = setTimeout(() => {
362
+ scrollRef.current?.scrollToEnd({ animated: true });
363
+ }, 50);
364
+ return () => clearTimeout(t);
365
+ }, [history.length, liveAiText]);
366
+ return (<react_native_1.View style={chatStyles.container}>
367
+ <react_native_1.ScrollView ref={scrollRef} style={chatStyles.scroll} contentContainerStyle={chatStyles.scrollContent} showsVerticalScrollIndicator={false}>
368
+ {visible.map((turn, idx) => {
369
+ // Opacity fade for older turns: newest = 1, then drop ~0.12 per step,
370
+ // floor at 0.35 so even old turns remain legible.
371
+ const stepsBack = visible.length - 1 - idx;
372
+ const opacity = Math.max(0.35, 1 - stepsBack * 0.12);
373
+ const isUser = turn.role === 'user';
374
+ return (<react_native_1.View key={turn.id} style={[
375
+ chatStyles.row,
376
+ { alignItems: isUser ? 'flex-end' : 'flex-start', opacity },
377
+ ]}>
378
+ <react_native_1.Text style={[
379
+ isUser ? chatStyles.userText : chatStyles.aiText,
380
+ { color: isUser ? secondaryTextColor : primaryTextColor },
381
+ ]}>
382
+ {turn.text}
383
+ </react_native_1.Text>
384
+ </react_native_1.View>);
385
+ })}
386
+ {liveAiText ? (<react_native_1.View style={[chatStyles.row, { alignItems: 'flex-start' }]}>
387
+ <react_native_1.Text style={[chatStyles.aiText, { color: primaryTextColor }]}>
388
+ {liveAiText}
389
+ </react_native_1.Text>
390
+ </react_native_1.View>) : null}
391
+ </react_native_1.ScrollView>
392
+ <react_native_1.Text style={[chatStyles.stateLabel, { color: STATE_COLORS[state] }]}>
393
+ {STATE_LABELS[state]}
394
+ </react_native_1.Text>
395
+ </react_native_1.View>);
396
+ }
397
+ // --- Main component ---
398
+ function VoicePage({ state, transcript, userTranscript, transcriptHistory, transcriptMode = 'centered', amplitude, theme, voiceComponents, toolStatus, renderedTools, registeredComponents, transcriptOverrideForTurn = false, DataChipListComponent, isMuted = false, onToggleMute, }) {
399
+ const Indicator = voiceComponents?.VoiceIndicator || DefaultVoiceIndicator;
400
+ const Background = voiceComponents?.VoiceBackground || DefaultVoiceBackground;
401
+ const Transcript = voiceComponents?.VoiceTranscript || DefaultVoiceTranscript;
402
+ const MuteButton = voiceComponents?.VoiceMuteButton || DefaultVoiceMuteButton;
403
+ // Disable rendering the button entirely when there's nothing to drive it,
404
+ // e.g. consumer drives the SDK without using useVoiceChat's mute layer.
405
+ const showMute = typeof onToggleMute === 'function';
406
+ const hasToolUI = !transcriptOverrideForTurn && (renderedTools?.length ?? 0) > 0;
407
+ // When tool UI is rendered, the compact status bar takes over regardless of
408
+ // chosen mode — preserves the single-screen / hands-free experience.
409
+ // Chat resumes automatically when the tool UI is dismissed.
410
+ const showTranscript = (!hasToolUI && transcriptMode !== 'off') || transcriptOverrideForTurn;
411
+ const isChatLayout = !hasToolUI && transcriptMode === 'chat';
412
+ // Edge-to-edge bg painting is handled by the parent Qafka container: when
413
+ // voice is enabled, it wraps the horizontal pager ScrollView with negative
414
+ // margins so that VoicePage's slot spans the full screen (including the
415
+ // notch and home-indicator bands). Here we only need to apply safe-area
416
+ // padding to the inner content so orb/transcript/tool UI stays clear of
417
+ // those bands. Background paints across the full slot.
418
+ const insets = (0, react_native_safe_area_context_1.useSafeAreaInsets)();
419
+ return (<Background state={state} amplitude={amplitude} theme={theme}>
420
+ <react_native_1.View style={{
421
+ flex: 1,
422
+ width: '100%',
423
+ paddingTop: insets.top,
424
+ paddingBottom: insets.bottom,
425
+ }}>
426
+ <react_native_1.View style={{
427
+ flex: 1,
428
+ width: '100%',
429
+ alignSelf: 'stretch',
430
+ }}>
431
+ {hasToolUI ? (
432
+ // Compact sticky status bar — replaces the large centered orb when
433
+ // tool UI is rendered. Maximizes vertical room for the tool cards
434
+ // below; keeps the indicator visible without dominating the view.
435
+ <react_native_1.View style={{
436
+ paddingHorizontal: 20,
437
+ paddingVertical: 6,
438
+ flexDirection: 'row',
439
+ alignItems: 'center',
440
+ gap: 12,
441
+ borderBottomWidth: react_native_1.StyleSheet.hairlineWidth,
442
+ borderBottomColor: 'rgba(0,0,0,0.08)',
443
+ backgroundColor: theme?.colors?.background ?? '#fff',
444
+ }}>
445
+ <react_native_1.View style={{
446
+ width: 44,
447
+ height: 44,
448
+ alignItems: 'center',
449
+ justifyContent: 'center',
450
+ overflow: 'hidden',
451
+ }}>
452
+ {/* Scale the consumer-supplied indicator down to fit a small
453
+ status-bar slot. Most custom indicators size themselves
454
+ around 180-200px; 0.22 scale gives ~40px effective. */}
455
+ <react_native_1.View style={{
456
+ width: 200,
457
+ height: 200,
458
+ alignItems: 'center',
459
+ justifyContent: 'center',
460
+ transform: [{ scale: 0.22 }],
461
+ }} pointerEvents="none">
462
+ <Indicator state={state} amplitude={amplitude} theme={theme} isMuted={isMuted}/>
463
+ </react_native_1.View>
464
+ </react_native_1.View>
465
+
466
+ <react_native_1.View style={{ flex: 1 }}>
467
+ <react_native_1.Text style={{
468
+ fontSize: 13,
469
+ fontWeight: react_native_1.Platform.select({ ios: '600', default: 'bold' }),
470
+ color: STATE_COLORS[state],
471
+ }} numberOfLines={1}>
472
+ {STATE_LABELS[state]}
473
+ </react_native_1.Text>
474
+ {toolStatus?.loadingMessage ? (<react_native_1.Text style={{ fontSize: 11, color: '#888', marginTop: 1 }} numberOfLines={1}>
475
+ {toolStatus.loadingMessage}
476
+ </react_native_1.Text>) : null}
477
+ </react_native_1.View>
478
+ {showMute ? (<MuteButton isMuted={isMuted} onToggle={onToggleMute} theme={theme} state={state}/>) : null}
479
+ </react_native_1.View>) : isChatLayout ? (
480
+ // Chat layout: orb anchored at the top, chat history fills the rest.
481
+ // The orb moves up (vs. centered) to give the transcript room to breathe.
482
+ <react_native_1.View style={{
483
+ flex: 1,
484
+ alignItems: 'center',
485
+ width: '100%',
486
+ paddingTop: 32,
487
+ }}>
488
+ <react_native_1.View style={{ alignItems: 'center', justifyContent: 'center' }}>
489
+ <Indicator state={state} amplitude={amplitude} theme={theme} isMuted={isMuted}/>
490
+ </react_native_1.View>
491
+
492
+ {toolStatus ? (<react_native_1.View style={{ marginTop: 4, alignItems: 'center' }}>
493
+ <ToolStatusPill_1.ToolStatusPill message={toolStatus.loadingMessage} theme={theme}/>
494
+ </react_native_1.View>) : null}
495
+
496
+ <react_native_1.View style={{ flex: 1, width: '100%' }}>
497
+ <Transcript transcript={transcript} userTranscript={userTranscript} state={state} theme={theme} mode="chat" history={transcriptHistory}/>
498
+ </react_native_1.View>
499
+ </react_native_1.View>) : (
500
+ // Centered (default) and 'off' both use the existing centered orb layout.
501
+ // For 'off' we simply omit the Transcript component.
502
+ <react_native_1.View style={{
503
+ flex: 1,
504
+ alignItems: 'center',
505
+ justifyContent: 'center',
506
+ }}>
507
+ <react_native_1.View style={{ alignItems: 'center', justifyContent: 'center' }}>
508
+ <Indicator state={state} amplitude={amplitude} theme={theme} isMuted={isMuted}/>
509
+ </react_native_1.View>
510
+
511
+ {toolStatus ? (<react_native_1.View style={{ marginTop: 12, alignItems: 'center' }}>
512
+ <ToolStatusPill_1.ToolStatusPill message={toolStatus.loadingMessage} theme={theme}/>
513
+ </react_native_1.View>) : null}
514
+
515
+ {showTranscript ? (<Transcript transcript={transcript} userTranscript={userTranscript} state={state} theme={theme} mode="centered"/>) : null}
516
+ </react_native_1.View>)}
517
+
518
+ {hasToolUI ? (<react_native_1.ScrollView style={{ flex: 1, width: '100%', alignSelf: 'stretch' }} contentContainerStyle={{
519
+ paddingHorizontal: 20,
520
+ paddingTop: 16,
521
+ paddingBottom: 24,
522
+ gap: 10,
523
+ }} showsVerticalScrollIndicator={false}>
524
+ {renderedTools.map((rtUnion) => {
525
+ if (rtUnion.kind === 'display') {
526
+ const ChipList = DataChipListComponent ?? DataChipList_1.DataChipList;
527
+ return (<react_native_1.View key={rtUnion.toolCallId}>
528
+ <ChipList items={rtUnion.items} theme={theme}/>
529
+ </react_native_1.View>);
530
+ }
531
+ const rt = rtUnion;
532
+ // Empty-data fallback (matches text chat list-item behavior)
533
+ const isListTool = rt.tool?.uiConfig?.responseType === 'list';
534
+ if (isListTool &&
535
+ (rt.data?.data === undefined ||
536
+ rt.data?.data === null ||
537
+ (Array.isArray(rt.data?.data) && rt.data.data.length === 0))) {
538
+ return (<react_native_1.View key={rt.toolCallId} style={{
539
+ padding: 12,
540
+ backgroundColor: '#f0f0f0',
541
+ borderRadius: 10,
542
+ }}>
543
+ <react_native_1.Text>No results.</react_native_1.Text>
544
+ </react_native_1.View>);
545
+ }
546
+ const componentKey = rt.tool?.uiConfig?.itemComponent || rt.toolKey;
547
+ const Component = registeredComponents?.[componentKey];
548
+ if (!Component) {
549
+ if (__DEV__) {
550
+ console.warn(`[Qafka] No component registered for "${componentKey}" (available: ${Object.keys(registeredComponents ?? {}).join(', ') || 'none'})`);
551
+ }
552
+ return null;
553
+ }
554
+ // List rendering: uiConfig.responseType === 'list' + dataPath === 'data'
555
+ const listData = rt.tool?.uiConfig?.responseType === 'list' && rt.data?.data
556
+ ? rt.data.data
557
+ : null;
558
+ if (Array.isArray(listData)) {
559
+ const max = rt.tool?.uiConfig?.maxItems || 10;
560
+ return (<react_native_1.View key={rt.toolCallId} style={{ gap: 10 }}>
561
+ {listData.slice(0, max).map((item, idx) => (<Component key={`${rt.toolCallId}_${idx}`} data={item}/>))}
562
+ </react_native_1.View>);
563
+ }
564
+ // Non-list: render once with full data
565
+ return <Component key={rt.toolCallId} data={rt.data}/>;
566
+ })}
567
+ </react_native_1.ScrollView>) : null}
568
+ </react_native_1.View>
569
+
570
+ {showMute && !hasToolUI ? (<react_native_1.View style={{
571
+ position: 'absolute',
572
+ top: 12,
573
+ right: 16,
574
+ }}>
575
+ <MuteButton isMuted={isMuted} onToggle={onToggleMute} theme={theme} state={state}/>
576
+ </react_native_1.View>) : null}
577
+ </react_native_1.View>
578
+ </Background>);
579
+ }
580
+ const styles = react_native_1.StyleSheet.create({
581
+ // Background: no horizontal padding so tool-mode ScrollView and consumer
582
+ // tool components control their own padding (default 20px). For orb-only
583
+ // mode, the inner content is centered by flex anyway.
584
+ container: { flex: 1, alignItems: 'center', justifyContent: 'center' },
585
+ absoluteFill: { position: 'absolute', top: 0, left: 0, right: 0, bottom: 0 },
586
+ transcriptContainer: { marginBottom: 16, paddingHorizontal: 16, maxWidth: '90%' },
587
+ userTranscript: { fontSize: 14, opacity: 0.6, textAlign: 'center' },
588
+ aiTranscript: { fontSize: 18, textAlign: 'center', lineHeight: 26 },
589
+ stateLabel: { fontSize: 14, marginTop: 24 },
590
+ });
591
+ const ringStyles = react_native_1.StyleSheet.create({
592
+ anchor: {
593
+ width: ANCHOR,
594
+ height: ANCHOR,
595
+ alignItems: 'center',
596
+ justifyContent: 'center',
597
+ marginBottom: 32,
598
+ },
599
+ ring: {
600
+ position: 'absolute',
601
+ top: (ANCHOR - RING_BASE) / 2,
602
+ left: (ANCHOR - RING_BASE) / 2,
603
+ width: RING_BASE,
604
+ height: RING_BASE,
605
+ borderRadius: RING_BASE / 2,
606
+ },
607
+ ringFill: {
608
+ width: '100%',
609
+ height: '100%',
610
+ borderRadius: RING_BASE / 2,
611
+ },
612
+ core: {
613
+ position: 'absolute',
614
+ top: (ANCHOR - CORE) / 2,
615
+ left: (ANCHOR - CORE) / 2,
616
+ width: CORE,
617
+ height: CORE,
618
+ borderRadius: CORE / 2,
619
+ // iOS-only colored shadow gives a subtle glow at no perf cost.
620
+ // Android falls back gracefully (no glow, but core stays vivid via core+highlight).
621
+ ...react_native_1.Platform.select({
622
+ ios: {
623
+ shadowColor: '#000',
624
+ shadowOffset: { width: 0, height: 2 },
625
+ shadowOpacity: 0.15,
626
+ shadowRadius: 12,
627
+ },
628
+ default: {},
629
+ }),
630
+ },
631
+ coreFill: {
632
+ width: '100%',
633
+ height: '100%',
634
+ borderRadius: CORE / 2,
635
+ },
636
+ highlight: {
637
+ position: 'absolute',
638
+ top: 10,
639
+ left: 12,
640
+ width: 22,
641
+ height: 22,
642
+ borderRadius: 11,
643
+ backgroundColor: 'rgba(255,255,255,0.45)',
644
+ },
645
+ });
646
+ const chatStyles = react_native_1.StyleSheet.create({
647
+ container: {
648
+ flex: 1,
649
+ width: '100%',
650
+ paddingHorizontal: 24,
651
+ paddingTop: 12,
652
+ paddingBottom: 12,
653
+ },
654
+ scroll: { flex: 1, width: '100%' },
655
+ scrollContent: {
656
+ flexGrow: 1,
657
+ justifyContent: 'flex-end',
658
+ paddingBottom: 8,
659
+ },
660
+ row: {
661
+ width: '100%',
662
+ marginBottom: 10,
663
+ },
664
+ aiText: {
665
+ fontSize: 16,
666
+ lineHeight: 22,
667
+ textAlign: 'left',
668
+ maxWidth: '92%',
669
+ },
670
+ userText: {
671
+ fontSize: 14,
672
+ lineHeight: 20,
673
+ fontStyle: 'italic',
674
+ textAlign: 'right',
675
+ maxWidth: '85%',
676
+ },
677
+ stateLabel: {
678
+ fontSize: 13,
679
+ textAlign: 'center',
680
+ marginTop: 8,
681
+ marginBottom: 4,
682
+ },
683
+ });