@sagepilot-ai/react-native-sdk 0.2.4 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE.md +21 -0
- package/README.md +197 -4
- package/android/build.gradle +13 -4
- package/android/src/main/java/ai/sagepilot/reactnativesdk/SagepilotReactNativeSdkPackage.java +2 -0
- package/dist/index.d.mts +215 -1
- package/dist/index.d.ts +215 -1
- package/dist/index.js +1322 -43
- package/dist/index.mjs +1301 -28
- package/package.json +18 -1
package/dist/index.mjs
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
// src/public/SdkClient.ts
|
|
2
|
+
import { Platform } from "react-native";
|
|
3
|
+
|
|
1
4
|
// src/core/errors/SagepilotChatError.ts
|
|
2
5
|
var SagepilotChatError = class extends Error {
|
|
3
6
|
constructor(code, message, options = {}) {
|
|
@@ -15,7 +18,7 @@ var SagepilotChatError = class extends Error {
|
|
|
15
18
|
|
|
16
19
|
// src/core/config/constants.ts
|
|
17
20
|
var SDK_NAME = "@sagepilot-ai/react-native-sdk";
|
|
18
|
-
var SDK_VERSION = "0.
|
|
21
|
+
var SDK_VERSION = "0.3.0";
|
|
19
22
|
var DEFAULT_HOST = "https://app.sagepilot.ai";
|
|
20
23
|
var DEFAULT_WIDGET_HOST = "https://app.sagepilot.ai";
|
|
21
24
|
var CUSTOMER_API_PREFIX = "/customer-api/v1";
|
|
@@ -324,6 +327,34 @@ async function resolveDeviceInfo(adapter) {
|
|
|
324
327
|
return adapter.getDeviceInfo();
|
|
325
328
|
}
|
|
326
329
|
|
|
330
|
+
// src/core/storage/cache.ts
|
|
331
|
+
function createAsyncStorageCacheStorage(asyncStorage) {
|
|
332
|
+
return {
|
|
333
|
+
getItem: (key) => asyncStorage.getItem(key),
|
|
334
|
+
setItem: (key, value) => asyncStorage.setItem(key, value),
|
|
335
|
+
removeItem: (key) => asyncStorage.removeItem(key)
|
|
336
|
+
};
|
|
337
|
+
}
|
|
338
|
+
function createJsonCache(storage, namespace) {
|
|
339
|
+
return {
|
|
340
|
+
async get(key) {
|
|
341
|
+
const value = await storage.getItem(`${namespace}:${key}`);
|
|
342
|
+
if (!value) return null;
|
|
343
|
+
try {
|
|
344
|
+
return JSON.parse(value);
|
|
345
|
+
} catch {
|
|
346
|
+
return null;
|
|
347
|
+
}
|
|
348
|
+
},
|
|
349
|
+
async set(key, value) {
|
|
350
|
+
await storage.setItem(`${namespace}:${key}`, JSON.stringify(value));
|
|
351
|
+
},
|
|
352
|
+
async remove(key) {
|
|
353
|
+
await storage.removeItem(`${namespace}:${key}`);
|
|
354
|
+
}
|
|
355
|
+
};
|
|
356
|
+
}
|
|
357
|
+
|
|
327
358
|
// src/resources/channels.ts
|
|
328
359
|
function bootstrapChannel(http, host, channelId) {
|
|
329
360
|
return http.request(
|
|
@@ -407,6 +438,9 @@ function fetchUnreadCount(http, host, channelId, sessionId, authorizationHeader)
|
|
|
407
438
|
}
|
|
408
439
|
|
|
409
440
|
// src/public/SdkClient.ts
|
|
441
|
+
var PRESENTATION_CACHE_NAMESPACE = "sagepilot_rn_presentation";
|
|
442
|
+
var PRESENTATION_CACHE_KEY = "state";
|
|
443
|
+
var PRESENTATION_RESTORE_MAX_AGE_MS = 15 * 60 * 1e3;
|
|
410
444
|
var DEFAULT_MOBILE_LAUNCHER_CONFIG = {
|
|
411
445
|
label: "Chat",
|
|
412
446
|
buttonColor: "#173c2d",
|
|
@@ -476,6 +510,10 @@ function readRecordField(input, key) {
|
|
|
476
510
|
if (!value || typeof value !== "object" || Array.isArray(value)) return void 0;
|
|
477
511
|
return value;
|
|
478
512
|
}
|
|
513
|
+
function isFreshPresentationState(updatedAt) {
|
|
514
|
+
if (typeof updatedAt !== "number" || !Number.isFinite(updatedAt)) return false;
|
|
515
|
+
return Date.now() - updatedAt <= PRESENTATION_RESTORE_MAX_AGE_MS;
|
|
516
|
+
}
|
|
479
517
|
function toPublicSessionState(session) {
|
|
480
518
|
return {
|
|
481
519
|
session_id: session.session_id,
|
|
@@ -532,6 +570,9 @@ var SagepilotReactNativeChat = class {
|
|
|
532
570
|
const { workspaceId, channelId } = parseKey(config.key);
|
|
533
571
|
this.runtimeOptions = {
|
|
534
572
|
behavior: config.behavior,
|
|
573
|
+
cacheStorage: config.cacheStorage,
|
|
574
|
+
filePicker: config.filePicker,
|
|
575
|
+
fileStore: config.fileStore,
|
|
535
576
|
presentation: config.presentation,
|
|
536
577
|
theme: config.theme,
|
|
537
578
|
hostedClaimsStorageKeyPrefix: config.hostedClaimsStorageKeyPrefix || DEFAULT_HOSTED_CLAIMS_STORAGE_KEY_PREFIX
|
|
@@ -562,6 +603,7 @@ var SagepilotReactNativeChat = class {
|
|
|
562
603
|
this.channel = channel;
|
|
563
604
|
this.session = session;
|
|
564
605
|
this.isConfigured = true;
|
|
606
|
+
await this.restorePresentationFromCache();
|
|
565
607
|
this.emitReady();
|
|
566
608
|
this.emitState();
|
|
567
609
|
if (config.behavior?.enableUnreadPolling ?? true) {
|
|
@@ -701,12 +743,14 @@ var SagepilotReactNativeChat = class {
|
|
|
701
743
|
this.hostedChatView = { screen: "home" };
|
|
702
744
|
if (this.presented) {
|
|
703
745
|
this.emitState();
|
|
746
|
+
this.persistPresentationState();
|
|
704
747
|
return true;
|
|
705
748
|
}
|
|
706
749
|
this.presented = true;
|
|
707
750
|
this.setUnreadCount(0);
|
|
708
751
|
this.presentCallbacks.forEach((callback) => callback(this.getLifecycleState()));
|
|
709
752
|
this.emitState();
|
|
753
|
+
this.persistPresentationState();
|
|
710
754
|
return true;
|
|
711
755
|
}
|
|
712
756
|
presentMessages() {
|
|
@@ -734,6 +778,7 @@ var SagepilotReactNativeChat = class {
|
|
|
734
778
|
this.emitUnreadChange();
|
|
735
779
|
this.dismissCallbacks.forEach((callback) => callback(this.getLifecycleState()));
|
|
736
780
|
this.emitState();
|
|
781
|
+
this.persistPresentationState();
|
|
737
782
|
return true;
|
|
738
783
|
}
|
|
739
784
|
hide() {
|
|
@@ -868,6 +913,10 @@ var SagepilotReactNativeChat = class {
|
|
|
868
913
|
}
|
|
869
914
|
return this.session?.conversation_id ?? void 0;
|
|
870
915
|
}
|
|
916
|
+
/** Returns the chat id that is explicitly part of the current hosted URL. */
|
|
917
|
+
getHostedRouteChatId() {
|
|
918
|
+
return this.hostedChatView.screen === "composer" ? this.hostedChatView.chatId : void 0;
|
|
919
|
+
}
|
|
871
920
|
getHostedIdentityMessage() {
|
|
872
921
|
if (!this.legacyWidgetJwt) return null;
|
|
873
922
|
return {
|
|
@@ -878,6 +927,34 @@ var SagepilotReactNativeChat = class {
|
|
|
878
927
|
}
|
|
879
928
|
};
|
|
880
929
|
}
|
|
930
|
+
/** Captures the hosted route that should receive a picked attachment batch. */
|
|
931
|
+
getHostedAttachmentTarget() {
|
|
932
|
+
return {
|
|
933
|
+
chatId: this.getActiveHostedChatId(),
|
|
934
|
+
routeChatId: this.getHostedRouteChatId(),
|
|
935
|
+
hostedChatView: this.serializeHostedChatView(this.hostedChatView)
|
|
936
|
+
};
|
|
937
|
+
}
|
|
938
|
+
/** Restores the hosted route for a queued attachment batch before delivery. */
|
|
939
|
+
restoreHostedAttachmentTarget(target) {
|
|
940
|
+
if (Platform.OS !== "android") return false;
|
|
941
|
+
if (!target) return false;
|
|
942
|
+
const hostedChatView = this.readPersistedHostedChatView(target.hostedChatView);
|
|
943
|
+
if (!hostedChatView) return false;
|
|
944
|
+
const currentChatId = this.getActiveHostedChatId();
|
|
945
|
+
const targetChatId = normalizeOptionalString(target.chatId);
|
|
946
|
+
const routeChatId = normalizeOptionalString(target.routeChatId) ?? (hostedChatView.screen === "composer" ? normalizeOptionalString(hostedChatView.chatId) : void 0);
|
|
947
|
+
const nextHostedChatView = routeChatId && hostedChatView.screen === "composer" ? this.withPinnedChatId(hostedChatView, routeChatId) : hostedChatView;
|
|
948
|
+
if (this.presented && (!targetChatId || currentChatId === targetChatId) && this.areHostedChatViewsEqual(this.hostedChatView, nextHostedChatView)) {
|
|
949
|
+
return false;
|
|
950
|
+
}
|
|
951
|
+
this.hostedChatView = nextHostedChatView;
|
|
952
|
+
this.presented = true;
|
|
953
|
+
this.setUnreadCount(0);
|
|
954
|
+
this.emitState();
|
|
955
|
+
this.persistPresentationState();
|
|
956
|
+
return true;
|
|
957
|
+
}
|
|
881
958
|
handleHostedBridgeMessage(message) {
|
|
882
959
|
if (!message) return false;
|
|
883
960
|
if (message.type === "close_widget") {
|
|
@@ -945,6 +1022,7 @@ var SagepilotReactNativeChat = class {
|
|
|
945
1022
|
}
|
|
946
1023
|
destroy() {
|
|
947
1024
|
this.stopUnreadPolling();
|
|
1025
|
+
this.persistPresentationState({ cleanShutdown: true, presented: false });
|
|
948
1026
|
this.resetRuntimeState();
|
|
949
1027
|
this.emitState();
|
|
950
1028
|
this.identifyCallbacks.clear();
|
|
@@ -1100,8 +1178,86 @@ var SagepilotReactNativeChat = class {
|
|
|
1100
1178
|
this.presentCallbacks.forEach((callback) => callback(this.getLifecycleState()));
|
|
1101
1179
|
}
|
|
1102
1180
|
this.emitState();
|
|
1181
|
+
this.persistPresentationState();
|
|
1103
1182
|
return true;
|
|
1104
1183
|
}
|
|
1184
|
+
/** Returns the dedicated cache used for hosted-chat presentation recovery. */
|
|
1185
|
+
getPresentationCache() {
|
|
1186
|
+
const cacheStorage = this.runtimeOptions?.cacheStorage;
|
|
1187
|
+
if (!cacheStorage) return null;
|
|
1188
|
+
return createJsonCache(cacheStorage, PRESENTATION_CACHE_NAMESPACE);
|
|
1189
|
+
}
|
|
1190
|
+
/** Removes callback-only fields so the hosted route can be safely serialized. */
|
|
1191
|
+
serializeHostedChatView(view) {
|
|
1192
|
+
if (view.screen !== "composer") return view;
|
|
1193
|
+
return {
|
|
1194
|
+
screen: "composer",
|
|
1195
|
+
message: view.message,
|
|
1196
|
+
mode: view.mode,
|
|
1197
|
+
chatId: view.chatId,
|
|
1198
|
+
metadata: view.metadata
|
|
1199
|
+
};
|
|
1200
|
+
}
|
|
1201
|
+
/** Validates a persisted hosted view before using it to rebuild a widget URL. */
|
|
1202
|
+
readPersistedHostedChatView(view) {
|
|
1203
|
+
if (!view || typeof view !== "object") return null;
|
|
1204
|
+
const record = view;
|
|
1205
|
+
if (record.screen === "home") return { screen: "home" };
|
|
1206
|
+
if (record.screen === "messages") return { screen: "messages" };
|
|
1207
|
+
if (record.screen !== "composer") return null;
|
|
1208
|
+
const mode = record.mode === "new" ? "new" : "auto";
|
|
1209
|
+
return {
|
|
1210
|
+
screen: "composer",
|
|
1211
|
+
message: typeof record.message === "string" ? record.message : void 0,
|
|
1212
|
+
mode,
|
|
1213
|
+
chatId: typeof record.chatId === "string" && record.chatId ? record.chatId : void 0,
|
|
1214
|
+
metadata: readRecordField(record, "metadata")
|
|
1215
|
+
};
|
|
1216
|
+
}
|
|
1217
|
+
/** Forces a persisted hosted route to target the exact chat that owns a pending attachment. */
|
|
1218
|
+
withPinnedChatId(view, chatId) {
|
|
1219
|
+
if (view.screen === "composer") {
|
|
1220
|
+
return {
|
|
1221
|
+
...view,
|
|
1222
|
+
chatId
|
|
1223
|
+
};
|
|
1224
|
+
}
|
|
1225
|
+
return {
|
|
1226
|
+
screen: "composer",
|
|
1227
|
+
mode: "auto",
|
|
1228
|
+
chatId
|
|
1229
|
+
};
|
|
1230
|
+
}
|
|
1231
|
+
/** Compares hosted route state without relying on object identity. */
|
|
1232
|
+
areHostedChatViewsEqual(first, second) {
|
|
1233
|
+
return JSON.stringify(this.serializeHostedChatView(first)) === JSON.stringify(this.serializeHostedChatView(second));
|
|
1234
|
+
}
|
|
1235
|
+
/** Persists presentation state without blocking customer-facing SDK methods. */
|
|
1236
|
+
persistPresentationState(options) {
|
|
1237
|
+
const cache = this.getPresentationCache();
|
|
1238
|
+
if (!cache) return;
|
|
1239
|
+
const state = {
|
|
1240
|
+
presented: options?.presented ?? this.presented,
|
|
1241
|
+
hostedChatView: this.serializeHostedChatView(this.hostedChatView),
|
|
1242
|
+
updatedAt: Date.now(),
|
|
1243
|
+
cleanShutdown: options?.cleanShutdown
|
|
1244
|
+
};
|
|
1245
|
+
void cache.set(PRESENTATION_CACHE_KEY, state).catch(() => void 0);
|
|
1246
|
+
}
|
|
1247
|
+
/** Restores Android presentation state after configure without resetting the hosted route to Home. */
|
|
1248
|
+
async restorePresentationFromCache() {
|
|
1249
|
+
const cache = this.getPresentationCache();
|
|
1250
|
+
if (!cache || Platform.OS !== "android") return;
|
|
1251
|
+
const persisted = await cache.get(PRESENTATION_CACHE_KEY).catch(() => null);
|
|
1252
|
+
if (persisted?.cleanShutdown) return;
|
|
1253
|
+
if (!isFreshPresentationState(persisted?.updatedAt)) return;
|
|
1254
|
+
if (!persisted?.presented) return;
|
|
1255
|
+
const hostedChatView = this.readPersistedHostedChatView(persisted.hostedChatView);
|
|
1256
|
+
if (!hostedChatView) return;
|
|
1257
|
+
this.hostedChatView = hostedChatView;
|
|
1258
|
+
this.presented = true;
|
|
1259
|
+
this.setUnreadCount(0);
|
|
1260
|
+
}
|
|
1105
1261
|
};
|
|
1106
1262
|
var internalSagepilotChat = new SagepilotReactNativeChat();
|
|
1107
1263
|
var SagepilotChat = {
|
|
@@ -1138,22 +1294,337 @@ var SagepilotChat = {
|
|
|
1138
1294
|
destroy: () => internalSagepilotChat.destroy()
|
|
1139
1295
|
};
|
|
1140
1296
|
|
|
1141
|
-
// src/core/storage/
|
|
1142
|
-
|
|
1297
|
+
// src/core/storage/fileStore.ts
|
|
1298
|
+
var DEFAULT_DIRECTORY_NAME = "sagepilot-attachments";
|
|
1299
|
+
function sanitizeKey(key) {
|
|
1300
|
+
return key.replace(/[^a-zA-Z0-9._-]/g, "_").replace(/\.{2,}/g, "_");
|
|
1301
|
+
}
|
|
1302
|
+
function createSagepilotFileStore(blobUtil, options = {}) {
|
|
1303
|
+
const directory = `${blobUtil.fs.dirs.DocumentDir}/${options.directoryName ?? DEFAULT_DIRECTORY_NAME}`;
|
|
1304
|
+
const pathFor = (key) => `${directory}/${sanitizeKey(key)}`;
|
|
1305
|
+
let dirReady = null;
|
|
1306
|
+
function ensureDirectory() {
|
|
1307
|
+
if (!dirReady) {
|
|
1308
|
+
dirReady = (async () => {
|
|
1309
|
+
if (!await blobUtil.fs.exists(directory)) {
|
|
1310
|
+
await blobUtil.fs.mkdir(directory);
|
|
1311
|
+
}
|
|
1312
|
+
})().catch((error) => {
|
|
1313
|
+
dirReady = null;
|
|
1314
|
+
throw error;
|
|
1315
|
+
});
|
|
1316
|
+
}
|
|
1317
|
+
return dirReady;
|
|
1318
|
+
}
|
|
1143
1319
|
return {
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1320
|
+
async write(key, base64) {
|
|
1321
|
+
await ensureDirectory();
|
|
1322
|
+
await blobUtil.fs.writeFile(pathFor(key), base64, "base64");
|
|
1323
|
+
},
|
|
1324
|
+
async read(key) {
|
|
1325
|
+
const content = await blobUtil.fs.readFile(pathFor(key), "base64");
|
|
1326
|
+
if (typeof content !== "string" || content.length === 0) {
|
|
1327
|
+
throw new Error("Persisted attachment is empty or unreadable.");
|
|
1328
|
+
}
|
|
1329
|
+
return content;
|
|
1330
|
+
},
|
|
1331
|
+
async remove(key) {
|
|
1332
|
+
try {
|
|
1333
|
+
if (await blobUtil.fs.exists(pathFor(key))) {
|
|
1334
|
+
await blobUtil.fs.unlink(pathFor(key));
|
|
1335
|
+
}
|
|
1336
|
+
} catch {
|
|
1337
|
+
}
|
|
1338
|
+
},
|
|
1339
|
+
async prune(keepKeys) {
|
|
1340
|
+
try {
|
|
1341
|
+
if (!await blobUtil.fs.exists(directory)) return;
|
|
1342
|
+
const keep = new Set(keepKeys.map(sanitizeKey));
|
|
1343
|
+
const entries = await blobUtil.fs.ls(directory);
|
|
1344
|
+
await Promise.all(
|
|
1345
|
+
entries.filter((entry) => !keep.has(entry)).map((entry) => blobUtil.fs.unlink(`${directory}/${entry}`).catch(() => void 0))
|
|
1346
|
+
);
|
|
1347
|
+
} catch {
|
|
1348
|
+
}
|
|
1349
|
+
}
|
|
1350
|
+
};
|
|
1351
|
+
}
|
|
1352
|
+
|
|
1353
|
+
// src/core/native/filePicker.ts
|
|
1354
|
+
import { Alert, Linking, PermissionsAndroid, Platform as Platform2 } from "react-native";
|
|
1355
|
+
|
|
1356
|
+
// src/core/native/filePickerShared.ts
|
|
1357
|
+
var SagepilotFilePickerError = class extends Error {
|
|
1358
|
+
/** Creates a typed picker error that can be surfaced over the widget bridge. */
|
|
1359
|
+
constructor(code, message) {
|
|
1360
|
+
super(message);
|
|
1361
|
+
this.name = "SagepilotFilePickerError";
|
|
1362
|
+
this.code = code;
|
|
1363
|
+
}
|
|
1364
|
+
};
|
|
1365
|
+
var SAGEPILOT_DEFAULT_IMAGE_MAX_DIMENSION = 1920;
|
|
1366
|
+
var SAGEPILOT_DEFAULT_IMAGE_QUALITY = 0.8;
|
|
1367
|
+
var SAGEPILOT_DEFAULT_IMAGE_MAX_FILE_SIZE_BYTES = 15 * 1024 * 1024;
|
|
1368
|
+
|
|
1369
|
+
// src/core/native/filePicker.ts
|
|
1370
|
+
var DEBUG_PREFIX = "[SagepilotSDK][FilePicker]";
|
|
1371
|
+
var DEFAULT_IMAGE_MAX_DIMENSION = SAGEPILOT_DEFAULT_IMAGE_MAX_DIMENSION;
|
|
1372
|
+
var DEFAULT_IMAGE_QUALITY = SAGEPILOT_DEFAULT_IMAGE_QUALITY;
|
|
1373
|
+
var DEFAULT_IMAGE_MIME_TYPE = "image/jpeg";
|
|
1374
|
+
var DEFAULT_DOCUMENT_MIME_TYPE = "application/octet-stream";
|
|
1375
|
+
var DEFAULT_IMAGE_SELECTION_LIMIT = 5;
|
|
1376
|
+
var DEFAULT_DOCUMENT_MAX_FILE_SIZE_BYTES = 20 * 1024 * 1024;
|
|
1377
|
+
var DEFAULT_IMAGE_MAX_FILE_SIZE_BYTES = SAGEPILOT_DEFAULT_IMAGE_MAX_FILE_SIZE_BYTES;
|
|
1378
|
+
function bytesToMb(bytes) {
|
|
1379
|
+
return Math.round(bytes / (1024 * 1024) * 10) / 10;
|
|
1380
|
+
}
|
|
1381
|
+
function estimateBase64ByteSize(dataBase64) {
|
|
1382
|
+
return Math.floor(dataBase64.length * 3 / 4);
|
|
1383
|
+
}
|
|
1384
|
+
function stripFileScheme(uri) {
|
|
1385
|
+
return uri.startsWith("file://") ? uri.replace("file://", "") : uri;
|
|
1386
|
+
}
|
|
1387
|
+
function debugFilePicker(message, details) {
|
|
1388
|
+
console.log(`${DEBUG_PREFIX} ${message}`, details ?? "");
|
|
1389
|
+
}
|
|
1390
|
+
async function promptOpenSagepilotCameraSettings() {
|
|
1391
|
+
if (Platform2.OS !== "android") return;
|
|
1392
|
+
await new Promise((resolve) => {
|
|
1393
|
+
Alert.alert(
|
|
1394
|
+
"Camera access is off",
|
|
1395
|
+
"Enable camera access for this app in Settings to take a photo.",
|
|
1396
|
+
[
|
|
1397
|
+
{
|
|
1398
|
+
text: "Not now",
|
|
1399
|
+
style: "cancel",
|
|
1400
|
+
onPress: resolve
|
|
1401
|
+
},
|
|
1402
|
+
{
|
|
1403
|
+
text: "Open Settings",
|
|
1404
|
+
onPress: () => {
|
|
1405
|
+
Linking.openSettings().finally(resolve);
|
|
1406
|
+
}
|
|
1407
|
+
}
|
|
1408
|
+
]
|
|
1409
|
+
);
|
|
1410
|
+
});
|
|
1411
|
+
}
|
|
1412
|
+
async function ensureSagepilotAndroidCameraPermission() {
|
|
1413
|
+
if (Platform2.OS !== "android") return;
|
|
1414
|
+
let result;
|
|
1415
|
+
try {
|
|
1416
|
+
debugFilePicker("requesting Android camera permission");
|
|
1417
|
+
result = await PermissionsAndroid.request(PermissionsAndroid.PERMISSIONS.CAMERA);
|
|
1418
|
+
} catch {
|
|
1419
|
+
debugFilePicker("camera permission request threw; continuing to native launch");
|
|
1420
|
+
return;
|
|
1421
|
+
}
|
|
1422
|
+
debugFilePicker("Android camera permission result", { result });
|
|
1423
|
+
if (result === PermissionsAndroid.RESULTS.NEVER_ASK_AGAIN) {
|
|
1424
|
+
await promptOpenSagepilotCameraSettings();
|
|
1425
|
+
throw new SagepilotFilePickerError(
|
|
1426
|
+
"permission_denied",
|
|
1427
|
+
"Camera access is turned off. Enable it for this app in Settings to take a photo."
|
|
1428
|
+
);
|
|
1429
|
+
}
|
|
1430
|
+
if (result === PermissionsAndroid.RESULTS.DENIED) {
|
|
1431
|
+
throw new SagepilotFilePickerError(
|
|
1432
|
+
"permission_denied",
|
|
1433
|
+
"Camera permission is required to take a photo."
|
|
1434
|
+
);
|
|
1435
|
+
}
|
|
1436
|
+
}
|
|
1437
|
+
function canUseCameraSource(imagePicker) {
|
|
1438
|
+
return Boolean(imagePicker);
|
|
1439
|
+
}
|
|
1440
|
+
function mapImagePickerErrorCode(errorCode) {
|
|
1441
|
+
switch (errorCode) {
|
|
1442
|
+
case "permission":
|
|
1443
|
+
return "permission_denied";
|
|
1444
|
+
case "camera_unavailable":
|
|
1445
|
+
return "camera_unavailable";
|
|
1446
|
+
default:
|
|
1447
|
+
return "unknown";
|
|
1448
|
+
}
|
|
1449
|
+
}
|
|
1450
|
+
function imageAssetsToPickedFiles(result, maxFileSizeBytes) {
|
|
1451
|
+
if (result.didCancel) return [];
|
|
1452
|
+
if (result.errorCode) {
|
|
1453
|
+
throw new SagepilotFilePickerError(
|
|
1454
|
+
mapImagePickerErrorCode(result.errorCode),
|
|
1455
|
+
result.errorMessage || `Could not capture the photo (${result.errorCode}).`
|
|
1456
|
+
);
|
|
1457
|
+
}
|
|
1458
|
+
const assets = result.assets ?? [];
|
|
1459
|
+
const usable = assets.filter((asset) => typeof asset.base64 === "string" && asset.base64.length > 0);
|
|
1460
|
+
if (assets.length > usable.length) {
|
|
1461
|
+
throw new SagepilotFilePickerError(
|
|
1462
|
+
"encode_failed",
|
|
1463
|
+
assets.length === 1 ? "The photo could not be processed. Please try again." : "Some photos could not be processed. Please try selecting them again."
|
|
1464
|
+
);
|
|
1465
|
+
}
|
|
1466
|
+
return usable.map((asset, index) => {
|
|
1467
|
+
const dataBase64 = asset.base64;
|
|
1468
|
+
const size = asset.fileSize ?? estimateBase64ByteSize(dataBase64);
|
|
1469
|
+
if (maxFileSizeBytes > 0 && size > maxFileSizeBytes) {
|
|
1470
|
+
throw new SagepilotFilePickerError(
|
|
1471
|
+
"file_too_large",
|
|
1472
|
+
`Image is too large (max ${bytesToMb(maxFileSizeBytes)}MB).`
|
|
1473
|
+
);
|
|
1474
|
+
}
|
|
1475
|
+
return {
|
|
1476
|
+
file_name: asset.fileName || `photo-${Date.now()}-${index + 1}.jpg`,
|
|
1477
|
+
mime_type: asset.type || DEFAULT_IMAGE_MIME_TYPE,
|
|
1478
|
+
size,
|
|
1479
|
+
data_base64: dataBase64
|
|
1480
|
+
};
|
|
1481
|
+
});
|
|
1482
|
+
}
|
|
1483
|
+
async function readUriAsBase64(uri, fileReader) {
|
|
1484
|
+
if (fileReader) {
|
|
1485
|
+
const content = await fileReader.fs.readFile(stripFileScheme(uri), "base64");
|
|
1486
|
+
if (typeof content !== "string") {
|
|
1487
|
+
throw new Error("File reader returned unexpected content.");
|
|
1488
|
+
}
|
|
1489
|
+
return content;
|
|
1490
|
+
}
|
|
1491
|
+
const response = await fetch(uri);
|
|
1492
|
+
const blob = await response.blob();
|
|
1493
|
+
return new Promise((resolve, reject) => {
|
|
1494
|
+
const reader = new FileReader();
|
|
1495
|
+
reader.onload = () => {
|
|
1496
|
+
const dataUrl = typeof reader.result === "string" ? reader.result : "";
|
|
1497
|
+
const [, payload = ""] = dataUrl.split(",");
|
|
1498
|
+
if (!payload) {
|
|
1499
|
+
reject(new Error("Could not read the selected file."));
|
|
1500
|
+
return;
|
|
1501
|
+
}
|
|
1502
|
+
resolve(payload);
|
|
1503
|
+
};
|
|
1504
|
+
reader.onerror = () => reject(new Error("Could not read the selected file."));
|
|
1505
|
+
reader.readAsDataURL(blob);
|
|
1506
|
+
});
|
|
1507
|
+
}
|
|
1508
|
+
function createSagepilotFilePicker(options) {
|
|
1509
|
+
const { imagePicker, documentsPicker, fileReader } = options;
|
|
1510
|
+
const imageMaxDimension = options.imageMaxDimension ?? DEFAULT_IMAGE_MAX_DIMENSION;
|
|
1511
|
+
const imageQuality = options.imageQuality ?? DEFAULT_IMAGE_QUALITY;
|
|
1512
|
+
const imageSelectionLimit = options.imageSelectionLimit ?? DEFAULT_IMAGE_SELECTION_LIMIT;
|
|
1513
|
+
const documentMaxFileSizeBytes = options.documentMaxFileSizeBytes ?? DEFAULT_DOCUMENT_MAX_FILE_SIZE_BYTES;
|
|
1514
|
+
const imageMaxFileSizeBytes = options.imageMaxFileSizeBytes ?? DEFAULT_IMAGE_MAX_FILE_SIZE_BYTES;
|
|
1515
|
+
const sources = [];
|
|
1516
|
+
if (canUseCameraSource(imagePicker)) sources.push("camera");
|
|
1517
|
+
if (imagePicker) sources.push("library");
|
|
1518
|
+
if (documentsPicker) sources.push("documents");
|
|
1519
|
+
debugFilePicker("adapter created", {
|
|
1520
|
+
platform: Platform2.OS,
|
|
1521
|
+
hasImagePicker: Boolean(imagePicker),
|
|
1522
|
+
hasDocumentsPicker: Boolean(documentsPicker),
|
|
1523
|
+
hasFileReader: Boolean(fileReader),
|
|
1524
|
+
sources
|
|
1525
|
+
});
|
|
1526
|
+
if (sources.length === 0) return void 0;
|
|
1527
|
+
const imageOptions = {
|
|
1528
|
+
mediaType: "photo",
|
|
1529
|
+
includeBase64: true,
|
|
1530
|
+
maxWidth: imageMaxDimension,
|
|
1531
|
+
maxHeight: imageMaxDimension,
|
|
1532
|
+
quality: imageQuality,
|
|
1533
|
+
saveToPhotos: false
|
|
1534
|
+
};
|
|
1535
|
+
async function pickFromCamera() {
|
|
1536
|
+
debugFilePicker("camera pick requested", {
|
|
1537
|
+
platform: Platform2.OS,
|
|
1538
|
+
imageMaxDimension,
|
|
1539
|
+
imageQuality,
|
|
1540
|
+
imageMaxFileSizeBytes
|
|
1541
|
+
});
|
|
1542
|
+
await ensureSagepilotAndroidCameraPermission();
|
|
1543
|
+
if (!imagePicker) {
|
|
1544
|
+
throw new SagepilotFilePickerError("camera_unavailable", "The camera picker is not available in this build.");
|
|
1545
|
+
}
|
|
1546
|
+
debugFilePicker("launching image-picker camera", { platform: Platform2.OS });
|
|
1547
|
+
return imageAssetsToPickedFiles(await imagePicker.launchCamera(imageOptions), imageMaxFileSizeBytes);
|
|
1548
|
+
}
|
|
1549
|
+
async function pickFromLibrary(multiple) {
|
|
1550
|
+
if (!imagePicker) return [];
|
|
1551
|
+
debugFilePicker("library pick requested", { multiple, imageSelectionLimit });
|
|
1552
|
+
return imageAssetsToPickedFiles(await imagePicker.launchImageLibrary({
|
|
1553
|
+
...imageOptions,
|
|
1554
|
+
// Bounded multi-select: 0 (unlimited) lets a huge batch OOM the bridge.
|
|
1555
|
+
selectionLimit: multiple ? Math.max(0, imageSelectionLimit) : 1
|
|
1556
|
+
}), imageMaxFileSizeBytes);
|
|
1557
|
+
}
|
|
1558
|
+
async function pickDocuments(multiple) {
|
|
1559
|
+
if (!documentsPicker) return [];
|
|
1560
|
+
debugFilePicker("document pick requested", { multiple, documentMaxFileSizeBytes });
|
|
1561
|
+
let picked;
|
|
1562
|
+
try {
|
|
1563
|
+
picked = await documentsPicker.pick({ allowMultiSelection: multiple });
|
|
1564
|
+
} catch (error) {
|
|
1565
|
+
if (error && typeof error === "object" && error.code === "OPERATION_CANCELED") {
|
|
1566
|
+
return [];
|
|
1567
|
+
}
|
|
1568
|
+
throw error;
|
|
1569
|
+
}
|
|
1570
|
+
const files = [];
|
|
1571
|
+
for (const file of picked) {
|
|
1572
|
+
if (documentMaxFileSizeBytes > 0 && typeof file.size === "number" && file.size > documentMaxFileSizeBytes) {
|
|
1573
|
+
throw new SagepilotFilePickerError(
|
|
1574
|
+
"file_too_large",
|
|
1575
|
+
`"${file.name || "File"}" is too large (max ${bytesToMb(documentMaxFileSizeBytes)}MB).`
|
|
1576
|
+
);
|
|
1577
|
+
}
|
|
1578
|
+
let readableUri = file.uri;
|
|
1579
|
+
if (documentsPicker.keepLocalCopy) {
|
|
1580
|
+
const fileName = file.name || `file-${Date.now()}`;
|
|
1581
|
+
const [copy] = await documentsPicker.keepLocalCopy({
|
|
1582
|
+
files: [{ uri: file.uri, fileName }],
|
|
1583
|
+
destination: "cachesDirectory"
|
|
1584
|
+
});
|
|
1585
|
+
if (copy?.status === "success" && copy.localUri) {
|
|
1586
|
+
readableUri = copy.localUri;
|
|
1587
|
+
}
|
|
1588
|
+
}
|
|
1589
|
+
let dataBase64;
|
|
1590
|
+
try {
|
|
1591
|
+
dataBase64 = await readUriAsBase64(readableUri, fileReader);
|
|
1592
|
+
} catch (error) {
|
|
1593
|
+
throw new SagepilotFilePickerError(
|
|
1594
|
+
"read_failed",
|
|
1595
|
+
error instanceof Error && error.message ? error.message : "Could not read the selected file."
|
|
1596
|
+
);
|
|
1597
|
+
}
|
|
1598
|
+
files.push({
|
|
1599
|
+
file_name: file.name || `file-${Date.now()}`,
|
|
1600
|
+
mime_type: file.type || DEFAULT_DOCUMENT_MIME_TYPE,
|
|
1601
|
+
size: file.size ?? estimateBase64ByteSize(dataBase64),
|
|
1602
|
+
data_base64: dataBase64
|
|
1603
|
+
});
|
|
1604
|
+
}
|
|
1605
|
+
return files;
|
|
1606
|
+
}
|
|
1607
|
+
return {
|
|
1608
|
+
sources,
|
|
1609
|
+
async pickFiles(request) {
|
|
1610
|
+
debugFilePicker("pickFiles request", request);
|
|
1611
|
+
if (request.source === "camera") return pickFromCamera();
|
|
1612
|
+
if (request.source === "library") return pickFromLibrary(request.multiple);
|
|
1613
|
+
return pickDocuments(request.multiple);
|
|
1614
|
+
}
|
|
1147
1615
|
};
|
|
1148
1616
|
}
|
|
1149
1617
|
|
|
1150
1618
|
// src/ui/SagepilotChatProvider.ts
|
|
1151
1619
|
import { createElement, useCallback, useEffect, useRef, useState } from "react";
|
|
1620
|
+
import { Camera, FileText, Images, X } from "lucide-react-native";
|
|
1152
1621
|
import {
|
|
1153
1622
|
ActivityIndicator,
|
|
1623
|
+
AppState,
|
|
1154
1624
|
KeyboardAvoidingView,
|
|
1625
|
+
Linking as Linking2,
|
|
1155
1626
|
Modal,
|
|
1156
|
-
Platform,
|
|
1627
|
+
Platform as Platform3,
|
|
1157
1628
|
Pressable,
|
|
1158
1629
|
SafeAreaView,
|
|
1159
1630
|
StyleSheet,
|
|
@@ -1163,6 +1634,40 @@ import {
|
|
|
1163
1634
|
import { WebView } from "react-native-webview";
|
|
1164
1635
|
|
|
1165
1636
|
// src/core/webview/mobileBridge.ts
|
|
1637
|
+
var FILE_PICKER_PROTOCOL_VERSION = 2;
|
|
1638
|
+
function buildBridgeCapabilitiesPayload(capabilities) {
|
|
1639
|
+
return { ...capabilities, filePickerProtocol: FILE_PICKER_PROTOCOL_VERSION };
|
|
1640
|
+
}
|
|
1641
|
+
function buildBridgeCapabilitiesScript(capabilities) {
|
|
1642
|
+
const payload = buildBridgeCapabilitiesPayload(capabilities);
|
|
1643
|
+
return [
|
|
1644
|
+
"(function(){",
|
|
1645
|
+
"try {",
|
|
1646
|
+
"if (window.SagepilotMobileBridge) {",
|
|
1647
|
+
`window.SagepilotMobileBridge.capabilities = Object.assign({}, window.SagepilotMobileBridge.capabilities, ${JSON.stringify(payload)});`,
|
|
1648
|
+
"}",
|
|
1649
|
+
"} catch (_) {}",
|
|
1650
|
+
"true;",
|
|
1651
|
+
"})();"
|
|
1652
|
+
].join("\n");
|
|
1653
|
+
}
|
|
1654
|
+
function buildLivenessPingScript(nonce, staleMs) {
|
|
1655
|
+
return [
|
|
1656
|
+
"(function(){",
|
|
1657
|
+
"try {",
|
|
1658
|
+
"var hb = window.__sagepilotWidgetHeartbeat;",
|
|
1659
|
+
"var supports = typeof hb === 'number';",
|
|
1660
|
+
`var alive = !supports ? true : (Date.now() - hb < ${Math.max(0, Math.floor(staleMs))});`,
|
|
1661
|
+
"var send = function(){",
|
|
1662
|
+
" var msg = JSON.stringify({ type: 'sagepilot:pong', nonce: " + JSON.stringify(nonce) + ", alive: alive, supportsHeartbeat: supports });",
|
|
1663
|
+
" if (window.ReactNativeWebView && window.ReactNativeWebView.postMessage) { window.ReactNativeWebView.postMessage(msg); }",
|
|
1664
|
+
"};",
|
|
1665
|
+
"send();",
|
|
1666
|
+
"} catch (_) {}",
|
|
1667
|
+
"true;",
|
|
1668
|
+
"})();"
|
|
1669
|
+
].join("\n");
|
|
1670
|
+
}
|
|
1166
1671
|
function isHostedBridgeMessage(value) {
|
|
1167
1672
|
if (!value || typeof value !== "object") return false;
|
|
1168
1673
|
const message = value;
|
|
@@ -1183,9 +1688,23 @@ function parseHostedBridgeMessage(rawData) {
|
|
|
1183
1688
|
return null;
|
|
1184
1689
|
}
|
|
1185
1690
|
}
|
|
1186
|
-
|
|
1691
|
+
function buildMobileWebViewBridgeScript(capabilities) {
|
|
1692
|
+
const payload = buildBridgeCapabilitiesPayload(capabilities);
|
|
1693
|
+
return `
|
|
1187
1694
|
(function () {
|
|
1188
|
-
|
|
1695
|
+
var bridgeCapabilities = ${JSON.stringify(payload)};
|
|
1696
|
+
var applyBridgeCapabilities = function () {
|
|
1697
|
+
try {
|
|
1698
|
+
if (window.SagepilotMobileBridge) {
|
|
1699
|
+
window.SagepilotMobileBridge.capabilities = Object.assign({}, window.SagepilotMobileBridge.capabilities, bridgeCapabilities);
|
|
1700
|
+
}
|
|
1701
|
+
} catch (error) {}
|
|
1702
|
+
};
|
|
1703
|
+
|
|
1704
|
+
if (window.__sagepilotRnBridgeInstalled) {
|
|
1705
|
+
applyBridgeCapabilities();
|
|
1706
|
+
return true;
|
|
1707
|
+
}
|
|
1189
1708
|
window.__sagepilotRnBridgeInstalled = true;
|
|
1190
1709
|
|
|
1191
1710
|
var ensureViewport = function () {
|
|
@@ -1226,9 +1745,11 @@ var mobileWebViewBridgeScript = `
|
|
|
1226
1745
|
};
|
|
1227
1746
|
|
|
1228
1747
|
window.SagepilotMobileBridge = {
|
|
1748
|
+
capabilities: bridgeCapabilities,
|
|
1229
1749
|
postMessage: sendToReactNative,
|
|
1230
1750
|
ready: function () { sendToReactNative({ type: "sagepilot:ready" }); }
|
|
1231
1751
|
};
|
|
1752
|
+
applyBridgeCapabilities();
|
|
1232
1753
|
|
|
1233
1754
|
document.addEventListener("click", function (event) {
|
|
1234
1755
|
try {
|
|
@@ -1269,6 +1790,7 @@ var mobileWebViewBridgeScript = `
|
|
|
1269
1790
|
return true;
|
|
1270
1791
|
})();
|
|
1271
1792
|
`;
|
|
1793
|
+
}
|
|
1272
1794
|
|
|
1273
1795
|
// src/specs/SagepilotInsetsViewNativeComponent.ts
|
|
1274
1796
|
import codegenNativeComponent from "react-native/Libraries/Utilities/codegenNativeComponent";
|
|
@@ -1285,12 +1807,133 @@ function readPresentationState() {
|
|
|
1285
1807
|
presentation: internalSagepilotChat.getConfig()?.presentation
|
|
1286
1808
|
};
|
|
1287
1809
|
}
|
|
1810
|
+
function getBridgeCapabilities() {
|
|
1811
|
+
return {
|
|
1812
|
+
nativeFilePicker: Boolean(internalSagepilotChat.getConfig()?.filePicker)
|
|
1813
|
+
};
|
|
1814
|
+
}
|
|
1288
1815
|
function getInjectedWebViewScript() {
|
|
1816
|
+
const capabilities = getBridgeCapabilities();
|
|
1289
1817
|
return [
|
|
1290
1818
|
internalSagepilotChat.getHostedAuthScript(),
|
|
1291
|
-
|
|
1819
|
+
buildMobileWebViewBridgeScript(capabilities),
|
|
1820
|
+
buildBridgeCapabilitiesScript(capabilities)
|
|
1292
1821
|
].filter(Boolean).join("\n");
|
|
1293
1822
|
}
|
|
1823
|
+
var FILE_PICKER_SOURCE_LABELS = {
|
|
1824
|
+
camera: "Take photo",
|
|
1825
|
+
library: "Choose from gallery",
|
|
1826
|
+
documents: "Browse files"
|
|
1827
|
+
};
|
|
1828
|
+
var FILE_PICKER_SOURCE_DESCRIPTIONS = {
|
|
1829
|
+
camera: "Use the in-app camera",
|
|
1830
|
+
library: "Photos and videos on this device",
|
|
1831
|
+
documents: "PDFs, documents, and Drive files"
|
|
1832
|
+
};
|
|
1833
|
+
var FILE_PICKER_SOURCE_ICONS = {
|
|
1834
|
+
camera: Camera,
|
|
1835
|
+
library: Images,
|
|
1836
|
+
documents: FileText
|
|
1837
|
+
};
|
|
1838
|
+
var FILE_PICKER_SOURCE_ICON_COLORS = {
|
|
1839
|
+
camera: "#0284c7",
|
|
1840
|
+
library: "#16a34a",
|
|
1841
|
+
documents: "#7c3aed"
|
|
1842
|
+
};
|
|
1843
|
+
var DEBUG_PREFIX2 = "[SagepilotSDK][Provider]";
|
|
1844
|
+
var DELIVERY_RETRY_MS = 1500;
|
|
1845
|
+
var DELIVERY_LEGACY_FALLBACK_ATTEMPTS = 2;
|
|
1846
|
+
var DELIVERY_MAX_ATTEMPTS = 8;
|
|
1847
|
+
var PERSIST_MAX_TOTAL_BYTES = 2 * 1024 * 1024;
|
|
1848
|
+
var PENDING_FILES_CACHE_NAMESPACE = "sagepilot_rn_picker";
|
|
1849
|
+
var PENDING_FILES_CACHE_KEY = "pending_batch";
|
|
1850
|
+
var LIVENESS_HEARTBEAT_STALE_MS = 4e3;
|
|
1851
|
+
var LIVENESS_PONG_TIMEOUT_MS = 1500;
|
|
1852
|
+
var LIVENESS_MAX_PING_ATTEMPTS = 2;
|
|
1853
|
+
var LIVENESS_PICKER_GRACE_MS = 8e3;
|
|
1854
|
+
var LIVENESS_MAX_REMOUNTS = 2;
|
|
1855
|
+
var batchSequence = 0;
|
|
1856
|
+
function nextBatchId() {
|
|
1857
|
+
batchSequence += 1;
|
|
1858
|
+
const entropy = Math.random().toString(36).slice(2, 8);
|
|
1859
|
+
return `b_${Date.now().toString(36)}_${batchSequence}_${entropy}`;
|
|
1860
|
+
}
|
|
1861
|
+
function totalBase64Bytes(files) {
|
|
1862
|
+
return files.reduce((sum, file) => sum + (file.data_base64?.length ?? 0), 0);
|
|
1863
|
+
}
|
|
1864
|
+
function storageKeyFor(batchId, index) {
|
|
1865
|
+
return `${batchId}_${index}.bin`;
|
|
1866
|
+
}
|
|
1867
|
+
function renderFilePickerSourceIcon(source) {
|
|
1868
|
+
const Icon = FILE_PICKER_SOURCE_ICONS[source];
|
|
1869
|
+
return createElement(
|
|
1870
|
+
View,
|
|
1871
|
+
{ style: getFilePickerSourceIconStyle(source) },
|
|
1872
|
+
createElement(Icon, {
|
|
1873
|
+
color: FILE_PICKER_SOURCE_ICON_COLORS[source],
|
|
1874
|
+
size: 23,
|
|
1875
|
+
strokeWidth: 2.35
|
|
1876
|
+
})
|
|
1877
|
+
);
|
|
1878
|
+
}
|
|
1879
|
+
function getFilePickerSourceIconStyle(source) {
|
|
1880
|
+
if (source === "camera") return [styles.sheetOptionIcon, styles.cameraOptionIcon];
|
|
1881
|
+
if (source === "library") return [styles.sheetOptionIcon, styles.libraryOptionIcon];
|
|
1882
|
+
return [styles.sheetOptionIcon, styles.documentsOptionIcon];
|
|
1883
|
+
}
|
|
1884
|
+
function getSheetOptionStyle(pressed) {
|
|
1885
|
+
return [styles.sheetOption, pressed && styles.sheetOptionPressed];
|
|
1886
|
+
}
|
|
1887
|
+
function getSheetCancelStyle(pressed) {
|
|
1888
|
+
return [styles.sheetCancelButton, pressed && styles.sheetCancelButtonPressed];
|
|
1889
|
+
}
|
|
1890
|
+
function buildDispatchScript(messageLiteral) {
|
|
1891
|
+
return [
|
|
1892
|
+
"(function(){",
|
|
1893
|
+
"try {",
|
|
1894
|
+
`var message = ${messageLiteral};`,
|
|
1895
|
+
"window.dispatchEvent(new MessageEvent('message', { data: message, origin: window.location.origin, source: window.parent || window }));",
|
|
1896
|
+
"} catch (_) {}",
|
|
1897
|
+
"true;",
|
|
1898
|
+
"})();"
|
|
1899
|
+
].join("\n");
|
|
1900
|
+
}
|
|
1901
|
+
function buildFilesPickedScript(batch) {
|
|
1902
|
+
return buildDispatchScript(JSON.stringify({
|
|
1903
|
+
type: "sagepilot:files_picked",
|
|
1904
|
+
batch_id: batch.batchId,
|
|
1905
|
+
chat_id: batch.target?.chatId,
|
|
1906
|
+
conversation_id: batch.target?.chatId,
|
|
1907
|
+
files: batch.files
|
|
1908
|
+
}));
|
|
1909
|
+
}
|
|
1910
|
+
function buildFilePickerErrorScript(message, code) {
|
|
1911
|
+
return buildDispatchScript(
|
|
1912
|
+
`{ type: "sagepilot:file_picker_error", message: ${JSON.stringify(message)}${code ? `, code: ${JSON.stringify(code)}` : ""} }`
|
|
1913
|
+
);
|
|
1914
|
+
}
|
|
1915
|
+
function buildFilePickerCancelledScript() {
|
|
1916
|
+
return buildDispatchScript(`{ type: "sagepilot:file_picker_cancelled" }`);
|
|
1917
|
+
}
|
|
1918
|
+
function readFilePickerError(error) {
|
|
1919
|
+
if (error && typeof error === "object") {
|
|
1920
|
+
const candidate = error;
|
|
1921
|
+
const message = typeof candidate.message === "string" && candidate.message ? candidate.message : "Could not attach the selected file.";
|
|
1922
|
+
const code = typeof candidate.code === "string" ? candidate.code : void 0;
|
|
1923
|
+
return { message, code };
|
|
1924
|
+
}
|
|
1925
|
+
return { message: "Could not attach the selected file." };
|
|
1926
|
+
}
|
|
1927
|
+
function debugProvider(message, details) {
|
|
1928
|
+
console.log(`${DEBUG_PREFIX2} ${message}`, details ?? "");
|
|
1929
|
+
}
|
|
1930
|
+
function describeAttachmentTarget(target) {
|
|
1931
|
+
return {
|
|
1932
|
+
targetChatId: target?.chatId,
|
|
1933
|
+
targetRouteChatId: target?.routeChatId,
|
|
1934
|
+
targetScreen: target?.hostedChatView.screen
|
|
1935
|
+
};
|
|
1936
|
+
}
|
|
1294
1937
|
function getHostedIdentityDispatchScript() {
|
|
1295
1938
|
const message = internalSagepilotChat.getHostedIdentityMessage();
|
|
1296
1939
|
if (!message) return "";
|
|
@@ -1313,10 +1956,13 @@ function readUrlOrigin(url) {
|
|
|
1313
1956
|
return null;
|
|
1314
1957
|
}
|
|
1315
1958
|
}
|
|
1959
|
+
function isInternalWebViewScheme(url) {
|
|
1960
|
+
return url.startsWith("about:") || url.startsWith("data:") || url.startsWith("blob:") || url.startsWith("javascript:") || url.startsWith("file:");
|
|
1961
|
+
}
|
|
1316
1962
|
var hostedChatWebViewProps = {
|
|
1317
1963
|
allowFileAccess: true,
|
|
1318
1964
|
allowFileAccessFromFileURLs: true,
|
|
1319
|
-
...
|
|
1965
|
+
...Platform3.OS === "ios" ? {
|
|
1320
1966
|
automaticallyAdjustContentInsets: false,
|
|
1321
1967
|
contentInsetAdjustmentBehavior: "never",
|
|
1322
1968
|
hideKeyboardAccessoryView: true
|
|
@@ -1331,7 +1977,7 @@ var hostedChatWebViewProps = {
|
|
|
1331
1977
|
sharedCookiesEnabled: true,
|
|
1332
1978
|
thirdPartyCookiesEnabled: true
|
|
1333
1979
|
};
|
|
1334
|
-
var AndroidInsetsView =
|
|
1980
|
+
var AndroidInsetsView = Platform3.OS === "android" ? SagepilotInsetsViewNativeComponent_default : View;
|
|
1335
1981
|
var emptyAndroidInsets = { top: 0, bottom: 0 };
|
|
1336
1982
|
function SagepilotChatProvider({
|
|
1337
1983
|
children,
|
|
@@ -1340,13 +1986,389 @@ function SagepilotChatProvider({
|
|
|
1340
1986
|
}) {
|
|
1341
1987
|
const [state, setState] = useState(readPresentationState);
|
|
1342
1988
|
const [androidModalInsets, setAndroidModalInsets] = useState(emptyAndroidInsets);
|
|
1989
|
+
const [nativeWebViewKey, setNativeWebViewKey] = useState(0);
|
|
1990
|
+
const [preloadWebViewKey, setPreloadWebViewKey] = useState(0);
|
|
1343
1991
|
const webFrameRef = useRef(null);
|
|
1344
1992
|
const nativeWebViewRef = useRef(null);
|
|
1993
|
+
const pendingBatchesRef = useRef([]);
|
|
1994
|
+
const widgetReadyRef = useRef(false);
|
|
1995
|
+
const deliveryTimerRef = useRef(null);
|
|
1996
|
+
const deliveryAttemptsRef = useRef(0);
|
|
1997
|
+
const pendingPingRef = useRef(null);
|
|
1998
|
+
const pingTimeoutRef = useRef(null);
|
|
1999
|
+
const livenessResumeTimerRef = useRef(null);
|
|
2000
|
+
const livenessPausedUntilRef = useRef(0);
|
|
2001
|
+
const pickerInFlightRef = useRef(false);
|
|
2002
|
+
const livenessRemountCountRef = useRef(0);
|
|
2003
|
+
const appStateRef = useRef(AppState.currentState);
|
|
2004
|
+
const didReconcileRef = useRef(false);
|
|
2005
|
+
const [androidRepaintTick, setAndroidRepaintTick] = useState(0);
|
|
2006
|
+
const [sourceChooser, setSourceChooser] = useState(null);
|
|
2007
|
+
const getPendingCache = useCallback(() => {
|
|
2008
|
+
const cacheStorage = internalSagepilotChat.getConfig()?.cacheStorage;
|
|
2009
|
+
if (!cacheStorage) return null;
|
|
2010
|
+
return createJsonCache(cacheStorage, PENDING_FILES_CACHE_NAMESPACE);
|
|
2011
|
+
}, []);
|
|
2012
|
+
const writeManifest = useCallback(() => {
|
|
2013
|
+
const cache = getPendingCache();
|
|
2014
|
+
if (!cache) return;
|
|
2015
|
+
const queue = pendingBatchesRef.current;
|
|
2016
|
+
if (queue.length === 0) {
|
|
2017
|
+
void cache.remove(PENDING_FILES_CACHE_KEY).catch(() => void 0);
|
|
2018
|
+
return;
|
|
2019
|
+
}
|
|
2020
|
+
const hasFileStore = Boolean(internalSagepilotChat.getConfig()?.fileStore);
|
|
2021
|
+
if (hasFileStore) {
|
|
2022
|
+
const manifest = queue.filter((batch) => batch.storageKeys && batch.storageKeys.length === batch.files.length).map((batch) => ({
|
|
2023
|
+
batchId: batch.batchId,
|
|
2024
|
+
target: batch.target,
|
|
2025
|
+
files: batch.files.map((file, index) => ({
|
|
2026
|
+
file_name: file.file_name,
|
|
2027
|
+
mime_type: file.mime_type,
|
|
2028
|
+
size: file.size,
|
|
2029
|
+
storageKey: batch.storageKeys[index]
|
|
2030
|
+
}))
|
|
2031
|
+
}));
|
|
2032
|
+
if (manifest.length === 0) {
|
|
2033
|
+
void cache.remove(PENDING_FILES_CACHE_KEY).catch(() => void 0);
|
|
2034
|
+
} else {
|
|
2035
|
+
void cache.set(PENDING_FILES_CACHE_KEY, manifest).catch(() => void 0);
|
|
2036
|
+
}
|
|
2037
|
+
return;
|
|
2038
|
+
}
|
|
2039
|
+
const totalBytes = queue.reduce((sum, batch) => sum + totalBase64Bytes(batch.files), 0);
|
|
2040
|
+
if (totalBytes <= PERSIST_MAX_TOTAL_BYTES) {
|
|
2041
|
+
const manifest = queue.map((batch) => ({
|
|
2042
|
+
batchId: batch.batchId,
|
|
2043
|
+
target: batch.target,
|
|
2044
|
+
files: batch.files.map((file) => ({
|
|
2045
|
+
file_name: file.file_name,
|
|
2046
|
+
mime_type: file.mime_type,
|
|
2047
|
+
size: file.size,
|
|
2048
|
+
data_base64: file.data_base64
|
|
2049
|
+
}))
|
|
2050
|
+
}));
|
|
2051
|
+
void cache.set(PENDING_FILES_CACHE_KEY, manifest).catch(() => void 0);
|
|
2052
|
+
} else {
|
|
2053
|
+
void cache.remove(PENDING_FILES_CACHE_KEY).catch(() => void 0);
|
|
2054
|
+
}
|
|
2055
|
+
}, [getPendingCache]);
|
|
2056
|
+
const writeBatchBytes = useCallback(async (batch) => {
|
|
2057
|
+
const fileStore = internalSagepilotChat.getConfig()?.fileStore;
|
|
2058
|
+
if (!fileStore) return;
|
|
2059
|
+
const entries = batch.files.map((file, index) => ({
|
|
2060
|
+
key: storageKeyFor(batch.batchId, index),
|
|
2061
|
+
base64: file.data_base64
|
|
2062
|
+
}));
|
|
2063
|
+
try {
|
|
2064
|
+
await Promise.all(entries.map((entry) => fileStore.write(entry.key, entry.base64)));
|
|
2065
|
+
} catch {
|
|
2066
|
+
return;
|
|
2067
|
+
}
|
|
2068
|
+
if (!pendingBatchesRef.current.some((queued) => queued.batchId === batch.batchId)) {
|
|
2069
|
+
void Promise.all(entries.map((entry) => fileStore.remove(entry.key).catch(() => void 0)));
|
|
2070
|
+
}
|
|
2071
|
+
}, []);
|
|
2072
|
+
const discardBatchFiles = useCallback((batch) => {
|
|
2073
|
+
if (!batch?.storageKeys) return;
|
|
2074
|
+
const fileStore = internalSagepilotChat.getConfig()?.fileStore;
|
|
2075
|
+
if (!fileStore) return;
|
|
2076
|
+
void Promise.all(batch.storageKeys.map((key) => fileStore.remove(key).catch(() => void 0)));
|
|
2077
|
+
}, []);
|
|
2078
|
+
const pumpDelivery = useCallback(() => {
|
|
2079
|
+
if (deliveryTimerRef.current) {
|
|
2080
|
+
clearTimeout(deliveryTimerRef.current);
|
|
2081
|
+
deliveryTimerRef.current = null;
|
|
2082
|
+
}
|
|
2083
|
+
const batch = pendingBatchesRef.current[0];
|
|
2084
|
+
if (!batch) return;
|
|
2085
|
+
if (internalSagepilotChat.restoreHostedAttachmentTarget(batch.target)) {
|
|
2086
|
+
debugProvider("restored hosted attachment target", {
|
|
2087
|
+
batchId: batch.batchId,
|
|
2088
|
+
...describeAttachmentTarget(batch.target)
|
|
2089
|
+
});
|
|
2090
|
+
widgetReadyRef.current = false;
|
|
2091
|
+
deliveryTimerRef.current = setTimeout(() => pumpDelivery(), DELIVERY_RETRY_MS);
|
|
2092
|
+
return;
|
|
2093
|
+
}
|
|
2094
|
+
deliveryAttemptsRef.current += 1;
|
|
2095
|
+
const attempts = deliveryAttemptsRef.current;
|
|
2096
|
+
const ref = nativeWebViewRef.current;
|
|
2097
|
+
const shouldDeliver = ref && (widgetReadyRef.current || attempts >= DELIVERY_LEGACY_FALLBACK_ATTEMPTS);
|
|
2098
|
+
if (shouldDeliver && ref) {
|
|
2099
|
+
debugProvider("delivering native picked files", {
|
|
2100
|
+
batchId: batch.batchId,
|
|
2101
|
+
attempt: attempts,
|
|
2102
|
+
widgetReady: widgetReadyRef.current,
|
|
2103
|
+
count: batch.files.length,
|
|
2104
|
+
...describeAttachmentTarget(batch.target)
|
|
2105
|
+
});
|
|
2106
|
+
ref.injectJavaScript(buildFilesPickedScript(batch));
|
|
2107
|
+
}
|
|
2108
|
+
if (attempts < DELIVERY_MAX_ATTEMPTS) {
|
|
2109
|
+
deliveryTimerRef.current = setTimeout(() => pumpDelivery(), DELIVERY_RETRY_MS);
|
|
2110
|
+
}
|
|
2111
|
+
}, []);
|
|
2112
|
+
const startDelivery = useCallback(() => {
|
|
2113
|
+
deliveryAttemptsRef.current = 0;
|
|
2114
|
+
pumpDelivery();
|
|
2115
|
+
}, [pumpDelivery]);
|
|
2116
|
+
const ensureDelivery = useCallback(() => {
|
|
2117
|
+
if (pendingBatchesRef.current.length === 0) return;
|
|
2118
|
+
if (deliveryTimerRef.current) return;
|
|
2119
|
+
startDelivery();
|
|
2120
|
+
}, [startDelivery]);
|
|
2121
|
+
const acknowledgeHeadBatch = useCallback(() => {
|
|
2122
|
+
const head = pendingBatchesRef.current[0];
|
|
2123
|
+
pendingBatchesRef.current = pendingBatchesRef.current.slice(1);
|
|
2124
|
+
deliveryAttemptsRef.current = 0;
|
|
2125
|
+
if (deliveryTimerRef.current) {
|
|
2126
|
+
clearTimeout(deliveryTimerRef.current);
|
|
2127
|
+
deliveryTimerRef.current = null;
|
|
2128
|
+
}
|
|
2129
|
+
discardBatchFiles(head);
|
|
2130
|
+
writeManifest();
|
|
2131
|
+
if (pendingBatchesRef.current.length > 0) startDelivery();
|
|
2132
|
+
}, [discardBatchFiles, writeManifest, startDelivery]);
|
|
2133
|
+
const queuePickedFiles = useCallback((files) => {
|
|
2134
|
+
const batchId = nextBatchId();
|
|
2135
|
+
const hasFileStore = Boolean(internalSagepilotChat.getConfig()?.fileStore);
|
|
2136
|
+
const storageKeys = hasFileStore ? files.map((_, index) => storageKeyFor(batchId, index)) : void 0;
|
|
2137
|
+
const batch = {
|
|
2138
|
+
batchId,
|
|
2139
|
+
files,
|
|
2140
|
+
storageKeys,
|
|
2141
|
+
target: internalSagepilotChat.getHostedAttachmentTarget()
|
|
2142
|
+
};
|
|
2143
|
+
debugProvider("queued native picked files", {
|
|
2144
|
+
batchId,
|
|
2145
|
+
count: files.length,
|
|
2146
|
+
totalBase64Bytes: totalBase64Bytes(files),
|
|
2147
|
+
persistedToFileStore: hasFileStore,
|
|
2148
|
+
...describeAttachmentTarget(batch.target)
|
|
2149
|
+
});
|
|
2150
|
+
pendingBatchesRef.current = [...pendingBatchesRef.current, batch];
|
|
2151
|
+
ensureDelivery();
|
|
2152
|
+
writeManifest();
|
|
2153
|
+
void writeBatchBytes(batch);
|
|
2154
|
+
}, [ensureDelivery, writeManifest, writeBatchBytes]);
|
|
2155
|
+
const recoverNativeWebView = useCallback((reasonOrEvent) => {
|
|
2156
|
+
const reason = typeof reasonOrEvent === "string" ? reasonOrEvent : "webview_render_process";
|
|
2157
|
+
debugProvider("recovering native WebView", { reason });
|
|
2158
|
+
widgetReadyRef.current = false;
|
|
2159
|
+
setNativeWebViewKey((key) => key + 1);
|
|
2160
|
+
}, []);
|
|
2161
|
+
const recoverPreloadWebView = useCallback(() => {
|
|
2162
|
+
setPreloadWebViewKey((key) => key + 1);
|
|
2163
|
+
}, []);
|
|
2164
|
+
const pauseLivenessAfterPicker = useCallback((durationMs) => {
|
|
2165
|
+
if (Platform3.OS !== "android") return;
|
|
2166
|
+
livenessPausedUntilRef.current = Math.max(livenessPausedUntilRef.current, Date.now() + durationMs);
|
|
2167
|
+
}, []);
|
|
2168
|
+
const runNativeFilePicker = useCallback((source, multiple) => {
|
|
2169
|
+
const filePicker = internalSagepilotChat.getConfig()?.filePicker;
|
|
2170
|
+
if (!filePicker) {
|
|
2171
|
+
debugProvider("native file picker requested but no adapter is configured", { source, multiple });
|
|
2172
|
+
return;
|
|
2173
|
+
}
|
|
2174
|
+
if (pickerInFlightRef.current) {
|
|
2175
|
+
debugProvider("native file picker ignored because another picker is in flight", { source, multiple });
|
|
2176
|
+
return;
|
|
2177
|
+
}
|
|
2178
|
+
debugProvider("native file picker starting", {
|
|
2179
|
+
source,
|
|
2180
|
+
multiple,
|
|
2181
|
+
configuredSources: filePicker.sources
|
|
2182
|
+
});
|
|
2183
|
+
pauseLivenessAfterPicker(6e4);
|
|
2184
|
+
pickerInFlightRef.current = true;
|
|
2185
|
+
filePicker.pickFiles({ source, multiple }).then((files) => {
|
|
2186
|
+
debugProvider("native file picker resolved", {
|
|
2187
|
+
source,
|
|
2188
|
+
count: files.length,
|
|
2189
|
+
files: files.map((file) => ({
|
|
2190
|
+
fileName: file.file_name,
|
|
2191
|
+
mimeType: file.mime_type,
|
|
2192
|
+
size: file.size,
|
|
2193
|
+
base64Length: file.data_base64.length
|
|
2194
|
+
}))
|
|
2195
|
+
});
|
|
2196
|
+
if (files.length === 0) {
|
|
2197
|
+
nativeWebViewRef.current?.injectJavaScript(buildFilePickerCancelledScript());
|
|
2198
|
+
return;
|
|
2199
|
+
}
|
|
2200
|
+
queuePickedFiles(files);
|
|
2201
|
+
}).catch((error) => {
|
|
2202
|
+
const { message, code } = readFilePickerError(error);
|
|
2203
|
+
debugProvider("native file picker failed", { source, code, message });
|
|
2204
|
+
nativeWebViewRef.current?.injectJavaScript(buildFilePickerErrorScript(message, code));
|
|
2205
|
+
}).finally(() => {
|
|
2206
|
+
pickerInFlightRef.current = false;
|
|
2207
|
+
pauseLivenessAfterPicker(LIVENESS_PICKER_GRACE_MS);
|
|
2208
|
+
});
|
|
2209
|
+
}, [pauseLivenessAfterPicker, queuePickedFiles]);
|
|
2210
|
+
const openNativeFilePicker = useCallback((multiple) => {
|
|
2211
|
+
const filePicker = internalSagepilotChat.getConfig()?.filePicker;
|
|
2212
|
+
if (!filePicker || filePicker.sources.length === 0) {
|
|
2213
|
+
debugProvider("open native file picker ignored", {
|
|
2214
|
+
multiple,
|
|
2215
|
+
hasAdapter: Boolean(filePicker),
|
|
2216
|
+
sources: filePicker?.sources ?? []
|
|
2217
|
+
});
|
|
2218
|
+
return;
|
|
2219
|
+
}
|
|
2220
|
+
debugProvider("open native file picker", { multiple, sources: filePicker.sources });
|
|
2221
|
+
const [onlySource] = filePicker.sources;
|
|
2222
|
+
if (filePicker.sources.length === 1 && onlySource && onlySource !== "camera") {
|
|
2223
|
+
runNativeFilePicker(onlySource, multiple);
|
|
2224
|
+
return;
|
|
2225
|
+
}
|
|
2226
|
+
setSourceChooser({ multiple });
|
|
2227
|
+
}, [runNativeFilePicker]);
|
|
2228
|
+
const handleSourceChoice = useCallback((source) => {
|
|
2229
|
+
const chooser = sourceChooser;
|
|
2230
|
+
setSourceChooser(null);
|
|
2231
|
+
if (chooser) runNativeFilePicker(source, chooser.multiple);
|
|
2232
|
+
}, [sourceChooser, runNativeFilePicker]);
|
|
1345
2233
|
useEffect(() => {
|
|
1346
2234
|
return internalSagepilotChat.onStateChange(() => {
|
|
1347
2235
|
setState(readPresentationState());
|
|
1348
2236
|
});
|
|
1349
2237
|
}, []);
|
|
2238
|
+
useEffect(() => {
|
|
2239
|
+
let cancelled = false;
|
|
2240
|
+
const cache = getPendingCache();
|
|
2241
|
+
if (!cache) return;
|
|
2242
|
+
const reconcile = async () => {
|
|
2243
|
+
if (didReconcileRef.current) return;
|
|
2244
|
+
const manifest = await cache.get(PENDING_FILES_CACHE_KEY).catch(() => null);
|
|
2245
|
+
if (cancelled || didReconcileRef.current || !Array.isArray(manifest) || manifest.length === 0) return;
|
|
2246
|
+
const fileStore = internalSagepilotChat.getConfig()?.fileStore;
|
|
2247
|
+
const restored = [];
|
|
2248
|
+
for (const batch of manifest) {
|
|
2249
|
+
if (!batch || typeof batch.batchId !== "string" || !Array.isArray(batch.files) || batch.files.length === 0) continue;
|
|
2250
|
+
const files = [];
|
|
2251
|
+
const storageKeys = [];
|
|
2252
|
+
let intact = true;
|
|
2253
|
+
for (const file of batch.files) {
|
|
2254
|
+
let dataBase64 = null;
|
|
2255
|
+
if (typeof file.storageKey === "string" && fileStore) {
|
|
2256
|
+
try {
|
|
2257
|
+
dataBase64 = await fileStore.read(file.storageKey);
|
|
2258
|
+
storageKeys.push(file.storageKey);
|
|
2259
|
+
} catch {
|
|
2260
|
+
intact = false;
|
|
2261
|
+
}
|
|
2262
|
+
} else if (typeof file.data_base64 === "string" && file.data_base64) {
|
|
2263
|
+
dataBase64 = file.data_base64;
|
|
2264
|
+
}
|
|
2265
|
+
if (!dataBase64) {
|
|
2266
|
+
intact = false;
|
|
2267
|
+
break;
|
|
2268
|
+
}
|
|
2269
|
+
files.push({ file_name: file.file_name, mime_type: file.mime_type, size: file.size, data_base64: dataBase64 });
|
|
2270
|
+
}
|
|
2271
|
+
if (intact && files.length === batch.files.length) {
|
|
2272
|
+
restored.push({
|
|
2273
|
+
batchId: batch.batchId,
|
|
2274
|
+
files,
|
|
2275
|
+
storageKeys: storageKeys.length === files.length ? storageKeys : void 0,
|
|
2276
|
+
target: batch.target
|
|
2277
|
+
});
|
|
2278
|
+
}
|
|
2279
|
+
}
|
|
2280
|
+
if (cancelled || didReconcileRef.current) return;
|
|
2281
|
+
didReconcileRef.current = true;
|
|
2282
|
+
pendingBatchesRef.current = [...restored, ...pendingBatchesRef.current];
|
|
2283
|
+
writeManifest();
|
|
2284
|
+
if (fileStore) {
|
|
2285
|
+
const keep = pendingBatchesRef.current.flatMap(
|
|
2286
|
+
(batch) => batch.files.map((_, index) => storageKeyFor(batch.batchId, index))
|
|
2287
|
+
);
|
|
2288
|
+
void fileStore.prune(keep).catch(() => void 0);
|
|
2289
|
+
}
|
|
2290
|
+
startDelivery();
|
|
2291
|
+
};
|
|
2292
|
+
void reconcile();
|
|
2293
|
+
return () => {
|
|
2294
|
+
cancelled = true;
|
|
2295
|
+
};
|
|
2296
|
+
}, [getPendingCache, startDelivery, writeManifest]);
|
|
2297
|
+
const clearPing = useCallback(() => {
|
|
2298
|
+
pendingPingRef.current = null;
|
|
2299
|
+
if (pingTimeoutRef.current) {
|
|
2300
|
+
clearTimeout(pingTimeoutRef.current);
|
|
2301
|
+
pingTimeoutRef.current = null;
|
|
2302
|
+
}
|
|
2303
|
+
}, []);
|
|
2304
|
+
const clearDeferredLivenessProbe = useCallback(() => {
|
|
2305
|
+
if (livenessResumeTimerRef.current) {
|
|
2306
|
+
clearTimeout(livenessResumeTimerRef.current);
|
|
2307
|
+
livenessResumeTimerRef.current = null;
|
|
2308
|
+
}
|
|
2309
|
+
}, []);
|
|
2310
|
+
useEffect(() => {
|
|
2311
|
+
return () => {
|
|
2312
|
+
if (deliveryTimerRef.current) {
|
|
2313
|
+
clearTimeout(deliveryTimerRef.current);
|
|
2314
|
+
deliveryTimerRef.current = null;
|
|
2315
|
+
}
|
|
2316
|
+
if (pingTimeoutRef.current) {
|
|
2317
|
+
clearTimeout(pingTimeoutRef.current);
|
|
2318
|
+
pingTimeoutRef.current = null;
|
|
2319
|
+
}
|
|
2320
|
+
clearDeferredLivenessProbe();
|
|
2321
|
+
};
|
|
2322
|
+
}, [clearDeferredLivenessProbe]);
|
|
2323
|
+
const runLivenessProbe = useCallback(() => {
|
|
2324
|
+
if (Platform3.OS !== "android") return;
|
|
2325
|
+
const ref = nativeWebViewRef.current;
|
|
2326
|
+
if (!ref || !internalSagepilotChat.isPresented()) return;
|
|
2327
|
+
const pauseRemainingMs = livenessPausedUntilRef.current - Date.now();
|
|
2328
|
+
if (pickerInFlightRef.current || pauseRemainingMs > 0) {
|
|
2329
|
+
const retryDelayMs = pickerInFlightRef.current ? 500 : Math.max(0, pauseRemainingMs);
|
|
2330
|
+
debugProvider("liveness probe deferred after picker", {
|
|
2331
|
+
pickerInFlight: pickerInFlightRef.current,
|
|
2332
|
+
retryDelayMs
|
|
2333
|
+
});
|
|
2334
|
+
clearDeferredLivenessProbe();
|
|
2335
|
+
livenessResumeTimerRef.current = setTimeout(() => {
|
|
2336
|
+
livenessResumeTimerRef.current = null;
|
|
2337
|
+
runLivenessProbe();
|
|
2338
|
+
}, retryDelayMs);
|
|
2339
|
+
return;
|
|
2340
|
+
}
|
|
2341
|
+
setAndroidRepaintTick((tick) => tick + 1);
|
|
2342
|
+
const attempts = (pendingPingRef.current?.attempts ?? 0) + 1;
|
|
2343
|
+
const nonce = `${Date.now().toString(36)}_${attempts}`;
|
|
2344
|
+
pendingPingRef.current = { nonce, attempts };
|
|
2345
|
+
ref.injectJavaScript(buildLivenessPingScript(nonce, LIVENESS_HEARTBEAT_STALE_MS));
|
|
2346
|
+
if (pingTimeoutRef.current) clearTimeout(pingTimeoutRef.current);
|
|
2347
|
+
pingTimeoutRef.current = setTimeout(() => {
|
|
2348
|
+
if (attempts >= LIVENESS_MAX_PING_ATTEMPTS) {
|
|
2349
|
+
clearPing();
|
|
2350
|
+
recoverNativeWebView("liveness_timeout");
|
|
2351
|
+
return;
|
|
2352
|
+
}
|
|
2353
|
+
runLivenessProbe();
|
|
2354
|
+
}, LIVENESS_PONG_TIMEOUT_MS);
|
|
2355
|
+
}, [clearDeferredLivenessProbe, clearPing, recoverNativeWebView]);
|
|
2356
|
+
useEffect(() => {
|
|
2357
|
+
const subscription = AppState.addEventListener("change", (nextState) => {
|
|
2358
|
+
const prev = appStateRef.current;
|
|
2359
|
+
appStateRef.current = nextState;
|
|
2360
|
+
if (nextState === "active" && (prev === "background" || prev === "inactive")) {
|
|
2361
|
+
clearPing();
|
|
2362
|
+
livenessRemountCountRef.current = 0;
|
|
2363
|
+
runLivenessProbe();
|
|
2364
|
+
ensureDelivery();
|
|
2365
|
+
}
|
|
2366
|
+
});
|
|
2367
|
+
return () => {
|
|
2368
|
+
subscription.remove();
|
|
2369
|
+
clearPing();
|
|
2370
|
+
};
|
|
2371
|
+
}, [clearPing, runLivenessProbe, ensureDelivery]);
|
|
1350
2372
|
const handleAndroidInsetsChange = useCallback((event) => {
|
|
1351
2373
|
const nextBottomInset = event.nativeEvent?.bottom;
|
|
1352
2374
|
const nextTopInset = event.nativeEvent?.top;
|
|
@@ -1360,11 +2382,11 @@ function SagepilotChatProvider({
|
|
|
1360
2382
|
const presentationStyle = state.presentation?.style ?? "sheet";
|
|
1361
2383
|
const isFullScreenModal = presentationStyle === "fullScreen";
|
|
1362
2384
|
const animationType = presentationStyle === "fullScreen" || presentationStyle === "push" ? "slide" : "fade";
|
|
1363
|
-
const ModalContainer =
|
|
1364
|
-
const NativeModalContainer =
|
|
1365
|
-
const ChatContentContainer =
|
|
1366
|
-
const nativeModalContainerProps =
|
|
1367
|
-
const chatContentContainerProps =
|
|
2385
|
+
const ModalContainer = Platform3.OS === "ios" && isFullScreenModal ? SafeAreaView : View;
|
|
2386
|
+
const NativeModalContainer = Platform3.OS === "android" ? AndroidInsetsView : ModalContainer;
|
|
2387
|
+
const ChatContentContainer = Platform3.OS === "ios" ? KeyboardAvoidingView : View;
|
|
2388
|
+
const nativeModalContainerProps = Platform3.OS === "android" ? { style: styles.container, onInsetsChange: handleAndroidInsetsChange } : { style: styles.container };
|
|
2389
|
+
const chatContentContainerProps = Platform3.OS === "ios" ? {
|
|
1368
2390
|
behavior: "padding",
|
|
1369
2391
|
enabled: true,
|
|
1370
2392
|
keyboardVerticalOffset: 0,
|
|
@@ -1372,14 +2394,57 @@ function SagepilotChatProvider({
|
|
|
1372
2394
|
} : {
|
|
1373
2395
|
style: [
|
|
1374
2396
|
styles.modalContent,
|
|
1375
|
-
|
|
2397
|
+
Platform3.OS === "android" ? {
|
|
1376
2398
|
paddingTop: androidModalInsets.top,
|
|
1377
2399
|
paddingBottom: androidModalInsets.bottom
|
|
1378
2400
|
} : null
|
|
1379
2401
|
].filter(Boolean)
|
|
1380
2402
|
};
|
|
1381
2403
|
const handleWebViewMessage = (event) => {
|
|
1382
|
-
|
|
2404
|
+
const message = parseHostedBridgeMessage(event.nativeEvent?.data);
|
|
2405
|
+
if (message?.type === "sagepilot:open_file_picker") {
|
|
2406
|
+
openNativeFilePicker(message.multiple ?? true);
|
|
2407
|
+
return;
|
|
2408
|
+
}
|
|
2409
|
+
if (message?.type === "sagepilot:widget_listener_ready") {
|
|
2410
|
+
widgetReadyRef.current = true;
|
|
2411
|
+
startDelivery();
|
|
2412
|
+
return;
|
|
2413
|
+
}
|
|
2414
|
+
if (message?.type === "sagepilot:files_received") {
|
|
2415
|
+
const ackBatchId = message.batch_id;
|
|
2416
|
+
const head = pendingBatchesRef.current[0];
|
|
2417
|
+
if (head && (!ackBatchId || ackBatchId === head.batchId)) {
|
|
2418
|
+
debugProvider("native picked files acknowledged", {
|
|
2419
|
+
batchId: head.batchId,
|
|
2420
|
+
ackBatchId,
|
|
2421
|
+
count: message.count
|
|
2422
|
+
});
|
|
2423
|
+
acknowledgeHeadBatch();
|
|
2424
|
+
}
|
|
2425
|
+
return;
|
|
2426
|
+
}
|
|
2427
|
+
if (message?.type === "sagepilot:pong") {
|
|
2428
|
+
const pending = pendingPingRef.current;
|
|
2429
|
+
if (pending && (!message.nonce || message.nonce === pending.nonce)) {
|
|
2430
|
+
if (message.alive === false) {
|
|
2431
|
+
clearPing();
|
|
2432
|
+
debugProvider("liveness probe reported dead widget", {
|
|
2433
|
+
supportsHeartbeat: message.supportsHeartbeat,
|
|
2434
|
+
remountCount: livenessRemountCountRef.current
|
|
2435
|
+
});
|
|
2436
|
+
if (livenessRemountCountRef.current < LIVENESS_MAX_REMOUNTS) {
|
|
2437
|
+
livenessRemountCountRef.current += 1;
|
|
2438
|
+
recoverNativeWebView("liveness_dead_pong");
|
|
2439
|
+
}
|
|
2440
|
+
} else {
|
|
2441
|
+
livenessRemountCountRef.current = 0;
|
|
2442
|
+
clearPing();
|
|
2443
|
+
}
|
|
2444
|
+
}
|
|
2445
|
+
return;
|
|
2446
|
+
}
|
|
2447
|
+
internalSagepilotChat.handleHostedBridgeMessage(message);
|
|
1383
2448
|
};
|
|
1384
2449
|
const postIdentityToWebFrame = () => {
|
|
1385
2450
|
const message = internalSagepilotChat.getHostedIdentityMessage();
|
|
@@ -1392,8 +2457,31 @@ function SagepilotChatProvider({
|
|
|
1392
2457
|
if (!script || !nativeWebViewRef.current) return;
|
|
1393
2458
|
nativeWebViewRef.current.injectJavaScript(script);
|
|
1394
2459
|
};
|
|
2460
|
+
const injectNativeBridgeToNativeWebView = () => {
|
|
2461
|
+
if (!nativeWebViewRef.current) return;
|
|
2462
|
+
nativeWebViewRef.current.injectJavaScript(getInjectedWebViewScript());
|
|
2463
|
+
};
|
|
2464
|
+
const handleNativeWebViewLoadEnd = () => {
|
|
2465
|
+
debugProvider("native WebView load end");
|
|
2466
|
+
injectNativeBridgeToNativeWebView();
|
|
2467
|
+
postIdentityToNativeWebView();
|
|
2468
|
+
widgetReadyRef.current = false;
|
|
2469
|
+
startDelivery();
|
|
2470
|
+
};
|
|
2471
|
+
const handleShouldStartLoadWithRequest = useCallback((request) => {
|
|
2472
|
+
const url = request?.url ?? "";
|
|
2473
|
+
if (!url || request?.isTopFrame === false || isInternalWebViewScheme(url)) {
|
|
2474
|
+
return true;
|
|
2475
|
+
}
|
|
2476
|
+
const widgetOrigin = readUrlOrigin(state.conversationUrl ?? state.preloadUrl);
|
|
2477
|
+
if (widgetOrigin && readUrlOrigin(url) === widgetOrigin) {
|
|
2478
|
+
return true;
|
|
2479
|
+
}
|
|
2480
|
+
Linking2.openURL(url).catch(() => void 0);
|
|
2481
|
+
return false;
|
|
2482
|
+
}, [state.conversationUrl, state.preloadUrl]);
|
|
1395
2483
|
useEffect(() => {
|
|
1396
|
-
if (
|
|
2484
|
+
if (Platform3.OS !== "web" || typeof window === "undefined") return;
|
|
1397
2485
|
const trustedWidgetOrigin = readUrlOrigin(state.conversationUrl);
|
|
1398
2486
|
if (!trustedWidgetOrigin) return;
|
|
1399
2487
|
const handleWindowMessage = (event) => {
|
|
@@ -1404,14 +2492,14 @@ function SagepilotChatProvider({
|
|
|
1404
2492
|
return () => window.removeEventListener("message", handleWindowMessage);
|
|
1405
2493
|
}, [state.conversationUrl]);
|
|
1406
2494
|
useEffect(() => {
|
|
1407
|
-
if (
|
|
2495
|
+
if (Platform3.OS !== "web" || !state.isPresented) return;
|
|
1408
2496
|
postIdentityToWebFrame();
|
|
1409
2497
|
}, [state.isPresented, state.conversationUrl, state.configured]);
|
|
1410
2498
|
useEffect(() => {
|
|
1411
|
-
if (
|
|
2499
|
+
if (Platform3.OS === "web" || !state.isPresented) return;
|
|
1412
2500
|
postIdentityToNativeWebView();
|
|
1413
2501
|
}, [state.isPresented, state.conversationUrl, state.configured]);
|
|
1414
|
-
if (
|
|
2502
|
+
if (Platform3.OS === "web") {
|
|
1415
2503
|
return createElement(
|
|
1416
2504
|
View,
|
|
1417
2505
|
{ style: styles.root },
|
|
@@ -1454,10 +2542,18 @@ function SagepilotChatProvider({
|
|
|
1454
2542
|
children,
|
|
1455
2543
|
state.configured && !state.isPresented && state.shouldPreload && state.preloadUrl ? createElement(WebView, {
|
|
1456
2544
|
...hostedChatWebViewProps,
|
|
2545
|
+
// Separate key from the hosted WebView: a hidden-preload renderer crash
|
|
2546
|
+
// must not bump the hosted key (which would reset widgetReadyRef and
|
|
2547
|
+
// disrupt an in-flight delivery). onRenderProcessGone must still be
|
|
2548
|
+
// handled here or an unhandled renderer kill crashes the whole app.
|
|
2549
|
+
key: `sagepilot-preload-webview-${preloadWebViewKey}`,
|
|
1457
2550
|
source: { uri: state.preloadUrl },
|
|
1458
2551
|
style: styles.preloadWebview,
|
|
1459
2552
|
injectedJavaScriptBeforeContentLoaded: getInjectedWebViewScript(),
|
|
1460
|
-
onMessage: handleWebViewMessage
|
|
2553
|
+
onMessage: handleWebViewMessage,
|
|
2554
|
+
onShouldStartLoadWithRequest: handleShouldStartLoadWithRequest,
|
|
2555
|
+
onRenderProcessGone: recoverPreloadWebView,
|
|
2556
|
+
onContentProcessDidTerminate: recoverPreloadWebView
|
|
1461
2557
|
}) : null,
|
|
1462
2558
|
createElement(
|
|
1463
2559
|
Modal,
|
|
@@ -1465,8 +2561,8 @@ function SagepilotChatProvider({
|
|
|
1465
2561
|
visible: state.configured && state.isPresented,
|
|
1466
2562
|
animationType,
|
|
1467
2563
|
presentationStyle: isFullScreenModal ? "fullScreen" : "pageSheet",
|
|
1468
|
-
statusBarTranslucent:
|
|
1469
|
-
navigationBarTranslucent:
|
|
2564
|
+
statusBarTranslucent: Platform3.OS === "android",
|
|
2565
|
+
navigationBarTranslucent: Platform3.OS === "android",
|
|
1470
2566
|
onRequestClose: () => internalSagepilotChat.dismiss()
|
|
1471
2567
|
},
|
|
1472
2568
|
createElement(
|
|
@@ -1491,13 +2587,23 @@ function SagepilotChatProvider({
|
|
|
1491
2587
|
) : null,
|
|
1492
2588
|
state.conversationUrl ? createElement(WebView, {
|
|
1493
2589
|
...hostedChatWebViewProps,
|
|
2590
|
+
key: `sagepilot-hosted-webview-${nativeWebViewKey}`,
|
|
1494
2591
|
ref: nativeWebViewRef,
|
|
1495
2592
|
source: { uri: state.conversationUrl },
|
|
1496
|
-
|
|
2593
|
+
// The imperceptible opacity toggle forces the Android WebView
|
|
2594
|
+
// surface to re-composite on resume, clearing the blank-but-alive
|
|
2595
|
+
// surface bug without a reload (see runLivenessProbe).
|
|
2596
|
+
style: Platform3.OS === "android" ? [styles.webview, { opacity: 1 - androidRepaintTick % 2 * 1e-3 }] : styles.webview,
|
|
1497
2597
|
startInLoadingState: true,
|
|
1498
2598
|
injectedJavaScriptBeforeContentLoaded: getInjectedWebViewScript(),
|
|
1499
2599
|
onMessage: handleWebViewMessage,
|
|
1500
|
-
onLoadEnd:
|
|
2600
|
+
onLoadEnd: handleNativeWebViewLoadEnd,
|
|
2601
|
+
onShouldStartLoadWithRequest: handleShouldStartLoadWithRequest,
|
|
2602
|
+
// Android: render process killed (commonly while the camera/file
|
|
2603
|
+
// chooser activity is foregrounded). Remount to recover.
|
|
2604
|
+
onRenderProcessGone: recoverNativeWebView,
|
|
2605
|
+
// iOS equivalent: WKWebView content process terminated.
|
|
2606
|
+
onContentProcessDidTerminate: recoverNativeWebView,
|
|
1501
2607
|
renderLoading: () => createElement(
|
|
1502
2608
|
View,
|
|
1503
2609
|
{ style: styles.loading },
|
|
@@ -1507,6 +2613,59 @@ function SagepilotChatProvider({
|
|
|
1507
2613
|
}) : null
|
|
1508
2614
|
)
|
|
1509
2615
|
)
|
|
2616
|
+
),
|
|
2617
|
+
// Native attachment-source chooser. Replaces the Android Alert (capped at
|
|
2618
|
+
// 3 buttons) so camera/library/documents all show on every platform.
|
|
2619
|
+
createElement(
|
|
2620
|
+
Modal,
|
|
2621
|
+
{
|
|
2622
|
+
visible: sourceChooser !== null,
|
|
2623
|
+
transparent: true,
|
|
2624
|
+
animationType: "fade",
|
|
2625
|
+
statusBarTranslucent: true,
|
|
2626
|
+
onRequestClose: () => setSourceChooser(null)
|
|
2627
|
+
},
|
|
2628
|
+
createElement(
|
|
2629
|
+
Pressable,
|
|
2630
|
+
{ style: styles.sheetBackdrop, onPress: () => setSourceChooser(null) },
|
|
2631
|
+
createElement(
|
|
2632
|
+
View,
|
|
2633
|
+
{ style: styles.sheetCard },
|
|
2634
|
+
createElement(View, { style: styles.sheetHandle }),
|
|
2635
|
+
createElement(Text, { style: styles.sheetTitle }, "Add attachment"),
|
|
2636
|
+
...(internalSagepilotChat.getConfig()?.filePicker?.sources ?? []).map(
|
|
2637
|
+
(source) => createElement(
|
|
2638
|
+
Pressable,
|
|
2639
|
+
{
|
|
2640
|
+
key: source,
|
|
2641
|
+
accessibilityRole: "button",
|
|
2642
|
+
android_ripple: { color: "rgba(15, 23, 42, 0.06)" },
|
|
2643
|
+
hitSlop: 4,
|
|
2644
|
+
style: ({ pressed }) => getSheetOptionStyle(pressed),
|
|
2645
|
+
onPress: () => handleSourceChoice(source)
|
|
2646
|
+
},
|
|
2647
|
+
renderFilePickerSourceIcon(source),
|
|
2648
|
+
createElement(
|
|
2649
|
+
View,
|
|
2650
|
+
{ style: styles.sheetOptionText },
|
|
2651
|
+
createElement(Text, { style: styles.sheetButtonText }, FILE_PICKER_SOURCE_LABELS[source]),
|
|
2652
|
+
createElement(Text, { style: styles.sheetButtonDescription }, FILE_PICKER_SOURCE_DESCRIPTIONS[source])
|
|
2653
|
+
)
|
|
2654
|
+
)
|
|
2655
|
+
),
|
|
2656
|
+
createElement(
|
|
2657
|
+
Pressable,
|
|
2658
|
+
{
|
|
2659
|
+
accessibilityRole: "button",
|
|
2660
|
+
android_ripple: { color: "rgba(15, 23, 42, 0.06)" },
|
|
2661
|
+
style: ({ pressed }) => getSheetCancelStyle(pressed),
|
|
2662
|
+
onPress: () => setSourceChooser(null)
|
|
2663
|
+
},
|
|
2664
|
+
createElement(X, { color: "#334155", size: 18, strokeWidth: 2.5, style: styles.sheetCancelIcon }),
|
|
2665
|
+
createElement(Text, { style: styles.sheetCancelText }, "Cancel")
|
|
2666
|
+
)
|
|
2667
|
+
)
|
|
2668
|
+
)
|
|
1510
2669
|
)
|
|
1511
2670
|
);
|
|
1512
2671
|
}
|
|
@@ -1605,6 +2764,112 @@ var styles = StyleSheet.create({
|
|
|
1605
2764
|
marginTop: 12,
|
|
1606
2765
|
color: "#4b5563",
|
|
1607
2766
|
fontSize: 14
|
|
2767
|
+
},
|
|
2768
|
+
sheetBackdrop: {
|
|
2769
|
+
flex: 1,
|
|
2770
|
+
alignItems: "center",
|
|
2771
|
+
justifyContent: "flex-end",
|
|
2772
|
+
backgroundColor: "rgba(15, 23, 42, 0.48)"
|
|
2773
|
+
},
|
|
2774
|
+
sheetCard: {
|
|
2775
|
+
width: "100%",
|
|
2776
|
+
maxWidth: 540,
|
|
2777
|
+
backgroundColor: "#ffffff",
|
|
2778
|
+
borderTopLeftRadius: 26,
|
|
2779
|
+
borderTopRightRadius: 26,
|
|
2780
|
+
paddingTop: 10,
|
|
2781
|
+
paddingBottom: 18,
|
|
2782
|
+
paddingHorizontal: 14,
|
|
2783
|
+
shadowColor: "#0f172a",
|
|
2784
|
+
shadowOpacity: 0.22,
|
|
2785
|
+
shadowRadius: 28,
|
|
2786
|
+
shadowOffset: { width: 0, height: -10 },
|
|
2787
|
+
elevation: 18
|
|
2788
|
+
},
|
|
2789
|
+
sheetHandle: {
|
|
2790
|
+
alignSelf: "center",
|
|
2791
|
+
width: 42,
|
|
2792
|
+
height: 5,
|
|
2793
|
+
borderRadius: 999,
|
|
2794
|
+
backgroundColor: "#d1d5db",
|
|
2795
|
+
marginBottom: 14
|
|
2796
|
+
},
|
|
2797
|
+
sheetTitle: {
|
|
2798
|
+
color: "#111827",
|
|
2799
|
+
fontSize: 17,
|
|
2800
|
+
fontWeight: "700",
|
|
2801
|
+
paddingBottom: 12,
|
|
2802
|
+
paddingHorizontal: 4
|
|
2803
|
+
},
|
|
2804
|
+
sheetOption: {
|
|
2805
|
+
minHeight: 70,
|
|
2806
|
+
flexDirection: "row",
|
|
2807
|
+
alignItems: "center",
|
|
2808
|
+
borderRadius: 18,
|
|
2809
|
+
paddingHorizontal: 12,
|
|
2810
|
+
marginBottom: 6,
|
|
2811
|
+
backgroundColor: "#ffffff"
|
|
2812
|
+
},
|
|
2813
|
+
sheetOptionPressed: {
|
|
2814
|
+
backgroundColor: "#f8fafc",
|
|
2815
|
+
transform: [{ scale: 0.985 }]
|
|
2816
|
+
},
|
|
2817
|
+
sheetOptionIcon: {
|
|
2818
|
+
width: 46,
|
|
2819
|
+
height: 46,
|
|
2820
|
+
borderRadius: 16,
|
|
2821
|
+
marginRight: 14,
|
|
2822
|
+
alignItems: "center",
|
|
2823
|
+
justifyContent: "center",
|
|
2824
|
+
position: "relative",
|
|
2825
|
+
overflow: "hidden"
|
|
2826
|
+
},
|
|
2827
|
+
sheetOptionText: {
|
|
2828
|
+
flex: 1,
|
|
2829
|
+
justifyContent: "center"
|
|
2830
|
+
},
|
|
2831
|
+
sheetButtonText: {
|
|
2832
|
+
color: "#0f172a",
|
|
2833
|
+
fontSize: 16,
|
|
2834
|
+
fontWeight: "700"
|
|
2835
|
+
},
|
|
2836
|
+
sheetButtonDescription: {
|
|
2837
|
+
color: "#64748b",
|
|
2838
|
+
fontSize: 13,
|
|
2839
|
+
fontWeight: "500",
|
|
2840
|
+
marginTop: 3
|
|
2841
|
+
},
|
|
2842
|
+
cameraOptionIcon: {
|
|
2843
|
+
backgroundColor: "#e0f2fe"
|
|
2844
|
+
},
|
|
2845
|
+
libraryOptionIcon: {
|
|
2846
|
+
backgroundColor: "#ecfdf5"
|
|
2847
|
+
},
|
|
2848
|
+
documentsOptionIcon: {
|
|
2849
|
+
backgroundColor: "#f5f3ff"
|
|
2850
|
+
},
|
|
2851
|
+
sheetCancelButton: {
|
|
2852
|
+
minHeight: 56,
|
|
2853
|
+
flexDirection: "row",
|
|
2854
|
+
alignItems: "center",
|
|
2855
|
+
justifyContent: "center",
|
|
2856
|
+
borderRadius: 18,
|
|
2857
|
+
marginTop: 8,
|
|
2858
|
+
backgroundColor: "#f1f5f9"
|
|
2859
|
+
},
|
|
2860
|
+
sheetCancelButtonPressed: {
|
|
2861
|
+
backgroundColor: "#e2e8f0",
|
|
2862
|
+
transform: [{ scale: 0.985 }]
|
|
2863
|
+
},
|
|
2864
|
+
sheetCancelIcon: {
|
|
2865
|
+
width: 18,
|
|
2866
|
+
height: 18,
|
|
2867
|
+
marginRight: 8
|
|
2868
|
+
},
|
|
2869
|
+
sheetCancelText: {
|
|
2870
|
+
color: "#0f172a",
|
|
2871
|
+
fontSize: 16,
|
|
2872
|
+
fontWeight: "700"
|
|
1608
2873
|
}
|
|
1609
2874
|
});
|
|
1610
2875
|
|
|
@@ -1658,10 +2923,18 @@ function useSagepilotChat() {
|
|
|
1658
2923
|
};
|
|
1659
2924
|
}
|
|
1660
2925
|
export {
|
|
2926
|
+
SAGEPILOT_DEFAULT_IMAGE_MAX_DIMENSION,
|
|
2927
|
+
SAGEPILOT_DEFAULT_IMAGE_MAX_FILE_SIZE_BYTES,
|
|
2928
|
+
SAGEPILOT_DEFAULT_IMAGE_QUALITY,
|
|
1661
2929
|
SagepilotChat,
|
|
1662
2930
|
SagepilotChatError,
|
|
1663
2931
|
SagepilotChatProvider,
|
|
2932
|
+
SagepilotFilePickerError,
|
|
1664
2933
|
createAsyncStorageCacheStorage,
|
|
1665
2934
|
createKeychainTokenStorage,
|
|
2935
|
+
createSagepilotFilePicker,
|
|
2936
|
+
createSagepilotFileStore,
|
|
2937
|
+
ensureSagepilotAndroidCameraPermission,
|
|
2938
|
+
promptOpenSagepilotCameraSettings,
|
|
1666
2939
|
useSagepilotChat
|
|
1667
2940
|
};
|