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