@sagepilot-ai/react-native-sdk 0.2.5 → 0.3.1
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/README.md +144 -33
- package/android/build.gradle +13 -4
- package/android/src/main/java/ai/sagepilot/reactnativesdk/SagepilotReactNativeSdkPackage.java +2 -0
- package/dist/index.d.mts +25 -12
- package/dist/index.d.ts +25 -12
- package/dist/index.js +511 -138
- package/dist/index.mjs +480 -112
- package/package.json +6 -1
package/dist/index.mjs
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
// src/public/SdkClient.ts
|
|
2
|
+
import { Platform } from "react-native";
|
|
3
|
+
|
|
1
4
|
// src/core/errors/SagepilotChatError.ts
|
|
2
5
|
var SagepilotChatError = class extends Error {
|
|
3
6
|
constructor(code, message, options = {}) {
|
|
@@ -15,7 +18,7 @@ var SagepilotChatError = class extends Error {
|
|
|
15
18
|
|
|
16
19
|
// src/core/config/constants.ts
|
|
17
20
|
var SDK_NAME = "@sagepilot-ai/react-native-sdk";
|
|
18
|
-
var SDK_VERSION = "0.
|
|
21
|
+
var SDK_VERSION = "0.3.1";
|
|
19
22
|
var DEFAULT_HOST = "https://app.sagepilot.ai";
|
|
20
23
|
var DEFAULT_WIDGET_HOST = "https://app.sagepilot.ai";
|
|
21
24
|
var CUSTOMER_API_PREFIX = "/customer-api/v1";
|
|
@@ -324,6 +327,34 @@ async function resolveDeviceInfo(adapter) {
|
|
|
324
327
|
return adapter.getDeviceInfo();
|
|
325
328
|
}
|
|
326
329
|
|
|
330
|
+
// src/core/storage/cache.ts
|
|
331
|
+
function createAsyncStorageCacheStorage(asyncStorage) {
|
|
332
|
+
return {
|
|
333
|
+
getItem: (key) => asyncStorage.getItem(key),
|
|
334
|
+
setItem: (key, value) => asyncStorage.setItem(key, value),
|
|
335
|
+
removeItem: (key) => asyncStorage.removeItem(key)
|
|
336
|
+
};
|
|
337
|
+
}
|
|
338
|
+
function createJsonCache(storage, namespace) {
|
|
339
|
+
return {
|
|
340
|
+
async get(key) {
|
|
341
|
+
const value = await storage.getItem(`${namespace}:${key}`);
|
|
342
|
+
if (!value) return null;
|
|
343
|
+
try {
|
|
344
|
+
return JSON.parse(value);
|
|
345
|
+
} catch {
|
|
346
|
+
return null;
|
|
347
|
+
}
|
|
348
|
+
},
|
|
349
|
+
async set(key, value) {
|
|
350
|
+
await storage.setItem(`${namespace}:${key}`, JSON.stringify(value));
|
|
351
|
+
},
|
|
352
|
+
async remove(key) {
|
|
353
|
+
await storage.removeItem(`${namespace}:${key}`);
|
|
354
|
+
}
|
|
355
|
+
};
|
|
356
|
+
}
|
|
357
|
+
|
|
327
358
|
// src/resources/channels.ts
|
|
328
359
|
function bootstrapChannel(http, host, channelId) {
|
|
329
360
|
return http.request(
|
|
@@ -407,6 +438,9 @@ function fetchUnreadCount(http, host, channelId, sessionId, authorizationHeader)
|
|
|
407
438
|
}
|
|
408
439
|
|
|
409
440
|
// src/public/SdkClient.ts
|
|
441
|
+
var PRESENTATION_CACHE_NAMESPACE = "sagepilot_rn_presentation";
|
|
442
|
+
var PRESENTATION_CACHE_KEY = "state";
|
|
443
|
+
var PRESENTATION_RESTORE_MAX_AGE_MS = 15 * 60 * 1e3;
|
|
410
444
|
var DEFAULT_MOBILE_LAUNCHER_CONFIG = {
|
|
411
445
|
label: "Chat",
|
|
412
446
|
buttonColor: "#173c2d",
|
|
@@ -476,6 +510,10 @@ function readRecordField(input, key) {
|
|
|
476
510
|
if (!value || typeof value !== "object" || Array.isArray(value)) return void 0;
|
|
477
511
|
return value;
|
|
478
512
|
}
|
|
513
|
+
function isFreshPresentationState(updatedAt) {
|
|
514
|
+
if (typeof updatedAt !== "number" || !Number.isFinite(updatedAt)) return false;
|
|
515
|
+
return Date.now() - updatedAt <= PRESENTATION_RESTORE_MAX_AGE_MS;
|
|
516
|
+
}
|
|
479
517
|
function toPublicSessionState(session) {
|
|
480
518
|
return {
|
|
481
519
|
session_id: session.session_id,
|
|
@@ -565,6 +603,7 @@ var SagepilotReactNativeChat = class {
|
|
|
565
603
|
this.channel = channel;
|
|
566
604
|
this.session = session;
|
|
567
605
|
this.isConfigured = true;
|
|
606
|
+
await this.restorePresentationFromCache();
|
|
568
607
|
this.emitReady();
|
|
569
608
|
this.emitState();
|
|
570
609
|
if (config.behavior?.enableUnreadPolling ?? true) {
|
|
@@ -704,12 +743,14 @@ var SagepilotReactNativeChat = class {
|
|
|
704
743
|
this.hostedChatView = { screen: "home" };
|
|
705
744
|
if (this.presented) {
|
|
706
745
|
this.emitState();
|
|
746
|
+
this.persistPresentationState();
|
|
707
747
|
return true;
|
|
708
748
|
}
|
|
709
749
|
this.presented = true;
|
|
710
750
|
this.setUnreadCount(0);
|
|
711
751
|
this.presentCallbacks.forEach((callback) => callback(this.getLifecycleState()));
|
|
712
752
|
this.emitState();
|
|
753
|
+
this.persistPresentationState();
|
|
713
754
|
return true;
|
|
714
755
|
}
|
|
715
756
|
presentMessages() {
|
|
@@ -737,6 +778,7 @@ var SagepilotReactNativeChat = class {
|
|
|
737
778
|
this.emitUnreadChange();
|
|
738
779
|
this.dismissCallbacks.forEach((callback) => callback(this.getLifecycleState()));
|
|
739
780
|
this.emitState();
|
|
781
|
+
this.persistPresentationState();
|
|
740
782
|
return true;
|
|
741
783
|
}
|
|
742
784
|
hide() {
|
|
@@ -871,6 +913,10 @@ var SagepilotReactNativeChat = class {
|
|
|
871
913
|
}
|
|
872
914
|
return this.session?.conversation_id ?? void 0;
|
|
873
915
|
}
|
|
916
|
+
/** Returns the chat id that is explicitly part of the current hosted URL. */
|
|
917
|
+
getHostedRouteChatId() {
|
|
918
|
+
return this.hostedChatView.screen === "composer" ? this.hostedChatView.chatId : void 0;
|
|
919
|
+
}
|
|
874
920
|
getHostedIdentityMessage() {
|
|
875
921
|
if (!this.legacyWidgetJwt) return null;
|
|
876
922
|
return {
|
|
@@ -881,6 +927,34 @@ var SagepilotReactNativeChat = class {
|
|
|
881
927
|
}
|
|
882
928
|
};
|
|
883
929
|
}
|
|
930
|
+
/** Captures the hosted route that should receive a picked attachment batch. */
|
|
931
|
+
getHostedAttachmentTarget() {
|
|
932
|
+
return {
|
|
933
|
+
chatId: this.getActiveHostedChatId(),
|
|
934
|
+
routeChatId: this.getHostedRouteChatId(),
|
|
935
|
+
hostedChatView: this.serializeHostedChatView(this.hostedChatView)
|
|
936
|
+
};
|
|
937
|
+
}
|
|
938
|
+
/** Restores the hosted route for a queued attachment batch before delivery. */
|
|
939
|
+
restoreHostedAttachmentTarget(target) {
|
|
940
|
+
if (Platform.OS !== "android") return false;
|
|
941
|
+
if (!target) return false;
|
|
942
|
+
const hostedChatView = this.readPersistedHostedChatView(target.hostedChatView);
|
|
943
|
+
if (!hostedChatView) return false;
|
|
944
|
+
const currentChatId = this.getActiveHostedChatId();
|
|
945
|
+
const targetChatId = normalizeOptionalString(target.chatId);
|
|
946
|
+
const routeChatId = normalizeOptionalString(target.routeChatId) ?? (hostedChatView.screen === "composer" ? normalizeOptionalString(hostedChatView.chatId) : void 0);
|
|
947
|
+
const nextHostedChatView = routeChatId && hostedChatView.screen === "composer" ? this.withPinnedChatId(hostedChatView, routeChatId) : hostedChatView;
|
|
948
|
+
if (this.presented && (!targetChatId || currentChatId === targetChatId) && this.areHostedChatViewsEqual(this.hostedChatView, nextHostedChatView)) {
|
|
949
|
+
return false;
|
|
950
|
+
}
|
|
951
|
+
this.hostedChatView = nextHostedChatView;
|
|
952
|
+
this.presented = true;
|
|
953
|
+
this.setUnreadCount(0);
|
|
954
|
+
this.emitState();
|
|
955
|
+
this.persistPresentationState();
|
|
956
|
+
return true;
|
|
957
|
+
}
|
|
884
958
|
handleHostedBridgeMessage(message) {
|
|
885
959
|
if (!message) return false;
|
|
886
960
|
if (message.type === "close_widget") {
|
|
@@ -948,6 +1022,7 @@ var SagepilotReactNativeChat = class {
|
|
|
948
1022
|
}
|
|
949
1023
|
destroy() {
|
|
950
1024
|
this.stopUnreadPolling();
|
|
1025
|
+
this.persistPresentationState({ cleanShutdown: true, presented: false });
|
|
951
1026
|
this.resetRuntimeState();
|
|
952
1027
|
this.emitState();
|
|
953
1028
|
this.identifyCallbacks.clear();
|
|
@@ -1103,8 +1178,86 @@ var SagepilotReactNativeChat = class {
|
|
|
1103
1178
|
this.presentCallbacks.forEach((callback) => callback(this.getLifecycleState()));
|
|
1104
1179
|
}
|
|
1105
1180
|
this.emitState();
|
|
1181
|
+
this.persistPresentationState();
|
|
1106
1182
|
return true;
|
|
1107
1183
|
}
|
|
1184
|
+
/** Returns the dedicated cache used for hosted-chat presentation recovery. */
|
|
1185
|
+
getPresentationCache() {
|
|
1186
|
+
const cacheStorage = this.runtimeOptions?.cacheStorage;
|
|
1187
|
+
if (!cacheStorage) return null;
|
|
1188
|
+
return createJsonCache(cacheStorage, PRESENTATION_CACHE_NAMESPACE);
|
|
1189
|
+
}
|
|
1190
|
+
/** Removes callback-only fields so the hosted route can be safely serialized. */
|
|
1191
|
+
serializeHostedChatView(view) {
|
|
1192
|
+
if (view.screen !== "composer") return view;
|
|
1193
|
+
return {
|
|
1194
|
+
screen: "composer",
|
|
1195
|
+
message: view.message,
|
|
1196
|
+
mode: view.mode,
|
|
1197
|
+
chatId: view.chatId,
|
|
1198
|
+
metadata: view.metadata
|
|
1199
|
+
};
|
|
1200
|
+
}
|
|
1201
|
+
/** Validates a persisted hosted view before using it to rebuild a widget URL. */
|
|
1202
|
+
readPersistedHostedChatView(view) {
|
|
1203
|
+
if (!view || typeof view !== "object") return null;
|
|
1204
|
+
const record = view;
|
|
1205
|
+
if (record.screen === "home") return { screen: "home" };
|
|
1206
|
+
if (record.screen === "messages") return { screen: "messages" };
|
|
1207
|
+
if (record.screen !== "composer") return null;
|
|
1208
|
+
const mode = record.mode === "new" ? "new" : "auto";
|
|
1209
|
+
return {
|
|
1210
|
+
screen: "composer",
|
|
1211
|
+
message: typeof record.message === "string" ? record.message : void 0,
|
|
1212
|
+
mode,
|
|
1213
|
+
chatId: typeof record.chatId === "string" && record.chatId ? record.chatId : void 0,
|
|
1214
|
+
metadata: readRecordField(record, "metadata")
|
|
1215
|
+
};
|
|
1216
|
+
}
|
|
1217
|
+
/** Forces a persisted hosted route to target the exact chat that owns a pending attachment. */
|
|
1218
|
+
withPinnedChatId(view, chatId) {
|
|
1219
|
+
if (view.screen === "composer") {
|
|
1220
|
+
return {
|
|
1221
|
+
...view,
|
|
1222
|
+
chatId
|
|
1223
|
+
};
|
|
1224
|
+
}
|
|
1225
|
+
return {
|
|
1226
|
+
screen: "composer",
|
|
1227
|
+
mode: "auto",
|
|
1228
|
+
chatId
|
|
1229
|
+
};
|
|
1230
|
+
}
|
|
1231
|
+
/** Compares hosted route state without relying on object identity. */
|
|
1232
|
+
areHostedChatViewsEqual(first, second) {
|
|
1233
|
+
return JSON.stringify(this.serializeHostedChatView(first)) === JSON.stringify(this.serializeHostedChatView(second));
|
|
1234
|
+
}
|
|
1235
|
+
/** Persists presentation state without blocking customer-facing SDK methods. */
|
|
1236
|
+
persistPresentationState(options) {
|
|
1237
|
+
const cache = this.getPresentationCache();
|
|
1238
|
+
if (!cache) return;
|
|
1239
|
+
const state = {
|
|
1240
|
+
presented: options?.presented ?? this.presented,
|
|
1241
|
+
hostedChatView: this.serializeHostedChatView(this.hostedChatView),
|
|
1242
|
+
updatedAt: Date.now(),
|
|
1243
|
+
cleanShutdown: options?.cleanShutdown
|
|
1244
|
+
};
|
|
1245
|
+
void cache.set(PRESENTATION_CACHE_KEY, state).catch(() => void 0);
|
|
1246
|
+
}
|
|
1247
|
+
/** Restores Android presentation state after configure without resetting the hosted route to Home. */
|
|
1248
|
+
async restorePresentationFromCache() {
|
|
1249
|
+
const cache = this.getPresentationCache();
|
|
1250
|
+
if (!cache || Platform.OS !== "android") return;
|
|
1251
|
+
const persisted = await cache.get(PRESENTATION_CACHE_KEY).catch(() => null);
|
|
1252
|
+
if (persisted?.cleanShutdown) return;
|
|
1253
|
+
if (!isFreshPresentationState(persisted?.updatedAt)) return;
|
|
1254
|
+
if (!persisted?.presented) return;
|
|
1255
|
+
const hostedChatView = this.readPersistedHostedChatView(persisted.hostedChatView);
|
|
1256
|
+
if (!hostedChatView) return;
|
|
1257
|
+
this.hostedChatView = hostedChatView;
|
|
1258
|
+
this.presented = true;
|
|
1259
|
+
this.setUnreadCount(0);
|
|
1260
|
+
}
|
|
1108
1261
|
};
|
|
1109
1262
|
var internalSagepilotChat = new SagepilotReactNativeChat();
|
|
1110
1263
|
var SagepilotChat = {
|
|
@@ -1141,34 +1294,6 @@ var SagepilotChat = {
|
|
|
1141
1294
|
destroy: () => internalSagepilotChat.destroy()
|
|
1142
1295
|
};
|
|
1143
1296
|
|
|
1144
|
-
// src/core/storage/cache.ts
|
|
1145
|
-
function createAsyncStorageCacheStorage(asyncStorage) {
|
|
1146
|
-
return {
|
|
1147
|
-
getItem: (key) => asyncStorage.getItem(key),
|
|
1148
|
-
setItem: (key, value) => asyncStorage.setItem(key, value),
|
|
1149
|
-
removeItem: (key) => asyncStorage.removeItem(key)
|
|
1150
|
-
};
|
|
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
1297
|
// src/core/storage/fileStore.ts
|
|
1173
1298
|
var DEFAULT_DIRECTORY_NAME = "sagepilot-attachments";
|
|
1174
1299
|
function sanitizeKey(key) {
|
|
@@ -1226,21 +1351,29 @@ function createSagepilotFileStore(blobUtil, options = {}) {
|
|
|
1226
1351
|
}
|
|
1227
1352
|
|
|
1228
1353
|
// src/core/native/filePicker.ts
|
|
1229
|
-
import { PermissionsAndroid, Platform } from "react-native";
|
|
1230
|
-
|
|
1231
|
-
|
|
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;
|
|
1354
|
+
import { Alert, Linking, PermissionsAndroid, Platform as Platform2 } from "react-native";
|
|
1355
|
+
|
|
1356
|
+
// src/core/native/filePickerShared.ts
|
|
1237
1357
|
var SagepilotFilePickerError = class extends Error {
|
|
1358
|
+
/** Creates a typed picker error that can be surfaced over the widget bridge. */
|
|
1238
1359
|
constructor(code, message) {
|
|
1239
1360
|
super(message);
|
|
1240
1361
|
this.name = "SagepilotFilePickerError";
|
|
1241
1362
|
this.code = code;
|
|
1242
1363
|
}
|
|
1243
1364
|
};
|
|
1365
|
+
var SAGEPILOT_DEFAULT_IMAGE_MAX_DIMENSION = 1920;
|
|
1366
|
+
var SAGEPILOT_DEFAULT_IMAGE_QUALITY = 0.8;
|
|
1367
|
+
var SAGEPILOT_DEFAULT_IMAGE_MAX_FILE_SIZE_BYTES = 15 * 1024 * 1024;
|
|
1368
|
+
|
|
1369
|
+
// src/core/native/filePicker.ts
|
|
1370
|
+
var DEFAULT_IMAGE_MAX_DIMENSION = SAGEPILOT_DEFAULT_IMAGE_MAX_DIMENSION;
|
|
1371
|
+
var DEFAULT_IMAGE_QUALITY = SAGEPILOT_DEFAULT_IMAGE_QUALITY;
|
|
1372
|
+
var DEFAULT_IMAGE_MIME_TYPE = "image/jpeg";
|
|
1373
|
+
var DEFAULT_DOCUMENT_MIME_TYPE = "application/octet-stream";
|
|
1374
|
+
var DEFAULT_IMAGE_SELECTION_LIMIT = 5;
|
|
1375
|
+
var DEFAULT_DOCUMENT_MAX_FILE_SIZE_BYTES = 20 * 1024 * 1024;
|
|
1376
|
+
var DEFAULT_IMAGE_MAX_FILE_SIZE_BYTES = SAGEPILOT_DEFAULT_IMAGE_MAX_FILE_SIZE_BYTES;
|
|
1244
1377
|
function bytesToMb(bytes) {
|
|
1245
1378
|
return Math.round(bytes / (1024 * 1024) * 10) / 10;
|
|
1246
1379
|
}
|
|
@@ -1250,8 +1383,30 @@ function estimateBase64ByteSize(dataBase64) {
|
|
|
1250
1383
|
function stripFileScheme(uri) {
|
|
1251
1384
|
return uri.startsWith("file://") ? uri.replace("file://", "") : uri;
|
|
1252
1385
|
}
|
|
1253
|
-
async function
|
|
1254
|
-
if (
|
|
1386
|
+
async function promptOpenSagepilotCameraSettings() {
|
|
1387
|
+
if (Platform2.OS !== "android") return;
|
|
1388
|
+
await new Promise((resolve) => {
|
|
1389
|
+
Alert.alert(
|
|
1390
|
+
"Camera access is off",
|
|
1391
|
+
"Enable camera access for this app in Settings to take a photo.",
|
|
1392
|
+
[
|
|
1393
|
+
{
|
|
1394
|
+
text: "Not now",
|
|
1395
|
+
style: "cancel",
|
|
1396
|
+
onPress: resolve
|
|
1397
|
+
},
|
|
1398
|
+
{
|
|
1399
|
+
text: "Open Settings",
|
|
1400
|
+
onPress: () => {
|
|
1401
|
+
Linking.openSettings().finally(resolve);
|
|
1402
|
+
}
|
|
1403
|
+
}
|
|
1404
|
+
]
|
|
1405
|
+
);
|
|
1406
|
+
});
|
|
1407
|
+
}
|
|
1408
|
+
async function ensureSagepilotAndroidCameraPermission() {
|
|
1409
|
+
if (Platform2.OS !== "android") return;
|
|
1255
1410
|
let result;
|
|
1256
1411
|
try {
|
|
1257
1412
|
result = await PermissionsAndroid.request(PermissionsAndroid.PERMISSIONS.CAMERA);
|
|
@@ -1259,6 +1414,7 @@ async function ensureAndroidCameraPermission() {
|
|
|
1259
1414
|
return;
|
|
1260
1415
|
}
|
|
1261
1416
|
if (result === PermissionsAndroid.RESULTS.NEVER_ASK_AGAIN) {
|
|
1417
|
+
await promptOpenSagepilotCameraSettings();
|
|
1262
1418
|
throw new SagepilotFilePickerError(
|
|
1263
1419
|
"permission_denied",
|
|
1264
1420
|
"Camera access is turned off. Enable it for this app in Settings to take a photo."
|
|
@@ -1271,6 +1427,9 @@ async function ensureAndroidCameraPermission() {
|
|
|
1271
1427
|
);
|
|
1272
1428
|
}
|
|
1273
1429
|
}
|
|
1430
|
+
function canUseCameraSource(imagePicker) {
|
|
1431
|
+
return Boolean(imagePicker);
|
|
1432
|
+
}
|
|
1274
1433
|
function mapImagePickerErrorCode(errorCode) {
|
|
1275
1434
|
switch (errorCode) {
|
|
1276
1435
|
case "permission":
|
|
@@ -1347,7 +1506,8 @@ function createSagepilotFilePicker(options) {
|
|
|
1347
1506
|
const documentMaxFileSizeBytes = options.documentMaxFileSizeBytes ?? DEFAULT_DOCUMENT_MAX_FILE_SIZE_BYTES;
|
|
1348
1507
|
const imageMaxFileSizeBytes = options.imageMaxFileSizeBytes ?? DEFAULT_IMAGE_MAX_FILE_SIZE_BYTES;
|
|
1349
1508
|
const sources = [];
|
|
1350
|
-
if (imagePicker) sources.push("camera"
|
|
1509
|
+
if (canUseCameraSource(imagePicker)) sources.push("camera");
|
|
1510
|
+
if (imagePicker) sources.push("library");
|
|
1351
1511
|
if (documentsPicker) sources.push("documents");
|
|
1352
1512
|
if (sources.length === 0) return void 0;
|
|
1353
1513
|
const imageOptions = {
|
|
@@ -1359,8 +1519,10 @@ function createSagepilotFilePicker(options) {
|
|
|
1359
1519
|
saveToPhotos: false
|
|
1360
1520
|
};
|
|
1361
1521
|
async function pickFromCamera() {
|
|
1362
|
-
|
|
1363
|
-
|
|
1522
|
+
await ensureSagepilotAndroidCameraPermission();
|
|
1523
|
+
if (!imagePicker) {
|
|
1524
|
+
throw new SagepilotFilePickerError("camera_unavailable", "The camera picker is not available in this build.");
|
|
1525
|
+
}
|
|
1364
1526
|
return imageAssetsToPickedFiles(await imagePicker.launchCamera(imageOptions), imageMaxFileSizeBytes);
|
|
1365
1527
|
}
|
|
1366
1528
|
async function pickFromLibrary(multiple) {
|
|
@@ -1431,13 +1593,14 @@ function createSagepilotFilePicker(options) {
|
|
|
1431
1593
|
|
|
1432
1594
|
// src/ui/SagepilotChatProvider.ts
|
|
1433
1595
|
import { createElement, useCallback, useEffect, useRef, useState } from "react";
|
|
1596
|
+
import { Camera, FileText, Images, X } from "lucide-react-native";
|
|
1434
1597
|
import {
|
|
1435
1598
|
ActivityIndicator,
|
|
1436
1599
|
AppState,
|
|
1437
1600
|
KeyboardAvoidingView,
|
|
1438
|
-
Linking,
|
|
1601
|
+
Linking as Linking2,
|
|
1439
1602
|
Modal,
|
|
1440
|
-
Platform as
|
|
1603
|
+
Platform as Platform3,
|
|
1441
1604
|
Pressable,
|
|
1442
1605
|
SafeAreaView,
|
|
1443
1606
|
StyleSheet,
|
|
@@ -1448,8 +1611,11 @@ import { WebView } from "react-native-webview";
|
|
|
1448
1611
|
|
|
1449
1612
|
// src/core/webview/mobileBridge.ts
|
|
1450
1613
|
var FILE_PICKER_PROTOCOL_VERSION = 2;
|
|
1614
|
+
function buildBridgeCapabilitiesPayload(capabilities) {
|
|
1615
|
+
return { ...capabilities, filePickerProtocol: FILE_PICKER_PROTOCOL_VERSION };
|
|
1616
|
+
}
|
|
1451
1617
|
function buildBridgeCapabilitiesScript(capabilities) {
|
|
1452
|
-
const payload =
|
|
1618
|
+
const payload = buildBridgeCapabilitiesPayload(capabilities);
|
|
1453
1619
|
return [
|
|
1454
1620
|
"(function(){",
|
|
1455
1621
|
"try {",
|
|
@@ -1498,9 +1664,23 @@ function parseHostedBridgeMessage(rawData) {
|
|
|
1498
1664
|
return null;
|
|
1499
1665
|
}
|
|
1500
1666
|
}
|
|
1501
|
-
|
|
1667
|
+
function buildMobileWebViewBridgeScript(capabilities) {
|
|
1668
|
+
const payload = buildBridgeCapabilitiesPayload(capabilities);
|
|
1669
|
+
return `
|
|
1502
1670
|
(function () {
|
|
1503
|
-
|
|
1671
|
+
var bridgeCapabilities = ${JSON.stringify(payload)};
|
|
1672
|
+
var applyBridgeCapabilities = function () {
|
|
1673
|
+
try {
|
|
1674
|
+
if (window.SagepilotMobileBridge) {
|
|
1675
|
+
window.SagepilotMobileBridge.capabilities = Object.assign({}, window.SagepilotMobileBridge.capabilities, bridgeCapabilities);
|
|
1676
|
+
}
|
|
1677
|
+
} catch (error) {}
|
|
1678
|
+
};
|
|
1679
|
+
|
|
1680
|
+
if (window.__sagepilotRnBridgeInstalled) {
|
|
1681
|
+
applyBridgeCapabilities();
|
|
1682
|
+
return true;
|
|
1683
|
+
}
|
|
1504
1684
|
window.__sagepilotRnBridgeInstalled = true;
|
|
1505
1685
|
|
|
1506
1686
|
var ensureViewport = function () {
|
|
@@ -1541,9 +1721,11 @@ var mobileWebViewBridgeScript = `
|
|
|
1541
1721
|
};
|
|
1542
1722
|
|
|
1543
1723
|
window.SagepilotMobileBridge = {
|
|
1724
|
+
capabilities: bridgeCapabilities,
|
|
1544
1725
|
postMessage: sendToReactNative,
|
|
1545
1726
|
ready: function () { sendToReactNative({ type: "sagepilot:ready" }); }
|
|
1546
1727
|
};
|
|
1728
|
+
applyBridgeCapabilities();
|
|
1547
1729
|
|
|
1548
1730
|
document.addEventListener("click", function (event) {
|
|
1549
1731
|
try {
|
|
@@ -1584,6 +1766,7 @@ var mobileWebViewBridgeScript = `
|
|
|
1584
1766
|
return true;
|
|
1585
1767
|
})();
|
|
1586
1768
|
`;
|
|
1769
|
+
}
|
|
1587
1770
|
|
|
1588
1771
|
// src/specs/SagepilotInsetsViewNativeComponent.ts
|
|
1589
1772
|
import codegenNativeComponent from "react-native/Libraries/Utilities/codegenNativeComponent";
|
|
@@ -1600,13 +1783,17 @@ function readPresentationState() {
|
|
|
1600
1783
|
presentation: internalSagepilotChat.getConfig()?.presentation
|
|
1601
1784
|
};
|
|
1602
1785
|
}
|
|
1786
|
+
function getBridgeCapabilities() {
|
|
1787
|
+
return {
|
|
1788
|
+
nativeFilePicker: Boolean(internalSagepilotChat.getConfig()?.filePicker)
|
|
1789
|
+
};
|
|
1790
|
+
}
|
|
1603
1791
|
function getInjectedWebViewScript() {
|
|
1792
|
+
const capabilities = getBridgeCapabilities();
|
|
1604
1793
|
return [
|
|
1605
1794
|
internalSagepilotChat.getHostedAuthScript(),
|
|
1606
|
-
|
|
1607
|
-
buildBridgeCapabilitiesScript(
|
|
1608
|
-
nativeFilePicker: Boolean(internalSagepilotChat.getConfig()?.filePicker)
|
|
1609
|
-
})
|
|
1795
|
+
buildMobileWebViewBridgeScript(capabilities),
|
|
1796
|
+
buildBridgeCapabilitiesScript(capabilities)
|
|
1610
1797
|
].filter(Boolean).join("\n");
|
|
1611
1798
|
}
|
|
1612
1799
|
var FILE_PICKER_SOURCE_LABELS = {
|
|
@@ -1614,6 +1801,21 @@ var FILE_PICKER_SOURCE_LABELS = {
|
|
|
1614
1801
|
library: "Choose from gallery",
|
|
1615
1802
|
documents: "Browse files"
|
|
1616
1803
|
};
|
|
1804
|
+
var FILE_PICKER_SOURCE_DESCRIPTIONS = {
|
|
1805
|
+
camera: "Use the in-app camera",
|
|
1806
|
+
library: "Photos and videos on this device",
|
|
1807
|
+
documents: "PDFs, documents, and Drive files"
|
|
1808
|
+
};
|
|
1809
|
+
var FILE_PICKER_SOURCE_ICONS = {
|
|
1810
|
+
camera: Camera,
|
|
1811
|
+
library: Images,
|
|
1812
|
+
documents: FileText
|
|
1813
|
+
};
|
|
1814
|
+
var FILE_PICKER_SOURCE_ICON_COLORS = {
|
|
1815
|
+
camera: "#0284c7",
|
|
1816
|
+
library: "#16a34a",
|
|
1817
|
+
documents: "#7c3aed"
|
|
1818
|
+
};
|
|
1617
1819
|
var DELIVERY_RETRY_MS = 1500;
|
|
1618
1820
|
var DELIVERY_LEGACY_FALLBACK_ATTEMPTS = 2;
|
|
1619
1821
|
var DELIVERY_MAX_ATTEMPTS = 8;
|
|
@@ -1623,6 +1825,7 @@ var PENDING_FILES_CACHE_KEY = "pending_batch";
|
|
|
1623
1825
|
var LIVENESS_HEARTBEAT_STALE_MS = 4e3;
|
|
1624
1826
|
var LIVENESS_PONG_TIMEOUT_MS = 1500;
|
|
1625
1827
|
var LIVENESS_MAX_PING_ATTEMPTS = 2;
|
|
1828
|
+
var LIVENESS_PICKER_GRACE_MS = 8e3;
|
|
1626
1829
|
var LIVENESS_MAX_REMOUNTS = 2;
|
|
1627
1830
|
var batchSequence = 0;
|
|
1628
1831
|
function nextBatchId() {
|
|
@@ -1636,6 +1839,29 @@ function totalBase64Bytes(files) {
|
|
|
1636
1839
|
function storageKeyFor(batchId, index) {
|
|
1637
1840
|
return `${batchId}_${index}.bin`;
|
|
1638
1841
|
}
|
|
1842
|
+
function renderFilePickerSourceIcon(source) {
|
|
1843
|
+
const Icon = FILE_PICKER_SOURCE_ICONS[source];
|
|
1844
|
+
return createElement(
|
|
1845
|
+
View,
|
|
1846
|
+
{ style: getFilePickerSourceIconStyle(source) },
|
|
1847
|
+
createElement(Icon, {
|
|
1848
|
+
color: FILE_PICKER_SOURCE_ICON_COLORS[source],
|
|
1849
|
+
size: 23,
|
|
1850
|
+
strokeWidth: 2.35
|
|
1851
|
+
})
|
|
1852
|
+
);
|
|
1853
|
+
}
|
|
1854
|
+
function getFilePickerSourceIconStyle(source) {
|
|
1855
|
+
if (source === "camera") return [styles.sheetOptionIcon, styles.cameraOptionIcon];
|
|
1856
|
+
if (source === "library") return [styles.sheetOptionIcon, styles.libraryOptionIcon];
|
|
1857
|
+
return [styles.sheetOptionIcon, styles.documentsOptionIcon];
|
|
1858
|
+
}
|
|
1859
|
+
function getSheetOptionStyle(pressed) {
|
|
1860
|
+
return [styles.sheetOption, pressed && styles.sheetOptionPressed];
|
|
1861
|
+
}
|
|
1862
|
+
function getSheetCancelStyle(pressed) {
|
|
1863
|
+
return [styles.sheetCancelButton, pressed && styles.sheetCancelButtonPressed];
|
|
1864
|
+
}
|
|
1639
1865
|
function buildDispatchScript(messageLiteral) {
|
|
1640
1866
|
return [
|
|
1641
1867
|
"(function(){",
|
|
@@ -1647,8 +1873,14 @@ function buildDispatchScript(messageLiteral) {
|
|
|
1647
1873
|
"})();"
|
|
1648
1874
|
].join("\n");
|
|
1649
1875
|
}
|
|
1650
|
-
function buildFilesPickedScript(
|
|
1651
|
-
return buildDispatchScript(
|
|
1876
|
+
function buildFilesPickedScript(batch) {
|
|
1877
|
+
return buildDispatchScript(JSON.stringify({
|
|
1878
|
+
type: "sagepilot:files_picked",
|
|
1879
|
+
batch_id: batch.batchId,
|
|
1880
|
+
chat_id: batch.target?.chatId,
|
|
1881
|
+
conversation_id: batch.target?.chatId,
|
|
1882
|
+
files: batch.files
|
|
1883
|
+
}));
|
|
1652
1884
|
}
|
|
1653
1885
|
function buildFilePickerErrorScript(message, code) {
|
|
1654
1886
|
return buildDispatchScript(
|
|
@@ -1695,7 +1927,7 @@ function isInternalWebViewScheme(url) {
|
|
|
1695
1927
|
var hostedChatWebViewProps = {
|
|
1696
1928
|
allowFileAccess: true,
|
|
1697
1929
|
allowFileAccessFromFileURLs: true,
|
|
1698
|
-
...
|
|
1930
|
+
...Platform3.OS === "ios" ? {
|
|
1699
1931
|
automaticallyAdjustContentInsets: false,
|
|
1700
1932
|
contentInsetAdjustmentBehavior: "never",
|
|
1701
1933
|
hideKeyboardAccessoryView: true
|
|
@@ -1710,7 +1942,7 @@ var hostedChatWebViewProps = {
|
|
|
1710
1942
|
sharedCookiesEnabled: true,
|
|
1711
1943
|
thirdPartyCookiesEnabled: true
|
|
1712
1944
|
};
|
|
1713
|
-
var AndroidInsetsView =
|
|
1945
|
+
var AndroidInsetsView = Platform3.OS === "android" ? SagepilotInsetsViewNativeComponent_default : View;
|
|
1714
1946
|
var emptyAndroidInsets = { top: 0, bottom: 0 };
|
|
1715
1947
|
function SagepilotChatProvider({
|
|
1716
1948
|
children,
|
|
@@ -1729,6 +1961,9 @@ function SagepilotChatProvider({
|
|
|
1729
1961
|
const deliveryAttemptsRef = useRef(0);
|
|
1730
1962
|
const pendingPingRef = useRef(null);
|
|
1731
1963
|
const pingTimeoutRef = useRef(null);
|
|
1964
|
+
const livenessResumeTimerRef = useRef(null);
|
|
1965
|
+
const livenessPausedUntilRef = useRef(0);
|
|
1966
|
+
const pickerInFlightRef = useRef(false);
|
|
1732
1967
|
const livenessRemountCountRef = useRef(0);
|
|
1733
1968
|
const appStateRef = useRef(AppState.currentState);
|
|
1734
1969
|
const didReconcileRef = useRef(false);
|
|
@@ -1751,6 +1986,7 @@ function SagepilotChatProvider({
|
|
|
1751
1986
|
if (hasFileStore) {
|
|
1752
1987
|
const manifest = queue.filter((batch) => batch.storageKeys && batch.storageKeys.length === batch.files.length).map((batch) => ({
|
|
1753
1988
|
batchId: batch.batchId,
|
|
1989
|
+
target: batch.target,
|
|
1754
1990
|
files: batch.files.map((file, index) => ({
|
|
1755
1991
|
file_name: file.file_name,
|
|
1756
1992
|
mime_type: file.mime_type,
|
|
@@ -1769,6 +2005,7 @@ function SagepilotChatProvider({
|
|
|
1769
2005
|
if (totalBytes <= PERSIST_MAX_TOTAL_BYTES) {
|
|
1770
2006
|
const manifest = queue.map((batch) => ({
|
|
1771
2007
|
batchId: batch.batchId,
|
|
2008
|
+
target: batch.target,
|
|
1772
2009
|
files: batch.files.map((file) => ({
|
|
1773
2010
|
file_name: file.file_name,
|
|
1774
2011
|
mime_type: file.mime_type,
|
|
@@ -1810,12 +2047,17 @@ function SagepilotChatProvider({
|
|
|
1810
2047
|
}
|
|
1811
2048
|
const batch = pendingBatchesRef.current[0];
|
|
1812
2049
|
if (!batch) return;
|
|
2050
|
+
if (internalSagepilotChat.restoreHostedAttachmentTarget(batch.target)) {
|
|
2051
|
+
widgetReadyRef.current = false;
|
|
2052
|
+
deliveryTimerRef.current = setTimeout(() => pumpDelivery(), DELIVERY_RETRY_MS);
|
|
2053
|
+
return;
|
|
2054
|
+
}
|
|
1813
2055
|
deliveryAttemptsRef.current += 1;
|
|
1814
2056
|
const attempts = deliveryAttemptsRef.current;
|
|
1815
2057
|
const ref = nativeWebViewRef.current;
|
|
1816
2058
|
const shouldDeliver = ref && (widgetReadyRef.current || attempts >= DELIVERY_LEGACY_FALLBACK_ATTEMPTS);
|
|
1817
2059
|
if (shouldDeliver && ref) {
|
|
1818
|
-
ref.injectJavaScript(buildFilesPickedScript(batch
|
|
2060
|
+
ref.injectJavaScript(buildFilesPickedScript(batch));
|
|
1819
2061
|
}
|
|
1820
2062
|
if (attempts < DELIVERY_MAX_ATTEMPTS) {
|
|
1821
2063
|
deliveryTimerRef.current = setTimeout(() => pumpDelivery(), DELIVERY_RETRY_MS);
|
|
@@ -1846,22 +2088,38 @@ function SagepilotChatProvider({
|
|
|
1846
2088
|
const batchId = nextBatchId();
|
|
1847
2089
|
const hasFileStore = Boolean(internalSagepilotChat.getConfig()?.fileStore);
|
|
1848
2090
|
const storageKeys = hasFileStore ? files.map((_, index) => storageKeyFor(batchId, index)) : void 0;
|
|
1849
|
-
const batch = {
|
|
2091
|
+
const batch = {
|
|
2092
|
+
batchId,
|
|
2093
|
+
files,
|
|
2094
|
+
storageKeys,
|
|
2095
|
+
target: internalSagepilotChat.getHostedAttachmentTarget()
|
|
2096
|
+
};
|
|
1850
2097
|
pendingBatchesRef.current = [...pendingBatchesRef.current, batch];
|
|
1851
2098
|
ensureDelivery();
|
|
1852
2099
|
writeManifest();
|
|
1853
2100
|
void writeBatchBytes(batch);
|
|
1854
2101
|
}, [ensureDelivery, writeManifest, writeBatchBytes]);
|
|
1855
|
-
const recoverNativeWebView = useCallback(() => {
|
|
2102
|
+
const recoverNativeWebView = useCallback((_reasonOrEvent) => {
|
|
1856
2103
|
widgetReadyRef.current = false;
|
|
1857
2104
|
setNativeWebViewKey((key) => key + 1);
|
|
1858
2105
|
}, []);
|
|
1859
2106
|
const recoverPreloadWebView = useCallback(() => {
|
|
1860
2107
|
setPreloadWebViewKey((key) => key + 1);
|
|
1861
2108
|
}, []);
|
|
2109
|
+
const pauseLivenessAfterPicker = useCallback((durationMs) => {
|
|
2110
|
+
if (Platform3.OS !== "android") return;
|
|
2111
|
+
livenessPausedUntilRef.current = Math.max(livenessPausedUntilRef.current, Date.now() + durationMs);
|
|
2112
|
+
}, []);
|
|
1862
2113
|
const runNativeFilePicker = useCallback((source, multiple) => {
|
|
1863
2114
|
const filePicker = internalSagepilotChat.getConfig()?.filePicker;
|
|
1864
|
-
if (!filePicker)
|
|
2115
|
+
if (!filePicker) {
|
|
2116
|
+
return;
|
|
2117
|
+
}
|
|
2118
|
+
if (pickerInFlightRef.current) {
|
|
2119
|
+
return;
|
|
2120
|
+
}
|
|
2121
|
+
pauseLivenessAfterPicker(6e4);
|
|
2122
|
+
pickerInFlightRef.current = true;
|
|
1865
2123
|
filePicker.pickFiles({ source, multiple }).then((files) => {
|
|
1866
2124
|
if (files.length === 0) {
|
|
1867
2125
|
nativeWebViewRef.current?.injectJavaScript(buildFilePickerCancelledScript());
|
|
@@ -1871,13 +2129,18 @@ function SagepilotChatProvider({
|
|
|
1871
2129
|
}).catch((error) => {
|
|
1872
2130
|
const { message, code } = readFilePickerError(error);
|
|
1873
2131
|
nativeWebViewRef.current?.injectJavaScript(buildFilePickerErrorScript(message, code));
|
|
2132
|
+
}).finally(() => {
|
|
2133
|
+
pickerInFlightRef.current = false;
|
|
2134
|
+
pauseLivenessAfterPicker(LIVENESS_PICKER_GRACE_MS);
|
|
1874
2135
|
});
|
|
1875
|
-
}, [queuePickedFiles]);
|
|
2136
|
+
}, [pauseLivenessAfterPicker, queuePickedFiles]);
|
|
1876
2137
|
const openNativeFilePicker = useCallback((multiple) => {
|
|
1877
2138
|
const filePicker = internalSagepilotChat.getConfig()?.filePicker;
|
|
1878
|
-
if (!filePicker || filePicker.sources.length === 0)
|
|
2139
|
+
if (!filePicker || filePicker.sources.length === 0) {
|
|
2140
|
+
return;
|
|
2141
|
+
}
|
|
1879
2142
|
const [onlySource] = filePicker.sources;
|
|
1880
|
-
if (filePicker.sources.length === 1 && onlySource) {
|
|
2143
|
+
if (filePicker.sources.length === 1 && onlySource && onlySource !== "camera") {
|
|
1881
2144
|
runNativeFilePicker(onlySource, multiple);
|
|
1882
2145
|
return;
|
|
1883
2146
|
}
|
|
@@ -1930,7 +2193,8 @@ function SagepilotChatProvider({
|
|
|
1930
2193
|
restored.push({
|
|
1931
2194
|
batchId: batch.batchId,
|
|
1932
2195
|
files,
|
|
1933
|
-
storageKeys: storageKeys.length === files.length ? storageKeys : void 0
|
|
2196
|
+
storageKeys: storageKeys.length === files.length ? storageKeys : void 0,
|
|
2197
|
+
target: batch.target
|
|
1934
2198
|
});
|
|
1935
2199
|
}
|
|
1936
2200
|
}
|
|
@@ -1951,6 +2215,19 @@ function SagepilotChatProvider({
|
|
|
1951
2215
|
cancelled = true;
|
|
1952
2216
|
};
|
|
1953
2217
|
}, [getPendingCache, startDelivery, writeManifest]);
|
|
2218
|
+
const clearPing = useCallback(() => {
|
|
2219
|
+
pendingPingRef.current = null;
|
|
2220
|
+
if (pingTimeoutRef.current) {
|
|
2221
|
+
clearTimeout(pingTimeoutRef.current);
|
|
2222
|
+
pingTimeoutRef.current = null;
|
|
2223
|
+
}
|
|
2224
|
+
}, []);
|
|
2225
|
+
const clearDeferredLivenessProbe = useCallback(() => {
|
|
2226
|
+
if (livenessResumeTimerRef.current) {
|
|
2227
|
+
clearTimeout(livenessResumeTimerRef.current);
|
|
2228
|
+
livenessResumeTimerRef.current = null;
|
|
2229
|
+
}
|
|
2230
|
+
}, []);
|
|
1954
2231
|
useEffect(() => {
|
|
1955
2232
|
return () => {
|
|
1956
2233
|
if (deliveryTimerRef.current) {
|
|
@@ -1961,19 +2238,23 @@ function SagepilotChatProvider({
|
|
|
1961
2238
|
clearTimeout(pingTimeoutRef.current);
|
|
1962
2239
|
pingTimeoutRef.current = null;
|
|
1963
2240
|
}
|
|
2241
|
+
clearDeferredLivenessProbe();
|
|
1964
2242
|
};
|
|
1965
|
-
}, []);
|
|
1966
|
-
const clearPing = useCallback(() => {
|
|
1967
|
-
pendingPingRef.current = null;
|
|
1968
|
-
if (pingTimeoutRef.current) {
|
|
1969
|
-
clearTimeout(pingTimeoutRef.current);
|
|
1970
|
-
pingTimeoutRef.current = null;
|
|
1971
|
-
}
|
|
1972
|
-
}, []);
|
|
2243
|
+
}, [clearDeferredLivenessProbe]);
|
|
1973
2244
|
const runLivenessProbe = useCallback(() => {
|
|
1974
|
-
if (
|
|
2245
|
+
if (Platform3.OS !== "android") return;
|
|
1975
2246
|
const ref = nativeWebViewRef.current;
|
|
1976
2247
|
if (!ref || !internalSagepilotChat.isPresented()) return;
|
|
2248
|
+
const pauseRemainingMs = livenessPausedUntilRef.current - Date.now();
|
|
2249
|
+
if (pickerInFlightRef.current || pauseRemainingMs > 0) {
|
|
2250
|
+
const retryDelayMs = pickerInFlightRef.current ? 500 : Math.max(0, pauseRemainingMs);
|
|
2251
|
+
clearDeferredLivenessProbe();
|
|
2252
|
+
livenessResumeTimerRef.current = setTimeout(() => {
|
|
2253
|
+
livenessResumeTimerRef.current = null;
|
|
2254
|
+
runLivenessProbe();
|
|
2255
|
+
}, retryDelayMs);
|
|
2256
|
+
return;
|
|
2257
|
+
}
|
|
1977
2258
|
setAndroidRepaintTick((tick) => tick + 1);
|
|
1978
2259
|
const attempts = (pendingPingRef.current?.attempts ?? 0) + 1;
|
|
1979
2260
|
const nonce = `${Date.now().toString(36)}_${attempts}`;
|
|
@@ -1983,12 +2264,12 @@ function SagepilotChatProvider({
|
|
|
1983
2264
|
pingTimeoutRef.current = setTimeout(() => {
|
|
1984
2265
|
if (attempts >= LIVENESS_MAX_PING_ATTEMPTS) {
|
|
1985
2266
|
clearPing();
|
|
1986
|
-
recoverNativeWebView();
|
|
2267
|
+
recoverNativeWebView("liveness_timeout");
|
|
1987
2268
|
return;
|
|
1988
2269
|
}
|
|
1989
2270
|
runLivenessProbe();
|
|
1990
2271
|
}, LIVENESS_PONG_TIMEOUT_MS);
|
|
1991
|
-
}, [clearPing, recoverNativeWebView]);
|
|
2272
|
+
}, [clearDeferredLivenessProbe, clearPing, recoverNativeWebView]);
|
|
1992
2273
|
useEffect(() => {
|
|
1993
2274
|
const subscription = AppState.addEventListener("change", (nextState) => {
|
|
1994
2275
|
const prev = appStateRef.current;
|
|
@@ -2018,11 +2299,11 @@ function SagepilotChatProvider({
|
|
|
2018
2299
|
const presentationStyle = state.presentation?.style ?? "sheet";
|
|
2019
2300
|
const isFullScreenModal = presentationStyle === "fullScreen";
|
|
2020
2301
|
const animationType = presentationStyle === "fullScreen" || presentationStyle === "push" ? "slide" : "fade";
|
|
2021
|
-
const ModalContainer =
|
|
2022
|
-
const NativeModalContainer =
|
|
2023
|
-
const ChatContentContainer =
|
|
2024
|
-
const nativeModalContainerProps =
|
|
2025
|
-
const chatContentContainerProps =
|
|
2302
|
+
const ModalContainer = Platform3.OS === "ios" && isFullScreenModal ? SafeAreaView : View;
|
|
2303
|
+
const NativeModalContainer = Platform3.OS === "android" ? AndroidInsetsView : ModalContainer;
|
|
2304
|
+
const ChatContentContainer = Platform3.OS === "ios" ? KeyboardAvoidingView : View;
|
|
2305
|
+
const nativeModalContainerProps = Platform3.OS === "android" ? { style: styles.container, onInsetsChange: handleAndroidInsetsChange } : { style: styles.container };
|
|
2306
|
+
const chatContentContainerProps = Platform3.OS === "ios" ? {
|
|
2026
2307
|
behavior: "padding",
|
|
2027
2308
|
enabled: true,
|
|
2028
2309
|
keyboardVerticalOffset: 0,
|
|
@@ -2030,7 +2311,7 @@ function SagepilotChatProvider({
|
|
|
2030
2311
|
} : {
|
|
2031
2312
|
style: [
|
|
2032
2313
|
styles.modalContent,
|
|
2033
|
-
|
|
2314
|
+
Platform3.OS === "android" ? {
|
|
2034
2315
|
paddingTop: androidModalInsets.top,
|
|
2035
2316
|
paddingBottom: androidModalInsets.bottom
|
|
2036
2317
|
} : null
|
|
@@ -2062,7 +2343,7 @@ function SagepilotChatProvider({
|
|
|
2062
2343
|
clearPing();
|
|
2063
2344
|
if (livenessRemountCountRef.current < LIVENESS_MAX_REMOUNTS) {
|
|
2064
2345
|
livenessRemountCountRef.current += 1;
|
|
2065
|
-
recoverNativeWebView();
|
|
2346
|
+
recoverNativeWebView("liveness_dead_pong");
|
|
2066
2347
|
}
|
|
2067
2348
|
} else {
|
|
2068
2349
|
livenessRemountCountRef.current = 0;
|
|
@@ -2084,7 +2365,12 @@ function SagepilotChatProvider({
|
|
|
2084
2365
|
if (!script || !nativeWebViewRef.current) return;
|
|
2085
2366
|
nativeWebViewRef.current.injectJavaScript(script);
|
|
2086
2367
|
};
|
|
2368
|
+
const injectNativeBridgeToNativeWebView = () => {
|
|
2369
|
+
if (!nativeWebViewRef.current) return;
|
|
2370
|
+
nativeWebViewRef.current.injectJavaScript(getInjectedWebViewScript());
|
|
2371
|
+
};
|
|
2087
2372
|
const handleNativeWebViewLoadEnd = () => {
|
|
2373
|
+
injectNativeBridgeToNativeWebView();
|
|
2088
2374
|
postIdentityToNativeWebView();
|
|
2089
2375
|
widgetReadyRef.current = false;
|
|
2090
2376
|
startDelivery();
|
|
@@ -2098,11 +2384,11 @@ function SagepilotChatProvider({
|
|
|
2098
2384
|
if (widgetOrigin && readUrlOrigin(url) === widgetOrigin) {
|
|
2099
2385
|
return true;
|
|
2100
2386
|
}
|
|
2101
|
-
|
|
2387
|
+
Linking2.openURL(url).catch(() => void 0);
|
|
2102
2388
|
return false;
|
|
2103
2389
|
}, [state.conversationUrl, state.preloadUrl]);
|
|
2104
2390
|
useEffect(() => {
|
|
2105
|
-
if (
|
|
2391
|
+
if (Platform3.OS !== "web" || typeof window === "undefined") return;
|
|
2106
2392
|
const trustedWidgetOrigin = readUrlOrigin(state.conversationUrl);
|
|
2107
2393
|
if (!trustedWidgetOrigin) return;
|
|
2108
2394
|
const handleWindowMessage = (event) => {
|
|
@@ -2113,14 +2399,14 @@ function SagepilotChatProvider({
|
|
|
2113
2399
|
return () => window.removeEventListener("message", handleWindowMessage);
|
|
2114
2400
|
}, [state.conversationUrl]);
|
|
2115
2401
|
useEffect(() => {
|
|
2116
|
-
if (
|
|
2402
|
+
if (Platform3.OS !== "web" || !state.isPresented) return;
|
|
2117
2403
|
postIdentityToWebFrame();
|
|
2118
2404
|
}, [state.isPresented, state.conversationUrl, state.configured]);
|
|
2119
2405
|
useEffect(() => {
|
|
2120
|
-
if (
|
|
2406
|
+
if (Platform3.OS === "web" || !state.isPresented) return;
|
|
2121
2407
|
postIdentityToNativeWebView();
|
|
2122
2408
|
}, [state.isPresented, state.conversationUrl, state.configured]);
|
|
2123
|
-
if (
|
|
2409
|
+
if (Platform3.OS === "web") {
|
|
2124
2410
|
return createElement(
|
|
2125
2411
|
View,
|
|
2126
2412
|
{ style: styles.root },
|
|
@@ -2182,8 +2468,8 @@ function SagepilotChatProvider({
|
|
|
2182
2468
|
visible: state.configured && state.isPresented,
|
|
2183
2469
|
animationType,
|
|
2184
2470
|
presentationStyle: isFullScreenModal ? "fullScreen" : "pageSheet",
|
|
2185
|
-
statusBarTranslucent:
|
|
2186
|
-
navigationBarTranslucent:
|
|
2471
|
+
statusBarTranslucent: Platform3.OS === "android",
|
|
2472
|
+
navigationBarTranslucent: Platform3.OS === "android",
|
|
2187
2473
|
onRequestClose: () => internalSagepilotChat.dismiss()
|
|
2188
2474
|
},
|
|
2189
2475
|
createElement(
|
|
@@ -2214,7 +2500,7 @@ function SagepilotChatProvider({
|
|
|
2214
2500
|
// The imperceptible opacity toggle forces the Android WebView
|
|
2215
2501
|
// surface to re-composite on resume, clearing the blank-but-alive
|
|
2216
2502
|
// surface bug without a reload (see runLivenessProbe).
|
|
2217
|
-
style:
|
|
2503
|
+
style: Platform3.OS === "android" ? [styles.webview, { opacity: 1 - androidRepaintTick % 2 * 1e-3 }] : styles.webview,
|
|
2218
2504
|
startInLoadingState: true,
|
|
2219
2505
|
injectedJavaScriptBeforeContentLoaded: getInjectedWebViewScript(),
|
|
2220
2506
|
onMessage: handleWebViewMessage,
|
|
@@ -2252,6 +2538,7 @@ function SagepilotChatProvider({
|
|
|
2252
2538
|
createElement(
|
|
2253
2539
|
View,
|
|
2254
2540
|
{ style: styles.sheetCard },
|
|
2541
|
+
createElement(View, { style: styles.sheetHandle }),
|
|
2255
2542
|
createElement(Text, { style: styles.sheetTitle }, "Add attachment"),
|
|
2256
2543
|
...(internalSagepilotChat.getConfig()?.filePicker?.sources ?? []).map(
|
|
2257
2544
|
(source) => createElement(
|
|
@@ -2259,19 +2546,29 @@ function SagepilotChatProvider({
|
|
|
2259
2546
|
{
|
|
2260
2547
|
key: source,
|
|
2261
2548
|
accessibilityRole: "button",
|
|
2262
|
-
|
|
2549
|
+
android_ripple: { color: "rgba(15, 23, 42, 0.06)" },
|
|
2550
|
+
hitSlop: 4,
|
|
2551
|
+
style: ({ pressed }) => getSheetOptionStyle(pressed),
|
|
2263
2552
|
onPress: () => handleSourceChoice(source)
|
|
2264
2553
|
},
|
|
2265
|
-
|
|
2554
|
+
renderFilePickerSourceIcon(source),
|
|
2555
|
+
createElement(
|
|
2556
|
+
View,
|
|
2557
|
+
{ style: styles.sheetOptionText },
|
|
2558
|
+
createElement(Text, { style: styles.sheetButtonText }, FILE_PICKER_SOURCE_LABELS[source]),
|
|
2559
|
+
createElement(Text, { style: styles.sheetButtonDescription }, FILE_PICKER_SOURCE_DESCRIPTIONS[source])
|
|
2560
|
+
)
|
|
2266
2561
|
)
|
|
2267
2562
|
),
|
|
2268
2563
|
createElement(
|
|
2269
2564
|
Pressable,
|
|
2270
2565
|
{
|
|
2271
2566
|
accessibilityRole: "button",
|
|
2272
|
-
|
|
2567
|
+
android_ripple: { color: "rgba(15, 23, 42, 0.06)" },
|
|
2568
|
+
style: ({ pressed }) => getSheetCancelStyle(pressed),
|
|
2273
2569
|
onPress: () => setSourceChooser(null)
|
|
2274
2570
|
},
|
|
2571
|
+
createElement(X, { color: "#334155", size: 18, strokeWidth: 2.5, style: styles.sheetCancelIcon }),
|
|
2275
2572
|
createElement(Text, { style: styles.sheetCancelText }, "Cancel")
|
|
2276
2573
|
)
|
|
2277
2574
|
)
|
|
@@ -2377,43 +2674,109 @@ var styles = StyleSheet.create({
|
|
|
2377
2674
|
},
|
|
2378
2675
|
sheetBackdrop: {
|
|
2379
2676
|
flex: 1,
|
|
2677
|
+
alignItems: "center",
|
|
2380
2678
|
justifyContent: "flex-end",
|
|
2381
|
-
backgroundColor: "rgba(
|
|
2679
|
+
backgroundColor: "rgba(15, 23, 42, 0.48)"
|
|
2382
2680
|
},
|
|
2383
2681
|
sheetCard: {
|
|
2682
|
+
width: "100%",
|
|
2683
|
+
maxWidth: 540,
|
|
2384
2684
|
backgroundColor: "#ffffff",
|
|
2385
|
-
borderTopLeftRadius:
|
|
2386
|
-
borderTopRightRadius:
|
|
2387
|
-
paddingTop:
|
|
2388
|
-
paddingBottom:
|
|
2389
|
-
paddingHorizontal:
|
|
2685
|
+
borderTopLeftRadius: 26,
|
|
2686
|
+
borderTopRightRadius: 26,
|
|
2687
|
+
paddingTop: 10,
|
|
2688
|
+
paddingBottom: 18,
|
|
2689
|
+
paddingHorizontal: 14,
|
|
2690
|
+
shadowColor: "#0f172a",
|
|
2691
|
+
shadowOpacity: 0.22,
|
|
2692
|
+
shadowRadius: 28,
|
|
2693
|
+
shadowOffset: { width: 0, height: -10 },
|
|
2694
|
+
elevation: 18
|
|
2695
|
+
},
|
|
2696
|
+
sheetHandle: {
|
|
2697
|
+
alignSelf: "center",
|
|
2698
|
+
width: 42,
|
|
2699
|
+
height: 5,
|
|
2700
|
+
borderRadius: 999,
|
|
2701
|
+
backgroundColor: "#d1d5db",
|
|
2702
|
+
marginBottom: 14
|
|
2390
2703
|
},
|
|
2391
2704
|
sheetTitle: {
|
|
2392
|
-
|
|
2393
|
-
|
|
2394
|
-
|
|
2395
|
-
|
|
2396
|
-
|
|
2705
|
+
color: "#111827",
|
|
2706
|
+
fontSize: 17,
|
|
2707
|
+
fontWeight: "700",
|
|
2708
|
+
paddingBottom: 12,
|
|
2709
|
+
paddingHorizontal: 4
|
|
2710
|
+
},
|
|
2711
|
+
sheetOption: {
|
|
2712
|
+
minHeight: 70,
|
|
2713
|
+
flexDirection: "row",
|
|
2714
|
+
alignItems: "center",
|
|
2715
|
+
borderRadius: 18,
|
|
2716
|
+
paddingHorizontal: 12,
|
|
2717
|
+
marginBottom: 6,
|
|
2718
|
+
backgroundColor: "#ffffff"
|
|
2719
|
+
},
|
|
2720
|
+
sheetOptionPressed: {
|
|
2721
|
+
backgroundColor: "#f8fafc",
|
|
2722
|
+
transform: [{ scale: 0.985 }]
|
|
2397
2723
|
},
|
|
2398
|
-
|
|
2399
|
-
|
|
2724
|
+
sheetOptionIcon: {
|
|
2725
|
+
width: 46,
|
|
2726
|
+
height: 46,
|
|
2727
|
+
borderRadius: 16,
|
|
2728
|
+
marginRight: 14,
|
|
2400
2729
|
alignItems: "center",
|
|
2401
2730
|
justifyContent: "center",
|
|
2402
|
-
|
|
2731
|
+
position: "relative",
|
|
2732
|
+
overflow: "hidden"
|
|
2733
|
+
},
|
|
2734
|
+
sheetOptionText: {
|
|
2735
|
+
flex: 1,
|
|
2736
|
+
justifyContent: "center"
|
|
2403
2737
|
},
|
|
2404
2738
|
sheetButtonText: {
|
|
2405
|
-
color: "#
|
|
2739
|
+
color: "#0f172a",
|
|
2406
2740
|
fontSize: 16,
|
|
2407
|
-
fontWeight: "
|
|
2741
|
+
fontWeight: "700"
|
|
2742
|
+
},
|
|
2743
|
+
sheetButtonDescription: {
|
|
2744
|
+
color: "#64748b",
|
|
2745
|
+
fontSize: 13,
|
|
2746
|
+
fontWeight: "500",
|
|
2747
|
+
marginTop: 3
|
|
2748
|
+
},
|
|
2749
|
+
cameraOptionIcon: {
|
|
2750
|
+
backgroundColor: "#e0f2fe"
|
|
2751
|
+
},
|
|
2752
|
+
libraryOptionIcon: {
|
|
2753
|
+
backgroundColor: "#ecfdf5"
|
|
2754
|
+
},
|
|
2755
|
+
documentsOptionIcon: {
|
|
2756
|
+
backgroundColor: "#f5f3ff"
|
|
2408
2757
|
},
|
|
2409
2758
|
sheetCancelButton: {
|
|
2410
|
-
|
|
2411
|
-
|
|
2759
|
+
minHeight: 56,
|
|
2760
|
+
flexDirection: "row",
|
|
2761
|
+
alignItems: "center",
|
|
2762
|
+
justifyContent: "center",
|
|
2763
|
+
borderRadius: 18,
|
|
2764
|
+
marginTop: 8,
|
|
2765
|
+
backgroundColor: "#f1f5f9"
|
|
2766
|
+
},
|
|
2767
|
+
sheetCancelButtonPressed: {
|
|
2768
|
+
backgroundColor: "#e2e8f0",
|
|
2769
|
+
transform: [{ scale: 0.985 }]
|
|
2770
|
+
},
|
|
2771
|
+
sheetCancelIcon: {
|
|
2772
|
+
width: 18,
|
|
2773
|
+
height: 18,
|
|
2774
|
+
marginRight: 8
|
|
2412
2775
|
},
|
|
2413
2776
|
sheetCancelText: {
|
|
2414
|
-
color: "#
|
|
2777
|
+
color: "#0f172a",
|
|
2415
2778
|
fontSize: 16,
|
|
2416
|
-
fontWeight: "
|
|
2779
|
+
fontWeight: "700"
|
|
2417
2780
|
}
|
|
2418
2781
|
});
|
|
2419
2782
|
|
|
@@ -2467,6 +2830,9 @@ function useSagepilotChat() {
|
|
|
2467
2830
|
};
|
|
2468
2831
|
}
|
|
2469
2832
|
export {
|
|
2833
|
+
SAGEPILOT_DEFAULT_IMAGE_MAX_DIMENSION,
|
|
2834
|
+
SAGEPILOT_DEFAULT_IMAGE_MAX_FILE_SIZE_BYTES,
|
|
2835
|
+
SAGEPILOT_DEFAULT_IMAGE_QUALITY,
|
|
2470
2836
|
SagepilotChat,
|
|
2471
2837
|
SagepilotChatError,
|
|
2472
2838
|
SagepilotChatProvider,
|
|
@@ -2475,5 +2841,7 @@ export {
|
|
|
2475
2841
|
createKeychainTokenStorage,
|
|
2476
2842
|
createSagepilotFilePicker,
|
|
2477
2843
|
createSagepilotFileStore,
|
|
2844
|
+
ensureSagepilotAndroidCameraPermission,
|
|
2845
|
+
promptOpenSagepilotCameraSettings,
|
|
2478
2846
|
useSagepilotChat
|
|
2479
2847
|
};
|