@sagepilot-ai/react-native-sdk 0.2.3 → 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 +110 -1
- package/dist/index.d.mts +228 -1
- package/dist/index.d.ts +228 -1
- package/dist/index.js +948 -40
- package/dist/index.mjs +932 -25
- package/package.json +13 -1
package/dist/index.js
CHANGED
|
@@ -33,8 +33,11 @@ __export(index_exports, {
|
|
|
33
33
|
SagepilotChat: () => SagepilotChat,
|
|
34
34
|
SagepilotChatError: () => SagepilotChatError,
|
|
35
35
|
SagepilotChatProvider: () => SagepilotChatProvider,
|
|
36
|
+
SagepilotFilePickerError: () => SagepilotFilePickerError,
|
|
36
37
|
createAsyncStorageCacheStorage: () => createAsyncStorageCacheStorage,
|
|
37
38
|
createKeychainTokenStorage: () => createKeychainTokenStorage,
|
|
39
|
+
createSagepilotFilePicker: () => createSagepilotFilePicker,
|
|
40
|
+
createSagepilotFileStore: () => createSagepilotFileStore,
|
|
38
41
|
useSagepilotChat: () => useSagepilotChat
|
|
39
42
|
});
|
|
40
43
|
module.exports = __toCommonJS(index_exports);
|
|
@@ -56,7 +59,7 @@ var SagepilotChatError = class extends Error {
|
|
|
56
59
|
|
|
57
60
|
// src/core/config/constants.ts
|
|
58
61
|
var SDK_NAME = "@sagepilot-ai/react-native-sdk";
|
|
59
|
-
var SDK_VERSION = "0.2.
|
|
62
|
+
var SDK_VERSION = "0.2.5";
|
|
60
63
|
var DEFAULT_HOST = "https://app.sagepilot.ai";
|
|
61
64
|
var DEFAULT_WIDGET_HOST = "https://app.sagepilot.ai";
|
|
62
65
|
var CUSTOMER_API_PREFIX = "/customer-api/v1";
|
|
@@ -505,6 +508,18 @@ function normalizeIdentity(identity) {
|
|
|
505
508
|
user_hash: identity.userHash ?? identity.user_hash
|
|
506
509
|
};
|
|
507
510
|
}
|
|
511
|
+
function readStringField(input, key) {
|
|
512
|
+
const value = input[key];
|
|
513
|
+
return typeof value === "string" && value.trim() ? value : void 0;
|
|
514
|
+
}
|
|
515
|
+
function normalizeOptionalString(value) {
|
|
516
|
+
return typeof value === "string" && value.trim() ? value.trim() : void 0;
|
|
517
|
+
}
|
|
518
|
+
function readRecordField(input, key) {
|
|
519
|
+
const value = input[key];
|
|
520
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return void 0;
|
|
521
|
+
return value;
|
|
522
|
+
}
|
|
508
523
|
function toPublicSessionState(session) {
|
|
509
524
|
return {
|
|
510
525
|
session_id: session.session_id,
|
|
@@ -538,6 +553,7 @@ var SagepilotReactNativeChat = class {
|
|
|
538
553
|
this.unreadPollTimer = null;
|
|
539
554
|
this.stateCallbacks = /* @__PURE__ */ new Set();
|
|
540
555
|
this.identifyCallbacks = /* @__PURE__ */ new Set();
|
|
556
|
+
this.conversationCreatedCallbacks = /* @__PURE__ */ new Set();
|
|
541
557
|
this.unreadCallbacks = /* @__PURE__ */ new Set();
|
|
542
558
|
this.readyCallbacks = /* @__PURE__ */ new Set();
|
|
543
559
|
this.presentCallbacks = /* @__PURE__ */ new Set();
|
|
@@ -560,6 +576,9 @@ var SagepilotReactNativeChat = class {
|
|
|
560
576
|
const { workspaceId, channelId } = parseKey(config.key);
|
|
561
577
|
this.runtimeOptions = {
|
|
562
578
|
behavior: config.behavior,
|
|
579
|
+
cacheStorage: config.cacheStorage,
|
|
580
|
+
filePicker: config.filePicker,
|
|
581
|
+
fileStore: config.fileStore,
|
|
563
582
|
presentation: config.presentation,
|
|
564
583
|
theme: config.theme,
|
|
565
584
|
hostedClaimsStorageKeyPrefix: config.hostedClaimsStorageKeyPrefix || DEFAULT_HOSTED_CLAIMS_STORAGE_KEY_PREFIX
|
|
@@ -744,7 +763,15 @@ var SagepilotReactNativeChat = class {
|
|
|
744
763
|
}
|
|
745
764
|
presentMessageComposer(message, options) {
|
|
746
765
|
this.requireConfigured();
|
|
747
|
-
|
|
766
|
+
const chatId = normalizeOptionalString(options?.chatId);
|
|
767
|
+
this.hostedChatView = {
|
|
768
|
+
screen: "composer",
|
|
769
|
+
message,
|
|
770
|
+
mode: options?.mode ?? "auto",
|
|
771
|
+
chatId,
|
|
772
|
+
metadata: options?.metadata,
|
|
773
|
+
onConversationCreated: options?.onConversationCreated
|
|
774
|
+
};
|
|
748
775
|
return this.showHostedChat();
|
|
749
776
|
}
|
|
750
777
|
dismiss() {
|
|
@@ -793,6 +820,13 @@ var SagepilotReactNativeChat = class {
|
|
|
793
820
|
this.dismissCallbacks.delete(callback);
|
|
794
821
|
};
|
|
795
822
|
}
|
|
823
|
+
onConversationCreated(callback) {
|
|
824
|
+
if (typeof callback !== "function") return () => void 0;
|
|
825
|
+
this.conversationCreatedCallbacks.add(callback);
|
|
826
|
+
return () => {
|
|
827
|
+
this.conversationCreatedCallbacks.delete(callback);
|
|
828
|
+
};
|
|
829
|
+
}
|
|
796
830
|
onError(callback) {
|
|
797
831
|
if (typeof callback !== "function") return () => void 0;
|
|
798
832
|
this.errorCallbacks.add(callback);
|
|
@@ -844,7 +878,10 @@ var SagepilotReactNativeChat = class {
|
|
|
844
878
|
url.searchParams.set("jwt", this.legacyWidgetJwt);
|
|
845
879
|
}
|
|
846
880
|
if (this.hostedChatView.screen === "composer") {
|
|
847
|
-
if (this.hostedChatView.
|
|
881
|
+
if (this.hostedChatView.chatId) {
|
|
882
|
+
url.searchParams.set("chat_id", this.hostedChatView.chatId);
|
|
883
|
+
}
|
|
884
|
+
if (this.hostedChatView.mode === "new" && !this.hostedChatView.chatId) {
|
|
848
885
|
url.searchParams.set("new", "true");
|
|
849
886
|
}
|
|
850
887
|
if (this.hostedChatView.message) {
|
|
@@ -872,13 +909,19 @@ var SagepilotReactNativeChat = class {
|
|
|
872
909
|
"})();"
|
|
873
910
|
].join("\n");
|
|
874
911
|
}
|
|
912
|
+
getActiveHostedChatId() {
|
|
913
|
+
if (this.hostedChatView.screen === "composer" && this.hostedChatView.chatId) {
|
|
914
|
+
return this.hostedChatView.chatId;
|
|
915
|
+
}
|
|
916
|
+
return this.session?.conversation_id ?? void 0;
|
|
917
|
+
}
|
|
875
918
|
getHostedIdentityMessage() {
|
|
876
919
|
if (!this.legacyWidgetJwt) return null;
|
|
877
920
|
return {
|
|
878
921
|
type: "identity_update",
|
|
879
922
|
data: {
|
|
880
923
|
jwt: this.legacyWidgetJwt,
|
|
881
|
-
chat_id: this.
|
|
924
|
+
chat_id: this.getActiveHostedChatId()
|
|
882
925
|
}
|
|
883
926
|
};
|
|
884
927
|
}
|
|
@@ -897,17 +940,62 @@ var SagepilotReactNativeChat = class {
|
|
|
897
940
|
}
|
|
898
941
|
return true;
|
|
899
942
|
}
|
|
943
|
+
if (message.type === "sagepilot:conversation_created") {
|
|
944
|
+
this.handleConversationCreated(message);
|
|
945
|
+
return true;
|
|
946
|
+
}
|
|
900
947
|
if (message.type === "sagepilot:error") {
|
|
901
948
|
this.emitError(message);
|
|
902
949
|
return true;
|
|
903
950
|
}
|
|
904
951
|
return true;
|
|
905
952
|
}
|
|
953
|
+
handleConversationCreated(message) {
|
|
954
|
+
const chatId = readStringField(message, "chat_id") ?? readStringField(message, "conversation_id");
|
|
955
|
+
if (!chatId) return;
|
|
956
|
+
void this.persistConversationId(chatId).catch((error) => this.emitError(error));
|
|
957
|
+
const bridgeMetadata = readRecordField(message, "metadata");
|
|
958
|
+
const composerMetadata = this.hostedChatView.screen === "composer" ? this.hostedChatView.metadata : void 0;
|
|
959
|
+
const metadata = {
|
|
960
|
+
...composerMetadata ?? {},
|
|
961
|
+
...bridgeMetadata ?? {}
|
|
962
|
+
};
|
|
963
|
+
const workspaceId = readStringField(message, "workspace_id") ?? this.workspaceId;
|
|
964
|
+
const channelId = readStringField(message, "channel_id") ?? this.channelId;
|
|
965
|
+
const sessionId = readStringField(message, "session_id") ?? this.session?.session_id;
|
|
966
|
+
const customerId = readStringField(message, "customer_id");
|
|
967
|
+
const messageId = readStringField(message, "message_id");
|
|
968
|
+
const createdAt = readStringField(message, "created_at");
|
|
969
|
+
const event = {
|
|
970
|
+
chat_id: chatId,
|
|
971
|
+
...workspaceId ? { workspace_id: workspaceId } : {},
|
|
972
|
+
...channelId ? { channel_id: channelId } : {},
|
|
973
|
+
...sessionId ? { session_id: sessionId } : {},
|
|
974
|
+
...customerId ? { customer_id: customerId } : {},
|
|
975
|
+
...messageId ? { message_id: messageId } : {},
|
|
976
|
+
...createdAt ? { created_at: createdAt } : {},
|
|
977
|
+
...Object.keys(metadata).length > 0 ? { metadata } : {}
|
|
978
|
+
};
|
|
979
|
+
if (this.hostedChatView.screen === "composer") {
|
|
980
|
+
this.hostedChatView.onConversationCreated?.(event);
|
|
981
|
+
}
|
|
982
|
+
this.conversationCreatedCallbacks.forEach((callback) => callback(event));
|
|
983
|
+
}
|
|
984
|
+
async persistConversationId(conversationId) {
|
|
985
|
+
if (!this.session || !this.sessionManager) return;
|
|
986
|
+
if (this.session.conversation_id === conversationId) return;
|
|
987
|
+
this.session = await this.sessionManager.setSession({
|
|
988
|
+
...this.session,
|
|
989
|
+
conversation_id: conversationId
|
|
990
|
+
});
|
|
991
|
+
this.emitState();
|
|
992
|
+
}
|
|
906
993
|
destroy() {
|
|
907
994
|
this.stopUnreadPolling();
|
|
908
995
|
this.resetRuntimeState();
|
|
909
996
|
this.emitState();
|
|
910
997
|
this.identifyCallbacks.clear();
|
|
998
|
+
this.conversationCreatedCallbacks.clear();
|
|
911
999
|
this.unreadCallbacks.clear();
|
|
912
1000
|
this.readyCallbacks.clear();
|
|
913
1001
|
this.presentCallbacks.clear();
|
|
@@ -1070,6 +1158,9 @@ var SagepilotChat = {
|
|
|
1070
1158
|
logout: () => internalSagepilotChat.logout(),
|
|
1071
1159
|
getIdentityState: () => internalSagepilotChat.getIdentityState(),
|
|
1072
1160
|
onIdentify: (callback) => internalSagepilotChat.onIdentify(callback),
|
|
1161
|
+
onConversationCreated: (callback) => {
|
|
1162
|
+
return internalSagepilotChat.onConversationCreated(callback);
|
|
1163
|
+
},
|
|
1073
1164
|
getUnreadCount: () => internalSagepilotChat.getUnreadCount(),
|
|
1074
1165
|
onUnreadChange: (callback) => internalSagepilotChat.onUnreadChange(callback),
|
|
1075
1166
|
startUnreadPolling: (intervalMs) => internalSagepilotChat.startUnreadPolling(intervalMs),
|
|
@@ -1102,13 +1193,323 @@ function createAsyncStorageCacheStorage(asyncStorage) {
|
|
|
1102
1193
|
removeItem: (key) => asyncStorage.removeItem(key)
|
|
1103
1194
|
};
|
|
1104
1195
|
}
|
|
1196
|
+
function createJsonCache(storage, namespace) {
|
|
1197
|
+
return {
|
|
1198
|
+
async get(key) {
|
|
1199
|
+
const value = await storage.getItem(`${namespace}:${key}`);
|
|
1200
|
+
if (!value) return null;
|
|
1201
|
+
try {
|
|
1202
|
+
return JSON.parse(value);
|
|
1203
|
+
} catch {
|
|
1204
|
+
return null;
|
|
1205
|
+
}
|
|
1206
|
+
},
|
|
1207
|
+
async set(key, value) {
|
|
1208
|
+
await storage.setItem(`${namespace}:${key}`, JSON.stringify(value));
|
|
1209
|
+
},
|
|
1210
|
+
async remove(key) {
|
|
1211
|
+
await storage.removeItem(`${namespace}:${key}`);
|
|
1212
|
+
}
|
|
1213
|
+
};
|
|
1214
|
+
}
|
|
1215
|
+
|
|
1216
|
+
// src/core/storage/fileStore.ts
|
|
1217
|
+
var DEFAULT_DIRECTORY_NAME = "sagepilot-attachments";
|
|
1218
|
+
function sanitizeKey(key) {
|
|
1219
|
+
return key.replace(/[^a-zA-Z0-9._-]/g, "_").replace(/\.{2,}/g, "_");
|
|
1220
|
+
}
|
|
1221
|
+
function createSagepilotFileStore(blobUtil, options = {}) {
|
|
1222
|
+
const directory = `${blobUtil.fs.dirs.DocumentDir}/${options.directoryName ?? DEFAULT_DIRECTORY_NAME}`;
|
|
1223
|
+
const pathFor = (key) => `${directory}/${sanitizeKey(key)}`;
|
|
1224
|
+
let dirReady = null;
|
|
1225
|
+
function ensureDirectory() {
|
|
1226
|
+
if (!dirReady) {
|
|
1227
|
+
dirReady = (async () => {
|
|
1228
|
+
if (!await blobUtil.fs.exists(directory)) {
|
|
1229
|
+
await blobUtil.fs.mkdir(directory);
|
|
1230
|
+
}
|
|
1231
|
+
})().catch((error) => {
|
|
1232
|
+
dirReady = null;
|
|
1233
|
+
throw error;
|
|
1234
|
+
});
|
|
1235
|
+
}
|
|
1236
|
+
return dirReady;
|
|
1237
|
+
}
|
|
1238
|
+
return {
|
|
1239
|
+
async write(key, base64) {
|
|
1240
|
+
await ensureDirectory();
|
|
1241
|
+
await blobUtil.fs.writeFile(pathFor(key), base64, "base64");
|
|
1242
|
+
},
|
|
1243
|
+
async read(key) {
|
|
1244
|
+
const content = await blobUtil.fs.readFile(pathFor(key), "base64");
|
|
1245
|
+
if (typeof content !== "string" || content.length === 0) {
|
|
1246
|
+
throw new Error("Persisted attachment is empty or unreadable.");
|
|
1247
|
+
}
|
|
1248
|
+
return content;
|
|
1249
|
+
},
|
|
1250
|
+
async remove(key) {
|
|
1251
|
+
try {
|
|
1252
|
+
if (await blobUtil.fs.exists(pathFor(key))) {
|
|
1253
|
+
await blobUtil.fs.unlink(pathFor(key));
|
|
1254
|
+
}
|
|
1255
|
+
} catch {
|
|
1256
|
+
}
|
|
1257
|
+
},
|
|
1258
|
+
async prune(keepKeys) {
|
|
1259
|
+
try {
|
|
1260
|
+
if (!await blobUtil.fs.exists(directory)) return;
|
|
1261
|
+
const keep = new Set(keepKeys.map(sanitizeKey));
|
|
1262
|
+
const entries = await blobUtil.fs.ls(directory);
|
|
1263
|
+
await Promise.all(
|
|
1264
|
+
entries.filter((entry) => !keep.has(entry)).map((entry) => blobUtil.fs.unlink(`${directory}/${entry}`).catch(() => void 0))
|
|
1265
|
+
);
|
|
1266
|
+
} catch {
|
|
1267
|
+
}
|
|
1268
|
+
}
|
|
1269
|
+
};
|
|
1270
|
+
}
|
|
1271
|
+
|
|
1272
|
+
// src/core/native/filePicker.ts
|
|
1273
|
+
var import_react_native = require("react-native");
|
|
1274
|
+
var DEFAULT_IMAGE_MAX_DIMENSION = 1920;
|
|
1275
|
+
var DEFAULT_IMAGE_QUALITY = 0.8;
|
|
1276
|
+
var DEFAULT_IMAGE_MIME_TYPE = "image/jpeg";
|
|
1277
|
+
var DEFAULT_DOCUMENT_MIME_TYPE = "application/octet-stream";
|
|
1278
|
+
var DEFAULT_IMAGE_SELECTION_LIMIT = 5;
|
|
1279
|
+
var DEFAULT_DOCUMENT_MAX_FILE_SIZE_BYTES = 20 * 1024 * 1024;
|
|
1280
|
+
var DEFAULT_IMAGE_MAX_FILE_SIZE_BYTES = 15 * 1024 * 1024;
|
|
1281
|
+
var SagepilotFilePickerError = class extends Error {
|
|
1282
|
+
constructor(code, message) {
|
|
1283
|
+
super(message);
|
|
1284
|
+
this.name = "SagepilotFilePickerError";
|
|
1285
|
+
this.code = code;
|
|
1286
|
+
}
|
|
1287
|
+
};
|
|
1288
|
+
function bytesToMb(bytes) {
|
|
1289
|
+
return Math.round(bytes / (1024 * 1024) * 10) / 10;
|
|
1290
|
+
}
|
|
1291
|
+
function estimateBase64ByteSize(dataBase64) {
|
|
1292
|
+
return Math.floor(dataBase64.length * 3 / 4);
|
|
1293
|
+
}
|
|
1294
|
+
function stripFileScheme(uri) {
|
|
1295
|
+
return uri.startsWith("file://") ? uri.replace("file://", "") : uri;
|
|
1296
|
+
}
|
|
1297
|
+
async function ensureAndroidCameraPermission() {
|
|
1298
|
+
if (import_react_native.Platform.OS !== "android") return;
|
|
1299
|
+
let result;
|
|
1300
|
+
try {
|
|
1301
|
+
result = await import_react_native.PermissionsAndroid.request(import_react_native.PermissionsAndroid.PERMISSIONS.CAMERA);
|
|
1302
|
+
} catch {
|
|
1303
|
+
return;
|
|
1304
|
+
}
|
|
1305
|
+
if (result === import_react_native.PermissionsAndroid.RESULTS.NEVER_ASK_AGAIN) {
|
|
1306
|
+
throw new SagepilotFilePickerError(
|
|
1307
|
+
"permission_denied",
|
|
1308
|
+
"Camera access is turned off. Enable it for this app in Settings to take a photo."
|
|
1309
|
+
);
|
|
1310
|
+
}
|
|
1311
|
+
if (result === import_react_native.PermissionsAndroid.RESULTS.DENIED) {
|
|
1312
|
+
throw new SagepilotFilePickerError(
|
|
1313
|
+
"permission_denied",
|
|
1314
|
+
"Camera permission is required to take a photo."
|
|
1315
|
+
);
|
|
1316
|
+
}
|
|
1317
|
+
}
|
|
1318
|
+
function mapImagePickerErrorCode(errorCode) {
|
|
1319
|
+
switch (errorCode) {
|
|
1320
|
+
case "permission":
|
|
1321
|
+
return "permission_denied";
|
|
1322
|
+
case "camera_unavailable":
|
|
1323
|
+
return "camera_unavailable";
|
|
1324
|
+
default:
|
|
1325
|
+
return "unknown";
|
|
1326
|
+
}
|
|
1327
|
+
}
|
|
1328
|
+
function imageAssetsToPickedFiles(result, maxFileSizeBytes) {
|
|
1329
|
+
if (result.didCancel) return [];
|
|
1330
|
+
if (result.errorCode) {
|
|
1331
|
+
throw new SagepilotFilePickerError(
|
|
1332
|
+
mapImagePickerErrorCode(result.errorCode),
|
|
1333
|
+
result.errorMessage || `Could not capture the photo (${result.errorCode}).`
|
|
1334
|
+
);
|
|
1335
|
+
}
|
|
1336
|
+
const assets = result.assets ?? [];
|
|
1337
|
+
const usable = assets.filter((asset) => typeof asset.base64 === "string" && asset.base64.length > 0);
|
|
1338
|
+
if (assets.length > usable.length) {
|
|
1339
|
+
throw new SagepilotFilePickerError(
|
|
1340
|
+
"encode_failed",
|
|
1341
|
+
assets.length === 1 ? "The photo could not be processed. Please try again." : "Some photos could not be processed. Please try selecting them again."
|
|
1342
|
+
);
|
|
1343
|
+
}
|
|
1344
|
+
return usable.map((asset, index) => {
|
|
1345
|
+
const dataBase64 = asset.base64;
|
|
1346
|
+
const size = asset.fileSize ?? estimateBase64ByteSize(dataBase64);
|
|
1347
|
+
if (maxFileSizeBytes > 0 && size > maxFileSizeBytes) {
|
|
1348
|
+
throw new SagepilotFilePickerError(
|
|
1349
|
+
"file_too_large",
|
|
1350
|
+
`Image is too large (max ${bytesToMb(maxFileSizeBytes)}MB).`
|
|
1351
|
+
);
|
|
1352
|
+
}
|
|
1353
|
+
return {
|
|
1354
|
+
file_name: asset.fileName || `photo-${Date.now()}-${index + 1}.jpg`,
|
|
1355
|
+
mime_type: asset.type || DEFAULT_IMAGE_MIME_TYPE,
|
|
1356
|
+
size,
|
|
1357
|
+
data_base64: dataBase64
|
|
1358
|
+
};
|
|
1359
|
+
});
|
|
1360
|
+
}
|
|
1361
|
+
async function readUriAsBase64(uri, fileReader) {
|
|
1362
|
+
if (fileReader) {
|
|
1363
|
+
const content = await fileReader.fs.readFile(stripFileScheme(uri), "base64");
|
|
1364
|
+
if (typeof content !== "string") {
|
|
1365
|
+
throw new Error("File reader returned unexpected content.");
|
|
1366
|
+
}
|
|
1367
|
+
return content;
|
|
1368
|
+
}
|
|
1369
|
+
const response = await fetch(uri);
|
|
1370
|
+
const blob = await response.blob();
|
|
1371
|
+
return new Promise((resolve, reject) => {
|
|
1372
|
+
const reader = new FileReader();
|
|
1373
|
+
reader.onload = () => {
|
|
1374
|
+
const dataUrl = typeof reader.result === "string" ? reader.result : "";
|
|
1375
|
+
const [, payload = ""] = dataUrl.split(",");
|
|
1376
|
+
if (!payload) {
|
|
1377
|
+
reject(new Error("Could not read the selected file."));
|
|
1378
|
+
return;
|
|
1379
|
+
}
|
|
1380
|
+
resolve(payload);
|
|
1381
|
+
};
|
|
1382
|
+
reader.onerror = () => reject(new Error("Could not read the selected file."));
|
|
1383
|
+
reader.readAsDataURL(blob);
|
|
1384
|
+
});
|
|
1385
|
+
}
|
|
1386
|
+
function createSagepilotFilePicker(options) {
|
|
1387
|
+
const { imagePicker, documentsPicker, fileReader } = options;
|
|
1388
|
+
const imageMaxDimension = options.imageMaxDimension ?? DEFAULT_IMAGE_MAX_DIMENSION;
|
|
1389
|
+
const imageQuality = options.imageQuality ?? DEFAULT_IMAGE_QUALITY;
|
|
1390
|
+
const imageSelectionLimit = options.imageSelectionLimit ?? DEFAULT_IMAGE_SELECTION_LIMIT;
|
|
1391
|
+
const documentMaxFileSizeBytes = options.documentMaxFileSizeBytes ?? DEFAULT_DOCUMENT_MAX_FILE_SIZE_BYTES;
|
|
1392
|
+
const imageMaxFileSizeBytes = options.imageMaxFileSizeBytes ?? DEFAULT_IMAGE_MAX_FILE_SIZE_BYTES;
|
|
1393
|
+
const sources = [];
|
|
1394
|
+
if (imagePicker) sources.push("camera", "library");
|
|
1395
|
+
if (documentsPicker) sources.push("documents");
|
|
1396
|
+
if (sources.length === 0) return void 0;
|
|
1397
|
+
const imageOptions = {
|
|
1398
|
+
mediaType: "photo",
|
|
1399
|
+
includeBase64: true,
|
|
1400
|
+
maxWidth: imageMaxDimension,
|
|
1401
|
+
maxHeight: imageMaxDimension,
|
|
1402
|
+
quality: imageQuality,
|
|
1403
|
+
saveToPhotos: false
|
|
1404
|
+
};
|
|
1405
|
+
async function pickFromCamera() {
|
|
1406
|
+
if (!imagePicker) return [];
|
|
1407
|
+
await ensureAndroidCameraPermission();
|
|
1408
|
+
return imageAssetsToPickedFiles(await imagePicker.launchCamera(imageOptions), imageMaxFileSizeBytes);
|
|
1409
|
+
}
|
|
1410
|
+
async function pickFromLibrary(multiple) {
|
|
1411
|
+
if (!imagePicker) return [];
|
|
1412
|
+
return imageAssetsToPickedFiles(await imagePicker.launchImageLibrary({
|
|
1413
|
+
...imageOptions,
|
|
1414
|
+
// Bounded multi-select: 0 (unlimited) lets a huge batch OOM the bridge.
|
|
1415
|
+
selectionLimit: multiple ? Math.max(0, imageSelectionLimit) : 1
|
|
1416
|
+
}), imageMaxFileSizeBytes);
|
|
1417
|
+
}
|
|
1418
|
+
async function pickDocuments(multiple) {
|
|
1419
|
+
if (!documentsPicker) return [];
|
|
1420
|
+
let picked;
|
|
1421
|
+
try {
|
|
1422
|
+
picked = await documentsPicker.pick({ allowMultiSelection: multiple });
|
|
1423
|
+
} catch (error) {
|
|
1424
|
+
if (error && typeof error === "object" && error.code === "OPERATION_CANCELED") {
|
|
1425
|
+
return [];
|
|
1426
|
+
}
|
|
1427
|
+
throw error;
|
|
1428
|
+
}
|
|
1429
|
+
const files = [];
|
|
1430
|
+
for (const file of picked) {
|
|
1431
|
+
if (documentMaxFileSizeBytes > 0 && typeof file.size === "number" && file.size > documentMaxFileSizeBytes) {
|
|
1432
|
+
throw new SagepilotFilePickerError(
|
|
1433
|
+
"file_too_large",
|
|
1434
|
+
`"${file.name || "File"}" is too large (max ${bytesToMb(documentMaxFileSizeBytes)}MB).`
|
|
1435
|
+
);
|
|
1436
|
+
}
|
|
1437
|
+
let readableUri = file.uri;
|
|
1438
|
+
if (documentsPicker.keepLocalCopy) {
|
|
1439
|
+
const fileName = file.name || `file-${Date.now()}`;
|
|
1440
|
+
const [copy] = await documentsPicker.keepLocalCopy({
|
|
1441
|
+
files: [{ uri: file.uri, fileName }],
|
|
1442
|
+
destination: "cachesDirectory"
|
|
1443
|
+
});
|
|
1444
|
+
if (copy?.status === "success" && copy.localUri) {
|
|
1445
|
+
readableUri = copy.localUri;
|
|
1446
|
+
}
|
|
1447
|
+
}
|
|
1448
|
+
let dataBase64;
|
|
1449
|
+
try {
|
|
1450
|
+
dataBase64 = await readUriAsBase64(readableUri, fileReader);
|
|
1451
|
+
} catch (error) {
|
|
1452
|
+
throw new SagepilotFilePickerError(
|
|
1453
|
+
"read_failed",
|
|
1454
|
+
error instanceof Error && error.message ? error.message : "Could not read the selected file."
|
|
1455
|
+
);
|
|
1456
|
+
}
|
|
1457
|
+
files.push({
|
|
1458
|
+
file_name: file.name || `file-${Date.now()}`,
|
|
1459
|
+
mime_type: file.type || DEFAULT_DOCUMENT_MIME_TYPE,
|
|
1460
|
+
size: file.size ?? estimateBase64ByteSize(dataBase64),
|
|
1461
|
+
data_base64: dataBase64
|
|
1462
|
+
});
|
|
1463
|
+
}
|
|
1464
|
+
return files;
|
|
1465
|
+
}
|
|
1466
|
+
return {
|
|
1467
|
+
sources,
|
|
1468
|
+
async pickFiles(request) {
|
|
1469
|
+
if (request.source === "camera") return pickFromCamera();
|
|
1470
|
+
if (request.source === "library") return pickFromLibrary(request.multiple);
|
|
1471
|
+
return pickDocuments(request.multiple);
|
|
1472
|
+
}
|
|
1473
|
+
};
|
|
1474
|
+
}
|
|
1105
1475
|
|
|
1106
1476
|
// src/ui/SagepilotChatProvider.ts
|
|
1107
1477
|
var import_react = require("react");
|
|
1108
|
-
var
|
|
1478
|
+
var import_react_native2 = require("react-native");
|
|
1109
1479
|
var import_react_native_webview = require("react-native-webview");
|
|
1110
1480
|
|
|
1111
1481
|
// src/core/webview/mobileBridge.ts
|
|
1482
|
+
var FILE_PICKER_PROTOCOL_VERSION = 2;
|
|
1483
|
+
function buildBridgeCapabilitiesScript(capabilities) {
|
|
1484
|
+
const payload = { ...capabilities, filePickerProtocol: FILE_PICKER_PROTOCOL_VERSION };
|
|
1485
|
+
return [
|
|
1486
|
+
"(function(){",
|
|
1487
|
+
"try {",
|
|
1488
|
+
"if (window.SagepilotMobileBridge) {",
|
|
1489
|
+
`window.SagepilotMobileBridge.capabilities = Object.assign({}, window.SagepilotMobileBridge.capabilities, ${JSON.stringify(payload)});`,
|
|
1490
|
+
"}",
|
|
1491
|
+
"} catch (_) {}",
|
|
1492
|
+
"true;",
|
|
1493
|
+
"})();"
|
|
1494
|
+
].join("\n");
|
|
1495
|
+
}
|
|
1496
|
+
function buildLivenessPingScript(nonce, staleMs) {
|
|
1497
|
+
return [
|
|
1498
|
+
"(function(){",
|
|
1499
|
+
"try {",
|
|
1500
|
+
"var hb = window.__sagepilotWidgetHeartbeat;",
|
|
1501
|
+
"var supports = typeof hb === 'number';",
|
|
1502
|
+
`var alive = !supports ? true : (Date.now() - hb < ${Math.max(0, Math.floor(staleMs))});`,
|
|
1503
|
+
"var send = function(){",
|
|
1504
|
+
" var msg = JSON.stringify({ type: 'sagepilot:pong', nonce: " + JSON.stringify(nonce) + ", alive: alive, supportsHeartbeat: supports });",
|
|
1505
|
+
" if (window.ReactNativeWebView && window.ReactNativeWebView.postMessage) { window.ReactNativeWebView.postMessage(msg); }",
|
|
1506
|
+
"};",
|
|
1507
|
+
"send();",
|
|
1508
|
+
"} catch (_) {}",
|
|
1509
|
+
"true;",
|
|
1510
|
+
"})();"
|
|
1511
|
+
].join("\n");
|
|
1512
|
+
}
|
|
1112
1513
|
function isHostedBridgeMessage(value) {
|
|
1113
1514
|
if (!value || typeof value !== "object") return false;
|
|
1114
1515
|
const message = value;
|
|
@@ -1234,9 +1635,70 @@ function readPresentationState() {
|
|
|
1234
1635
|
function getInjectedWebViewScript() {
|
|
1235
1636
|
return [
|
|
1236
1637
|
internalSagepilotChat.getHostedAuthScript(),
|
|
1237
|
-
mobileWebViewBridgeScript
|
|
1638
|
+
mobileWebViewBridgeScript,
|
|
1639
|
+
buildBridgeCapabilitiesScript({
|
|
1640
|
+
nativeFilePicker: Boolean(internalSagepilotChat.getConfig()?.filePicker)
|
|
1641
|
+
})
|
|
1238
1642
|
].filter(Boolean).join("\n");
|
|
1239
1643
|
}
|
|
1644
|
+
var FILE_PICKER_SOURCE_LABELS = {
|
|
1645
|
+
camera: "Take photo",
|
|
1646
|
+
library: "Choose from gallery",
|
|
1647
|
+
documents: "Browse files"
|
|
1648
|
+
};
|
|
1649
|
+
var DELIVERY_RETRY_MS = 1500;
|
|
1650
|
+
var DELIVERY_LEGACY_FALLBACK_ATTEMPTS = 2;
|
|
1651
|
+
var DELIVERY_MAX_ATTEMPTS = 8;
|
|
1652
|
+
var PERSIST_MAX_TOTAL_BYTES = 2 * 1024 * 1024;
|
|
1653
|
+
var PENDING_FILES_CACHE_NAMESPACE = "sagepilot_rn_picker";
|
|
1654
|
+
var PENDING_FILES_CACHE_KEY = "pending_batch";
|
|
1655
|
+
var LIVENESS_HEARTBEAT_STALE_MS = 4e3;
|
|
1656
|
+
var LIVENESS_PONG_TIMEOUT_MS = 1500;
|
|
1657
|
+
var LIVENESS_MAX_PING_ATTEMPTS = 2;
|
|
1658
|
+
var LIVENESS_MAX_REMOUNTS = 2;
|
|
1659
|
+
var batchSequence = 0;
|
|
1660
|
+
function nextBatchId() {
|
|
1661
|
+
batchSequence += 1;
|
|
1662
|
+
const entropy = Math.random().toString(36).slice(2, 8);
|
|
1663
|
+
return `b_${Date.now().toString(36)}_${batchSequence}_${entropy}`;
|
|
1664
|
+
}
|
|
1665
|
+
function totalBase64Bytes(files) {
|
|
1666
|
+
return files.reduce((sum, file) => sum + (file.data_base64?.length ?? 0), 0);
|
|
1667
|
+
}
|
|
1668
|
+
function storageKeyFor(batchId, index) {
|
|
1669
|
+
return `${batchId}_${index}.bin`;
|
|
1670
|
+
}
|
|
1671
|
+
function buildDispatchScript(messageLiteral) {
|
|
1672
|
+
return [
|
|
1673
|
+
"(function(){",
|
|
1674
|
+
"try {",
|
|
1675
|
+
`var message = ${messageLiteral};`,
|
|
1676
|
+
"window.dispatchEvent(new MessageEvent('message', { data: message, origin: window.location.origin, source: window.parent || window }));",
|
|
1677
|
+
"} catch (_) {}",
|
|
1678
|
+
"true;",
|
|
1679
|
+
"})();"
|
|
1680
|
+
].join("\n");
|
|
1681
|
+
}
|
|
1682
|
+
function buildFilesPickedScript(files, batchId) {
|
|
1683
|
+
return buildDispatchScript(`{ type: "sagepilot:files_picked", batch_id: ${JSON.stringify(batchId)}, files: ${JSON.stringify(files)} }`);
|
|
1684
|
+
}
|
|
1685
|
+
function buildFilePickerErrorScript(message, code) {
|
|
1686
|
+
return buildDispatchScript(
|
|
1687
|
+
`{ type: "sagepilot:file_picker_error", message: ${JSON.stringify(message)}${code ? `, code: ${JSON.stringify(code)}` : ""} }`
|
|
1688
|
+
);
|
|
1689
|
+
}
|
|
1690
|
+
function buildFilePickerCancelledScript() {
|
|
1691
|
+
return buildDispatchScript(`{ type: "sagepilot:file_picker_cancelled" }`);
|
|
1692
|
+
}
|
|
1693
|
+
function readFilePickerError(error) {
|
|
1694
|
+
if (error && typeof error === "object") {
|
|
1695
|
+
const candidate = error;
|
|
1696
|
+
const message = typeof candidate.message === "string" && candidate.message ? candidate.message : "Could not attach the selected file.";
|
|
1697
|
+
const code = typeof candidate.code === "string" ? candidate.code : void 0;
|
|
1698
|
+
return { message, code };
|
|
1699
|
+
}
|
|
1700
|
+
return { message: "Could not attach the selected file." };
|
|
1701
|
+
}
|
|
1240
1702
|
function getHostedIdentityDispatchScript() {
|
|
1241
1703
|
const message = internalSagepilotChat.getHostedIdentityMessage();
|
|
1242
1704
|
if (!message) return "";
|
|
@@ -1259,19 +1721,28 @@ function readUrlOrigin(url) {
|
|
|
1259
1721
|
return null;
|
|
1260
1722
|
}
|
|
1261
1723
|
}
|
|
1724
|
+
function isInternalWebViewScheme(url) {
|
|
1725
|
+
return url.startsWith("about:") || url.startsWith("data:") || url.startsWith("blob:") || url.startsWith("javascript:") || url.startsWith("file:");
|
|
1726
|
+
}
|
|
1262
1727
|
var hostedChatWebViewProps = {
|
|
1263
1728
|
allowFileAccess: true,
|
|
1264
1729
|
allowFileAccessFromFileURLs: true,
|
|
1730
|
+
...import_react_native2.Platform.OS === "ios" ? {
|
|
1731
|
+
automaticallyAdjustContentInsets: false,
|
|
1732
|
+
contentInsetAdjustmentBehavior: "never",
|
|
1733
|
+
hideKeyboardAccessoryView: true
|
|
1734
|
+
} : null,
|
|
1265
1735
|
bounces: false,
|
|
1266
1736
|
domStorageEnabled: true,
|
|
1267
1737
|
javaScriptEnabled: true,
|
|
1268
1738
|
overScrollMode: "never",
|
|
1269
|
-
|
|
1739
|
+
// The hosted widget owns feed scrolling internally; outer WebView scrolling lets iOS focus-scroll the page over the keyboard.
|
|
1740
|
+
scrollEnabled: false,
|
|
1270
1741
|
setSupportMultipleWindows: false,
|
|
1271
1742
|
sharedCookiesEnabled: true,
|
|
1272
1743
|
thirdPartyCookiesEnabled: true
|
|
1273
1744
|
};
|
|
1274
|
-
var AndroidInsetsView =
|
|
1745
|
+
var AndroidInsetsView = import_react_native2.Platform.OS === "android" ? SagepilotInsetsViewNativeComponent_default : import_react_native2.View;
|
|
1275
1746
|
var emptyAndroidInsets = { top: 0, bottom: 0 };
|
|
1276
1747
|
function SagepilotChatProvider({
|
|
1277
1748
|
children,
|
|
@@ -1280,13 +1751,292 @@ function SagepilotChatProvider({
|
|
|
1280
1751
|
}) {
|
|
1281
1752
|
const [state, setState] = (0, import_react.useState)(readPresentationState);
|
|
1282
1753
|
const [androidModalInsets, setAndroidModalInsets] = (0, import_react.useState)(emptyAndroidInsets);
|
|
1754
|
+
const [nativeWebViewKey, setNativeWebViewKey] = (0, import_react.useState)(0);
|
|
1755
|
+
const [preloadWebViewKey, setPreloadWebViewKey] = (0, import_react.useState)(0);
|
|
1283
1756
|
const webFrameRef = (0, import_react.useRef)(null);
|
|
1284
1757
|
const nativeWebViewRef = (0, import_react.useRef)(null);
|
|
1758
|
+
const pendingBatchesRef = (0, import_react.useRef)([]);
|
|
1759
|
+
const widgetReadyRef = (0, import_react.useRef)(false);
|
|
1760
|
+
const deliveryTimerRef = (0, import_react.useRef)(null);
|
|
1761
|
+
const deliveryAttemptsRef = (0, import_react.useRef)(0);
|
|
1762
|
+
const pendingPingRef = (0, import_react.useRef)(null);
|
|
1763
|
+
const pingTimeoutRef = (0, import_react.useRef)(null);
|
|
1764
|
+
const livenessRemountCountRef = (0, import_react.useRef)(0);
|
|
1765
|
+
const appStateRef = (0, import_react.useRef)(import_react_native2.AppState.currentState);
|
|
1766
|
+
const didReconcileRef = (0, import_react.useRef)(false);
|
|
1767
|
+
const [androidRepaintTick, setAndroidRepaintTick] = (0, import_react.useState)(0);
|
|
1768
|
+
const [sourceChooser, setSourceChooser] = (0, import_react.useState)(null);
|
|
1769
|
+
const getPendingCache = (0, import_react.useCallback)(() => {
|
|
1770
|
+
const cacheStorage = internalSagepilotChat.getConfig()?.cacheStorage;
|
|
1771
|
+
if (!cacheStorage) return null;
|
|
1772
|
+
return createJsonCache(cacheStorage, PENDING_FILES_CACHE_NAMESPACE);
|
|
1773
|
+
}, []);
|
|
1774
|
+
const writeManifest = (0, import_react.useCallback)(() => {
|
|
1775
|
+
const cache = getPendingCache();
|
|
1776
|
+
if (!cache) return;
|
|
1777
|
+
const queue = pendingBatchesRef.current;
|
|
1778
|
+
if (queue.length === 0) {
|
|
1779
|
+
void cache.remove(PENDING_FILES_CACHE_KEY).catch(() => void 0);
|
|
1780
|
+
return;
|
|
1781
|
+
}
|
|
1782
|
+
const hasFileStore = Boolean(internalSagepilotChat.getConfig()?.fileStore);
|
|
1783
|
+
if (hasFileStore) {
|
|
1784
|
+
const manifest = queue.filter((batch) => batch.storageKeys && batch.storageKeys.length === batch.files.length).map((batch) => ({
|
|
1785
|
+
batchId: batch.batchId,
|
|
1786
|
+
files: batch.files.map((file, index) => ({
|
|
1787
|
+
file_name: file.file_name,
|
|
1788
|
+
mime_type: file.mime_type,
|
|
1789
|
+
size: file.size,
|
|
1790
|
+
storageKey: batch.storageKeys[index]
|
|
1791
|
+
}))
|
|
1792
|
+
}));
|
|
1793
|
+
if (manifest.length === 0) {
|
|
1794
|
+
void cache.remove(PENDING_FILES_CACHE_KEY).catch(() => void 0);
|
|
1795
|
+
} else {
|
|
1796
|
+
void cache.set(PENDING_FILES_CACHE_KEY, manifest).catch(() => void 0);
|
|
1797
|
+
}
|
|
1798
|
+
return;
|
|
1799
|
+
}
|
|
1800
|
+
const totalBytes = queue.reduce((sum, batch) => sum + totalBase64Bytes(batch.files), 0);
|
|
1801
|
+
if (totalBytes <= PERSIST_MAX_TOTAL_BYTES) {
|
|
1802
|
+
const manifest = queue.map((batch) => ({
|
|
1803
|
+
batchId: batch.batchId,
|
|
1804
|
+
files: batch.files.map((file) => ({
|
|
1805
|
+
file_name: file.file_name,
|
|
1806
|
+
mime_type: file.mime_type,
|
|
1807
|
+
size: file.size,
|
|
1808
|
+
data_base64: file.data_base64
|
|
1809
|
+
}))
|
|
1810
|
+
}));
|
|
1811
|
+
void cache.set(PENDING_FILES_CACHE_KEY, manifest).catch(() => void 0);
|
|
1812
|
+
} else {
|
|
1813
|
+
void cache.remove(PENDING_FILES_CACHE_KEY).catch(() => void 0);
|
|
1814
|
+
}
|
|
1815
|
+
}, [getPendingCache]);
|
|
1816
|
+
const writeBatchBytes = (0, import_react.useCallback)(async (batch) => {
|
|
1817
|
+
const fileStore = internalSagepilotChat.getConfig()?.fileStore;
|
|
1818
|
+
if (!fileStore) return;
|
|
1819
|
+
const entries = batch.files.map((file, index) => ({
|
|
1820
|
+
key: storageKeyFor(batch.batchId, index),
|
|
1821
|
+
base64: file.data_base64
|
|
1822
|
+
}));
|
|
1823
|
+
try {
|
|
1824
|
+
await Promise.all(entries.map((entry) => fileStore.write(entry.key, entry.base64)));
|
|
1825
|
+
} catch {
|
|
1826
|
+
return;
|
|
1827
|
+
}
|
|
1828
|
+
if (!pendingBatchesRef.current.some((queued) => queued.batchId === batch.batchId)) {
|
|
1829
|
+
void Promise.all(entries.map((entry) => fileStore.remove(entry.key).catch(() => void 0)));
|
|
1830
|
+
}
|
|
1831
|
+
}, []);
|
|
1832
|
+
const discardBatchFiles = (0, import_react.useCallback)((batch) => {
|
|
1833
|
+
if (!batch?.storageKeys) return;
|
|
1834
|
+
const fileStore = internalSagepilotChat.getConfig()?.fileStore;
|
|
1835
|
+
if (!fileStore) return;
|
|
1836
|
+
void Promise.all(batch.storageKeys.map((key) => fileStore.remove(key).catch(() => void 0)));
|
|
1837
|
+
}, []);
|
|
1838
|
+
const pumpDelivery = (0, import_react.useCallback)(() => {
|
|
1839
|
+
if (deliveryTimerRef.current) {
|
|
1840
|
+
clearTimeout(deliveryTimerRef.current);
|
|
1841
|
+
deliveryTimerRef.current = null;
|
|
1842
|
+
}
|
|
1843
|
+
const batch = pendingBatchesRef.current[0];
|
|
1844
|
+
if (!batch) return;
|
|
1845
|
+
deliveryAttemptsRef.current += 1;
|
|
1846
|
+
const attempts = deliveryAttemptsRef.current;
|
|
1847
|
+
const ref = nativeWebViewRef.current;
|
|
1848
|
+
const shouldDeliver = ref && (widgetReadyRef.current || attempts >= DELIVERY_LEGACY_FALLBACK_ATTEMPTS);
|
|
1849
|
+
if (shouldDeliver && ref) {
|
|
1850
|
+
ref.injectJavaScript(buildFilesPickedScript(batch.files, batch.batchId));
|
|
1851
|
+
}
|
|
1852
|
+
if (attempts < DELIVERY_MAX_ATTEMPTS) {
|
|
1853
|
+
deliveryTimerRef.current = setTimeout(() => pumpDelivery(), DELIVERY_RETRY_MS);
|
|
1854
|
+
}
|
|
1855
|
+
}, []);
|
|
1856
|
+
const startDelivery = (0, import_react.useCallback)(() => {
|
|
1857
|
+
deliveryAttemptsRef.current = 0;
|
|
1858
|
+
pumpDelivery();
|
|
1859
|
+
}, [pumpDelivery]);
|
|
1860
|
+
const ensureDelivery = (0, import_react.useCallback)(() => {
|
|
1861
|
+
if (pendingBatchesRef.current.length === 0) return;
|
|
1862
|
+
if (deliveryTimerRef.current) return;
|
|
1863
|
+
startDelivery();
|
|
1864
|
+
}, [startDelivery]);
|
|
1865
|
+
const acknowledgeHeadBatch = (0, import_react.useCallback)(() => {
|
|
1866
|
+
const head = pendingBatchesRef.current[0];
|
|
1867
|
+
pendingBatchesRef.current = pendingBatchesRef.current.slice(1);
|
|
1868
|
+
deliveryAttemptsRef.current = 0;
|
|
1869
|
+
if (deliveryTimerRef.current) {
|
|
1870
|
+
clearTimeout(deliveryTimerRef.current);
|
|
1871
|
+
deliveryTimerRef.current = null;
|
|
1872
|
+
}
|
|
1873
|
+
discardBatchFiles(head);
|
|
1874
|
+
writeManifest();
|
|
1875
|
+
if (pendingBatchesRef.current.length > 0) startDelivery();
|
|
1876
|
+
}, [discardBatchFiles, writeManifest, startDelivery]);
|
|
1877
|
+
const queuePickedFiles = (0, import_react.useCallback)((files) => {
|
|
1878
|
+
const batchId = nextBatchId();
|
|
1879
|
+
const hasFileStore = Boolean(internalSagepilotChat.getConfig()?.fileStore);
|
|
1880
|
+
const storageKeys = hasFileStore ? files.map((_, index) => storageKeyFor(batchId, index)) : void 0;
|
|
1881
|
+
const batch = { batchId, files, storageKeys };
|
|
1882
|
+
pendingBatchesRef.current = [...pendingBatchesRef.current, batch];
|
|
1883
|
+
ensureDelivery();
|
|
1884
|
+
writeManifest();
|
|
1885
|
+
void writeBatchBytes(batch);
|
|
1886
|
+
}, [ensureDelivery, writeManifest, writeBatchBytes]);
|
|
1887
|
+
const recoverNativeWebView = (0, import_react.useCallback)(() => {
|
|
1888
|
+
widgetReadyRef.current = false;
|
|
1889
|
+
setNativeWebViewKey((key) => key + 1);
|
|
1890
|
+
}, []);
|
|
1891
|
+
const recoverPreloadWebView = (0, import_react.useCallback)(() => {
|
|
1892
|
+
setPreloadWebViewKey((key) => key + 1);
|
|
1893
|
+
}, []);
|
|
1894
|
+
const runNativeFilePicker = (0, import_react.useCallback)((source, multiple) => {
|
|
1895
|
+
const filePicker = internalSagepilotChat.getConfig()?.filePicker;
|
|
1896
|
+
if (!filePicker) return;
|
|
1897
|
+
filePicker.pickFiles({ source, multiple }).then((files) => {
|
|
1898
|
+
if (files.length === 0) {
|
|
1899
|
+
nativeWebViewRef.current?.injectJavaScript(buildFilePickerCancelledScript());
|
|
1900
|
+
return;
|
|
1901
|
+
}
|
|
1902
|
+
queuePickedFiles(files);
|
|
1903
|
+
}).catch((error) => {
|
|
1904
|
+
const { message, code } = readFilePickerError(error);
|
|
1905
|
+
nativeWebViewRef.current?.injectJavaScript(buildFilePickerErrorScript(message, code));
|
|
1906
|
+
});
|
|
1907
|
+
}, [queuePickedFiles]);
|
|
1908
|
+
const openNativeFilePicker = (0, import_react.useCallback)((multiple) => {
|
|
1909
|
+
const filePicker = internalSagepilotChat.getConfig()?.filePicker;
|
|
1910
|
+
if (!filePicker || filePicker.sources.length === 0) return;
|
|
1911
|
+
const [onlySource] = filePicker.sources;
|
|
1912
|
+
if (filePicker.sources.length === 1 && onlySource) {
|
|
1913
|
+
runNativeFilePicker(onlySource, multiple);
|
|
1914
|
+
return;
|
|
1915
|
+
}
|
|
1916
|
+
setSourceChooser({ multiple });
|
|
1917
|
+
}, [runNativeFilePicker]);
|
|
1918
|
+
const handleSourceChoice = (0, import_react.useCallback)((source) => {
|
|
1919
|
+
const chooser = sourceChooser;
|
|
1920
|
+
setSourceChooser(null);
|
|
1921
|
+
if (chooser) runNativeFilePicker(source, chooser.multiple);
|
|
1922
|
+
}, [sourceChooser, runNativeFilePicker]);
|
|
1285
1923
|
(0, import_react.useEffect)(() => {
|
|
1286
1924
|
return internalSagepilotChat.onStateChange(() => {
|
|
1287
1925
|
setState(readPresentationState());
|
|
1288
1926
|
});
|
|
1289
1927
|
}, []);
|
|
1928
|
+
(0, import_react.useEffect)(() => {
|
|
1929
|
+
let cancelled = false;
|
|
1930
|
+
const cache = getPendingCache();
|
|
1931
|
+
if (!cache) return;
|
|
1932
|
+
const reconcile = async () => {
|
|
1933
|
+
if (didReconcileRef.current) return;
|
|
1934
|
+
const manifest = await cache.get(PENDING_FILES_CACHE_KEY).catch(() => null);
|
|
1935
|
+
if (cancelled || didReconcileRef.current || !Array.isArray(manifest) || manifest.length === 0) return;
|
|
1936
|
+
const fileStore = internalSagepilotChat.getConfig()?.fileStore;
|
|
1937
|
+
const restored = [];
|
|
1938
|
+
for (const batch of manifest) {
|
|
1939
|
+
if (!batch || typeof batch.batchId !== "string" || !Array.isArray(batch.files) || batch.files.length === 0) continue;
|
|
1940
|
+
const files = [];
|
|
1941
|
+
const storageKeys = [];
|
|
1942
|
+
let intact = true;
|
|
1943
|
+
for (const file of batch.files) {
|
|
1944
|
+
let dataBase64 = null;
|
|
1945
|
+
if (typeof file.storageKey === "string" && fileStore) {
|
|
1946
|
+
try {
|
|
1947
|
+
dataBase64 = await fileStore.read(file.storageKey);
|
|
1948
|
+
storageKeys.push(file.storageKey);
|
|
1949
|
+
} catch {
|
|
1950
|
+
intact = false;
|
|
1951
|
+
}
|
|
1952
|
+
} else if (typeof file.data_base64 === "string" && file.data_base64) {
|
|
1953
|
+
dataBase64 = file.data_base64;
|
|
1954
|
+
}
|
|
1955
|
+
if (!dataBase64) {
|
|
1956
|
+
intact = false;
|
|
1957
|
+
break;
|
|
1958
|
+
}
|
|
1959
|
+
files.push({ file_name: file.file_name, mime_type: file.mime_type, size: file.size, data_base64: dataBase64 });
|
|
1960
|
+
}
|
|
1961
|
+
if (intact && files.length === batch.files.length) {
|
|
1962
|
+
restored.push({
|
|
1963
|
+
batchId: batch.batchId,
|
|
1964
|
+
files,
|
|
1965
|
+
storageKeys: storageKeys.length === files.length ? storageKeys : void 0
|
|
1966
|
+
});
|
|
1967
|
+
}
|
|
1968
|
+
}
|
|
1969
|
+
if (cancelled || didReconcileRef.current) return;
|
|
1970
|
+
didReconcileRef.current = true;
|
|
1971
|
+
pendingBatchesRef.current = [...restored, ...pendingBatchesRef.current];
|
|
1972
|
+
writeManifest();
|
|
1973
|
+
if (fileStore) {
|
|
1974
|
+
const keep = pendingBatchesRef.current.flatMap(
|
|
1975
|
+
(batch) => batch.files.map((_, index) => storageKeyFor(batch.batchId, index))
|
|
1976
|
+
);
|
|
1977
|
+
void fileStore.prune(keep).catch(() => void 0);
|
|
1978
|
+
}
|
|
1979
|
+
startDelivery();
|
|
1980
|
+
};
|
|
1981
|
+
void reconcile();
|
|
1982
|
+
return () => {
|
|
1983
|
+
cancelled = true;
|
|
1984
|
+
};
|
|
1985
|
+
}, [getPendingCache, startDelivery, writeManifest]);
|
|
1986
|
+
(0, import_react.useEffect)(() => {
|
|
1987
|
+
return () => {
|
|
1988
|
+
if (deliveryTimerRef.current) {
|
|
1989
|
+
clearTimeout(deliveryTimerRef.current);
|
|
1990
|
+
deliveryTimerRef.current = null;
|
|
1991
|
+
}
|
|
1992
|
+
if (pingTimeoutRef.current) {
|
|
1993
|
+
clearTimeout(pingTimeoutRef.current);
|
|
1994
|
+
pingTimeoutRef.current = null;
|
|
1995
|
+
}
|
|
1996
|
+
};
|
|
1997
|
+
}, []);
|
|
1998
|
+
const clearPing = (0, import_react.useCallback)(() => {
|
|
1999
|
+
pendingPingRef.current = null;
|
|
2000
|
+
if (pingTimeoutRef.current) {
|
|
2001
|
+
clearTimeout(pingTimeoutRef.current);
|
|
2002
|
+
pingTimeoutRef.current = null;
|
|
2003
|
+
}
|
|
2004
|
+
}, []);
|
|
2005
|
+
const runLivenessProbe = (0, import_react.useCallback)(() => {
|
|
2006
|
+
if (import_react_native2.Platform.OS !== "android") return;
|
|
2007
|
+
const ref = nativeWebViewRef.current;
|
|
2008
|
+
if (!ref || !internalSagepilotChat.isPresented()) return;
|
|
2009
|
+
setAndroidRepaintTick((tick) => tick + 1);
|
|
2010
|
+
const attempts = (pendingPingRef.current?.attempts ?? 0) + 1;
|
|
2011
|
+
const nonce = `${Date.now().toString(36)}_${attempts}`;
|
|
2012
|
+
pendingPingRef.current = { nonce, attempts };
|
|
2013
|
+
ref.injectJavaScript(buildLivenessPingScript(nonce, LIVENESS_HEARTBEAT_STALE_MS));
|
|
2014
|
+
if (pingTimeoutRef.current) clearTimeout(pingTimeoutRef.current);
|
|
2015
|
+
pingTimeoutRef.current = setTimeout(() => {
|
|
2016
|
+
if (attempts >= LIVENESS_MAX_PING_ATTEMPTS) {
|
|
2017
|
+
clearPing();
|
|
2018
|
+
recoverNativeWebView();
|
|
2019
|
+
return;
|
|
2020
|
+
}
|
|
2021
|
+
runLivenessProbe();
|
|
2022
|
+
}, LIVENESS_PONG_TIMEOUT_MS);
|
|
2023
|
+
}, [clearPing, recoverNativeWebView]);
|
|
2024
|
+
(0, import_react.useEffect)(() => {
|
|
2025
|
+
const subscription = import_react_native2.AppState.addEventListener("change", (nextState) => {
|
|
2026
|
+
const prev = appStateRef.current;
|
|
2027
|
+
appStateRef.current = nextState;
|
|
2028
|
+
if (nextState === "active" && (prev === "background" || prev === "inactive")) {
|
|
2029
|
+
clearPing();
|
|
2030
|
+
livenessRemountCountRef.current = 0;
|
|
2031
|
+
runLivenessProbe();
|
|
2032
|
+
ensureDelivery();
|
|
2033
|
+
}
|
|
2034
|
+
});
|
|
2035
|
+
return () => {
|
|
2036
|
+
subscription.remove();
|
|
2037
|
+
clearPing();
|
|
2038
|
+
};
|
|
2039
|
+
}, [clearPing, runLivenessProbe, ensureDelivery]);
|
|
1290
2040
|
const handleAndroidInsetsChange = (0, import_react.useCallback)((event) => {
|
|
1291
2041
|
const nextBottomInset = event.nativeEvent?.bottom;
|
|
1292
2042
|
const nextTopInset = event.nativeEvent?.top;
|
|
@@ -1300,11 +2050,11 @@ function SagepilotChatProvider({
|
|
|
1300
2050
|
const presentationStyle = state.presentation?.style ?? "sheet";
|
|
1301
2051
|
const isFullScreenModal = presentationStyle === "fullScreen";
|
|
1302
2052
|
const animationType = presentationStyle === "fullScreen" || presentationStyle === "push" ? "slide" : "fade";
|
|
1303
|
-
const ModalContainer =
|
|
1304
|
-
const NativeModalContainer =
|
|
1305
|
-
const ChatContentContainer =
|
|
1306
|
-
const nativeModalContainerProps =
|
|
1307
|
-
const chatContentContainerProps =
|
|
2053
|
+
const ModalContainer = import_react_native2.Platform.OS === "ios" && isFullScreenModal ? import_react_native2.SafeAreaView : import_react_native2.View;
|
|
2054
|
+
const NativeModalContainer = import_react_native2.Platform.OS === "android" ? AndroidInsetsView : ModalContainer;
|
|
2055
|
+
const ChatContentContainer = import_react_native2.Platform.OS === "ios" ? import_react_native2.KeyboardAvoidingView : import_react_native2.View;
|
|
2056
|
+
const nativeModalContainerProps = import_react_native2.Platform.OS === "android" ? { style: styles.container, onInsetsChange: handleAndroidInsetsChange } : { style: styles.container };
|
|
2057
|
+
const chatContentContainerProps = import_react_native2.Platform.OS === "ios" ? {
|
|
1308
2058
|
behavior: "padding",
|
|
1309
2059
|
enabled: true,
|
|
1310
2060
|
keyboardVerticalOffset: 0,
|
|
@@ -1312,14 +2062,48 @@ function SagepilotChatProvider({
|
|
|
1312
2062
|
} : {
|
|
1313
2063
|
style: [
|
|
1314
2064
|
styles.modalContent,
|
|
1315
|
-
|
|
2065
|
+
import_react_native2.Platform.OS === "android" ? {
|
|
1316
2066
|
paddingTop: androidModalInsets.top,
|
|
1317
2067
|
paddingBottom: androidModalInsets.bottom
|
|
1318
2068
|
} : null
|
|
1319
2069
|
].filter(Boolean)
|
|
1320
2070
|
};
|
|
1321
2071
|
const handleWebViewMessage = (event) => {
|
|
1322
|
-
|
|
2072
|
+
const message = parseHostedBridgeMessage(event.nativeEvent?.data);
|
|
2073
|
+
if (message?.type === "sagepilot:open_file_picker") {
|
|
2074
|
+
openNativeFilePicker(message.multiple ?? true);
|
|
2075
|
+
return;
|
|
2076
|
+
}
|
|
2077
|
+
if (message?.type === "sagepilot:widget_listener_ready") {
|
|
2078
|
+
widgetReadyRef.current = true;
|
|
2079
|
+
startDelivery();
|
|
2080
|
+
return;
|
|
2081
|
+
}
|
|
2082
|
+
if (message?.type === "sagepilot:files_received") {
|
|
2083
|
+
const ackBatchId = message.batch_id;
|
|
2084
|
+
const head = pendingBatchesRef.current[0];
|
|
2085
|
+
if (head && (!ackBatchId || ackBatchId === head.batchId)) {
|
|
2086
|
+
acknowledgeHeadBatch();
|
|
2087
|
+
}
|
|
2088
|
+
return;
|
|
2089
|
+
}
|
|
2090
|
+
if (message?.type === "sagepilot:pong") {
|
|
2091
|
+
const pending = pendingPingRef.current;
|
|
2092
|
+
if (pending && (!message.nonce || message.nonce === pending.nonce)) {
|
|
2093
|
+
if (message.alive === false) {
|
|
2094
|
+
clearPing();
|
|
2095
|
+
if (livenessRemountCountRef.current < LIVENESS_MAX_REMOUNTS) {
|
|
2096
|
+
livenessRemountCountRef.current += 1;
|
|
2097
|
+
recoverNativeWebView();
|
|
2098
|
+
}
|
|
2099
|
+
} else {
|
|
2100
|
+
livenessRemountCountRef.current = 0;
|
|
2101
|
+
clearPing();
|
|
2102
|
+
}
|
|
2103
|
+
}
|
|
2104
|
+
return;
|
|
2105
|
+
}
|
|
2106
|
+
internalSagepilotChat.handleHostedBridgeMessage(message);
|
|
1323
2107
|
};
|
|
1324
2108
|
const postIdentityToWebFrame = () => {
|
|
1325
2109
|
const message = internalSagepilotChat.getHostedIdentityMessage();
|
|
@@ -1332,8 +2116,25 @@ function SagepilotChatProvider({
|
|
|
1332
2116
|
if (!script || !nativeWebViewRef.current) return;
|
|
1333
2117
|
nativeWebViewRef.current.injectJavaScript(script);
|
|
1334
2118
|
};
|
|
2119
|
+
const handleNativeWebViewLoadEnd = () => {
|
|
2120
|
+
postIdentityToNativeWebView();
|
|
2121
|
+
widgetReadyRef.current = false;
|
|
2122
|
+
startDelivery();
|
|
2123
|
+
};
|
|
2124
|
+
const handleShouldStartLoadWithRequest = (0, import_react.useCallback)((request) => {
|
|
2125
|
+
const url = request?.url ?? "";
|
|
2126
|
+
if (!url || request?.isTopFrame === false || isInternalWebViewScheme(url)) {
|
|
2127
|
+
return true;
|
|
2128
|
+
}
|
|
2129
|
+
const widgetOrigin = readUrlOrigin(state.conversationUrl ?? state.preloadUrl);
|
|
2130
|
+
if (widgetOrigin && readUrlOrigin(url) === widgetOrigin) {
|
|
2131
|
+
return true;
|
|
2132
|
+
}
|
|
2133
|
+
import_react_native2.Linking.openURL(url).catch(() => void 0);
|
|
2134
|
+
return false;
|
|
2135
|
+
}, [state.conversationUrl, state.preloadUrl]);
|
|
1335
2136
|
(0, import_react.useEffect)(() => {
|
|
1336
|
-
if (
|
|
2137
|
+
if (import_react_native2.Platform.OS !== "web" || typeof window === "undefined") return;
|
|
1337
2138
|
const trustedWidgetOrigin = readUrlOrigin(state.conversationUrl);
|
|
1338
2139
|
if (!trustedWidgetOrigin) return;
|
|
1339
2140
|
const handleWindowMessage = (event) => {
|
|
@@ -1344,16 +2145,16 @@ function SagepilotChatProvider({
|
|
|
1344
2145
|
return () => window.removeEventListener("message", handleWindowMessage);
|
|
1345
2146
|
}, [state.conversationUrl]);
|
|
1346
2147
|
(0, import_react.useEffect)(() => {
|
|
1347
|
-
if (
|
|
2148
|
+
if (import_react_native2.Platform.OS !== "web" || !state.isPresented) return;
|
|
1348
2149
|
postIdentityToWebFrame();
|
|
1349
2150
|
}, [state.isPresented, state.conversationUrl, state.configured]);
|
|
1350
2151
|
(0, import_react.useEffect)(() => {
|
|
1351
|
-
if (
|
|
2152
|
+
if (import_react_native2.Platform.OS === "web" || !state.isPresented) return;
|
|
1352
2153
|
postIdentityToNativeWebView();
|
|
1353
2154
|
}, [state.isPresented, state.conversationUrl, state.configured]);
|
|
1354
|
-
if (
|
|
2155
|
+
if (import_react_native2.Platform.OS === "web") {
|
|
1355
2156
|
return (0, import_react.createElement)(
|
|
1356
|
-
|
|
2157
|
+
import_react_native2.View,
|
|
1357
2158
|
{ style: styles.root },
|
|
1358
2159
|
children,
|
|
1359
2160
|
state.configured && !state.isPresented && state.shouldPreload && state.preloadUrl ? (0, import_react.createElement)("iframe", {
|
|
@@ -1362,20 +2163,20 @@ function SagepilotChatProvider({
|
|
|
1362
2163
|
title: "Sagepilot chat preload"
|
|
1363
2164
|
}) : null,
|
|
1364
2165
|
state.configured && state.isPresented && state.conversationUrl ? (0, import_react.createElement)(
|
|
1365
|
-
|
|
2166
|
+
import_react_native2.View,
|
|
1366
2167
|
{ style: styles.webOverlay },
|
|
1367
2168
|
(0, import_react.createElement)(
|
|
1368
|
-
|
|
2169
|
+
import_react_native2.View,
|
|
1369
2170
|
{ style: styles.webPanel },
|
|
1370
2171
|
showCloseButton ? (0, import_react.createElement)(
|
|
1371
|
-
|
|
2172
|
+
import_react_native2.Pressable,
|
|
1372
2173
|
{
|
|
1373
2174
|
accessibilityRole: "button",
|
|
1374
2175
|
accessibilityLabel: closeLabel,
|
|
1375
2176
|
onPress: () => internalSagepilotChat.dismiss(),
|
|
1376
2177
|
style: styles.webCloseButton
|
|
1377
2178
|
},
|
|
1378
|
-
(0, import_react.createElement)(
|
|
2179
|
+
(0, import_react.createElement)(import_react_native2.Text, { style: styles.closeText }, closeLabel)
|
|
1379
2180
|
) : null,
|
|
1380
2181
|
(0, import_react.createElement)("iframe", {
|
|
1381
2182
|
ref: webFrameRef,
|
|
@@ -1389,24 +2190,32 @@ function SagepilotChatProvider({
|
|
|
1389
2190
|
);
|
|
1390
2191
|
}
|
|
1391
2192
|
return (0, import_react.createElement)(
|
|
1392
|
-
|
|
2193
|
+
import_react_native2.View,
|
|
1393
2194
|
{ style: styles.root },
|
|
1394
2195
|
children,
|
|
1395
2196
|
state.configured && !state.isPresented && state.shouldPreload && state.preloadUrl ? (0, import_react.createElement)(import_react_native_webview.WebView, {
|
|
1396
2197
|
...hostedChatWebViewProps,
|
|
2198
|
+
// Separate key from the hosted WebView: a hidden-preload renderer crash
|
|
2199
|
+
// must not bump the hosted key (which would reset widgetReadyRef and
|
|
2200
|
+
// disrupt an in-flight delivery). onRenderProcessGone must still be
|
|
2201
|
+
// handled here or an unhandled renderer kill crashes the whole app.
|
|
2202
|
+
key: `sagepilot-preload-webview-${preloadWebViewKey}`,
|
|
1397
2203
|
source: { uri: state.preloadUrl },
|
|
1398
2204
|
style: styles.preloadWebview,
|
|
1399
2205
|
injectedJavaScriptBeforeContentLoaded: getInjectedWebViewScript(),
|
|
1400
|
-
onMessage: handleWebViewMessage
|
|
2206
|
+
onMessage: handleWebViewMessage,
|
|
2207
|
+
onShouldStartLoadWithRequest: handleShouldStartLoadWithRequest,
|
|
2208
|
+
onRenderProcessGone: recoverPreloadWebView,
|
|
2209
|
+
onContentProcessDidTerminate: recoverPreloadWebView
|
|
1401
2210
|
}) : null,
|
|
1402
2211
|
(0, import_react.createElement)(
|
|
1403
|
-
|
|
2212
|
+
import_react_native2.Modal,
|
|
1404
2213
|
{
|
|
1405
2214
|
visible: state.configured && state.isPresented,
|
|
1406
2215
|
animationType,
|
|
1407
2216
|
presentationStyle: isFullScreenModal ? "fullScreen" : "pageSheet",
|
|
1408
|
-
statusBarTranslucent:
|
|
1409
|
-
navigationBarTranslucent:
|
|
2217
|
+
statusBarTranslucent: import_react_native2.Platform.OS === "android",
|
|
2218
|
+
navigationBarTranslucent: import_react_native2.Platform.OS === "android",
|
|
1410
2219
|
onRequestClose: () => internalSagepilotChat.dismiss()
|
|
1411
2220
|
},
|
|
1412
2221
|
(0, import_react.createElement)(
|
|
@@ -1416,41 +2225,93 @@ function SagepilotChatProvider({
|
|
|
1416
2225
|
ChatContentContainer,
|
|
1417
2226
|
chatContentContainerProps,
|
|
1418
2227
|
showCloseButton ? (0, import_react.createElement)(
|
|
1419
|
-
|
|
2228
|
+
import_react_native2.View,
|
|
1420
2229
|
{ style: styles.header },
|
|
1421
2230
|
(0, import_react.createElement)(
|
|
1422
|
-
|
|
2231
|
+
import_react_native2.Pressable,
|
|
1423
2232
|
{
|
|
1424
2233
|
accessibilityRole: "button",
|
|
1425
2234
|
accessibilityLabel: closeLabel,
|
|
1426
2235
|
onPress: () => internalSagepilotChat.dismiss(),
|
|
1427
2236
|
style: styles.closeButton
|
|
1428
2237
|
},
|
|
1429
|
-
(0, import_react.createElement)(
|
|
2238
|
+
(0, import_react.createElement)(import_react_native2.Text, { style: styles.closeText }, closeLabel)
|
|
1430
2239
|
)
|
|
1431
2240
|
) : null,
|
|
1432
2241
|
state.conversationUrl ? (0, import_react.createElement)(import_react_native_webview.WebView, {
|
|
1433
2242
|
...hostedChatWebViewProps,
|
|
2243
|
+
key: `sagepilot-hosted-webview-${nativeWebViewKey}`,
|
|
1434
2244
|
ref: nativeWebViewRef,
|
|
1435
2245
|
source: { uri: state.conversationUrl },
|
|
1436
|
-
|
|
2246
|
+
// The imperceptible opacity toggle forces the Android WebView
|
|
2247
|
+
// surface to re-composite on resume, clearing the blank-but-alive
|
|
2248
|
+
// surface bug without a reload (see runLivenessProbe).
|
|
2249
|
+
style: import_react_native2.Platform.OS === "android" ? [styles.webview, { opacity: 1 - androidRepaintTick % 2 * 1e-3 }] : styles.webview,
|
|
1437
2250
|
startInLoadingState: true,
|
|
1438
2251
|
injectedJavaScriptBeforeContentLoaded: getInjectedWebViewScript(),
|
|
1439
2252
|
onMessage: handleWebViewMessage,
|
|
1440
|
-
onLoadEnd:
|
|
2253
|
+
onLoadEnd: handleNativeWebViewLoadEnd,
|
|
2254
|
+
onShouldStartLoadWithRequest: handleShouldStartLoadWithRequest,
|
|
2255
|
+
// Android: render process killed (commonly while the camera/file
|
|
2256
|
+
// chooser activity is foregrounded). Remount to recover.
|
|
2257
|
+
onRenderProcessGone: recoverNativeWebView,
|
|
2258
|
+
// iOS equivalent: WKWebView content process terminated.
|
|
2259
|
+
onContentProcessDidTerminate: recoverNativeWebView,
|
|
1441
2260
|
renderLoading: () => (0, import_react.createElement)(
|
|
1442
|
-
|
|
2261
|
+
import_react_native2.View,
|
|
1443
2262
|
{ style: styles.loading },
|
|
1444
|
-
(0, import_react.createElement)(
|
|
1445
|
-
(0, import_react.createElement)(
|
|
2263
|
+
(0, import_react.createElement)(import_react_native2.ActivityIndicator, null),
|
|
2264
|
+
(0, import_react.createElement)(import_react_native2.Text, { style: styles.loadingText }, loadingLabel)
|
|
1446
2265
|
)
|
|
1447
2266
|
}) : null
|
|
1448
2267
|
)
|
|
1449
2268
|
)
|
|
2269
|
+
),
|
|
2270
|
+
// Native attachment-source chooser. Replaces the Android Alert (capped at
|
|
2271
|
+
// 3 buttons) so camera/library/documents all show on every platform.
|
|
2272
|
+
(0, import_react.createElement)(
|
|
2273
|
+
import_react_native2.Modal,
|
|
2274
|
+
{
|
|
2275
|
+
visible: sourceChooser !== null,
|
|
2276
|
+
transparent: true,
|
|
2277
|
+
animationType: "fade",
|
|
2278
|
+
statusBarTranslucent: true,
|
|
2279
|
+
onRequestClose: () => setSourceChooser(null)
|
|
2280
|
+
},
|
|
2281
|
+
(0, import_react.createElement)(
|
|
2282
|
+
import_react_native2.Pressable,
|
|
2283
|
+
{ style: styles.sheetBackdrop, onPress: () => setSourceChooser(null) },
|
|
2284
|
+
(0, import_react.createElement)(
|
|
2285
|
+
import_react_native2.View,
|
|
2286
|
+
{ style: styles.sheetCard },
|
|
2287
|
+
(0, import_react.createElement)(import_react_native2.Text, { style: styles.sheetTitle }, "Add attachment"),
|
|
2288
|
+
...(internalSagepilotChat.getConfig()?.filePicker?.sources ?? []).map(
|
|
2289
|
+
(source) => (0, import_react.createElement)(
|
|
2290
|
+
import_react_native2.Pressable,
|
|
2291
|
+
{
|
|
2292
|
+
key: source,
|
|
2293
|
+
accessibilityRole: "button",
|
|
2294
|
+
style: styles.sheetButton,
|
|
2295
|
+
onPress: () => handleSourceChoice(source)
|
|
2296
|
+
},
|
|
2297
|
+
(0, import_react.createElement)(import_react_native2.Text, { style: styles.sheetButtonText }, FILE_PICKER_SOURCE_LABELS[source])
|
|
2298
|
+
)
|
|
2299
|
+
),
|
|
2300
|
+
(0, import_react.createElement)(
|
|
2301
|
+
import_react_native2.Pressable,
|
|
2302
|
+
{
|
|
2303
|
+
accessibilityRole: "button",
|
|
2304
|
+
style: [styles.sheetButton, styles.sheetCancelButton],
|
|
2305
|
+
onPress: () => setSourceChooser(null)
|
|
2306
|
+
},
|
|
2307
|
+
(0, import_react.createElement)(import_react_native2.Text, { style: styles.sheetCancelText }, "Cancel")
|
|
2308
|
+
)
|
|
2309
|
+
)
|
|
2310
|
+
)
|
|
1450
2311
|
)
|
|
1451
2312
|
);
|
|
1452
2313
|
}
|
|
1453
|
-
var styles =
|
|
2314
|
+
var styles = import_react_native2.StyleSheet.create({
|
|
1454
2315
|
root: {
|
|
1455
2316
|
flex: 1
|
|
1456
2317
|
},
|
|
@@ -1536,7 +2397,7 @@ var styles = import_react_native.StyleSheet.create({
|
|
|
1536
2397
|
borderStyle: "none"
|
|
1537
2398
|
},
|
|
1538
2399
|
loading: {
|
|
1539
|
-
...
|
|
2400
|
+
...import_react_native2.StyleSheet.absoluteFillObject,
|
|
1540
2401
|
alignItems: "center",
|
|
1541
2402
|
justifyContent: "center",
|
|
1542
2403
|
backgroundColor: "#ffffff"
|
|
@@ -1545,6 +2406,46 @@ var styles = import_react_native.StyleSheet.create({
|
|
|
1545
2406
|
marginTop: 12,
|
|
1546
2407
|
color: "#4b5563",
|
|
1547
2408
|
fontSize: 14
|
|
2409
|
+
},
|
|
2410
|
+
sheetBackdrop: {
|
|
2411
|
+
flex: 1,
|
|
2412
|
+
justifyContent: "flex-end",
|
|
2413
|
+
backgroundColor: "rgba(17, 24, 39, 0.36)"
|
|
2414
|
+
},
|
|
2415
|
+
sheetCard: {
|
|
2416
|
+
backgroundColor: "#ffffff",
|
|
2417
|
+
borderTopLeftRadius: 16,
|
|
2418
|
+
borderTopRightRadius: 16,
|
|
2419
|
+
paddingTop: 8,
|
|
2420
|
+
paddingBottom: 24,
|
|
2421
|
+
paddingHorizontal: 8
|
|
2422
|
+
},
|
|
2423
|
+
sheetTitle: {
|
|
2424
|
+
textAlign: "center",
|
|
2425
|
+
color: "#6b7280",
|
|
2426
|
+
fontSize: 13,
|
|
2427
|
+
fontWeight: "600",
|
|
2428
|
+
paddingVertical: 10
|
|
2429
|
+
},
|
|
2430
|
+
sheetButton: {
|
|
2431
|
+
minHeight: 52,
|
|
2432
|
+
alignItems: "center",
|
|
2433
|
+
justifyContent: "center",
|
|
2434
|
+
borderRadius: 12
|
|
2435
|
+
},
|
|
2436
|
+
sheetButtonText: {
|
|
2437
|
+
color: "#111827",
|
|
2438
|
+
fontSize: 16,
|
|
2439
|
+
fontWeight: "500"
|
|
2440
|
+
},
|
|
2441
|
+
sheetCancelButton: {
|
|
2442
|
+
marginTop: 6,
|
|
2443
|
+
backgroundColor: "#f3f4f6"
|
|
2444
|
+
},
|
|
2445
|
+
sheetCancelText: {
|
|
2446
|
+
color: "#111827",
|
|
2447
|
+
fontSize: 16,
|
|
2448
|
+
fontWeight: "600"
|
|
1548
2449
|
}
|
|
1549
2450
|
});
|
|
1550
2451
|
|
|
@@ -1580,6 +2481,9 @@ function useSagepilotChat() {
|
|
|
1580
2481
|
}, []);
|
|
1581
2482
|
const logout = (0, import_react2.useCallback)(() => SagepilotChat.logout(), []);
|
|
1582
2483
|
const getUnreadCount = (0, import_react2.useCallback)(() => SagepilotChat.getUnreadCount(), []);
|
|
2484
|
+
const onConversationCreated = (0, import_react2.useCallback)((callback) => {
|
|
2485
|
+
return SagepilotChat.onConversationCreated(callback);
|
|
2486
|
+
}, []);
|
|
1583
2487
|
return {
|
|
1584
2488
|
...state,
|
|
1585
2489
|
present,
|
|
@@ -1590,7 +2494,8 @@ function useSagepilotChat() {
|
|
|
1590
2494
|
toggle,
|
|
1591
2495
|
identify,
|
|
1592
2496
|
logout,
|
|
1593
|
-
getUnreadCount
|
|
2497
|
+
getUnreadCount,
|
|
2498
|
+
onConversationCreated
|
|
1594
2499
|
};
|
|
1595
2500
|
}
|
|
1596
2501
|
// Annotate the CommonJS export names for ESM import in node:
|
|
@@ -1598,7 +2503,10 @@ function useSagepilotChat() {
|
|
|
1598
2503
|
SagepilotChat,
|
|
1599
2504
|
SagepilotChatError,
|
|
1600
2505
|
SagepilotChatProvider,
|
|
2506
|
+
SagepilotFilePickerError,
|
|
1601
2507
|
createAsyncStorageCacheStorage,
|
|
1602
2508
|
createKeychainTokenStorage,
|
|
2509
|
+
createSagepilotFilePicker,
|
|
2510
|
+
createSagepilotFileStore,
|
|
1603
2511
|
useSagepilotChat
|
|
1604
2512
|
});
|