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