@ateam-ai-mobile/ui 0.1.5 → 0.1.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.mts +385 -21
- package/dist/index.d.ts +385 -21
- package/dist/index.js +1270 -175
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +1264 -176
- package/dist/index.mjs.map +1 -1
- package/package.json +3 -3
- package/src/WorkspaceBanner.tsx +71 -0
- package/src/auth/HandoffScreen.tsx +94 -0
- package/src/auth/JoinScreen.tsx +145 -0
- package/src/auth/LoginScreen.tsx +181 -0
- package/src/auth/OpenJoinScreen.tsx +131 -0
- package/src/auth/handoffExchange.ts +49 -0
- package/src/auth/inviteJoin.ts +44 -0
- package/src/auth/login.ts +57 -0
- package/src/auth/openJoin.ts +56 -0
- package/src/chat/MessageBubble.tsx +3 -0
- package/src/index.ts +38 -1
- package/src/plugin/Plugin.tsx +15 -9
- package/src/providers.ts +44 -0
- package/src/voice/VoiceCallSheet.tsx +4 -32
- package/src/voice/VoiceMicIndicator.tsx +99 -0
- package/src/voice/VoiceSessionHost.tsx +77 -0
- package/src/voice/useVoiceCall.ts +40 -231
- package/src/voice/voiceSession.ts +807 -0
package/dist/index.mjs
CHANGED
|
@@ -208,6 +208,13 @@ import { ada as ada4, fontSize as fontSize2 } from "@ateam-ai-mobile/core";
|
|
|
208
208
|
import { useSurfaceStore as useSurfaceStore2, getHaptics } from "@ateam-ai-mobile/plugin-runtime";
|
|
209
209
|
|
|
210
210
|
// src/providers.ts
|
|
211
|
+
var _oauth = null;
|
|
212
|
+
function setOAuthProvider(oauth) {
|
|
213
|
+
_oauth = oauth;
|
|
214
|
+
}
|
|
215
|
+
function getOAuthProvider() {
|
|
216
|
+
return _oauth;
|
|
217
|
+
}
|
|
211
218
|
var _clipboard = null;
|
|
212
219
|
function setClipboardProvider(clipboard) {
|
|
213
220
|
_clipboard = clipboard;
|
|
@@ -254,8 +261,7 @@ import {
|
|
|
254
261
|
usePlugin,
|
|
255
262
|
useNativeBridge,
|
|
256
263
|
usePluginTheme,
|
|
257
|
-
|
|
258
|
-
gatePluginNative
|
|
264
|
+
resolvePluginRuntimeProps
|
|
259
265
|
} from "@ateam-ai-mobile/plugin-runtime";
|
|
260
266
|
|
|
261
267
|
// src/plugin/EmbeddedPluginWebView.tsx
|
|
@@ -749,8 +755,10 @@ function Plugin({ id, manifest, commandBufferRef, props: engineProps }) {
|
|
|
749
755
|
const { plugin, loading, error, bundleMissing } = usePlugin(id, manifest);
|
|
750
756
|
const connectorId = id.startsWith("mcp:") ? id.split(":")[1] : manifest._connectorId;
|
|
751
757
|
const bridge = useNativeBridge(id, connectorId, commandBufferRef);
|
|
752
|
-
const
|
|
753
|
-
|
|
758
|
+
const { device, native, deviceState, voice } = useMemo2(
|
|
759
|
+
() => resolvePluginRuntimeProps({ pluginId: id, manifest, plugin }),
|
|
760
|
+
[id, manifest, plugin]
|
|
761
|
+
);
|
|
754
762
|
const theme = usePluginTheme();
|
|
755
763
|
const user = getUser();
|
|
756
764
|
const context = useMemo2(() => ({
|
|
@@ -773,7 +781,7 @@ function Plugin({ id, manifest, commandBufferRef, props: engineProps }) {
|
|
|
773
781
|
return /* @__PURE__ */ jsx3(View3, { style: styles3.center, children: /* @__PURE__ */ jsx3(Text3, { style: styles3.errorMessage, children: error || "Plugin not found" }) });
|
|
774
782
|
}
|
|
775
783
|
const { Component } = plugin;
|
|
776
|
-
return /* @__PURE__ */ jsx3(PluginErrorBoundary, { pluginId: id, children: /* @__PURE__ */ jsx3(Component, { bridge, native, theme, context, props: engineProps }) });
|
|
784
|
+
return /* @__PURE__ */ jsx3(PluginErrorBoundary, { pluginId: id, children: /* @__PURE__ */ jsx3(Component, { bridge, device, native, deviceState, voice, theme, context, props: engineProps }) });
|
|
777
785
|
}
|
|
778
786
|
var styles3 = StyleSheet3.create({
|
|
779
787
|
center: {
|
|
@@ -1270,7 +1278,10 @@ function MessageBubble({ message }) {
|
|
|
1270
1278
|
message.toolCalls && message.toolCalls.length > 0 && /* @__PURE__ */ jsx9(ToolCallsBadge, { calls: message.toolCalls }),
|
|
1271
1279
|
message.jobProgress && /* @__PURE__ */ jsx9(JobProgressBar, { progress: message.jobProgress, standalone: isProgressOnly }),
|
|
1272
1280
|
!isUser && message.actions && message.actions.length > 0 && /* @__PURE__ */ jsx9(UIActionsRow, { actions: message.actions }),
|
|
1273
|
-
pq &&
|
|
1281
|
+
pq && // Per-chain: this bubble's OWN chain has a pending question. Parallel
|
|
1282
|
+
// pending chains each render their own chips (jobProgress is per
|
|
1283
|
+
// bubble); answered questions have no jobProgress → no chips.
|
|
1284
|
+
/* @__PURE__ */ jsx9(QuestionChips, { inputType: pq.inputType, options: pq.options }),
|
|
1274
1285
|
!isProgressOnly && /* @__PURE__ */ jsx9(Text7, { style: [styles7.timeText, isUser && styles7.timeTextUser], children: formatTime(message.timestamp) })
|
|
1275
1286
|
]
|
|
1276
1287
|
}
|
|
@@ -2122,24 +2133,541 @@ var styles13 = StyleSheet13.create({
|
|
|
2122
2133
|
}
|
|
2123
2134
|
});
|
|
2124
2135
|
|
|
2136
|
+
// src/WorkspaceBanner.tsx
|
|
2137
|
+
import { View as View15, Text as Text11, StyleSheet as StyleSheet14 } from "react-native";
|
|
2138
|
+
import { ada as ada11, fontSize as fontSize7, spacing as spacing2, useWorkspaceStore } from "@ateam-ai-mobile/core";
|
|
2139
|
+
import { jsx as jsx16, jsxs as jsxs12 } from "react/jsx-runtime";
|
|
2140
|
+
var WARM_AMBER2 = "#D49A4E";
|
|
2141
|
+
var WARM_AMBER_SOFT2 = "rgba(212, 154, 78, 0.16)";
|
|
2142
|
+
var WARM_AMBER_BORDER = "rgba(212, 154, 78, 0.4)";
|
|
2143
|
+
function WorkspaceBanner({ topInset = 0 }) {
|
|
2144
|
+
const snapshot = useWorkspaceStore((s) => s.snapshot);
|
|
2145
|
+
if (!snapshot) return null;
|
|
2146
|
+
if (snapshot.state === "active" || snapshot.state === "unknown") return null;
|
|
2147
|
+
if (!snapshot.message) return null;
|
|
2148
|
+
return /* @__PURE__ */ jsxs12(
|
|
2149
|
+
View15,
|
|
2150
|
+
{
|
|
2151
|
+
style: [styles14.container, { paddingTop: spacing2.sm + topInset }],
|
|
2152
|
+
accessibilityRole: "alert",
|
|
2153
|
+
accessibilityLabel: snapshot.message,
|
|
2154
|
+
children: [
|
|
2155
|
+
/* @__PURE__ */ jsx16(View15, { style: styles14.dot }),
|
|
2156
|
+
/* @__PURE__ */ jsx16(Text11, { style: styles14.message, children: snapshot.message })
|
|
2157
|
+
]
|
|
2158
|
+
}
|
|
2159
|
+
);
|
|
2160
|
+
}
|
|
2161
|
+
var styles14 = StyleSheet14.create({
|
|
2162
|
+
container: {
|
|
2163
|
+
flexDirection: "row",
|
|
2164
|
+
alignItems: "flex-start",
|
|
2165
|
+
gap: spacing2.sm,
|
|
2166
|
+
paddingHorizontal: spacing2.md,
|
|
2167
|
+
paddingBottom: spacing2.sm,
|
|
2168
|
+
backgroundColor: WARM_AMBER_SOFT2,
|
|
2169
|
+
borderBottomWidth: StyleSheet14.hairlineWidth,
|
|
2170
|
+
borderBottomColor: WARM_AMBER_BORDER
|
|
2171
|
+
},
|
|
2172
|
+
dot: {
|
|
2173
|
+
width: 8,
|
|
2174
|
+
height: 8,
|
|
2175
|
+
borderRadius: 4,
|
|
2176
|
+
marginTop: 5,
|
|
2177
|
+
backgroundColor: WARM_AMBER2
|
|
2178
|
+
},
|
|
2179
|
+
message: {
|
|
2180
|
+
flex: 1,
|
|
2181
|
+
color: ada11.ink,
|
|
2182
|
+
fontSize: fontSize7.sm,
|
|
2183
|
+
lineHeight: fontSize7.sm * 1.35
|
|
2184
|
+
}
|
|
2185
|
+
});
|
|
2186
|
+
|
|
2187
|
+
// src/auth/HandoffScreen.tsx
|
|
2188
|
+
import { useEffect as useEffect8, useRef as useRef5, useState as useState9 } from "react";
|
|
2189
|
+
import { View as View16, Text as Text12, Pressable as Pressable9, StyleSheet as StyleSheet15, Platform as Platform10, ActivityIndicator as ActivityIndicator6 } from "react-native";
|
|
2190
|
+
import { exchangeHandoff, ada as ada12 } from "@ateam-ai-mobile/core";
|
|
2191
|
+
import { AdaOrb as AdaOrb3 } from "@ateam-ai-mobile/plugin-runtime";
|
|
2192
|
+
|
|
2193
|
+
// src/auth/handoffExchange.ts
|
|
2194
|
+
async function runHandoffExchange(code, deps) {
|
|
2195
|
+
const res = await deps.exchange(code);
|
|
2196
|
+
if (res.tenant) await deps.onAdoptTenant(res.tenant);
|
|
2197
|
+
await deps.onSignedIn(res.jwt);
|
|
2198
|
+
deps.onDone();
|
|
2199
|
+
}
|
|
2200
|
+
function handoffErrorMessage(msg) {
|
|
2201
|
+
const m = (msg || "").toLowerCase();
|
|
2202
|
+
if (m.includes("429") || m.includes("rate") || m.includes("too many")) {
|
|
2203
|
+
return "Too many attempts \u2014 wait a moment, then get a new sign-in link on the web.";
|
|
2204
|
+
}
|
|
2205
|
+
return "This sign-in link is no longer valid. Get a new one on the web and try again.";
|
|
2206
|
+
}
|
|
2207
|
+
|
|
2208
|
+
// src/auth/HandoffScreen.tsx
|
|
2209
|
+
import { Fragment as Fragment3, jsx as jsx17, jsxs as jsxs13 } from "react/jsx-runtime";
|
|
2210
|
+
var fontFam7 = Platform10.OS === "ios" ? "SF Pro Rounded" : void 0;
|
|
2211
|
+
function HandoffScreen({ code, onAdoptTenant, onSignedIn, onDone, onCancel }) {
|
|
2212
|
+
const [err, setErr] = useState9(null);
|
|
2213
|
+
const startedForRef = useRef5(null);
|
|
2214
|
+
useEffect8(() => {
|
|
2215
|
+
if (!code || startedForRef.current === code) return;
|
|
2216
|
+
startedForRef.current = code;
|
|
2217
|
+
runHandoffExchange(code, { exchange: exchangeHandoff, onAdoptTenant, onSignedIn, onDone }).catch((e) => setErr(handoffErrorMessage(e?.message)));
|
|
2218
|
+
}, [code, onAdoptTenant, onSignedIn, onDone]);
|
|
2219
|
+
return /* @__PURE__ */ jsx17(View16, { style: styles15.container, children: /* @__PURE__ */ jsxs13(View16, { style: styles15.center, children: [
|
|
2220
|
+
/* @__PURE__ */ jsx17(AdaOrb3, { size: 72, state: err ? "idle" : "thinking" }),
|
|
2221
|
+
!err ? /* @__PURE__ */ jsxs13(Fragment3, { children: [
|
|
2222
|
+
/* @__PURE__ */ jsx17(Text12, { style: styles15.title, children: "Signing you in\u2026" }),
|
|
2223
|
+
/* @__PURE__ */ jsx17(ActivityIndicator6, { color: ada12.ink, style: { marginTop: 6 } })
|
|
2224
|
+
] }) : /* @__PURE__ */ jsxs13(Fragment3, { children: [
|
|
2225
|
+
/* @__PURE__ */ jsx17(Text12, { style: styles15.title, children: "Couldn\u2019t finish sign-in" }),
|
|
2226
|
+
/* @__PURE__ */ jsx17(Text12, { style: styles15.sub, children: err }),
|
|
2227
|
+
/* @__PURE__ */ jsx17(Pressable9, { onPress: onCancel, hitSlop: 8, style: styles15.back, children: /* @__PURE__ */ jsx17(Text12, { style: styles15.backText, children: "Back to sign in" }) })
|
|
2228
|
+
] })
|
|
2229
|
+
] }) });
|
|
2230
|
+
}
|
|
2231
|
+
var styles15 = StyleSheet15.create({
|
|
2232
|
+
container: { flex: 1, backgroundColor: ada12.paper },
|
|
2233
|
+
center: { flex: 1, alignItems: "center", justifyContent: "center", gap: 14, paddingHorizontal: 32 },
|
|
2234
|
+
title: { fontSize: 20, fontWeight: "600", color: ada12.ink, fontFamily: fontFam7, marginTop: 8 },
|
|
2235
|
+
sub: { fontSize: 14, lineHeight: 20, color: ada12.inkSoft, textAlign: "center", fontFamily: fontFam7 },
|
|
2236
|
+
back: { marginTop: 10, paddingVertical: 10 },
|
|
2237
|
+
backText: { fontSize: 15, fontWeight: "600", color: ada12.orangeDeep, fontFamily: fontFam7 }
|
|
2238
|
+
});
|
|
2239
|
+
|
|
2240
|
+
// src/auth/OpenJoinScreen.tsx
|
|
2241
|
+
import { useEffect as useEffect9, useState as useState10 } from "react";
|
|
2242
|
+
import { View as View17, Text as Text13, Pressable as Pressable10, StyleSheet as StyleSheet16, ActivityIndicator as ActivityIndicator7, Platform as Platform11, TextInput as TextInput2 } from "react-native";
|
|
2243
|
+
import { openJoinWithGoogle, openJoinWithApple, ada as ada13, spacing as spacing3 } from "@ateam-ai-mobile/core";
|
|
2244
|
+
import { AdaOrb as AdaOrb4 } from "@ateam-ai-mobile/plugin-runtime";
|
|
2245
|
+
|
|
2246
|
+
// src/auth/openJoin.ts
|
|
2247
|
+
async function completeOpenJoin(slug, idToken, deps) {
|
|
2248
|
+
if (!idToken) return false;
|
|
2249
|
+
const consumer = await deps.join(slug, idToken);
|
|
2250
|
+
await deps.onAdoptTenant(slug);
|
|
2251
|
+
await deps.onSignedIn(consumer.jwt);
|
|
2252
|
+
return true;
|
|
2253
|
+
}
|
|
2254
|
+
function openJoinErrorMessage(msg) {
|
|
2255
|
+
const m = (msg || "").toLowerCase();
|
|
2256
|
+
if (m.includes("invite-only") || m.includes("invite only")) {
|
|
2257
|
+
return "This team is invite-only \u2014 ask an admin for an invite link.";
|
|
2258
|
+
}
|
|
2259
|
+
if (m.includes("not found") || m.includes("unknown tenant")) {
|
|
2260
|
+
return "No team with that handle.";
|
|
2261
|
+
}
|
|
2262
|
+
return msg || "Could not join";
|
|
2263
|
+
}
|
|
2264
|
+
|
|
2265
|
+
// src/auth/OpenJoinScreen.tsx
|
|
2266
|
+
import { jsx as jsx18, jsxs as jsxs14 } from "react/jsx-runtime";
|
|
2267
|
+
var fontFam8 = Platform11.OS === "ios" ? "SF Pro Rounded" : void 0;
|
|
2268
|
+
function OpenJoinScreen({ onAdoptTenant, onSignedIn, onBack }) {
|
|
2269
|
+
const [team, setTeam] = useState10("");
|
|
2270
|
+
const [busy, setBusy] = useState10(false);
|
|
2271
|
+
const [err, setErr] = useState10(null);
|
|
2272
|
+
const [appleAvailable, setAppleAvailable] = useState10(false);
|
|
2273
|
+
useEffect9(() => {
|
|
2274
|
+
getOAuthProvider()?.isAppleAvailable().then(setAppleAvailable).catch(() => setAppleAvailable(false));
|
|
2275
|
+
}, []);
|
|
2276
|
+
const slug = team.trim().toLowerCase();
|
|
2277
|
+
const canSubmit = slug.length > 0 && !busy;
|
|
2278
|
+
async function run(acquire, join) {
|
|
2279
|
+
if (!slug) return;
|
|
2280
|
+
const oauth = getOAuthProvider();
|
|
2281
|
+
if (!oauth) {
|
|
2282
|
+
setErr("Sign-in is unavailable on this device.");
|
|
2283
|
+
return;
|
|
2284
|
+
}
|
|
2285
|
+
setErr(null);
|
|
2286
|
+
setBusy(true);
|
|
2287
|
+
try {
|
|
2288
|
+
const idToken = await acquire(oauth);
|
|
2289
|
+
await completeOpenJoin(slug, idToken, { join, onAdoptTenant, onSignedIn });
|
|
2290
|
+
} catch (e) {
|
|
2291
|
+
setErr(openJoinErrorMessage(e?.message));
|
|
2292
|
+
} finally {
|
|
2293
|
+
setBusy(false);
|
|
2294
|
+
}
|
|
2295
|
+
}
|
|
2296
|
+
const handleGoogle = () => run((oauth) => oauth.signInWithGoogle(), openJoinWithGoogle);
|
|
2297
|
+
const handleApple = () => run((oauth) => oauth.signInWithApple(), openJoinWithApple);
|
|
2298
|
+
return /* @__PURE__ */ jsxs14(View17, { style: styles16.container, children: [
|
|
2299
|
+
/* @__PURE__ */ jsxs14(View17, { style: styles16.hero, children: [
|
|
2300
|
+
/* @__PURE__ */ jsx18(AdaOrb4, { size: 110 }),
|
|
2301
|
+
/* @__PURE__ */ jsx18(Text13, { style: styles16.wordmark, children: "Join a team" }),
|
|
2302
|
+
/* @__PURE__ */ jsx18(Text13, { style: styles16.tagline, children: "Enter the team's handle. If it's open to join, you're in." })
|
|
2303
|
+
] }),
|
|
2304
|
+
/* @__PURE__ */ jsxs14(View17, { style: styles16.form, children: [
|
|
2305
|
+
/* @__PURE__ */ jsx18(
|
|
2306
|
+
TextInput2,
|
|
2307
|
+
{
|
|
2308
|
+
style: styles16.input,
|
|
2309
|
+
value: team,
|
|
2310
|
+
onChangeText: setTeam,
|
|
2311
|
+
placeholder: "team handle (e.g. acme)",
|
|
2312
|
+
placeholderTextColor: ada13.inkMuted,
|
|
2313
|
+
autoCapitalize: "none",
|
|
2314
|
+
autoCorrect: false,
|
|
2315
|
+
returnKeyType: "done"
|
|
2316
|
+
}
|
|
2317
|
+
),
|
|
2318
|
+
/* @__PURE__ */ jsxs14(
|
|
2319
|
+
Pressable10,
|
|
2320
|
+
{
|
|
2321
|
+
style: [styles16.googleBtn, !canSubmit && styles16.btnDisabled],
|
|
2322
|
+
onPress: handleGoogle,
|
|
2323
|
+
disabled: !canSubmit,
|
|
2324
|
+
children: [
|
|
2325
|
+
/* @__PURE__ */ jsx18(Text13, { style: styles16.googleGlyph, children: "G" }),
|
|
2326
|
+
/* @__PURE__ */ jsx18(Text13, { style: styles16.googleText, children: "Continue with Google" })
|
|
2327
|
+
]
|
|
2328
|
+
}
|
|
2329
|
+
),
|
|
2330
|
+
appleAvailable && /* @__PURE__ */ jsx18(View17, { style: { opacity: canSubmit ? 1 : 0.5 }, pointerEvents: canSubmit ? "auto" : "none", children: getOAuthProvider()?.renderAppleButton({ onPress: handleApple, disabled: !canSubmit, style: styles16.appleBtn }) }),
|
|
2331
|
+
busy && /* @__PURE__ */ jsx18(ActivityIndicator7, { color: ada13.ink, style: { marginTop: 10 } }),
|
|
2332
|
+
!!err && /* @__PURE__ */ jsx18(Text13, { style: styles16.err, children: err }),
|
|
2333
|
+
/* @__PURE__ */ jsx18(Pressable10, { onPress: onBack, hitSlop: 8, style: styles16.back, children: /* @__PURE__ */ jsx18(Text13, { style: styles16.backText, children: "Back to sign in" }) })
|
|
2334
|
+
] })
|
|
2335
|
+
] });
|
|
2336
|
+
}
|
|
2337
|
+
var styles16 = StyleSheet16.create({
|
|
2338
|
+
container: { flex: 1, backgroundColor: ada13.paper, justifyContent: "space-between" },
|
|
2339
|
+
hero: { alignItems: "center", marginTop: spacing3.xxl },
|
|
2340
|
+
wordmark: { fontSize: 26, fontWeight: "800", color: ada13.ink, marginTop: 14, fontFamily: fontFam8 },
|
|
2341
|
+
tagline: { fontSize: 15, color: ada13.inkSoft, marginTop: 8, textAlign: "center", paddingHorizontal: 28, fontFamily: fontFam8 },
|
|
2342
|
+
form: { paddingHorizontal: 24, paddingBottom: 40, gap: 12 },
|
|
2343
|
+
input: {
|
|
2344
|
+
backgroundColor: ada13.paperSoft,
|
|
2345
|
+
borderWidth: 1,
|
|
2346
|
+
borderColor: ada13.border,
|
|
2347
|
+
borderRadius: 14,
|
|
2348
|
+
paddingHorizontal: 16,
|
|
2349
|
+
paddingVertical: 14,
|
|
2350
|
+
fontSize: 16,
|
|
2351
|
+
color: ada13.ink
|
|
2352
|
+
},
|
|
2353
|
+
googleBtn: {
|
|
2354
|
+
flexDirection: "row",
|
|
2355
|
+
alignItems: "center",
|
|
2356
|
+
justifyContent: "center",
|
|
2357
|
+
gap: 10,
|
|
2358
|
+
backgroundColor: ada13.ink,
|
|
2359
|
+
paddingVertical: 15,
|
|
2360
|
+
borderRadius: 999
|
|
2361
|
+
},
|
|
2362
|
+
btnDisabled: { opacity: 0.5 },
|
|
2363
|
+
googleGlyph: { color: "#fff", fontSize: 18, fontWeight: "800" },
|
|
2364
|
+
googleText: { color: "#fff", fontSize: 16, fontWeight: "600", fontFamily: fontFam8 },
|
|
2365
|
+
appleBtn: { height: 52, width: "100%" },
|
|
2366
|
+
err: { color: "#C15545", fontSize: 13, textAlign: "center", paddingHorizontal: 16, marginTop: 4 },
|
|
2367
|
+
back: { alignItems: "center", paddingVertical: 10 },
|
|
2368
|
+
backText: { color: ada13.inkMuted, fontSize: 15, fontWeight: "500" }
|
|
2369
|
+
});
|
|
2370
|
+
|
|
2371
|
+
// src/auth/JoinScreen.tsx
|
|
2372
|
+
import { useEffect as useEffect10, useMemo as useMemo5, useState as useState11 } from "react";
|
|
2373
|
+
import { View as View18, Text as Text14, Pressable as Pressable11, StyleSheet as StyleSheet17, ActivityIndicator as ActivityIndicator8, Platform as Platform12 } from "react-native";
|
|
2374
|
+
import {
|
|
2375
|
+
previewInvite,
|
|
2376
|
+
joinWithGoogle,
|
|
2377
|
+
joinWithApple,
|
|
2378
|
+
ada as ada14,
|
|
2379
|
+
spacing as spacing4
|
|
2380
|
+
} from "@ateam-ai-mobile/core";
|
|
2381
|
+
import { AdaOrb as AdaOrb5 } from "@ateam-ai-mobile/plugin-runtime";
|
|
2382
|
+
|
|
2383
|
+
// src/auth/inviteJoin.ts
|
|
2384
|
+
async function completeInviteJoin(code, idToken, deps) {
|
|
2385
|
+
if (!idToken) return false;
|
|
2386
|
+
const consumer = await deps.join(code, idToken);
|
|
2387
|
+
deps.onDone();
|
|
2388
|
+
await deps.onSignedIn(consumer.jwt);
|
|
2389
|
+
return true;
|
|
2390
|
+
}
|
|
2391
|
+
function inviteHeadline(preview) {
|
|
2392
|
+
if (!preview) return "";
|
|
2393
|
+
return preview.role ? `Join ${preview.org} as ${preview.role}?` : `Join ${preview.org}?`;
|
|
2394
|
+
}
|
|
2395
|
+
|
|
2396
|
+
// src/auth/JoinScreen.tsx
|
|
2397
|
+
import { Fragment as Fragment4, jsx as jsx19, jsxs as jsxs15 } from "react/jsx-runtime";
|
|
2398
|
+
var fontFam9 = Platform12.OS === "ios" ? "SF Pro Rounded" : void 0;
|
|
2399
|
+
function JoinScreen({ code, onSignedIn, onDone, onCancel }) {
|
|
2400
|
+
const [preview, setPreview] = useState11(null);
|
|
2401
|
+
const [loadErr, setLoadErr] = useState11(null);
|
|
2402
|
+
const [busy, setBusy] = useState11(false);
|
|
2403
|
+
const [err, setErr] = useState11(null);
|
|
2404
|
+
const [appleAvailable, setAppleAvailable] = useState11(false);
|
|
2405
|
+
useEffect10(() => {
|
|
2406
|
+
if (!code) return;
|
|
2407
|
+
setPreview(null);
|
|
2408
|
+
setLoadErr(null);
|
|
2409
|
+
previewInvite(code).then(setPreview).catch((e) => setLoadErr(e?.message || "Could not load invite"));
|
|
2410
|
+
}, [code]);
|
|
2411
|
+
useEffect10(() => {
|
|
2412
|
+
getOAuthProvider()?.isAppleAvailable().then(setAppleAvailable).catch(() => setAppleAvailable(false));
|
|
2413
|
+
}, []);
|
|
2414
|
+
const orgLine = useMemo5(() => inviteHeadline(preview), [preview]);
|
|
2415
|
+
async function run(acquire, join) {
|
|
2416
|
+
if (!code) return;
|
|
2417
|
+
const oauth = getOAuthProvider();
|
|
2418
|
+
if (!oauth) {
|
|
2419
|
+
setErr("Sign-in is unavailable on this device.");
|
|
2420
|
+
return;
|
|
2421
|
+
}
|
|
2422
|
+
setErr(null);
|
|
2423
|
+
setBusy(true);
|
|
2424
|
+
try {
|
|
2425
|
+
const idToken = await acquire(oauth);
|
|
2426
|
+
await completeInviteJoin(code, idToken, { join, onDone, onSignedIn });
|
|
2427
|
+
} catch (e) {
|
|
2428
|
+
setErr(e?.message || "Sign-in failed");
|
|
2429
|
+
} finally {
|
|
2430
|
+
setBusy(false);
|
|
2431
|
+
}
|
|
2432
|
+
}
|
|
2433
|
+
const handleGoogle = () => run((oauth) => oauth.signInWithGoogle(), joinWithGoogle);
|
|
2434
|
+
const handleApple = () => run((oauth) => oauth.signInWithApple(), joinWithApple);
|
|
2435
|
+
return /* @__PURE__ */ jsxs15(View18, { style: styles17.container, children: [
|
|
2436
|
+
/* @__PURE__ */ jsxs15(View18, { style: styles17.hero, children: [
|
|
2437
|
+
/* @__PURE__ */ jsx19(AdaOrb5, { size: 120 }),
|
|
2438
|
+
/* @__PURE__ */ jsx19(Text14, { style: styles17.wordmark, children: "ada" }),
|
|
2439
|
+
loadErr ? /* @__PURE__ */ jsx19(Text14, { style: styles17.err, children: loadErr }) : preview ? /* @__PURE__ */ jsx19(Text14, { style: styles17.tagline, children: orgLine }) : /* @__PURE__ */ jsx19(ActivityIndicator8, { color: ada14.ink, style: { marginTop: 12 } })
|
|
2440
|
+
] }),
|
|
2441
|
+
/* @__PURE__ */ jsxs15(View18, { style: styles17.buttons, children: [
|
|
2442
|
+
preview && !loadErr && /* @__PURE__ */ jsxs15(Fragment4, { children: [
|
|
2443
|
+
/* @__PURE__ */ jsxs15(
|
|
2444
|
+
Pressable11,
|
|
2445
|
+
{
|
|
2446
|
+
style: [styles17.googleBtn, busy && styles17.btnDisabled],
|
|
2447
|
+
onPress: handleGoogle,
|
|
2448
|
+
disabled: busy,
|
|
2449
|
+
children: [
|
|
2450
|
+
/* @__PURE__ */ jsx19(Text14, { style: styles17.googleGlyph, children: "G" }),
|
|
2451
|
+
/* @__PURE__ */ jsx19(Text14, { style: styles17.googleText, children: "Continue with Google" })
|
|
2452
|
+
]
|
|
2453
|
+
}
|
|
2454
|
+
),
|
|
2455
|
+
appleAvailable && getOAuthProvider()?.renderAppleButton({ onPress: handleApple, disabled: busy, style: styles17.appleBtn })
|
|
2456
|
+
] }),
|
|
2457
|
+
loadErr && /* @__PURE__ */ jsx19(Pressable11, { style: styles17.dismiss, onPress: onCancel, hitSlop: 8, children: /* @__PURE__ */ jsx19(Text14, { style: styles17.dismissText, children: "Back to sign in" }) }),
|
|
2458
|
+
!!err && /* @__PURE__ */ jsx19(Text14, { style: styles17.err, children: err }),
|
|
2459
|
+
busy && /* @__PURE__ */ jsx19(ActivityIndicator8, { color: ada14.ink, style: { marginTop: 12 } }),
|
|
2460
|
+
/* @__PURE__ */ jsx19(Text14, { style: styles17.disclaimer, children: "By continuing you agree to Ada's Terms and Privacy Policy." })
|
|
2461
|
+
] })
|
|
2462
|
+
] });
|
|
2463
|
+
}
|
|
2464
|
+
var styles17 = StyleSheet17.create({
|
|
2465
|
+
container: { flex: 1, backgroundColor: ada14.paper, justifyContent: "space-between" },
|
|
2466
|
+
hero: { alignItems: "center", marginTop: spacing4.xxl * 1.4 },
|
|
2467
|
+
wordmark: { fontSize: 34, fontWeight: "800", color: ada14.ink, letterSpacing: 4, marginTop: 12, fontFamily: fontFam9 },
|
|
2468
|
+
tagline: { fontSize: 17, color: ada14.inkSoft, marginTop: 10, textAlign: "center", paddingHorizontal: 24, fontFamily: fontFam9 },
|
|
2469
|
+
buttons: { paddingHorizontal: 24, paddingBottom: 40, gap: 12 },
|
|
2470
|
+
googleBtn: {
|
|
2471
|
+
flexDirection: "row",
|
|
2472
|
+
alignItems: "center",
|
|
2473
|
+
justifyContent: "center",
|
|
2474
|
+
gap: 10,
|
|
2475
|
+
backgroundColor: ada14.ink,
|
|
2476
|
+
paddingVertical: 15,
|
|
2477
|
+
borderRadius: 999
|
|
2478
|
+
},
|
|
2479
|
+
btnDisabled: { opacity: 0.5 },
|
|
2480
|
+
googleGlyph: { color: "#fff", fontSize: 18, fontWeight: "800" },
|
|
2481
|
+
googleText: { color: "#fff", fontSize: 16, fontWeight: "600", fontFamily: fontFam9 },
|
|
2482
|
+
appleBtn: { height: 52, width: "100%" },
|
|
2483
|
+
dismiss: { alignItems: "center", paddingVertical: 10 },
|
|
2484
|
+
dismissText: { color: ada14.inkMuted, fontSize: 15, fontWeight: "500" },
|
|
2485
|
+
err: { color: "#C15545", fontSize: 13, textAlign: "center", paddingHorizontal: 16, marginTop: 8 },
|
|
2486
|
+
disclaimer: { fontSize: 12, color: ada14.inkMuted, textAlign: "center", marginTop: 8 }
|
|
2487
|
+
});
|
|
2488
|
+
|
|
2489
|
+
// src/auth/LoginScreen.tsx
|
|
2490
|
+
import { useEffect as useEffect11, useMemo as useMemo6, useState as useState12 } from "react";
|
|
2491
|
+
import { View as View19, Text as Text15, Pressable as Pressable12, StyleSheet as StyleSheet18, Platform as Platform13, ActivityIndicator as ActivityIndicator9 } from "react-native";
|
|
2492
|
+
import { fetchConsumerConfig, signInWithGoogle, signInWithApple, ada as ada15 } from "@ateam-ai-mobile/core";
|
|
2493
|
+
import { AdaOrb as AdaOrb6 } from "@ateam-ai-mobile/plugin-runtime";
|
|
2494
|
+
|
|
2495
|
+
// src/auth/login.ts
|
|
2496
|
+
function googleClientIds(config) {
|
|
2497
|
+
const g = config.providers.google || {};
|
|
2498
|
+
return {
|
|
2499
|
+
webClientId: g.web_client_id || g.client_id || "",
|
|
2500
|
+
iosClientId: g.ios_client_id || ""
|
|
2501
|
+
};
|
|
2502
|
+
}
|
|
2503
|
+
function shouldRecoverToDefaultTenant(errorMsg, currentTenant, defaultTenant) {
|
|
2504
|
+
return /capability not enabled/i.test(errorMsg || "") && currentTenant !== defaultTenant;
|
|
2505
|
+
}
|
|
2506
|
+
async function completeProviderSignIn(idToken, deps) {
|
|
2507
|
+
if (!idToken) return false;
|
|
2508
|
+
const consumer = await deps.exchange(idToken);
|
|
2509
|
+
await deps.onSignedIn(consumer.jwt);
|
|
2510
|
+
return true;
|
|
2511
|
+
}
|
|
2512
|
+
|
|
2513
|
+
// src/auth/LoginScreen.tsx
|
|
2514
|
+
import { Fragment as Fragment5, jsx as jsx20, jsxs as jsxs16 } from "react/jsx-runtime";
|
|
2515
|
+
var fontFam10 = Platform13.OS === "ios" ? "SF Pro Rounded" : void 0;
|
|
2516
|
+
function LoginScreen({ activeTenant, defaultTenant, onConsumerDisabled, onSignedIn, onJoinTeam }) {
|
|
2517
|
+
const [config, setConfig] = useState12(null);
|
|
2518
|
+
const [error, setError] = useState12(null);
|
|
2519
|
+
useEffect11(() => {
|
|
2520
|
+
setConfig(null);
|
|
2521
|
+
setError(null);
|
|
2522
|
+
fetchConsumerConfig().then(setConfig).catch(async (err) => {
|
|
2523
|
+
const msg = err?.message || "Failed to load sign-in config";
|
|
2524
|
+
if (shouldRecoverToDefaultTenant(msg, activeTenant, defaultTenant)) {
|
|
2525
|
+
await onConsumerDisabled();
|
|
2526
|
+
return;
|
|
2527
|
+
}
|
|
2528
|
+
setError(msg);
|
|
2529
|
+
});
|
|
2530
|
+
}, [activeTenant, defaultTenant, onConsumerDisabled]);
|
|
2531
|
+
return /* @__PURE__ */ jsxs16(View19, { style: styles18.container, children: [
|
|
2532
|
+
/* @__PURE__ */ jsxs16(View19, { style: styles18.hero, children: [
|
|
2533
|
+
/* @__PURE__ */ jsx20(AdaOrb6, { size: 140 }),
|
|
2534
|
+
/* @__PURE__ */ jsx20(Text15, { style: styles18.wordmark, children: "ada" }),
|
|
2535
|
+
/* @__PURE__ */ jsx20(Text15, { style: styles18.tagline, children: "Your private personal AI-Team" })
|
|
2536
|
+
] }),
|
|
2537
|
+
/* @__PURE__ */ jsxs16(View19, { style: styles18.buttonSection, children: [
|
|
2538
|
+
!config && !error && /* @__PURE__ */ jsx20(ActivityIndicator9, { color: ada15.ink }),
|
|
2539
|
+
!!error && /* @__PURE__ */ jsx20(Text15, { style: styles18.errorText, children: error }),
|
|
2540
|
+
!!config && /* @__PURE__ */ jsx20(SignInButtons, { config, onSignedIn }),
|
|
2541
|
+
onJoinTeam && /* @__PURE__ */ jsx20(Pressable12, { onPress: onJoinTeam, hitSlop: 8, style: { paddingVertical: 8 }, children: /* @__PURE__ */ jsx20(Text15, { style: styles18.joinLink, children: "Join a team by name" }) }),
|
|
2542
|
+
/* @__PURE__ */ jsx20(Text15, { style: styles18.disclaimer, children: "By continuing you agree to Ada's Terms and Privacy Policy." })
|
|
2543
|
+
] })
|
|
2544
|
+
] });
|
|
2545
|
+
}
|
|
2546
|
+
function SignInButtons({ config, onSignedIn }) {
|
|
2547
|
+
const [busy, setBusy] = useState12(false);
|
|
2548
|
+
const [error, setError] = useState12(null);
|
|
2549
|
+
const [appleAvailable, setAppleAvailable] = useState12(false);
|
|
2550
|
+
const { webClientId, iosClientId } = useMemo6(() => googleClientIds(config), [config]);
|
|
2551
|
+
useEffect11(() => {
|
|
2552
|
+
getOAuthProvider()?.isAppleAvailable().then(setAppleAvailable).catch(() => setAppleAvailable(false));
|
|
2553
|
+
}, []);
|
|
2554
|
+
const handleGoogleLogin = async () => {
|
|
2555
|
+
setError(null);
|
|
2556
|
+
if (!webClientId) {
|
|
2557
|
+
setError("Google sign-in is not configured for this tenant");
|
|
2558
|
+
return;
|
|
2559
|
+
}
|
|
2560
|
+
const oauth = getOAuthProvider();
|
|
2561
|
+
if (!oauth) {
|
|
2562
|
+
setError("Sign-in is unavailable on this device.");
|
|
2563
|
+
return;
|
|
2564
|
+
}
|
|
2565
|
+
setBusy(true);
|
|
2566
|
+
try {
|
|
2567
|
+
const idToken = await oauth.signInWithGoogle({ webClientId, iosClientId });
|
|
2568
|
+
await completeProviderSignIn(idToken, { exchange: signInWithGoogle, onSignedIn });
|
|
2569
|
+
} catch (err) {
|
|
2570
|
+
setError(err?.message || "Sign-in failed");
|
|
2571
|
+
} finally {
|
|
2572
|
+
setBusy(false);
|
|
2573
|
+
}
|
|
2574
|
+
};
|
|
2575
|
+
const handleAppleLogin = async () => {
|
|
2576
|
+
setError(null);
|
|
2577
|
+
const oauth = getOAuthProvider();
|
|
2578
|
+
if (!oauth) {
|
|
2579
|
+
setError("Sign-in is unavailable on this device.");
|
|
2580
|
+
return;
|
|
2581
|
+
}
|
|
2582
|
+
setBusy(true);
|
|
2583
|
+
try {
|
|
2584
|
+
const idToken = await oauth.signInWithApple();
|
|
2585
|
+
await completeProviderSignIn(idToken, { exchange: signInWithApple, onSignedIn });
|
|
2586
|
+
} catch (err) {
|
|
2587
|
+
setError(err?.message || "Apple sign-in failed");
|
|
2588
|
+
} finally {
|
|
2589
|
+
setBusy(false);
|
|
2590
|
+
}
|
|
2591
|
+
};
|
|
2592
|
+
const googleEnabled = !!webClientId && !busy;
|
|
2593
|
+
const appleEnabled = appleAvailable && !!config.providers.apple?.service_id && !busy;
|
|
2594
|
+
return /* @__PURE__ */ jsxs16(Fragment5, { children: [
|
|
2595
|
+
!!config.providers.google && /* @__PURE__ */ jsxs16(
|
|
2596
|
+
Pressable12,
|
|
2597
|
+
{
|
|
2598
|
+
style: [styles18.signInBtn, !googleEnabled && styles18.signInBtnDisabled],
|
|
2599
|
+
onPress: handleGoogleLogin,
|
|
2600
|
+
disabled: !googleEnabled,
|
|
2601
|
+
children: [
|
|
2602
|
+
/* @__PURE__ */ jsx20(Text15, { style: styles18.signInGlyph, children: "G" }),
|
|
2603
|
+
/* @__PURE__ */ jsx20(Text15, { style: styles18.signInText, children: "Continue with Google" })
|
|
2604
|
+
]
|
|
2605
|
+
}
|
|
2606
|
+
),
|
|
2607
|
+
appleEnabled && getOAuthProvider()?.renderAppleButton({ onPress: handleAppleLogin, disabled: busy, style: styles18.appleBtn }),
|
|
2608
|
+
!!error && /* @__PURE__ */ jsx20(Text15, { style: styles18.errorText, children: error }),
|
|
2609
|
+
busy && /* @__PURE__ */ jsx20(ActivityIndicator9, { color: ada15.ink })
|
|
2610
|
+
] });
|
|
2611
|
+
}
|
|
2612
|
+
var styles18 = StyleSheet18.create({
|
|
2613
|
+
container: {
|
|
2614
|
+
flex: 1,
|
|
2615
|
+
backgroundColor: ada15.paper,
|
|
2616
|
+
justifyContent: "space-between",
|
|
2617
|
+
paddingHorizontal: 32,
|
|
2618
|
+
paddingBottom: 24
|
|
2619
|
+
},
|
|
2620
|
+
hero: { flex: 1, alignItems: "center", justifyContent: "center", gap: 18 },
|
|
2621
|
+
wordmark: { fontSize: 32, fontWeight: "600", color: ada15.ink, letterSpacing: 4, fontFamily: fontFam10, marginTop: 8 },
|
|
2622
|
+
tagline: { fontSize: 16, fontWeight: "500", color: ada15.inkSoft, letterSpacing: -0.1, fontFamily: fontFam10 },
|
|
2623
|
+
buttonSection: { alignItems: "center", gap: 12 },
|
|
2624
|
+
signInBtn: {
|
|
2625
|
+
flexDirection: "row",
|
|
2626
|
+
alignItems: "center",
|
|
2627
|
+
justifyContent: "center",
|
|
2628
|
+
backgroundColor: ada15.ink,
|
|
2629
|
+
paddingVertical: 16,
|
|
2630
|
+
paddingHorizontal: 24,
|
|
2631
|
+
borderRadius: 999,
|
|
2632
|
+
width: "100%",
|
|
2633
|
+
gap: 12
|
|
2634
|
+
},
|
|
2635
|
+
signInBtnDisabled: { opacity: 0.5 },
|
|
2636
|
+
signInGlyph: { fontSize: 18, fontWeight: "700", color: ada15.paper, fontFamily: fontFam10 },
|
|
2637
|
+
signInText: { fontSize: 16, fontWeight: "600", color: ada15.paper, letterSpacing: -0.2, fontFamily: fontFam10 },
|
|
2638
|
+
appleBtn: { width: "100%", height: 52 },
|
|
2639
|
+
errorText: { fontSize: 13, color: "#c43d3d", textAlign: "center", paddingHorizontal: 16 },
|
|
2640
|
+
disclaimer: { fontSize: 12, color: ada15.inkMuted, textAlign: "center", marginTop: 8, lineHeight: 18, fontFamily: fontFam10 },
|
|
2641
|
+
joinLink: { fontSize: 14, fontWeight: "600", color: ada15.orangeDeep, textAlign: "center", fontFamily: fontFam10 }
|
|
2642
|
+
});
|
|
2643
|
+
|
|
2125
2644
|
// src/voice/VoiceCallSheet.tsx
|
|
2126
|
-
import { useEffect as
|
|
2645
|
+
import { useEffect as useEffect13, useRef as useRef6, useMemo as useMemo7 } from "react";
|
|
2127
2646
|
import {
|
|
2128
|
-
View as
|
|
2129
|
-
Text as
|
|
2130
|
-
StyleSheet as
|
|
2131
|
-
Platform as
|
|
2132
|
-
Pressable as
|
|
2647
|
+
View as View20,
|
|
2648
|
+
Text as Text16,
|
|
2649
|
+
StyleSheet as StyleSheet19,
|
|
2650
|
+
Platform as Platform14,
|
|
2651
|
+
Pressable as Pressable13,
|
|
2133
2652
|
ScrollView
|
|
2134
2653
|
} from "react-native";
|
|
2135
2654
|
import Svg2, { Path as Path2, Line as Line2 } from "react-native-svg";
|
|
2136
|
-
import {
|
|
2137
|
-
import {
|
|
2138
|
-
import { AdaOrb as AdaOrb3, SurfaceFullscreen as SurfaceFullscreen2 } from "@ateam-ai-mobile/plugin-runtime";
|
|
2655
|
+
import { ada as ada16 } from "@ateam-ai-mobile/core";
|
|
2656
|
+
import { AdaOrb as AdaOrb7, SurfaceFullscreen as SurfaceFullscreen2 } from "@ateam-ai-mobile/plugin-runtime";
|
|
2139
2657
|
|
|
2140
2658
|
// src/voice/useVoiceCall.ts
|
|
2141
|
-
import {
|
|
2659
|
+
import { useCallback as useCallback6, useEffect as useEffect12, useReducer } from "react";
|
|
2660
|
+
|
|
2661
|
+
// src/voice/voiceSession.ts
|
|
2142
2662
|
import { getVoiceApiUrl as getVoiceApiUrl2, getToken as getToken2, getTenant as getTenant5, getUser as getUser2, refreshToken } from "@ateam-ai-mobile/core";
|
|
2663
|
+
import {
|
|
2664
|
+
callDeviceTool,
|
|
2665
|
+
ensureDeviceToolsLoaded,
|
|
2666
|
+
listDeviceTools,
|
|
2667
|
+
hasDeviceTool,
|
|
2668
|
+
PLATFORM_VOICE_CALLER
|
|
2669
|
+
} from "@ateam-ai-mobile/plugin-runtime";
|
|
2670
|
+
var CONNECT_TIMEOUT_MS = 3e4;
|
|
2143
2671
|
var TOKEN_REFRESH_SKEW_MS = 9e4;
|
|
2144
2672
|
function tokenNeedsRefresh(token) {
|
|
2145
2673
|
if (!token) return true;
|
|
@@ -2167,7 +2695,6 @@ var INJECT_TRACK_HOOK = `
|
|
|
2167
2695
|
}
|
|
2168
2696
|
}
|
|
2169
2697
|
|
|
2170
|
-
// 1) Hook getUserMedia \u2014 captures mic streams at request time.
|
|
2171
2698
|
try {
|
|
2172
2699
|
if (navigator.mediaDevices && navigator.mediaDevices.getUserMedia) {
|
|
2173
2700
|
const origGUM = navigator.mediaDevices.getUserMedia.bind(navigator.mediaDevices);
|
|
@@ -2181,7 +2708,6 @@ var INJECT_TRACK_HOOK = `
|
|
|
2181
2708
|
}
|
|
2182
2709
|
} catch (e) {}
|
|
2183
2710
|
|
|
2184
|
-
// 2) Hook RTCPeerConnection.addTrack \u2014 catches whatever the app sends.
|
|
2185
2711
|
try {
|
|
2186
2712
|
if (window.RTCPeerConnection) {
|
|
2187
2713
|
const origPC = window.RTCPeerConnection;
|
|
@@ -2201,9 +2727,7 @@ var INJECT_TRACK_HOOK = `
|
|
|
2201
2727
|
}
|
|
2202
2728
|
if (origAddTransceiver) {
|
|
2203
2729
|
origPC.prototype.addTransceiver = function(arg) {
|
|
2204
|
-
if (arg && arg.kind === 'audio') {
|
|
2205
|
-
// Transceiver created with kind 'audio' \u2014 sender track set later.
|
|
2206
|
-
} else if (arg && arg.kind === undefined && arg.track && arg.track.kind === 'audio') {
|
|
2730
|
+
if (arg && arg.kind === undefined && arg.track && arg.track.kind === 'audio') {
|
|
2207
2731
|
track(arg.track);
|
|
2208
2732
|
}
|
|
2209
2733
|
const tx = origAddTransceiver.apply(this, arguments);
|
|
@@ -2216,7 +2740,6 @@ var INJECT_TRACK_HOOK = `
|
|
|
2216
2740
|
}
|
|
2217
2741
|
} catch (e) {}
|
|
2218
2742
|
|
|
2219
|
-
// 3) Helper exposed for direct calls.
|
|
2220
2743
|
window._adaSetMuted = function(muted) {
|
|
2221
2744
|
let n = 0;
|
|
2222
2745
|
if (Array.isArray(window._allAudioTracks)) {
|
|
@@ -2235,7 +2758,6 @@ var buildMuteScript = (muted) => `
|
|
|
2235
2758
|
if (typeof window._adaSetMuted === 'function') {
|
|
2236
2759
|
n = window._adaSetMuted(muted);
|
|
2237
2760
|
}
|
|
2238
|
-
// Last-resort scan if hook somehow missed.
|
|
2239
2761
|
if (n === 0 && Array.isArray(window._allAudioTracks)) {
|
|
2240
2762
|
window._allAudioTracks.forEach(function(t) {
|
|
2241
2763
|
try { t.enabled = !muted; n++; } catch (e) {}
|
|
@@ -2244,104 +2766,557 @@ var buildMuteScript = (muted) => `
|
|
|
2244
2766
|
console.log('[ada-mute]', muted ? 'MUTED' : 'UNMUTED', 'tracks=', n);
|
|
2245
2767
|
})(${muted}); true;
|
|
2246
2768
|
`;
|
|
2247
|
-
|
|
2248
|
-
|
|
2249
|
-
|
|
2250
|
-
|
|
2251
|
-
|
|
2252
|
-
|
|
2253
|
-
|
|
2254
|
-
|
|
2255
|
-
|
|
2256
|
-
|
|
2769
|
+
var buildAgentTurnScript = (input) => `
|
|
2770
|
+
(function(input) {
|
|
2771
|
+
if (typeof window.__adasAgentTurn === 'function') {
|
|
2772
|
+
try { window.__adasAgentTurn(input); }
|
|
2773
|
+
catch (e) {
|
|
2774
|
+
window.ReactNativeWebView && window.ReactNativeWebView.postMessage(JSON.stringify({
|
|
2775
|
+
type: 'agentTurnFailed', message: String(e && e.message || e),
|
|
2776
|
+
}));
|
|
2777
|
+
}
|
|
2778
|
+
} else if (window.ReactNativeWebView) {
|
|
2779
|
+
window.ReactNativeWebView.postMessage(JSON.stringify({
|
|
2780
|
+
type: 'agentTurnUnsupported',
|
|
2781
|
+
message: 'voice app does not support agent turns (__adasAgentTurn missing) \u2014 update the voice page',
|
|
2782
|
+
}));
|
|
2783
|
+
}
|
|
2784
|
+
})(${JSON.stringify(input)}); true;
|
|
2785
|
+
`;
|
|
2786
|
+
var buildObserveScript = (text) => `
|
|
2787
|
+
(function(text) {
|
|
2788
|
+
if (typeof window.__adasObserve === 'function') {
|
|
2789
|
+
try { window.__adasObserve(text); }
|
|
2790
|
+
catch (e) {
|
|
2791
|
+
window.ReactNativeWebView && window.ReactNativeWebView.postMessage(JSON.stringify({
|
|
2792
|
+
type: 'observeFailed', message: String(e && e.message || e),
|
|
2793
|
+
}));
|
|
2794
|
+
}
|
|
2795
|
+
} else if (window.ReactNativeWebView) {
|
|
2796
|
+
window.ReactNativeWebView.postMessage(JSON.stringify({
|
|
2797
|
+
type: 'observeUnsupported',
|
|
2798
|
+
message: 'voice app does not support observe (__adasObserve missing) \u2014 update the voice page',
|
|
2799
|
+
}));
|
|
2800
|
+
}
|
|
2801
|
+
})(${JSON.stringify(text)}); true;
|
|
2802
|
+
`;
|
|
2803
|
+
function originOf(url) {
|
|
2804
|
+
if (!url) return null;
|
|
2805
|
+
const m = /^([a-z][a-z0-9+.-]*:\/\/[^/?#]+)/i.exec(url);
|
|
2806
|
+
return m ? m[1].toLowerCase() : null;
|
|
2807
|
+
}
|
|
2808
|
+
var IDLE = {
|
|
2809
|
+
callState: "idle",
|
|
2810
|
+
sessionId: null,
|
|
2811
|
+
error: null,
|
|
2812
|
+
transcript: [],
|
|
2813
|
+
aiPartial: "",
|
|
2814
|
+
userPartial: "",
|
|
2815
|
+
muted: false,
|
|
2816
|
+
userSpeaking: false,
|
|
2817
|
+
lastUserSpeechEndedAt: null,
|
|
2818
|
+
lastDeviceTool: null,
|
|
2819
|
+
mounted: false,
|
|
2820
|
+
webViewSource: null,
|
|
2821
|
+
skillSlug: void 0
|
|
2822
|
+
};
|
|
2823
|
+
var VoiceSessionManager = class {
|
|
2824
|
+
constructor() {
|
|
2825
|
+
this.state = { ...IDLE };
|
|
2826
|
+
this.listeners = /* @__PURE__ */ new Set();
|
|
2827
|
+
this.webView = null;
|
|
2828
|
+
this.pending = [];
|
|
2829
|
+
/**
|
|
2830
|
+
* WHO owns the one physical session. Set to the caller that started it from
|
|
2831
|
+
* idle (a plugin's pluginId, or the PLATFORM symbol for the built-in sheet),
|
|
2832
|
+
* and kept after stop until the next start reassigns it — so a plugin can read
|
|
2833
|
+
* its own just-ended transcript but another plugin never can.
|
|
2834
|
+
*
|
|
2835
|
+
* ISOLATION (GPT blocker on 8881225): every access is scoped through this. A
|
|
2836
|
+
* different plugin must not read, control, or attach to another plugin's live
|
|
2837
|
+
* conversation just because it declared voice.
|
|
2838
|
+
*/
|
|
2839
|
+
this.owner = null;
|
|
2840
|
+
/** EVERY start() still in its pre-mount awaits (tool preload, token refresh).
|
|
2841
|
+
* The session is not 'connecting' yet, so a stop() arriving now would otherwise
|
|
2842
|
+
* be an idle no-op — and a start would then mount a call for a screen that has
|
|
2843
|
+
* already gone. A set, not one slot: two concurrent starts must BOTH be
|
|
2844
|
+
* cancellable. stop() cancels every pending start of its caller (the platform:
|
|
2845
|
+
* all of them); another plugin's stop cancels none. */
|
|
2846
|
+
this.pendingStarts = /* @__PURE__ */ new Set();
|
|
2847
|
+
/**
|
|
2848
|
+
* The device-tool names Core ALLOWED for this voice session — Core's sanitized
|
|
2849
|
+
* allowset, delivered to the phone in a `deviceToolAllowset` message.
|
|
2850
|
+
* Authorization gates against THIS (necessary, alongside the registry's can-do).
|
|
2851
|
+
*
|
|
2852
|
+
* WHY (GPT gate 1, corrected): the phone only PROPOSES its available tools
|
|
2853
|
+
* (sendDeviceToolList); CORE is the session authority and decides which are
|
|
2854
|
+
* accepted (session.device_tools). Freezing the phone's own proposal would let
|
|
2855
|
+
* a tool Core REJECTED still run locally. So the authz set is populated from
|
|
2856
|
+
* Core's accepted set, never from listDeviceTools(). Until Core's set arrives
|
|
2857
|
+
* this is null and every call FAILS CLOSED. Pinned once per session; the live
|
|
2858
|
+
* registry (which drifts via lazy loads / host registration / tenant changes)
|
|
2859
|
+
* never feeds it.
|
|
2860
|
+
*/
|
|
2861
|
+
this.frozenTools = null;
|
|
2862
|
+
/** The voice `sessionId` the current allowset is BOUND to. Authorization
|
|
2863
|
+
* requires it to equal the active session's id (state.sessionId), so a stale
|
|
2864
|
+
* allowset from a previous/other session is refused. */
|
|
2865
|
+
this.frozenSessionId = null;
|
|
2866
|
+
/** A CLEAN call (skills:'none') started by a plugin offers and runs ONLY that
|
|
2867
|
+
* plugin's own device tools — not other solutions' tools that happen to be
|
|
2868
|
+
* registered on the same phone. null = every registered tool (a normal call,
|
|
2869
|
+
* or the platform's). Set by every start(); like `owner`, kept after stop.
|
|
2870
|
+
* ONE identity: the caller is the TRUSTED manifest pluginId gateVoice stamps,
|
|
2871
|
+
* and the bundleManager registers a plugin's tools under that same id (never
|
|
2872
|
+
* the bundle's own short `id`), so exact equality is the whole check. */
|
|
2873
|
+
this.toolScope = null;
|
|
2874
|
+
this.injectedTrackHook = INJECT_TRACK_HOOK;
|
|
2875
|
+
}
|
|
2876
|
+
/** The platform (built-in sheet, mic indicator, session host) sees and
|
|
2877
|
+
* controls any session — it is the user's own surface. A plugin caller may
|
|
2878
|
+
* only touch a session it owns. */
|
|
2879
|
+
canAccess(caller) {
|
|
2880
|
+
return caller === PLATFORM_VOICE_CALLER || this.owner === caller;
|
|
2881
|
+
}
|
|
2882
|
+
foreignError() {
|
|
2883
|
+
const err = new Error(
|
|
2884
|
+
"voice: another solution is using the microphone \u2014 one live session per device."
|
|
2885
|
+
);
|
|
2886
|
+
err.code = "RESTRICTED";
|
|
2887
|
+
return err;
|
|
2888
|
+
}
|
|
2889
|
+
// ── Host wiring (used only by VoiceSessionHost) ──────────────
|
|
2890
|
+
/** The offscreen WebView sets its ref here so the session can inject into it. */
|
|
2891
|
+
setWebView(ref) {
|
|
2892
|
+
this.webView = ref;
|
|
2893
|
+
}
|
|
2894
|
+
/** Whole state — the host reads `mounted` + `webViewSource` to decide what to
|
|
2895
|
+
* render, and every reader derives its snapshot from this. */
|
|
2896
|
+
getInternal() {
|
|
2897
|
+
return this.state;
|
|
2898
|
+
}
|
|
2899
|
+
// ── VoiceAPI (what props.voice and useVoiceCall call) ────────
|
|
2900
|
+
async start(opts, caller = PLATFORM_VOICE_CALLER) {
|
|
2901
|
+
if (this.state.callState === "connected") {
|
|
2902
|
+
if (!this.canAccess(caller)) throw this.foreignError();
|
|
2903
|
+
return this.realSnapshot();
|
|
2904
|
+
}
|
|
2905
|
+
if (this.state.callState === "connecting") {
|
|
2906
|
+
if (!this.canAccess(caller)) throw this.foreignError();
|
|
2907
|
+
return this.waitForConnect();
|
|
2908
|
+
}
|
|
2909
|
+
if (opts?.skills === "none" && opts?.skillSlug) {
|
|
2910
|
+
throw new Error("voice: skills:'none' (a clean call) cannot also hand the mic to a skill (skillSlug).");
|
|
2911
|
+
}
|
|
2912
|
+
const ticket = { caller, cancelled: false };
|
|
2913
|
+
this.pendingStarts.add(ticket);
|
|
2257
2914
|
try {
|
|
2258
|
-
|
|
2259
|
-
|
|
2260
|
-
|
|
2261
|
-
case "callState":
|
|
2262
|
-
case "call-state":
|
|
2263
|
-
setCallState(data.state);
|
|
2264
|
-
if (data.sessionId) setSessionId(data.sessionId);
|
|
2265
|
-
if (data.error) setError(data.error);
|
|
2266
|
-
break;
|
|
2267
|
-
case "transcript":
|
|
2268
|
-
setTranscript((prev) => [...prev, { role: data.role, text: data.text, ts: Date.now() }]);
|
|
2269
|
-
break;
|
|
2270
|
-
case "aiPartial":
|
|
2271
|
-
setAiPartial(data.text);
|
|
2272
|
-
break;
|
|
2273
|
-
case "userPartial":
|
|
2274
|
-
setUserPartial(data.text);
|
|
2275
|
-
break;
|
|
2276
|
-
case "clearPartials":
|
|
2277
|
-
setAiPartial("");
|
|
2278
|
-
setUserPartial("");
|
|
2279
|
-
break;
|
|
2280
|
-
case "error":
|
|
2281
|
-
setError(data.message);
|
|
2282
|
-
setCallState("error");
|
|
2283
|
-
break;
|
|
2284
|
-
}
|
|
2285
|
-
} catch {
|
|
2915
|
+
await ensureDeviceToolsLoaded();
|
|
2916
|
+
} catch (e) {
|
|
2917
|
+
console.warn("[voiceSession] device tool preload failed:", e?.message || String(e));
|
|
2286
2918
|
}
|
|
2287
|
-
}, []);
|
|
2288
|
-
const startCall = useCallback6(async () => {
|
|
2289
|
-
setError(null);
|
|
2290
|
-
setTranscript([]);
|
|
2291
|
-
setAiPartial("");
|
|
2292
|
-
setUserPartial("");
|
|
2293
2919
|
try {
|
|
2294
|
-
if (tokenNeedsRefresh(getToken2()))
|
|
2295
|
-
await refreshToken();
|
|
2296
|
-
}
|
|
2920
|
+
if (tokenNeedsRefresh(getToken2())) await refreshToken();
|
|
2297
2921
|
} catch {
|
|
2298
2922
|
}
|
|
2299
|
-
|
|
2300
|
-
|
|
2301
|
-
|
|
2302
|
-
|
|
2303
|
-
|
|
2304
|
-
|
|
2305
|
-
|
|
2306
|
-
|
|
2307
|
-
|
|
2308
|
-
|
|
2309
|
-
|
|
2310
|
-
|
|
2311
|
-
|
|
2312
|
-
|
|
2923
|
+
this.pendingStarts.delete(ticket);
|
|
2924
|
+
if (ticket.cancelled) throw new Error("voice: stopped before the session started");
|
|
2925
|
+
const after = this.getInternal().callState;
|
|
2926
|
+
if (after === "connected") {
|
|
2927
|
+
if (!this.canAccess(caller)) throw this.foreignError();
|
|
2928
|
+
return this.realSnapshot();
|
|
2929
|
+
}
|
|
2930
|
+
if (after === "connecting") {
|
|
2931
|
+
if (!this.canAccess(caller)) throw this.foreignError();
|
|
2932
|
+
return this.waitForConnect();
|
|
2933
|
+
}
|
|
2934
|
+
this.owner = caller;
|
|
2935
|
+
this.toolScope = opts?.skills === "none" && typeof caller === "string" ? caller : null;
|
|
2936
|
+
this.frozenTools = null;
|
|
2937
|
+
this.frozenSessionId = null;
|
|
2938
|
+
const skillSlug = opts?.skillSlug;
|
|
2939
|
+
this.setState({
|
|
2940
|
+
callState: "connecting",
|
|
2941
|
+
error: null,
|
|
2942
|
+
transcript: [],
|
|
2943
|
+
aiPartial: "",
|
|
2944
|
+
userPartial: "",
|
|
2945
|
+
muted: false,
|
|
2946
|
+
mounted: true,
|
|
2947
|
+
skillSlug,
|
|
2948
|
+
persona: opts?.persona,
|
|
2949
|
+
webViewSource: { uri: this.buildUri(skillSlug, opts?.persona, opts?.skills) }
|
|
2313
2950
|
});
|
|
2314
|
-
|
|
2315
|
-
|
|
2951
|
+
return this.waitForConnect();
|
|
2952
|
+
}
|
|
2953
|
+
stop(caller = PLATFORM_VOICE_CALLER) {
|
|
2954
|
+
if (this.state.callState !== "idle" && !this.canAccess(caller)) throw this.foreignError();
|
|
2955
|
+
for (const t of [...this.pendingStarts]) {
|
|
2956
|
+
if (caller === PLATFORM_VOICE_CALLER || caller === t.caller) {
|
|
2957
|
+
t.cancelled = true;
|
|
2958
|
+
this.pendingStarts.delete(t);
|
|
2959
|
+
}
|
|
2960
|
+
}
|
|
2961
|
+
this.webView?.injectJavaScript?.("window.hangUp && window.hangUp(); true;");
|
|
2962
|
+
this.rejectPending(new Error("voice: session stopped before it connected"));
|
|
2963
|
+
this.frozenTools = null;
|
|
2964
|
+
this.frozenSessionId = null;
|
|
2965
|
+
this.setState({
|
|
2966
|
+
...IDLE,
|
|
2967
|
+
// keep the transcript of the call that just ended for any reader still up
|
|
2968
|
+
transcript: this.state.transcript
|
|
2969
|
+
});
|
|
2970
|
+
}
|
|
2971
|
+
setMuted(muted, caller = PLATFORM_VOICE_CALLER) {
|
|
2972
|
+
if (this.state.callState !== "idle" && !this.canAccess(caller)) throw this.foreignError();
|
|
2973
|
+
this.webView?.injectJavaScript?.(buildMuteScript(muted));
|
|
2974
|
+
this.setState({ muted });
|
|
2975
|
+
}
|
|
2976
|
+
prompt(input, caller = PLATFORM_VOICE_CALLER) {
|
|
2977
|
+
if (this.state.callState !== "connected") {
|
|
2978
|
+
throw new Error("voice: session is not connected \u2014 call start() and await it before prompt().");
|
|
2979
|
+
}
|
|
2980
|
+
if (!this.canAccess(caller)) throw this.foreignError();
|
|
2981
|
+
this.webView?.injectJavaScript?.(buildAgentTurnScript(input));
|
|
2982
|
+
}
|
|
2983
|
+
/**
|
|
2984
|
+
* Stream a silent observation into the live session — tell the model without
|
|
2985
|
+
* asking it to speak. The cheap channel behind the gated props.voice.observe();
|
|
2986
|
+
* every item stays in the model's context for the call, so a caller sends only
|
|
2987
|
+
* material changes. Owner-scoped like every other control.
|
|
2988
|
+
*/
|
|
2989
|
+
observe(text, caller = PLATFORM_VOICE_CALLER) {
|
|
2990
|
+
if (this.state.callState !== "connected") {
|
|
2991
|
+
throw new Error("voice: session is not connected \u2014 observe() needs a live session.");
|
|
2992
|
+
}
|
|
2993
|
+
if (!this.canAccess(caller)) throw this.foreignError();
|
|
2994
|
+
this.webView?.injectJavaScript?.(buildObserveScript(text));
|
|
2995
|
+
}
|
|
2996
|
+
getSnapshot(caller = PLATFORM_VOICE_CALLER) {
|
|
2997
|
+
return this.canAccess(caller) ? this.realSnapshot() : this.foreignSnapshot();
|
|
2998
|
+
}
|
|
2999
|
+
realSnapshot() {
|
|
3000
|
+
const s = this.state;
|
|
3001
|
+
return {
|
|
3002
|
+
state: s.callState,
|
|
3003
|
+
sessionId: s.sessionId,
|
|
3004
|
+
muted: s.muted,
|
|
3005
|
+
speaking: !!s.aiPartial,
|
|
3006
|
+
listening: !!s.userPartial,
|
|
3007
|
+
userSpeaking: s.userSpeaking,
|
|
3008
|
+
lastUserSpeechEndedAt: s.lastUserSpeechEndedAt,
|
|
3009
|
+
lastDeviceTool: s.lastDeviceTool,
|
|
3010
|
+
authorizedSessionId: this.frozenSessionId,
|
|
3011
|
+
lastError: s.error
|
|
3012
|
+
};
|
|
3013
|
+
}
|
|
3014
|
+
foreignSnapshot() {
|
|
3015
|
+
return {
|
|
3016
|
+
state: "idle",
|
|
3017
|
+
sessionId: null,
|
|
3018
|
+
muted: false,
|
|
3019
|
+
speaking: false,
|
|
3020
|
+
listening: false,
|
|
3021
|
+
userSpeaking: false,
|
|
3022
|
+
lastUserSpeechEndedAt: null,
|
|
3023
|
+
lastDeviceTool: null,
|
|
3024
|
+
authorizedSessionId: null,
|
|
3025
|
+
lastError: null
|
|
3026
|
+
};
|
|
3027
|
+
}
|
|
3028
|
+
subscribe(cb, caller = PLATFORM_VOICE_CALLER) {
|
|
3029
|
+
const wrapped = (event) => {
|
|
3030
|
+
if (this.canAccess(caller)) cb(event);
|
|
3031
|
+
};
|
|
3032
|
+
this.listeners.add(wrapped);
|
|
3033
|
+
return { unsubscribe: () => {
|
|
3034
|
+
this.listeners.delete(wrapped);
|
|
3035
|
+
} };
|
|
3036
|
+
}
|
|
3037
|
+
transcript(caller = PLATFORM_VOICE_CALLER) {
|
|
3038
|
+
return this.canAccess(caller) ? this.state.transcript.slice() : [];
|
|
3039
|
+
}
|
|
3040
|
+
// ── WebView plumbing ─────────────────────────────────────────
|
|
3041
|
+
buildUri(skillSlug, persona, skills) {
|
|
2316
3042
|
const token = getToken2();
|
|
2317
3043
|
const tenant = getTenant5();
|
|
2318
3044
|
const actorId = getUser2()?.actorId || "";
|
|
2319
3045
|
const skillParam = skillSlug ? `&skillSlug=${encodeURIComponent(skillSlug)}` : "";
|
|
2320
|
-
|
|
2321
|
-
|
|
3046
|
+
const p = persona && (persona.name || persona.style) ? { ...persona.name ? { name: persona.name } : {}, ...persona.style ? { style: persona.style } : {} } : null;
|
|
3047
|
+
const personaParam = p ? `&persona=${encodeURIComponent(JSON.stringify(p))}` : "";
|
|
3048
|
+
const skillsParam = skills === "none" ? "&skills=none" : "";
|
|
3049
|
+
return `${getVoiceApiUrl2()}/app?mode=mini&embedded=rn&authToken=${encodeURIComponent(token || "")}&tenant=${encodeURIComponent(tenant)}&actorId=${encodeURIComponent(actorId)}${skillParam}${personaParam}${skillsParam}`;
|
|
3050
|
+
}
|
|
3051
|
+
/** Handle a postMessage from the WebView. `event` is the RN WebView event. */
|
|
3052
|
+
handleMessage(event) {
|
|
3053
|
+
let data;
|
|
3054
|
+
try {
|
|
3055
|
+
data = JSON.parse(event.nativeEvent.data);
|
|
3056
|
+
} catch {
|
|
3057
|
+
return;
|
|
3058
|
+
}
|
|
3059
|
+
switch (data.type) {
|
|
3060
|
+
case "callState":
|
|
3061
|
+
case "call-state":
|
|
3062
|
+
if (data.state) this.setState({ callState: data.state });
|
|
3063
|
+
if (data.sessionId) this.setState({ sessionId: data.sessionId });
|
|
3064
|
+
if (data.error) this.setState({ error: data.error });
|
|
3065
|
+
if (data.state === "connected") this.resolvePending();
|
|
3066
|
+
if (data.state === "error") this.rejectPending(new Error(data.error || "voice: connection error"));
|
|
3067
|
+
break;
|
|
3068
|
+
case "transcript":
|
|
3069
|
+
this.appendTranscript({ role: data.role, text: data.text, ts: Date.now() });
|
|
3070
|
+
break;
|
|
3071
|
+
case "aiPartial":
|
|
3072
|
+
this.setState({ aiPartial: data.text });
|
|
3073
|
+
break;
|
|
3074
|
+
case "userPartial":
|
|
3075
|
+
this.setState({ userPartial: data.text });
|
|
3076
|
+
break;
|
|
3077
|
+
case "clearPartials":
|
|
3078
|
+
this.setState({ aiPartial: "", userPartial: "" });
|
|
3079
|
+
break;
|
|
3080
|
+
// User VAD from the mini-app. `at` is when it happened; on speaking:false
|
|
3081
|
+
// we stamp lastUserSpeechEndedAt so a solution can gate "speak after N s of
|
|
3082
|
+
// silence" on the first real stop, never before the user has spoken.
|
|
3083
|
+
case "speechActivity": {
|
|
3084
|
+
const speaking = !!data.speaking;
|
|
3085
|
+
const at = typeof data.at === "number" ? data.at : Date.now();
|
|
3086
|
+
this.setState(
|
|
3087
|
+
speaking ? { userSpeaking: true } : { userSpeaking: false, lastUserSpeechEndedAt: at }
|
|
3088
|
+
);
|
|
3089
|
+
break;
|
|
3090
|
+
}
|
|
3091
|
+
case "error":
|
|
3092
|
+
this.setState({ error: data.message, callState: "error" });
|
|
3093
|
+
this.rejectPending(new Error(data.message || "voice: error"));
|
|
3094
|
+
break;
|
|
3095
|
+
// A proactive turn couldn't run (older voice page / hook threw). NON-FATAL:
|
|
3096
|
+
// the session stays live; we only surface why the guide didn't speak.
|
|
3097
|
+
case "agentTurnUnsupported":
|
|
3098
|
+
case "agentTurnFailed":
|
|
3099
|
+
case "observeUnsupported":
|
|
3100
|
+
case "observeFailed":
|
|
3101
|
+
this.setState({ error: data.message || "voice: channel notice" });
|
|
3102
|
+
break;
|
|
3103
|
+
// Device tools — executed locally, origin-checked. The model's function
|
|
3104
|
+
// call already arrived on this phone; only the voice app's own origin may
|
|
3105
|
+
// drive the camera/GPS.
|
|
3106
|
+
case "device-tool-list": {
|
|
3107
|
+
const listSender = originOf(event?.nativeEvent?.url);
|
|
3108
|
+
const listExpected = originOf(getVoiceApiUrl2());
|
|
3109
|
+
if (!listSender || !listExpected || listSender !== listExpected) break;
|
|
3110
|
+
this.sendDeviceToolList(data.correlationId);
|
|
3111
|
+
break;
|
|
3112
|
+
}
|
|
3113
|
+
// CORE's ALLOWSET for this session — what the model may ASK for. Core sends
|
|
3114
|
+
// it (even empty, on purpose) at session start. It is a NECESSARY gate, not
|
|
3115
|
+
// the whole story: a call must be in the allowset AND the device must
|
|
3116
|
+
// actually have the tool (the registry check inside callDeviceTool) — Core
|
|
3117
|
+
// says what may be asked, the device decides what it will do.
|
|
3118
|
+
//
|
|
3119
|
+
// Origin-checked (only the voice app may set it) and pinned ONCE per session
|
|
3120
|
+
// (a later message cannot widen it). An EMPTY allowset (arrived) refuses
|
|
3121
|
+
// everything but is distinct from NEVER-arrived (frozenTools stays null);
|
|
3122
|
+
// both fail closed here — we never fall back to the phone's own registry,
|
|
3123
|
+
// which is the seam this closes.
|
|
3124
|
+
case "deviceToolAllowset": {
|
|
3125
|
+
const sender = originOf(event?.nativeEvent?.url);
|
|
3126
|
+
const expected = originOf(getVoiceApiUrl2());
|
|
3127
|
+
if (!sender || !expected || sender !== expected) break;
|
|
3128
|
+
if (Array.isArray(data.tools) && typeof data.sessionId === "string" && data.sessionId !== this.frozenSessionId) {
|
|
3129
|
+
this.frozenTools = new Set(data.tools.filter((n) => typeof n === "string"));
|
|
3130
|
+
this.frozenSessionId = data.sessionId;
|
|
3131
|
+
}
|
|
3132
|
+
break;
|
|
3133
|
+
}
|
|
3134
|
+
case "device-tool-call": {
|
|
3135
|
+
const sender = originOf(event?.nativeEvent?.url);
|
|
3136
|
+
const expected = originOf(getVoiceApiUrl2());
|
|
3137
|
+
if (!sender || !expected || sender !== expected) {
|
|
3138
|
+
console.warn(`[voiceSession] REFUSED device tool call from "${sender || "unknown origin"}" (expected "${expected}")`);
|
|
3139
|
+
break;
|
|
3140
|
+
}
|
|
3141
|
+
if (data.name) this.setState({ lastDeviceTool: { name: data.name, at: Date.now() } });
|
|
3142
|
+
if (!this.isToolAuthorized(data.name)) {
|
|
3143
|
+
const notReady = this.frozenTools === null;
|
|
3144
|
+
console.warn(`[voiceSession] REFUSED "${data.name}" \u2014 ${notReady ? "allowset not received yet" : "not authorized for this session"}`);
|
|
3145
|
+
this.postDeviceToolRefusal(data.correlationId, data.name, notReady);
|
|
3146
|
+
break;
|
|
3147
|
+
}
|
|
3148
|
+
void this.runDeviceTool(data);
|
|
3149
|
+
break;
|
|
3150
|
+
}
|
|
3151
|
+
}
|
|
3152
|
+
}
|
|
3153
|
+
/**
|
|
3154
|
+
* Final authorization = Core's frozen allowset ∩ the current valid local
|
|
3155
|
+
* registry, bound to the ACTIVE session. All must hold:
|
|
3156
|
+
* - a Core allowset arrived and is bound to THIS session (frozenSessionId ===
|
|
3157
|
+
* the active state.sessionId) — a missing or stale/wrong-session allowset
|
|
3158
|
+
* fails closed;
|
|
3159
|
+
* - the tool is IN that allowset (Core approved it); AND
|
|
3160
|
+
* - the tool is STILL registered locally right now (removed-after-approval is
|
|
3161
|
+
* refused; a lazily-added tool Core never approved is refused) — and, on a
|
|
3162
|
+
* clean call, registered by the call's owner.
|
|
3163
|
+
* No fallback to the live registry alone, ever.
|
|
3164
|
+
*/
|
|
3165
|
+
isToolAuthorized(name) {
|
|
3166
|
+
if (typeof name !== "string") return false;
|
|
3167
|
+
if (this.frozenTools === null || this.frozenSessionId === null) return false;
|
|
3168
|
+
if (this.frozenSessionId !== this.state.sessionId) return false;
|
|
3169
|
+
if (!this.frozenTools.has(name)) return false;
|
|
3170
|
+
return hasDeviceTool(name, this.toolScope ?? void 0);
|
|
3171
|
+
}
|
|
3172
|
+
/** Tell the mini-app the call was refused — a stated reason, never a silent drop.
|
|
3173
|
+
* `notReady` distinguishes a transient "allowset not here yet" (retry) from a
|
|
3174
|
+
* final "not allowed in this session". */
|
|
3175
|
+
postDeviceToolRefusal(correlationId, name, notReady) {
|
|
3176
|
+
const payload = notReady ? {
|
|
3177
|
+
correlationId,
|
|
3178
|
+
ok: false,
|
|
3179
|
+
reason: "not_ready",
|
|
3180
|
+
error: "The voice session is still starting up \u2014 its tool allowset has not arrived yet. Try again in a moment."
|
|
3181
|
+
} : {
|
|
3182
|
+
correlationId,
|
|
3183
|
+
ok: false,
|
|
3184
|
+
reason: "not_authorized",
|
|
3185
|
+
error: `"${name}" is not available in this voice session.`
|
|
2322
3186
|
};
|
|
2323
|
-
|
|
3187
|
+
this.webView?.injectJavaScript?.(
|
|
3188
|
+
`window.__adasDeviceToolResult && window.__adasDeviceToolResult(${JSON.stringify(
|
|
3189
|
+
JSON.stringify(payload)
|
|
3190
|
+
)}); true;`
|
|
3191
|
+
);
|
|
3192
|
+
}
|
|
3193
|
+
/** PROPOSE the phone's available tools — this is an offer to Core, not the
|
|
3194
|
+
* authorization set. Core sanitizes it and returns the accepted set in a
|
|
3195
|
+
* `deviceToolAllowset` message, which is what actually gates execution. */
|
|
3196
|
+
sendDeviceToolList(correlationId) {
|
|
3197
|
+
const scope = this.toolScope;
|
|
3198
|
+
const tools = listDeviceTools().filter((t) => scope === null || t.pluginId === scope).map((t) => ({
|
|
3199
|
+
name: t.name,
|
|
3200
|
+
description: t.description,
|
|
3201
|
+
input_schema: t.input_schema
|
|
3202
|
+
}));
|
|
3203
|
+
this.webView?.injectJavaScript?.(
|
|
3204
|
+
`window.__adasDeviceToolResult && window.__adasDeviceToolResult(${JSON.stringify(
|
|
3205
|
+
JSON.stringify({ correlationId, ok: true, tools })
|
|
3206
|
+
)}); true;`
|
|
3207
|
+
);
|
|
3208
|
+
}
|
|
3209
|
+
/** Run a device tool and post the outcome back into the WebView. */
|
|
3210
|
+
async runDeviceTool(data) {
|
|
3211
|
+
const { correlationId, name, args, timeoutMs } = data || {};
|
|
3212
|
+
if (!correlationId || typeof name !== "string") return;
|
|
3213
|
+
let payload;
|
|
3214
|
+
try {
|
|
3215
|
+
const outcome = await callDeviceTool(name, args || {}, { timeoutMs });
|
|
3216
|
+
payload = outcome.ok ? { correlationId, ok: true, result: outcome.result } : { correlationId, ok: false, error: outcome.error, reason: outcome.reason };
|
|
3217
|
+
} catch (err) {
|
|
3218
|
+
payload = { correlationId, ok: false, reason: "failed", error: err?.message || String(err) };
|
|
3219
|
+
}
|
|
3220
|
+
this.webView?.injectJavaScript?.(
|
|
3221
|
+
`window.__adasDeviceToolResult && window.__adasDeviceToolResult(${JSON.stringify(
|
|
3222
|
+
JSON.stringify(payload)
|
|
3223
|
+
)}); true;`
|
|
3224
|
+
);
|
|
3225
|
+
}
|
|
3226
|
+
// ── internals ────────────────────────────────────────────────
|
|
3227
|
+
setState(partial) {
|
|
3228
|
+
this.state = { ...this.state, ...partial };
|
|
3229
|
+
this.emit({ type: "state" });
|
|
3230
|
+
}
|
|
3231
|
+
appendTranscript(entry) {
|
|
3232
|
+
this.state = { ...this.state, transcript: [...this.state.transcript, entry] };
|
|
3233
|
+
this.emit({ type: "transcript" });
|
|
3234
|
+
}
|
|
3235
|
+
emit(event) {
|
|
3236
|
+
for (const cb of this.listeners) {
|
|
3237
|
+
try {
|
|
3238
|
+
cb(event);
|
|
3239
|
+
} catch (e) {
|
|
3240
|
+
console.warn("[voiceSession] listener threw", e);
|
|
3241
|
+
}
|
|
3242
|
+
}
|
|
3243
|
+
}
|
|
3244
|
+
waitForConnect() {
|
|
3245
|
+
return new Promise((resolve, reject) => {
|
|
3246
|
+
const timer = setTimeout(() => {
|
|
3247
|
+
this.pending = this.pending.filter((p) => p.timer !== timer);
|
|
3248
|
+
reject(new Error("voice: timed out waiting to connect (is VoiceSessionHost mounted?)"));
|
|
3249
|
+
}, CONNECT_TIMEOUT_MS);
|
|
3250
|
+
this.pending.push({ resolve, reject, timer });
|
|
3251
|
+
});
|
|
3252
|
+
}
|
|
3253
|
+
resolvePending() {
|
|
3254
|
+
const snap = this.realSnapshot();
|
|
3255
|
+
const waiting = this.pending;
|
|
3256
|
+
this.pending = [];
|
|
3257
|
+
for (const p of waiting) {
|
|
3258
|
+
clearTimeout(p.timer);
|
|
3259
|
+
p.resolve(snap);
|
|
3260
|
+
}
|
|
3261
|
+
}
|
|
3262
|
+
rejectPending(err) {
|
|
3263
|
+
const waiting = this.pending;
|
|
3264
|
+
this.pending = [];
|
|
3265
|
+
for (const p of waiting) {
|
|
3266
|
+
clearTimeout(p.timer);
|
|
3267
|
+
p.reject(err);
|
|
3268
|
+
}
|
|
3269
|
+
}
|
|
3270
|
+
/** TEST ONLY: reset the singleton to a clean idle state (mirrors the
|
|
3271
|
+
* __resetSessionSyncForTest precedent). Not part of any surface. */
|
|
3272
|
+
__resetForTest() {
|
|
3273
|
+
this.rejectPending(new Error("voice: reset"));
|
|
3274
|
+
this.state = { ...IDLE };
|
|
3275
|
+
this.owner = null;
|
|
3276
|
+
this.toolScope = null;
|
|
3277
|
+
this.pendingStarts.clear();
|
|
3278
|
+
this.frozenTools = null;
|
|
3279
|
+
this.frozenSessionId = null;
|
|
3280
|
+
this.listeners.clear();
|
|
3281
|
+
this.webView = null;
|
|
3282
|
+
}
|
|
3283
|
+
};
|
|
3284
|
+
var voiceSession = new VoiceSessionManager();
|
|
3285
|
+
|
|
3286
|
+
// src/voice/useVoiceCall.ts
|
|
3287
|
+
function useVoiceCall({ skillSlug } = {}) {
|
|
3288
|
+
const [, force] = useReducer((x) => x + 1, 0);
|
|
3289
|
+
useEffect12(() => {
|
|
3290
|
+
const sub = voiceSession.subscribe(() => force());
|
|
3291
|
+
return () => sub.unsubscribe();
|
|
3292
|
+
}, []);
|
|
3293
|
+
const s = voiceSession.getInternal();
|
|
3294
|
+
const startCall = useCallback6(
|
|
3295
|
+
(opts) => voiceSession.start(opts ?? { skillSlug }),
|
|
3296
|
+
[skillSlug]
|
|
3297
|
+
);
|
|
3298
|
+
const stopCall = useCallback6(() => voiceSession.stop(), []);
|
|
3299
|
+
const toggleMute = useCallback6(
|
|
3300
|
+
() => voiceSession.setMuted(!voiceSession.getSnapshot().muted),
|
|
3301
|
+
[]
|
|
3302
|
+
);
|
|
2324
3303
|
return {
|
|
2325
|
-
callState,
|
|
2326
|
-
sessionId,
|
|
2327
|
-
error,
|
|
2328
|
-
transcript,
|
|
2329
|
-
aiPartial,
|
|
2330
|
-
userPartial,
|
|
2331
|
-
muted,
|
|
3304
|
+
callState: s.callState,
|
|
3305
|
+
sessionId: s.sessionId,
|
|
3306
|
+
error: s.error,
|
|
3307
|
+
transcript: s.transcript,
|
|
3308
|
+
aiPartial: s.aiPartial,
|
|
3309
|
+
userPartial: s.userPartial,
|
|
3310
|
+
muted: s.muted,
|
|
2332
3311
|
startCall,
|
|
2333
3312
|
stopCall,
|
|
2334
|
-
toggleMute
|
|
2335
|
-
handleWebViewMessage,
|
|
2336
|
-
getWebViewSource,
|
|
2337
|
-
webViewRef,
|
|
2338
|
-
injectedTrackHook: INJECT_TRACK_HOOK
|
|
3313
|
+
toggleMute
|
|
2339
3314
|
};
|
|
2340
3315
|
}
|
|
2341
3316
|
|
|
2342
3317
|
// src/voice/VoiceCallSheet.tsx
|
|
2343
|
-
import { jsx as
|
|
2344
|
-
var
|
|
3318
|
+
import { jsx as jsx21, jsxs as jsxs17 } from "react/jsx-runtime";
|
|
3319
|
+
var fontFam11 = Platform14.OS === "ios" ? "SF Pro Rounded" : void 0;
|
|
2345
3320
|
var HIGHLIGHT_PATTERNS = [
|
|
2346
3321
|
/[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}/g,
|
|
2347
3322
|
/(?:\+?\d[\s\-().]?){7,}\d/g,
|
|
@@ -2384,8 +3359,8 @@ function HighlightedText({
|
|
|
2384
3359
|
baseStyle,
|
|
2385
3360
|
importantStyle
|
|
2386
3361
|
}) {
|
|
2387
|
-
const segments =
|
|
2388
|
-
return /* @__PURE__ */
|
|
3362
|
+
const segments = useMemo7(() => highlightSegments(text), [text]);
|
|
3363
|
+
return /* @__PURE__ */ jsx21(Text16, { style: baseStyle, children: segments.map((seg, i) => /* @__PURE__ */ jsx21(Text16, { style: seg.important ? importantStyle : void 0, children: seg.text }, i)) });
|
|
2389
3364
|
}
|
|
2390
3365
|
function VoiceCallSheet({ visible, onClose, skillSlug }) {
|
|
2391
3366
|
const {
|
|
@@ -2397,31 +3372,27 @@ function VoiceCallSheet({ visible, onClose, skillSlug }) {
|
|
|
2397
3372
|
muted,
|
|
2398
3373
|
startCall,
|
|
2399
3374
|
stopCall,
|
|
2400
|
-
toggleMute
|
|
2401
|
-
handleWebViewMessage,
|
|
2402
|
-
getWebViewSource,
|
|
2403
|
-
webViewRef,
|
|
2404
|
-
injectedTrackHook
|
|
3375
|
+
toggleMute
|
|
2405
3376
|
} = useVoiceCall({ skillSlug });
|
|
2406
3377
|
const scrollRef = useRef6(null);
|
|
2407
|
-
|
|
3378
|
+
useEffect13(() => {
|
|
2408
3379
|
if (visible && callState === "idle") startCall();
|
|
2409
3380
|
}, [visible]);
|
|
2410
|
-
|
|
3381
|
+
useEffect13(() => {
|
|
2411
3382
|
scrollRef.current?.scrollToEnd?.({ animated: true });
|
|
2412
3383
|
}, [transcript.length, aiPartial, userPartial]);
|
|
2413
3384
|
const handleHangUp = () => {
|
|
2414
3385
|
stopCall();
|
|
2415
3386
|
onClose();
|
|
2416
3387
|
};
|
|
2417
|
-
const orbState =
|
|
3388
|
+
const orbState = useMemo7(() => {
|
|
2418
3389
|
if (callState === "connecting") return "thinking";
|
|
2419
3390
|
if (callState === "error") return "idle";
|
|
2420
3391
|
if (aiPartial) return "speaking";
|
|
2421
3392
|
if (userPartial) return "listening";
|
|
2422
3393
|
return "idle";
|
|
2423
3394
|
}, [callState, aiPartial, userPartial]);
|
|
2424
|
-
const statusLabel =
|
|
3395
|
+
const statusLabel = useMemo7(() => {
|
|
2425
3396
|
if (callState === "connecting") return "Connecting\u2026";
|
|
2426
3397
|
if (callState === "error") return error || "Couldn\u2019t connect";
|
|
2427
3398
|
if (aiPartial) return "Ada is speaking";
|
|
@@ -2431,92 +3402,76 @@ function VoiceCallSheet({ visible, onClose, skillSlug }) {
|
|
|
2431
3402
|
}, [callState, aiPartial, userPartial, error]);
|
|
2432
3403
|
if (!visible) return null;
|
|
2433
3404
|
const visibleEntries = transcript.filter((e) => e.role === "user" || e.role === "ai");
|
|
2434
|
-
return /* @__PURE__ */
|
|
3405
|
+
return /* @__PURE__ */ jsxs17(
|
|
2435
3406
|
SurfaceFullscreen2,
|
|
2436
3407
|
{
|
|
2437
3408
|
visible,
|
|
2438
3409
|
onClose: handleHangUp,
|
|
2439
3410
|
status: statusLabel,
|
|
2440
|
-
hero: /* @__PURE__ */
|
|
3411
|
+
hero: /* @__PURE__ */ jsx21(AdaOrb7, { size: 180, state: orbState }),
|
|
2441
3412
|
closeIcon: "hangup",
|
|
2442
3413
|
closeLabel: "End",
|
|
2443
3414
|
children: [
|
|
2444
|
-
/* @__PURE__ */
|
|
3415
|
+
/* @__PURE__ */ jsxs17(
|
|
2445
3416
|
ScrollView,
|
|
2446
3417
|
{
|
|
2447
3418
|
ref: scrollRef,
|
|
2448
|
-
style:
|
|
2449
|
-
contentContainerStyle:
|
|
3419
|
+
style: styles19.transcript,
|
|
3420
|
+
contentContainerStyle: styles19.transcriptContent,
|
|
2450
3421
|
showsVerticalScrollIndicator: false,
|
|
2451
3422
|
children: [
|
|
2452
|
-
callState === "error" && error ? /* @__PURE__ */
|
|
2453
|
-
visibleEntries.map((entry, i) => /* @__PURE__ */
|
|
2454
|
-
/* @__PURE__ */
|
|
2455
|
-
/* @__PURE__ */
|
|
3423
|
+
callState === "error" && error ? /* @__PURE__ */ jsx21(View20, { style: styles19.errorBox, children: /* @__PURE__ */ jsx21(Text16, { style: styles19.errorText, children: error }) }) : null,
|
|
3424
|
+
visibleEntries.map((entry, i) => /* @__PURE__ */ jsxs17(View20, { style: styles19.entry, children: [
|
|
3425
|
+
/* @__PURE__ */ jsx21(Text16, { style: styles19.role, children: entry.role === "user" ? "YOU" : "ADA" }),
|
|
3426
|
+
/* @__PURE__ */ jsx21(
|
|
2456
3427
|
HighlightedText,
|
|
2457
3428
|
{
|
|
2458
3429
|
text: entry.text,
|
|
2459
|
-
baseStyle:
|
|
2460
|
-
importantStyle:
|
|
3430
|
+
baseStyle: styles19.entryText,
|
|
3431
|
+
importantStyle: styles19.important
|
|
2461
3432
|
}
|
|
2462
3433
|
)
|
|
2463
3434
|
] }, `${entry.ts}-${i}`)),
|
|
2464
|
-
userPartial || aiPartial ? /* @__PURE__ */
|
|
2465
|
-
/* @__PURE__ */
|
|
2466
|
-
/* @__PURE__ */
|
|
3435
|
+
userPartial || aiPartial ? /* @__PURE__ */ jsxs17(View20, { style: styles19.entry, children: [
|
|
3436
|
+
/* @__PURE__ */ jsx21(Text16, { style: styles19.role, children: userPartial ? "YOU" : "ADA" }),
|
|
3437
|
+
/* @__PURE__ */ jsx21(
|
|
2467
3438
|
HighlightedText,
|
|
2468
3439
|
{
|
|
2469
3440
|
text: userPartial || aiPartial,
|
|
2470
|
-
baseStyle:
|
|
2471
|
-
importantStyle:
|
|
3441
|
+
baseStyle: styles19.entryTextLive,
|
|
3442
|
+
importantStyle: styles19.important
|
|
2472
3443
|
}
|
|
2473
3444
|
)
|
|
2474
3445
|
] }) : null
|
|
2475
3446
|
]
|
|
2476
3447
|
}
|
|
2477
3448
|
),
|
|
2478
|
-
callState === "connected" || callState === "connecting" ? /* @__PURE__ */
|
|
2479
|
-
/* @__PURE__ */
|
|
2480
|
-
|
|
3449
|
+
callState === "connected" || callState === "connecting" ? /* @__PURE__ */ jsxs17(View20, { style: styles19.muteWrap, pointerEvents: "box-none", children: [
|
|
3450
|
+
/* @__PURE__ */ jsx21(
|
|
3451
|
+
Pressable13,
|
|
2481
3452
|
{
|
|
2482
3453
|
onPress: toggleMute,
|
|
2483
3454
|
accessibilityRole: "button",
|
|
2484
3455
|
accessibilityLabel: muted ? "Unmute microphone" : "Mute microphone",
|
|
2485
3456
|
accessibilityState: { selected: muted },
|
|
2486
|
-
style: [
|
|
3457
|
+
style: [styles19.muteBtn, muted && styles19.muteBtnActive],
|
|
2487
3458
|
hitSlop: 12,
|
|
2488
|
-
children: /* @__PURE__ */
|
|
2489
|
-
/* @__PURE__ */
|
|
2490
|
-
/* @__PURE__ */
|
|
2491
|
-
/* @__PURE__ */
|
|
2492
|
-
/* @__PURE__ */
|
|
2493
|
-
muted ? /* @__PURE__ */
|
|
3459
|
+
children: /* @__PURE__ */ jsxs17(Svg2, { width: 22, height: 22, viewBox: "0 0 24 24", fill: "none", stroke: muted ? ada16.paper : ada16.ink, strokeWidth: 2, strokeLinecap: "round", strokeLinejoin: "round", children: [
|
|
3460
|
+
/* @__PURE__ */ jsx21(Path2, { d: "M12 1a3 3 0 0 0-3 3v8a3 3 0 0 0 6 0V4a3 3 0 0 0-3-3z" }),
|
|
3461
|
+
/* @__PURE__ */ jsx21(Path2, { d: "M19 10v2a7 7 0 0 1-14 0v-2" }),
|
|
3462
|
+
/* @__PURE__ */ jsx21(Line2, { x1: "12", y1: "19", x2: "12", y2: "23" }),
|
|
3463
|
+
/* @__PURE__ */ jsx21(Line2, { x1: "8", y1: "23", x2: "16", y2: "23" }),
|
|
3464
|
+
muted ? /* @__PURE__ */ jsx21(Line2, { x1: "3", y1: "3", x2: "21", y2: "21", stroke: ada16.paper, strokeWidth: 2.4 }) : null
|
|
2494
3465
|
] })
|
|
2495
3466
|
}
|
|
2496
3467
|
),
|
|
2497
|
-
/* @__PURE__ */
|
|
2498
|
-
] }) : null
|
|
2499
|
-
callState !== "idle" && /* @__PURE__ */ jsx16(View15, { style: styles14.hiddenWebView, pointerEvents: "none", children: /* @__PURE__ */ jsx16(
|
|
2500
|
-
WebView3,
|
|
2501
|
-
{
|
|
2502
|
-
ref: webViewRef,
|
|
2503
|
-
source: getWebViewSource(),
|
|
2504
|
-
style: { flex: 1, backgroundColor: "transparent" },
|
|
2505
|
-
onMessage: handleWebViewMessage,
|
|
2506
|
-
onError: (e) => console.log("[VoiceWebView] error:", e.nativeEvent?.description),
|
|
2507
|
-
injectedJavaScriptBeforeContentLoaded: injectedTrackHook,
|
|
2508
|
-
mediaPlaybackRequiresUserAction: false,
|
|
2509
|
-
allowsInlineMediaPlayback: true,
|
|
2510
|
-
mediaCapturePermissionGrantType: "grant",
|
|
2511
|
-
javaScriptEnabled: true,
|
|
2512
|
-
originWhitelist: ["*"]
|
|
2513
|
-
}
|
|
2514
|
-
) })
|
|
3468
|
+
/* @__PURE__ */ jsx21(Text16, { style: styles19.muteLabel, children: muted ? "Muted" : "Mute" })
|
|
3469
|
+
] }) : null
|
|
2515
3470
|
]
|
|
2516
3471
|
}
|
|
2517
3472
|
);
|
|
2518
3473
|
}
|
|
2519
|
-
var
|
|
3474
|
+
var styles19 = StyleSheet19.create({
|
|
2520
3475
|
transcript: {
|
|
2521
3476
|
flex: 1,
|
|
2522
3477
|
paddingHorizontal: 28
|
|
@@ -2533,9 +3488,9 @@ var styles14 = StyleSheet14.create({
|
|
|
2533
3488
|
},
|
|
2534
3489
|
role: {
|
|
2535
3490
|
fontSize: 10,
|
|
2536
|
-
fontFamily:
|
|
3491
|
+
fontFamily: fontFam11,
|
|
2537
3492
|
fontWeight: "700",
|
|
2538
|
-
color:
|
|
3493
|
+
color: ada16.inkMuted,
|
|
2539
3494
|
letterSpacing: 1.4,
|
|
2540
3495
|
minWidth: 32,
|
|
2541
3496
|
paddingTop: 3,
|
|
@@ -2544,9 +3499,9 @@ var styles14 = StyleSheet14.create({
|
|
|
2544
3499
|
entryText: {
|
|
2545
3500
|
flex: 1,
|
|
2546
3501
|
fontSize: 14,
|
|
2547
|
-
fontFamily:
|
|
3502
|
+
fontFamily: fontFam11,
|
|
2548
3503
|
fontWeight: "400",
|
|
2549
|
-
color:
|
|
3504
|
+
color: ada16.inkSoft,
|
|
2550
3505
|
lineHeight: 20,
|
|
2551
3506
|
letterSpacing: -0.1,
|
|
2552
3507
|
opacity: 0.42
|
|
@@ -2554,18 +3509,18 @@ var styles14 = StyleSheet14.create({
|
|
|
2554
3509
|
entryTextLive: {
|
|
2555
3510
|
flex: 1,
|
|
2556
3511
|
fontSize: 14,
|
|
2557
|
-
fontFamily:
|
|
3512
|
+
fontFamily: fontFam11,
|
|
2558
3513
|
fontWeight: "400",
|
|
2559
|
-
color:
|
|
3514
|
+
color: ada16.inkSoft,
|
|
2560
3515
|
lineHeight: 20,
|
|
2561
3516
|
letterSpacing: -0.1,
|
|
2562
3517
|
opacity: 0.7
|
|
2563
3518
|
},
|
|
2564
3519
|
important: {
|
|
2565
3520
|
fontSize: 17,
|
|
2566
|
-
fontFamily:
|
|
3521
|
+
fontFamily: fontFam11,
|
|
2567
3522
|
fontWeight: "700",
|
|
2568
|
-
color:
|
|
3523
|
+
color: ada16.ink,
|
|
2569
3524
|
letterSpacing: -0.1,
|
|
2570
3525
|
opacity: 1
|
|
2571
3526
|
},
|
|
@@ -2580,14 +3535,14 @@ var styles14 = StyleSheet14.create({
|
|
|
2580
3535
|
},
|
|
2581
3536
|
errorText: {
|
|
2582
3537
|
fontSize: 13,
|
|
2583
|
-
fontFamily:
|
|
3538
|
+
fontFamily: fontFam11,
|
|
2584
3539
|
fontWeight: "500",
|
|
2585
3540
|
color: "#fca5a5",
|
|
2586
3541
|
lineHeight: 18
|
|
2587
3542
|
},
|
|
2588
3543
|
muteWrap: {
|
|
2589
3544
|
position: "absolute",
|
|
2590
|
-
bottom:
|
|
3545
|
+
bottom: Platform14.OS === "ios" ? 56 : 40,
|
|
2591
3546
|
left: 28,
|
|
2592
3547
|
alignItems: "center",
|
|
2593
3548
|
gap: 6
|
|
@@ -2596,9 +3551,9 @@ var styles14 = StyleSheet14.create({
|
|
|
2596
3551
|
width: 56,
|
|
2597
3552
|
height: 56,
|
|
2598
3553
|
borderRadius: 28,
|
|
2599
|
-
backgroundColor:
|
|
3554
|
+
backgroundColor: ada16.paperSoft,
|
|
2600
3555
|
borderWidth: 1,
|
|
2601
|
-
borderColor:
|
|
3556
|
+
borderColor: ada16.border,
|
|
2602
3557
|
justifyContent: "center",
|
|
2603
3558
|
alignItems: "center"
|
|
2604
3559
|
},
|
|
@@ -2608,18 +3563,133 @@ var styles14 = StyleSheet14.create({
|
|
|
2608
3563
|
},
|
|
2609
3564
|
muteLabel: {
|
|
2610
3565
|
fontSize: 12,
|
|
2611
|
-
fontFamily:
|
|
3566
|
+
fontFamily: fontFam11,
|
|
2612
3567
|
fontWeight: "500",
|
|
2613
|
-
color:
|
|
3568
|
+
color: ada16.inkMuted,
|
|
2614
3569
|
letterSpacing: 0.3
|
|
2615
|
-
}
|
|
2616
|
-
|
|
3570
|
+
}
|
|
3571
|
+
});
|
|
3572
|
+
|
|
3573
|
+
// src/voice/VoiceSessionHost.tsx
|
|
3574
|
+
import { useCallback as useCallback7, useEffect as useEffect14, useReducer as useReducer2, useRef as useRef7 } from "react";
|
|
3575
|
+
import { View as View21, StyleSheet as StyleSheet20 } from "react-native";
|
|
3576
|
+
import { WebView as WebView3 } from "react-native-webview";
|
|
3577
|
+
import { jsx as jsx22 } from "react/jsx-runtime";
|
|
3578
|
+
function VoiceSessionHost() {
|
|
3579
|
+
const [, force] = useReducer2((x) => x + 1, 0);
|
|
3580
|
+
const lastSig = useRef7("");
|
|
3581
|
+
useEffect14(() => {
|
|
3582
|
+
const sub = voiceSession.subscribe(() => {
|
|
3583
|
+
const s2 = voiceSession.getInternal();
|
|
3584
|
+
const sig = `${s2.mounted}|${s2.webViewSource?.uri || ""}`;
|
|
3585
|
+
if (sig !== lastSig.current) {
|
|
3586
|
+
lastSig.current = sig;
|
|
3587
|
+
force();
|
|
3588
|
+
}
|
|
3589
|
+
});
|
|
3590
|
+
return () => sub.unsubscribe();
|
|
3591
|
+
}, []);
|
|
3592
|
+
const setRef = useCallback7((r) => {
|
|
3593
|
+
voiceSession.setWebView(r);
|
|
3594
|
+
}, []);
|
|
3595
|
+
const s = voiceSession.getInternal();
|
|
3596
|
+
if (!s.mounted || !s.webViewSource) return null;
|
|
3597
|
+
return /* @__PURE__ */ jsx22(View21, { style: styles20.hidden, pointerEvents: "none", children: /* @__PURE__ */ jsx22(
|
|
3598
|
+
WebView3,
|
|
3599
|
+
{
|
|
3600
|
+
ref: setRef,
|
|
3601
|
+
source: s.webViewSource,
|
|
3602
|
+
style: styles20.web,
|
|
3603
|
+
onMessage: (e) => voiceSession.handleMessage(e),
|
|
3604
|
+
onError: (e) => console.log("[VoiceSessionHost] error:", e.nativeEvent?.description),
|
|
3605
|
+
injectedJavaScriptBeforeContentLoaded: voiceSession.injectedTrackHook,
|
|
3606
|
+
mediaPlaybackRequiresUserAction: false,
|
|
3607
|
+
allowsInlineMediaPlayback: true,
|
|
3608
|
+
mediaCapturePermissionGrantType: "grant",
|
|
3609
|
+
javaScriptEnabled: true,
|
|
3610
|
+
originWhitelist: ["*"]
|
|
3611
|
+
}
|
|
3612
|
+
) });
|
|
3613
|
+
}
|
|
3614
|
+
var styles20 = StyleSheet20.create({
|
|
3615
|
+
hidden: {
|
|
2617
3616
|
position: "absolute",
|
|
2618
3617
|
width: 1,
|
|
2619
3618
|
height: 1,
|
|
2620
3619
|
top: -100,
|
|
2621
3620
|
left: -100,
|
|
2622
3621
|
opacity: 0
|
|
3622
|
+
},
|
|
3623
|
+
web: { flex: 1, backgroundColor: "transparent" }
|
|
3624
|
+
});
|
|
3625
|
+
|
|
3626
|
+
// src/voice/VoiceMicIndicator.tsx
|
|
3627
|
+
import { useEffect as useEffect15, useReducer as useReducer3, useRef as useRef8 } from "react";
|
|
3628
|
+
import { View as View22, Text as Text17, Animated as Animated2, StyleSheet as StyleSheet21, Platform as Platform15 } from "react-native";
|
|
3629
|
+
import { ada as ada17 } from "@ateam-ai-mobile/core";
|
|
3630
|
+
import { jsx as jsx23, jsxs as jsxs18 } from "react/jsx-runtime";
|
|
3631
|
+
var fontFam12 = Platform15.OS === "ios" ? "SF Pro Rounded" : void 0;
|
|
3632
|
+
function VoiceMicIndicator({ topInset = 8 }) {
|
|
3633
|
+
const [, force] = useReducer3((x) => x + 1, 0);
|
|
3634
|
+
const pulse = useRef8(new Animated2.Value(0.4)).current;
|
|
3635
|
+
useEffect15(() => {
|
|
3636
|
+
const sub = voiceSession.subscribe(() => force());
|
|
3637
|
+
return () => sub.unsubscribe();
|
|
3638
|
+
}, []);
|
|
3639
|
+
const snap = voiceSession.getSnapshot();
|
|
3640
|
+
const live = snap.state === "connecting" || snap.state === "connected";
|
|
3641
|
+
useEffect15(() => {
|
|
3642
|
+
if (!live) return;
|
|
3643
|
+
const loop = Animated2.loop(
|
|
3644
|
+
Animated2.sequence([
|
|
3645
|
+
Animated2.timing(pulse, { toValue: 1, duration: 700, useNativeDriver: true }),
|
|
3646
|
+
Animated2.timing(pulse, { toValue: 0.4, duration: 700, useNativeDriver: true })
|
|
3647
|
+
])
|
|
3648
|
+
);
|
|
3649
|
+
loop.start();
|
|
3650
|
+
return () => loop.stop();
|
|
3651
|
+
}, [live, pulse]);
|
|
3652
|
+
if (!live) return null;
|
|
3653
|
+
const label = snap.state === "connecting" ? "Connecting\u2026" : snap.muted ? "Mic muted" : "Ada is listening";
|
|
3654
|
+
const dotColor = snap.muted ? ada17.inkMuted : "#E5484D";
|
|
3655
|
+
return /* @__PURE__ */ jsx23(View22, { style: [styles21.wrap, { top: topInset }], pointerEvents: "none", children: /* @__PURE__ */ jsxs18(View22, { style: styles21.pill, children: [
|
|
3656
|
+
/* @__PURE__ */ jsx23(Animated2.View, { style: [styles21.dot, { backgroundColor: dotColor, opacity: snap.muted ? 1 : pulse }] }),
|
|
3657
|
+
/* @__PURE__ */ jsx23(Text17, { style: styles21.label, children: label })
|
|
3658
|
+
] }) });
|
|
3659
|
+
}
|
|
3660
|
+
var styles21 = StyleSheet21.create({
|
|
3661
|
+
wrap: {
|
|
3662
|
+
position: "absolute",
|
|
3663
|
+
left: 0,
|
|
3664
|
+
right: 0,
|
|
3665
|
+
alignItems: "center",
|
|
3666
|
+
zIndex: 9999,
|
|
3667
|
+
elevation: 9999
|
|
3668
|
+
},
|
|
3669
|
+
pill: {
|
|
3670
|
+
flexDirection: "row",
|
|
3671
|
+
alignItems: "center",
|
|
3672
|
+
gap: 7,
|
|
3673
|
+
paddingHorizontal: 12,
|
|
3674
|
+
paddingVertical: 6,
|
|
3675
|
+
borderRadius: 999,
|
|
3676
|
+
backgroundColor: ada17.ink,
|
|
3677
|
+
shadowColor: "#000",
|
|
3678
|
+
shadowOpacity: 0.2,
|
|
3679
|
+
shadowRadius: 8,
|
|
3680
|
+
shadowOffset: { width: 0, height: 2 }
|
|
3681
|
+
},
|
|
3682
|
+
dot: {
|
|
3683
|
+
width: 8,
|
|
3684
|
+
height: 8,
|
|
3685
|
+
borderRadius: 4
|
|
3686
|
+
},
|
|
3687
|
+
label: {
|
|
3688
|
+
fontSize: 12,
|
|
3689
|
+
fontFamily: fontFam12,
|
|
3690
|
+
fontWeight: "600",
|
|
3691
|
+
color: ada17.paper,
|
|
3692
|
+
letterSpacing: 0.2
|
|
2623
3693
|
}
|
|
2624
3694
|
});
|
|
2625
3695
|
export {
|
|
@@ -2627,9 +3697,13 @@ export {
|
|
|
2627
3697
|
ChatInput,
|
|
2628
3698
|
DaySeparator,
|
|
2629
3699
|
EmbeddedPluginWebView,
|
|
3700
|
+
HandoffScreen,
|
|
2630
3701
|
JobProgressBar,
|
|
3702
|
+
JoinScreen,
|
|
2631
3703
|
Lightbox,
|
|
3704
|
+
LoginScreen,
|
|
2632
3705
|
MessageBubble,
|
|
3706
|
+
OpenJoinScreen,
|
|
2633
3707
|
Plugin,
|
|
2634
3708
|
PluginSurface,
|
|
2635
3709
|
TableCardBlock,
|
|
@@ -2637,16 +3711,30 @@ export {
|
|
|
2637
3711
|
TypingIndicator,
|
|
2638
3712
|
UIActionsRow,
|
|
2639
3713
|
VoiceCallSheet,
|
|
3714
|
+
VoiceMicIndicator,
|
|
3715
|
+
VoiceSessionHost,
|
|
2640
3716
|
WidgetHost,
|
|
2641
3717
|
WidgetWebView,
|
|
3718
|
+
WorkspaceBanner,
|
|
2642
3719
|
buildPluginBridgeJS,
|
|
3720
|
+
completeInviteJoin,
|
|
3721
|
+
completeOpenJoin,
|
|
3722
|
+
completeProviderSignIn,
|
|
3723
|
+
googleClientIds,
|
|
3724
|
+
handoffErrorMessage,
|
|
3725
|
+
inviteHeadline,
|
|
3726
|
+
openJoinErrorMessage,
|
|
2643
3727
|
openWidget,
|
|
3728
|
+
runHandoffExchange,
|
|
2644
3729
|
setClipboardProvider,
|
|
3730
|
+
setOAuthProvider,
|
|
2645
3731
|
setPluginWebViewExtraHosts,
|
|
2646
3732
|
setUIActionNavigator,
|
|
3733
|
+
shouldRecoverToDefaultTenant,
|
|
2647
3734
|
useLightboxStore,
|
|
2648
3735
|
usePluginBridge,
|
|
2649
3736
|
useVoiceCall,
|
|
2650
|
-
useWidgetHostStore
|
|
3737
|
+
useWidgetHostStore,
|
|
3738
|
+
voiceSession
|
|
2651
3739
|
};
|
|
2652
3740
|
//# sourceMappingURL=index.mjs.map
|