@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.js
CHANGED
|
@@ -30,15 +30,26 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
30
30
|
// src/index.ts
|
|
31
31
|
var index_exports = {};
|
|
32
32
|
__export(index_exports, {
|
|
33
|
+
SAGEPILOT_DEFAULT_IMAGE_MAX_DIMENSION: () => SAGEPILOT_DEFAULT_IMAGE_MAX_DIMENSION,
|
|
34
|
+
SAGEPILOT_DEFAULT_IMAGE_MAX_FILE_SIZE_BYTES: () => SAGEPILOT_DEFAULT_IMAGE_MAX_FILE_SIZE_BYTES,
|
|
35
|
+
SAGEPILOT_DEFAULT_IMAGE_QUALITY: () => SAGEPILOT_DEFAULT_IMAGE_QUALITY,
|
|
33
36
|
SagepilotChat: () => SagepilotChat,
|
|
34
37
|
SagepilotChatError: () => SagepilotChatError,
|
|
35
38
|
SagepilotChatProvider: () => SagepilotChatProvider,
|
|
39
|
+
SagepilotFilePickerError: () => SagepilotFilePickerError,
|
|
36
40
|
createAsyncStorageCacheStorage: () => createAsyncStorageCacheStorage,
|
|
37
41
|
createKeychainTokenStorage: () => createKeychainTokenStorage,
|
|
42
|
+
createSagepilotFilePicker: () => createSagepilotFilePicker,
|
|
43
|
+
createSagepilotFileStore: () => createSagepilotFileStore,
|
|
44
|
+
ensureSagepilotAndroidCameraPermission: () => ensureSagepilotAndroidCameraPermission,
|
|
45
|
+
promptOpenSagepilotCameraSettings: () => promptOpenSagepilotCameraSettings,
|
|
38
46
|
useSagepilotChat: () => useSagepilotChat
|
|
39
47
|
});
|
|
40
48
|
module.exports = __toCommonJS(index_exports);
|
|
41
49
|
|
|
50
|
+
// src/public/SdkClient.ts
|
|
51
|
+
var import_react_native = require("react-native");
|
|
52
|
+
|
|
42
53
|
// src/core/errors/SagepilotChatError.ts
|
|
43
54
|
var SagepilotChatError = class extends Error {
|
|
44
55
|
constructor(code, message, options = {}) {
|
|
@@ -56,7 +67,7 @@ var SagepilotChatError = class extends Error {
|
|
|
56
67
|
|
|
57
68
|
// src/core/config/constants.ts
|
|
58
69
|
var SDK_NAME = "@sagepilot-ai/react-native-sdk";
|
|
59
|
-
var SDK_VERSION = "0.
|
|
70
|
+
var SDK_VERSION = "0.3.0";
|
|
60
71
|
var DEFAULT_HOST = "https://app.sagepilot.ai";
|
|
61
72
|
var DEFAULT_WIDGET_HOST = "https://app.sagepilot.ai";
|
|
62
73
|
var CUSTOMER_API_PREFIX = "/customer-api/v1";
|
|
@@ -365,6 +376,34 @@ async function resolveDeviceInfo(adapter) {
|
|
|
365
376
|
return adapter.getDeviceInfo();
|
|
366
377
|
}
|
|
367
378
|
|
|
379
|
+
// src/core/storage/cache.ts
|
|
380
|
+
function createAsyncStorageCacheStorage(asyncStorage) {
|
|
381
|
+
return {
|
|
382
|
+
getItem: (key) => asyncStorage.getItem(key),
|
|
383
|
+
setItem: (key, value) => asyncStorage.setItem(key, value),
|
|
384
|
+
removeItem: (key) => asyncStorage.removeItem(key)
|
|
385
|
+
};
|
|
386
|
+
}
|
|
387
|
+
function createJsonCache(storage, namespace) {
|
|
388
|
+
return {
|
|
389
|
+
async get(key) {
|
|
390
|
+
const value = await storage.getItem(`${namespace}:${key}`);
|
|
391
|
+
if (!value) return null;
|
|
392
|
+
try {
|
|
393
|
+
return JSON.parse(value);
|
|
394
|
+
} catch {
|
|
395
|
+
return null;
|
|
396
|
+
}
|
|
397
|
+
},
|
|
398
|
+
async set(key, value) {
|
|
399
|
+
await storage.setItem(`${namespace}:${key}`, JSON.stringify(value));
|
|
400
|
+
},
|
|
401
|
+
async remove(key) {
|
|
402
|
+
await storage.removeItem(`${namespace}:${key}`);
|
|
403
|
+
}
|
|
404
|
+
};
|
|
405
|
+
}
|
|
406
|
+
|
|
368
407
|
// src/resources/channels.ts
|
|
369
408
|
function bootstrapChannel(http, host, channelId) {
|
|
370
409
|
return http.request(
|
|
@@ -448,6 +487,9 @@ function fetchUnreadCount(http, host, channelId, sessionId, authorizationHeader)
|
|
|
448
487
|
}
|
|
449
488
|
|
|
450
489
|
// src/public/SdkClient.ts
|
|
490
|
+
var PRESENTATION_CACHE_NAMESPACE = "sagepilot_rn_presentation";
|
|
491
|
+
var PRESENTATION_CACHE_KEY = "state";
|
|
492
|
+
var PRESENTATION_RESTORE_MAX_AGE_MS = 15 * 60 * 1e3;
|
|
451
493
|
var DEFAULT_MOBILE_LAUNCHER_CONFIG = {
|
|
452
494
|
label: "Chat",
|
|
453
495
|
buttonColor: "#173c2d",
|
|
@@ -517,6 +559,10 @@ function readRecordField(input, key) {
|
|
|
517
559
|
if (!value || typeof value !== "object" || Array.isArray(value)) return void 0;
|
|
518
560
|
return value;
|
|
519
561
|
}
|
|
562
|
+
function isFreshPresentationState(updatedAt) {
|
|
563
|
+
if (typeof updatedAt !== "number" || !Number.isFinite(updatedAt)) return false;
|
|
564
|
+
return Date.now() - updatedAt <= PRESENTATION_RESTORE_MAX_AGE_MS;
|
|
565
|
+
}
|
|
520
566
|
function toPublicSessionState(session) {
|
|
521
567
|
return {
|
|
522
568
|
session_id: session.session_id,
|
|
@@ -573,6 +619,9 @@ var SagepilotReactNativeChat = class {
|
|
|
573
619
|
const { workspaceId, channelId } = parseKey(config.key);
|
|
574
620
|
this.runtimeOptions = {
|
|
575
621
|
behavior: config.behavior,
|
|
622
|
+
cacheStorage: config.cacheStorage,
|
|
623
|
+
filePicker: config.filePicker,
|
|
624
|
+
fileStore: config.fileStore,
|
|
576
625
|
presentation: config.presentation,
|
|
577
626
|
theme: config.theme,
|
|
578
627
|
hostedClaimsStorageKeyPrefix: config.hostedClaimsStorageKeyPrefix || DEFAULT_HOSTED_CLAIMS_STORAGE_KEY_PREFIX
|
|
@@ -603,6 +652,7 @@ var SagepilotReactNativeChat = class {
|
|
|
603
652
|
this.channel = channel;
|
|
604
653
|
this.session = session;
|
|
605
654
|
this.isConfigured = true;
|
|
655
|
+
await this.restorePresentationFromCache();
|
|
606
656
|
this.emitReady();
|
|
607
657
|
this.emitState();
|
|
608
658
|
if (config.behavior?.enableUnreadPolling ?? true) {
|
|
@@ -742,12 +792,14 @@ var SagepilotReactNativeChat = class {
|
|
|
742
792
|
this.hostedChatView = { screen: "home" };
|
|
743
793
|
if (this.presented) {
|
|
744
794
|
this.emitState();
|
|
795
|
+
this.persistPresentationState();
|
|
745
796
|
return true;
|
|
746
797
|
}
|
|
747
798
|
this.presented = true;
|
|
748
799
|
this.setUnreadCount(0);
|
|
749
800
|
this.presentCallbacks.forEach((callback) => callback(this.getLifecycleState()));
|
|
750
801
|
this.emitState();
|
|
802
|
+
this.persistPresentationState();
|
|
751
803
|
return true;
|
|
752
804
|
}
|
|
753
805
|
presentMessages() {
|
|
@@ -775,6 +827,7 @@ var SagepilotReactNativeChat = class {
|
|
|
775
827
|
this.emitUnreadChange();
|
|
776
828
|
this.dismissCallbacks.forEach((callback) => callback(this.getLifecycleState()));
|
|
777
829
|
this.emitState();
|
|
830
|
+
this.persistPresentationState();
|
|
778
831
|
return true;
|
|
779
832
|
}
|
|
780
833
|
hide() {
|
|
@@ -909,6 +962,10 @@ var SagepilotReactNativeChat = class {
|
|
|
909
962
|
}
|
|
910
963
|
return this.session?.conversation_id ?? void 0;
|
|
911
964
|
}
|
|
965
|
+
/** Returns the chat id that is explicitly part of the current hosted URL. */
|
|
966
|
+
getHostedRouteChatId() {
|
|
967
|
+
return this.hostedChatView.screen === "composer" ? this.hostedChatView.chatId : void 0;
|
|
968
|
+
}
|
|
912
969
|
getHostedIdentityMessage() {
|
|
913
970
|
if (!this.legacyWidgetJwt) return null;
|
|
914
971
|
return {
|
|
@@ -919,6 +976,34 @@ var SagepilotReactNativeChat = class {
|
|
|
919
976
|
}
|
|
920
977
|
};
|
|
921
978
|
}
|
|
979
|
+
/** Captures the hosted route that should receive a picked attachment batch. */
|
|
980
|
+
getHostedAttachmentTarget() {
|
|
981
|
+
return {
|
|
982
|
+
chatId: this.getActiveHostedChatId(),
|
|
983
|
+
routeChatId: this.getHostedRouteChatId(),
|
|
984
|
+
hostedChatView: this.serializeHostedChatView(this.hostedChatView)
|
|
985
|
+
};
|
|
986
|
+
}
|
|
987
|
+
/** Restores the hosted route for a queued attachment batch before delivery. */
|
|
988
|
+
restoreHostedAttachmentTarget(target) {
|
|
989
|
+
if (import_react_native.Platform.OS !== "android") return false;
|
|
990
|
+
if (!target) return false;
|
|
991
|
+
const hostedChatView = this.readPersistedHostedChatView(target.hostedChatView);
|
|
992
|
+
if (!hostedChatView) return false;
|
|
993
|
+
const currentChatId = this.getActiveHostedChatId();
|
|
994
|
+
const targetChatId = normalizeOptionalString(target.chatId);
|
|
995
|
+
const routeChatId = normalizeOptionalString(target.routeChatId) ?? (hostedChatView.screen === "composer" ? normalizeOptionalString(hostedChatView.chatId) : void 0);
|
|
996
|
+
const nextHostedChatView = routeChatId && hostedChatView.screen === "composer" ? this.withPinnedChatId(hostedChatView, routeChatId) : hostedChatView;
|
|
997
|
+
if (this.presented && (!targetChatId || currentChatId === targetChatId) && this.areHostedChatViewsEqual(this.hostedChatView, nextHostedChatView)) {
|
|
998
|
+
return false;
|
|
999
|
+
}
|
|
1000
|
+
this.hostedChatView = nextHostedChatView;
|
|
1001
|
+
this.presented = true;
|
|
1002
|
+
this.setUnreadCount(0);
|
|
1003
|
+
this.emitState();
|
|
1004
|
+
this.persistPresentationState();
|
|
1005
|
+
return true;
|
|
1006
|
+
}
|
|
922
1007
|
handleHostedBridgeMessage(message) {
|
|
923
1008
|
if (!message) return false;
|
|
924
1009
|
if (message.type === "close_widget") {
|
|
@@ -986,6 +1071,7 @@ var SagepilotReactNativeChat = class {
|
|
|
986
1071
|
}
|
|
987
1072
|
destroy() {
|
|
988
1073
|
this.stopUnreadPolling();
|
|
1074
|
+
this.persistPresentationState({ cleanShutdown: true, presented: false });
|
|
989
1075
|
this.resetRuntimeState();
|
|
990
1076
|
this.emitState();
|
|
991
1077
|
this.identifyCallbacks.clear();
|
|
@@ -1141,8 +1227,86 @@ var SagepilotReactNativeChat = class {
|
|
|
1141
1227
|
this.presentCallbacks.forEach((callback) => callback(this.getLifecycleState()));
|
|
1142
1228
|
}
|
|
1143
1229
|
this.emitState();
|
|
1230
|
+
this.persistPresentationState();
|
|
1144
1231
|
return true;
|
|
1145
1232
|
}
|
|
1233
|
+
/** Returns the dedicated cache used for hosted-chat presentation recovery. */
|
|
1234
|
+
getPresentationCache() {
|
|
1235
|
+
const cacheStorage = this.runtimeOptions?.cacheStorage;
|
|
1236
|
+
if (!cacheStorage) return null;
|
|
1237
|
+
return createJsonCache(cacheStorage, PRESENTATION_CACHE_NAMESPACE);
|
|
1238
|
+
}
|
|
1239
|
+
/** Removes callback-only fields so the hosted route can be safely serialized. */
|
|
1240
|
+
serializeHostedChatView(view) {
|
|
1241
|
+
if (view.screen !== "composer") return view;
|
|
1242
|
+
return {
|
|
1243
|
+
screen: "composer",
|
|
1244
|
+
message: view.message,
|
|
1245
|
+
mode: view.mode,
|
|
1246
|
+
chatId: view.chatId,
|
|
1247
|
+
metadata: view.metadata
|
|
1248
|
+
};
|
|
1249
|
+
}
|
|
1250
|
+
/** Validates a persisted hosted view before using it to rebuild a widget URL. */
|
|
1251
|
+
readPersistedHostedChatView(view) {
|
|
1252
|
+
if (!view || typeof view !== "object") return null;
|
|
1253
|
+
const record = view;
|
|
1254
|
+
if (record.screen === "home") return { screen: "home" };
|
|
1255
|
+
if (record.screen === "messages") return { screen: "messages" };
|
|
1256
|
+
if (record.screen !== "composer") return null;
|
|
1257
|
+
const mode = record.mode === "new" ? "new" : "auto";
|
|
1258
|
+
return {
|
|
1259
|
+
screen: "composer",
|
|
1260
|
+
message: typeof record.message === "string" ? record.message : void 0,
|
|
1261
|
+
mode,
|
|
1262
|
+
chatId: typeof record.chatId === "string" && record.chatId ? record.chatId : void 0,
|
|
1263
|
+
metadata: readRecordField(record, "metadata")
|
|
1264
|
+
};
|
|
1265
|
+
}
|
|
1266
|
+
/** Forces a persisted hosted route to target the exact chat that owns a pending attachment. */
|
|
1267
|
+
withPinnedChatId(view, chatId) {
|
|
1268
|
+
if (view.screen === "composer") {
|
|
1269
|
+
return {
|
|
1270
|
+
...view,
|
|
1271
|
+
chatId
|
|
1272
|
+
};
|
|
1273
|
+
}
|
|
1274
|
+
return {
|
|
1275
|
+
screen: "composer",
|
|
1276
|
+
mode: "auto",
|
|
1277
|
+
chatId
|
|
1278
|
+
};
|
|
1279
|
+
}
|
|
1280
|
+
/** Compares hosted route state without relying on object identity. */
|
|
1281
|
+
areHostedChatViewsEqual(first, second) {
|
|
1282
|
+
return JSON.stringify(this.serializeHostedChatView(first)) === JSON.stringify(this.serializeHostedChatView(second));
|
|
1283
|
+
}
|
|
1284
|
+
/** Persists presentation state without blocking customer-facing SDK methods. */
|
|
1285
|
+
persistPresentationState(options) {
|
|
1286
|
+
const cache = this.getPresentationCache();
|
|
1287
|
+
if (!cache) return;
|
|
1288
|
+
const state = {
|
|
1289
|
+
presented: options?.presented ?? this.presented,
|
|
1290
|
+
hostedChatView: this.serializeHostedChatView(this.hostedChatView),
|
|
1291
|
+
updatedAt: Date.now(),
|
|
1292
|
+
cleanShutdown: options?.cleanShutdown
|
|
1293
|
+
};
|
|
1294
|
+
void cache.set(PRESENTATION_CACHE_KEY, state).catch(() => void 0);
|
|
1295
|
+
}
|
|
1296
|
+
/** Restores Android presentation state after configure without resetting the hosted route to Home. */
|
|
1297
|
+
async restorePresentationFromCache() {
|
|
1298
|
+
const cache = this.getPresentationCache();
|
|
1299
|
+
if (!cache || import_react_native.Platform.OS !== "android") return;
|
|
1300
|
+
const persisted = await cache.get(PRESENTATION_CACHE_KEY).catch(() => null);
|
|
1301
|
+
if (persisted?.cleanShutdown) return;
|
|
1302
|
+
if (!isFreshPresentationState(persisted?.updatedAt)) return;
|
|
1303
|
+
if (!persisted?.presented) return;
|
|
1304
|
+
const hostedChatView = this.readPersistedHostedChatView(persisted.hostedChatView);
|
|
1305
|
+
if (!hostedChatView) return;
|
|
1306
|
+
this.hostedChatView = hostedChatView;
|
|
1307
|
+
this.presented = true;
|
|
1308
|
+
this.setUnreadCount(0);
|
|
1309
|
+
}
|
|
1146
1310
|
};
|
|
1147
1311
|
var internalSagepilotChat = new SagepilotReactNativeChat();
|
|
1148
1312
|
var SagepilotChat = {
|
|
@@ -1179,21 +1343,368 @@ var SagepilotChat = {
|
|
|
1179
1343
|
destroy: () => internalSagepilotChat.destroy()
|
|
1180
1344
|
};
|
|
1181
1345
|
|
|
1182
|
-
// src/core/storage/
|
|
1183
|
-
|
|
1346
|
+
// src/core/storage/fileStore.ts
|
|
1347
|
+
var DEFAULT_DIRECTORY_NAME = "sagepilot-attachments";
|
|
1348
|
+
function sanitizeKey(key) {
|
|
1349
|
+
return key.replace(/[^a-zA-Z0-9._-]/g, "_").replace(/\.{2,}/g, "_");
|
|
1350
|
+
}
|
|
1351
|
+
function createSagepilotFileStore(blobUtil, options = {}) {
|
|
1352
|
+
const directory = `${blobUtil.fs.dirs.DocumentDir}/${options.directoryName ?? DEFAULT_DIRECTORY_NAME}`;
|
|
1353
|
+
const pathFor = (key) => `${directory}/${sanitizeKey(key)}`;
|
|
1354
|
+
let dirReady = null;
|
|
1355
|
+
function ensureDirectory() {
|
|
1356
|
+
if (!dirReady) {
|
|
1357
|
+
dirReady = (async () => {
|
|
1358
|
+
if (!await blobUtil.fs.exists(directory)) {
|
|
1359
|
+
await blobUtil.fs.mkdir(directory);
|
|
1360
|
+
}
|
|
1361
|
+
})().catch((error) => {
|
|
1362
|
+
dirReady = null;
|
|
1363
|
+
throw error;
|
|
1364
|
+
});
|
|
1365
|
+
}
|
|
1366
|
+
return dirReady;
|
|
1367
|
+
}
|
|
1184
1368
|
return {
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1369
|
+
async write(key, base64) {
|
|
1370
|
+
await ensureDirectory();
|
|
1371
|
+
await blobUtil.fs.writeFile(pathFor(key), base64, "base64");
|
|
1372
|
+
},
|
|
1373
|
+
async read(key) {
|
|
1374
|
+
const content = await blobUtil.fs.readFile(pathFor(key), "base64");
|
|
1375
|
+
if (typeof content !== "string" || content.length === 0) {
|
|
1376
|
+
throw new Error("Persisted attachment is empty or unreadable.");
|
|
1377
|
+
}
|
|
1378
|
+
return content;
|
|
1379
|
+
},
|
|
1380
|
+
async remove(key) {
|
|
1381
|
+
try {
|
|
1382
|
+
if (await blobUtil.fs.exists(pathFor(key))) {
|
|
1383
|
+
await blobUtil.fs.unlink(pathFor(key));
|
|
1384
|
+
}
|
|
1385
|
+
} catch {
|
|
1386
|
+
}
|
|
1387
|
+
},
|
|
1388
|
+
async prune(keepKeys) {
|
|
1389
|
+
try {
|
|
1390
|
+
if (!await blobUtil.fs.exists(directory)) return;
|
|
1391
|
+
const keep = new Set(keepKeys.map(sanitizeKey));
|
|
1392
|
+
const entries = await blobUtil.fs.ls(directory);
|
|
1393
|
+
await Promise.all(
|
|
1394
|
+
entries.filter((entry) => !keep.has(entry)).map((entry) => blobUtil.fs.unlink(`${directory}/${entry}`).catch(() => void 0))
|
|
1395
|
+
);
|
|
1396
|
+
} catch {
|
|
1397
|
+
}
|
|
1398
|
+
}
|
|
1399
|
+
};
|
|
1400
|
+
}
|
|
1401
|
+
|
|
1402
|
+
// src/core/native/filePicker.ts
|
|
1403
|
+
var import_react_native2 = require("react-native");
|
|
1404
|
+
|
|
1405
|
+
// src/core/native/filePickerShared.ts
|
|
1406
|
+
var SagepilotFilePickerError = class extends Error {
|
|
1407
|
+
/** Creates a typed picker error that can be surfaced over the widget bridge. */
|
|
1408
|
+
constructor(code, message) {
|
|
1409
|
+
super(message);
|
|
1410
|
+
this.name = "SagepilotFilePickerError";
|
|
1411
|
+
this.code = code;
|
|
1412
|
+
}
|
|
1413
|
+
};
|
|
1414
|
+
var SAGEPILOT_DEFAULT_IMAGE_MAX_DIMENSION = 1920;
|
|
1415
|
+
var SAGEPILOT_DEFAULT_IMAGE_QUALITY = 0.8;
|
|
1416
|
+
var SAGEPILOT_DEFAULT_IMAGE_MAX_FILE_SIZE_BYTES = 15 * 1024 * 1024;
|
|
1417
|
+
|
|
1418
|
+
// src/core/native/filePicker.ts
|
|
1419
|
+
var DEBUG_PREFIX = "[SagepilotSDK][FilePicker]";
|
|
1420
|
+
var DEFAULT_IMAGE_MAX_DIMENSION = SAGEPILOT_DEFAULT_IMAGE_MAX_DIMENSION;
|
|
1421
|
+
var DEFAULT_IMAGE_QUALITY = SAGEPILOT_DEFAULT_IMAGE_QUALITY;
|
|
1422
|
+
var DEFAULT_IMAGE_MIME_TYPE = "image/jpeg";
|
|
1423
|
+
var DEFAULT_DOCUMENT_MIME_TYPE = "application/octet-stream";
|
|
1424
|
+
var DEFAULT_IMAGE_SELECTION_LIMIT = 5;
|
|
1425
|
+
var DEFAULT_DOCUMENT_MAX_FILE_SIZE_BYTES = 20 * 1024 * 1024;
|
|
1426
|
+
var DEFAULT_IMAGE_MAX_FILE_SIZE_BYTES = SAGEPILOT_DEFAULT_IMAGE_MAX_FILE_SIZE_BYTES;
|
|
1427
|
+
function bytesToMb(bytes) {
|
|
1428
|
+
return Math.round(bytes / (1024 * 1024) * 10) / 10;
|
|
1429
|
+
}
|
|
1430
|
+
function estimateBase64ByteSize(dataBase64) {
|
|
1431
|
+
return Math.floor(dataBase64.length * 3 / 4);
|
|
1432
|
+
}
|
|
1433
|
+
function stripFileScheme(uri) {
|
|
1434
|
+
return uri.startsWith("file://") ? uri.replace("file://", "") : uri;
|
|
1435
|
+
}
|
|
1436
|
+
function debugFilePicker(message, details) {
|
|
1437
|
+
console.log(`${DEBUG_PREFIX} ${message}`, details ?? "");
|
|
1438
|
+
}
|
|
1439
|
+
async function promptOpenSagepilotCameraSettings() {
|
|
1440
|
+
if (import_react_native2.Platform.OS !== "android") return;
|
|
1441
|
+
await new Promise((resolve) => {
|
|
1442
|
+
import_react_native2.Alert.alert(
|
|
1443
|
+
"Camera access is off",
|
|
1444
|
+
"Enable camera access for this app in Settings to take a photo.",
|
|
1445
|
+
[
|
|
1446
|
+
{
|
|
1447
|
+
text: "Not now",
|
|
1448
|
+
style: "cancel",
|
|
1449
|
+
onPress: resolve
|
|
1450
|
+
},
|
|
1451
|
+
{
|
|
1452
|
+
text: "Open Settings",
|
|
1453
|
+
onPress: () => {
|
|
1454
|
+
import_react_native2.Linking.openSettings().finally(resolve);
|
|
1455
|
+
}
|
|
1456
|
+
}
|
|
1457
|
+
]
|
|
1458
|
+
);
|
|
1459
|
+
});
|
|
1460
|
+
}
|
|
1461
|
+
async function ensureSagepilotAndroidCameraPermission() {
|
|
1462
|
+
if (import_react_native2.Platform.OS !== "android") return;
|
|
1463
|
+
let result;
|
|
1464
|
+
try {
|
|
1465
|
+
debugFilePicker("requesting Android camera permission");
|
|
1466
|
+
result = await import_react_native2.PermissionsAndroid.request(import_react_native2.PermissionsAndroid.PERMISSIONS.CAMERA);
|
|
1467
|
+
} catch {
|
|
1468
|
+
debugFilePicker("camera permission request threw; continuing to native launch");
|
|
1469
|
+
return;
|
|
1470
|
+
}
|
|
1471
|
+
debugFilePicker("Android camera permission result", { result });
|
|
1472
|
+
if (result === import_react_native2.PermissionsAndroid.RESULTS.NEVER_ASK_AGAIN) {
|
|
1473
|
+
await promptOpenSagepilotCameraSettings();
|
|
1474
|
+
throw new SagepilotFilePickerError(
|
|
1475
|
+
"permission_denied",
|
|
1476
|
+
"Camera access is turned off. Enable it for this app in Settings to take a photo."
|
|
1477
|
+
);
|
|
1478
|
+
}
|
|
1479
|
+
if (result === import_react_native2.PermissionsAndroid.RESULTS.DENIED) {
|
|
1480
|
+
throw new SagepilotFilePickerError(
|
|
1481
|
+
"permission_denied",
|
|
1482
|
+
"Camera permission is required to take a photo."
|
|
1483
|
+
);
|
|
1484
|
+
}
|
|
1485
|
+
}
|
|
1486
|
+
function canUseCameraSource(imagePicker) {
|
|
1487
|
+
return Boolean(imagePicker);
|
|
1488
|
+
}
|
|
1489
|
+
function mapImagePickerErrorCode(errorCode) {
|
|
1490
|
+
switch (errorCode) {
|
|
1491
|
+
case "permission":
|
|
1492
|
+
return "permission_denied";
|
|
1493
|
+
case "camera_unavailable":
|
|
1494
|
+
return "camera_unavailable";
|
|
1495
|
+
default:
|
|
1496
|
+
return "unknown";
|
|
1497
|
+
}
|
|
1498
|
+
}
|
|
1499
|
+
function imageAssetsToPickedFiles(result, maxFileSizeBytes) {
|
|
1500
|
+
if (result.didCancel) return [];
|
|
1501
|
+
if (result.errorCode) {
|
|
1502
|
+
throw new SagepilotFilePickerError(
|
|
1503
|
+
mapImagePickerErrorCode(result.errorCode),
|
|
1504
|
+
result.errorMessage || `Could not capture the photo (${result.errorCode}).`
|
|
1505
|
+
);
|
|
1506
|
+
}
|
|
1507
|
+
const assets = result.assets ?? [];
|
|
1508
|
+
const usable = assets.filter((asset) => typeof asset.base64 === "string" && asset.base64.length > 0);
|
|
1509
|
+
if (assets.length > usable.length) {
|
|
1510
|
+
throw new SagepilotFilePickerError(
|
|
1511
|
+
"encode_failed",
|
|
1512
|
+
assets.length === 1 ? "The photo could not be processed. Please try again." : "Some photos could not be processed. Please try selecting them again."
|
|
1513
|
+
);
|
|
1514
|
+
}
|
|
1515
|
+
return usable.map((asset, index) => {
|
|
1516
|
+
const dataBase64 = asset.base64;
|
|
1517
|
+
const size = asset.fileSize ?? estimateBase64ByteSize(dataBase64);
|
|
1518
|
+
if (maxFileSizeBytes > 0 && size > maxFileSizeBytes) {
|
|
1519
|
+
throw new SagepilotFilePickerError(
|
|
1520
|
+
"file_too_large",
|
|
1521
|
+
`Image is too large (max ${bytesToMb(maxFileSizeBytes)}MB).`
|
|
1522
|
+
);
|
|
1523
|
+
}
|
|
1524
|
+
return {
|
|
1525
|
+
file_name: asset.fileName || `photo-${Date.now()}-${index + 1}.jpg`,
|
|
1526
|
+
mime_type: asset.type || DEFAULT_IMAGE_MIME_TYPE,
|
|
1527
|
+
size,
|
|
1528
|
+
data_base64: dataBase64
|
|
1529
|
+
};
|
|
1530
|
+
});
|
|
1531
|
+
}
|
|
1532
|
+
async function readUriAsBase64(uri, fileReader) {
|
|
1533
|
+
if (fileReader) {
|
|
1534
|
+
const content = await fileReader.fs.readFile(stripFileScheme(uri), "base64");
|
|
1535
|
+
if (typeof content !== "string") {
|
|
1536
|
+
throw new Error("File reader returned unexpected content.");
|
|
1537
|
+
}
|
|
1538
|
+
return content;
|
|
1539
|
+
}
|
|
1540
|
+
const response = await fetch(uri);
|
|
1541
|
+
const blob = await response.blob();
|
|
1542
|
+
return new Promise((resolve, reject) => {
|
|
1543
|
+
const reader = new FileReader();
|
|
1544
|
+
reader.onload = () => {
|
|
1545
|
+
const dataUrl = typeof reader.result === "string" ? reader.result : "";
|
|
1546
|
+
const [, payload = ""] = dataUrl.split(",");
|
|
1547
|
+
if (!payload) {
|
|
1548
|
+
reject(new Error("Could not read the selected file."));
|
|
1549
|
+
return;
|
|
1550
|
+
}
|
|
1551
|
+
resolve(payload);
|
|
1552
|
+
};
|
|
1553
|
+
reader.onerror = () => reject(new Error("Could not read the selected file."));
|
|
1554
|
+
reader.readAsDataURL(blob);
|
|
1555
|
+
});
|
|
1556
|
+
}
|
|
1557
|
+
function createSagepilotFilePicker(options) {
|
|
1558
|
+
const { imagePicker, documentsPicker, fileReader } = options;
|
|
1559
|
+
const imageMaxDimension = options.imageMaxDimension ?? DEFAULT_IMAGE_MAX_DIMENSION;
|
|
1560
|
+
const imageQuality = options.imageQuality ?? DEFAULT_IMAGE_QUALITY;
|
|
1561
|
+
const imageSelectionLimit = options.imageSelectionLimit ?? DEFAULT_IMAGE_SELECTION_LIMIT;
|
|
1562
|
+
const documentMaxFileSizeBytes = options.documentMaxFileSizeBytes ?? DEFAULT_DOCUMENT_MAX_FILE_SIZE_BYTES;
|
|
1563
|
+
const imageMaxFileSizeBytes = options.imageMaxFileSizeBytes ?? DEFAULT_IMAGE_MAX_FILE_SIZE_BYTES;
|
|
1564
|
+
const sources = [];
|
|
1565
|
+
if (canUseCameraSource(imagePicker)) sources.push("camera");
|
|
1566
|
+
if (imagePicker) sources.push("library");
|
|
1567
|
+
if (documentsPicker) sources.push("documents");
|
|
1568
|
+
debugFilePicker("adapter created", {
|
|
1569
|
+
platform: import_react_native2.Platform.OS,
|
|
1570
|
+
hasImagePicker: Boolean(imagePicker),
|
|
1571
|
+
hasDocumentsPicker: Boolean(documentsPicker),
|
|
1572
|
+
hasFileReader: Boolean(fileReader),
|
|
1573
|
+
sources
|
|
1574
|
+
});
|
|
1575
|
+
if (sources.length === 0) return void 0;
|
|
1576
|
+
const imageOptions = {
|
|
1577
|
+
mediaType: "photo",
|
|
1578
|
+
includeBase64: true,
|
|
1579
|
+
maxWidth: imageMaxDimension,
|
|
1580
|
+
maxHeight: imageMaxDimension,
|
|
1581
|
+
quality: imageQuality,
|
|
1582
|
+
saveToPhotos: false
|
|
1583
|
+
};
|
|
1584
|
+
async function pickFromCamera() {
|
|
1585
|
+
debugFilePicker("camera pick requested", {
|
|
1586
|
+
platform: import_react_native2.Platform.OS,
|
|
1587
|
+
imageMaxDimension,
|
|
1588
|
+
imageQuality,
|
|
1589
|
+
imageMaxFileSizeBytes
|
|
1590
|
+
});
|
|
1591
|
+
await ensureSagepilotAndroidCameraPermission();
|
|
1592
|
+
if (!imagePicker) {
|
|
1593
|
+
throw new SagepilotFilePickerError("camera_unavailable", "The camera picker is not available in this build.");
|
|
1594
|
+
}
|
|
1595
|
+
debugFilePicker("launching image-picker camera", { platform: import_react_native2.Platform.OS });
|
|
1596
|
+
return imageAssetsToPickedFiles(await imagePicker.launchCamera(imageOptions), imageMaxFileSizeBytes);
|
|
1597
|
+
}
|
|
1598
|
+
async function pickFromLibrary(multiple) {
|
|
1599
|
+
if (!imagePicker) return [];
|
|
1600
|
+
debugFilePicker("library pick requested", { multiple, imageSelectionLimit });
|
|
1601
|
+
return imageAssetsToPickedFiles(await imagePicker.launchImageLibrary({
|
|
1602
|
+
...imageOptions,
|
|
1603
|
+
// Bounded multi-select: 0 (unlimited) lets a huge batch OOM the bridge.
|
|
1604
|
+
selectionLimit: multiple ? Math.max(0, imageSelectionLimit) : 1
|
|
1605
|
+
}), imageMaxFileSizeBytes);
|
|
1606
|
+
}
|
|
1607
|
+
async function pickDocuments(multiple) {
|
|
1608
|
+
if (!documentsPicker) return [];
|
|
1609
|
+
debugFilePicker("document pick requested", { multiple, documentMaxFileSizeBytes });
|
|
1610
|
+
let picked;
|
|
1611
|
+
try {
|
|
1612
|
+
picked = await documentsPicker.pick({ allowMultiSelection: multiple });
|
|
1613
|
+
} catch (error) {
|
|
1614
|
+
if (error && typeof error === "object" && error.code === "OPERATION_CANCELED") {
|
|
1615
|
+
return [];
|
|
1616
|
+
}
|
|
1617
|
+
throw error;
|
|
1618
|
+
}
|
|
1619
|
+
const files = [];
|
|
1620
|
+
for (const file of picked) {
|
|
1621
|
+
if (documentMaxFileSizeBytes > 0 && typeof file.size === "number" && file.size > documentMaxFileSizeBytes) {
|
|
1622
|
+
throw new SagepilotFilePickerError(
|
|
1623
|
+
"file_too_large",
|
|
1624
|
+
`"${file.name || "File"}" is too large (max ${bytesToMb(documentMaxFileSizeBytes)}MB).`
|
|
1625
|
+
);
|
|
1626
|
+
}
|
|
1627
|
+
let readableUri = file.uri;
|
|
1628
|
+
if (documentsPicker.keepLocalCopy) {
|
|
1629
|
+
const fileName = file.name || `file-${Date.now()}`;
|
|
1630
|
+
const [copy] = await documentsPicker.keepLocalCopy({
|
|
1631
|
+
files: [{ uri: file.uri, fileName }],
|
|
1632
|
+
destination: "cachesDirectory"
|
|
1633
|
+
});
|
|
1634
|
+
if (copy?.status === "success" && copy.localUri) {
|
|
1635
|
+
readableUri = copy.localUri;
|
|
1636
|
+
}
|
|
1637
|
+
}
|
|
1638
|
+
let dataBase64;
|
|
1639
|
+
try {
|
|
1640
|
+
dataBase64 = await readUriAsBase64(readableUri, fileReader);
|
|
1641
|
+
} catch (error) {
|
|
1642
|
+
throw new SagepilotFilePickerError(
|
|
1643
|
+
"read_failed",
|
|
1644
|
+
error instanceof Error && error.message ? error.message : "Could not read the selected file."
|
|
1645
|
+
);
|
|
1646
|
+
}
|
|
1647
|
+
files.push({
|
|
1648
|
+
file_name: file.name || `file-${Date.now()}`,
|
|
1649
|
+
mime_type: file.type || DEFAULT_DOCUMENT_MIME_TYPE,
|
|
1650
|
+
size: file.size ?? estimateBase64ByteSize(dataBase64),
|
|
1651
|
+
data_base64: dataBase64
|
|
1652
|
+
});
|
|
1653
|
+
}
|
|
1654
|
+
return files;
|
|
1655
|
+
}
|
|
1656
|
+
return {
|
|
1657
|
+
sources,
|
|
1658
|
+
async pickFiles(request) {
|
|
1659
|
+
debugFilePicker("pickFiles request", request);
|
|
1660
|
+
if (request.source === "camera") return pickFromCamera();
|
|
1661
|
+
if (request.source === "library") return pickFromLibrary(request.multiple);
|
|
1662
|
+
return pickDocuments(request.multiple);
|
|
1663
|
+
}
|
|
1188
1664
|
};
|
|
1189
1665
|
}
|
|
1190
1666
|
|
|
1191
1667
|
// src/ui/SagepilotChatProvider.ts
|
|
1192
1668
|
var import_react = require("react");
|
|
1193
|
-
var
|
|
1669
|
+
var import_lucide_react_native = require("lucide-react-native");
|
|
1670
|
+
var import_react_native3 = require("react-native");
|
|
1194
1671
|
var import_react_native_webview = require("react-native-webview");
|
|
1195
1672
|
|
|
1196
1673
|
// src/core/webview/mobileBridge.ts
|
|
1674
|
+
var FILE_PICKER_PROTOCOL_VERSION = 2;
|
|
1675
|
+
function buildBridgeCapabilitiesPayload(capabilities) {
|
|
1676
|
+
return { ...capabilities, filePickerProtocol: FILE_PICKER_PROTOCOL_VERSION };
|
|
1677
|
+
}
|
|
1678
|
+
function buildBridgeCapabilitiesScript(capabilities) {
|
|
1679
|
+
const payload = buildBridgeCapabilitiesPayload(capabilities);
|
|
1680
|
+
return [
|
|
1681
|
+
"(function(){",
|
|
1682
|
+
"try {",
|
|
1683
|
+
"if (window.SagepilotMobileBridge) {",
|
|
1684
|
+
`window.SagepilotMobileBridge.capabilities = Object.assign({}, window.SagepilotMobileBridge.capabilities, ${JSON.stringify(payload)});`,
|
|
1685
|
+
"}",
|
|
1686
|
+
"} catch (_) {}",
|
|
1687
|
+
"true;",
|
|
1688
|
+
"})();"
|
|
1689
|
+
].join("\n");
|
|
1690
|
+
}
|
|
1691
|
+
function buildLivenessPingScript(nonce, staleMs) {
|
|
1692
|
+
return [
|
|
1693
|
+
"(function(){",
|
|
1694
|
+
"try {",
|
|
1695
|
+
"var hb = window.__sagepilotWidgetHeartbeat;",
|
|
1696
|
+
"var supports = typeof hb === 'number';",
|
|
1697
|
+
`var alive = !supports ? true : (Date.now() - hb < ${Math.max(0, Math.floor(staleMs))});`,
|
|
1698
|
+
"var send = function(){",
|
|
1699
|
+
" var msg = JSON.stringify({ type: 'sagepilot:pong', nonce: " + JSON.stringify(nonce) + ", alive: alive, supportsHeartbeat: supports });",
|
|
1700
|
+
" if (window.ReactNativeWebView && window.ReactNativeWebView.postMessage) { window.ReactNativeWebView.postMessage(msg); }",
|
|
1701
|
+
"};",
|
|
1702
|
+
"send();",
|
|
1703
|
+
"} catch (_) {}",
|
|
1704
|
+
"true;",
|
|
1705
|
+
"})();"
|
|
1706
|
+
].join("\n");
|
|
1707
|
+
}
|
|
1197
1708
|
function isHostedBridgeMessage(value) {
|
|
1198
1709
|
if (!value || typeof value !== "object") return false;
|
|
1199
1710
|
const message = value;
|
|
@@ -1214,9 +1725,23 @@ function parseHostedBridgeMessage(rawData) {
|
|
|
1214
1725
|
return null;
|
|
1215
1726
|
}
|
|
1216
1727
|
}
|
|
1217
|
-
|
|
1728
|
+
function buildMobileWebViewBridgeScript(capabilities) {
|
|
1729
|
+
const payload = buildBridgeCapabilitiesPayload(capabilities);
|
|
1730
|
+
return `
|
|
1218
1731
|
(function () {
|
|
1219
|
-
|
|
1732
|
+
var bridgeCapabilities = ${JSON.stringify(payload)};
|
|
1733
|
+
var applyBridgeCapabilities = function () {
|
|
1734
|
+
try {
|
|
1735
|
+
if (window.SagepilotMobileBridge) {
|
|
1736
|
+
window.SagepilotMobileBridge.capabilities = Object.assign({}, window.SagepilotMobileBridge.capabilities, bridgeCapabilities);
|
|
1737
|
+
}
|
|
1738
|
+
} catch (error) {}
|
|
1739
|
+
};
|
|
1740
|
+
|
|
1741
|
+
if (window.__sagepilotRnBridgeInstalled) {
|
|
1742
|
+
applyBridgeCapabilities();
|
|
1743
|
+
return true;
|
|
1744
|
+
}
|
|
1220
1745
|
window.__sagepilotRnBridgeInstalled = true;
|
|
1221
1746
|
|
|
1222
1747
|
var ensureViewport = function () {
|
|
@@ -1257,9 +1782,11 @@ var mobileWebViewBridgeScript = `
|
|
|
1257
1782
|
};
|
|
1258
1783
|
|
|
1259
1784
|
window.SagepilotMobileBridge = {
|
|
1785
|
+
capabilities: bridgeCapabilities,
|
|
1260
1786
|
postMessage: sendToReactNative,
|
|
1261
1787
|
ready: function () { sendToReactNative({ type: "sagepilot:ready" }); }
|
|
1262
1788
|
};
|
|
1789
|
+
applyBridgeCapabilities();
|
|
1263
1790
|
|
|
1264
1791
|
document.addEventListener("click", function (event) {
|
|
1265
1792
|
try {
|
|
@@ -1300,6 +1827,7 @@ var mobileWebViewBridgeScript = `
|
|
|
1300
1827
|
return true;
|
|
1301
1828
|
})();
|
|
1302
1829
|
`;
|
|
1830
|
+
}
|
|
1303
1831
|
|
|
1304
1832
|
// src/specs/SagepilotInsetsViewNativeComponent.ts
|
|
1305
1833
|
var import_codegenNativeComponent = __toESM(require("react-native/Libraries/Utilities/codegenNativeComponent"));
|
|
@@ -1316,12 +1844,133 @@ function readPresentationState() {
|
|
|
1316
1844
|
presentation: internalSagepilotChat.getConfig()?.presentation
|
|
1317
1845
|
};
|
|
1318
1846
|
}
|
|
1847
|
+
function getBridgeCapabilities() {
|
|
1848
|
+
return {
|
|
1849
|
+
nativeFilePicker: Boolean(internalSagepilotChat.getConfig()?.filePicker)
|
|
1850
|
+
};
|
|
1851
|
+
}
|
|
1319
1852
|
function getInjectedWebViewScript() {
|
|
1853
|
+
const capabilities = getBridgeCapabilities();
|
|
1320
1854
|
return [
|
|
1321
1855
|
internalSagepilotChat.getHostedAuthScript(),
|
|
1322
|
-
|
|
1856
|
+
buildMobileWebViewBridgeScript(capabilities),
|
|
1857
|
+
buildBridgeCapabilitiesScript(capabilities)
|
|
1323
1858
|
].filter(Boolean).join("\n");
|
|
1324
1859
|
}
|
|
1860
|
+
var FILE_PICKER_SOURCE_LABELS = {
|
|
1861
|
+
camera: "Take photo",
|
|
1862
|
+
library: "Choose from gallery",
|
|
1863
|
+
documents: "Browse files"
|
|
1864
|
+
};
|
|
1865
|
+
var FILE_PICKER_SOURCE_DESCRIPTIONS = {
|
|
1866
|
+
camera: "Use the in-app camera",
|
|
1867
|
+
library: "Photos and videos on this device",
|
|
1868
|
+
documents: "PDFs, documents, and Drive files"
|
|
1869
|
+
};
|
|
1870
|
+
var FILE_PICKER_SOURCE_ICONS = {
|
|
1871
|
+
camera: import_lucide_react_native.Camera,
|
|
1872
|
+
library: import_lucide_react_native.Images,
|
|
1873
|
+
documents: import_lucide_react_native.FileText
|
|
1874
|
+
};
|
|
1875
|
+
var FILE_PICKER_SOURCE_ICON_COLORS = {
|
|
1876
|
+
camera: "#0284c7",
|
|
1877
|
+
library: "#16a34a",
|
|
1878
|
+
documents: "#7c3aed"
|
|
1879
|
+
};
|
|
1880
|
+
var DEBUG_PREFIX2 = "[SagepilotSDK][Provider]";
|
|
1881
|
+
var DELIVERY_RETRY_MS = 1500;
|
|
1882
|
+
var DELIVERY_LEGACY_FALLBACK_ATTEMPTS = 2;
|
|
1883
|
+
var DELIVERY_MAX_ATTEMPTS = 8;
|
|
1884
|
+
var PERSIST_MAX_TOTAL_BYTES = 2 * 1024 * 1024;
|
|
1885
|
+
var PENDING_FILES_CACHE_NAMESPACE = "sagepilot_rn_picker";
|
|
1886
|
+
var PENDING_FILES_CACHE_KEY = "pending_batch";
|
|
1887
|
+
var LIVENESS_HEARTBEAT_STALE_MS = 4e3;
|
|
1888
|
+
var LIVENESS_PONG_TIMEOUT_MS = 1500;
|
|
1889
|
+
var LIVENESS_MAX_PING_ATTEMPTS = 2;
|
|
1890
|
+
var LIVENESS_PICKER_GRACE_MS = 8e3;
|
|
1891
|
+
var LIVENESS_MAX_REMOUNTS = 2;
|
|
1892
|
+
var batchSequence = 0;
|
|
1893
|
+
function nextBatchId() {
|
|
1894
|
+
batchSequence += 1;
|
|
1895
|
+
const entropy = Math.random().toString(36).slice(2, 8);
|
|
1896
|
+
return `b_${Date.now().toString(36)}_${batchSequence}_${entropy}`;
|
|
1897
|
+
}
|
|
1898
|
+
function totalBase64Bytes(files) {
|
|
1899
|
+
return files.reduce((sum, file) => sum + (file.data_base64?.length ?? 0), 0);
|
|
1900
|
+
}
|
|
1901
|
+
function storageKeyFor(batchId, index) {
|
|
1902
|
+
return `${batchId}_${index}.bin`;
|
|
1903
|
+
}
|
|
1904
|
+
function renderFilePickerSourceIcon(source) {
|
|
1905
|
+
const Icon = FILE_PICKER_SOURCE_ICONS[source];
|
|
1906
|
+
return (0, import_react.createElement)(
|
|
1907
|
+
import_react_native3.View,
|
|
1908
|
+
{ style: getFilePickerSourceIconStyle(source) },
|
|
1909
|
+
(0, import_react.createElement)(Icon, {
|
|
1910
|
+
color: FILE_PICKER_SOURCE_ICON_COLORS[source],
|
|
1911
|
+
size: 23,
|
|
1912
|
+
strokeWidth: 2.35
|
|
1913
|
+
})
|
|
1914
|
+
);
|
|
1915
|
+
}
|
|
1916
|
+
function getFilePickerSourceIconStyle(source) {
|
|
1917
|
+
if (source === "camera") return [styles.sheetOptionIcon, styles.cameraOptionIcon];
|
|
1918
|
+
if (source === "library") return [styles.sheetOptionIcon, styles.libraryOptionIcon];
|
|
1919
|
+
return [styles.sheetOptionIcon, styles.documentsOptionIcon];
|
|
1920
|
+
}
|
|
1921
|
+
function getSheetOptionStyle(pressed) {
|
|
1922
|
+
return [styles.sheetOption, pressed && styles.sheetOptionPressed];
|
|
1923
|
+
}
|
|
1924
|
+
function getSheetCancelStyle(pressed) {
|
|
1925
|
+
return [styles.sheetCancelButton, pressed && styles.sheetCancelButtonPressed];
|
|
1926
|
+
}
|
|
1927
|
+
function buildDispatchScript(messageLiteral) {
|
|
1928
|
+
return [
|
|
1929
|
+
"(function(){",
|
|
1930
|
+
"try {",
|
|
1931
|
+
`var message = ${messageLiteral};`,
|
|
1932
|
+
"window.dispatchEvent(new MessageEvent('message', { data: message, origin: window.location.origin, source: window.parent || window }));",
|
|
1933
|
+
"} catch (_) {}",
|
|
1934
|
+
"true;",
|
|
1935
|
+
"})();"
|
|
1936
|
+
].join("\n");
|
|
1937
|
+
}
|
|
1938
|
+
function buildFilesPickedScript(batch) {
|
|
1939
|
+
return buildDispatchScript(JSON.stringify({
|
|
1940
|
+
type: "sagepilot:files_picked",
|
|
1941
|
+
batch_id: batch.batchId,
|
|
1942
|
+
chat_id: batch.target?.chatId,
|
|
1943
|
+
conversation_id: batch.target?.chatId,
|
|
1944
|
+
files: batch.files
|
|
1945
|
+
}));
|
|
1946
|
+
}
|
|
1947
|
+
function buildFilePickerErrorScript(message, code) {
|
|
1948
|
+
return buildDispatchScript(
|
|
1949
|
+
`{ type: "sagepilot:file_picker_error", message: ${JSON.stringify(message)}${code ? `, code: ${JSON.stringify(code)}` : ""} }`
|
|
1950
|
+
);
|
|
1951
|
+
}
|
|
1952
|
+
function buildFilePickerCancelledScript() {
|
|
1953
|
+
return buildDispatchScript(`{ type: "sagepilot:file_picker_cancelled" }`);
|
|
1954
|
+
}
|
|
1955
|
+
function readFilePickerError(error) {
|
|
1956
|
+
if (error && typeof error === "object") {
|
|
1957
|
+
const candidate = error;
|
|
1958
|
+
const message = typeof candidate.message === "string" && candidate.message ? candidate.message : "Could not attach the selected file.";
|
|
1959
|
+
const code = typeof candidate.code === "string" ? candidate.code : void 0;
|
|
1960
|
+
return { message, code };
|
|
1961
|
+
}
|
|
1962
|
+
return { message: "Could not attach the selected file." };
|
|
1963
|
+
}
|
|
1964
|
+
function debugProvider(message, details) {
|
|
1965
|
+
console.log(`${DEBUG_PREFIX2} ${message}`, details ?? "");
|
|
1966
|
+
}
|
|
1967
|
+
function describeAttachmentTarget(target) {
|
|
1968
|
+
return {
|
|
1969
|
+
targetChatId: target?.chatId,
|
|
1970
|
+
targetRouteChatId: target?.routeChatId,
|
|
1971
|
+
targetScreen: target?.hostedChatView.screen
|
|
1972
|
+
};
|
|
1973
|
+
}
|
|
1325
1974
|
function getHostedIdentityDispatchScript() {
|
|
1326
1975
|
const message = internalSagepilotChat.getHostedIdentityMessage();
|
|
1327
1976
|
if (!message) return "";
|
|
@@ -1344,10 +1993,13 @@ function readUrlOrigin(url) {
|
|
|
1344
1993
|
return null;
|
|
1345
1994
|
}
|
|
1346
1995
|
}
|
|
1996
|
+
function isInternalWebViewScheme(url) {
|
|
1997
|
+
return url.startsWith("about:") || url.startsWith("data:") || url.startsWith("blob:") || url.startsWith("javascript:") || url.startsWith("file:");
|
|
1998
|
+
}
|
|
1347
1999
|
var hostedChatWebViewProps = {
|
|
1348
2000
|
allowFileAccess: true,
|
|
1349
2001
|
allowFileAccessFromFileURLs: true,
|
|
1350
|
-
...
|
|
2002
|
+
...import_react_native3.Platform.OS === "ios" ? {
|
|
1351
2003
|
automaticallyAdjustContentInsets: false,
|
|
1352
2004
|
contentInsetAdjustmentBehavior: "never",
|
|
1353
2005
|
hideKeyboardAccessoryView: true
|
|
@@ -1362,7 +2014,7 @@ var hostedChatWebViewProps = {
|
|
|
1362
2014
|
sharedCookiesEnabled: true,
|
|
1363
2015
|
thirdPartyCookiesEnabled: true
|
|
1364
2016
|
};
|
|
1365
|
-
var AndroidInsetsView =
|
|
2017
|
+
var AndroidInsetsView = import_react_native3.Platform.OS === "android" ? SagepilotInsetsViewNativeComponent_default : import_react_native3.View;
|
|
1366
2018
|
var emptyAndroidInsets = { top: 0, bottom: 0 };
|
|
1367
2019
|
function SagepilotChatProvider({
|
|
1368
2020
|
children,
|
|
@@ -1371,13 +2023,389 @@ function SagepilotChatProvider({
|
|
|
1371
2023
|
}) {
|
|
1372
2024
|
const [state, setState] = (0, import_react.useState)(readPresentationState);
|
|
1373
2025
|
const [androidModalInsets, setAndroidModalInsets] = (0, import_react.useState)(emptyAndroidInsets);
|
|
2026
|
+
const [nativeWebViewKey, setNativeWebViewKey] = (0, import_react.useState)(0);
|
|
2027
|
+
const [preloadWebViewKey, setPreloadWebViewKey] = (0, import_react.useState)(0);
|
|
1374
2028
|
const webFrameRef = (0, import_react.useRef)(null);
|
|
1375
2029
|
const nativeWebViewRef = (0, import_react.useRef)(null);
|
|
2030
|
+
const pendingBatchesRef = (0, import_react.useRef)([]);
|
|
2031
|
+
const widgetReadyRef = (0, import_react.useRef)(false);
|
|
2032
|
+
const deliveryTimerRef = (0, import_react.useRef)(null);
|
|
2033
|
+
const deliveryAttemptsRef = (0, import_react.useRef)(0);
|
|
2034
|
+
const pendingPingRef = (0, import_react.useRef)(null);
|
|
2035
|
+
const pingTimeoutRef = (0, import_react.useRef)(null);
|
|
2036
|
+
const livenessResumeTimerRef = (0, import_react.useRef)(null);
|
|
2037
|
+
const livenessPausedUntilRef = (0, import_react.useRef)(0);
|
|
2038
|
+
const pickerInFlightRef = (0, import_react.useRef)(false);
|
|
2039
|
+
const livenessRemountCountRef = (0, import_react.useRef)(0);
|
|
2040
|
+
const appStateRef = (0, import_react.useRef)(import_react_native3.AppState.currentState);
|
|
2041
|
+
const didReconcileRef = (0, import_react.useRef)(false);
|
|
2042
|
+
const [androidRepaintTick, setAndroidRepaintTick] = (0, import_react.useState)(0);
|
|
2043
|
+
const [sourceChooser, setSourceChooser] = (0, import_react.useState)(null);
|
|
2044
|
+
const getPendingCache = (0, import_react.useCallback)(() => {
|
|
2045
|
+
const cacheStorage = internalSagepilotChat.getConfig()?.cacheStorage;
|
|
2046
|
+
if (!cacheStorage) return null;
|
|
2047
|
+
return createJsonCache(cacheStorage, PENDING_FILES_CACHE_NAMESPACE);
|
|
2048
|
+
}, []);
|
|
2049
|
+
const writeManifest = (0, import_react.useCallback)(() => {
|
|
2050
|
+
const cache = getPendingCache();
|
|
2051
|
+
if (!cache) return;
|
|
2052
|
+
const queue = pendingBatchesRef.current;
|
|
2053
|
+
if (queue.length === 0) {
|
|
2054
|
+
void cache.remove(PENDING_FILES_CACHE_KEY).catch(() => void 0);
|
|
2055
|
+
return;
|
|
2056
|
+
}
|
|
2057
|
+
const hasFileStore = Boolean(internalSagepilotChat.getConfig()?.fileStore);
|
|
2058
|
+
if (hasFileStore) {
|
|
2059
|
+
const manifest = queue.filter((batch) => batch.storageKeys && batch.storageKeys.length === batch.files.length).map((batch) => ({
|
|
2060
|
+
batchId: batch.batchId,
|
|
2061
|
+
target: batch.target,
|
|
2062
|
+
files: batch.files.map((file, index) => ({
|
|
2063
|
+
file_name: file.file_name,
|
|
2064
|
+
mime_type: file.mime_type,
|
|
2065
|
+
size: file.size,
|
|
2066
|
+
storageKey: batch.storageKeys[index]
|
|
2067
|
+
}))
|
|
2068
|
+
}));
|
|
2069
|
+
if (manifest.length === 0) {
|
|
2070
|
+
void cache.remove(PENDING_FILES_CACHE_KEY).catch(() => void 0);
|
|
2071
|
+
} else {
|
|
2072
|
+
void cache.set(PENDING_FILES_CACHE_KEY, manifest).catch(() => void 0);
|
|
2073
|
+
}
|
|
2074
|
+
return;
|
|
2075
|
+
}
|
|
2076
|
+
const totalBytes = queue.reduce((sum, batch) => sum + totalBase64Bytes(batch.files), 0);
|
|
2077
|
+
if (totalBytes <= PERSIST_MAX_TOTAL_BYTES) {
|
|
2078
|
+
const manifest = queue.map((batch) => ({
|
|
2079
|
+
batchId: batch.batchId,
|
|
2080
|
+
target: batch.target,
|
|
2081
|
+
files: batch.files.map((file) => ({
|
|
2082
|
+
file_name: file.file_name,
|
|
2083
|
+
mime_type: file.mime_type,
|
|
2084
|
+
size: file.size,
|
|
2085
|
+
data_base64: file.data_base64
|
|
2086
|
+
}))
|
|
2087
|
+
}));
|
|
2088
|
+
void cache.set(PENDING_FILES_CACHE_KEY, manifest).catch(() => void 0);
|
|
2089
|
+
} else {
|
|
2090
|
+
void cache.remove(PENDING_FILES_CACHE_KEY).catch(() => void 0);
|
|
2091
|
+
}
|
|
2092
|
+
}, [getPendingCache]);
|
|
2093
|
+
const writeBatchBytes = (0, import_react.useCallback)(async (batch) => {
|
|
2094
|
+
const fileStore = internalSagepilotChat.getConfig()?.fileStore;
|
|
2095
|
+
if (!fileStore) return;
|
|
2096
|
+
const entries = batch.files.map((file, index) => ({
|
|
2097
|
+
key: storageKeyFor(batch.batchId, index),
|
|
2098
|
+
base64: file.data_base64
|
|
2099
|
+
}));
|
|
2100
|
+
try {
|
|
2101
|
+
await Promise.all(entries.map((entry) => fileStore.write(entry.key, entry.base64)));
|
|
2102
|
+
} catch {
|
|
2103
|
+
return;
|
|
2104
|
+
}
|
|
2105
|
+
if (!pendingBatchesRef.current.some((queued) => queued.batchId === batch.batchId)) {
|
|
2106
|
+
void Promise.all(entries.map((entry) => fileStore.remove(entry.key).catch(() => void 0)));
|
|
2107
|
+
}
|
|
2108
|
+
}, []);
|
|
2109
|
+
const discardBatchFiles = (0, import_react.useCallback)((batch) => {
|
|
2110
|
+
if (!batch?.storageKeys) return;
|
|
2111
|
+
const fileStore = internalSagepilotChat.getConfig()?.fileStore;
|
|
2112
|
+
if (!fileStore) return;
|
|
2113
|
+
void Promise.all(batch.storageKeys.map((key) => fileStore.remove(key).catch(() => void 0)));
|
|
2114
|
+
}, []);
|
|
2115
|
+
const pumpDelivery = (0, import_react.useCallback)(() => {
|
|
2116
|
+
if (deliveryTimerRef.current) {
|
|
2117
|
+
clearTimeout(deliveryTimerRef.current);
|
|
2118
|
+
deliveryTimerRef.current = null;
|
|
2119
|
+
}
|
|
2120
|
+
const batch = pendingBatchesRef.current[0];
|
|
2121
|
+
if (!batch) return;
|
|
2122
|
+
if (internalSagepilotChat.restoreHostedAttachmentTarget(batch.target)) {
|
|
2123
|
+
debugProvider("restored hosted attachment target", {
|
|
2124
|
+
batchId: batch.batchId,
|
|
2125
|
+
...describeAttachmentTarget(batch.target)
|
|
2126
|
+
});
|
|
2127
|
+
widgetReadyRef.current = false;
|
|
2128
|
+
deliveryTimerRef.current = setTimeout(() => pumpDelivery(), DELIVERY_RETRY_MS);
|
|
2129
|
+
return;
|
|
2130
|
+
}
|
|
2131
|
+
deliveryAttemptsRef.current += 1;
|
|
2132
|
+
const attempts = deliveryAttemptsRef.current;
|
|
2133
|
+
const ref = nativeWebViewRef.current;
|
|
2134
|
+
const shouldDeliver = ref && (widgetReadyRef.current || attempts >= DELIVERY_LEGACY_FALLBACK_ATTEMPTS);
|
|
2135
|
+
if (shouldDeliver && ref) {
|
|
2136
|
+
debugProvider("delivering native picked files", {
|
|
2137
|
+
batchId: batch.batchId,
|
|
2138
|
+
attempt: attempts,
|
|
2139
|
+
widgetReady: widgetReadyRef.current,
|
|
2140
|
+
count: batch.files.length,
|
|
2141
|
+
...describeAttachmentTarget(batch.target)
|
|
2142
|
+
});
|
|
2143
|
+
ref.injectJavaScript(buildFilesPickedScript(batch));
|
|
2144
|
+
}
|
|
2145
|
+
if (attempts < DELIVERY_MAX_ATTEMPTS) {
|
|
2146
|
+
deliveryTimerRef.current = setTimeout(() => pumpDelivery(), DELIVERY_RETRY_MS);
|
|
2147
|
+
}
|
|
2148
|
+
}, []);
|
|
2149
|
+
const startDelivery = (0, import_react.useCallback)(() => {
|
|
2150
|
+
deliveryAttemptsRef.current = 0;
|
|
2151
|
+
pumpDelivery();
|
|
2152
|
+
}, [pumpDelivery]);
|
|
2153
|
+
const ensureDelivery = (0, import_react.useCallback)(() => {
|
|
2154
|
+
if (pendingBatchesRef.current.length === 0) return;
|
|
2155
|
+
if (deliveryTimerRef.current) return;
|
|
2156
|
+
startDelivery();
|
|
2157
|
+
}, [startDelivery]);
|
|
2158
|
+
const acknowledgeHeadBatch = (0, import_react.useCallback)(() => {
|
|
2159
|
+
const head = pendingBatchesRef.current[0];
|
|
2160
|
+
pendingBatchesRef.current = pendingBatchesRef.current.slice(1);
|
|
2161
|
+
deliveryAttemptsRef.current = 0;
|
|
2162
|
+
if (deliveryTimerRef.current) {
|
|
2163
|
+
clearTimeout(deliveryTimerRef.current);
|
|
2164
|
+
deliveryTimerRef.current = null;
|
|
2165
|
+
}
|
|
2166
|
+
discardBatchFiles(head);
|
|
2167
|
+
writeManifest();
|
|
2168
|
+
if (pendingBatchesRef.current.length > 0) startDelivery();
|
|
2169
|
+
}, [discardBatchFiles, writeManifest, startDelivery]);
|
|
2170
|
+
const queuePickedFiles = (0, import_react.useCallback)((files) => {
|
|
2171
|
+
const batchId = nextBatchId();
|
|
2172
|
+
const hasFileStore = Boolean(internalSagepilotChat.getConfig()?.fileStore);
|
|
2173
|
+
const storageKeys = hasFileStore ? files.map((_, index) => storageKeyFor(batchId, index)) : void 0;
|
|
2174
|
+
const batch = {
|
|
2175
|
+
batchId,
|
|
2176
|
+
files,
|
|
2177
|
+
storageKeys,
|
|
2178
|
+
target: internalSagepilotChat.getHostedAttachmentTarget()
|
|
2179
|
+
};
|
|
2180
|
+
debugProvider("queued native picked files", {
|
|
2181
|
+
batchId,
|
|
2182
|
+
count: files.length,
|
|
2183
|
+
totalBase64Bytes: totalBase64Bytes(files),
|
|
2184
|
+
persistedToFileStore: hasFileStore,
|
|
2185
|
+
...describeAttachmentTarget(batch.target)
|
|
2186
|
+
});
|
|
2187
|
+
pendingBatchesRef.current = [...pendingBatchesRef.current, batch];
|
|
2188
|
+
ensureDelivery();
|
|
2189
|
+
writeManifest();
|
|
2190
|
+
void writeBatchBytes(batch);
|
|
2191
|
+
}, [ensureDelivery, writeManifest, writeBatchBytes]);
|
|
2192
|
+
const recoverNativeWebView = (0, import_react.useCallback)((reasonOrEvent) => {
|
|
2193
|
+
const reason = typeof reasonOrEvent === "string" ? reasonOrEvent : "webview_render_process";
|
|
2194
|
+
debugProvider("recovering native WebView", { reason });
|
|
2195
|
+
widgetReadyRef.current = false;
|
|
2196
|
+
setNativeWebViewKey((key) => key + 1);
|
|
2197
|
+
}, []);
|
|
2198
|
+
const recoverPreloadWebView = (0, import_react.useCallback)(() => {
|
|
2199
|
+
setPreloadWebViewKey((key) => key + 1);
|
|
2200
|
+
}, []);
|
|
2201
|
+
const pauseLivenessAfterPicker = (0, import_react.useCallback)((durationMs) => {
|
|
2202
|
+
if (import_react_native3.Platform.OS !== "android") return;
|
|
2203
|
+
livenessPausedUntilRef.current = Math.max(livenessPausedUntilRef.current, Date.now() + durationMs);
|
|
2204
|
+
}, []);
|
|
2205
|
+
const runNativeFilePicker = (0, import_react.useCallback)((source, multiple) => {
|
|
2206
|
+
const filePicker = internalSagepilotChat.getConfig()?.filePicker;
|
|
2207
|
+
if (!filePicker) {
|
|
2208
|
+
debugProvider("native file picker requested but no adapter is configured", { source, multiple });
|
|
2209
|
+
return;
|
|
2210
|
+
}
|
|
2211
|
+
if (pickerInFlightRef.current) {
|
|
2212
|
+
debugProvider("native file picker ignored because another picker is in flight", { source, multiple });
|
|
2213
|
+
return;
|
|
2214
|
+
}
|
|
2215
|
+
debugProvider("native file picker starting", {
|
|
2216
|
+
source,
|
|
2217
|
+
multiple,
|
|
2218
|
+
configuredSources: filePicker.sources
|
|
2219
|
+
});
|
|
2220
|
+
pauseLivenessAfterPicker(6e4);
|
|
2221
|
+
pickerInFlightRef.current = true;
|
|
2222
|
+
filePicker.pickFiles({ source, multiple }).then((files) => {
|
|
2223
|
+
debugProvider("native file picker resolved", {
|
|
2224
|
+
source,
|
|
2225
|
+
count: files.length,
|
|
2226
|
+
files: files.map((file) => ({
|
|
2227
|
+
fileName: file.file_name,
|
|
2228
|
+
mimeType: file.mime_type,
|
|
2229
|
+
size: file.size,
|
|
2230
|
+
base64Length: file.data_base64.length
|
|
2231
|
+
}))
|
|
2232
|
+
});
|
|
2233
|
+
if (files.length === 0) {
|
|
2234
|
+
nativeWebViewRef.current?.injectJavaScript(buildFilePickerCancelledScript());
|
|
2235
|
+
return;
|
|
2236
|
+
}
|
|
2237
|
+
queuePickedFiles(files);
|
|
2238
|
+
}).catch((error) => {
|
|
2239
|
+
const { message, code } = readFilePickerError(error);
|
|
2240
|
+
debugProvider("native file picker failed", { source, code, message });
|
|
2241
|
+
nativeWebViewRef.current?.injectJavaScript(buildFilePickerErrorScript(message, code));
|
|
2242
|
+
}).finally(() => {
|
|
2243
|
+
pickerInFlightRef.current = false;
|
|
2244
|
+
pauseLivenessAfterPicker(LIVENESS_PICKER_GRACE_MS);
|
|
2245
|
+
});
|
|
2246
|
+
}, [pauseLivenessAfterPicker, queuePickedFiles]);
|
|
2247
|
+
const openNativeFilePicker = (0, import_react.useCallback)((multiple) => {
|
|
2248
|
+
const filePicker = internalSagepilotChat.getConfig()?.filePicker;
|
|
2249
|
+
if (!filePicker || filePicker.sources.length === 0) {
|
|
2250
|
+
debugProvider("open native file picker ignored", {
|
|
2251
|
+
multiple,
|
|
2252
|
+
hasAdapter: Boolean(filePicker),
|
|
2253
|
+
sources: filePicker?.sources ?? []
|
|
2254
|
+
});
|
|
2255
|
+
return;
|
|
2256
|
+
}
|
|
2257
|
+
debugProvider("open native file picker", { multiple, sources: filePicker.sources });
|
|
2258
|
+
const [onlySource] = filePicker.sources;
|
|
2259
|
+
if (filePicker.sources.length === 1 && onlySource && onlySource !== "camera") {
|
|
2260
|
+
runNativeFilePicker(onlySource, multiple);
|
|
2261
|
+
return;
|
|
2262
|
+
}
|
|
2263
|
+
setSourceChooser({ multiple });
|
|
2264
|
+
}, [runNativeFilePicker]);
|
|
2265
|
+
const handleSourceChoice = (0, import_react.useCallback)((source) => {
|
|
2266
|
+
const chooser = sourceChooser;
|
|
2267
|
+
setSourceChooser(null);
|
|
2268
|
+
if (chooser) runNativeFilePicker(source, chooser.multiple);
|
|
2269
|
+
}, [sourceChooser, runNativeFilePicker]);
|
|
1376
2270
|
(0, import_react.useEffect)(() => {
|
|
1377
2271
|
return internalSagepilotChat.onStateChange(() => {
|
|
1378
2272
|
setState(readPresentationState());
|
|
1379
2273
|
});
|
|
1380
2274
|
}, []);
|
|
2275
|
+
(0, import_react.useEffect)(() => {
|
|
2276
|
+
let cancelled = false;
|
|
2277
|
+
const cache = getPendingCache();
|
|
2278
|
+
if (!cache) return;
|
|
2279
|
+
const reconcile = async () => {
|
|
2280
|
+
if (didReconcileRef.current) return;
|
|
2281
|
+
const manifest = await cache.get(PENDING_FILES_CACHE_KEY).catch(() => null);
|
|
2282
|
+
if (cancelled || didReconcileRef.current || !Array.isArray(manifest) || manifest.length === 0) return;
|
|
2283
|
+
const fileStore = internalSagepilotChat.getConfig()?.fileStore;
|
|
2284
|
+
const restored = [];
|
|
2285
|
+
for (const batch of manifest) {
|
|
2286
|
+
if (!batch || typeof batch.batchId !== "string" || !Array.isArray(batch.files) || batch.files.length === 0) continue;
|
|
2287
|
+
const files = [];
|
|
2288
|
+
const storageKeys = [];
|
|
2289
|
+
let intact = true;
|
|
2290
|
+
for (const file of batch.files) {
|
|
2291
|
+
let dataBase64 = null;
|
|
2292
|
+
if (typeof file.storageKey === "string" && fileStore) {
|
|
2293
|
+
try {
|
|
2294
|
+
dataBase64 = await fileStore.read(file.storageKey);
|
|
2295
|
+
storageKeys.push(file.storageKey);
|
|
2296
|
+
} catch {
|
|
2297
|
+
intact = false;
|
|
2298
|
+
}
|
|
2299
|
+
} else if (typeof file.data_base64 === "string" && file.data_base64) {
|
|
2300
|
+
dataBase64 = file.data_base64;
|
|
2301
|
+
}
|
|
2302
|
+
if (!dataBase64) {
|
|
2303
|
+
intact = false;
|
|
2304
|
+
break;
|
|
2305
|
+
}
|
|
2306
|
+
files.push({ file_name: file.file_name, mime_type: file.mime_type, size: file.size, data_base64: dataBase64 });
|
|
2307
|
+
}
|
|
2308
|
+
if (intact && files.length === batch.files.length) {
|
|
2309
|
+
restored.push({
|
|
2310
|
+
batchId: batch.batchId,
|
|
2311
|
+
files,
|
|
2312
|
+
storageKeys: storageKeys.length === files.length ? storageKeys : void 0,
|
|
2313
|
+
target: batch.target
|
|
2314
|
+
});
|
|
2315
|
+
}
|
|
2316
|
+
}
|
|
2317
|
+
if (cancelled || didReconcileRef.current) return;
|
|
2318
|
+
didReconcileRef.current = true;
|
|
2319
|
+
pendingBatchesRef.current = [...restored, ...pendingBatchesRef.current];
|
|
2320
|
+
writeManifest();
|
|
2321
|
+
if (fileStore) {
|
|
2322
|
+
const keep = pendingBatchesRef.current.flatMap(
|
|
2323
|
+
(batch) => batch.files.map((_, index) => storageKeyFor(batch.batchId, index))
|
|
2324
|
+
);
|
|
2325
|
+
void fileStore.prune(keep).catch(() => void 0);
|
|
2326
|
+
}
|
|
2327
|
+
startDelivery();
|
|
2328
|
+
};
|
|
2329
|
+
void reconcile();
|
|
2330
|
+
return () => {
|
|
2331
|
+
cancelled = true;
|
|
2332
|
+
};
|
|
2333
|
+
}, [getPendingCache, startDelivery, writeManifest]);
|
|
2334
|
+
const clearPing = (0, import_react.useCallback)(() => {
|
|
2335
|
+
pendingPingRef.current = null;
|
|
2336
|
+
if (pingTimeoutRef.current) {
|
|
2337
|
+
clearTimeout(pingTimeoutRef.current);
|
|
2338
|
+
pingTimeoutRef.current = null;
|
|
2339
|
+
}
|
|
2340
|
+
}, []);
|
|
2341
|
+
const clearDeferredLivenessProbe = (0, import_react.useCallback)(() => {
|
|
2342
|
+
if (livenessResumeTimerRef.current) {
|
|
2343
|
+
clearTimeout(livenessResumeTimerRef.current);
|
|
2344
|
+
livenessResumeTimerRef.current = null;
|
|
2345
|
+
}
|
|
2346
|
+
}, []);
|
|
2347
|
+
(0, import_react.useEffect)(() => {
|
|
2348
|
+
return () => {
|
|
2349
|
+
if (deliveryTimerRef.current) {
|
|
2350
|
+
clearTimeout(deliveryTimerRef.current);
|
|
2351
|
+
deliveryTimerRef.current = null;
|
|
2352
|
+
}
|
|
2353
|
+
if (pingTimeoutRef.current) {
|
|
2354
|
+
clearTimeout(pingTimeoutRef.current);
|
|
2355
|
+
pingTimeoutRef.current = null;
|
|
2356
|
+
}
|
|
2357
|
+
clearDeferredLivenessProbe();
|
|
2358
|
+
};
|
|
2359
|
+
}, [clearDeferredLivenessProbe]);
|
|
2360
|
+
const runLivenessProbe = (0, import_react.useCallback)(() => {
|
|
2361
|
+
if (import_react_native3.Platform.OS !== "android") return;
|
|
2362
|
+
const ref = nativeWebViewRef.current;
|
|
2363
|
+
if (!ref || !internalSagepilotChat.isPresented()) return;
|
|
2364
|
+
const pauseRemainingMs = livenessPausedUntilRef.current - Date.now();
|
|
2365
|
+
if (pickerInFlightRef.current || pauseRemainingMs > 0) {
|
|
2366
|
+
const retryDelayMs = pickerInFlightRef.current ? 500 : Math.max(0, pauseRemainingMs);
|
|
2367
|
+
debugProvider("liveness probe deferred after picker", {
|
|
2368
|
+
pickerInFlight: pickerInFlightRef.current,
|
|
2369
|
+
retryDelayMs
|
|
2370
|
+
});
|
|
2371
|
+
clearDeferredLivenessProbe();
|
|
2372
|
+
livenessResumeTimerRef.current = setTimeout(() => {
|
|
2373
|
+
livenessResumeTimerRef.current = null;
|
|
2374
|
+
runLivenessProbe();
|
|
2375
|
+
}, retryDelayMs);
|
|
2376
|
+
return;
|
|
2377
|
+
}
|
|
2378
|
+
setAndroidRepaintTick((tick) => tick + 1);
|
|
2379
|
+
const attempts = (pendingPingRef.current?.attempts ?? 0) + 1;
|
|
2380
|
+
const nonce = `${Date.now().toString(36)}_${attempts}`;
|
|
2381
|
+
pendingPingRef.current = { nonce, attempts };
|
|
2382
|
+
ref.injectJavaScript(buildLivenessPingScript(nonce, LIVENESS_HEARTBEAT_STALE_MS));
|
|
2383
|
+
if (pingTimeoutRef.current) clearTimeout(pingTimeoutRef.current);
|
|
2384
|
+
pingTimeoutRef.current = setTimeout(() => {
|
|
2385
|
+
if (attempts >= LIVENESS_MAX_PING_ATTEMPTS) {
|
|
2386
|
+
clearPing();
|
|
2387
|
+
recoverNativeWebView("liveness_timeout");
|
|
2388
|
+
return;
|
|
2389
|
+
}
|
|
2390
|
+
runLivenessProbe();
|
|
2391
|
+
}, LIVENESS_PONG_TIMEOUT_MS);
|
|
2392
|
+
}, [clearDeferredLivenessProbe, clearPing, recoverNativeWebView]);
|
|
2393
|
+
(0, import_react.useEffect)(() => {
|
|
2394
|
+
const subscription = import_react_native3.AppState.addEventListener("change", (nextState) => {
|
|
2395
|
+
const prev = appStateRef.current;
|
|
2396
|
+
appStateRef.current = nextState;
|
|
2397
|
+
if (nextState === "active" && (prev === "background" || prev === "inactive")) {
|
|
2398
|
+
clearPing();
|
|
2399
|
+
livenessRemountCountRef.current = 0;
|
|
2400
|
+
runLivenessProbe();
|
|
2401
|
+
ensureDelivery();
|
|
2402
|
+
}
|
|
2403
|
+
});
|
|
2404
|
+
return () => {
|
|
2405
|
+
subscription.remove();
|
|
2406
|
+
clearPing();
|
|
2407
|
+
};
|
|
2408
|
+
}, [clearPing, runLivenessProbe, ensureDelivery]);
|
|
1381
2409
|
const handleAndroidInsetsChange = (0, import_react.useCallback)((event) => {
|
|
1382
2410
|
const nextBottomInset = event.nativeEvent?.bottom;
|
|
1383
2411
|
const nextTopInset = event.nativeEvent?.top;
|
|
@@ -1391,11 +2419,11 @@ function SagepilotChatProvider({
|
|
|
1391
2419
|
const presentationStyle = state.presentation?.style ?? "sheet";
|
|
1392
2420
|
const isFullScreenModal = presentationStyle === "fullScreen";
|
|
1393
2421
|
const animationType = presentationStyle === "fullScreen" || presentationStyle === "push" ? "slide" : "fade";
|
|
1394
|
-
const ModalContainer =
|
|
1395
|
-
const NativeModalContainer =
|
|
1396
|
-
const ChatContentContainer =
|
|
1397
|
-
const nativeModalContainerProps =
|
|
1398
|
-
const chatContentContainerProps =
|
|
2422
|
+
const ModalContainer = import_react_native3.Platform.OS === "ios" && isFullScreenModal ? import_react_native3.SafeAreaView : import_react_native3.View;
|
|
2423
|
+
const NativeModalContainer = import_react_native3.Platform.OS === "android" ? AndroidInsetsView : ModalContainer;
|
|
2424
|
+
const ChatContentContainer = import_react_native3.Platform.OS === "ios" ? import_react_native3.KeyboardAvoidingView : import_react_native3.View;
|
|
2425
|
+
const nativeModalContainerProps = import_react_native3.Platform.OS === "android" ? { style: styles.container, onInsetsChange: handleAndroidInsetsChange } : { style: styles.container };
|
|
2426
|
+
const chatContentContainerProps = import_react_native3.Platform.OS === "ios" ? {
|
|
1399
2427
|
behavior: "padding",
|
|
1400
2428
|
enabled: true,
|
|
1401
2429
|
keyboardVerticalOffset: 0,
|
|
@@ -1403,14 +2431,57 @@ function SagepilotChatProvider({
|
|
|
1403
2431
|
} : {
|
|
1404
2432
|
style: [
|
|
1405
2433
|
styles.modalContent,
|
|
1406
|
-
|
|
2434
|
+
import_react_native3.Platform.OS === "android" ? {
|
|
1407
2435
|
paddingTop: androidModalInsets.top,
|
|
1408
2436
|
paddingBottom: androidModalInsets.bottom
|
|
1409
2437
|
} : null
|
|
1410
2438
|
].filter(Boolean)
|
|
1411
2439
|
};
|
|
1412
2440
|
const handleWebViewMessage = (event) => {
|
|
1413
|
-
|
|
2441
|
+
const message = parseHostedBridgeMessage(event.nativeEvent?.data);
|
|
2442
|
+
if (message?.type === "sagepilot:open_file_picker") {
|
|
2443
|
+
openNativeFilePicker(message.multiple ?? true);
|
|
2444
|
+
return;
|
|
2445
|
+
}
|
|
2446
|
+
if (message?.type === "sagepilot:widget_listener_ready") {
|
|
2447
|
+
widgetReadyRef.current = true;
|
|
2448
|
+
startDelivery();
|
|
2449
|
+
return;
|
|
2450
|
+
}
|
|
2451
|
+
if (message?.type === "sagepilot:files_received") {
|
|
2452
|
+
const ackBatchId = message.batch_id;
|
|
2453
|
+
const head = pendingBatchesRef.current[0];
|
|
2454
|
+
if (head && (!ackBatchId || ackBatchId === head.batchId)) {
|
|
2455
|
+
debugProvider("native picked files acknowledged", {
|
|
2456
|
+
batchId: head.batchId,
|
|
2457
|
+
ackBatchId,
|
|
2458
|
+
count: message.count
|
|
2459
|
+
});
|
|
2460
|
+
acknowledgeHeadBatch();
|
|
2461
|
+
}
|
|
2462
|
+
return;
|
|
2463
|
+
}
|
|
2464
|
+
if (message?.type === "sagepilot:pong") {
|
|
2465
|
+
const pending = pendingPingRef.current;
|
|
2466
|
+
if (pending && (!message.nonce || message.nonce === pending.nonce)) {
|
|
2467
|
+
if (message.alive === false) {
|
|
2468
|
+
clearPing();
|
|
2469
|
+
debugProvider("liveness probe reported dead widget", {
|
|
2470
|
+
supportsHeartbeat: message.supportsHeartbeat,
|
|
2471
|
+
remountCount: livenessRemountCountRef.current
|
|
2472
|
+
});
|
|
2473
|
+
if (livenessRemountCountRef.current < LIVENESS_MAX_REMOUNTS) {
|
|
2474
|
+
livenessRemountCountRef.current += 1;
|
|
2475
|
+
recoverNativeWebView("liveness_dead_pong");
|
|
2476
|
+
}
|
|
2477
|
+
} else {
|
|
2478
|
+
livenessRemountCountRef.current = 0;
|
|
2479
|
+
clearPing();
|
|
2480
|
+
}
|
|
2481
|
+
}
|
|
2482
|
+
return;
|
|
2483
|
+
}
|
|
2484
|
+
internalSagepilotChat.handleHostedBridgeMessage(message);
|
|
1414
2485
|
};
|
|
1415
2486
|
const postIdentityToWebFrame = () => {
|
|
1416
2487
|
const message = internalSagepilotChat.getHostedIdentityMessage();
|
|
@@ -1423,8 +2494,31 @@ function SagepilotChatProvider({
|
|
|
1423
2494
|
if (!script || !nativeWebViewRef.current) return;
|
|
1424
2495
|
nativeWebViewRef.current.injectJavaScript(script);
|
|
1425
2496
|
};
|
|
2497
|
+
const injectNativeBridgeToNativeWebView = () => {
|
|
2498
|
+
if (!nativeWebViewRef.current) return;
|
|
2499
|
+
nativeWebViewRef.current.injectJavaScript(getInjectedWebViewScript());
|
|
2500
|
+
};
|
|
2501
|
+
const handleNativeWebViewLoadEnd = () => {
|
|
2502
|
+
debugProvider("native WebView load end");
|
|
2503
|
+
injectNativeBridgeToNativeWebView();
|
|
2504
|
+
postIdentityToNativeWebView();
|
|
2505
|
+
widgetReadyRef.current = false;
|
|
2506
|
+
startDelivery();
|
|
2507
|
+
};
|
|
2508
|
+
const handleShouldStartLoadWithRequest = (0, import_react.useCallback)((request) => {
|
|
2509
|
+
const url = request?.url ?? "";
|
|
2510
|
+
if (!url || request?.isTopFrame === false || isInternalWebViewScheme(url)) {
|
|
2511
|
+
return true;
|
|
2512
|
+
}
|
|
2513
|
+
const widgetOrigin = readUrlOrigin(state.conversationUrl ?? state.preloadUrl);
|
|
2514
|
+
if (widgetOrigin && readUrlOrigin(url) === widgetOrigin) {
|
|
2515
|
+
return true;
|
|
2516
|
+
}
|
|
2517
|
+
import_react_native3.Linking.openURL(url).catch(() => void 0);
|
|
2518
|
+
return false;
|
|
2519
|
+
}, [state.conversationUrl, state.preloadUrl]);
|
|
1426
2520
|
(0, import_react.useEffect)(() => {
|
|
1427
|
-
if (
|
|
2521
|
+
if (import_react_native3.Platform.OS !== "web" || typeof window === "undefined") return;
|
|
1428
2522
|
const trustedWidgetOrigin = readUrlOrigin(state.conversationUrl);
|
|
1429
2523
|
if (!trustedWidgetOrigin) return;
|
|
1430
2524
|
const handleWindowMessage = (event) => {
|
|
@@ -1435,16 +2529,16 @@ function SagepilotChatProvider({
|
|
|
1435
2529
|
return () => window.removeEventListener("message", handleWindowMessage);
|
|
1436
2530
|
}, [state.conversationUrl]);
|
|
1437
2531
|
(0, import_react.useEffect)(() => {
|
|
1438
|
-
if (
|
|
2532
|
+
if (import_react_native3.Platform.OS !== "web" || !state.isPresented) return;
|
|
1439
2533
|
postIdentityToWebFrame();
|
|
1440
2534
|
}, [state.isPresented, state.conversationUrl, state.configured]);
|
|
1441
2535
|
(0, import_react.useEffect)(() => {
|
|
1442
|
-
if (
|
|
2536
|
+
if (import_react_native3.Platform.OS === "web" || !state.isPresented) return;
|
|
1443
2537
|
postIdentityToNativeWebView();
|
|
1444
2538
|
}, [state.isPresented, state.conversationUrl, state.configured]);
|
|
1445
|
-
if (
|
|
2539
|
+
if (import_react_native3.Platform.OS === "web") {
|
|
1446
2540
|
return (0, import_react.createElement)(
|
|
1447
|
-
|
|
2541
|
+
import_react_native3.View,
|
|
1448
2542
|
{ style: styles.root },
|
|
1449
2543
|
children,
|
|
1450
2544
|
state.configured && !state.isPresented && state.shouldPreload && state.preloadUrl ? (0, import_react.createElement)("iframe", {
|
|
@@ -1453,20 +2547,20 @@ function SagepilotChatProvider({
|
|
|
1453
2547
|
title: "Sagepilot chat preload"
|
|
1454
2548
|
}) : null,
|
|
1455
2549
|
state.configured && state.isPresented && state.conversationUrl ? (0, import_react.createElement)(
|
|
1456
|
-
|
|
2550
|
+
import_react_native3.View,
|
|
1457
2551
|
{ style: styles.webOverlay },
|
|
1458
2552
|
(0, import_react.createElement)(
|
|
1459
|
-
|
|
2553
|
+
import_react_native3.View,
|
|
1460
2554
|
{ style: styles.webPanel },
|
|
1461
2555
|
showCloseButton ? (0, import_react.createElement)(
|
|
1462
|
-
|
|
2556
|
+
import_react_native3.Pressable,
|
|
1463
2557
|
{
|
|
1464
2558
|
accessibilityRole: "button",
|
|
1465
2559
|
accessibilityLabel: closeLabel,
|
|
1466
2560
|
onPress: () => internalSagepilotChat.dismiss(),
|
|
1467
2561
|
style: styles.webCloseButton
|
|
1468
2562
|
},
|
|
1469
|
-
(0, import_react.createElement)(
|
|
2563
|
+
(0, import_react.createElement)(import_react_native3.Text, { style: styles.closeText }, closeLabel)
|
|
1470
2564
|
) : null,
|
|
1471
2565
|
(0, import_react.createElement)("iframe", {
|
|
1472
2566
|
ref: webFrameRef,
|
|
@@ -1480,24 +2574,32 @@ function SagepilotChatProvider({
|
|
|
1480
2574
|
);
|
|
1481
2575
|
}
|
|
1482
2576
|
return (0, import_react.createElement)(
|
|
1483
|
-
|
|
2577
|
+
import_react_native3.View,
|
|
1484
2578
|
{ style: styles.root },
|
|
1485
2579
|
children,
|
|
1486
2580
|
state.configured && !state.isPresented && state.shouldPreload && state.preloadUrl ? (0, import_react.createElement)(import_react_native_webview.WebView, {
|
|
1487
2581
|
...hostedChatWebViewProps,
|
|
2582
|
+
// Separate key from the hosted WebView: a hidden-preload renderer crash
|
|
2583
|
+
// must not bump the hosted key (which would reset widgetReadyRef and
|
|
2584
|
+
// disrupt an in-flight delivery). onRenderProcessGone must still be
|
|
2585
|
+
// handled here or an unhandled renderer kill crashes the whole app.
|
|
2586
|
+
key: `sagepilot-preload-webview-${preloadWebViewKey}`,
|
|
1488
2587
|
source: { uri: state.preloadUrl },
|
|
1489
2588
|
style: styles.preloadWebview,
|
|
1490
2589
|
injectedJavaScriptBeforeContentLoaded: getInjectedWebViewScript(),
|
|
1491
|
-
onMessage: handleWebViewMessage
|
|
2590
|
+
onMessage: handleWebViewMessage,
|
|
2591
|
+
onShouldStartLoadWithRequest: handleShouldStartLoadWithRequest,
|
|
2592
|
+
onRenderProcessGone: recoverPreloadWebView,
|
|
2593
|
+
onContentProcessDidTerminate: recoverPreloadWebView
|
|
1492
2594
|
}) : null,
|
|
1493
2595
|
(0, import_react.createElement)(
|
|
1494
|
-
|
|
2596
|
+
import_react_native3.Modal,
|
|
1495
2597
|
{
|
|
1496
2598
|
visible: state.configured && state.isPresented,
|
|
1497
2599
|
animationType,
|
|
1498
2600
|
presentationStyle: isFullScreenModal ? "fullScreen" : "pageSheet",
|
|
1499
|
-
statusBarTranslucent:
|
|
1500
|
-
navigationBarTranslucent:
|
|
2601
|
+
statusBarTranslucent: import_react_native3.Platform.OS === "android",
|
|
2602
|
+
navigationBarTranslucent: import_react_native3.Platform.OS === "android",
|
|
1501
2603
|
onRequestClose: () => internalSagepilotChat.dismiss()
|
|
1502
2604
|
},
|
|
1503
2605
|
(0, import_react.createElement)(
|
|
@@ -1507,41 +2609,104 @@ function SagepilotChatProvider({
|
|
|
1507
2609
|
ChatContentContainer,
|
|
1508
2610
|
chatContentContainerProps,
|
|
1509
2611
|
showCloseButton ? (0, import_react.createElement)(
|
|
1510
|
-
|
|
2612
|
+
import_react_native3.View,
|
|
1511
2613
|
{ style: styles.header },
|
|
1512
2614
|
(0, import_react.createElement)(
|
|
1513
|
-
|
|
2615
|
+
import_react_native3.Pressable,
|
|
1514
2616
|
{
|
|
1515
2617
|
accessibilityRole: "button",
|
|
1516
2618
|
accessibilityLabel: closeLabel,
|
|
1517
2619
|
onPress: () => internalSagepilotChat.dismiss(),
|
|
1518
2620
|
style: styles.closeButton
|
|
1519
2621
|
},
|
|
1520
|
-
(0, import_react.createElement)(
|
|
2622
|
+
(0, import_react.createElement)(import_react_native3.Text, { style: styles.closeText }, closeLabel)
|
|
1521
2623
|
)
|
|
1522
2624
|
) : null,
|
|
1523
2625
|
state.conversationUrl ? (0, import_react.createElement)(import_react_native_webview.WebView, {
|
|
1524
2626
|
...hostedChatWebViewProps,
|
|
2627
|
+
key: `sagepilot-hosted-webview-${nativeWebViewKey}`,
|
|
1525
2628
|
ref: nativeWebViewRef,
|
|
1526
2629
|
source: { uri: state.conversationUrl },
|
|
1527
|
-
|
|
2630
|
+
// The imperceptible opacity toggle forces the Android WebView
|
|
2631
|
+
// surface to re-composite on resume, clearing the blank-but-alive
|
|
2632
|
+
// surface bug without a reload (see runLivenessProbe).
|
|
2633
|
+
style: import_react_native3.Platform.OS === "android" ? [styles.webview, { opacity: 1 - androidRepaintTick % 2 * 1e-3 }] : styles.webview,
|
|
1528
2634
|
startInLoadingState: true,
|
|
1529
2635
|
injectedJavaScriptBeforeContentLoaded: getInjectedWebViewScript(),
|
|
1530
2636
|
onMessage: handleWebViewMessage,
|
|
1531
|
-
onLoadEnd:
|
|
2637
|
+
onLoadEnd: handleNativeWebViewLoadEnd,
|
|
2638
|
+
onShouldStartLoadWithRequest: handleShouldStartLoadWithRequest,
|
|
2639
|
+
// Android: render process killed (commonly while the camera/file
|
|
2640
|
+
// chooser activity is foregrounded). Remount to recover.
|
|
2641
|
+
onRenderProcessGone: recoverNativeWebView,
|
|
2642
|
+
// iOS equivalent: WKWebView content process terminated.
|
|
2643
|
+
onContentProcessDidTerminate: recoverNativeWebView,
|
|
1532
2644
|
renderLoading: () => (0, import_react.createElement)(
|
|
1533
|
-
|
|
2645
|
+
import_react_native3.View,
|
|
1534
2646
|
{ style: styles.loading },
|
|
1535
|
-
(0, import_react.createElement)(
|
|
1536
|
-
(0, import_react.createElement)(
|
|
2647
|
+
(0, import_react.createElement)(import_react_native3.ActivityIndicator, null),
|
|
2648
|
+
(0, import_react.createElement)(import_react_native3.Text, { style: styles.loadingText }, loadingLabel)
|
|
1537
2649
|
)
|
|
1538
2650
|
}) : null
|
|
1539
2651
|
)
|
|
1540
2652
|
)
|
|
2653
|
+
),
|
|
2654
|
+
// Native attachment-source chooser. Replaces the Android Alert (capped at
|
|
2655
|
+
// 3 buttons) so camera/library/documents all show on every platform.
|
|
2656
|
+
(0, import_react.createElement)(
|
|
2657
|
+
import_react_native3.Modal,
|
|
2658
|
+
{
|
|
2659
|
+
visible: sourceChooser !== null,
|
|
2660
|
+
transparent: true,
|
|
2661
|
+
animationType: "fade",
|
|
2662
|
+
statusBarTranslucent: true,
|
|
2663
|
+
onRequestClose: () => setSourceChooser(null)
|
|
2664
|
+
},
|
|
2665
|
+
(0, import_react.createElement)(
|
|
2666
|
+
import_react_native3.Pressable,
|
|
2667
|
+
{ style: styles.sheetBackdrop, onPress: () => setSourceChooser(null) },
|
|
2668
|
+
(0, import_react.createElement)(
|
|
2669
|
+
import_react_native3.View,
|
|
2670
|
+
{ style: styles.sheetCard },
|
|
2671
|
+
(0, import_react.createElement)(import_react_native3.View, { style: styles.sheetHandle }),
|
|
2672
|
+
(0, import_react.createElement)(import_react_native3.Text, { style: styles.sheetTitle }, "Add attachment"),
|
|
2673
|
+
...(internalSagepilotChat.getConfig()?.filePicker?.sources ?? []).map(
|
|
2674
|
+
(source) => (0, import_react.createElement)(
|
|
2675
|
+
import_react_native3.Pressable,
|
|
2676
|
+
{
|
|
2677
|
+
key: source,
|
|
2678
|
+
accessibilityRole: "button",
|
|
2679
|
+
android_ripple: { color: "rgba(15, 23, 42, 0.06)" },
|
|
2680
|
+
hitSlop: 4,
|
|
2681
|
+
style: ({ pressed }) => getSheetOptionStyle(pressed),
|
|
2682
|
+
onPress: () => handleSourceChoice(source)
|
|
2683
|
+
},
|
|
2684
|
+
renderFilePickerSourceIcon(source),
|
|
2685
|
+
(0, import_react.createElement)(
|
|
2686
|
+
import_react_native3.View,
|
|
2687
|
+
{ style: styles.sheetOptionText },
|
|
2688
|
+
(0, import_react.createElement)(import_react_native3.Text, { style: styles.sheetButtonText }, FILE_PICKER_SOURCE_LABELS[source]),
|
|
2689
|
+
(0, import_react.createElement)(import_react_native3.Text, { style: styles.sheetButtonDescription }, FILE_PICKER_SOURCE_DESCRIPTIONS[source])
|
|
2690
|
+
)
|
|
2691
|
+
)
|
|
2692
|
+
),
|
|
2693
|
+
(0, import_react.createElement)(
|
|
2694
|
+
import_react_native3.Pressable,
|
|
2695
|
+
{
|
|
2696
|
+
accessibilityRole: "button",
|
|
2697
|
+
android_ripple: { color: "rgba(15, 23, 42, 0.06)" },
|
|
2698
|
+
style: ({ pressed }) => getSheetCancelStyle(pressed),
|
|
2699
|
+
onPress: () => setSourceChooser(null)
|
|
2700
|
+
},
|
|
2701
|
+
(0, import_react.createElement)(import_lucide_react_native.X, { color: "#334155", size: 18, strokeWidth: 2.5, style: styles.sheetCancelIcon }),
|
|
2702
|
+
(0, import_react.createElement)(import_react_native3.Text, { style: styles.sheetCancelText }, "Cancel")
|
|
2703
|
+
)
|
|
2704
|
+
)
|
|
2705
|
+
)
|
|
1541
2706
|
)
|
|
1542
2707
|
);
|
|
1543
2708
|
}
|
|
1544
|
-
var styles =
|
|
2709
|
+
var styles = import_react_native3.StyleSheet.create({
|
|
1545
2710
|
root: {
|
|
1546
2711
|
flex: 1
|
|
1547
2712
|
},
|
|
@@ -1627,7 +2792,7 @@ var styles = import_react_native.StyleSheet.create({
|
|
|
1627
2792
|
borderStyle: "none"
|
|
1628
2793
|
},
|
|
1629
2794
|
loading: {
|
|
1630
|
-
...
|
|
2795
|
+
...import_react_native3.StyleSheet.absoluteFillObject,
|
|
1631
2796
|
alignItems: "center",
|
|
1632
2797
|
justifyContent: "center",
|
|
1633
2798
|
backgroundColor: "#ffffff"
|
|
@@ -1636,6 +2801,112 @@ var styles = import_react_native.StyleSheet.create({
|
|
|
1636
2801
|
marginTop: 12,
|
|
1637
2802
|
color: "#4b5563",
|
|
1638
2803
|
fontSize: 14
|
|
2804
|
+
},
|
|
2805
|
+
sheetBackdrop: {
|
|
2806
|
+
flex: 1,
|
|
2807
|
+
alignItems: "center",
|
|
2808
|
+
justifyContent: "flex-end",
|
|
2809
|
+
backgroundColor: "rgba(15, 23, 42, 0.48)"
|
|
2810
|
+
},
|
|
2811
|
+
sheetCard: {
|
|
2812
|
+
width: "100%",
|
|
2813
|
+
maxWidth: 540,
|
|
2814
|
+
backgroundColor: "#ffffff",
|
|
2815
|
+
borderTopLeftRadius: 26,
|
|
2816
|
+
borderTopRightRadius: 26,
|
|
2817
|
+
paddingTop: 10,
|
|
2818
|
+
paddingBottom: 18,
|
|
2819
|
+
paddingHorizontal: 14,
|
|
2820
|
+
shadowColor: "#0f172a",
|
|
2821
|
+
shadowOpacity: 0.22,
|
|
2822
|
+
shadowRadius: 28,
|
|
2823
|
+
shadowOffset: { width: 0, height: -10 },
|
|
2824
|
+
elevation: 18
|
|
2825
|
+
},
|
|
2826
|
+
sheetHandle: {
|
|
2827
|
+
alignSelf: "center",
|
|
2828
|
+
width: 42,
|
|
2829
|
+
height: 5,
|
|
2830
|
+
borderRadius: 999,
|
|
2831
|
+
backgroundColor: "#d1d5db",
|
|
2832
|
+
marginBottom: 14
|
|
2833
|
+
},
|
|
2834
|
+
sheetTitle: {
|
|
2835
|
+
color: "#111827",
|
|
2836
|
+
fontSize: 17,
|
|
2837
|
+
fontWeight: "700",
|
|
2838
|
+
paddingBottom: 12,
|
|
2839
|
+
paddingHorizontal: 4
|
|
2840
|
+
},
|
|
2841
|
+
sheetOption: {
|
|
2842
|
+
minHeight: 70,
|
|
2843
|
+
flexDirection: "row",
|
|
2844
|
+
alignItems: "center",
|
|
2845
|
+
borderRadius: 18,
|
|
2846
|
+
paddingHorizontal: 12,
|
|
2847
|
+
marginBottom: 6,
|
|
2848
|
+
backgroundColor: "#ffffff"
|
|
2849
|
+
},
|
|
2850
|
+
sheetOptionPressed: {
|
|
2851
|
+
backgroundColor: "#f8fafc",
|
|
2852
|
+
transform: [{ scale: 0.985 }]
|
|
2853
|
+
},
|
|
2854
|
+
sheetOptionIcon: {
|
|
2855
|
+
width: 46,
|
|
2856
|
+
height: 46,
|
|
2857
|
+
borderRadius: 16,
|
|
2858
|
+
marginRight: 14,
|
|
2859
|
+
alignItems: "center",
|
|
2860
|
+
justifyContent: "center",
|
|
2861
|
+
position: "relative",
|
|
2862
|
+
overflow: "hidden"
|
|
2863
|
+
},
|
|
2864
|
+
sheetOptionText: {
|
|
2865
|
+
flex: 1,
|
|
2866
|
+
justifyContent: "center"
|
|
2867
|
+
},
|
|
2868
|
+
sheetButtonText: {
|
|
2869
|
+
color: "#0f172a",
|
|
2870
|
+
fontSize: 16,
|
|
2871
|
+
fontWeight: "700"
|
|
2872
|
+
},
|
|
2873
|
+
sheetButtonDescription: {
|
|
2874
|
+
color: "#64748b",
|
|
2875
|
+
fontSize: 13,
|
|
2876
|
+
fontWeight: "500",
|
|
2877
|
+
marginTop: 3
|
|
2878
|
+
},
|
|
2879
|
+
cameraOptionIcon: {
|
|
2880
|
+
backgroundColor: "#e0f2fe"
|
|
2881
|
+
},
|
|
2882
|
+
libraryOptionIcon: {
|
|
2883
|
+
backgroundColor: "#ecfdf5"
|
|
2884
|
+
},
|
|
2885
|
+
documentsOptionIcon: {
|
|
2886
|
+
backgroundColor: "#f5f3ff"
|
|
2887
|
+
},
|
|
2888
|
+
sheetCancelButton: {
|
|
2889
|
+
minHeight: 56,
|
|
2890
|
+
flexDirection: "row",
|
|
2891
|
+
alignItems: "center",
|
|
2892
|
+
justifyContent: "center",
|
|
2893
|
+
borderRadius: 18,
|
|
2894
|
+
marginTop: 8,
|
|
2895
|
+
backgroundColor: "#f1f5f9"
|
|
2896
|
+
},
|
|
2897
|
+
sheetCancelButtonPressed: {
|
|
2898
|
+
backgroundColor: "#e2e8f0",
|
|
2899
|
+
transform: [{ scale: 0.985 }]
|
|
2900
|
+
},
|
|
2901
|
+
sheetCancelIcon: {
|
|
2902
|
+
width: 18,
|
|
2903
|
+
height: 18,
|
|
2904
|
+
marginRight: 8
|
|
2905
|
+
},
|
|
2906
|
+
sheetCancelText: {
|
|
2907
|
+
color: "#0f172a",
|
|
2908
|
+
fontSize: 16,
|
|
2909
|
+
fontWeight: "700"
|
|
1639
2910
|
}
|
|
1640
2911
|
});
|
|
1641
2912
|
|
|
@@ -1690,10 +2961,18 @@ function useSagepilotChat() {
|
|
|
1690
2961
|
}
|
|
1691
2962
|
// Annotate the CommonJS export names for ESM import in node:
|
|
1692
2963
|
0 && (module.exports = {
|
|
2964
|
+
SAGEPILOT_DEFAULT_IMAGE_MAX_DIMENSION,
|
|
2965
|
+
SAGEPILOT_DEFAULT_IMAGE_MAX_FILE_SIZE_BYTES,
|
|
2966
|
+
SAGEPILOT_DEFAULT_IMAGE_QUALITY,
|
|
1693
2967
|
SagepilotChat,
|
|
1694
2968
|
SagepilotChatError,
|
|
1695
2969
|
SagepilotChatProvider,
|
|
2970
|
+
SagepilotFilePickerError,
|
|
1696
2971
|
createAsyncStorageCacheStorage,
|
|
1697
2972
|
createKeychainTokenStorage,
|
|
2973
|
+
createSagepilotFilePicker,
|
|
2974
|
+
createSagepilotFileStore,
|
|
2975
|
+
ensureSagepilotAndroidCameraPermission,
|
|
2976
|
+
promptOpenSagepilotCameraSettings,
|
|
1698
2977
|
useSagepilotChat
|
|
1699
2978
|
});
|