@opengeni/react-native 1.4.5-canary.37615713321001

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 (132) hide show
  1. package/LICENSE +190 -0
  2. package/README.md +74 -0
  3. package/dist/adapters.d.ts +53 -0
  4. package/dist/adapters.js +3 -0
  5. package/dist/adapters.js.map +1 -0
  6. package/dist/attachments.d.ts +75 -0
  7. package/dist/chunk-3VSZAQ7G.js +391 -0
  8. package/dist/chunk-3VSZAQ7G.js.map +1 -0
  9. package/dist/chunk-5ZVK7WKA.js +88 -0
  10. package/dist/chunk-5ZVK7WKA.js.map +1 -0
  11. package/dist/chunk-EEDIMY3L.js +240 -0
  12. package/dist/chunk-EEDIMY3L.js.map +1 -0
  13. package/dist/chunk-M23SDI4R.js +83 -0
  14. package/dist/chunk-M23SDI4R.js.map +1 -0
  15. package/dist/chunk-PHJBSAR3.js +2006 -0
  16. package/dist/chunk-PHJBSAR3.js.map +1 -0
  17. package/dist/environment.d.ts +19 -0
  18. package/dist/expo.d.ts +25 -0
  19. package/dist/expo.js +267 -0
  20. package/dist/expo.js.map +1 -0
  21. package/dist/index.d.ts +13 -0
  22. package/dist/index.js +3345 -0
  23. package/dist/index.js.map +1 -0
  24. package/dist/presentation.d.ts +262 -0
  25. package/dist/read-acknowledgement.d.ts +39 -0
  26. package/dist/realtime/call-events.d.ts +11 -0
  27. package/dist/realtime/call-view.d.ts +26 -0
  28. package/dist/realtime/expo-call.d.ts +11 -0
  29. package/dist/realtime/native-call.d.ts +104 -0
  30. package/dist/realtime/native-realtime.d.ts +35 -0
  31. package/dist/realtime/use-native-session-realtime.d.ts +25 -0
  32. package/dist/session-view.d.ts +87 -0
  33. package/dist/timeline/activity.d.ts +34 -0
  34. package/dist/timeline/composer.d.ts +86 -0
  35. package/dist/timeline/controls.d.ts +27 -0
  36. package/dist/timeline/decisions.d.ts +29 -0
  37. package/dist/timeline/feedback.d.ts +26 -0
  38. package/dist/timeline/icon-nodes.generated.d.ts +738 -0
  39. package/dist/timeline/icon.d.ts +11 -0
  40. package/dist/timeline/index.d.ts +32 -0
  41. package/dist/timeline/index.js +5419 -0
  42. package/dist/timeline/index.js.map +1 -0
  43. package/dist/timeline/markdown.d.ts +11 -0
  44. package/dist/timeline/markdown.js +415 -0
  45. package/dist/timeline/markdown.js.map +1 -0
  46. package/dist/timeline/message-attachments.d.ts +13 -0
  47. package/dist/timeline/message-timeline.d.ts +36 -0
  48. package/dist/timeline/model-picker.d.ts +17 -0
  49. package/dist/timeline/primitives.d.ts +85 -0
  50. package/dist/timeline/queue-dock.d.ts +19 -0
  51. package/dist/timeline/session-actions.d.ts +34 -0
  52. package/dist/timeline/session-list.d.ts +50 -0
  53. package/dist/timeline/session-screen.d.ts +35 -0
  54. package/dist/timeline/session-signals.d.ts +37 -0
  55. package/dist/timeline/sheet.d.ts +14 -0
  56. package/dist/timeline/theme.d.ts +58 -0
  57. package/dist/timeline/thinking-orb.d.ts +13 -0
  58. package/dist/timeline/turn.d.ts +80 -0
  59. package/dist/ui/attention.d.ts +14 -0
  60. package/dist/ui/cards.d.ts +21 -0
  61. package/dist/ui/composer.d.ts +21 -0
  62. package/dist/ui/format.d.ts +13 -0
  63. package/dist/ui/icons.d.ts +9 -0
  64. package/dist/ui/index.d.ts +12 -0
  65. package/dist/ui/index.js +1757 -0
  66. package/dist/ui/index.js.map +1 -0
  67. package/dist/ui/steps-sheet.d.ts +10 -0
  68. package/dist/ui/theme.d.ts +63 -0
  69. package/dist/ui/timeline.d.ts +48 -0
  70. package/dist/ui/web-tokens.gen.d.ts +90 -0
  71. package/dist/use-native-session.d.ts +40 -0
  72. package/dist/view-model.d.ts +52 -0
  73. package/dist/voice-input.d.ts +102 -0
  74. package/dist/webrtc.d.ts +9 -0
  75. package/dist/webrtc.js +19 -0
  76. package/dist/webrtc.js.map +1 -0
  77. package/expo-module.config.json +7 -0
  78. package/ios/OpenGeniCall.podspec +22 -0
  79. package/ios/OpenGeniCallAppDelegateSubscriber.swift +29 -0
  80. package/ios/OpenGeniCallCenter.swift +189 -0
  81. package/ios/OpenGeniCallModule.swift +64 -0
  82. package/package.json +93 -0
  83. package/src/adapters.ts +150 -0
  84. package/src/attachments.ts +396 -0
  85. package/src/environment.tsx +214 -0
  86. package/src/expo.ts +295 -0
  87. package/src/index.ts +98 -0
  88. package/src/presentation.ts +285 -0
  89. package/src/read-acknowledgement.ts +72 -0
  90. package/src/realtime/call-events.ts +41 -0
  91. package/src/realtime/call-view.tsx +220 -0
  92. package/src/realtime/expo-call.ts +49 -0
  93. package/src/realtime/native-call.ts +266 -0
  94. package/src/realtime/native-realtime.ts +115 -0
  95. package/src/realtime/use-native-session-realtime.ts +29 -0
  96. package/src/session-view.tsx +2251 -0
  97. package/src/timeline/activity.tsx +736 -0
  98. package/src/timeline/composer.tsx +686 -0
  99. package/src/timeline/controls.tsx +151 -0
  100. package/src/timeline/decisions.tsx +651 -0
  101. package/src/timeline/feedback.tsx +251 -0
  102. package/src/timeline/icon-nodes.generated.ts +1830 -0
  103. package/src/timeline/icon.tsx +44 -0
  104. package/src/timeline/index.ts +67 -0
  105. package/src/timeline/markdown.tsx +510 -0
  106. package/src/timeline/message-attachments.tsx +155 -0
  107. package/src/timeline/message-timeline.tsx +1042 -0
  108. package/src/timeline/model-picker.tsx +450 -0
  109. package/src/timeline/primitives.tsx +561 -0
  110. package/src/timeline/queue-dock.tsx +331 -0
  111. package/src/timeline/session-actions.tsx +379 -0
  112. package/src/timeline/session-list.tsx +338 -0
  113. package/src/timeline/session-screen.tsx +488 -0
  114. package/src/timeline/session-signals.tsx +312 -0
  115. package/src/timeline/sheet.tsx +153 -0
  116. package/src/timeline/theme.tsx +128 -0
  117. package/src/timeline/thinking-orb.tsx +93 -0
  118. package/src/timeline/turn.tsx +569 -0
  119. package/src/ui/attention.tsx +220 -0
  120. package/src/ui/cards.tsx +267 -0
  121. package/src/ui/composer.tsx +296 -0
  122. package/src/ui/format.ts +86 -0
  123. package/src/ui/icons.tsx +131 -0
  124. package/src/ui/index.ts +18 -0
  125. package/src/ui/steps-sheet.tsx +71 -0
  126. package/src/ui/theme.ts +261 -0
  127. package/src/ui/timeline.tsx +837 -0
  128. package/src/ui/web-tokens.gen.ts +94 -0
  129. package/src/use-native-session.ts +219 -0
  130. package/src/view-model.ts +485 -0
  131. package/src/voice-input.ts +382 -0
  132. package/src/webrtc.ts +30 -0
@@ -0,0 +1,214 @@
1
+ import {
2
+ createContext,
3
+ type ReactNode,
4
+ useContext,
5
+ useEffect,
6
+ useLayoutEffect,
7
+ useMemo,
8
+ useState,
9
+ } from "react";
10
+ import type { OpenGeniReactNativeAdapters } from "./adapters";
11
+ import { sha256HexToArrayBuffer } from "./adapters";
12
+
13
+ interface EventTargetLike {
14
+ addEventListener(type: string, listener: () => void): void;
15
+ removeEventListener(type: string, listener: () => void): void;
16
+ dispatch(type: string): void;
17
+ }
18
+
19
+ function createEventTarget(): EventTargetLike {
20
+ const listeners = new Map<string, Set<() => void>>();
21
+ return {
22
+ addEventListener: (type, listener) => {
23
+ const existing = listeners.get(type) ?? new Set<() => void>();
24
+ existing.add(listener);
25
+ listeners.set(type, existing);
26
+ },
27
+ removeEventListener: (type, listener) => {
28
+ listeners.get(type)?.delete(listener);
29
+ },
30
+ dispatch: (type) => {
31
+ for (const listener of listeners.get(type) ?? []) listener();
32
+ },
33
+ };
34
+ }
35
+
36
+ interface InstalledEnvironment {
37
+ cleanup(): void;
38
+ }
39
+
40
+ /** Install only the browser-compatible primitives currently read by the official session hooks. */
41
+ export function installOpenGeniReactNativeEnvironment(
42
+ adapters: OpenGeniReactNativeAdapters,
43
+ ): InstalledEnvironment {
44
+ const globals = globalThis;
45
+ const windowTarget = createEventTarget();
46
+ const documentTarget = createEventTarget();
47
+ const existingWindow = Reflect.get(globals, "window");
48
+ const existingSessionStorage = Reflect.get(globals, "sessionStorage");
49
+ const existingDocument = Reflect.get(globals, "document");
50
+ const existingCrypto = Reflect.get(globals, "crypto");
51
+ const nativeWindow = createDelegatingObject(isObject(existingWindow) ? existingWindow : globals);
52
+ let lifecycleState = adapters.lifecycle.currentState();
53
+
54
+ const sessionStorage = {
55
+ getItem: (key: string) => adapters.persistence.getItem(key),
56
+ setItem: (key: string, value: string) => adapters.persistence.setItem(key, value),
57
+ removeItem: (key: string) => adapters.persistence.removeItem(key),
58
+ clear: () => adapters.persistence.clear(),
59
+ key: () => null,
60
+ get length() {
61
+ return 0;
62
+ },
63
+ };
64
+
65
+ const nativeCrypto = createDelegatingObject(isObject(existingCrypto) ? existingCrypto : null);
66
+ const subtleValue = isObject(existingCrypto) ? Reflect.get(existingCrypto, "subtle") : undefined;
67
+ const nativeSubtle = createDelegatingObject(isObject(subtleValue) ? subtleValue : null);
68
+ Reflect.set(nativeCrypto, "randomUUID", () => adapters.crypto.randomUUID());
69
+ Reflect.set(
70
+ nativeSubtle,
71
+ "digest",
72
+ async (algorithm: AlgorithmIdentifier, data: BufferSource) => {
73
+ const name = typeof algorithm === "string" ? algorithm : algorithm.name;
74
+ if (name.toUpperCase() !== "SHA-256") {
75
+ throw new TypeError(`Unsupported native digest algorithm: ${name}`);
76
+ }
77
+ const bytes =
78
+ data instanceof ArrayBuffer
79
+ ? new Uint8Array(data)
80
+ : new Uint8Array(data.buffer, data.byteOffset, data.byteLength);
81
+ return sha256HexToArrayBuffer(await adapters.crypto.sha256Hex(bytes));
82
+ },
83
+ );
84
+ Reflect.set(nativeCrypto, "subtle", nativeSubtle);
85
+ Reflect.set(globals, "crypto", nativeCrypto);
86
+
87
+ Reflect.set(nativeWindow, "sessionStorage", sessionStorage);
88
+ if (typeof Reflect.get(nativeWindow, "setTimeout") !== "function") {
89
+ Reflect.set(nativeWindow, "setTimeout", globalThis.setTimeout.bind(globalThis));
90
+ }
91
+ if (typeof Reflect.get(nativeWindow, "clearTimeout") !== "function") {
92
+ Reflect.set(nativeWindow, "clearTimeout", globalThis.clearTimeout.bind(globalThis));
93
+ }
94
+ if (typeof Reflect.get(nativeWindow, "addEventListener") !== "function") {
95
+ Reflect.set(nativeWindow, "addEventListener", (type: string, listener: () => void) =>
96
+ windowTarget.addEventListener(type, listener),
97
+ );
98
+ }
99
+ if (typeof Reflect.get(nativeWindow, "removeEventListener") !== "function") {
100
+ Reflect.set(nativeWindow, "removeEventListener", (type: string, listener: () => void) =>
101
+ windowTarget.removeEventListener(type, listener),
102
+ );
103
+ }
104
+ Reflect.set(globals, "window", nativeWindow);
105
+ Reflect.set(globals, "sessionStorage", sessionStorage);
106
+
107
+ const nativeDocument = {
108
+ get visibilityState() {
109
+ return lifecycleState === "active" ? "visible" : "hidden";
110
+ },
111
+ addEventListener: (type: string, listener: () => void) =>
112
+ documentTarget.addEventListener(type, listener),
113
+ removeEventListener: (type: string, listener: () => void) =>
114
+ documentTarget.removeEventListener(type, listener),
115
+ };
116
+ Reflect.set(globals, "document", nativeDocument);
117
+
118
+ const unsubscribe = adapters.lifecycle.subscribe((next) => {
119
+ const wasActive = lifecycleState === "active";
120
+ lifecycleState = next;
121
+ documentTarget.dispatch("visibilitychange");
122
+ if (!wasActive && next === "active") windowTarget.dispatch("pageshow");
123
+ });
124
+
125
+ return {
126
+ cleanup: () => {
127
+ unsubscribe();
128
+ restoreGlobal(globals, "window", nativeWindow, existingWindow);
129
+ restoreGlobal(globals, "sessionStorage", sessionStorage, existingSessionStorage);
130
+ restoreGlobal(globals, "document", nativeDocument, existingDocument);
131
+ restoreGlobal(globals, "crypto", nativeCrypto, existingCrypto);
132
+ },
133
+ };
134
+ }
135
+
136
+ function restoreGlobal(
137
+ target: typeof globalThis,
138
+ key: string,
139
+ installedValue: unknown,
140
+ previousValue: unknown,
141
+ ): void {
142
+ if (Reflect.get(target, key) !== installedValue) return;
143
+ if (previousValue === undefined) Reflect.deleteProperty(target, key);
144
+ else Reflect.set(target, key, previousValue);
145
+ }
146
+
147
+ function isObject(value: unknown): value is object {
148
+ return (typeof value === "object" && value !== null) || typeof value === "function";
149
+ }
150
+
151
+ function createDelegatingObject(prototype: object | null): object {
152
+ const value = {};
153
+ Object.setPrototypeOf(value, prototype);
154
+ return value;
155
+ }
156
+
157
+ export interface NativeEnvironmentContextValue {
158
+ adapters: OpenGeniReactNativeAdapters;
159
+ active: boolean;
160
+ }
161
+
162
+ const NativeEnvironmentContext = createContext<NativeEnvironmentContextValue | null>(null);
163
+
164
+ export interface OpenGeniReactNativeProviderProps {
165
+ adapters: OpenGeniReactNativeAdapters;
166
+ children: ReactNode;
167
+ loadingFallback?: ReactNode;
168
+ }
169
+
170
+ export function OpenGeniReactNativeProvider({
171
+ adapters,
172
+ children,
173
+ loadingFallback = null,
174
+ }: OpenGeniReactNativeProviderProps) {
175
+ const [hydrated, setHydrated] = useState(false);
176
+ const [active, setActive] = useState(adapters.lifecycle.currentState() === "active");
177
+
178
+ useEffect(() => {
179
+ let mounted = true;
180
+ setHydrated(false);
181
+ void adapters.persistence.hydrate().then(() => {
182
+ if (!mounted) return;
183
+ setHydrated(true);
184
+ });
185
+ const unsubscribe = adapters.lifecycle.subscribe((state) => setActive(state === "active"));
186
+ return () => {
187
+ mounted = false;
188
+ unsubscribe();
189
+ };
190
+ }, [adapters]);
191
+
192
+ useLayoutEffect(() => {
193
+ const installed = installOpenGeniReactNativeEnvironment(adapters);
194
+ return () => {
195
+ // React removes parent layout effects before passive effects in deleted children.
196
+ // Keep the browser primitives alive until those official hook cleanups have run.
197
+ globalThis.setTimeout(() => installed.cleanup(), 0);
198
+ };
199
+ }, [adapters]);
200
+
201
+ const value = useMemo(() => ({ adapters, active }), [active, adapters]);
202
+ if (!hydrated) return loadingFallback;
203
+ return (
204
+ <NativeEnvironmentContext.Provider value={value}>{children}</NativeEnvironmentContext.Provider>
205
+ );
206
+ }
207
+
208
+ export function useOpenGeniReactNativeEnvironment(): NativeEnvironmentContextValue {
209
+ const value = useContext(NativeEnvironmentContext);
210
+ if (!value) {
211
+ throw new Error("OpenGeni native hooks require OpenGeniReactNativeProvider");
212
+ }
213
+ return value;
214
+ }
package/src/expo.ts ADDED
@@ -0,0 +1,295 @@
1
+ // @opengeni/react-native/expo: ready-made adapters for Expo hosts.
2
+ //
3
+ // Hosts own persistence (where drafts and pending operations live, and how they are
4
+ // namespaced per signed-in principal); everything else here is the standard Expo
5
+ // implementation of the adapter contract.
6
+ import { AppState } from "react-native";
7
+ import { useMemo } from "react";
8
+ import * as Crypto from "expo-crypto";
9
+ import * as DocumentPicker from "expo-document-picker";
10
+ import { fetch as expoFetch } from "expo/fetch";
11
+ import {
12
+ RecordingPresets,
13
+ requestRecordingPermissionsAsync,
14
+ setAudioModeAsync,
15
+ useAudioRecorder,
16
+ } from "expo-audio";
17
+ import { Directory, File, Paths } from "expo-file-system";
18
+ import * as ImagePicker from "expo-image-picker";
19
+ import type { FetchLike, OpenGeniClient } from "@opengeni/sdk";
20
+ import type {
21
+ NativeLifecycleState,
22
+ NativePersistenceAdapter,
23
+ NativePickedFile,
24
+ OpenGeniReactNativeAdapters,
25
+ } from "./adapters";
26
+ import {
27
+ NativeTranscriptionError,
28
+ type NativeVoiceRecorder,
29
+ type NativeVoiceRecording,
30
+ } from "./voice-input";
31
+
32
+ export { createExpoCallAdapter, parseNativeCallEvent } from "./realtime/expo-call";
33
+
34
+ function lifecycleState(value: string | null | undefined): NativeLifecycleState {
35
+ if (value === "active") return "active";
36
+ if (value === "background") return "background";
37
+ return "inactive";
38
+ }
39
+
40
+ function bytesToHex(bytes: Uint8Array): string {
41
+ return [...bytes].map((byte) => byte.toString(16).padStart(2, "0")).join("");
42
+ }
43
+
44
+ function base64ToBytes(base64: string): Uint8Array {
45
+ const binary = globalThis.atob(base64);
46
+ const bytes = new Uint8Array(binary.length);
47
+ for (let index = 0; index < binary.length; index += 1) bytes[index] = binary.charCodeAt(index);
48
+ return bytes;
49
+ }
50
+
51
+ function fallbackFilename(uri: string, contentType: string): string {
52
+ const raw = uri.split("/").at(-1)?.split("?")[0];
53
+ if (raw) {
54
+ try {
55
+ return decodeURIComponent(raw);
56
+ } catch {
57
+ return raw;
58
+ }
59
+ }
60
+ return contentType.startsWith("image/") ? "image" : "document";
61
+ }
62
+
63
+ function isRuntimeFetchResponse(value: unknown): value is Response {
64
+ return (
65
+ typeof value === "object" &&
66
+ value !== null &&
67
+ "status" in value &&
68
+ typeof value.status === "number" &&
69
+ "headers" in value &&
70
+ typeof value.headers === "object" &&
71
+ value.headers !== null &&
72
+ "json" in value &&
73
+ typeof value.json === "function" &&
74
+ "text" in value &&
75
+ typeof value.text === "function" &&
76
+ "arrayBuffer" in value &&
77
+ typeof value.arrayBuffer === "function" &&
78
+ "clone" in value &&
79
+ typeof value.clone === "function"
80
+ );
81
+ }
82
+
83
+ /** Streaming fetch backed by Expo's native implementation (required for live event streams). */
84
+ export const expoStreamingFetch: FetchLike = async (input, init) => {
85
+ const response = await expoFetch(input as string, init as never);
86
+ // Preserve Expo's native response object: re-wrapping its body corrupts JSON reads on iOS.
87
+ if (!isRuntimeFetchResponse(response)) {
88
+ throw new TypeError("Expo fetch returned an incompatible response");
89
+ }
90
+ return response;
91
+ };
92
+
93
+ export const expoLifecycleAdapter: OpenGeniReactNativeAdapters["lifecycle"] = {
94
+ currentState: () => lifecycleState(AppState.currentState),
95
+ subscribe: (listener) => {
96
+ const subscription = AppState.addEventListener("change", (state) =>
97
+ listener(lifecycleState(state)),
98
+ );
99
+ return () => subscription.remove();
100
+ },
101
+ };
102
+
103
+ export const expoCryptoAdapter: OpenGeniReactNativeAdapters["crypto"] = {
104
+ randomUUID: Crypto.randomUUID,
105
+ sha256Hex: async (bytes) => {
106
+ const digestInput: Uint8Array<ArrayBuffer> =
107
+ bytes.buffer instanceof ArrayBuffer
108
+ ? new Uint8Array(bytes.buffer, bytes.byteOffset, bytes.byteLength)
109
+ : Uint8Array.from(bytes);
110
+ const digest = await Crypto.digest(Crypto.CryptoDigestAlgorithm.SHA256, digestInput);
111
+ return bytesToHex(new Uint8Array(digest));
112
+ },
113
+ };
114
+
115
+ export const expoFileAdapter: OpenGeniReactNativeAdapters["files"] = {
116
+ pickDocuments: async () => {
117
+ const result = await DocumentPicker.getDocumentAsync({
118
+ copyToCacheDirectory: true,
119
+ multiple: true,
120
+ });
121
+ if (result.canceled) return [];
122
+ return result.assets.map(
123
+ (asset): NativePickedFile => ({
124
+ id: Crypto.randomUUID(),
125
+ uri: asset.uri,
126
+ name: asset.name,
127
+ contentType: asset.mimeType ?? "application/octet-stream",
128
+ sizeBytes: asset.size ?? null,
129
+ kind: "document",
130
+ }),
131
+ );
132
+ },
133
+ pickImages: async () => {
134
+ const result = await ImagePicker.launchImageLibraryAsync({
135
+ allowsMultipleSelection: true,
136
+ mediaTypes: ["images"],
137
+ quality: 1,
138
+ });
139
+ if (result.canceled) return [];
140
+ return result.assets.map((asset): NativePickedFile => {
141
+ const contentType = asset.mimeType ?? "image/jpeg";
142
+ return {
143
+ id: Crypto.randomUUID(),
144
+ uri: asset.uri,
145
+ name: asset.fileName ?? fallbackFilename(asset.uri, contentType),
146
+ contentType,
147
+ sizeBytes: asset.fileSize ?? null,
148
+ kind: "image",
149
+ previewUri: asset.uri,
150
+ };
151
+ });
152
+ },
153
+ // Expo FileSystem's iOS `bytes()` requests write authority that read-only photo-picker
154
+ // exports do not grant; `base64()` uses read authority, so decode once here.
155
+ readBytes: async (source) => base64ToBytes(await new File(source.uri).base64()),
156
+ };
157
+
158
+ /** Standard Expo adapters; the host supplies persistence. */
159
+ export function createExpoOpenGeniAdapters(input: {
160
+ persistence: NativePersistenceAdapter;
161
+ }): OpenGeniReactNativeAdapters {
162
+ return {
163
+ lifecycle: expoLifecycleAdapter,
164
+ crypto: expoCryptoAdapter,
165
+ files: expoFileAdapter,
166
+ fetch: expoStreamingFetch,
167
+ persistence: input.persistence,
168
+ };
169
+ }
170
+
171
+ // Mono AAC in an MPEG-4 container: small uploads, accepted by every transcription provider.
172
+ const DICTATION_RECORDING = {
173
+ ...RecordingPresets.HIGH_QUALITY,
174
+ numberOfChannels: 1,
175
+ bitRate: 64_000,
176
+ isMeteringEnabled: true,
177
+ };
178
+
179
+ function discardRecordingFile(uri: string | null) {
180
+ if (!uri) return;
181
+ try {
182
+ const file = new File(uri);
183
+ if (file.exists) file.delete();
184
+ } catch {
185
+ // A leftover cache file is harmless; the system clears the cache directory.
186
+ }
187
+ }
188
+
189
+ /** Move a finished recording out of the recorder's scratch path into its own file. */
190
+ async function keepRecordingFile(uri: string): Promise<string> {
191
+ try {
192
+ const directory = new Directory(Paths.document, "opengeni-dictation");
193
+ if (!directory.exists) directory.create({ intermediates: true, idempotent: true });
194
+ const kept = new File(directory, `${Crypto.randomUUID()}.m4a`);
195
+ await new File(uri).copy(kept);
196
+ discardRecordingFile(uri);
197
+ return kept.uri;
198
+ } catch {
199
+ // Keeping the original path is still better than losing the recording.
200
+ return uri;
201
+ }
202
+ }
203
+
204
+ /** Dictation recorder backed by expo-audio. */
205
+ export function useExpoVoiceRecorder(): NativeVoiceRecorder {
206
+ const recorder = useAudioRecorder(DICTATION_RECORDING);
207
+ return useMemo<NativeVoiceRecorder>(
208
+ () => ({
209
+ async requestPermission() {
210
+ const permission = await requestRecordingPermissionsAsync();
211
+ return permission.granted;
212
+ },
213
+ async start() {
214
+ await setAudioModeAsync({ allowsRecording: true, playsInSilentMode: true });
215
+ await recorder.prepareToRecordAsync();
216
+ recorder.record();
217
+ },
218
+ async stop() {
219
+ const durationSeconds = recorder.getStatus().durationMillis / 1000;
220
+ await recorder.stop();
221
+ await setAudioModeAsync({ allowsRecording: false }).catch(() => undefined);
222
+ const uri = recorder.uri;
223
+ if (!uri) return null;
224
+ // Kept until its text is in the draft: a following segment or a retry
225
+ // must never find the recorder has reused or cleared this file.
226
+ return { uri: await keepRecordingFile(uri), mimeType: "audio/mp4", durationSeconds };
227
+ },
228
+ async cancel() {
229
+ if (recorder.isRecording) await recorder.stop();
230
+ await setAudioModeAsync({ allowsRecording: false }).catch(() => undefined);
231
+ discardRecordingFile(recorder.uri);
232
+ },
233
+ discard(recording) {
234
+ discardRecordingFile(recording.uri);
235
+ },
236
+ durationSeconds() {
237
+ return recorder.getStatus().durationMillis / 1000;
238
+ },
239
+ level() {
240
+ // Metering is in decibels (silence is about -160); map the useful -55…0 range.
241
+ const decibels = recorder.getStatus().metering;
242
+ if (typeof decibels !== "number") return null;
243
+ return Math.max(0, Math.min(1, (decibels + 55) / 55));
244
+ },
245
+ }),
246
+ [recorder],
247
+ );
248
+ }
249
+
250
+ /**
251
+ * Send a recording to the deployment's transcription API. The device file is
252
+ * streamed as multipart form data and kept: dictation deletes it only once its
253
+ * text is in the draft. Failures carry the HTTP status (null when offline).
254
+ */
255
+ export async function transcribeExpoRecording(input: {
256
+ client: Pick<OpenGeniClient, "fetchApi">;
257
+ workspaceId: string;
258
+ recording: NativeVoiceRecording;
259
+ }): Promise<string> {
260
+ const form = new FormData();
261
+ // expo-file-system's File is a Blob, which Expo's fetch uploads from disk.
262
+ form.append("audio", new File(input.recording.uri) as unknown as Blob, "dictation.m4a");
263
+ form.append("mimeType", input.recording.mimeType);
264
+ form.append("durationSeconds", input.recording.durationSeconds.toFixed(3));
265
+ try {
266
+ const response = await input.client.fetchApi(
267
+ `/v1/workspaces/${encodeURIComponent(input.workspaceId)}/transcriptions`,
268
+ { method: "POST", body: form },
269
+ );
270
+ const text = await response.text();
271
+ let body: { text?: unknown; error?: { message?: unknown }; message?: unknown } = {};
272
+ try {
273
+ body = JSON.parse(text) as typeof body;
274
+ } catch {
275
+ // A non-JSON body is reported by status below.
276
+ }
277
+ if (!response.ok) {
278
+ const message =
279
+ typeof body.error?.message === "string"
280
+ ? body.error.message
281
+ : typeof body.message === "string"
282
+ ? body.message
283
+ : `Transcription failed (${response.status}).`;
284
+ throw new NativeTranscriptionError(message, response.status);
285
+ }
286
+ return typeof body.text === "string" ? body.text : "";
287
+ } catch (cause) {
288
+ if (cause instanceof NativeTranscriptionError) throw cause;
289
+ // No response at all: offline, timed out or dropped. The recording is kept.
290
+ throw new NativeTranscriptionError(
291
+ cause instanceof Error ? cause.message : "Network request failed",
292
+ null,
293
+ );
294
+ }
295
+ }
package/src/index.ts ADDED
@@ -0,0 +1,98 @@
1
+ export type {
2
+ AsyncKeyValueStorage,
3
+ NativeCryptoAdapter,
4
+ NativeFileAdapter,
5
+ NativeLifecycleAdapter,
6
+ NativeLifecycleState,
7
+ NativePersistenceAdapter,
8
+ NativePickedFile,
9
+ OpenGeniReactNativeAdapters,
10
+ } from "./adapters";
11
+ export { createHydratedPersistenceAdapter, sha256HexToArrayBuffer } from "./adapters";
12
+ export {
13
+ installOpenGeniReactNativeEnvironment,
14
+ OpenGeniReactNativeProvider,
15
+ useOpenGeniReactNativeEnvironment,
16
+ type NativeEnvironmentContextValue,
17
+ type OpenGeniReactNativeProviderProps,
18
+ } from "./environment";
19
+ export {
20
+ NATIVE_ATTACHMENT_PREPARATION_TIMEOUT_MS,
21
+ prepareNativeAttachmentUpload,
22
+ useNativeFileAttachments,
23
+ type NativeAttachment,
24
+ type NativeAttachmentStatus,
25
+ type NativeFileAttachmentsResult,
26
+ } from "./attachments";
27
+ export {
28
+ useOpenGeniNativeSession,
29
+ type OpenGeniNativeSessionClient,
30
+ type OpenGeniNativeSessionController,
31
+ } from "./use-native-session";
32
+ export {
33
+ appendDictation,
34
+ isRetryableTranscriptionError,
35
+ NativeTranscriptionError,
36
+ useNativeVoiceInput,
37
+ type NativeVoiceInput,
38
+ type NativeVoiceInputStatus,
39
+ type NativeVoiceRecorder,
40
+ type NativeVoiceRecording,
41
+ } from "./voice-input";
42
+ export {
43
+ boundedJson,
44
+ formatNativeRelativeTime,
45
+ nativeHumanInputRequestPreview,
46
+ nativeSessionStatusTone,
47
+ timelineAccessibilityLabel,
48
+ validateHumanInputAnswers,
49
+ type NativeSessionStatusTone,
50
+ } from "./view-model";
51
+ export {
52
+ DEFAULT_OPENGENI_NATIVE_LABELS,
53
+ DEFAULT_OPENGENI_NATIVE_THEME,
54
+ type OpenGeniNativeLabels,
55
+ type OpenGeniNativeTheme,
56
+ } from "./presentation";
57
+ export {
58
+ OpenGeniNativeAttachmentStrip,
59
+ OpenGeniNativeSessionView,
60
+ type NativeComposerActionContext,
61
+ type NativeComposerActionRenderer,
62
+ type NativeComposerRenderContext,
63
+ type NativeComposerRenderer,
64
+ type NativeComposerSurfaceContext,
65
+ type NativeComposerSurfaceRenderer,
66
+ type NativeMessageRenderer,
67
+ type NativeToolRenderer,
68
+ type NativeTurnSummaryContext,
69
+ type NativeTurnSummaryRenderer,
70
+ type NativeTurnSummaryResult,
71
+ type OpenGeniNativeSessionViewProps,
72
+ } from "./session-view";
73
+ export {
74
+ createMemoryRealtimeOwnerStorage,
75
+ createNativeRealtimeControllerFactory,
76
+ createNativeRemoteAudio,
77
+ type NativeRealtimeControllerFactoryOptions,
78
+ type NativeWebRtcAdapter,
79
+ } from "./realtime/native-realtime";
80
+ export {
81
+ useNativeSessionRealtime,
82
+ type UseNativeSessionRealtimeOptions,
83
+ } from "./realtime/use-native-session-realtime";
84
+ export {
85
+ nativeRealtimeModelSupported,
86
+ useNativeRealtimeCall,
87
+ type NativeCallAdapter,
88
+ type NativeCallAudioRoute,
89
+ type NativeCallEvent,
90
+ type NativeCallPhase,
91
+ type NativeCallRealtime,
92
+ type NativeRealtimeCall,
93
+ } from "./realtime/native-call";
94
+ export {
95
+ DEFAULT_OPENGENI_NATIVE_CALL_LABELS,
96
+ OpenGeniNativeCallView,
97
+ type OpenGeniNativeCallLabels,
98
+ } from "./realtime/call-view";