@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/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.2.5";
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
- 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;
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 ensureAndroidCameraPermission() {
1254
- if (Platform.OS !== "android") return;
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", "library");
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
- if (!imagePicker) return [];
1363
- await ensureAndroidCameraPermission();
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 Platform2,
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 = { ...capabilities, filePickerProtocol: FILE_PICKER_PROTOCOL_VERSION };
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
- var mobileWebViewBridgeScript = `
1667
+ function buildMobileWebViewBridgeScript(capabilities) {
1668
+ const payload = buildBridgeCapabilitiesPayload(capabilities);
1669
+ return `
1502
1670
  (function () {
1503
- if (window.__sagepilotRnBridgeInstalled) return true;
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
- mobileWebViewBridgeScript,
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(files, batchId) {
1651
- return buildDispatchScript(`{ type: "sagepilot:files_picked", batch_id: ${JSON.stringify(batchId)}, files: ${JSON.stringify(files)} }`);
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
- ...Platform2.OS === "ios" ? {
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 = Platform2.OS === "android" ? SagepilotInsetsViewNativeComponent_default : View;
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.files, batch.batchId));
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 = { batchId, files, storageKeys };
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) return;
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) return;
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 (Platform2.OS !== "android") return;
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 = 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" ? {
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
- Platform2.OS === "android" ? {
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
- Linking.openURL(url).catch(() => void 0);
2387
+ Linking2.openURL(url).catch(() => void 0);
2102
2388
  return false;
2103
2389
  }, [state.conversationUrl, state.preloadUrl]);
2104
2390
  useEffect(() => {
2105
- if (Platform2.OS !== "web" || typeof window === "undefined") return;
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 (Platform2.OS !== "web" || !state.isPresented) return;
2402
+ if (Platform3.OS !== "web" || !state.isPresented) return;
2117
2403
  postIdentityToWebFrame();
2118
2404
  }, [state.isPresented, state.conversationUrl, state.configured]);
2119
2405
  useEffect(() => {
2120
- if (Platform2.OS === "web" || !state.isPresented) return;
2406
+ if (Platform3.OS === "web" || !state.isPresented) return;
2121
2407
  postIdentityToNativeWebView();
2122
2408
  }, [state.isPresented, state.conversationUrl, state.configured]);
2123
- if (Platform2.OS === "web") {
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: Platform2.OS === "android",
2186
- navigationBarTranslucent: Platform2.OS === "android",
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: Platform2.OS === "android" ? [styles.webview, { opacity: 1 - androidRepaintTick % 2 * 1e-3 }] : styles.webview,
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
- style: styles.sheetButton,
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
- createElement(Text, { style: styles.sheetButtonText }, FILE_PICKER_SOURCE_LABELS[source])
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
- style: [styles.sheetButton, styles.sheetCancelButton],
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(17, 24, 39, 0.36)"
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: 16,
2386
- borderTopRightRadius: 16,
2387
- paddingTop: 8,
2388
- paddingBottom: 24,
2389
- paddingHorizontal: 8
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
- textAlign: "center",
2393
- color: "#6b7280",
2394
- fontSize: 13,
2395
- fontWeight: "600",
2396
- paddingVertical: 10
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
- sheetButton: {
2399
- minHeight: 52,
2724
+ sheetOptionIcon: {
2725
+ width: 46,
2726
+ height: 46,
2727
+ borderRadius: 16,
2728
+ marginRight: 14,
2400
2729
  alignItems: "center",
2401
2730
  justifyContent: "center",
2402
- borderRadius: 12
2731
+ position: "relative",
2732
+ overflow: "hidden"
2733
+ },
2734
+ sheetOptionText: {
2735
+ flex: 1,
2736
+ justifyContent: "center"
2403
2737
  },
2404
2738
  sheetButtonText: {
2405
- color: "#111827",
2739
+ color: "#0f172a",
2406
2740
  fontSize: 16,
2407
- fontWeight: "500"
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
- marginTop: 6,
2411
- backgroundColor: "#f3f4f6"
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: "#111827",
2777
+ color: "#0f172a",
2415
2778
  fontSize: 16,
2416
- fontWeight: "600"
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
  };