@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.
- package/dist/expo/index.cjs +66 -3
- package/dist/expo/index.d.mts +17 -1
- package/dist/expo/index.d.ts +17 -1
- package/dist/expo/index.mjs +65 -3
- package/dist/react/index.cjs +1186 -53
- package/dist/react/index.d.mts +500 -2
- package/dist/react/index.d.ts +500 -2
- package/dist/react/index.mjs +1171 -45
- package/package.json +1 -1
package/dist/react/index.cjs
CHANGED
|
@@ -32,9 +32,16 @@ var index_exports = {};
|
|
|
32
32
|
__export(index_exports, {
|
|
33
33
|
ChatConversation: () => Conversation,
|
|
34
34
|
ChatMessage: () => Message,
|
|
35
|
+
DEFAULT_BACKUP_FOLDER: () => DEFAULT_BACKUP_FOLDER,
|
|
36
|
+
DEFAULT_DRIVE_CONVERSATIONS_FOLDER: () => DEFAULT_CONVERSATIONS_FOLDER,
|
|
37
|
+
DEFAULT_DRIVE_ROOT_FOLDER: () => DEFAULT_ROOT_FOLDER,
|
|
35
38
|
DEFAULT_TOOL_SELECTOR_MODEL: () => DEFAULT_TOOL_SELECTOR_MODEL,
|
|
39
|
+
DropboxAuthProvider: () => DropboxAuthProvider,
|
|
36
40
|
StoredMemoryModel: () => Memory,
|
|
41
|
+
StoredModelPreferenceModel: () => ModelPreference,
|
|
42
|
+
chatStorageMigrations: () => chatStorageMigrations,
|
|
37
43
|
chatStorageSchema: () => chatStorageSchema,
|
|
44
|
+
clearDropboxToken: () => clearToken,
|
|
38
45
|
createMemoryContextSystemMessage: () => createMemoryContextSystemMessage,
|
|
39
46
|
decryptData: () => decryptData,
|
|
40
47
|
decryptDataBytes: () => decryptDataBytes,
|
|
@@ -45,19 +52,26 @@ __export(index_exports, {
|
|
|
45
52
|
generateCompositeKey: () => generateCompositeKey,
|
|
46
53
|
generateConversationId: () => generateConversationId,
|
|
47
54
|
generateUniqueKey: () => generateUniqueKey,
|
|
55
|
+
getDropboxToken: () => getStoredToken,
|
|
48
56
|
hasEncryptionKey: () => hasEncryptionKey,
|
|
49
57
|
memoryStorageSchema: () => memoryStorageSchema,
|
|
50
58
|
requestEncryptionKey: () => requestEncryptionKey,
|
|
51
59
|
selectTool: () => selectTool,
|
|
60
|
+
settingsStorageSchema: () => settingsStorageSchema,
|
|
61
|
+
storeDropboxToken: () => storeToken,
|
|
52
62
|
useChat: () => useChat,
|
|
53
63
|
useChatStorage: () => useChatStorage,
|
|
64
|
+
useDropboxAuth: () => useDropboxAuth,
|
|
65
|
+
useDropboxBackup: () => useDropboxBackup,
|
|
54
66
|
useEncryption: () => useEncryption,
|
|
67
|
+
useGoogleDriveBackup: () => useGoogleDriveBackup,
|
|
55
68
|
useImageGeneration: () => useImageGeneration,
|
|
56
69
|
useMemoryStorage: () => useMemoryStorage,
|
|
57
70
|
useModels: () => useModels,
|
|
58
71
|
useOCR: () => useOCR,
|
|
59
72
|
usePdf: () => usePdf,
|
|
60
|
-
useSearch: () => useSearch
|
|
73
|
+
useSearch: () => useSearch,
|
|
74
|
+
useSettings: () => useSettings
|
|
61
75
|
});
|
|
62
76
|
module.exports = __toCommonJS(index_exports);
|
|
63
77
|
|
|
@@ -1503,20 +1517,42 @@ Please inform the user about this issue and try to help them alternatively.`
|
|
|
1503
1517
|
}
|
|
1504
1518
|
});
|
|
1505
1519
|
const accumulator = createStreamAccumulator();
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
if (
|
|
1517
|
-
|
|
1520
|
+
try {
|
|
1521
|
+
for await (const chunk of sseResult.stream) {
|
|
1522
|
+
if (isDoneMarker(chunk)) {
|
|
1523
|
+
continue;
|
|
1524
|
+
}
|
|
1525
|
+
if (chunk && typeof chunk === "object") {
|
|
1526
|
+
const contentDelta = processStreamingChunk(
|
|
1527
|
+
chunk,
|
|
1528
|
+
accumulator
|
|
1529
|
+
);
|
|
1530
|
+
if (contentDelta) {
|
|
1531
|
+
if (onData) onData(contentDelta);
|
|
1532
|
+
if (globalOnData) globalOnData(contentDelta);
|
|
1533
|
+
}
|
|
1518
1534
|
}
|
|
1519
1535
|
}
|
|
1536
|
+
} catch (streamErr) {
|
|
1537
|
+
if (isAbortError(streamErr) || abortController.signal.aborted) {
|
|
1538
|
+
setIsLoading(false);
|
|
1539
|
+
const partialCompletion = buildCompletionResponse(accumulator);
|
|
1540
|
+
return {
|
|
1541
|
+
data: partialCompletion,
|
|
1542
|
+
error: "Request aborted",
|
|
1543
|
+
toolExecution: toolExecutionResult
|
|
1544
|
+
};
|
|
1545
|
+
}
|
|
1546
|
+
throw streamErr;
|
|
1547
|
+
}
|
|
1548
|
+
if (abortController.signal.aborted) {
|
|
1549
|
+
setIsLoading(false);
|
|
1550
|
+
const partialCompletion = buildCompletionResponse(accumulator);
|
|
1551
|
+
return {
|
|
1552
|
+
data: partialCompletion,
|
|
1553
|
+
error: "Request aborted",
|
|
1554
|
+
toolExecution: toolExecutionResult
|
|
1555
|
+
};
|
|
1520
1556
|
}
|
|
1521
1557
|
if (sseError) {
|
|
1522
1558
|
throw sseError;
|
|
@@ -1741,7 +1777,8 @@ function messageToStored(message) {
|
|
|
1741
1777
|
embeddingModel: message.embeddingModel,
|
|
1742
1778
|
usage: message.usage,
|
|
1743
1779
|
sources: message.sources,
|
|
1744
|
-
responseDuration: message.responseDuration
|
|
1780
|
+
responseDuration: message.responseDuration,
|
|
1781
|
+
wasStopped: message.wasStopped
|
|
1745
1782
|
};
|
|
1746
1783
|
}
|
|
1747
1784
|
function conversationToStored(conversation) {
|
|
@@ -1830,6 +1867,7 @@ async function createMessageOp(ctx, opts) {
|
|
|
1830
1867
|
msg._setRaw("response_duration", opts.responseDuration);
|
|
1831
1868
|
if (opts.vector) msg._setRaw("vector", JSON.stringify(opts.vector));
|
|
1832
1869
|
if (opts.embeddingModel) msg._setRaw("embedding_model", opts.embeddingModel);
|
|
1870
|
+
if (opts.wasStopped) msg._setRaw("was_stopped", opts.wasStopped);
|
|
1833
1871
|
});
|
|
1834
1872
|
});
|
|
1835
1873
|
return messageToStored(created);
|
|
@@ -2120,6 +2158,45 @@ function useChatStorage(options) {
|
|
|
2120
2158
|
});
|
|
2121
2159
|
const responseDuration = (Date.now() - startTime) / 1e3;
|
|
2122
2160
|
if (result.error || !result.data) {
|
|
2161
|
+
const abortedResult = result;
|
|
2162
|
+
if (abortedResult.error === "Request aborted") {
|
|
2163
|
+
const assistantContent2 = abortedResult.data?.choices?.[0]?.message?.content?.map((part) => part.text || "").join("") || "";
|
|
2164
|
+
const responseModel = abortedResult.data?.model || model || "";
|
|
2165
|
+
let storedAssistantMessage2;
|
|
2166
|
+
try {
|
|
2167
|
+
storedAssistantMessage2 = await createMessageOp(storageCtx, {
|
|
2168
|
+
conversationId: convId,
|
|
2169
|
+
role: "assistant",
|
|
2170
|
+
content: assistantContent2,
|
|
2171
|
+
model: responseModel,
|
|
2172
|
+
usage: convertUsageToStored(abortedResult.data?.usage),
|
|
2173
|
+
responseDuration,
|
|
2174
|
+
wasStopped: true
|
|
2175
|
+
});
|
|
2176
|
+
const completionData = abortedResult.data || {
|
|
2177
|
+
id: `aborted-${Date.now()}`,
|
|
2178
|
+
model: responseModel,
|
|
2179
|
+
choices: [{
|
|
2180
|
+
index: 0,
|
|
2181
|
+
message: {
|
|
2182
|
+
role: "assistant",
|
|
2183
|
+
content: [{ type: "text", text: assistantContent2 }]
|
|
2184
|
+
},
|
|
2185
|
+
finish_reason: "stop"
|
|
2186
|
+
}],
|
|
2187
|
+
usage: void 0
|
|
2188
|
+
};
|
|
2189
|
+
return {
|
|
2190
|
+
data: completionData,
|
|
2191
|
+
error: null,
|
|
2192
|
+
// Treat as success to the caller
|
|
2193
|
+
toolExecution: abortedResult.toolExecution,
|
|
2194
|
+
userMessage: storedUserMessage,
|
|
2195
|
+
assistantMessage: storedAssistantMessage2
|
|
2196
|
+
};
|
|
2197
|
+
} catch (err) {
|
|
2198
|
+
}
|
|
2199
|
+
}
|
|
2123
2200
|
return {
|
|
2124
2201
|
data: null,
|
|
2125
2202
|
error: result.error || "No response data received",
|
|
@@ -2191,8 +2268,9 @@ function useChatStorage(options) {
|
|
|
2191
2268
|
|
|
2192
2269
|
// src/lib/chatStorage/schema.ts
|
|
2193
2270
|
var import_watermelondb2 = require("@nozbe/watermelondb");
|
|
2271
|
+
var import_migrations = require("@nozbe/watermelondb/Schema/migrations");
|
|
2194
2272
|
var chatStorageSchema = (0, import_watermelondb2.appSchema)({
|
|
2195
|
-
version:
|
|
2273
|
+
version: 2,
|
|
2196
2274
|
tables: [
|
|
2197
2275
|
(0, import_watermelondb2.tableSchema)({
|
|
2198
2276
|
name: "history",
|
|
@@ -2215,7 +2293,8 @@ var chatStorageSchema = (0, import_watermelondb2.appSchema)({
|
|
|
2215
2293
|
// JSON stringified ChatCompletionUsage
|
|
2216
2294
|
{ name: "sources", type: "string", isOptional: true },
|
|
2217
2295
|
// JSON stringified SearchSource[]
|
|
2218
|
-
{ name: "response_duration", type: "number", isOptional: true }
|
|
2296
|
+
{ name: "response_duration", type: "number", isOptional: true },
|
|
2297
|
+
{ name: "was_stopped", type: "boolean", isOptional: true }
|
|
2219
2298
|
]
|
|
2220
2299
|
}),
|
|
2221
2300
|
(0, import_watermelondb2.tableSchema)({
|
|
@@ -2230,6 +2309,21 @@ var chatStorageSchema = (0, import_watermelondb2.appSchema)({
|
|
|
2230
2309
|
})
|
|
2231
2310
|
]
|
|
2232
2311
|
});
|
|
2312
|
+
var chatStorageMigrations = (0, import_migrations.schemaMigrations)({
|
|
2313
|
+
migrations: [
|
|
2314
|
+
{
|
|
2315
|
+
toVersion: 2,
|
|
2316
|
+
steps: [
|
|
2317
|
+
(0, import_migrations.addColumns)({
|
|
2318
|
+
table: "history",
|
|
2319
|
+
columns: [
|
|
2320
|
+
{ name: "was_stopped", type: "boolean", isOptional: true }
|
|
2321
|
+
]
|
|
2322
|
+
})
|
|
2323
|
+
]
|
|
2324
|
+
}
|
|
2325
|
+
]
|
|
2326
|
+
});
|
|
2233
2327
|
|
|
2234
2328
|
// src/lib/chatStorage/models.ts
|
|
2235
2329
|
var import_watermelondb3 = require("@nozbe/watermelondb");
|
|
@@ -2313,6 +2407,10 @@ var Message = class extends import_watermelondb3.Model {
|
|
|
2313
2407
|
const value = this._getRaw("response_duration");
|
|
2314
2408
|
return value !== null && value !== void 0 ? value : void 0;
|
|
2315
2409
|
}
|
|
2410
|
+
/** Whether the message generation was stopped by the user */
|
|
2411
|
+
get wasStopped() {
|
|
2412
|
+
return this._getRaw("was_stopped");
|
|
2413
|
+
}
|
|
2316
2414
|
};
|
|
2317
2415
|
Message.table = "history";
|
|
2318
2416
|
Message.associations = {
|
|
@@ -3515,9 +3613,181 @@ var Memory = class extends import_watermelondb6.Model {
|
|
|
3515
3613
|
};
|
|
3516
3614
|
Memory.table = "memories";
|
|
3517
3615
|
|
|
3518
|
-
// src/react/
|
|
3616
|
+
// src/react/useSettings.ts
|
|
3519
3617
|
var import_react4 = require("react");
|
|
3520
3618
|
|
|
3619
|
+
// src/lib/settingsStorage/operations.ts
|
|
3620
|
+
var import_watermelondb7 = require("@nozbe/watermelondb");
|
|
3621
|
+
function modelPreferenceToStored(preference) {
|
|
3622
|
+
return {
|
|
3623
|
+
uniqueId: preference.id,
|
|
3624
|
+
walletAddress: preference.walletAddress,
|
|
3625
|
+
models: preference.models
|
|
3626
|
+
};
|
|
3627
|
+
}
|
|
3628
|
+
async function getModelPreferenceOp(ctx, walletAddress) {
|
|
3629
|
+
const results = await ctx.modelPreferencesCollection.query(import_watermelondb7.Q.where("wallet_address", walletAddress)).fetch();
|
|
3630
|
+
return results.length > 0 ? modelPreferenceToStored(results[0]) : null;
|
|
3631
|
+
}
|
|
3632
|
+
async function setModelPreferenceOp(ctx, walletAddress, models) {
|
|
3633
|
+
const result = await ctx.database.write(async () => {
|
|
3634
|
+
const results = await ctx.modelPreferencesCollection.query(import_watermelondb7.Q.where("wallet_address", walletAddress)).fetch();
|
|
3635
|
+
if (results.length > 0) {
|
|
3636
|
+
const preference = results[0];
|
|
3637
|
+
await preference.update((pref) => {
|
|
3638
|
+
if (models !== void 0) {
|
|
3639
|
+
pref._setRaw("models", models || null);
|
|
3640
|
+
}
|
|
3641
|
+
});
|
|
3642
|
+
return preference;
|
|
3643
|
+
}
|
|
3644
|
+
return await ctx.modelPreferencesCollection.create((pref) => {
|
|
3645
|
+
pref._setRaw("wallet_address", walletAddress);
|
|
3646
|
+
if (models) pref._setRaw("models", models);
|
|
3647
|
+
});
|
|
3648
|
+
});
|
|
3649
|
+
return modelPreferenceToStored(result);
|
|
3650
|
+
}
|
|
3651
|
+
async function deleteModelPreferenceOp(ctx, walletAddress) {
|
|
3652
|
+
const results = await ctx.modelPreferencesCollection.query(import_watermelondb7.Q.where("wallet_address", walletAddress)).fetch();
|
|
3653
|
+
if (results.length === 0) return false;
|
|
3654
|
+
await ctx.database.write(async () => {
|
|
3655
|
+
await results[0].destroyPermanently();
|
|
3656
|
+
});
|
|
3657
|
+
return true;
|
|
3658
|
+
}
|
|
3659
|
+
|
|
3660
|
+
// src/react/useSettings.ts
|
|
3661
|
+
function useSettings(options) {
|
|
3662
|
+
const { database, walletAddress } = options;
|
|
3663
|
+
const [modelPreference, setModelPreferenceState] = (0, import_react4.useState)(null);
|
|
3664
|
+
const [isLoading, setIsLoading] = (0, import_react4.useState)(false);
|
|
3665
|
+
const modelPreferencesCollection = (0, import_react4.useMemo)(
|
|
3666
|
+
() => database.get("modelPreferences"),
|
|
3667
|
+
[database]
|
|
3668
|
+
);
|
|
3669
|
+
const storageCtx = (0, import_react4.useMemo)(
|
|
3670
|
+
() => ({
|
|
3671
|
+
database,
|
|
3672
|
+
modelPreferencesCollection
|
|
3673
|
+
}),
|
|
3674
|
+
[database, modelPreferencesCollection]
|
|
3675
|
+
);
|
|
3676
|
+
const getModelPreference = (0, import_react4.useCallback)(
|
|
3677
|
+
async (address) => {
|
|
3678
|
+
try {
|
|
3679
|
+
if (!address) throw new Error("Wallet address is required");
|
|
3680
|
+
const result = await getModelPreferenceOp(storageCtx, address);
|
|
3681
|
+
return result;
|
|
3682
|
+
} catch (error) {
|
|
3683
|
+
throw new Error(
|
|
3684
|
+
error instanceof Error ? error.message : "An unknown error occurred"
|
|
3685
|
+
);
|
|
3686
|
+
}
|
|
3687
|
+
},
|
|
3688
|
+
[storageCtx]
|
|
3689
|
+
);
|
|
3690
|
+
const setModelPreference = (0, import_react4.useCallback)(
|
|
3691
|
+
async (address, models) => {
|
|
3692
|
+
try {
|
|
3693
|
+
if (!address) throw new Error("Wallet address is required");
|
|
3694
|
+
const result = await setModelPreferenceOp(storageCtx, address, models);
|
|
3695
|
+
if (walletAddress && address === walletAddress) {
|
|
3696
|
+
setModelPreferenceState(result);
|
|
3697
|
+
}
|
|
3698
|
+
return result;
|
|
3699
|
+
} catch (error) {
|
|
3700
|
+
throw new Error(
|
|
3701
|
+
error instanceof Error ? error.message : "An unknown error occurred"
|
|
3702
|
+
);
|
|
3703
|
+
}
|
|
3704
|
+
},
|
|
3705
|
+
[storageCtx, walletAddress]
|
|
3706
|
+
);
|
|
3707
|
+
const deleteModelPreference = (0, import_react4.useCallback)(
|
|
3708
|
+
async (address) => {
|
|
3709
|
+
try {
|
|
3710
|
+
if (!address) throw new Error("Wallet address is required");
|
|
3711
|
+
const deleted = await deleteModelPreferenceOp(storageCtx, address);
|
|
3712
|
+
if (deleted && walletAddress && address === walletAddress) {
|
|
3713
|
+
setModelPreferenceState(null);
|
|
3714
|
+
}
|
|
3715
|
+
return deleted;
|
|
3716
|
+
} catch (error) {
|
|
3717
|
+
throw new Error(
|
|
3718
|
+
error instanceof Error ? error.message : "An unknown error occurred"
|
|
3719
|
+
);
|
|
3720
|
+
}
|
|
3721
|
+
},
|
|
3722
|
+
[storageCtx, walletAddress]
|
|
3723
|
+
);
|
|
3724
|
+
(0, import_react4.useEffect)(() => {
|
|
3725
|
+
if (!walletAddress) {
|
|
3726
|
+
setModelPreferenceState(null);
|
|
3727
|
+
return;
|
|
3728
|
+
}
|
|
3729
|
+
let cancelled = false;
|
|
3730
|
+
const loadPreference = async () => {
|
|
3731
|
+
setIsLoading(true);
|
|
3732
|
+
try {
|
|
3733
|
+
const preference = await getModelPreference(walletAddress);
|
|
3734
|
+
if (!cancelled) {
|
|
3735
|
+
setModelPreferenceState(preference);
|
|
3736
|
+
}
|
|
3737
|
+
} finally {
|
|
3738
|
+
if (!cancelled) {
|
|
3739
|
+
setIsLoading(false);
|
|
3740
|
+
}
|
|
3741
|
+
}
|
|
3742
|
+
};
|
|
3743
|
+
loadPreference();
|
|
3744
|
+
return () => {
|
|
3745
|
+
cancelled = true;
|
|
3746
|
+
};
|
|
3747
|
+
}, [walletAddress, getModelPreference]);
|
|
3748
|
+
return {
|
|
3749
|
+
modelPreference,
|
|
3750
|
+
isLoading,
|
|
3751
|
+
getModelPreference,
|
|
3752
|
+
setModelPreference,
|
|
3753
|
+
deleteModelPreference
|
|
3754
|
+
};
|
|
3755
|
+
}
|
|
3756
|
+
|
|
3757
|
+
// src/lib/settingsStorage/schema.ts
|
|
3758
|
+
var import_watermelondb8 = require("@nozbe/watermelondb");
|
|
3759
|
+
var settingsStorageSchema = (0, import_watermelondb8.appSchema)({
|
|
3760
|
+
version: 1,
|
|
3761
|
+
tables: [
|
|
3762
|
+
(0, import_watermelondb8.tableSchema)({
|
|
3763
|
+
name: "modelPreferences",
|
|
3764
|
+
columns: [
|
|
3765
|
+
{ name: "wallet_address", type: "string", isIndexed: true },
|
|
3766
|
+
{ name: "models", type: "string", isOptional: true }
|
|
3767
|
+
// stored as JSON stringified ModelPreference[]
|
|
3768
|
+
]
|
|
3769
|
+
})
|
|
3770
|
+
]
|
|
3771
|
+
});
|
|
3772
|
+
|
|
3773
|
+
// src/lib/settingsStorage/models.ts
|
|
3774
|
+
var import_watermelondb9 = require("@nozbe/watermelondb");
|
|
3775
|
+
var ModelPreference = class extends import_watermelondb9.Model {
|
|
3776
|
+
/** User's wallet address */
|
|
3777
|
+
get walletAddress() {
|
|
3778
|
+
return this._getRaw("wallet_address");
|
|
3779
|
+
}
|
|
3780
|
+
/** Preferred model identifier */
|
|
3781
|
+
get models() {
|
|
3782
|
+
const value = this._getRaw("models");
|
|
3783
|
+
return value ? value : void 0;
|
|
3784
|
+
}
|
|
3785
|
+
};
|
|
3786
|
+
ModelPreference.table = "modelPreferences";
|
|
3787
|
+
|
|
3788
|
+
// src/react/usePdf.ts
|
|
3789
|
+
var import_react5 = require("react");
|
|
3790
|
+
|
|
3521
3791
|
// src/lib/pdf.ts
|
|
3522
3792
|
var pdfjs = __toESM(require("pdfjs-dist"));
|
|
3523
3793
|
pdfjs.GlobalWorkerOptions.workerSrc = `https://unpkg.com/pdfjs-dist@${pdfjs.version}/build/pdf.worker.min.mjs`;
|
|
@@ -3569,9 +3839,9 @@ async function convertPdfToImages(pdfDataUrl) {
|
|
|
3569
3839
|
// src/react/usePdf.ts
|
|
3570
3840
|
var PDF_MIME_TYPE = "application/pdf";
|
|
3571
3841
|
function usePdf() {
|
|
3572
|
-
const [isProcessing, setIsProcessing] = (0,
|
|
3573
|
-
const [error, setError] = (0,
|
|
3574
|
-
const extractPdfContext = (0,
|
|
3842
|
+
const [isProcessing, setIsProcessing] = (0, import_react5.useState)(false);
|
|
3843
|
+
const [error, setError] = (0, import_react5.useState)(null);
|
|
3844
|
+
const extractPdfContext = (0, import_react5.useCallback)(
|
|
3575
3845
|
async (files) => {
|
|
3576
3846
|
setIsProcessing(true);
|
|
3577
3847
|
setError(null);
|
|
@@ -3618,12 +3888,12 @@ ${text}`;
|
|
|
3618
3888
|
}
|
|
3619
3889
|
|
|
3620
3890
|
// src/react/useOCR.ts
|
|
3621
|
-
var
|
|
3891
|
+
var import_react6 = require("react");
|
|
3622
3892
|
var import_tesseract = __toESM(require("tesseract.js"));
|
|
3623
3893
|
function useOCR() {
|
|
3624
|
-
const [isProcessing, setIsProcessing] = (0,
|
|
3625
|
-
const [error, setError] = (0,
|
|
3626
|
-
const extractOCRContext = (0,
|
|
3894
|
+
const [isProcessing, setIsProcessing] = (0, import_react6.useState)(false);
|
|
3895
|
+
const [error, setError] = (0, import_react6.useState)(null);
|
|
3896
|
+
const extractOCRContext = (0, import_react6.useCallback)(
|
|
3627
3897
|
async (files) => {
|
|
3628
3898
|
setIsProcessing(true);
|
|
3629
3899
|
setError(null);
|
|
@@ -3709,22 +3979,22 @@ ${text}`;
|
|
|
3709
3979
|
}
|
|
3710
3980
|
|
|
3711
3981
|
// src/react/useModels.ts
|
|
3712
|
-
var
|
|
3982
|
+
var import_react7 = require("react");
|
|
3713
3983
|
function useModels(options = {}) {
|
|
3714
3984
|
const { getToken, baseUrl = BASE_URL, provider, autoFetch = true } = options;
|
|
3715
|
-
const [models, setModels] = (0,
|
|
3716
|
-
const [isLoading, setIsLoading] = (0,
|
|
3717
|
-
const [error, setError] = (0,
|
|
3718
|
-
const getTokenRef = (0,
|
|
3719
|
-
const baseUrlRef = (0,
|
|
3720
|
-
const providerRef = (0,
|
|
3721
|
-
const abortControllerRef = (0,
|
|
3722
|
-
(0,
|
|
3985
|
+
const [models, setModels] = (0, import_react7.useState)([]);
|
|
3986
|
+
const [isLoading, setIsLoading] = (0, import_react7.useState)(false);
|
|
3987
|
+
const [error, setError] = (0, import_react7.useState)(null);
|
|
3988
|
+
const getTokenRef = (0, import_react7.useRef)(getToken);
|
|
3989
|
+
const baseUrlRef = (0, import_react7.useRef)(baseUrl);
|
|
3990
|
+
const providerRef = (0, import_react7.useRef)(provider);
|
|
3991
|
+
const abortControllerRef = (0, import_react7.useRef)(null);
|
|
3992
|
+
(0, import_react7.useEffect)(() => {
|
|
3723
3993
|
getTokenRef.current = getToken;
|
|
3724
3994
|
baseUrlRef.current = baseUrl;
|
|
3725
3995
|
providerRef.current = provider;
|
|
3726
3996
|
});
|
|
3727
|
-
(0,
|
|
3997
|
+
(0, import_react7.useEffect)(() => {
|
|
3728
3998
|
return () => {
|
|
3729
3999
|
if (abortControllerRef.current) {
|
|
3730
4000
|
abortControllerRef.current.abort();
|
|
@@ -3732,7 +4002,7 @@ function useModels(options = {}) {
|
|
|
3732
4002
|
}
|
|
3733
4003
|
};
|
|
3734
4004
|
}, []);
|
|
3735
|
-
const fetchModels = (0,
|
|
4005
|
+
const fetchModels = (0, import_react7.useCallback)(async () => {
|
|
3736
4006
|
if (abortControllerRef.current) {
|
|
3737
4007
|
abortControllerRef.current.abort();
|
|
3738
4008
|
}
|
|
@@ -3790,12 +4060,12 @@ function useModels(options = {}) {
|
|
|
3790
4060
|
}
|
|
3791
4061
|
}
|
|
3792
4062
|
}, []);
|
|
3793
|
-
const refetch = (0,
|
|
4063
|
+
const refetch = (0, import_react7.useCallback)(async () => {
|
|
3794
4064
|
setModels([]);
|
|
3795
4065
|
await fetchModels();
|
|
3796
4066
|
}, [fetchModels]);
|
|
3797
|
-
const hasFetchedRef = (0,
|
|
3798
|
-
(0,
|
|
4067
|
+
const hasFetchedRef = (0, import_react7.useRef)(false);
|
|
4068
|
+
(0, import_react7.useEffect)(() => {
|
|
3799
4069
|
if (autoFetch && !hasFetchedRef.current) {
|
|
3800
4070
|
hasFetchedRef.current = true;
|
|
3801
4071
|
fetchModels();
|
|
@@ -3813,15 +4083,15 @@ function useModels(options = {}) {
|
|
|
3813
4083
|
}
|
|
3814
4084
|
|
|
3815
4085
|
// src/react/useSearch.ts
|
|
3816
|
-
var
|
|
4086
|
+
var import_react8 = require("react");
|
|
3817
4087
|
function useSearch(options = {}) {
|
|
3818
4088
|
const { getToken, baseUrl = BASE_URL, onError } = options;
|
|
3819
|
-
const [isLoading, setIsLoading] = (0,
|
|
3820
|
-
const [results, setResults] = (0,
|
|
3821
|
-
const [response, setResponse] = (0,
|
|
3822
|
-
const [error, setError] = (0,
|
|
3823
|
-
const abortControllerRef = (0,
|
|
3824
|
-
(0,
|
|
4089
|
+
const [isLoading, setIsLoading] = (0, import_react8.useState)(false);
|
|
4090
|
+
const [results, setResults] = (0, import_react8.useState)(null);
|
|
4091
|
+
const [response, setResponse] = (0, import_react8.useState)(null);
|
|
4092
|
+
const [error, setError] = (0, import_react8.useState)(null);
|
|
4093
|
+
const abortControllerRef = (0, import_react8.useRef)(null);
|
|
4094
|
+
(0, import_react8.useEffect)(() => {
|
|
3825
4095
|
return () => {
|
|
3826
4096
|
if (abortControllerRef.current) {
|
|
3827
4097
|
abortControllerRef.current.abort();
|
|
@@ -3829,7 +4099,7 @@ function useSearch(options = {}) {
|
|
|
3829
4099
|
}
|
|
3830
4100
|
};
|
|
3831
4101
|
}, []);
|
|
3832
|
-
const search = (0,
|
|
4102
|
+
const search = (0, import_react8.useCallback)(
|
|
3833
4103
|
async (query, searchOptions = {}) => {
|
|
3834
4104
|
if (abortControllerRef.current) {
|
|
3835
4105
|
abortControllerRef.current.abort();
|
|
@@ -3897,12 +4167,12 @@ function useSearch(options = {}) {
|
|
|
3897
4167
|
}
|
|
3898
4168
|
|
|
3899
4169
|
// src/react/useImageGeneration.ts
|
|
3900
|
-
var
|
|
4170
|
+
var import_react9 = require("react");
|
|
3901
4171
|
function useImageGeneration(options = {}) {
|
|
3902
4172
|
const { getToken, baseUrl = BASE_URL, onFinish, onError } = options;
|
|
3903
|
-
const [isLoading, setIsLoading] = (0,
|
|
3904
|
-
const abortControllerRef = (0,
|
|
3905
|
-
(0,
|
|
4173
|
+
const [isLoading, setIsLoading] = (0, import_react9.useState)(false);
|
|
4174
|
+
const abortControllerRef = (0, import_react9.useRef)(null);
|
|
4175
|
+
(0, import_react9.useEffect)(() => {
|
|
3906
4176
|
return () => {
|
|
3907
4177
|
if (abortControllerRef.current) {
|
|
3908
4178
|
abortControllerRef.current.abort();
|
|
@@ -3910,13 +4180,13 @@ function useImageGeneration(options = {}) {
|
|
|
3910
4180
|
}
|
|
3911
4181
|
};
|
|
3912
4182
|
}, []);
|
|
3913
|
-
const stop = (0,
|
|
4183
|
+
const stop = (0, import_react9.useCallback)(() => {
|
|
3914
4184
|
if (abortControllerRef.current) {
|
|
3915
4185
|
abortControllerRef.current.abort();
|
|
3916
4186
|
abortControllerRef.current = null;
|
|
3917
4187
|
}
|
|
3918
4188
|
}, []);
|
|
3919
|
-
const generateImage = (0,
|
|
4189
|
+
const generateImage = (0, import_react9.useCallback)(
|
|
3920
4190
|
async (args) => {
|
|
3921
4191
|
if (abortControllerRef.current) {
|
|
3922
4192
|
abortControllerRef.current.abort();
|
|
@@ -4032,13 +4302,869 @@ var extractConversationContext = (messages, maxMessages = 3) => {
|
|
|
4032
4302
|
const userMessages = messages.filter((msg) => msg.role === "user").slice(-maxMessages).map((msg) => msg.content).join(" ");
|
|
4033
4303
|
return userMessages.trim();
|
|
4034
4304
|
};
|
|
4305
|
+
|
|
4306
|
+
// src/react/useDropboxBackup.ts
|
|
4307
|
+
var import_react11 = require("react");
|
|
4308
|
+
|
|
4309
|
+
// src/lib/backup/dropbox/api.ts
|
|
4310
|
+
var DROPBOX_API_URL = "https://api.dropboxapi.com/2";
|
|
4311
|
+
var DROPBOX_CONTENT_URL = "https://content.dropboxapi.com/2";
|
|
4312
|
+
var DEFAULT_BACKUP_FOLDER = "/ai-chat-app/conversations";
|
|
4313
|
+
async function ensureBackupFolder(accessToken, folder = DEFAULT_BACKUP_FOLDER) {
|
|
4314
|
+
try {
|
|
4315
|
+
await fetch(`${DROPBOX_API_URL}/files/create_folder_v2`, {
|
|
4316
|
+
method: "POST",
|
|
4317
|
+
headers: {
|
|
4318
|
+
Authorization: `Bearer ${accessToken}`,
|
|
4319
|
+
"Content-Type": "application/json"
|
|
4320
|
+
},
|
|
4321
|
+
body: JSON.stringify({
|
|
4322
|
+
path: folder,
|
|
4323
|
+
autorename: false
|
|
4324
|
+
})
|
|
4325
|
+
});
|
|
4326
|
+
} catch {
|
|
4327
|
+
}
|
|
4328
|
+
}
|
|
4329
|
+
async function uploadFileToDropbox(accessToken, filename, content, folder = DEFAULT_BACKUP_FOLDER) {
|
|
4330
|
+
await ensureBackupFolder(accessToken, folder);
|
|
4331
|
+
const path = `${folder}/${filename}`;
|
|
4332
|
+
const response = await fetch(`${DROPBOX_CONTENT_URL}/files/upload`, {
|
|
4333
|
+
method: "POST",
|
|
4334
|
+
headers: {
|
|
4335
|
+
Authorization: `Bearer ${accessToken}`,
|
|
4336
|
+
"Content-Type": "application/octet-stream",
|
|
4337
|
+
"Dropbox-API-Arg": JSON.stringify({
|
|
4338
|
+
path,
|
|
4339
|
+
mode: "overwrite",
|
|
4340
|
+
autorename: false,
|
|
4341
|
+
mute: true
|
|
4342
|
+
})
|
|
4343
|
+
},
|
|
4344
|
+
body: content
|
|
4345
|
+
});
|
|
4346
|
+
if (!response.ok) {
|
|
4347
|
+
const errorText = await response.text();
|
|
4348
|
+
throw new Error(`Dropbox upload failed: ${response.status} - ${errorText}`);
|
|
4349
|
+
}
|
|
4350
|
+
return response.json();
|
|
4351
|
+
}
|
|
4352
|
+
async function listDropboxFiles(accessToken, folder = DEFAULT_BACKUP_FOLDER) {
|
|
4353
|
+
await ensureBackupFolder(accessToken, folder);
|
|
4354
|
+
const response = await fetch(`${DROPBOX_API_URL}/files/list_folder`, {
|
|
4355
|
+
method: "POST",
|
|
4356
|
+
headers: {
|
|
4357
|
+
Authorization: `Bearer ${accessToken}`,
|
|
4358
|
+
"Content-Type": "application/json"
|
|
4359
|
+
},
|
|
4360
|
+
body: JSON.stringify({
|
|
4361
|
+
path: folder,
|
|
4362
|
+
recursive: false,
|
|
4363
|
+
include_deleted: false
|
|
4364
|
+
})
|
|
4365
|
+
});
|
|
4366
|
+
if (!response.ok) {
|
|
4367
|
+
const error = await response.json();
|
|
4368
|
+
if (error.error?.path?.[".tag"] === "not_found") {
|
|
4369
|
+
return [];
|
|
4370
|
+
}
|
|
4371
|
+
throw new Error(`Dropbox list failed: ${error.error_summary}`);
|
|
4372
|
+
}
|
|
4373
|
+
let data = await response.json();
|
|
4374
|
+
const allEntries = [...data.entries];
|
|
4375
|
+
while (data.has_more) {
|
|
4376
|
+
const continueResponse = await fetch(`${DROPBOX_API_URL}/files/list_folder/continue`, {
|
|
4377
|
+
method: "POST",
|
|
4378
|
+
headers: {
|
|
4379
|
+
Authorization: `Bearer ${accessToken}`,
|
|
4380
|
+
"Content-Type": "application/json"
|
|
4381
|
+
},
|
|
4382
|
+
body: JSON.stringify({
|
|
4383
|
+
cursor: data.cursor
|
|
4384
|
+
})
|
|
4385
|
+
});
|
|
4386
|
+
if (!continueResponse.ok) {
|
|
4387
|
+
const errorText = await continueResponse.text();
|
|
4388
|
+
throw new Error(`Dropbox list continue failed: ${continueResponse.status} - ${errorText}`);
|
|
4389
|
+
}
|
|
4390
|
+
data = await continueResponse.json();
|
|
4391
|
+
allEntries.push(...data.entries);
|
|
4392
|
+
}
|
|
4393
|
+
const files = allEntries.filter((entry) => entry[".tag"] === "file").map((entry) => ({
|
|
4394
|
+
id: entry.id,
|
|
4395
|
+
name: entry.name,
|
|
4396
|
+
path_lower: entry.path_lower,
|
|
4397
|
+
path_display: entry.path_display,
|
|
4398
|
+
client_modified: entry.client_modified || "",
|
|
4399
|
+
server_modified: entry.server_modified || "",
|
|
4400
|
+
size: entry.size || 0
|
|
4401
|
+
}));
|
|
4402
|
+
return files;
|
|
4403
|
+
}
|
|
4404
|
+
async function downloadDropboxFile(accessToken, path) {
|
|
4405
|
+
const response = await fetch(`${DROPBOX_CONTENT_URL}/files/download`, {
|
|
4406
|
+
method: "POST",
|
|
4407
|
+
headers: {
|
|
4408
|
+
Authorization: `Bearer ${accessToken}`,
|
|
4409
|
+
"Dropbox-API-Arg": JSON.stringify({ path })
|
|
4410
|
+
}
|
|
4411
|
+
});
|
|
4412
|
+
if (!response.ok) {
|
|
4413
|
+
throw new Error(`Dropbox download failed: ${response.status}`);
|
|
4414
|
+
}
|
|
4415
|
+
return response.blob();
|
|
4416
|
+
}
|
|
4417
|
+
async function findDropboxFile(accessToken, filename, folder = DEFAULT_BACKUP_FOLDER) {
|
|
4418
|
+
const files = await listDropboxFiles(accessToken, folder);
|
|
4419
|
+
return files.find((f) => f.name === filename) || null;
|
|
4420
|
+
}
|
|
4421
|
+
|
|
4422
|
+
// src/lib/backup/dropbox/backup.ts
|
|
4423
|
+
var isAuthError = (err) => err instanceof Error && (err.message.includes("401") || err.message.includes("invalid_access_token"));
|
|
4424
|
+
async function pushConversationToDropbox(database, conversationId, userAddress, token, deps, backupFolder = DEFAULT_BACKUP_FOLDER, _retried = false) {
|
|
4425
|
+
try {
|
|
4426
|
+
await deps.requestEncryptionKey(userAddress);
|
|
4427
|
+
const filename = `${conversationId}.json`;
|
|
4428
|
+
const existingFile = await findDropboxFile(token, filename, backupFolder);
|
|
4429
|
+
if (existingFile) {
|
|
4430
|
+
const { Q: Q4 } = await import("@nozbe/watermelondb");
|
|
4431
|
+
const conversationsCollection = database.get("conversations");
|
|
4432
|
+
const records = await conversationsCollection.query(Q4.where("conversation_id", conversationId)).fetch();
|
|
4433
|
+
if (records.length > 0) {
|
|
4434
|
+
const conversation = conversationToStored(records[0]);
|
|
4435
|
+
const localUpdated = conversation.updatedAt.getTime();
|
|
4436
|
+
const remoteModified = new Date(existingFile.server_modified).getTime();
|
|
4437
|
+
if (localUpdated <= remoteModified) {
|
|
4438
|
+
return "skipped";
|
|
4439
|
+
}
|
|
4440
|
+
}
|
|
4441
|
+
}
|
|
4442
|
+
const exportResult = await deps.exportConversation(conversationId, userAddress);
|
|
4443
|
+
if (!exportResult.success || !exportResult.blob) {
|
|
4444
|
+
return "failed";
|
|
4445
|
+
}
|
|
4446
|
+
await uploadFileToDropbox(token, filename, exportResult.blob, backupFolder);
|
|
4447
|
+
return "uploaded";
|
|
4448
|
+
} catch (err) {
|
|
4449
|
+
if (isAuthError(err) && !_retried) {
|
|
4450
|
+
try {
|
|
4451
|
+
const newToken = await deps.requestDropboxAccess();
|
|
4452
|
+
return pushConversationToDropbox(database, conversationId, userAddress, newToken, deps, backupFolder, true);
|
|
4453
|
+
} catch {
|
|
4454
|
+
return "failed";
|
|
4455
|
+
}
|
|
4456
|
+
}
|
|
4457
|
+
return "failed";
|
|
4458
|
+
}
|
|
4459
|
+
}
|
|
4460
|
+
async function performDropboxExport(database, userAddress, token, deps, onProgress, backupFolder = DEFAULT_BACKUP_FOLDER) {
|
|
4461
|
+
await deps.requestEncryptionKey(userAddress);
|
|
4462
|
+
const { Q: Q4 } = await import("@nozbe/watermelondb");
|
|
4463
|
+
const conversationsCollection = database.get("conversations");
|
|
4464
|
+
const records = await conversationsCollection.query(Q4.where("is_deleted", false)).fetch();
|
|
4465
|
+
const conversations = records.map(conversationToStored);
|
|
4466
|
+
const total = conversations.length;
|
|
4467
|
+
if (total === 0) {
|
|
4468
|
+
return { success: true, uploaded: 0, skipped: 0, total: 0 };
|
|
4469
|
+
}
|
|
4470
|
+
let uploaded = 0;
|
|
4471
|
+
let skipped = 0;
|
|
4472
|
+
for (let i = 0; i < conversations.length; i++) {
|
|
4473
|
+
const conv = conversations[i];
|
|
4474
|
+
onProgress?.(i + 1, total);
|
|
4475
|
+
const result = await pushConversationToDropbox(
|
|
4476
|
+
database,
|
|
4477
|
+
conv.conversationId,
|
|
4478
|
+
userAddress,
|
|
4479
|
+
token,
|
|
4480
|
+
deps,
|
|
4481
|
+
backupFolder
|
|
4482
|
+
);
|
|
4483
|
+
if (result === "uploaded") uploaded++;
|
|
4484
|
+
if (result === "skipped") skipped++;
|
|
4485
|
+
}
|
|
4486
|
+
return { success: true, uploaded, skipped, total };
|
|
4487
|
+
}
|
|
4488
|
+
async function performDropboxImport(userAddress, token, deps, onProgress, backupFolder = DEFAULT_BACKUP_FOLDER) {
|
|
4489
|
+
await deps.requestEncryptionKey(userAddress);
|
|
4490
|
+
const remoteFiles = await listDropboxFiles(token, backupFolder);
|
|
4491
|
+
if (remoteFiles.length === 0) {
|
|
4492
|
+
return { success: false, restored: 0, failed: 0, total: 0, noBackupsFound: true };
|
|
4493
|
+
}
|
|
4494
|
+
const jsonFiles = remoteFiles.filter((file) => file.name.endsWith(".json"));
|
|
4495
|
+
const total = jsonFiles.length;
|
|
4496
|
+
let restored = 0;
|
|
4497
|
+
let failed = 0;
|
|
4498
|
+
let currentToken = token;
|
|
4499
|
+
for (let i = 0; i < jsonFiles.length; i++) {
|
|
4500
|
+
const file = jsonFiles[i];
|
|
4501
|
+
onProgress?.(i + 1, total);
|
|
4502
|
+
try {
|
|
4503
|
+
const blob = await downloadDropboxFile(currentToken, file.path_lower);
|
|
4504
|
+
const result = await deps.importConversation(blob, userAddress);
|
|
4505
|
+
if (result.success) {
|
|
4506
|
+
restored++;
|
|
4507
|
+
} else {
|
|
4508
|
+
failed++;
|
|
4509
|
+
}
|
|
4510
|
+
} catch (err) {
|
|
4511
|
+
if (isAuthError(err)) {
|
|
4512
|
+
try {
|
|
4513
|
+
currentToken = await deps.requestDropboxAccess();
|
|
4514
|
+
const blob = await downloadDropboxFile(currentToken, file.path_lower);
|
|
4515
|
+
const result = await deps.importConversation(blob, userAddress);
|
|
4516
|
+
if (result.success) {
|
|
4517
|
+
restored++;
|
|
4518
|
+
} else {
|
|
4519
|
+
failed++;
|
|
4520
|
+
}
|
|
4521
|
+
} catch {
|
|
4522
|
+
failed++;
|
|
4523
|
+
}
|
|
4524
|
+
} else {
|
|
4525
|
+
failed++;
|
|
4526
|
+
}
|
|
4527
|
+
}
|
|
4528
|
+
}
|
|
4529
|
+
return { success: true, restored, failed, total };
|
|
4530
|
+
}
|
|
4531
|
+
|
|
4532
|
+
// src/react/useDropboxAuth.ts
|
|
4533
|
+
var import_react10 = require("react");
|
|
4534
|
+
|
|
4535
|
+
// src/lib/backup/dropbox/auth.ts
|
|
4536
|
+
var DROPBOX_AUTH_URL = "https://www.dropbox.com/oauth2/authorize";
|
|
4537
|
+
var DROPBOX_TOKEN_URL = "https://api.dropboxapi.com/oauth2/token";
|
|
4538
|
+
var TOKEN_STORAGE_KEY = "dropbox_access_token";
|
|
4539
|
+
var VERIFIER_STORAGE_KEY = "dropbox_code_verifier";
|
|
4540
|
+
function generateCodeVerifier() {
|
|
4541
|
+
const array = new Uint8Array(32);
|
|
4542
|
+
crypto.getRandomValues(array);
|
|
4543
|
+
return Array.from(array, (byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
4544
|
+
}
|
|
4545
|
+
async function generateCodeChallenge(verifier) {
|
|
4546
|
+
const encoder = new TextEncoder();
|
|
4547
|
+
const data = encoder.encode(verifier);
|
|
4548
|
+
const hash = await crypto.subtle.digest("SHA-256", data);
|
|
4549
|
+
const base64 = btoa(String.fromCharCode(...new Uint8Array(hash)));
|
|
4550
|
+
return base64.replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
4551
|
+
}
|
|
4552
|
+
function getStoredToken() {
|
|
4553
|
+
if (typeof window === "undefined") return null;
|
|
4554
|
+
return sessionStorage.getItem(TOKEN_STORAGE_KEY);
|
|
4555
|
+
}
|
|
4556
|
+
function storeToken(token) {
|
|
4557
|
+
if (typeof window === "undefined") return;
|
|
4558
|
+
sessionStorage.setItem(TOKEN_STORAGE_KEY, token);
|
|
4559
|
+
}
|
|
4560
|
+
function clearToken() {
|
|
4561
|
+
if (typeof window === "undefined") return;
|
|
4562
|
+
sessionStorage.removeItem(TOKEN_STORAGE_KEY);
|
|
4563
|
+
}
|
|
4564
|
+
function getStoredVerifier() {
|
|
4565
|
+
if (typeof window === "undefined") return null;
|
|
4566
|
+
return sessionStorage.getItem(VERIFIER_STORAGE_KEY);
|
|
4567
|
+
}
|
|
4568
|
+
function storeVerifier(verifier) {
|
|
4569
|
+
if (typeof window === "undefined") return;
|
|
4570
|
+
sessionStorage.setItem(VERIFIER_STORAGE_KEY, verifier);
|
|
4571
|
+
}
|
|
4572
|
+
function clearVerifier() {
|
|
4573
|
+
if (typeof window === "undefined") return;
|
|
4574
|
+
sessionStorage.removeItem(VERIFIER_STORAGE_KEY);
|
|
4575
|
+
}
|
|
4576
|
+
function getRedirectUri(callbackPath) {
|
|
4577
|
+
if (typeof window === "undefined") return "";
|
|
4578
|
+
return `${window.location.origin}${callbackPath}`;
|
|
4579
|
+
}
|
|
4580
|
+
async function handleDropboxCallback(appKey, callbackPath) {
|
|
4581
|
+
if (typeof window === "undefined") return null;
|
|
4582
|
+
const url = new URL(window.location.href);
|
|
4583
|
+
const code = url.searchParams.get("code");
|
|
4584
|
+
const state = url.searchParams.get("state");
|
|
4585
|
+
if (!code || state !== "dropbox_auth") return null;
|
|
4586
|
+
const verifier = getStoredVerifier();
|
|
4587
|
+
if (!verifier) return null;
|
|
4588
|
+
try {
|
|
4589
|
+
const response = await fetch(DROPBOX_TOKEN_URL, {
|
|
4590
|
+
method: "POST",
|
|
4591
|
+
headers: {
|
|
4592
|
+
"Content-Type": "application/x-www-form-urlencoded"
|
|
4593
|
+
},
|
|
4594
|
+
body: new URLSearchParams({
|
|
4595
|
+
code,
|
|
4596
|
+
grant_type: "authorization_code",
|
|
4597
|
+
client_id: appKey,
|
|
4598
|
+
redirect_uri: getRedirectUri(callbackPath),
|
|
4599
|
+
code_verifier: verifier
|
|
4600
|
+
})
|
|
4601
|
+
});
|
|
4602
|
+
if (!response.ok) {
|
|
4603
|
+
throw new Error("Token exchange failed");
|
|
4604
|
+
}
|
|
4605
|
+
const data = await response.json();
|
|
4606
|
+
const token = data.access_token;
|
|
4607
|
+
if (typeof token !== "string" || token.trim() === "") {
|
|
4608
|
+
throw new Error("Invalid token response: access_token is missing or empty");
|
|
4609
|
+
}
|
|
4610
|
+
storeToken(token);
|
|
4611
|
+
clearVerifier();
|
|
4612
|
+
window.history.replaceState({}, "", window.location.pathname);
|
|
4613
|
+
return token;
|
|
4614
|
+
} catch {
|
|
4615
|
+
clearVerifier();
|
|
4616
|
+
return null;
|
|
4617
|
+
}
|
|
4618
|
+
}
|
|
4619
|
+
async function startDropboxAuth(appKey, callbackPath) {
|
|
4620
|
+
const verifier = generateCodeVerifier();
|
|
4621
|
+
const challenge = await generateCodeChallenge(verifier);
|
|
4622
|
+
storeVerifier(verifier);
|
|
4623
|
+
const params = new URLSearchParams({
|
|
4624
|
+
client_id: appKey,
|
|
4625
|
+
redirect_uri: getRedirectUri(callbackPath),
|
|
4626
|
+
response_type: "code",
|
|
4627
|
+
code_challenge: challenge,
|
|
4628
|
+
code_challenge_method: "S256",
|
|
4629
|
+
state: "dropbox_auth",
|
|
4630
|
+
token_access_type: "offline"
|
|
4631
|
+
});
|
|
4632
|
+
window.location.href = `${DROPBOX_AUTH_URL}?${params.toString()}`;
|
|
4633
|
+
return new Promise(() => {
|
|
4634
|
+
});
|
|
4635
|
+
}
|
|
4636
|
+
async function requestDropboxAccess(appKey, callbackPath) {
|
|
4637
|
+
if (!appKey) {
|
|
4638
|
+
throw new Error("Dropbox is not configured");
|
|
4639
|
+
}
|
|
4640
|
+
const storedToken = getStoredToken();
|
|
4641
|
+
if (storedToken) {
|
|
4642
|
+
return storedToken;
|
|
4643
|
+
}
|
|
4644
|
+
return startDropboxAuth(appKey, callbackPath);
|
|
4645
|
+
}
|
|
4646
|
+
|
|
4647
|
+
// src/react/useDropboxAuth.ts
|
|
4648
|
+
var DropboxAuthContext = (0, import_react10.createContext)(null);
|
|
4649
|
+
function DropboxAuthProvider({
|
|
4650
|
+
appKey,
|
|
4651
|
+
callbackPath = "/auth/dropbox/callback",
|
|
4652
|
+
children
|
|
4653
|
+
}) {
|
|
4654
|
+
const [accessToken, setAccessToken] = (0, import_react10.useState)(null);
|
|
4655
|
+
const isConfigured = !!appKey;
|
|
4656
|
+
(0, import_react10.useEffect)(() => {
|
|
4657
|
+
const storedToken = getStoredToken();
|
|
4658
|
+
if (storedToken) {
|
|
4659
|
+
setAccessToken(storedToken);
|
|
4660
|
+
}
|
|
4661
|
+
}, []);
|
|
4662
|
+
(0, import_react10.useEffect)(() => {
|
|
4663
|
+
if (!isConfigured || !appKey) return;
|
|
4664
|
+
const handleCallback = async () => {
|
|
4665
|
+
const token = await handleDropboxCallback(appKey, callbackPath);
|
|
4666
|
+
if (token) {
|
|
4667
|
+
setAccessToken(token);
|
|
4668
|
+
}
|
|
4669
|
+
};
|
|
4670
|
+
handleCallback();
|
|
4671
|
+
}, [appKey, callbackPath, isConfigured]);
|
|
4672
|
+
const requestAccess = (0, import_react10.useCallback)(async () => {
|
|
4673
|
+
if (!isConfigured || !appKey) {
|
|
4674
|
+
throw new Error("Dropbox is not configured");
|
|
4675
|
+
}
|
|
4676
|
+
if (accessToken) {
|
|
4677
|
+
return accessToken;
|
|
4678
|
+
}
|
|
4679
|
+
const storedToken = getStoredToken();
|
|
4680
|
+
if (storedToken) {
|
|
4681
|
+
setAccessToken(storedToken);
|
|
4682
|
+
return storedToken;
|
|
4683
|
+
}
|
|
4684
|
+
return requestDropboxAccess(appKey, callbackPath);
|
|
4685
|
+
}, [accessToken, appKey, callbackPath, isConfigured]);
|
|
4686
|
+
const logout = (0, import_react10.useCallback)(() => {
|
|
4687
|
+
clearToken();
|
|
4688
|
+
setAccessToken(null);
|
|
4689
|
+
}, []);
|
|
4690
|
+
return (0, import_react10.createElement)(
|
|
4691
|
+
DropboxAuthContext.Provider,
|
|
4692
|
+
{
|
|
4693
|
+
value: {
|
|
4694
|
+
accessToken,
|
|
4695
|
+
isAuthenticated: !!accessToken,
|
|
4696
|
+
isConfigured,
|
|
4697
|
+
requestAccess,
|
|
4698
|
+
logout
|
|
4699
|
+
}
|
|
4700
|
+
},
|
|
4701
|
+
children
|
|
4702
|
+
);
|
|
4703
|
+
}
|
|
4704
|
+
function useDropboxAuth() {
|
|
4705
|
+
const context = (0, import_react10.useContext)(DropboxAuthContext);
|
|
4706
|
+
if (!context) {
|
|
4707
|
+
throw new Error("useDropboxAuth must be used within DropboxAuthProvider");
|
|
4708
|
+
}
|
|
4709
|
+
return context;
|
|
4710
|
+
}
|
|
4711
|
+
|
|
4712
|
+
// src/react/useDropboxBackup.ts
|
|
4713
|
+
function useDropboxBackup(options) {
|
|
4714
|
+
const {
|
|
4715
|
+
database,
|
|
4716
|
+
userAddress,
|
|
4717
|
+
requestEncryptionKey: requestEncryptionKey2,
|
|
4718
|
+
exportConversation,
|
|
4719
|
+
importConversation,
|
|
4720
|
+
backupFolder = DEFAULT_BACKUP_FOLDER
|
|
4721
|
+
} = options;
|
|
4722
|
+
const {
|
|
4723
|
+
accessToken: dropboxToken,
|
|
4724
|
+
isConfigured: isDropboxConfigured,
|
|
4725
|
+
requestAccess: requestDropboxAccess2
|
|
4726
|
+
} = useDropboxAuth();
|
|
4727
|
+
const deps = (0, import_react11.useMemo)(
|
|
4728
|
+
() => ({
|
|
4729
|
+
requestDropboxAccess: requestDropboxAccess2,
|
|
4730
|
+
requestEncryptionKey: requestEncryptionKey2,
|
|
4731
|
+
exportConversation,
|
|
4732
|
+
importConversation
|
|
4733
|
+
}),
|
|
4734
|
+
[requestDropboxAccess2, requestEncryptionKey2, exportConversation, importConversation]
|
|
4735
|
+
);
|
|
4736
|
+
const ensureToken = (0, import_react11.useCallback)(async () => {
|
|
4737
|
+
if (dropboxToken) return dropboxToken;
|
|
4738
|
+
try {
|
|
4739
|
+
return await requestDropboxAccess2();
|
|
4740
|
+
} catch {
|
|
4741
|
+
return null;
|
|
4742
|
+
}
|
|
4743
|
+
}, [dropboxToken, requestDropboxAccess2]);
|
|
4744
|
+
const backup = (0, import_react11.useCallback)(
|
|
4745
|
+
async (backupOptions) => {
|
|
4746
|
+
if (!userAddress) {
|
|
4747
|
+
return { error: "Please sign in to backup to Dropbox" };
|
|
4748
|
+
}
|
|
4749
|
+
const token = await ensureToken();
|
|
4750
|
+
if (!token) {
|
|
4751
|
+
return { error: "Dropbox access denied" };
|
|
4752
|
+
}
|
|
4753
|
+
try {
|
|
4754
|
+
return await performDropboxExport(
|
|
4755
|
+
database,
|
|
4756
|
+
userAddress,
|
|
4757
|
+
token,
|
|
4758
|
+
deps,
|
|
4759
|
+
backupOptions?.onProgress,
|
|
4760
|
+
backupFolder
|
|
4761
|
+
);
|
|
4762
|
+
} catch (err) {
|
|
4763
|
+
return {
|
|
4764
|
+
error: err instanceof Error ? err.message : "Failed to backup to Dropbox"
|
|
4765
|
+
};
|
|
4766
|
+
}
|
|
4767
|
+
},
|
|
4768
|
+
[database, userAddress, ensureToken, deps, backupFolder]
|
|
4769
|
+
);
|
|
4770
|
+
const restore = (0, import_react11.useCallback)(
|
|
4771
|
+
async (restoreOptions) => {
|
|
4772
|
+
if (!userAddress) {
|
|
4773
|
+
return { error: "Please sign in to restore from Dropbox" };
|
|
4774
|
+
}
|
|
4775
|
+
const token = await ensureToken();
|
|
4776
|
+
if (!token) {
|
|
4777
|
+
return { error: "Dropbox access denied" };
|
|
4778
|
+
}
|
|
4779
|
+
try {
|
|
4780
|
+
return await performDropboxImport(
|
|
4781
|
+
userAddress,
|
|
4782
|
+
token,
|
|
4783
|
+
deps,
|
|
4784
|
+
restoreOptions?.onProgress,
|
|
4785
|
+
backupFolder
|
|
4786
|
+
);
|
|
4787
|
+
} catch (err) {
|
|
4788
|
+
return {
|
|
4789
|
+
error: err instanceof Error ? err.message : "Failed to restore from Dropbox"
|
|
4790
|
+
};
|
|
4791
|
+
}
|
|
4792
|
+
},
|
|
4793
|
+
[userAddress, ensureToken, deps, backupFolder]
|
|
4794
|
+
);
|
|
4795
|
+
return {
|
|
4796
|
+
backup,
|
|
4797
|
+
restore,
|
|
4798
|
+
isConfigured: isDropboxConfigured,
|
|
4799
|
+
isAuthenticated: !!dropboxToken
|
|
4800
|
+
};
|
|
4801
|
+
}
|
|
4802
|
+
|
|
4803
|
+
// src/react/useGoogleDriveBackup.ts
|
|
4804
|
+
var import_react12 = require("react");
|
|
4805
|
+
|
|
4806
|
+
// src/lib/backup/google/api.ts
|
|
4807
|
+
var DRIVE_API_URL = "https://www.googleapis.com/drive/v3";
|
|
4808
|
+
var DRIVE_UPLOAD_URL = "https://www.googleapis.com/upload/drive/v3";
|
|
4809
|
+
var FOLDER_MIME_TYPE = "application/vnd.google-apps.folder";
|
|
4810
|
+
function escapeQueryValue(value) {
|
|
4811
|
+
return value.replace(/'/g, "''");
|
|
4812
|
+
}
|
|
4813
|
+
var DEFAULT_ROOT_FOLDER = "ai-chat-app";
|
|
4814
|
+
var DEFAULT_CONVERSATIONS_FOLDER = "conversations";
|
|
4815
|
+
async function ensureFolder(accessToken, name, parentId) {
|
|
4816
|
+
const parentQuery = parentId ? `'${escapeQueryValue(parentId)}' in parents and ` : "";
|
|
4817
|
+
const query = `${parentQuery}mimeType='${FOLDER_MIME_TYPE}' and name='${escapeQueryValue(name)}' and trashed=false`;
|
|
4818
|
+
const response = await fetch(
|
|
4819
|
+
`${DRIVE_API_URL}/files?q=${encodeURIComponent(query)}&fields=files(id)`,
|
|
4820
|
+
{
|
|
4821
|
+
headers: { Authorization: `Bearer ${accessToken}` }
|
|
4822
|
+
}
|
|
4823
|
+
);
|
|
4824
|
+
if (!response.ok) {
|
|
4825
|
+
throw new Error(`Failed to search for folder ${name}: ${response.status}`);
|
|
4826
|
+
}
|
|
4827
|
+
const data = await response.json();
|
|
4828
|
+
if (data.files && data.files.length > 0) {
|
|
4829
|
+
return data.files[0].id;
|
|
4830
|
+
}
|
|
4831
|
+
const body = {
|
|
4832
|
+
name,
|
|
4833
|
+
mimeType: FOLDER_MIME_TYPE
|
|
4834
|
+
};
|
|
4835
|
+
if (parentId) {
|
|
4836
|
+
body.parents = [parentId];
|
|
4837
|
+
}
|
|
4838
|
+
const createResponse = await fetch(`${DRIVE_API_URL}/files`, {
|
|
4839
|
+
method: "POST",
|
|
4840
|
+
headers: {
|
|
4841
|
+
Authorization: `Bearer ${accessToken}`,
|
|
4842
|
+
"Content-Type": "application/json"
|
|
4843
|
+
},
|
|
4844
|
+
body: JSON.stringify(body)
|
|
4845
|
+
});
|
|
4846
|
+
if (!createResponse.ok) {
|
|
4847
|
+
throw new Error(`Failed to create folder ${name}: ${createResponse.status}`);
|
|
4848
|
+
}
|
|
4849
|
+
const folderData = await createResponse.json();
|
|
4850
|
+
return folderData.id;
|
|
4851
|
+
}
|
|
4852
|
+
async function getBackupFolder(accessToken, rootFolder = DEFAULT_ROOT_FOLDER, subfolder = DEFAULT_CONVERSATIONS_FOLDER) {
|
|
4853
|
+
const rootId = await ensureFolder(accessToken, rootFolder);
|
|
4854
|
+
return ensureFolder(accessToken, subfolder, rootId);
|
|
4855
|
+
}
|
|
4856
|
+
async function uploadFileToDrive(accessToken, folderId, content, filename) {
|
|
4857
|
+
const metadata = {
|
|
4858
|
+
name: filename,
|
|
4859
|
+
parents: [folderId],
|
|
4860
|
+
mimeType: "application/json"
|
|
4861
|
+
};
|
|
4862
|
+
const form = new FormData();
|
|
4863
|
+
form.append("metadata", new Blob([JSON.stringify(metadata)], { type: "application/json" }));
|
|
4864
|
+
form.append("file", content);
|
|
4865
|
+
const response = await fetch(`${DRIVE_UPLOAD_URL}/files?uploadType=multipart`, {
|
|
4866
|
+
method: "POST",
|
|
4867
|
+
headers: { Authorization: `Bearer ${accessToken}` },
|
|
4868
|
+
body: form
|
|
4869
|
+
});
|
|
4870
|
+
if (!response.ok) {
|
|
4871
|
+
const errorText = await response.text();
|
|
4872
|
+
throw new Error(`Drive upload failed: ${response.status} - ${errorText}`);
|
|
4873
|
+
}
|
|
4874
|
+
return response.json();
|
|
4875
|
+
}
|
|
4876
|
+
async function updateDriveFile(accessToken, fileId, content) {
|
|
4877
|
+
const response = await fetch(`${DRIVE_UPLOAD_URL}/files/${fileId}?uploadType=media`, {
|
|
4878
|
+
method: "PATCH",
|
|
4879
|
+
headers: {
|
|
4880
|
+
Authorization: `Bearer ${accessToken}`,
|
|
4881
|
+
"Content-Type": "application/json"
|
|
4882
|
+
},
|
|
4883
|
+
body: content
|
|
4884
|
+
});
|
|
4885
|
+
if (!response.ok) {
|
|
4886
|
+
const errorText = await response.text();
|
|
4887
|
+
throw new Error(`Drive update failed: ${response.status} - ${errorText}`);
|
|
4888
|
+
}
|
|
4889
|
+
return response.json();
|
|
4890
|
+
}
|
|
4891
|
+
async function listDriveFiles(accessToken, folderId) {
|
|
4892
|
+
const query = `'${escapeQueryValue(folderId)}' in parents and mimeType='application/json' and trashed=false`;
|
|
4893
|
+
const fields = "files(id,name,createdTime,modifiedTime,size)";
|
|
4894
|
+
const response = await fetch(
|
|
4895
|
+
`${DRIVE_API_URL}/files?q=${encodeURIComponent(query)}&fields=${fields}&pageSize=1000`,
|
|
4896
|
+
{
|
|
4897
|
+
headers: { Authorization: `Bearer ${accessToken}` }
|
|
4898
|
+
}
|
|
4899
|
+
);
|
|
4900
|
+
if (!response.ok) {
|
|
4901
|
+
throw new Error(`Failed to list files: ${response.status}`);
|
|
4902
|
+
}
|
|
4903
|
+
const data = await response.json();
|
|
4904
|
+
return data.files ?? [];
|
|
4905
|
+
}
|
|
4906
|
+
async function downloadDriveFile(accessToken, fileId) {
|
|
4907
|
+
const response = await fetch(`${DRIVE_API_URL}/files/${fileId}?alt=media`, {
|
|
4908
|
+
headers: { Authorization: `Bearer ${accessToken}` }
|
|
4909
|
+
});
|
|
4910
|
+
if (!response.ok) {
|
|
4911
|
+
throw new Error(`Failed to download file: ${response.status}`);
|
|
4912
|
+
}
|
|
4913
|
+
return response.blob();
|
|
4914
|
+
}
|
|
4915
|
+
async function findDriveFile(accessToken, folderId, filename) {
|
|
4916
|
+
const query = `'${escapeQueryValue(folderId)}' in parents and name='${escapeQueryValue(filename)}' and trashed=false`;
|
|
4917
|
+
const fields = "files(id,name,createdTime,modifiedTime,size)";
|
|
4918
|
+
const response = await fetch(
|
|
4919
|
+
`${DRIVE_API_URL}/files?q=${encodeURIComponent(query)}&fields=${fields}&pageSize=1`,
|
|
4920
|
+
{
|
|
4921
|
+
headers: { Authorization: `Bearer ${accessToken}` }
|
|
4922
|
+
}
|
|
4923
|
+
);
|
|
4924
|
+
if (!response.ok) {
|
|
4925
|
+
throw new Error(`Failed to find file: ${response.status}`);
|
|
4926
|
+
}
|
|
4927
|
+
const data = await response.json();
|
|
4928
|
+
return data.files?.[0] ?? null;
|
|
4929
|
+
}
|
|
4930
|
+
|
|
4931
|
+
// src/lib/backup/google/backup.ts
|
|
4932
|
+
var isAuthError2 = (err) => err instanceof Error && (err.message.includes("401") || err.message.includes("403"));
|
|
4933
|
+
async function getConversationsFolder(token, requestDriveAccess, rootFolder, subfolder) {
|
|
4934
|
+
try {
|
|
4935
|
+
const folderId = await getBackupFolder(token, rootFolder, subfolder);
|
|
4936
|
+
return { folderId, token };
|
|
4937
|
+
} catch (err) {
|
|
4938
|
+
if (isAuthError2(err)) {
|
|
4939
|
+
try {
|
|
4940
|
+
const newToken = await requestDriveAccess();
|
|
4941
|
+
const folderId = await getBackupFolder(newToken, rootFolder, subfolder);
|
|
4942
|
+
return { folderId, token: newToken };
|
|
4943
|
+
} catch {
|
|
4944
|
+
return null;
|
|
4945
|
+
}
|
|
4946
|
+
}
|
|
4947
|
+
throw err;
|
|
4948
|
+
}
|
|
4949
|
+
}
|
|
4950
|
+
async function pushConversationToDrive(database, conversationId, userAddress, token, deps, rootFolder = DEFAULT_ROOT_FOLDER, subfolder = DEFAULT_CONVERSATIONS_FOLDER, _retried = false) {
|
|
4951
|
+
try {
|
|
4952
|
+
await deps.requestEncryptionKey(userAddress);
|
|
4953
|
+
const folderResult = await getConversationsFolder(token, deps.requestDriveAccess, rootFolder, subfolder);
|
|
4954
|
+
if (!folderResult) return "failed";
|
|
4955
|
+
const { folderId, token: activeToken } = folderResult;
|
|
4956
|
+
const filename = `${conversationId}.json`;
|
|
4957
|
+
const existingFile = await findDriveFile(activeToken, folderId, filename);
|
|
4958
|
+
if (existingFile) {
|
|
4959
|
+
const { Q: Q4 } = await import("@nozbe/watermelondb");
|
|
4960
|
+
const conversationsCollection = database.get("conversations");
|
|
4961
|
+
const records = await conversationsCollection.query(Q4.where("conversation_id", conversationId)).fetch();
|
|
4962
|
+
if (records.length > 0) {
|
|
4963
|
+
const conversation = conversationToStored(records[0]);
|
|
4964
|
+
const localUpdated = conversation.updatedAt.getTime();
|
|
4965
|
+
const remoteModified = new Date(existingFile.modifiedTime).getTime();
|
|
4966
|
+
if (localUpdated <= remoteModified) {
|
|
4967
|
+
return "skipped";
|
|
4968
|
+
}
|
|
4969
|
+
}
|
|
4970
|
+
}
|
|
4971
|
+
const exportResult = await deps.exportConversation(conversationId, userAddress);
|
|
4972
|
+
if (!exportResult.success || !exportResult.blob) {
|
|
4973
|
+
return "failed";
|
|
4974
|
+
}
|
|
4975
|
+
if (existingFile) {
|
|
4976
|
+
await updateDriveFile(activeToken, existingFile.id, exportResult.blob);
|
|
4977
|
+
} else {
|
|
4978
|
+
await uploadFileToDrive(activeToken, folderId, exportResult.blob, filename);
|
|
4979
|
+
}
|
|
4980
|
+
return "uploaded";
|
|
4981
|
+
} catch (err) {
|
|
4982
|
+
if (isAuthError2(err) && !_retried) {
|
|
4983
|
+
try {
|
|
4984
|
+
const newToken = await deps.requestDriveAccess();
|
|
4985
|
+
return pushConversationToDrive(database, conversationId, userAddress, newToken, deps, rootFolder, subfolder, true);
|
|
4986
|
+
} catch {
|
|
4987
|
+
return "failed";
|
|
4988
|
+
}
|
|
4989
|
+
}
|
|
4990
|
+
return "failed";
|
|
4991
|
+
}
|
|
4992
|
+
}
|
|
4993
|
+
async function performGoogleDriveExport(database, userAddress, token, deps, onProgress, rootFolder = DEFAULT_ROOT_FOLDER, subfolder = DEFAULT_CONVERSATIONS_FOLDER) {
|
|
4994
|
+
await deps.requestEncryptionKey(userAddress);
|
|
4995
|
+
const folderResult = await getConversationsFolder(token, deps.requestDriveAccess, rootFolder, subfolder);
|
|
4996
|
+
if (!folderResult) {
|
|
4997
|
+
return { success: false, uploaded: 0, skipped: 0, total: 0 };
|
|
4998
|
+
}
|
|
4999
|
+
const { token: activeToken } = folderResult;
|
|
5000
|
+
const { Q: Q4 } = await import("@nozbe/watermelondb");
|
|
5001
|
+
const conversationsCollection = database.get("conversations");
|
|
5002
|
+
const records = await conversationsCollection.query(Q4.where("is_deleted", false)).fetch();
|
|
5003
|
+
const conversations = records.map(conversationToStored);
|
|
5004
|
+
const total = conversations.length;
|
|
5005
|
+
if (total === 0) {
|
|
5006
|
+
return { success: true, uploaded: 0, skipped: 0, total: 0 };
|
|
5007
|
+
}
|
|
5008
|
+
let uploaded = 0;
|
|
5009
|
+
let skipped = 0;
|
|
5010
|
+
for (let i = 0; i < conversations.length; i++) {
|
|
5011
|
+
const conv = conversations[i];
|
|
5012
|
+
onProgress?.(i + 1, total);
|
|
5013
|
+
const result = await pushConversationToDrive(
|
|
5014
|
+
database,
|
|
5015
|
+
conv.conversationId,
|
|
5016
|
+
userAddress,
|
|
5017
|
+
activeToken,
|
|
5018
|
+
deps,
|
|
5019
|
+
rootFolder,
|
|
5020
|
+
subfolder
|
|
5021
|
+
);
|
|
5022
|
+
if (result === "uploaded") uploaded++;
|
|
5023
|
+
if (result === "skipped") skipped++;
|
|
5024
|
+
}
|
|
5025
|
+
return { success: true, uploaded, skipped, total };
|
|
5026
|
+
}
|
|
5027
|
+
async function performGoogleDriveImport(userAddress, token, deps, onProgress, rootFolder = DEFAULT_ROOT_FOLDER, subfolder = DEFAULT_CONVERSATIONS_FOLDER) {
|
|
5028
|
+
await deps.requestEncryptionKey(userAddress);
|
|
5029
|
+
const folderResult = await getConversationsFolder(token, deps.requestDriveAccess, rootFolder, subfolder);
|
|
5030
|
+
if (!folderResult) {
|
|
5031
|
+
return { success: false, restored: 0, failed: 0, total: 0, noBackupsFound: true };
|
|
5032
|
+
}
|
|
5033
|
+
const { folderId, token: activeToken } = folderResult;
|
|
5034
|
+
const remoteFiles = await listDriveFiles(activeToken, folderId);
|
|
5035
|
+
if (remoteFiles.length === 0) {
|
|
5036
|
+
return { success: false, restored: 0, failed: 0, total: 0, noBackupsFound: true };
|
|
5037
|
+
}
|
|
5038
|
+
const jsonFiles = remoteFiles.filter((file) => file.name.endsWith(".json"));
|
|
5039
|
+
const total = jsonFiles.length;
|
|
5040
|
+
let restored = 0;
|
|
5041
|
+
let failed = 0;
|
|
5042
|
+
for (let i = 0; i < jsonFiles.length; i++) {
|
|
5043
|
+
const file = jsonFiles[i];
|
|
5044
|
+
onProgress?.(i + 1, total);
|
|
5045
|
+
try {
|
|
5046
|
+
const blob = await downloadDriveFile(activeToken, file.id);
|
|
5047
|
+
const result = await deps.importConversation(blob, userAddress);
|
|
5048
|
+
if (result.success) {
|
|
5049
|
+
restored++;
|
|
5050
|
+
} else {
|
|
5051
|
+
failed++;
|
|
5052
|
+
}
|
|
5053
|
+
} catch {
|
|
5054
|
+
failed++;
|
|
5055
|
+
}
|
|
5056
|
+
}
|
|
5057
|
+
return { success: true, restored, failed, total };
|
|
5058
|
+
}
|
|
5059
|
+
|
|
5060
|
+
// src/react/useGoogleDriveBackup.ts
|
|
5061
|
+
function useGoogleDriveBackup(options) {
|
|
5062
|
+
const {
|
|
5063
|
+
database,
|
|
5064
|
+
userAddress,
|
|
5065
|
+
accessToken,
|
|
5066
|
+
requestDriveAccess,
|
|
5067
|
+
requestEncryptionKey: requestEncryptionKey2,
|
|
5068
|
+
exportConversation,
|
|
5069
|
+
importConversation,
|
|
5070
|
+
rootFolder = DEFAULT_ROOT_FOLDER,
|
|
5071
|
+
conversationsFolder = DEFAULT_CONVERSATIONS_FOLDER
|
|
5072
|
+
} = options;
|
|
5073
|
+
const deps = (0, import_react12.useMemo)(
|
|
5074
|
+
() => ({
|
|
5075
|
+
requestDriveAccess,
|
|
5076
|
+
requestEncryptionKey: requestEncryptionKey2,
|
|
5077
|
+
exportConversation,
|
|
5078
|
+
importConversation
|
|
5079
|
+
}),
|
|
5080
|
+
[
|
|
5081
|
+
requestDriveAccess,
|
|
5082
|
+
requestEncryptionKey2,
|
|
5083
|
+
exportConversation,
|
|
5084
|
+
importConversation
|
|
5085
|
+
]
|
|
5086
|
+
);
|
|
5087
|
+
const ensureToken = (0, import_react12.useCallback)(async () => {
|
|
5088
|
+
if (accessToken) return accessToken;
|
|
5089
|
+
try {
|
|
5090
|
+
return await requestDriveAccess();
|
|
5091
|
+
} catch {
|
|
5092
|
+
return null;
|
|
5093
|
+
}
|
|
5094
|
+
}, [accessToken, requestDriveAccess]);
|
|
5095
|
+
const backup = (0, import_react12.useCallback)(
|
|
5096
|
+
async (backupOptions) => {
|
|
5097
|
+
if (!userAddress) {
|
|
5098
|
+
return { error: "Please sign in to backup to Google Drive" };
|
|
5099
|
+
}
|
|
5100
|
+
const token = await ensureToken();
|
|
5101
|
+
if (!token) {
|
|
5102
|
+
return { error: "Google Drive access denied" };
|
|
5103
|
+
}
|
|
5104
|
+
try {
|
|
5105
|
+
return await performGoogleDriveExport(
|
|
5106
|
+
database,
|
|
5107
|
+
userAddress,
|
|
5108
|
+
token,
|
|
5109
|
+
deps,
|
|
5110
|
+
backupOptions?.onProgress,
|
|
5111
|
+
rootFolder,
|
|
5112
|
+
conversationsFolder
|
|
5113
|
+
);
|
|
5114
|
+
} catch (err) {
|
|
5115
|
+
return {
|
|
5116
|
+
error: err instanceof Error ? err.message : "Failed to backup to Google Drive"
|
|
5117
|
+
};
|
|
5118
|
+
}
|
|
5119
|
+
},
|
|
5120
|
+
[database, userAddress, ensureToken, deps, rootFolder, conversationsFolder]
|
|
5121
|
+
);
|
|
5122
|
+
const restore = (0, import_react12.useCallback)(
|
|
5123
|
+
async (restoreOptions) => {
|
|
5124
|
+
if (!userAddress) {
|
|
5125
|
+
return { error: "Please sign in to restore from Google Drive" };
|
|
5126
|
+
}
|
|
5127
|
+
const token = await ensureToken();
|
|
5128
|
+
if (!token) {
|
|
5129
|
+
return { error: "Google Drive access denied" };
|
|
5130
|
+
}
|
|
5131
|
+
try {
|
|
5132
|
+
return await performGoogleDriveImport(
|
|
5133
|
+
userAddress,
|
|
5134
|
+
token,
|
|
5135
|
+
deps,
|
|
5136
|
+
restoreOptions?.onProgress,
|
|
5137
|
+
rootFolder,
|
|
5138
|
+
conversationsFolder
|
|
5139
|
+
);
|
|
5140
|
+
} catch (err) {
|
|
5141
|
+
return {
|
|
5142
|
+
error: err instanceof Error ? err.message : "Failed to restore from Google Drive"
|
|
5143
|
+
};
|
|
5144
|
+
}
|
|
5145
|
+
},
|
|
5146
|
+
[userAddress, ensureToken, deps, rootFolder, conversationsFolder]
|
|
5147
|
+
);
|
|
5148
|
+
return {
|
|
5149
|
+
backup,
|
|
5150
|
+
restore,
|
|
5151
|
+
isAuthenticated: !!accessToken
|
|
5152
|
+
};
|
|
5153
|
+
}
|
|
4035
5154
|
// Annotate the CommonJS export names for ESM import in node:
|
|
4036
5155
|
0 && (module.exports = {
|
|
4037
5156
|
ChatConversation,
|
|
4038
5157
|
ChatMessage,
|
|
5158
|
+
DEFAULT_BACKUP_FOLDER,
|
|
5159
|
+
DEFAULT_DRIVE_CONVERSATIONS_FOLDER,
|
|
5160
|
+
DEFAULT_DRIVE_ROOT_FOLDER,
|
|
4039
5161
|
DEFAULT_TOOL_SELECTOR_MODEL,
|
|
5162
|
+
DropboxAuthProvider,
|
|
4040
5163
|
StoredMemoryModel,
|
|
5164
|
+
StoredModelPreferenceModel,
|
|
5165
|
+
chatStorageMigrations,
|
|
4041
5166
|
chatStorageSchema,
|
|
5167
|
+
clearDropboxToken,
|
|
4042
5168
|
createMemoryContextSystemMessage,
|
|
4043
5169
|
decryptData,
|
|
4044
5170
|
decryptDataBytes,
|
|
@@ -4049,17 +5175,24 @@ var extractConversationContext = (messages, maxMessages = 3) => {
|
|
|
4049
5175
|
generateCompositeKey,
|
|
4050
5176
|
generateConversationId,
|
|
4051
5177
|
generateUniqueKey,
|
|
5178
|
+
getDropboxToken,
|
|
4052
5179
|
hasEncryptionKey,
|
|
4053
5180
|
memoryStorageSchema,
|
|
4054
5181
|
requestEncryptionKey,
|
|
4055
5182
|
selectTool,
|
|
5183
|
+
settingsStorageSchema,
|
|
5184
|
+
storeDropboxToken,
|
|
4056
5185
|
useChat,
|
|
4057
5186
|
useChatStorage,
|
|
5187
|
+
useDropboxAuth,
|
|
5188
|
+
useDropboxBackup,
|
|
4058
5189
|
useEncryption,
|
|
5190
|
+
useGoogleDriveBackup,
|
|
4059
5191
|
useImageGeneration,
|
|
4060
5192
|
useMemoryStorage,
|
|
4061
5193
|
useModels,
|
|
4062
5194
|
useOCR,
|
|
4063
5195
|
usePdf,
|
|
4064
|
-
useSearch
|
|
5196
|
+
useSearch,
|
|
5197
|
+
useSettings
|
|
4065
5198
|
});
|