@sagepilot-ai/react-native-sdk 0.2.4 → 0.2.5
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 +86 -0
- package/dist/index.d.mts +202 -1
- package/dist/index.d.ts +202 -1
- package/dist/index.js +849 -36
- package/dist/index.mjs +833 -21
- package/package.json +13 -1
package/dist/index.mjs
CHANGED
|
@@ -15,7 +15,7 @@ var SagepilotChatError = class extends Error {
|
|
|
15
15
|
|
|
16
16
|
// src/core/config/constants.ts
|
|
17
17
|
var SDK_NAME = "@sagepilot-ai/react-native-sdk";
|
|
18
|
-
var SDK_VERSION = "0.2.
|
|
18
|
+
var SDK_VERSION = "0.2.5";
|
|
19
19
|
var DEFAULT_HOST = "https://app.sagepilot.ai";
|
|
20
20
|
var DEFAULT_WIDGET_HOST = "https://app.sagepilot.ai";
|
|
21
21
|
var CUSTOMER_API_PREFIX = "/customer-api/v1";
|
|
@@ -532,6 +532,9 @@ var SagepilotReactNativeChat = class {
|
|
|
532
532
|
const { workspaceId, channelId } = parseKey(config.key);
|
|
533
533
|
this.runtimeOptions = {
|
|
534
534
|
behavior: config.behavior,
|
|
535
|
+
cacheStorage: config.cacheStorage,
|
|
536
|
+
filePicker: config.filePicker,
|
|
537
|
+
fileStore: config.fileStore,
|
|
535
538
|
presentation: config.presentation,
|
|
536
539
|
theme: config.theme,
|
|
537
540
|
hostedClaimsStorageKeyPrefix: config.hostedClaimsStorageKeyPrefix || DEFAULT_HOSTED_CLAIMS_STORAGE_KEY_PREFIX
|
|
@@ -1146,14 +1149,295 @@ function createAsyncStorageCacheStorage(asyncStorage) {
|
|
|
1146
1149
|
removeItem: (key) => asyncStorage.removeItem(key)
|
|
1147
1150
|
};
|
|
1148
1151
|
}
|
|
1152
|
+
function createJsonCache(storage, namespace) {
|
|
1153
|
+
return {
|
|
1154
|
+
async get(key) {
|
|
1155
|
+
const value = await storage.getItem(`${namespace}:${key}`);
|
|
1156
|
+
if (!value) return null;
|
|
1157
|
+
try {
|
|
1158
|
+
return JSON.parse(value);
|
|
1159
|
+
} catch {
|
|
1160
|
+
return null;
|
|
1161
|
+
}
|
|
1162
|
+
},
|
|
1163
|
+
async set(key, value) {
|
|
1164
|
+
await storage.setItem(`${namespace}:${key}`, JSON.stringify(value));
|
|
1165
|
+
},
|
|
1166
|
+
async remove(key) {
|
|
1167
|
+
await storage.removeItem(`${namespace}:${key}`);
|
|
1168
|
+
}
|
|
1169
|
+
};
|
|
1170
|
+
}
|
|
1171
|
+
|
|
1172
|
+
// src/core/storage/fileStore.ts
|
|
1173
|
+
var DEFAULT_DIRECTORY_NAME = "sagepilot-attachments";
|
|
1174
|
+
function sanitizeKey(key) {
|
|
1175
|
+
return key.replace(/[^a-zA-Z0-9._-]/g, "_").replace(/\.{2,}/g, "_");
|
|
1176
|
+
}
|
|
1177
|
+
function createSagepilotFileStore(blobUtil, options = {}) {
|
|
1178
|
+
const directory = `${blobUtil.fs.dirs.DocumentDir}/${options.directoryName ?? DEFAULT_DIRECTORY_NAME}`;
|
|
1179
|
+
const pathFor = (key) => `${directory}/${sanitizeKey(key)}`;
|
|
1180
|
+
let dirReady = null;
|
|
1181
|
+
function ensureDirectory() {
|
|
1182
|
+
if (!dirReady) {
|
|
1183
|
+
dirReady = (async () => {
|
|
1184
|
+
if (!await blobUtil.fs.exists(directory)) {
|
|
1185
|
+
await blobUtil.fs.mkdir(directory);
|
|
1186
|
+
}
|
|
1187
|
+
})().catch((error) => {
|
|
1188
|
+
dirReady = null;
|
|
1189
|
+
throw error;
|
|
1190
|
+
});
|
|
1191
|
+
}
|
|
1192
|
+
return dirReady;
|
|
1193
|
+
}
|
|
1194
|
+
return {
|
|
1195
|
+
async write(key, base64) {
|
|
1196
|
+
await ensureDirectory();
|
|
1197
|
+
await blobUtil.fs.writeFile(pathFor(key), base64, "base64");
|
|
1198
|
+
},
|
|
1199
|
+
async read(key) {
|
|
1200
|
+
const content = await blobUtil.fs.readFile(pathFor(key), "base64");
|
|
1201
|
+
if (typeof content !== "string" || content.length === 0) {
|
|
1202
|
+
throw new Error("Persisted attachment is empty or unreadable.");
|
|
1203
|
+
}
|
|
1204
|
+
return content;
|
|
1205
|
+
},
|
|
1206
|
+
async remove(key) {
|
|
1207
|
+
try {
|
|
1208
|
+
if (await blobUtil.fs.exists(pathFor(key))) {
|
|
1209
|
+
await blobUtil.fs.unlink(pathFor(key));
|
|
1210
|
+
}
|
|
1211
|
+
} catch {
|
|
1212
|
+
}
|
|
1213
|
+
},
|
|
1214
|
+
async prune(keepKeys) {
|
|
1215
|
+
try {
|
|
1216
|
+
if (!await blobUtil.fs.exists(directory)) return;
|
|
1217
|
+
const keep = new Set(keepKeys.map(sanitizeKey));
|
|
1218
|
+
const entries = await blobUtil.fs.ls(directory);
|
|
1219
|
+
await Promise.all(
|
|
1220
|
+
entries.filter((entry) => !keep.has(entry)).map((entry) => blobUtil.fs.unlink(`${directory}/${entry}`).catch(() => void 0))
|
|
1221
|
+
);
|
|
1222
|
+
} catch {
|
|
1223
|
+
}
|
|
1224
|
+
}
|
|
1225
|
+
};
|
|
1226
|
+
}
|
|
1227
|
+
|
|
1228
|
+
// src/core/native/filePicker.ts
|
|
1229
|
+
import { PermissionsAndroid, Platform } from "react-native";
|
|
1230
|
+
var DEFAULT_IMAGE_MAX_DIMENSION = 1920;
|
|
1231
|
+
var DEFAULT_IMAGE_QUALITY = 0.8;
|
|
1232
|
+
var DEFAULT_IMAGE_MIME_TYPE = "image/jpeg";
|
|
1233
|
+
var DEFAULT_DOCUMENT_MIME_TYPE = "application/octet-stream";
|
|
1234
|
+
var DEFAULT_IMAGE_SELECTION_LIMIT = 5;
|
|
1235
|
+
var DEFAULT_DOCUMENT_MAX_FILE_SIZE_BYTES = 20 * 1024 * 1024;
|
|
1236
|
+
var DEFAULT_IMAGE_MAX_FILE_SIZE_BYTES = 15 * 1024 * 1024;
|
|
1237
|
+
var SagepilotFilePickerError = class extends Error {
|
|
1238
|
+
constructor(code, message) {
|
|
1239
|
+
super(message);
|
|
1240
|
+
this.name = "SagepilotFilePickerError";
|
|
1241
|
+
this.code = code;
|
|
1242
|
+
}
|
|
1243
|
+
};
|
|
1244
|
+
function bytesToMb(bytes) {
|
|
1245
|
+
return Math.round(bytes / (1024 * 1024) * 10) / 10;
|
|
1246
|
+
}
|
|
1247
|
+
function estimateBase64ByteSize(dataBase64) {
|
|
1248
|
+
return Math.floor(dataBase64.length * 3 / 4);
|
|
1249
|
+
}
|
|
1250
|
+
function stripFileScheme(uri) {
|
|
1251
|
+
return uri.startsWith("file://") ? uri.replace("file://", "") : uri;
|
|
1252
|
+
}
|
|
1253
|
+
async function ensureAndroidCameraPermission() {
|
|
1254
|
+
if (Platform.OS !== "android") return;
|
|
1255
|
+
let result;
|
|
1256
|
+
try {
|
|
1257
|
+
result = await PermissionsAndroid.request(PermissionsAndroid.PERMISSIONS.CAMERA);
|
|
1258
|
+
} catch {
|
|
1259
|
+
return;
|
|
1260
|
+
}
|
|
1261
|
+
if (result === PermissionsAndroid.RESULTS.NEVER_ASK_AGAIN) {
|
|
1262
|
+
throw new SagepilotFilePickerError(
|
|
1263
|
+
"permission_denied",
|
|
1264
|
+
"Camera access is turned off. Enable it for this app in Settings to take a photo."
|
|
1265
|
+
);
|
|
1266
|
+
}
|
|
1267
|
+
if (result === PermissionsAndroid.RESULTS.DENIED) {
|
|
1268
|
+
throw new SagepilotFilePickerError(
|
|
1269
|
+
"permission_denied",
|
|
1270
|
+
"Camera permission is required to take a photo."
|
|
1271
|
+
);
|
|
1272
|
+
}
|
|
1273
|
+
}
|
|
1274
|
+
function mapImagePickerErrorCode(errorCode) {
|
|
1275
|
+
switch (errorCode) {
|
|
1276
|
+
case "permission":
|
|
1277
|
+
return "permission_denied";
|
|
1278
|
+
case "camera_unavailable":
|
|
1279
|
+
return "camera_unavailable";
|
|
1280
|
+
default:
|
|
1281
|
+
return "unknown";
|
|
1282
|
+
}
|
|
1283
|
+
}
|
|
1284
|
+
function imageAssetsToPickedFiles(result, maxFileSizeBytes) {
|
|
1285
|
+
if (result.didCancel) return [];
|
|
1286
|
+
if (result.errorCode) {
|
|
1287
|
+
throw new SagepilotFilePickerError(
|
|
1288
|
+
mapImagePickerErrorCode(result.errorCode),
|
|
1289
|
+
result.errorMessage || `Could not capture the photo (${result.errorCode}).`
|
|
1290
|
+
);
|
|
1291
|
+
}
|
|
1292
|
+
const assets = result.assets ?? [];
|
|
1293
|
+
const usable = assets.filter((asset) => typeof asset.base64 === "string" && asset.base64.length > 0);
|
|
1294
|
+
if (assets.length > usable.length) {
|
|
1295
|
+
throw new SagepilotFilePickerError(
|
|
1296
|
+
"encode_failed",
|
|
1297
|
+
assets.length === 1 ? "The photo could not be processed. Please try again." : "Some photos could not be processed. Please try selecting them again."
|
|
1298
|
+
);
|
|
1299
|
+
}
|
|
1300
|
+
return usable.map((asset, index) => {
|
|
1301
|
+
const dataBase64 = asset.base64;
|
|
1302
|
+
const size = asset.fileSize ?? estimateBase64ByteSize(dataBase64);
|
|
1303
|
+
if (maxFileSizeBytes > 0 && size > maxFileSizeBytes) {
|
|
1304
|
+
throw new SagepilotFilePickerError(
|
|
1305
|
+
"file_too_large",
|
|
1306
|
+
`Image is too large (max ${bytesToMb(maxFileSizeBytes)}MB).`
|
|
1307
|
+
);
|
|
1308
|
+
}
|
|
1309
|
+
return {
|
|
1310
|
+
file_name: asset.fileName || `photo-${Date.now()}-${index + 1}.jpg`,
|
|
1311
|
+
mime_type: asset.type || DEFAULT_IMAGE_MIME_TYPE,
|
|
1312
|
+
size,
|
|
1313
|
+
data_base64: dataBase64
|
|
1314
|
+
};
|
|
1315
|
+
});
|
|
1316
|
+
}
|
|
1317
|
+
async function readUriAsBase64(uri, fileReader) {
|
|
1318
|
+
if (fileReader) {
|
|
1319
|
+
const content = await fileReader.fs.readFile(stripFileScheme(uri), "base64");
|
|
1320
|
+
if (typeof content !== "string") {
|
|
1321
|
+
throw new Error("File reader returned unexpected content.");
|
|
1322
|
+
}
|
|
1323
|
+
return content;
|
|
1324
|
+
}
|
|
1325
|
+
const response = await fetch(uri);
|
|
1326
|
+
const blob = await response.blob();
|
|
1327
|
+
return new Promise((resolve, reject) => {
|
|
1328
|
+
const reader = new FileReader();
|
|
1329
|
+
reader.onload = () => {
|
|
1330
|
+
const dataUrl = typeof reader.result === "string" ? reader.result : "";
|
|
1331
|
+
const [, payload = ""] = dataUrl.split(",");
|
|
1332
|
+
if (!payload) {
|
|
1333
|
+
reject(new Error("Could not read the selected file."));
|
|
1334
|
+
return;
|
|
1335
|
+
}
|
|
1336
|
+
resolve(payload);
|
|
1337
|
+
};
|
|
1338
|
+
reader.onerror = () => reject(new Error("Could not read the selected file."));
|
|
1339
|
+
reader.readAsDataURL(blob);
|
|
1340
|
+
});
|
|
1341
|
+
}
|
|
1342
|
+
function createSagepilotFilePicker(options) {
|
|
1343
|
+
const { imagePicker, documentsPicker, fileReader } = options;
|
|
1344
|
+
const imageMaxDimension = options.imageMaxDimension ?? DEFAULT_IMAGE_MAX_DIMENSION;
|
|
1345
|
+
const imageQuality = options.imageQuality ?? DEFAULT_IMAGE_QUALITY;
|
|
1346
|
+
const imageSelectionLimit = options.imageSelectionLimit ?? DEFAULT_IMAGE_SELECTION_LIMIT;
|
|
1347
|
+
const documentMaxFileSizeBytes = options.documentMaxFileSizeBytes ?? DEFAULT_DOCUMENT_MAX_FILE_SIZE_BYTES;
|
|
1348
|
+
const imageMaxFileSizeBytes = options.imageMaxFileSizeBytes ?? DEFAULT_IMAGE_MAX_FILE_SIZE_BYTES;
|
|
1349
|
+
const sources = [];
|
|
1350
|
+
if (imagePicker) sources.push("camera", "library");
|
|
1351
|
+
if (documentsPicker) sources.push("documents");
|
|
1352
|
+
if (sources.length === 0) return void 0;
|
|
1353
|
+
const imageOptions = {
|
|
1354
|
+
mediaType: "photo",
|
|
1355
|
+
includeBase64: true,
|
|
1356
|
+
maxWidth: imageMaxDimension,
|
|
1357
|
+
maxHeight: imageMaxDimension,
|
|
1358
|
+
quality: imageQuality,
|
|
1359
|
+
saveToPhotos: false
|
|
1360
|
+
};
|
|
1361
|
+
async function pickFromCamera() {
|
|
1362
|
+
if (!imagePicker) return [];
|
|
1363
|
+
await ensureAndroidCameraPermission();
|
|
1364
|
+
return imageAssetsToPickedFiles(await imagePicker.launchCamera(imageOptions), imageMaxFileSizeBytes);
|
|
1365
|
+
}
|
|
1366
|
+
async function pickFromLibrary(multiple) {
|
|
1367
|
+
if (!imagePicker) return [];
|
|
1368
|
+
return imageAssetsToPickedFiles(await imagePicker.launchImageLibrary({
|
|
1369
|
+
...imageOptions,
|
|
1370
|
+
// Bounded multi-select: 0 (unlimited) lets a huge batch OOM the bridge.
|
|
1371
|
+
selectionLimit: multiple ? Math.max(0, imageSelectionLimit) : 1
|
|
1372
|
+
}), imageMaxFileSizeBytes);
|
|
1373
|
+
}
|
|
1374
|
+
async function pickDocuments(multiple) {
|
|
1375
|
+
if (!documentsPicker) return [];
|
|
1376
|
+
let picked;
|
|
1377
|
+
try {
|
|
1378
|
+
picked = await documentsPicker.pick({ allowMultiSelection: multiple });
|
|
1379
|
+
} catch (error) {
|
|
1380
|
+
if (error && typeof error === "object" && error.code === "OPERATION_CANCELED") {
|
|
1381
|
+
return [];
|
|
1382
|
+
}
|
|
1383
|
+
throw error;
|
|
1384
|
+
}
|
|
1385
|
+
const files = [];
|
|
1386
|
+
for (const file of picked) {
|
|
1387
|
+
if (documentMaxFileSizeBytes > 0 && typeof file.size === "number" && file.size > documentMaxFileSizeBytes) {
|
|
1388
|
+
throw new SagepilotFilePickerError(
|
|
1389
|
+
"file_too_large",
|
|
1390
|
+
`"${file.name || "File"}" is too large (max ${bytesToMb(documentMaxFileSizeBytes)}MB).`
|
|
1391
|
+
);
|
|
1392
|
+
}
|
|
1393
|
+
let readableUri = file.uri;
|
|
1394
|
+
if (documentsPicker.keepLocalCopy) {
|
|
1395
|
+
const fileName = file.name || `file-${Date.now()}`;
|
|
1396
|
+
const [copy] = await documentsPicker.keepLocalCopy({
|
|
1397
|
+
files: [{ uri: file.uri, fileName }],
|
|
1398
|
+
destination: "cachesDirectory"
|
|
1399
|
+
});
|
|
1400
|
+
if (copy?.status === "success" && copy.localUri) {
|
|
1401
|
+
readableUri = copy.localUri;
|
|
1402
|
+
}
|
|
1403
|
+
}
|
|
1404
|
+
let dataBase64;
|
|
1405
|
+
try {
|
|
1406
|
+
dataBase64 = await readUriAsBase64(readableUri, fileReader);
|
|
1407
|
+
} catch (error) {
|
|
1408
|
+
throw new SagepilotFilePickerError(
|
|
1409
|
+
"read_failed",
|
|
1410
|
+
error instanceof Error && error.message ? error.message : "Could not read the selected file."
|
|
1411
|
+
);
|
|
1412
|
+
}
|
|
1413
|
+
files.push({
|
|
1414
|
+
file_name: file.name || `file-${Date.now()}`,
|
|
1415
|
+
mime_type: file.type || DEFAULT_DOCUMENT_MIME_TYPE,
|
|
1416
|
+
size: file.size ?? estimateBase64ByteSize(dataBase64),
|
|
1417
|
+
data_base64: dataBase64
|
|
1418
|
+
});
|
|
1419
|
+
}
|
|
1420
|
+
return files;
|
|
1421
|
+
}
|
|
1422
|
+
return {
|
|
1423
|
+
sources,
|
|
1424
|
+
async pickFiles(request) {
|
|
1425
|
+
if (request.source === "camera") return pickFromCamera();
|
|
1426
|
+
if (request.source === "library") return pickFromLibrary(request.multiple);
|
|
1427
|
+
return pickDocuments(request.multiple);
|
|
1428
|
+
}
|
|
1429
|
+
};
|
|
1430
|
+
}
|
|
1149
1431
|
|
|
1150
1432
|
// src/ui/SagepilotChatProvider.ts
|
|
1151
1433
|
import { createElement, useCallback, useEffect, useRef, useState } from "react";
|
|
1152
1434
|
import {
|
|
1153
1435
|
ActivityIndicator,
|
|
1436
|
+
AppState,
|
|
1154
1437
|
KeyboardAvoidingView,
|
|
1438
|
+
Linking,
|
|
1155
1439
|
Modal,
|
|
1156
|
-
Platform,
|
|
1440
|
+
Platform as Platform2,
|
|
1157
1441
|
Pressable,
|
|
1158
1442
|
SafeAreaView,
|
|
1159
1443
|
StyleSheet,
|
|
@@ -1163,6 +1447,37 @@ import {
|
|
|
1163
1447
|
import { WebView } from "react-native-webview";
|
|
1164
1448
|
|
|
1165
1449
|
// src/core/webview/mobileBridge.ts
|
|
1450
|
+
var FILE_PICKER_PROTOCOL_VERSION = 2;
|
|
1451
|
+
function buildBridgeCapabilitiesScript(capabilities) {
|
|
1452
|
+
const payload = { ...capabilities, filePickerProtocol: FILE_PICKER_PROTOCOL_VERSION };
|
|
1453
|
+
return [
|
|
1454
|
+
"(function(){",
|
|
1455
|
+
"try {",
|
|
1456
|
+
"if (window.SagepilotMobileBridge) {",
|
|
1457
|
+
`window.SagepilotMobileBridge.capabilities = Object.assign({}, window.SagepilotMobileBridge.capabilities, ${JSON.stringify(payload)});`,
|
|
1458
|
+
"}",
|
|
1459
|
+
"} catch (_) {}",
|
|
1460
|
+
"true;",
|
|
1461
|
+
"})();"
|
|
1462
|
+
].join("\n");
|
|
1463
|
+
}
|
|
1464
|
+
function buildLivenessPingScript(nonce, staleMs) {
|
|
1465
|
+
return [
|
|
1466
|
+
"(function(){",
|
|
1467
|
+
"try {",
|
|
1468
|
+
"var hb = window.__sagepilotWidgetHeartbeat;",
|
|
1469
|
+
"var supports = typeof hb === 'number';",
|
|
1470
|
+
`var alive = !supports ? true : (Date.now() - hb < ${Math.max(0, Math.floor(staleMs))});`,
|
|
1471
|
+
"var send = function(){",
|
|
1472
|
+
" var msg = JSON.stringify({ type: 'sagepilot:pong', nonce: " + JSON.stringify(nonce) + ", alive: alive, supportsHeartbeat: supports });",
|
|
1473
|
+
" if (window.ReactNativeWebView && window.ReactNativeWebView.postMessage) { window.ReactNativeWebView.postMessage(msg); }",
|
|
1474
|
+
"};",
|
|
1475
|
+
"send();",
|
|
1476
|
+
"} catch (_) {}",
|
|
1477
|
+
"true;",
|
|
1478
|
+
"})();"
|
|
1479
|
+
].join("\n");
|
|
1480
|
+
}
|
|
1166
1481
|
function isHostedBridgeMessage(value) {
|
|
1167
1482
|
if (!value || typeof value !== "object") return false;
|
|
1168
1483
|
const message = value;
|
|
@@ -1288,9 +1603,70 @@ function readPresentationState() {
|
|
|
1288
1603
|
function getInjectedWebViewScript() {
|
|
1289
1604
|
return [
|
|
1290
1605
|
internalSagepilotChat.getHostedAuthScript(),
|
|
1291
|
-
mobileWebViewBridgeScript
|
|
1606
|
+
mobileWebViewBridgeScript,
|
|
1607
|
+
buildBridgeCapabilitiesScript({
|
|
1608
|
+
nativeFilePicker: Boolean(internalSagepilotChat.getConfig()?.filePicker)
|
|
1609
|
+
})
|
|
1292
1610
|
].filter(Boolean).join("\n");
|
|
1293
1611
|
}
|
|
1612
|
+
var FILE_PICKER_SOURCE_LABELS = {
|
|
1613
|
+
camera: "Take photo",
|
|
1614
|
+
library: "Choose from gallery",
|
|
1615
|
+
documents: "Browse files"
|
|
1616
|
+
};
|
|
1617
|
+
var DELIVERY_RETRY_MS = 1500;
|
|
1618
|
+
var DELIVERY_LEGACY_FALLBACK_ATTEMPTS = 2;
|
|
1619
|
+
var DELIVERY_MAX_ATTEMPTS = 8;
|
|
1620
|
+
var PERSIST_MAX_TOTAL_BYTES = 2 * 1024 * 1024;
|
|
1621
|
+
var PENDING_FILES_CACHE_NAMESPACE = "sagepilot_rn_picker";
|
|
1622
|
+
var PENDING_FILES_CACHE_KEY = "pending_batch";
|
|
1623
|
+
var LIVENESS_HEARTBEAT_STALE_MS = 4e3;
|
|
1624
|
+
var LIVENESS_PONG_TIMEOUT_MS = 1500;
|
|
1625
|
+
var LIVENESS_MAX_PING_ATTEMPTS = 2;
|
|
1626
|
+
var LIVENESS_MAX_REMOUNTS = 2;
|
|
1627
|
+
var batchSequence = 0;
|
|
1628
|
+
function nextBatchId() {
|
|
1629
|
+
batchSequence += 1;
|
|
1630
|
+
const entropy = Math.random().toString(36).slice(2, 8);
|
|
1631
|
+
return `b_${Date.now().toString(36)}_${batchSequence}_${entropy}`;
|
|
1632
|
+
}
|
|
1633
|
+
function totalBase64Bytes(files) {
|
|
1634
|
+
return files.reduce((sum, file) => sum + (file.data_base64?.length ?? 0), 0);
|
|
1635
|
+
}
|
|
1636
|
+
function storageKeyFor(batchId, index) {
|
|
1637
|
+
return `${batchId}_${index}.bin`;
|
|
1638
|
+
}
|
|
1639
|
+
function buildDispatchScript(messageLiteral) {
|
|
1640
|
+
return [
|
|
1641
|
+
"(function(){",
|
|
1642
|
+
"try {",
|
|
1643
|
+
`var message = ${messageLiteral};`,
|
|
1644
|
+
"window.dispatchEvent(new MessageEvent('message', { data: message, origin: window.location.origin, source: window.parent || window }));",
|
|
1645
|
+
"} catch (_) {}",
|
|
1646
|
+
"true;",
|
|
1647
|
+
"})();"
|
|
1648
|
+
].join("\n");
|
|
1649
|
+
}
|
|
1650
|
+
function buildFilesPickedScript(files, batchId) {
|
|
1651
|
+
return buildDispatchScript(`{ type: "sagepilot:files_picked", batch_id: ${JSON.stringify(batchId)}, files: ${JSON.stringify(files)} }`);
|
|
1652
|
+
}
|
|
1653
|
+
function buildFilePickerErrorScript(message, code) {
|
|
1654
|
+
return buildDispatchScript(
|
|
1655
|
+
`{ type: "sagepilot:file_picker_error", message: ${JSON.stringify(message)}${code ? `, code: ${JSON.stringify(code)}` : ""} }`
|
|
1656
|
+
);
|
|
1657
|
+
}
|
|
1658
|
+
function buildFilePickerCancelledScript() {
|
|
1659
|
+
return buildDispatchScript(`{ type: "sagepilot:file_picker_cancelled" }`);
|
|
1660
|
+
}
|
|
1661
|
+
function readFilePickerError(error) {
|
|
1662
|
+
if (error && typeof error === "object") {
|
|
1663
|
+
const candidate = error;
|
|
1664
|
+
const message = typeof candidate.message === "string" && candidate.message ? candidate.message : "Could not attach the selected file.";
|
|
1665
|
+
const code = typeof candidate.code === "string" ? candidate.code : void 0;
|
|
1666
|
+
return { message, code };
|
|
1667
|
+
}
|
|
1668
|
+
return { message: "Could not attach the selected file." };
|
|
1669
|
+
}
|
|
1294
1670
|
function getHostedIdentityDispatchScript() {
|
|
1295
1671
|
const message = internalSagepilotChat.getHostedIdentityMessage();
|
|
1296
1672
|
if (!message) return "";
|
|
@@ -1313,10 +1689,13 @@ function readUrlOrigin(url) {
|
|
|
1313
1689
|
return null;
|
|
1314
1690
|
}
|
|
1315
1691
|
}
|
|
1692
|
+
function isInternalWebViewScheme(url) {
|
|
1693
|
+
return url.startsWith("about:") || url.startsWith("data:") || url.startsWith("blob:") || url.startsWith("javascript:") || url.startsWith("file:");
|
|
1694
|
+
}
|
|
1316
1695
|
var hostedChatWebViewProps = {
|
|
1317
1696
|
allowFileAccess: true,
|
|
1318
1697
|
allowFileAccessFromFileURLs: true,
|
|
1319
|
-
...
|
|
1698
|
+
...Platform2.OS === "ios" ? {
|
|
1320
1699
|
automaticallyAdjustContentInsets: false,
|
|
1321
1700
|
contentInsetAdjustmentBehavior: "never",
|
|
1322
1701
|
hideKeyboardAccessoryView: true
|
|
@@ -1331,7 +1710,7 @@ var hostedChatWebViewProps = {
|
|
|
1331
1710
|
sharedCookiesEnabled: true,
|
|
1332
1711
|
thirdPartyCookiesEnabled: true
|
|
1333
1712
|
};
|
|
1334
|
-
var AndroidInsetsView =
|
|
1713
|
+
var AndroidInsetsView = Platform2.OS === "android" ? SagepilotInsetsViewNativeComponent_default : View;
|
|
1335
1714
|
var emptyAndroidInsets = { top: 0, bottom: 0 };
|
|
1336
1715
|
function SagepilotChatProvider({
|
|
1337
1716
|
children,
|
|
@@ -1340,13 +1719,292 @@ function SagepilotChatProvider({
|
|
|
1340
1719
|
}) {
|
|
1341
1720
|
const [state, setState] = useState(readPresentationState);
|
|
1342
1721
|
const [androidModalInsets, setAndroidModalInsets] = useState(emptyAndroidInsets);
|
|
1722
|
+
const [nativeWebViewKey, setNativeWebViewKey] = useState(0);
|
|
1723
|
+
const [preloadWebViewKey, setPreloadWebViewKey] = useState(0);
|
|
1343
1724
|
const webFrameRef = useRef(null);
|
|
1344
1725
|
const nativeWebViewRef = useRef(null);
|
|
1726
|
+
const pendingBatchesRef = useRef([]);
|
|
1727
|
+
const widgetReadyRef = useRef(false);
|
|
1728
|
+
const deliveryTimerRef = useRef(null);
|
|
1729
|
+
const deliveryAttemptsRef = useRef(0);
|
|
1730
|
+
const pendingPingRef = useRef(null);
|
|
1731
|
+
const pingTimeoutRef = useRef(null);
|
|
1732
|
+
const livenessRemountCountRef = useRef(0);
|
|
1733
|
+
const appStateRef = useRef(AppState.currentState);
|
|
1734
|
+
const didReconcileRef = useRef(false);
|
|
1735
|
+
const [androidRepaintTick, setAndroidRepaintTick] = useState(0);
|
|
1736
|
+
const [sourceChooser, setSourceChooser] = useState(null);
|
|
1737
|
+
const getPendingCache = useCallback(() => {
|
|
1738
|
+
const cacheStorage = internalSagepilotChat.getConfig()?.cacheStorage;
|
|
1739
|
+
if (!cacheStorage) return null;
|
|
1740
|
+
return createJsonCache(cacheStorage, PENDING_FILES_CACHE_NAMESPACE);
|
|
1741
|
+
}, []);
|
|
1742
|
+
const writeManifest = useCallback(() => {
|
|
1743
|
+
const cache = getPendingCache();
|
|
1744
|
+
if (!cache) return;
|
|
1745
|
+
const queue = pendingBatchesRef.current;
|
|
1746
|
+
if (queue.length === 0) {
|
|
1747
|
+
void cache.remove(PENDING_FILES_CACHE_KEY).catch(() => void 0);
|
|
1748
|
+
return;
|
|
1749
|
+
}
|
|
1750
|
+
const hasFileStore = Boolean(internalSagepilotChat.getConfig()?.fileStore);
|
|
1751
|
+
if (hasFileStore) {
|
|
1752
|
+
const manifest = queue.filter((batch) => batch.storageKeys && batch.storageKeys.length === batch.files.length).map((batch) => ({
|
|
1753
|
+
batchId: batch.batchId,
|
|
1754
|
+
files: batch.files.map((file, index) => ({
|
|
1755
|
+
file_name: file.file_name,
|
|
1756
|
+
mime_type: file.mime_type,
|
|
1757
|
+
size: file.size,
|
|
1758
|
+
storageKey: batch.storageKeys[index]
|
|
1759
|
+
}))
|
|
1760
|
+
}));
|
|
1761
|
+
if (manifest.length === 0) {
|
|
1762
|
+
void cache.remove(PENDING_FILES_CACHE_KEY).catch(() => void 0);
|
|
1763
|
+
} else {
|
|
1764
|
+
void cache.set(PENDING_FILES_CACHE_KEY, manifest).catch(() => void 0);
|
|
1765
|
+
}
|
|
1766
|
+
return;
|
|
1767
|
+
}
|
|
1768
|
+
const totalBytes = queue.reduce((sum, batch) => sum + totalBase64Bytes(batch.files), 0);
|
|
1769
|
+
if (totalBytes <= PERSIST_MAX_TOTAL_BYTES) {
|
|
1770
|
+
const manifest = queue.map((batch) => ({
|
|
1771
|
+
batchId: batch.batchId,
|
|
1772
|
+
files: batch.files.map((file) => ({
|
|
1773
|
+
file_name: file.file_name,
|
|
1774
|
+
mime_type: file.mime_type,
|
|
1775
|
+
size: file.size,
|
|
1776
|
+
data_base64: file.data_base64
|
|
1777
|
+
}))
|
|
1778
|
+
}));
|
|
1779
|
+
void cache.set(PENDING_FILES_CACHE_KEY, manifest).catch(() => void 0);
|
|
1780
|
+
} else {
|
|
1781
|
+
void cache.remove(PENDING_FILES_CACHE_KEY).catch(() => void 0);
|
|
1782
|
+
}
|
|
1783
|
+
}, [getPendingCache]);
|
|
1784
|
+
const writeBatchBytes = useCallback(async (batch) => {
|
|
1785
|
+
const fileStore = internalSagepilotChat.getConfig()?.fileStore;
|
|
1786
|
+
if (!fileStore) return;
|
|
1787
|
+
const entries = batch.files.map((file, index) => ({
|
|
1788
|
+
key: storageKeyFor(batch.batchId, index),
|
|
1789
|
+
base64: file.data_base64
|
|
1790
|
+
}));
|
|
1791
|
+
try {
|
|
1792
|
+
await Promise.all(entries.map((entry) => fileStore.write(entry.key, entry.base64)));
|
|
1793
|
+
} catch {
|
|
1794
|
+
return;
|
|
1795
|
+
}
|
|
1796
|
+
if (!pendingBatchesRef.current.some((queued) => queued.batchId === batch.batchId)) {
|
|
1797
|
+
void Promise.all(entries.map((entry) => fileStore.remove(entry.key).catch(() => void 0)));
|
|
1798
|
+
}
|
|
1799
|
+
}, []);
|
|
1800
|
+
const discardBatchFiles = useCallback((batch) => {
|
|
1801
|
+
if (!batch?.storageKeys) return;
|
|
1802
|
+
const fileStore = internalSagepilotChat.getConfig()?.fileStore;
|
|
1803
|
+
if (!fileStore) return;
|
|
1804
|
+
void Promise.all(batch.storageKeys.map((key) => fileStore.remove(key).catch(() => void 0)));
|
|
1805
|
+
}, []);
|
|
1806
|
+
const pumpDelivery = useCallback(() => {
|
|
1807
|
+
if (deliveryTimerRef.current) {
|
|
1808
|
+
clearTimeout(deliveryTimerRef.current);
|
|
1809
|
+
deliveryTimerRef.current = null;
|
|
1810
|
+
}
|
|
1811
|
+
const batch = pendingBatchesRef.current[0];
|
|
1812
|
+
if (!batch) return;
|
|
1813
|
+
deliveryAttemptsRef.current += 1;
|
|
1814
|
+
const attempts = deliveryAttemptsRef.current;
|
|
1815
|
+
const ref = nativeWebViewRef.current;
|
|
1816
|
+
const shouldDeliver = ref && (widgetReadyRef.current || attempts >= DELIVERY_LEGACY_FALLBACK_ATTEMPTS);
|
|
1817
|
+
if (shouldDeliver && ref) {
|
|
1818
|
+
ref.injectJavaScript(buildFilesPickedScript(batch.files, batch.batchId));
|
|
1819
|
+
}
|
|
1820
|
+
if (attempts < DELIVERY_MAX_ATTEMPTS) {
|
|
1821
|
+
deliveryTimerRef.current = setTimeout(() => pumpDelivery(), DELIVERY_RETRY_MS);
|
|
1822
|
+
}
|
|
1823
|
+
}, []);
|
|
1824
|
+
const startDelivery = useCallback(() => {
|
|
1825
|
+
deliveryAttemptsRef.current = 0;
|
|
1826
|
+
pumpDelivery();
|
|
1827
|
+
}, [pumpDelivery]);
|
|
1828
|
+
const ensureDelivery = useCallback(() => {
|
|
1829
|
+
if (pendingBatchesRef.current.length === 0) return;
|
|
1830
|
+
if (deliveryTimerRef.current) return;
|
|
1831
|
+
startDelivery();
|
|
1832
|
+
}, [startDelivery]);
|
|
1833
|
+
const acknowledgeHeadBatch = useCallback(() => {
|
|
1834
|
+
const head = pendingBatchesRef.current[0];
|
|
1835
|
+
pendingBatchesRef.current = pendingBatchesRef.current.slice(1);
|
|
1836
|
+
deliveryAttemptsRef.current = 0;
|
|
1837
|
+
if (deliveryTimerRef.current) {
|
|
1838
|
+
clearTimeout(deliveryTimerRef.current);
|
|
1839
|
+
deliveryTimerRef.current = null;
|
|
1840
|
+
}
|
|
1841
|
+
discardBatchFiles(head);
|
|
1842
|
+
writeManifest();
|
|
1843
|
+
if (pendingBatchesRef.current.length > 0) startDelivery();
|
|
1844
|
+
}, [discardBatchFiles, writeManifest, startDelivery]);
|
|
1845
|
+
const queuePickedFiles = useCallback((files) => {
|
|
1846
|
+
const batchId = nextBatchId();
|
|
1847
|
+
const hasFileStore = Boolean(internalSagepilotChat.getConfig()?.fileStore);
|
|
1848
|
+
const storageKeys = hasFileStore ? files.map((_, index) => storageKeyFor(batchId, index)) : void 0;
|
|
1849
|
+
const batch = { batchId, files, storageKeys };
|
|
1850
|
+
pendingBatchesRef.current = [...pendingBatchesRef.current, batch];
|
|
1851
|
+
ensureDelivery();
|
|
1852
|
+
writeManifest();
|
|
1853
|
+
void writeBatchBytes(batch);
|
|
1854
|
+
}, [ensureDelivery, writeManifest, writeBatchBytes]);
|
|
1855
|
+
const recoverNativeWebView = useCallback(() => {
|
|
1856
|
+
widgetReadyRef.current = false;
|
|
1857
|
+
setNativeWebViewKey((key) => key + 1);
|
|
1858
|
+
}, []);
|
|
1859
|
+
const recoverPreloadWebView = useCallback(() => {
|
|
1860
|
+
setPreloadWebViewKey((key) => key + 1);
|
|
1861
|
+
}, []);
|
|
1862
|
+
const runNativeFilePicker = useCallback((source, multiple) => {
|
|
1863
|
+
const filePicker = internalSagepilotChat.getConfig()?.filePicker;
|
|
1864
|
+
if (!filePicker) return;
|
|
1865
|
+
filePicker.pickFiles({ source, multiple }).then((files) => {
|
|
1866
|
+
if (files.length === 0) {
|
|
1867
|
+
nativeWebViewRef.current?.injectJavaScript(buildFilePickerCancelledScript());
|
|
1868
|
+
return;
|
|
1869
|
+
}
|
|
1870
|
+
queuePickedFiles(files);
|
|
1871
|
+
}).catch((error) => {
|
|
1872
|
+
const { message, code } = readFilePickerError(error);
|
|
1873
|
+
nativeWebViewRef.current?.injectJavaScript(buildFilePickerErrorScript(message, code));
|
|
1874
|
+
});
|
|
1875
|
+
}, [queuePickedFiles]);
|
|
1876
|
+
const openNativeFilePicker = useCallback((multiple) => {
|
|
1877
|
+
const filePicker = internalSagepilotChat.getConfig()?.filePicker;
|
|
1878
|
+
if (!filePicker || filePicker.sources.length === 0) return;
|
|
1879
|
+
const [onlySource] = filePicker.sources;
|
|
1880
|
+
if (filePicker.sources.length === 1 && onlySource) {
|
|
1881
|
+
runNativeFilePicker(onlySource, multiple);
|
|
1882
|
+
return;
|
|
1883
|
+
}
|
|
1884
|
+
setSourceChooser({ multiple });
|
|
1885
|
+
}, [runNativeFilePicker]);
|
|
1886
|
+
const handleSourceChoice = useCallback((source) => {
|
|
1887
|
+
const chooser = sourceChooser;
|
|
1888
|
+
setSourceChooser(null);
|
|
1889
|
+
if (chooser) runNativeFilePicker(source, chooser.multiple);
|
|
1890
|
+
}, [sourceChooser, runNativeFilePicker]);
|
|
1345
1891
|
useEffect(() => {
|
|
1346
1892
|
return internalSagepilotChat.onStateChange(() => {
|
|
1347
1893
|
setState(readPresentationState());
|
|
1348
1894
|
});
|
|
1349
1895
|
}, []);
|
|
1896
|
+
useEffect(() => {
|
|
1897
|
+
let cancelled = false;
|
|
1898
|
+
const cache = getPendingCache();
|
|
1899
|
+
if (!cache) return;
|
|
1900
|
+
const reconcile = async () => {
|
|
1901
|
+
if (didReconcileRef.current) return;
|
|
1902
|
+
const manifest = await cache.get(PENDING_FILES_CACHE_KEY).catch(() => null);
|
|
1903
|
+
if (cancelled || didReconcileRef.current || !Array.isArray(manifest) || manifest.length === 0) return;
|
|
1904
|
+
const fileStore = internalSagepilotChat.getConfig()?.fileStore;
|
|
1905
|
+
const restored = [];
|
|
1906
|
+
for (const batch of manifest) {
|
|
1907
|
+
if (!batch || typeof batch.batchId !== "string" || !Array.isArray(batch.files) || batch.files.length === 0) continue;
|
|
1908
|
+
const files = [];
|
|
1909
|
+
const storageKeys = [];
|
|
1910
|
+
let intact = true;
|
|
1911
|
+
for (const file of batch.files) {
|
|
1912
|
+
let dataBase64 = null;
|
|
1913
|
+
if (typeof file.storageKey === "string" && fileStore) {
|
|
1914
|
+
try {
|
|
1915
|
+
dataBase64 = await fileStore.read(file.storageKey);
|
|
1916
|
+
storageKeys.push(file.storageKey);
|
|
1917
|
+
} catch {
|
|
1918
|
+
intact = false;
|
|
1919
|
+
}
|
|
1920
|
+
} else if (typeof file.data_base64 === "string" && file.data_base64) {
|
|
1921
|
+
dataBase64 = file.data_base64;
|
|
1922
|
+
}
|
|
1923
|
+
if (!dataBase64) {
|
|
1924
|
+
intact = false;
|
|
1925
|
+
break;
|
|
1926
|
+
}
|
|
1927
|
+
files.push({ file_name: file.file_name, mime_type: file.mime_type, size: file.size, data_base64: dataBase64 });
|
|
1928
|
+
}
|
|
1929
|
+
if (intact && files.length === batch.files.length) {
|
|
1930
|
+
restored.push({
|
|
1931
|
+
batchId: batch.batchId,
|
|
1932
|
+
files,
|
|
1933
|
+
storageKeys: storageKeys.length === files.length ? storageKeys : void 0
|
|
1934
|
+
});
|
|
1935
|
+
}
|
|
1936
|
+
}
|
|
1937
|
+
if (cancelled || didReconcileRef.current) return;
|
|
1938
|
+
didReconcileRef.current = true;
|
|
1939
|
+
pendingBatchesRef.current = [...restored, ...pendingBatchesRef.current];
|
|
1940
|
+
writeManifest();
|
|
1941
|
+
if (fileStore) {
|
|
1942
|
+
const keep = pendingBatchesRef.current.flatMap(
|
|
1943
|
+
(batch) => batch.files.map((_, index) => storageKeyFor(batch.batchId, index))
|
|
1944
|
+
);
|
|
1945
|
+
void fileStore.prune(keep).catch(() => void 0);
|
|
1946
|
+
}
|
|
1947
|
+
startDelivery();
|
|
1948
|
+
};
|
|
1949
|
+
void reconcile();
|
|
1950
|
+
return () => {
|
|
1951
|
+
cancelled = true;
|
|
1952
|
+
};
|
|
1953
|
+
}, [getPendingCache, startDelivery, writeManifest]);
|
|
1954
|
+
useEffect(() => {
|
|
1955
|
+
return () => {
|
|
1956
|
+
if (deliveryTimerRef.current) {
|
|
1957
|
+
clearTimeout(deliveryTimerRef.current);
|
|
1958
|
+
deliveryTimerRef.current = null;
|
|
1959
|
+
}
|
|
1960
|
+
if (pingTimeoutRef.current) {
|
|
1961
|
+
clearTimeout(pingTimeoutRef.current);
|
|
1962
|
+
pingTimeoutRef.current = null;
|
|
1963
|
+
}
|
|
1964
|
+
};
|
|
1965
|
+
}, []);
|
|
1966
|
+
const clearPing = useCallback(() => {
|
|
1967
|
+
pendingPingRef.current = null;
|
|
1968
|
+
if (pingTimeoutRef.current) {
|
|
1969
|
+
clearTimeout(pingTimeoutRef.current);
|
|
1970
|
+
pingTimeoutRef.current = null;
|
|
1971
|
+
}
|
|
1972
|
+
}, []);
|
|
1973
|
+
const runLivenessProbe = useCallback(() => {
|
|
1974
|
+
if (Platform2.OS !== "android") return;
|
|
1975
|
+
const ref = nativeWebViewRef.current;
|
|
1976
|
+
if (!ref || !internalSagepilotChat.isPresented()) return;
|
|
1977
|
+
setAndroidRepaintTick((tick) => tick + 1);
|
|
1978
|
+
const attempts = (pendingPingRef.current?.attempts ?? 0) + 1;
|
|
1979
|
+
const nonce = `${Date.now().toString(36)}_${attempts}`;
|
|
1980
|
+
pendingPingRef.current = { nonce, attempts };
|
|
1981
|
+
ref.injectJavaScript(buildLivenessPingScript(nonce, LIVENESS_HEARTBEAT_STALE_MS));
|
|
1982
|
+
if (pingTimeoutRef.current) clearTimeout(pingTimeoutRef.current);
|
|
1983
|
+
pingTimeoutRef.current = setTimeout(() => {
|
|
1984
|
+
if (attempts >= LIVENESS_MAX_PING_ATTEMPTS) {
|
|
1985
|
+
clearPing();
|
|
1986
|
+
recoverNativeWebView();
|
|
1987
|
+
return;
|
|
1988
|
+
}
|
|
1989
|
+
runLivenessProbe();
|
|
1990
|
+
}, LIVENESS_PONG_TIMEOUT_MS);
|
|
1991
|
+
}, [clearPing, recoverNativeWebView]);
|
|
1992
|
+
useEffect(() => {
|
|
1993
|
+
const subscription = AppState.addEventListener("change", (nextState) => {
|
|
1994
|
+
const prev = appStateRef.current;
|
|
1995
|
+
appStateRef.current = nextState;
|
|
1996
|
+
if (nextState === "active" && (prev === "background" || prev === "inactive")) {
|
|
1997
|
+
clearPing();
|
|
1998
|
+
livenessRemountCountRef.current = 0;
|
|
1999
|
+
runLivenessProbe();
|
|
2000
|
+
ensureDelivery();
|
|
2001
|
+
}
|
|
2002
|
+
});
|
|
2003
|
+
return () => {
|
|
2004
|
+
subscription.remove();
|
|
2005
|
+
clearPing();
|
|
2006
|
+
};
|
|
2007
|
+
}, [clearPing, runLivenessProbe, ensureDelivery]);
|
|
1350
2008
|
const handleAndroidInsetsChange = useCallback((event) => {
|
|
1351
2009
|
const nextBottomInset = event.nativeEvent?.bottom;
|
|
1352
2010
|
const nextTopInset = event.nativeEvent?.top;
|
|
@@ -1360,11 +2018,11 @@ function SagepilotChatProvider({
|
|
|
1360
2018
|
const presentationStyle = state.presentation?.style ?? "sheet";
|
|
1361
2019
|
const isFullScreenModal = presentationStyle === "fullScreen";
|
|
1362
2020
|
const animationType = presentationStyle === "fullScreen" || presentationStyle === "push" ? "slide" : "fade";
|
|
1363
|
-
const ModalContainer =
|
|
1364
|
-
const NativeModalContainer =
|
|
1365
|
-
const ChatContentContainer =
|
|
1366
|
-
const nativeModalContainerProps =
|
|
1367
|
-
const chatContentContainerProps =
|
|
2021
|
+
const ModalContainer = Platform2.OS === "ios" && isFullScreenModal ? SafeAreaView : View;
|
|
2022
|
+
const NativeModalContainer = Platform2.OS === "android" ? AndroidInsetsView : ModalContainer;
|
|
2023
|
+
const ChatContentContainer = Platform2.OS === "ios" ? KeyboardAvoidingView : View;
|
|
2024
|
+
const nativeModalContainerProps = Platform2.OS === "android" ? { style: styles.container, onInsetsChange: handleAndroidInsetsChange } : { style: styles.container };
|
|
2025
|
+
const chatContentContainerProps = Platform2.OS === "ios" ? {
|
|
1368
2026
|
behavior: "padding",
|
|
1369
2027
|
enabled: true,
|
|
1370
2028
|
keyboardVerticalOffset: 0,
|
|
@@ -1372,14 +2030,48 @@ function SagepilotChatProvider({
|
|
|
1372
2030
|
} : {
|
|
1373
2031
|
style: [
|
|
1374
2032
|
styles.modalContent,
|
|
1375
|
-
|
|
2033
|
+
Platform2.OS === "android" ? {
|
|
1376
2034
|
paddingTop: androidModalInsets.top,
|
|
1377
2035
|
paddingBottom: androidModalInsets.bottom
|
|
1378
2036
|
} : null
|
|
1379
2037
|
].filter(Boolean)
|
|
1380
2038
|
};
|
|
1381
2039
|
const handleWebViewMessage = (event) => {
|
|
1382
|
-
|
|
2040
|
+
const message = parseHostedBridgeMessage(event.nativeEvent?.data);
|
|
2041
|
+
if (message?.type === "sagepilot:open_file_picker") {
|
|
2042
|
+
openNativeFilePicker(message.multiple ?? true);
|
|
2043
|
+
return;
|
|
2044
|
+
}
|
|
2045
|
+
if (message?.type === "sagepilot:widget_listener_ready") {
|
|
2046
|
+
widgetReadyRef.current = true;
|
|
2047
|
+
startDelivery();
|
|
2048
|
+
return;
|
|
2049
|
+
}
|
|
2050
|
+
if (message?.type === "sagepilot:files_received") {
|
|
2051
|
+
const ackBatchId = message.batch_id;
|
|
2052
|
+
const head = pendingBatchesRef.current[0];
|
|
2053
|
+
if (head && (!ackBatchId || ackBatchId === head.batchId)) {
|
|
2054
|
+
acknowledgeHeadBatch();
|
|
2055
|
+
}
|
|
2056
|
+
return;
|
|
2057
|
+
}
|
|
2058
|
+
if (message?.type === "sagepilot:pong") {
|
|
2059
|
+
const pending = pendingPingRef.current;
|
|
2060
|
+
if (pending && (!message.nonce || message.nonce === pending.nonce)) {
|
|
2061
|
+
if (message.alive === false) {
|
|
2062
|
+
clearPing();
|
|
2063
|
+
if (livenessRemountCountRef.current < LIVENESS_MAX_REMOUNTS) {
|
|
2064
|
+
livenessRemountCountRef.current += 1;
|
|
2065
|
+
recoverNativeWebView();
|
|
2066
|
+
}
|
|
2067
|
+
} else {
|
|
2068
|
+
livenessRemountCountRef.current = 0;
|
|
2069
|
+
clearPing();
|
|
2070
|
+
}
|
|
2071
|
+
}
|
|
2072
|
+
return;
|
|
2073
|
+
}
|
|
2074
|
+
internalSagepilotChat.handleHostedBridgeMessage(message);
|
|
1383
2075
|
};
|
|
1384
2076
|
const postIdentityToWebFrame = () => {
|
|
1385
2077
|
const message = internalSagepilotChat.getHostedIdentityMessage();
|
|
@@ -1392,8 +2084,25 @@ function SagepilotChatProvider({
|
|
|
1392
2084
|
if (!script || !nativeWebViewRef.current) return;
|
|
1393
2085
|
nativeWebViewRef.current.injectJavaScript(script);
|
|
1394
2086
|
};
|
|
2087
|
+
const handleNativeWebViewLoadEnd = () => {
|
|
2088
|
+
postIdentityToNativeWebView();
|
|
2089
|
+
widgetReadyRef.current = false;
|
|
2090
|
+
startDelivery();
|
|
2091
|
+
};
|
|
2092
|
+
const handleShouldStartLoadWithRequest = useCallback((request) => {
|
|
2093
|
+
const url = request?.url ?? "";
|
|
2094
|
+
if (!url || request?.isTopFrame === false || isInternalWebViewScheme(url)) {
|
|
2095
|
+
return true;
|
|
2096
|
+
}
|
|
2097
|
+
const widgetOrigin = readUrlOrigin(state.conversationUrl ?? state.preloadUrl);
|
|
2098
|
+
if (widgetOrigin && readUrlOrigin(url) === widgetOrigin) {
|
|
2099
|
+
return true;
|
|
2100
|
+
}
|
|
2101
|
+
Linking.openURL(url).catch(() => void 0);
|
|
2102
|
+
return false;
|
|
2103
|
+
}, [state.conversationUrl, state.preloadUrl]);
|
|
1395
2104
|
useEffect(() => {
|
|
1396
|
-
if (
|
|
2105
|
+
if (Platform2.OS !== "web" || typeof window === "undefined") return;
|
|
1397
2106
|
const trustedWidgetOrigin = readUrlOrigin(state.conversationUrl);
|
|
1398
2107
|
if (!trustedWidgetOrigin) return;
|
|
1399
2108
|
const handleWindowMessage = (event) => {
|
|
@@ -1404,14 +2113,14 @@ function SagepilotChatProvider({
|
|
|
1404
2113
|
return () => window.removeEventListener("message", handleWindowMessage);
|
|
1405
2114
|
}, [state.conversationUrl]);
|
|
1406
2115
|
useEffect(() => {
|
|
1407
|
-
if (
|
|
2116
|
+
if (Platform2.OS !== "web" || !state.isPresented) return;
|
|
1408
2117
|
postIdentityToWebFrame();
|
|
1409
2118
|
}, [state.isPresented, state.conversationUrl, state.configured]);
|
|
1410
2119
|
useEffect(() => {
|
|
1411
|
-
if (
|
|
2120
|
+
if (Platform2.OS === "web" || !state.isPresented) return;
|
|
1412
2121
|
postIdentityToNativeWebView();
|
|
1413
2122
|
}, [state.isPresented, state.conversationUrl, state.configured]);
|
|
1414
|
-
if (
|
|
2123
|
+
if (Platform2.OS === "web") {
|
|
1415
2124
|
return createElement(
|
|
1416
2125
|
View,
|
|
1417
2126
|
{ style: styles.root },
|
|
@@ -1454,10 +2163,18 @@ function SagepilotChatProvider({
|
|
|
1454
2163
|
children,
|
|
1455
2164
|
state.configured && !state.isPresented && state.shouldPreload && state.preloadUrl ? createElement(WebView, {
|
|
1456
2165
|
...hostedChatWebViewProps,
|
|
2166
|
+
// Separate key from the hosted WebView: a hidden-preload renderer crash
|
|
2167
|
+
// must not bump the hosted key (which would reset widgetReadyRef and
|
|
2168
|
+
// disrupt an in-flight delivery). onRenderProcessGone must still be
|
|
2169
|
+
// handled here or an unhandled renderer kill crashes the whole app.
|
|
2170
|
+
key: `sagepilot-preload-webview-${preloadWebViewKey}`,
|
|
1457
2171
|
source: { uri: state.preloadUrl },
|
|
1458
2172
|
style: styles.preloadWebview,
|
|
1459
2173
|
injectedJavaScriptBeforeContentLoaded: getInjectedWebViewScript(),
|
|
1460
|
-
onMessage: handleWebViewMessage
|
|
2174
|
+
onMessage: handleWebViewMessage,
|
|
2175
|
+
onShouldStartLoadWithRequest: handleShouldStartLoadWithRequest,
|
|
2176
|
+
onRenderProcessGone: recoverPreloadWebView,
|
|
2177
|
+
onContentProcessDidTerminate: recoverPreloadWebView
|
|
1461
2178
|
}) : null,
|
|
1462
2179
|
createElement(
|
|
1463
2180
|
Modal,
|
|
@@ -1465,8 +2182,8 @@ function SagepilotChatProvider({
|
|
|
1465
2182
|
visible: state.configured && state.isPresented,
|
|
1466
2183
|
animationType,
|
|
1467
2184
|
presentationStyle: isFullScreenModal ? "fullScreen" : "pageSheet",
|
|
1468
|
-
statusBarTranslucent:
|
|
1469
|
-
navigationBarTranslucent:
|
|
2185
|
+
statusBarTranslucent: Platform2.OS === "android",
|
|
2186
|
+
navigationBarTranslucent: Platform2.OS === "android",
|
|
1470
2187
|
onRequestClose: () => internalSagepilotChat.dismiss()
|
|
1471
2188
|
},
|
|
1472
2189
|
createElement(
|
|
@@ -1491,13 +2208,23 @@ function SagepilotChatProvider({
|
|
|
1491
2208
|
) : null,
|
|
1492
2209
|
state.conversationUrl ? createElement(WebView, {
|
|
1493
2210
|
...hostedChatWebViewProps,
|
|
2211
|
+
key: `sagepilot-hosted-webview-${nativeWebViewKey}`,
|
|
1494
2212
|
ref: nativeWebViewRef,
|
|
1495
2213
|
source: { uri: state.conversationUrl },
|
|
1496
|
-
|
|
2214
|
+
// The imperceptible opacity toggle forces the Android WebView
|
|
2215
|
+
// surface to re-composite on resume, clearing the blank-but-alive
|
|
2216
|
+
// surface bug without a reload (see runLivenessProbe).
|
|
2217
|
+
style: Platform2.OS === "android" ? [styles.webview, { opacity: 1 - androidRepaintTick % 2 * 1e-3 }] : styles.webview,
|
|
1497
2218
|
startInLoadingState: true,
|
|
1498
2219
|
injectedJavaScriptBeforeContentLoaded: getInjectedWebViewScript(),
|
|
1499
2220
|
onMessage: handleWebViewMessage,
|
|
1500
|
-
onLoadEnd:
|
|
2221
|
+
onLoadEnd: handleNativeWebViewLoadEnd,
|
|
2222
|
+
onShouldStartLoadWithRequest: handleShouldStartLoadWithRequest,
|
|
2223
|
+
// Android: render process killed (commonly while the camera/file
|
|
2224
|
+
// chooser activity is foregrounded). Remount to recover.
|
|
2225
|
+
onRenderProcessGone: recoverNativeWebView,
|
|
2226
|
+
// iOS equivalent: WKWebView content process terminated.
|
|
2227
|
+
onContentProcessDidTerminate: recoverNativeWebView,
|
|
1501
2228
|
renderLoading: () => createElement(
|
|
1502
2229
|
View,
|
|
1503
2230
|
{ style: styles.loading },
|
|
@@ -1507,6 +2234,48 @@ function SagepilotChatProvider({
|
|
|
1507
2234
|
}) : null
|
|
1508
2235
|
)
|
|
1509
2236
|
)
|
|
2237
|
+
),
|
|
2238
|
+
// Native attachment-source chooser. Replaces the Android Alert (capped at
|
|
2239
|
+
// 3 buttons) so camera/library/documents all show on every platform.
|
|
2240
|
+
createElement(
|
|
2241
|
+
Modal,
|
|
2242
|
+
{
|
|
2243
|
+
visible: sourceChooser !== null,
|
|
2244
|
+
transparent: true,
|
|
2245
|
+
animationType: "fade",
|
|
2246
|
+
statusBarTranslucent: true,
|
|
2247
|
+
onRequestClose: () => setSourceChooser(null)
|
|
2248
|
+
},
|
|
2249
|
+
createElement(
|
|
2250
|
+
Pressable,
|
|
2251
|
+
{ style: styles.sheetBackdrop, onPress: () => setSourceChooser(null) },
|
|
2252
|
+
createElement(
|
|
2253
|
+
View,
|
|
2254
|
+
{ style: styles.sheetCard },
|
|
2255
|
+
createElement(Text, { style: styles.sheetTitle }, "Add attachment"),
|
|
2256
|
+
...(internalSagepilotChat.getConfig()?.filePicker?.sources ?? []).map(
|
|
2257
|
+
(source) => createElement(
|
|
2258
|
+
Pressable,
|
|
2259
|
+
{
|
|
2260
|
+
key: source,
|
|
2261
|
+
accessibilityRole: "button",
|
|
2262
|
+
style: styles.sheetButton,
|
|
2263
|
+
onPress: () => handleSourceChoice(source)
|
|
2264
|
+
},
|
|
2265
|
+
createElement(Text, { style: styles.sheetButtonText }, FILE_PICKER_SOURCE_LABELS[source])
|
|
2266
|
+
)
|
|
2267
|
+
),
|
|
2268
|
+
createElement(
|
|
2269
|
+
Pressable,
|
|
2270
|
+
{
|
|
2271
|
+
accessibilityRole: "button",
|
|
2272
|
+
style: [styles.sheetButton, styles.sheetCancelButton],
|
|
2273
|
+
onPress: () => setSourceChooser(null)
|
|
2274
|
+
},
|
|
2275
|
+
createElement(Text, { style: styles.sheetCancelText }, "Cancel")
|
|
2276
|
+
)
|
|
2277
|
+
)
|
|
2278
|
+
)
|
|
1510
2279
|
)
|
|
1511
2280
|
);
|
|
1512
2281
|
}
|
|
@@ -1605,6 +2374,46 @@ var styles = StyleSheet.create({
|
|
|
1605
2374
|
marginTop: 12,
|
|
1606
2375
|
color: "#4b5563",
|
|
1607
2376
|
fontSize: 14
|
|
2377
|
+
},
|
|
2378
|
+
sheetBackdrop: {
|
|
2379
|
+
flex: 1,
|
|
2380
|
+
justifyContent: "flex-end",
|
|
2381
|
+
backgroundColor: "rgba(17, 24, 39, 0.36)"
|
|
2382
|
+
},
|
|
2383
|
+
sheetCard: {
|
|
2384
|
+
backgroundColor: "#ffffff",
|
|
2385
|
+
borderTopLeftRadius: 16,
|
|
2386
|
+
borderTopRightRadius: 16,
|
|
2387
|
+
paddingTop: 8,
|
|
2388
|
+
paddingBottom: 24,
|
|
2389
|
+
paddingHorizontal: 8
|
|
2390
|
+
},
|
|
2391
|
+
sheetTitle: {
|
|
2392
|
+
textAlign: "center",
|
|
2393
|
+
color: "#6b7280",
|
|
2394
|
+
fontSize: 13,
|
|
2395
|
+
fontWeight: "600",
|
|
2396
|
+
paddingVertical: 10
|
|
2397
|
+
},
|
|
2398
|
+
sheetButton: {
|
|
2399
|
+
minHeight: 52,
|
|
2400
|
+
alignItems: "center",
|
|
2401
|
+
justifyContent: "center",
|
|
2402
|
+
borderRadius: 12
|
|
2403
|
+
},
|
|
2404
|
+
sheetButtonText: {
|
|
2405
|
+
color: "#111827",
|
|
2406
|
+
fontSize: 16,
|
|
2407
|
+
fontWeight: "500"
|
|
2408
|
+
},
|
|
2409
|
+
sheetCancelButton: {
|
|
2410
|
+
marginTop: 6,
|
|
2411
|
+
backgroundColor: "#f3f4f6"
|
|
2412
|
+
},
|
|
2413
|
+
sheetCancelText: {
|
|
2414
|
+
color: "#111827",
|
|
2415
|
+
fontSize: 16,
|
|
2416
|
+
fontWeight: "600"
|
|
1608
2417
|
}
|
|
1609
2418
|
});
|
|
1610
2419
|
|
|
@@ -1661,7 +2470,10 @@ export {
|
|
|
1661
2470
|
SagepilotChat,
|
|
1662
2471
|
SagepilotChatError,
|
|
1663
2472
|
SagepilotChatProvider,
|
|
2473
|
+
SagepilotFilePickerError,
|
|
1664
2474
|
createAsyncStorageCacheStorage,
|
|
1665
2475
|
createKeychainTokenStorage,
|
|
2476
|
+
createSagepilotFilePicker,
|
|
2477
|
+
createSagepilotFileStore,
|
|
1666
2478
|
useSagepilotChat
|
|
1667
2479
|
};
|