@reverbia/sdk 1.0.0-next.20251217123222 → 1.0.0-next.20251217144159

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.
@@ -1440,20 +1440,42 @@ Please inform the user about this issue and try to help them alternatively.`
1440
1440
  }
1441
1441
  });
1442
1442
  const accumulator = createStreamAccumulator();
1443
- for await (const chunk of sseResult.stream) {
1444
- if (isDoneMarker(chunk)) {
1445
- continue;
1446
- }
1447
- if (chunk && typeof chunk === "object") {
1448
- const contentDelta = processStreamingChunk(
1449
- chunk,
1450
- accumulator
1451
- );
1452
- if (contentDelta) {
1453
- if (onData) onData(contentDelta);
1454
- if (globalOnData) globalOnData(contentDelta);
1443
+ try {
1444
+ for await (const chunk of sseResult.stream) {
1445
+ if (isDoneMarker(chunk)) {
1446
+ continue;
1447
+ }
1448
+ if (chunk && typeof chunk === "object") {
1449
+ const contentDelta = processStreamingChunk(
1450
+ chunk,
1451
+ accumulator
1452
+ );
1453
+ if (contentDelta) {
1454
+ if (onData) onData(contentDelta);
1455
+ if (globalOnData) globalOnData(contentDelta);
1456
+ }
1455
1457
  }
1456
1458
  }
1459
+ } catch (streamErr) {
1460
+ if (isAbortError(streamErr) || abortController.signal.aborted) {
1461
+ setIsLoading(false);
1462
+ const partialCompletion = buildCompletionResponse(accumulator);
1463
+ return {
1464
+ data: partialCompletion,
1465
+ error: "Request aborted",
1466
+ toolExecution: toolExecutionResult
1467
+ };
1468
+ }
1469
+ throw streamErr;
1470
+ }
1471
+ if (abortController.signal.aborted) {
1472
+ setIsLoading(false);
1473
+ const partialCompletion = buildCompletionResponse(accumulator);
1474
+ return {
1475
+ data: partialCompletion,
1476
+ error: "Request aborted",
1477
+ toolExecution: toolExecutionResult
1478
+ };
1457
1479
  }
1458
1480
  if (sseError) {
1459
1481
  throw sseError;
@@ -1678,7 +1700,8 @@ function messageToStored(message) {
1678
1700
  embeddingModel: message.embeddingModel,
1679
1701
  usage: message.usage,
1680
1702
  sources: message.sources,
1681
- responseDuration: message.responseDuration
1703
+ responseDuration: message.responseDuration,
1704
+ wasStopped: message.wasStopped
1682
1705
  };
1683
1706
  }
1684
1707
  function conversationToStored(conversation) {
@@ -1767,6 +1790,7 @@ async function createMessageOp(ctx, opts) {
1767
1790
  msg._setRaw("response_duration", opts.responseDuration);
1768
1791
  if (opts.vector) msg._setRaw("vector", JSON.stringify(opts.vector));
1769
1792
  if (opts.embeddingModel) msg._setRaw("embedding_model", opts.embeddingModel);
1793
+ if (opts.wasStopped) msg._setRaw("was_stopped", opts.wasStopped);
1770
1794
  });
1771
1795
  });
1772
1796
  return messageToStored(created);
@@ -2057,6 +2081,45 @@ function useChatStorage(options) {
2057
2081
  });
2058
2082
  const responseDuration = (Date.now() - startTime) / 1e3;
2059
2083
  if (result.error || !result.data) {
2084
+ const abortedResult = result;
2085
+ if (abortedResult.error === "Request aborted") {
2086
+ const assistantContent2 = abortedResult.data?.choices?.[0]?.message?.content?.map((part) => part.text || "").join("") || "";
2087
+ const responseModel = abortedResult.data?.model || model || "";
2088
+ let storedAssistantMessage2;
2089
+ try {
2090
+ storedAssistantMessage2 = await createMessageOp(storageCtx, {
2091
+ conversationId: convId,
2092
+ role: "assistant",
2093
+ content: assistantContent2,
2094
+ model: responseModel,
2095
+ usage: convertUsageToStored(abortedResult.data?.usage),
2096
+ responseDuration,
2097
+ wasStopped: true
2098
+ });
2099
+ const completionData = abortedResult.data || {
2100
+ id: `aborted-${Date.now()}`,
2101
+ model: responseModel,
2102
+ choices: [{
2103
+ index: 0,
2104
+ message: {
2105
+ role: "assistant",
2106
+ content: [{ type: "text", text: assistantContent2 }]
2107
+ },
2108
+ finish_reason: "stop"
2109
+ }],
2110
+ usage: void 0
2111
+ };
2112
+ return {
2113
+ data: completionData,
2114
+ error: null,
2115
+ // Treat as success to the caller
2116
+ toolExecution: abortedResult.toolExecution,
2117
+ userMessage: storedUserMessage,
2118
+ assistantMessage: storedAssistantMessage2
2119
+ };
2120
+ } catch (err) {
2121
+ }
2122
+ }
2060
2123
  return {
2061
2124
  data: null,
2062
2125
  error: result.error || "No response data received",
@@ -2128,8 +2191,9 @@ function useChatStorage(options) {
2128
2191
 
2129
2192
  // src/lib/chatStorage/schema.ts
2130
2193
  import { appSchema, tableSchema } from "@nozbe/watermelondb";
2194
+ import { schemaMigrations, addColumns } from "@nozbe/watermelondb/Schema/migrations";
2131
2195
  var chatStorageSchema = appSchema({
2132
- version: 1,
2196
+ version: 2,
2133
2197
  tables: [
2134
2198
  tableSchema({
2135
2199
  name: "history",
@@ -2152,7 +2216,8 @@ var chatStorageSchema = appSchema({
2152
2216
  // JSON stringified ChatCompletionUsage
2153
2217
  { name: "sources", type: "string", isOptional: true },
2154
2218
  // JSON stringified SearchSource[]
2155
- { name: "response_duration", type: "number", isOptional: true }
2219
+ { name: "response_duration", type: "number", isOptional: true },
2220
+ { name: "was_stopped", type: "boolean", isOptional: true }
2156
2221
  ]
2157
2222
  }),
2158
2223
  tableSchema({
@@ -2167,6 +2232,21 @@ var chatStorageSchema = appSchema({
2167
2232
  })
2168
2233
  ]
2169
2234
  });
2235
+ var chatStorageMigrations = schemaMigrations({
2236
+ migrations: [
2237
+ {
2238
+ toVersion: 2,
2239
+ steps: [
2240
+ addColumns({
2241
+ table: "history",
2242
+ columns: [
2243
+ { name: "was_stopped", type: "boolean", isOptional: true }
2244
+ ]
2245
+ })
2246
+ ]
2247
+ }
2248
+ ]
2249
+ });
2170
2250
 
2171
2251
  // src/lib/chatStorage/models.ts
2172
2252
  import { Model } from "@nozbe/watermelondb";
@@ -2250,6 +2330,10 @@ var Message = class extends Model {
2250
2330
  const value = this._getRaw("response_duration");
2251
2331
  return value !== null && value !== void 0 ? value : void 0;
2252
2332
  }
2333
+ /** Whether the message generation was stopped by the user */
2334
+ get wasStopped() {
2335
+ return this._getRaw("was_stopped");
2336
+ }
2253
2337
  };
2254
2338
  Message.table = "history";
2255
2339
  Message.associations = {
@@ -3452,8 +3536,180 @@ var Memory = class extends Model2 {
3452
3536
  };
3453
3537
  Memory.table = "memories";
3454
3538
 
3539
+ // src/react/useSettings.ts
3540
+ import { useCallback as useCallback4, useState as useState4, useMemo as useMemo3, useEffect as useEffect2 } from "react";
3541
+
3542
+ // src/lib/settingsStorage/operations.ts
3543
+ import { Q as Q3 } from "@nozbe/watermelondb";
3544
+ function modelPreferenceToStored(preference) {
3545
+ return {
3546
+ uniqueId: preference.id,
3547
+ walletAddress: preference.walletAddress,
3548
+ models: preference.models
3549
+ };
3550
+ }
3551
+ async function getModelPreferenceOp(ctx, walletAddress) {
3552
+ const results = await ctx.modelPreferencesCollection.query(Q3.where("wallet_address", walletAddress)).fetch();
3553
+ return results.length > 0 ? modelPreferenceToStored(results[0]) : null;
3554
+ }
3555
+ async function setModelPreferenceOp(ctx, walletAddress, models) {
3556
+ const result = await ctx.database.write(async () => {
3557
+ const results = await ctx.modelPreferencesCollection.query(Q3.where("wallet_address", walletAddress)).fetch();
3558
+ if (results.length > 0) {
3559
+ const preference = results[0];
3560
+ await preference.update((pref) => {
3561
+ if (models !== void 0) {
3562
+ pref._setRaw("models", models || null);
3563
+ }
3564
+ });
3565
+ return preference;
3566
+ }
3567
+ return await ctx.modelPreferencesCollection.create((pref) => {
3568
+ pref._setRaw("wallet_address", walletAddress);
3569
+ if (models) pref._setRaw("models", models);
3570
+ });
3571
+ });
3572
+ return modelPreferenceToStored(result);
3573
+ }
3574
+ async function deleteModelPreferenceOp(ctx, walletAddress) {
3575
+ const results = await ctx.modelPreferencesCollection.query(Q3.where("wallet_address", walletAddress)).fetch();
3576
+ if (results.length === 0) return false;
3577
+ await ctx.database.write(async () => {
3578
+ await results[0].destroyPermanently();
3579
+ });
3580
+ return true;
3581
+ }
3582
+
3583
+ // src/react/useSettings.ts
3584
+ function useSettings(options) {
3585
+ const { database, walletAddress } = options;
3586
+ const [modelPreference, setModelPreferenceState] = useState4(null);
3587
+ const [isLoading, setIsLoading] = useState4(false);
3588
+ const modelPreferencesCollection = useMemo3(
3589
+ () => database.get("modelPreferences"),
3590
+ [database]
3591
+ );
3592
+ const storageCtx = useMemo3(
3593
+ () => ({
3594
+ database,
3595
+ modelPreferencesCollection
3596
+ }),
3597
+ [database, modelPreferencesCollection]
3598
+ );
3599
+ const getModelPreference = useCallback4(
3600
+ async (address) => {
3601
+ try {
3602
+ if (!address) throw new Error("Wallet address is required");
3603
+ const result = await getModelPreferenceOp(storageCtx, address);
3604
+ return result;
3605
+ } catch (error) {
3606
+ throw new Error(
3607
+ error instanceof Error ? error.message : "An unknown error occurred"
3608
+ );
3609
+ }
3610
+ },
3611
+ [storageCtx]
3612
+ );
3613
+ const setModelPreference = useCallback4(
3614
+ async (address, models) => {
3615
+ try {
3616
+ if (!address) throw new Error("Wallet address is required");
3617
+ const result = await setModelPreferenceOp(storageCtx, address, models);
3618
+ if (walletAddress && address === walletAddress) {
3619
+ setModelPreferenceState(result);
3620
+ }
3621
+ return result;
3622
+ } catch (error) {
3623
+ throw new Error(
3624
+ error instanceof Error ? error.message : "An unknown error occurred"
3625
+ );
3626
+ }
3627
+ },
3628
+ [storageCtx, walletAddress]
3629
+ );
3630
+ const deleteModelPreference = useCallback4(
3631
+ async (address) => {
3632
+ try {
3633
+ if (!address) throw new Error("Wallet address is required");
3634
+ const deleted = await deleteModelPreferenceOp(storageCtx, address);
3635
+ if (deleted && walletAddress && address === walletAddress) {
3636
+ setModelPreferenceState(null);
3637
+ }
3638
+ return deleted;
3639
+ } catch (error) {
3640
+ throw new Error(
3641
+ error instanceof Error ? error.message : "An unknown error occurred"
3642
+ );
3643
+ }
3644
+ },
3645
+ [storageCtx, walletAddress]
3646
+ );
3647
+ useEffect2(() => {
3648
+ if (!walletAddress) {
3649
+ setModelPreferenceState(null);
3650
+ return;
3651
+ }
3652
+ let cancelled = false;
3653
+ const loadPreference = async () => {
3654
+ setIsLoading(true);
3655
+ try {
3656
+ const preference = await getModelPreference(walletAddress);
3657
+ if (!cancelled) {
3658
+ setModelPreferenceState(preference);
3659
+ }
3660
+ } finally {
3661
+ if (!cancelled) {
3662
+ setIsLoading(false);
3663
+ }
3664
+ }
3665
+ };
3666
+ loadPreference();
3667
+ return () => {
3668
+ cancelled = true;
3669
+ };
3670
+ }, [walletAddress, getModelPreference]);
3671
+ return {
3672
+ modelPreference,
3673
+ isLoading,
3674
+ getModelPreference,
3675
+ setModelPreference,
3676
+ deleteModelPreference
3677
+ };
3678
+ }
3679
+
3680
+ // src/lib/settingsStorage/schema.ts
3681
+ import { appSchema as appSchema3, tableSchema as tableSchema3 } from "@nozbe/watermelondb";
3682
+ var settingsStorageSchema = appSchema3({
3683
+ version: 1,
3684
+ tables: [
3685
+ tableSchema3({
3686
+ name: "modelPreferences",
3687
+ columns: [
3688
+ { name: "wallet_address", type: "string", isIndexed: true },
3689
+ { name: "models", type: "string", isOptional: true }
3690
+ // stored as JSON stringified ModelPreference[]
3691
+ ]
3692
+ })
3693
+ ]
3694
+ });
3695
+
3696
+ // src/lib/settingsStorage/models.ts
3697
+ import { Model as Model3 } from "@nozbe/watermelondb";
3698
+ var ModelPreference = class extends Model3 {
3699
+ /** User's wallet address */
3700
+ get walletAddress() {
3701
+ return this._getRaw("wallet_address");
3702
+ }
3703
+ /** Preferred model identifier */
3704
+ get models() {
3705
+ const value = this._getRaw("models");
3706
+ return value ? value : void 0;
3707
+ }
3708
+ };
3709
+ ModelPreference.table = "modelPreferences";
3710
+
3455
3711
  // src/react/usePdf.ts
3456
- import { useCallback as useCallback4, useState as useState4 } from "react";
3712
+ import { useCallback as useCallback5, useState as useState5 } from "react";
3457
3713
 
3458
3714
  // src/lib/pdf.ts
3459
3715
  import * as pdfjs from "pdfjs-dist";
@@ -3506,9 +3762,9 @@ async function convertPdfToImages(pdfDataUrl) {
3506
3762
  // src/react/usePdf.ts
3507
3763
  var PDF_MIME_TYPE = "application/pdf";
3508
3764
  function usePdf() {
3509
- const [isProcessing, setIsProcessing] = useState4(false);
3510
- const [error, setError] = useState4(null);
3511
- const extractPdfContext = useCallback4(
3765
+ const [isProcessing, setIsProcessing] = useState5(false);
3766
+ const [error, setError] = useState5(null);
3767
+ const extractPdfContext = useCallback5(
3512
3768
  async (files) => {
3513
3769
  setIsProcessing(true);
3514
3770
  setError(null);
@@ -3555,12 +3811,12 @@ ${text}`;
3555
3811
  }
3556
3812
 
3557
3813
  // src/react/useOCR.ts
3558
- import { useCallback as useCallback5, useState as useState5 } from "react";
3814
+ import { useCallback as useCallback6, useState as useState6 } from "react";
3559
3815
  import Tesseract from "tesseract.js";
3560
3816
  function useOCR() {
3561
- const [isProcessing, setIsProcessing] = useState5(false);
3562
- const [error, setError] = useState5(null);
3563
- const extractOCRContext = useCallback5(
3817
+ const [isProcessing, setIsProcessing] = useState6(false);
3818
+ const [error, setError] = useState6(null);
3819
+ const extractOCRContext = useCallback6(
3564
3820
  async (files) => {
3565
3821
  setIsProcessing(true);
3566
3822
  setError(null);
@@ -3646,22 +3902,22 @@ ${text}`;
3646
3902
  }
3647
3903
 
3648
3904
  // src/react/useModels.ts
3649
- import { useCallback as useCallback6, useEffect as useEffect2, useRef as useRef3, useState as useState6 } from "react";
3905
+ import { useCallback as useCallback7, useEffect as useEffect3, useRef as useRef3, useState as useState7 } from "react";
3650
3906
  function useModels(options = {}) {
3651
3907
  const { getToken, baseUrl = BASE_URL, provider, autoFetch = true } = options;
3652
- const [models, setModels] = useState6([]);
3653
- const [isLoading, setIsLoading] = useState6(false);
3654
- const [error, setError] = useState6(null);
3908
+ const [models, setModels] = useState7([]);
3909
+ const [isLoading, setIsLoading] = useState7(false);
3910
+ const [error, setError] = useState7(null);
3655
3911
  const getTokenRef = useRef3(getToken);
3656
3912
  const baseUrlRef = useRef3(baseUrl);
3657
3913
  const providerRef = useRef3(provider);
3658
3914
  const abortControllerRef = useRef3(null);
3659
- useEffect2(() => {
3915
+ useEffect3(() => {
3660
3916
  getTokenRef.current = getToken;
3661
3917
  baseUrlRef.current = baseUrl;
3662
3918
  providerRef.current = provider;
3663
3919
  });
3664
- useEffect2(() => {
3920
+ useEffect3(() => {
3665
3921
  return () => {
3666
3922
  if (abortControllerRef.current) {
3667
3923
  abortControllerRef.current.abort();
@@ -3669,7 +3925,7 @@ function useModels(options = {}) {
3669
3925
  }
3670
3926
  };
3671
3927
  }, []);
3672
- const fetchModels = useCallback6(async () => {
3928
+ const fetchModels = useCallback7(async () => {
3673
3929
  if (abortControllerRef.current) {
3674
3930
  abortControllerRef.current.abort();
3675
3931
  }
@@ -3727,12 +3983,12 @@ function useModels(options = {}) {
3727
3983
  }
3728
3984
  }
3729
3985
  }, []);
3730
- const refetch = useCallback6(async () => {
3986
+ const refetch = useCallback7(async () => {
3731
3987
  setModels([]);
3732
3988
  await fetchModels();
3733
3989
  }, [fetchModels]);
3734
3990
  const hasFetchedRef = useRef3(false);
3735
- useEffect2(() => {
3991
+ useEffect3(() => {
3736
3992
  if (autoFetch && !hasFetchedRef.current) {
3737
3993
  hasFetchedRef.current = true;
3738
3994
  fetchModels();
@@ -3750,15 +4006,15 @@ function useModels(options = {}) {
3750
4006
  }
3751
4007
 
3752
4008
  // src/react/useSearch.ts
3753
- import { useCallback as useCallback7, useEffect as useEffect3, useRef as useRef4, useState as useState7 } from "react";
4009
+ import { useCallback as useCallback8, useEffect as useEffect4, useRef as useRef4, useState as useState8 } from "react";
3754
4010
  function useSearch(options = {}) {
3755
4011
  const { getToken, baseUrl = BASE_URL, onError } = options;
3756
- const [isLoading, setIsLoading] = useState7(false);
3757
- const [results, setResults] = useState7(null);
3758
- const [response, setResponse] = useState7(null);
3759
- const [error, setError] = useState7(null);
4012
+ const [isLoading, setIsLoading] = useState8(false);
4013
+ const [results, setResults] = useState8(null);
4014
+ const [response, setResponse] = useState8(null);
4015
+ const [error, setError] = useState8(null);
3760
4016
  const abortControllerRef = useRef4(null);
3761
- useEffect3(() => {
4017
+ useEffect4(() => {
3762
4018
  return () => {
3763
4019
  if (abortControllerRef.current) {
3764
4020
  abortControllerRef.current.abort();
@@ -3766,7 +4022,7 @@ function useSearch(options = {}) {
3766
4022
  }
3767
4023
  };
3768
4024
  }, []);
3769
- const search = useCallback7(
4025
+ const search = useCallback8(
3770
4026
  async (query, searchOptions = {}) => {
3771
4027
  if (abortControllerRef.current) {
3772
4028
  abortControllerRef.current.abort();
@@ -3834,12 +4090,12 @@ function useSearch(options = {}) {
3834
4090
  }
3835
4091
 
3836
4092
  // src/react/useImageGeneration.ts
3837
- import { useCallback as useCallback8, useEffect as useEffect4, useRef as useRef5, useState as useState8 } from "react";
4093
+ import { useCallback as useCallback9, useEffect as useEffect5, useRef as useRef5, useState as useState9 } from "react";
3838
4094
  function useImageGeneration(options = {}) {
3839
4095
  const { getToken, baseUrl = BASE_URL, onFinish, onError } = options;
3840
- const [isLoading, setIsLoading] = useState8(false);
4096
+ const [isLoading, setIsLoading] = useState9(false);
3841
4097
  const abortControllerRef = useRef5(null);
3842
- useEffect4(() => {
4098
+ useEffect5(() => {
3843
4099
  return () => {
3844
4100
  if (abortControllerRef.current) {
3845
4101
  abortControllerRef.current.abort();
@@ -3847,13 +4103,13 @@ function useImageGeneration(options = {}) {
3847
4103
  }
3848
4104
  };
3849
4105
  }, []);
3850
- const stop = useCallback8(() => {
4106
+ const stop = useCallback9(() => {
3851
4107
  if (abortControllerRef.current) {
3852
4108
  abortControllerRef.current.abort();
3853
4109
  abortControllerRef.current = null;
3854
4110
  }
3855
4111
  }, []);
3856
- const generateImage = useCallback8(
4112
+ const generateImage = useCallback9(
3857
4113
  async (args) => {
3858
4114
  if (abortControllerRef.current) {
3859
4115
  abortControllerRef.current.abort();
@@ -3969,12 +4225,875 @@ var extractConversationContext = (messages, maxMessages = 3) => {
3969
4225
  const userMessages = messages.filter((msg) => msg.role === "user").slice(-maxMessages).map((msg) => msg.content).join(" ");
3970
4226
  return userMessages.trim();
3971
4227
  };
4228
+
4229
+ // src/react/useDropboxBackup.ts
4230
+ import { useCallback as useCallback11, useMemo as useMemo4 } from "react";
4231
+
4232
+ // src/lib/backup/dropbox/api.ts
4233
+ var DROPBOX_API_URL = "https://api.dropboxapi.com/2";
4234
+ var DROPBOX_CONTENT_URL = "https://content.dropboxapi.com/2";
4235
+ var DEFAULT_BACKUP_FOLDER = "/ai-chat-app/conversations";
4236
+ async function ensureBackupFolder(accessToken, folder = DEFAULT_BACKUP_FOLDER) {
4237
+ try {
4238
+ await fetch(`${DROPBOX_API_URL}/files/create_folder_v2`, {
4239
+ method: "POST",
4240
+ headers: {
4241
+ Authorization: `Bearer ${accessToken}`,
4242
+ "Content-Type": "application/json"
4243
+ },
4244
+ body: JSON.stringify({
4245
+ path: folder,
4246
+ autorename: false
4247
+ })
4248
+ });
4249
+ } catch {
4250
+ }
4251
+ }
4252
+ async function uploadFileToDropbox(accessToken, filename, content, folder = DEFAULT_BACKUP_FOLDER) {
4253
+ await ensureBackupFolder(accessToken, folder);
4254
+ const path = `${folder}/${filename}`;
4255
+ const response = await fetch(`${DROPBOX_CONTENT_URL}/files/upload`, {
4256
+ method: "POST",
4257
+ headers: {
4258
+ Authorization: `Bearer ${accessToken}`,
4259
+ "Content-Type": "application/octet-stream",
4260
+ "Dropbox-API-Arg": JSON.stringify({
4261
+ path,
4262
+ mode: "overwrite",
4263
+ autorename: false,
4264
+ mute: true
4265
+ })
4266
+ },
4267
+ body: content
4268
+ });
4269
+ if (!response.ok) {
4270
+ const errorText = await response.text();
4271
+ throw new Error(`Dropbox upload failed: ${response.status} - ${errorText}`);
4272
+ }
4273
+ return response.json();
4274
+ }
4275
+ async function listDropboxFiles(accessToken, folder = DEFAULT_BACKUP_FOLDER) {
4276
+ await ensureBackupFolder(accessToken, folder);
4277
+ const response = await fetch(`${DROPBOX_API_URL}/files/list_folder`, {
4278
+ method: "POST",
4279
+ headers: {
4280
+ Authorization: `Bearer ${accessToken}`,
4281
+ "Content-Type": "application/json"
4282
+ },
4283
+ body: JSON.stringify({
4284
+ path: folder,
4285
+ recursive: false,
4286
+ include_deleted: false
4287
+ })
4288
+ });
4289
+ if (!response.ok) {
4290
+ const error = await response.json();
4291
+ if (error.error?.path?.[".tag"] === "not_found") {
4292
+ return [];
4293
+ }
4294
+ throw new Error(`Dropbox list failed: ${error.error_summary}`);
4295
+ }
4296
+ let data = await response.json();
4297
+ const allEntries = [...data.entries];
4298
+ while (data.has_more) {
4299
+ const continueResponse = await fetch(`${DROPBOX_API_URL}/files/list_folder/continue`, {
4300
+ method: "POST",
4301
+ headers: {
4302
+ Authorization: `Bearer ${accessToken}`,
4303
+ "Content-Type": "application/json"
4304
+ },
4305
+ body: JSON.stringify({
4306
+ cursor: data.cursor
4307
+ })
4308
+ });
4309
+ if (!continueResponse.ok) {
4310
+ const errorText = await continueResponse.text();
4311
+ throw new Error(`Dropbox list continue failed: ${continueResponse.status} - ${errorText}`);
4312
+ }
4313
+ data = await continueResponse.json();
4314
+ allEntries.push(...data.entries);
4315
+ }
4316
+ const files = allEntries.filter((entry) => entry[".tag"] === "file").map((entry) => ({
4317
+ id: entry.id,
4318
+ name: entry.name,
4319
+ path_lower: entry.path_lower,
4320
+ path_display: entry.path_display,
4321
+ client_modified: entry.client_modified || "",
4322
+ server_modified: entry.server_modified || "",
4323
+ size: entry.size || 0
4324
+ }));
4325
+ return files;
4326
+ }
4327
+ async function downloadDropboxFile(accessToken, path) {
4328
+ const response = await fetch(`${DROPBOX_CONTENT_URL}/files/download`, {
4329
+ method: "POST",
4330
+ headers: {
4331
+ Authorization: `Bearer ${accessToken}`,
4332
+ "Dropbox-API-Arg": JSON.stringify({ path })
4333
+ }
4334
+ });
4335
+ if (!response.ok) {
4336
+ throw new Error(`Dropbox download failed: ${response.status}`);
4337
+ }
4338
+ return response.blob();
4339
+ }
4340
+ async function findDropboxFile(accessToken, filename, folder = DEFAULT_BACKUP_FOLDER) {
4341
+ const files = await listDropboxFiles(accessToken, folder);
4342
+ return files.find((f) => f.name === filename) || null;
4343
+ }
4344
+
4345
+ // src/lib/backup/dropbox/backup.ts
4346
+ var isAuthError = (err) => err instanceof Error && (err.message.includes("401") || err.message.includes("invalid_access_token"));
4347
+ async function pushConversationToDropbox(database, conversationId, userAddress, token, deps, backupFolder = DEFAULT_BACKUP_FOLDER, _retried = false) {
4348
+ try {
4349
+ await deps.requestEncryptionKey(userAddress);
4350
+ const filename = `${conversationId}.json`;
4351
+ const existingFile = await findDropboxFile(token, filename, backupFolder);
4352
+ if (existingFile) {
4353
+ const { Q: Q4 } = await import("@nozbe/watermelondb");
4354
+ const conversationsCollection = database.get("conversations");
4355
+ const records = await conversationsCollection.query(Q4.where("conversation_id", conversationId)).fetch();
4356
+ if (records.length > 0) {
4357
+ const conversation = conversationToStored(records[0]);
4358
+ const localUpdated = conversation.updatedAt.getTime();
4359
+ const remoteModified = new Date(existingFile.server_modified).getTime();
4360
+ if (localUpdated <= remoteModified) {
4361
+ return "skipped";
4362
+ }
4363
+ }
4364
+ }
4365
+ const exportResult = await deps.exportConversation(conversationId, userAddress);
4366
+ if (!exportResult.success || !exportResult.blob) {
4367
+ return "failed";
4368
+ }
4369
+ await uploadFileToDropbox(token, filename, exportResult.blob, backupFolder);
4370
+ return "uploaded";
4371
+ } catch (err) {
4372
+ if (isAuthError(err) && !_retried) {
4373
+ try {
4374
+ const newToken = await deps.requestDropboxAccess();
4375
+ return pushConversationToDropbox(database, conversationId, userAddress, newToken, deps, backupFolder, true);
4376
+ } catch {
4377
+ return "failed";
4378
+ }
4379
+ }
4380
+ return "failed";
4381
+ }
4382
+ }
4383
+ async function performDropboxExport(database, userAddress, token, deps, onProgress, backupFolder = DEFAULT_BACKUP_FOLDER) {
4384
+ await deps.requestEncryptionKey(userAddress);
4385
+ const { Q: Q4 } = await import("@nozbe/watermelondb");
4386
+ const conversationsCollection = database.get("conversations");
4387
+ const records = await conversationsCollection.query(Q4.where("is_deleted", false)).fetch();
4388
+ const conversations = records.map(conversationToStored);
4389
+ const total = conversations.length;
4390
+ if (total === 0) {
4391
+ return { success: true, uploaded: 0, skipped: 0, total: 0 };
4392
+ }
4393
+ let uploaded = 0;
4394
+ let skipped = 0;
4395
+ for (let i = 0; i < conversations.length; i++) {
4396
+ const conv = conversations[i];
4397
+ onProgress?.(i + 1, total);
4398
+ const result = await pushConversationToDropbox(
4399
+ database,
4400
+ conv.conversationId,
4401
+ userAddress,
4402
+ token,
4403
+ deps,
4404
+ backupFolder
4405
+ );
4406
+ if (result === "uploaded") uploaded++;
4407
+ if (result === "skipped") skipped++;
4408
+ }
4409
+ return { success: true, uploaded, skipped, total };
4410
+ }
4411
+ async function performDropboxImport(userAddress, token, deps, onProgress, backupFolder = DEFAULT_BACKUP_FOLDER) {
4412
+ await deps.requestEncryptionKey(userAddress);
4413
+ const remoteFiles = await listDropboxFiles(token, backupFolder);
4414
+ if (remoteFiles.length === 0) {
4415
+ return { success: false, restored: 0, failed: 0, total: 0, noBackupsFound: true };
4416
+ }
4417
+ const jsonFiles = remoteFiles.filter((file) => file.name.endsWith(".json"));
4418
+ const total = jsonFiles.length;
4419
+ let restored = 0;
4420
+ let failed = 0;
4421
+ let currentToken = token;
4422
+ for (let i = 0; i < jsonFiles.length; i++) {
4423
+ const file = jsonFiles[i];
4424
+ onProgress?.(i + 1, total);
4425
+ try {
4426
+ const blob = await downloadDropboxFile(currentToken, file.path_lower);
4427
+ const result = await deps.importConversation(blob, userAddress);
4428
+ if (result.success) {
4429
+ restored++;
4430
+ } else {
4431
+ failed++;
4432
+ }
4433
+ } catch (err) {
4434
+ if (isAuthError(err)) {
4435
+ try {
4436
+ currentToken = await deps.requestDropboxAccess();
4437
+ const blob = await downloadDropboxFile(currentToken, file.path_lower);
4438
+ const result = await deps.importConversation(blob, userAddress);
4439
+ if (result.success) {
4440
+ restored++;
4441
+ } else {
4442
+ failed++;
4443
+ }
4444
+ } catch {
4445
+ failed++;
4446
+ }
4447
+ } else {
4448
+ failed++;
4449
+ }
4450
+ }
4451
+ }
4452
+ return { success: true, restored, failed, total };
4453
+ }
4454
+
4455
+ // src/react/useDropboxAuth.ts
4456
+ import {
4457
+ createContext,
4458
+ createElement,
4459
+ useCallback as useCallback10,
4460
+ useContext,
4461
+ useEffect as useEffect6,
4462
+ useState as useState10
4463
+ } from "react";
4464
+
4465
+ // src/lib/backup/dropbox/auth.ts
4466
+ var DROPBOX_AUTH_URL = "https://www.dropbox.com/oauth2/authorize";
4467
+ var DROPBOX_TOKEN_URL = "https://api.dropboxapi.com/oauth2/token";
4468
+ var TOKEN_STORAGE_KEY = "dropbox_access_token";
4469
+ var VERIFIER_STORAGE_KEY = "dropbox_code_verifier";
4470
+ function generateCodeVerifier() {
4471
+ const array = new Uint8Array(32);
4472
+ crypto.getRandomValues(array);
4473
+ return Array.from(array, (byte) => byte.toString(16).padStart(2, "0")).join("");
4474
+ }
4475
+ async function generateCodeChallenge(verifier) {
4476
+ const encoder = new TextEncoder();
4477
+ const data = encoder.encode(verifier);
4478
+ const hash = await crypto.subtle.digest("SHA-256", data);
4479
+ const base64 = btoa(String.fromCharCode(...new Uint8Array(hash)));
4480
+ return base64.replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
4481
+ }
4482
+ function getStoredToken() {
4483
+ if (typeof window === "undefined") return null;
4484
+ return sessionStorage.getItem(TOKEN_STORAGE_KEY);
4485
+ }
4486
+ function storeToken(token) {
4487
+ if (typeof window === "undefined") return;
4488
+ sessionStorage.setItem(TOKEN_STORAGE_KEY, token);
4489
+ }
4490
+ function clearToken() {
4491
+ if (typeof window === "undefined") return;
4492
+ sessionStorage.removeItem(TOKEN_STORAGE_KEY);
4493
+ }
4494
+ function getStoredVerifier() {
4495
+ if (typeof window === "undefined") return null;
4496
+ return sessionStorage.getItem(VERIFIER_STORAGE_KEY);
4497
+ }
4498
+ function storeVerifier(verifier) {
4499
+ if (typeof window === "undefined") return;
4500
+ sessionStorage.setItem(VERIFIER_STORAGE_KEY, verifier);
4501
+ }
4502
+ function clearVerifier() {
4503
+ if (typeof window === "undefined") return;
4504
+ sessionStorage.removeItem(VERIFIER_STORAGE_KEY);
4505
+ }
4506
+ function getRedirectUri(callbackPath) {
4507
+ if (typeof window === "undefined") return "";
4508
+ return `${window.location.origin}${callbackPath}`;
4509
+ }
4510
+ async function handleDropboxCallback(appKey, callbackPath) {
4511
+ if (typeof window === "undefined") return null;
4512
+ const url = new URL(window.location.href);
4513
+ const code = url.searchParams.get("code");
4514
+ const state = url.searchParams.get("state");
4515
+ if (!code || state !== "dropbox_auth") return null;
4516
+ const verifier = getStoredVerifier();
4517
+ if (!verifier) return null;
4518
+ try {
4519
+ const response = await fetch(DROPBOX_TOKEN_URL, {
4520
+ method: "POST",
4521
+ headers: {
4522
+ "Content-Type": "application/x-www-form-urlencoded"
4523
+ },
4524
+ body: new URLSearchParams({
4525
+ code,
4526
+ grant_type: "authorization_code",
4527
+ client_id: appKey,
4528
+ redirect_uri: getRedirectUri(callbackPath),
4529
+ code_verifier: verifier
4530
+ })
4531
+ });
4532
+ if (!response.ok) {
4533
+ throw new Error("Token exchange failed");
4534
+ }
4535
+ const data = await response.json();
4536
+ const token = data.access_token;
4537
+ if (typeof token !== "string" || token.trim() === "") {
4538
+ throw new Error("Invalid token response: access_token is missing or empty");
4539
+ }
4540
+ storeToken(token);
4541
+ clearVerifier();
4542
+ window.history.replaceState({}, "", window.location.pathname);
4543
+ return token;
4544
+ } catch {
4545
+ clearVerifier();
4546
+ return null;
4547
+ }
4548
+ }
4549
+ async function startDropboxAuth(appKey, callbackPath) {
4550
+ const verifier = generateCodeVerifier();
4551
+ const challenge = await generateCodeChallenge(verifier);
4552
+ storeVerifier(verifier);
4553
+ const params = new URLSearchParams({
4554
+ client_id: appKey,
4555
+ redirect_uri: getRedirectUri(callbackPath),
4556
+ response_type: "code",
4557
+ code_challenge: challenge,
4558
+ code_challenge_method: "S256",
4559
+ state: "dropbox_auth",
4560
+ token_access_type: "offline"
4561
+ });
4562
+ window.location.href = `${DROPBOX_AUTH_URL}?${params.toString()}`;
4563
+ return new Promise(() => {
4564
+ });
4565
+ }
4566
+ async function requestDropboxAccess(appKey, callbackPath) {
4567
+ if (!appKey) {
4568
+ throw new Error("Dropbox is not configured");
4569
+ }
4570
+ const storedToken = getStoredToken();
4571
+ if (storedToken) {
4572
+ return storedToken;
4573
+ }
4574
+ return startDropboxAuth(appKey, callbackPath);
4575
+ }
4576
+
4577
+ // src/react/useDropboxAuth.ts
4578
+ var DropboxAuthContext = createContext(null);
4579
+ function DropboxAuthProvider({
4580
+ appKey,
4581
+ callbackPath = "/auth/dropbox/callback",
4582
+ children
4583
+ }) {
4584
+ const [accessToken, setAccessToken] = useState10(null);
4585
+ const isConfigured = !!appKey;
4586
+ useEffect6(() => {
4587
+ const storedToken = getStoredToken();
4588
+ if (storedToken) {
4589
+ setAccessToken(storedToken);
4590
+ }
4591
+ }, []);
4592
+ useEffect6(() => {
4593
+ if (!isConfigured || !appKey) return;
4594
+ const handleCallback = async () => {
4595
+ const token = await handleDropboxCallback(appKey, callbackPath);
4596
+ if (token) {
4597
+ setAccessToken(token);
4598
+ }
4599
+ };
4600
+ handleCallback();
4601
+ }, [appKey, callbackPath, isConfigured]);
4602
+ const requestAccess = useCallback10(async () => {
4603
+ if (!isConfigured || !appKey) {
4604
+ throw new Error("Dropbox is not configured");
4605
+ }
4606
+ if (accessToken) {
4607
+ return accessToken;
4608
+ }
4609
+ const storedToken = getStoredToken();
4610
+ if (storedToken) {
4611
+ setAccessToken(storedToken);
4612
+ return storedToken;
4613
+ }
4614
+ return requestDropboxAccess(appKey, callbackPath);
4615
+ }, [accessToken, appKey, callbackPath, isConfigured]);
4616
+ const logout = useCallback10(() => {
4617
+ clearToken();
4618
+ setAccessToken(null);
4619
+ }, []);
4620
+ return createElement(
4621
+ DropboxAuthContext.Provider,
4622
+ {
4623
+ value: {
4624
+ accessToken,
4625
+ isAuthenticated: !!accessToken,
4626
+ isConfigured,
4627
+ requestAccess,
4628
+ logout
4629
+ }
4630
+ },
4631
+ children
4632
+ );
4633
+ }
4634
+ function useDropboxAuth() {
4635
+ const context = useContext(DropboxAuthContext);
4636
+ if (!context) {
4637
+ throw new Error("useDropboxAuth must be used within DropboxAuthProvider");
4638
+ }
4639
+ return context;
4640
+ }
4641
+
4642
+ // src/react/useDropboxBackup.ts
4643
+ function useDropboxBackup(options) {
4644
+ const {
4645
+ database,
4646
+ userAddress,
4647
+ requestEncryptionKey: requestEncryptionKey2,
4648
+ exportConversation,
4649
+ importConversation,
4650
+ backupFolder = DEFAULT_BACKUP_FOLDER
4651
+ } = options;
4652
+ const {
4653
+ accessToken: dropboxToken,
4654
+ isConfigured: isDropboxConfigured,
4655
+ requestAccess: requestDropboxAccess2
4656
+ } = useDropboxAuth();
4657
+ const deps = useMemo4(
4658
+ () => ({
4659
+ requestDropboxAccess: requestDropboxAccess2,
4660
+ requestEncryptionKey: requestEncryptionKey2,
4661
+ exportConversation,
4662
+ importConversation
4663
+ }),
4664
+ [requestDropboxAccess2, requestEncryptionKey2, exportConversation, importConversation]
4665
+ );
4666
+ const ensureToken = useCallback11(async () => {
4667
+ if (dropboxToken) return dropboxToken;
4668
+ try {
4669
+ return await requestDropboxAccess2();
4670
+ } catch {
4671
+ return null;
4672
+ }
4673
+ }, [dropboxToken, requestDropboxAccess2]);
4674
+ const backup = useCallback11(
4675
+ async (backupOptions) => {
4676
+ if (!userAddress) {
4677
+ return { error: "Please sign in to backup to Dropbox" };
4678
+ }
4679
+ const token = await ensureToken();
4680
+ if (!token) {
4681
+ return { error: "Dropbox access denied" };
4682
+ }
4683
+ try {
4684
+ return await performDropboxExport(
4685
+ database,
4686
+ userAddress,
4687
+ token,
4688
+ deps,
4689
+ backupOptions?.onProgress,
4690
+ backupFolder
4691
+ );
4692
+ } catch (err) {
4693
+ return {
4694
+ error: err instanceof Error ? err.message : "Failed to backup to Dropbox"
4695
+ };
4696
+ }
4697
+ },
4698
+ [database, userAddress, ensureToken, deps, backupFolder]
4699
+ );
4700
+ const restore = useCallback11(
4701
+ async (restoreOptions) => {
4702
+ if (!userAddress) {
4703
+ return { error: "Please sign in to restore from Dropbox" };
4704
+ }
4705
+ const token = await ensureToken();
4706
+ if (!token) {
4707
+ return { error: "Dropbox access denied" };
4708
+ }
4709
+ try {
4710
+ return await performDropboxImport(
4711
+ userAddress,
4712
+ token,
4713
+ deps,
4714
+ restoreOptions?.onProgress,
4715
+ backupFolder
4716
+ );
4717
+ } catch (err) {
4718
+ return {
4719
+ error: err instanceof Error ? err.message : "Failed to restore from Dropbox"
4720
+ };
4721
+ }
4722
+ },
4723
+ [userAddress, ensureToken, deps, backupFolder]
4724
+ );
4725
+ return {
4726
+ backup,
4727
+ restore,
4728
+ isConfigured: isDropboxConfigured,
4729
+ isAuthenticated: !!dropboxToken
4730
+ };
4731
+ }
4732
+
4733
+ // src/react/useGoogleDriveBackup.ts
4734
+ import { useCallback as useCallback12, useMemo as useMemo5 } from "react";
4735
+
4736
+ // src/lib/backup/google/api.ts
4737
+ var DRIVE_API_URL = "https://www.googleapis.com/drive/v3";
4738
+ var DRIVE_UPLOAD_URL = "https://www.googleapis.com/upload/drive/v3";
4739
+ var FOLDER_MIME_TYPE = "application/vnd.google-apps.folder";
4740
+ function escapeQueryValue(value) {
4741
+ return value.replace(/'/g, "''");
4742
+ }
4743
+ var DEFAULT_ROOT_FOLDER = "ai-chat-app";
4744
+ var DEFAULT_CONVERSATIONS_FOLDER = "conversations";
4745
+ async function ensureFolder(accessToken, name, parentId) {
4746
+ const parentQuery = parentId ? `'${escapeQueryValue(parentId)}' in parents and ` : "";
4747
+ const query = `${parentQuery}mimeType='${FOLDER_MIME_TYPE}' and name='${escapeQueryValue(name)}' and trashed=false`;
4748
+ const response = await fetch(
4749
+ `${DRIVE_API_URL}/files?q=${encodeURIComponent(query)}&fields=files(id)`,
4750
+ {
4751
+ headers: { Authorization: `Bearer ${accessToken}` }
4752
+ }
4753
+ );
4754
+ if (!response.ok) {
4755
+ throw new Error(`Failed to search for folder ${name}: ${response.status}`);
4756
+ }
4757
+ const data = await response.json();
4758
+ if (data.files && data.files.length > 0) {
4759
+ return data.files[0].id;
4760
+ }
4761
+ const body = {
4762
+ name,
4763
+ mimeType: FOLDER_MIME_TYPE
4764
+ };
4765
+ if (parentId) {
4766
+ body.parents = [parentId];
4767
+ }
4768
+ const createResponse = await fetch(`${DRIVE_API_URL}/files`, {
4769
+ method: "POST",
4770
+ headers: {
4771
+ Authorization: `Bearer ${accessToken}`,
4772
+ "Content-Type": "application/json"
4773
+ },
4774
+ body: JSON.stringify(body)
4775
+ });
4776
+ if (!createResponse.ok) {
4777
+ throw new Error(`Failed to create folder ${name}: ${createResponse.status}`);
4778
+ }
4779
+ const folderData = await createResponse.json();
4780
+ return folderData.id;
4781
+ }
4782
+ async function getBackupFolder(accessToken, rootFolder = DEFAULT_ROOT_FOLDER, subfolder = DEFAULT_CONVERSATIONS_FOLDER) {
4783
+ const rootId = await ensureFolder(accessToken, rootFolder);
4784
+ return ensureFolder(accessToken, subfolder, rootId);
4785
+ }
4786
+ async function uploadFileToDrive(accessToken, folderId, content, filename) {
4787
+ const metadata = {
4788
+ name: filename,
4789
+ parents: [folderId],
4790
+ mimeType: "application/json"
4791
+ };
4792
+ const form = new FormData();
4793
+ form.append("metadata", new Blob([JSON.stringify(metadata)], { type: "application/json" }));
4794
+ form.append("file", content);
4795
+ const response = await fetch(`${DRIVE_UPLOAD_URL}/files?uploadType=multipart`, {
4796
+ method: "POST",
4797
+ headers: { Authorization: `Bearer ${accessToken}` },
4798
+ body: form
4799
+ });
4800
+ if (!response.ok) {
4801
+ const errorText = await response.text();
4802
+ throw new Error(`Drive upload failed: ${response.status} - ${errorText}`);
4803
+ }
4804
+ return response.json();
4805
+ }
4806
+ async function updateDriveFile(accessToken, fileId, content) {
4807
+ const response = await fetch(`${DRIVE_UPLOAD_URL}/files/${fileId}?uploadType=media`, {
4808
+ method: "PATCH",
4809
+ headers: {
4810
+ Authorization: `Bearer ${accessToken}`,
4811
+ "Content-Type": "application/json"
4812
+ },
4813
+ body: content
4814
+ });
4815
+ if (!response.ok) {
4816
+ const errorText = await response.text();
4817
+ throw new Error(`Drive update failed: ${response.status} - ${errorText}`);
4818
+ }
4819
+ return response.json();
4820
+ }
4821
+ async function listDriveFiles(accessToken, folderId) {
4822
+ const query = `'${escapeQueryValue(folderId)}' in parents and mimeType='application/json' and trashed=false`;
4823
+ const fields = "files(id,name,createdTime,modifiedTime,size)";
4824
+ const response = await fetch(
4825
+ `${DRIVE_API_URL}/files?q=${encodeURIComponent(query)}&fields=${fields}&pageSize=1000`,
4826
+ {
4827
+ headers: { Authorization: `Bearer ${accessToken}` }
4828
+ }
4829
+ );
4830
+ if (!response.ok) {
4831
+ throw new Error(`Failed to list files: ${response.status}`);
4832
+ }
4833
+ const data = await response.json();
4834
+ return data.files ?? [];
4835
+ }
4836
+ async function downloadDriveFile(accessToken, fileId) {
4837
+ const response = await fetch(`${DRIVE_API_URL}/files/${fileId}?alt=media`, {
4838
+ headers: { Authorization: `Bearer ${accessToken}` }
4839
+ });
4840
+ if (!response.ok) {
4841
+ throw new Error(`Failed to download file: ${response.status}`);
4842
+ }
4843
+ return response.blob();
4844
+ }
4845
+ async function findDriveFile(accessToken, folderId, filename) {
4846
+ const query = `'${escapeQueryValue(folderId)}' in parents and name='${escapeQueryValue(filename)}' and trashed=false`;
4847
+ const fields = "files(id,name,createdTime,modifiedTime,size)";
4848
+ const response = await fetch(
4849
+ `${DRIVE_API_URL}/files?q=${encodeURIComponent(query)}&fields=${fields}&pageSize=1`,
4850
+ {
4851
+ headers: { Authorization: `Bearer ${accessToken}` }
4852
+ }
4853
+ );
4854
+ if (!response.ok) {
4855
+ throw new Error(`Failed to find file: ${response.status}`);
4856
+ }
4857
+ const data = await response.json();
4858
+ return data.files?.[0] ?? null;
4859
+ }
4860
+
4861
+ // src/lib/backup/google/backup.ts
4862
+ var isAuthError2 = (err) => err instanceof Error && (err.message.includes("401") || err.message.includes("403"));
4863
+ async function getConversationsFolder(token, requestDriveAccess, rootFolder, subfolder) {
4864
+ try {
4865
+ const folderId = await getBackupFolder(token, rootFolder, subfolder);
4866
+ return { folderId, token };
4867
+ } catch (err) {
4868
+ if (isAuthError2(err)) {
4869
+ try {
4870
+ const newToken = await requestDriveAccess();
4871
+ const folderId = await getBackupFolder(newToken, rootFolder, subfolder);
4872
+ return { folderId, token: newToken };
4873
+ } catch {
4874
+ return null;
4875
+ }
4876
+ }
4877
+ throw err;
4878
+ }
4879
+ }
4880
+ async function pushConversationToDrive(database, conversationId, userAddress, token, deps, rootFolder = DEFAULT_ROOT_FOLDER, subfolder = DEFAULT_CONVERSATIONS_FOLDER, _retried = false) {
4881
+ try {
4882
+ await deps.requestEncryptionKey(userAddress);
4883
+ const folderResult = await getConversationsFolder(token, deps.requestDriveAccess, rootFolder, subfolder);
4884
+ if (!folderResult) return "failed";
4885
+ const { folderId, token: activeToken } = folderResult;
4886
+ const filename = `${conversationId}.json`;
4887
+ const existingFile = await findDriveFile(activeToken, folderId, filename);
4888
+ if (existingFile) {
4889
+ const { Q: Q4 } = await import("@nozbe/watermelondb");
4890
+ const conversationsCollection = database.get("conversations");
4891
+ const records = await conversationsCollection.query(Q4.where("conversation_id", conversationId)).fetch();
4892
+ if (records.length > 0) {
4893
+ const conversation = conversationToStored(records[0]);
4894
+ const localUpdated = conversation.updatedAt.getTime();
4895
+ const remoteModified = new Date(existingFile.modifiedTime).getTime();
4896
+ if (localUpdated <= remoteModified) {
4897
+ return "skipped";
4898
+ }
4899
+ }
4900
+ }
4901
+ const exportResult = await deps.exportConversation(conversationId, userAddress);
4902
+ if (!exportResult.success || !exportResult.blob) {
4903
+ return "failed";
4904
+ }
4905
+ if (existingFile) {
4906
+ await updateDriveFile(activeToken, existingFile.id, exportResult.blob);
4907
+ } else {
4908
+ await uploadFileToDrive(activeToken, folderId, exportResult.blob, filename);
4909
+ }
4910
+ return "uploaded";
4911
+ } catch (err) {
4912
+ if (isAuthError2(err) && !_retried) {
4913
+ try {
4914
+ const newToken = await deps.requestDriveAccess();
4915
+ return pushConversationToDrive(database, conversationId, userAddress, newToken, deps, rootFolder, subfolder, true);
4916
+ } catch {
4917
+ return "failed";
4918
+ }
4919
+ }
4920
+ return "failed";
4921
+ }
4922
+ }
4923
+ async function performGoogleDriveExport(database, userAddress, token, deps, onProgress, rootFolder = DEFAULT_ROOT_FOLDER, subfolder = DEFAULT_CONVERSATIONS_FOLDER) {
4924
+ await deps.requestEncryptionKey(userAddress);
4925
+ const folderResult = await getConversationsFolder(token, deps.requestDriveAccess, rootFolder, subfolder);
4926
+ if (!folderResult) {
4927
+ return { success: false, uploaded: 0, skipped: 0, total: 0 };
4928
+ }
4929
+ const { token: activeToken } = folderResult;
4930
+ const { Q: Q4 } = await import("@nozbe/watermelondb");
4931
+ const conversationsCollection = database.get("conversations");
4932
+ const records = await conversationsCollection.query(Q4.where("is_deleted", false)).fetch();
4933
+ const conversations = records.map(conversationToStored);
4934
+ const total = conversations.length;
4935
+ if (total === 0) {
4936
+ return { success: true, uploaded: 0, skipped: 0, total: 0 };
4937
+ }
4938
+ let uploaded = 0;
4939
+ let skipped = 0;
4940
+ for (let i = 0; i < conversations.length; i++) {
4941
+ const conv = conversations[i];
4942
+ onProgress?.(i + 1, total);
4943
+ const result = await pushConversationToDrive(
4944
+ database,
4945
+ conv.conversationId,
4946
+ userAddress,
4947
+ activeToken,
4948
+ deps,
4949
+ rootFolder,
4950
+ subfolder
4951
+ );
4952
+ if (result === "uploaded") uploaded++;
4953
+ if (result === "skipped") skipped++;
4954
+ }
4955
+ return { success: true, uploaded, skipped, total };
4956
+ }
4957
+ async function performGoogleDriveImport(userAddress, token, deps, onProgress, rootFolder = DEFAULT_ROOT_FOLDER, subfolder = DEFAULT_CONVERSATIONS_FOLDER) {
4958
+ await deps.requestEncryptionKey(userAddress);
4959
+ const folderResult = await getConversationsFolder(token, deps.requestDriveAccess, rootFolder, subfolder);
4960
+ if (!folderResult) {
4961
+ return { success: false, restored: 0, failed: 0, total: 0, noBackupsFound: true };
4962
+ }
4963
+ const { folderId, token: activeToken } = folderResult;
4964
+ const remoteFiles = await listDriveFiles(activeToken, folderId);
4965
+ if (remoteFiles.length === 0) {
4966
+ return { success: false, restored: 0, failed: 0, total: 0, noBackupsFound: true };
4967
+ }
4968
+ const jsonFiles = remoteFiles.filter((file) => file.name.endsWith(".json"));
4969
+ const total = jsonFiles.length;
4970
+ let restored = 0;
4971
+ let failed = 0;
4972
+ for (let i = 0; i < jsonFiles.length; i++) {
4973
+ const file = jsonFiles[i];
4974
+ onProgress?.(i + 1, total);
4975
+ try {
4976
+ const blob = await downloadDriveFile(activeToken, file.id);
4977
+ const result = await deps.importConversation(blob, userAddress);
4978
+ if (result.success) {
4979
+ restored++;
4980
+ } else {
4981
+ failed++;
4982
+ }
4983
+ } catch {
4984
+ failed++;
4985
+ }
4986
+ }
4987
+ return { success: true, restored, failed, total };
4988
+ }
4989
+
4990
+ // src/react/useGoogleDriveBackup.ts
4991
+ function useGoogleDriveBackup(options) {
4992
+ const {
4993
+ database,
4994
+ userAddress,
4995
+ accessToken,
4996
+ requestDriveAccess,
4997
+ requestEncryptionKey: requestEncryptionKey2,
4998
+ exportConversation,
4999
+ importConversation,
5000
+ rootFolder = DEFAULT_ROOT_FOLDER,
5001
+ conversationsFolder = DEFAULT_CONVERSATIONS_FOLDER
5002
+ } = options;
5003
+ const deps = useMemo5(
5004
+ () => ({
5005
+ requestDriveAccess,
5006
+ requestEncryptionKey: requestEncryptionKey2,
5007
+ exportConversation,
5008
+ importConversation
5009
+ }),
5010
+ [
5011
+ requestDriveAccess,
5012
+ requestEncryptionKey2,
5013
+ exportConversation,
5014
+ importConversation
5015
+ ]
5016
+ );
5017
+ const ensureToken = useCallback12(async () => {
5018
+ if (accessToken) return accessToken;
5019
+ try {
5020
+ return await requestDriveAccess();
5021
+ } catch {
5022
+ return null;
5023
+ }
5024
+ }, [accessToken, requestDriveAccess]);
5025
+ const backup = useCallback12(
5026
+ async (backupOptions) => {
5027
+ if (!userAddress) {
5028
+ return { error: "Please sign in to backup to Google Drive" };
5029
+ }
5030
+ const token = await ensureToken();
5031
+ if (!token) {
5032
+ return { error: "Google Drive access denied" };
5033
+ }
5034
+ try {
5035
+ return await performGoogleDriveExport(
5036
+ database,
5037
+ userAddress,
5038
+ token,
5039
+ deps,
5040
+ backupOptions?.onProgress,
5041
+ rootFolder,
5042
+ conversationsFolder
5043
+ );
5044
+ } catch (err) {
5045
+ return {
5046
+ error: err instanceof Error ? err.message : "Failed to backup to Google Drive"
5047
+ };
5048
+ }
5049
+ },
5050
+ [database, userAddress, ensureToken, deps, rootFolder, conversationsFolder]
5051
+ );
5052
+ const restore = useCallback12(
5053
+ async (restoreOptions) => {
5054
+ if (!userAddress) {
5055
+ return { error: "Please sign in to restore from Google Drive" };
5056
+ }
5057
+ const token = await ensureToken();
5058
+ if (!token) {
5059
+ return { error: "Google Drive access denied" };
5060
+ }
5061
+ try {
5062
+ return await performGoogleDriveImport(
5063
+ userAddress,
5064
+ token,
5065
+ deps,
5066
+ restoreOptions?.onProgress,
5067
+ rootFolder,
5068
+ conversationsFolder
5069
+ );
5070
+ } catch (err) {
5071
+ return {
5072
+ error: err instanceof Error ? err.message : "Failed to restore from Google Drive"
5073
+ };
5074
+ }
5075
+ },
5076
+ [userAddress, ensureToken, deps, rootFolder, conversationsFolder]
5077
+ );
5078
+ return {
5079
+ backup,
5080
+ restore,
5081
+ isAuthenticated: !!accessToken
5082
+ };
5083
+ }
3972
5084
  export {
3973
5085
  Conversation as ChatConversation,
3974
5086
  Message as ChatMessage,
5087
+ DEFAULT_BACKUP_FOLDER,
5088
+ DEFAULT_CONVERSATIONS_FOLDER as DEFAULT_DRIVE_CONVERSATIONS_FOLDER,
5089
+ DEFAULT_ROOT_FOLDER as DEFAULT_DRIVE_ROOT_FOLDER,
3975
5090
  DEFAULT_TOOL_SELECTOR_MODEL,
5091
+ DropboxAuthProvider,
3976
5092
  Memory as StoredMemoryModel,
5093
+ ModelPreference as StoredModelPreferenceModel,
5094
+ chatStorageMigrations,
3977
5095
  chatStorageSchema,
5096
+ clearToken as clearDropboxToken,
3978
5097
  createMemoryContextSystemMessage,
3979
5098
  decryptData,
3980
5099
  decryptDataBytes,
@@ -3985,17 +5104,24 @@ export {
3985
5104
  generateCompositeKey,
3986
5105
  generateConversationId,
3987
5106
  generateUniqueKey,
5107
+ getStoredToken as getDropboxToken,
3988
5108
  hasEncryptionKey,
3989
5109
  memoryStorageSchema,
3990
5110
  requestEncryptionKey,
3991
5111
  selectTool,
5112
+ settingsStorageSchema,
5113
+ storeToken as storeDropboxToken,
3992
5114
  useChat,
3993
5115
  useChatStorage,
5116
+ useDropboxAuth,
5117
+ useDropboxBackup,
3994
5118
  useEncryption,
5119
+ useGoogleDriveBackup,
3995
5120
  useImageGeneration,
3996
5121
  useMemoryStorage,
3997
5122
  useModels,
3998
5123
  useOCR,
3999
5124
  usePdf,
4000
- useSearch
5125
+ useSearch,
5126
+ useSettings
4001
5127
  };