@reverbia/sdk 1.0.0-next.20251217134403 → 1.0.0-next.20251217144909
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 +240 -318
- package/dist/expo/index.d.mts +74 -432
- package/dist/expo/index.d.ts +74 -432
- package/dist/expo/index.mjs +225 -297
- package/dist/index.cjs +46 -2
- package/dist/index.d.mts +175 -2
- package/dist/index.d.ts +175 -2
- package/dist/index.mjs +41 -1
- package/dist/react/index.cjs +1203 -350
- package/dist/react/index.d.mts +423 -465
- package/dist/react/index.d.ts +423 -465
- package/dist/react/index.mjs +1179 -323
- package/package.json +2 -2
package/dist/react/index.mjs
CHANGED
|
@@ -1,3 +1,14 @@
|
|
|
1
|
+
var __defProp = Object.defineProperty;
|
|
2
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
3
|
+
var __decorateClass = (decorators, target, key, kind) => {
|
|
4
|
+
var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc(target, key) : target;
|
|
5
|
+
for (var i = decorators.length - 1, decorator; i >= 0; i--)
|
|
6
|
+
if (decorator = decorators[i])
|
|
7
|
+
result = (kind ? decorator(target, key, result) : decorator(result)) || result;
|
|
8
|
+
if (kind && result) __defProp(target, key, result);
|
|
9
|
+
return result;
|
|
10
|
+
};
|
|
11
|
+
|
|
1
12
|
// src/react/useChat.ts
|
|
2
13
|
import { useCallback, useEffect, useRef, useState } from "react";
|
|
3
14
|
|
|
@@ -991,11 +1002,11 @@ async function generateLocalChatCompletion(messages, options = {}) {
|
|
|
991
1002
|
});
|
|
992
1003
|
this.cb = cb;
|
|
993
1004
|
}
|
|
994
|
-
on_finalized_text(
|
|
1005
|
+
on_finalized_text(text4) {
|
|
995
1006
|
if (signal?.aborted) {
|
|
996
1007
|
throw new Error("AbortError");
|
|
997
1008
|
}
|
|
998
|
-
this.cb(
|
|
1009
|
+
this.cb(text4);
|
|
999
1010
|
}
|
|
1000
1011
|
}
|
|
1001
1012
|
const streamer = onToken ? new CallbackStreamer(chatPipeline.tokenizer, onToken) : void 0;
|
|
@@ -1669,7 +1680,137 @@ function useEncryption(signMessage) {
|
|
|
1669
1680
|
// src/react/useChatStorage.ts
|
|
1670
1681
|
import { useCallback as useCallback2, useState as useState2, useMemo } from "react";
|
|
1671
1682
|
|
|
1672
|
-
// src/lib/
|
|
1683
|
+
// src/lib/db/chat/schema.ts
|
|
1684
|
+
import { appSchema, tableSchema } from "@nozbe/watermelondb";
|
|
1685
|
+
import {
|
|
1686
|
+
schemaMigrations,
|
|
1687
|
+
addColumns
|
|
1688
|
+
} from "@nozbe/watermelondb/Schema/migrations";
|
|
1689
|
+
var chatStorageSchema = appSchema({
|
|
1690
|
+
version: 2,
|
|
1691
|
+
tables: [
|
|
1692
|
+
tableSchema({
|
|
1693
|
+
name: "history",
|
|
1694
|
+
columns: [
|
|
1695
|
+
{ name: "message_id", type: "number" },
|
|
1696
|
+
{ name: "conversation_id", type: "string", isIndexed: true },
|
|
1697
|
+
{ name: "role", type: "string", isIndexed: true },
|
|
1698
|
+
{ name: "content", type: "string" },
|
|
1699
|
+
{ name: "model", type: "string", isOptional: true },
|
|
1700
|
+
{ name: "files", type: "string", isOptional: true },
|
|
1701
|
+
{ name: "created_at", type: "number", isIndexed: true },
|
|
1702
|
+
{ name: "updated_at", type: "number" },
|
|
1703
|
+
{ name: "vector", type: "string", isOptional: true },
|
|
1704
|
+
{ name: "embedding_model", type: "string", isOptional: true },
|
|
1705
|
+
{ name: "usage", type: "string", isOptional: true },
|
|
1706
|
+
{ name: "sources", type: "string", isOptional: true },
|
|
1707
|
+
{ name: "response_duration", type: "number", isOptional: true },
|
|
1708
|
+
{ name: "was_stopped", type: "boolean", isOptional: true }
|
|
1709
|
+
]
|
|
1710
|
+
}),
|
|
1711
|
+
tableSchema({
|
|
1712
|
+
name: "conversations",
|
|
1713
|
+
columns: [
|
|
1714
|
+
{ name: "conversation_id", type: "string", isIndexed: true },
|
|
1715
|
+
{ name: "title", type: "string" },
|
|
1716
|
+
{ name: "created_at", type: "number" },
|
|
1717
|
+
{ name: "updated_at", type: "number" },
|
|
1718
|
+
{ name: "is_deleted", type: "boolean", isIndexed: true }
|
|
1719
|
+
]
|
|
1720
|
+
})
|
|
1721
|
+
]
|
|
1722
|
+
});
|
|
1723
|
+
var chatStorageMigrations = schemaMigrations({
|
|
1724
|
+
migrations: [
|
|
1725
|
+
{
|
|
1726
|
+
toVersion: 2,
|
|
1727
|
+
steps: [
|
|
1728
|
+
addColumns({
|
|
1729
|
+
table: "history",
|
|
1730
|
+
columns: [{ name: "was_stopped", type: "boolean", isOptional: true }]
|
|
1731
|
+
})
|
|
1732
|
+
]
|
|
1733
|
+
}
|
|
1734
|
+
]
|
|
1735
|
+
});
|
|
1736
|
+
|
|
1737
|
+
// src/lib/db/chat/models.ts
|
|
1738
|
+
import { Model } from "@nozbe/watermelondb";
|
|
1739
|
+
import { field, text, date, json } from "@nozbe/watermelondb/decorators";
|
|
1740
|
+
var Message = class extends Model {
|
|
1741
|
+
};
|
|
1742
|
+
Message.table = "history";
|
|
1743
|
+
Message.associations = {
|
|
1744
|
+
conversations: { type: "belongs_to", key: "conversation_id" }
|
|
1745
|
+
};
|
|
1746
|
+
__decorateClass([
|
|
1747
|
+
field("message_id")
|
|
1748
|
+
], Message.prototype, "messageId", 2);
|
|
1749
|
+
__decorateClass([
|
|
1750
|
+
text("conversation_id")
|
|
1751
|
+
], Message.prototype, "conversationId", 2);
|
|
1752
|
+
__decorateClass([
|
|
1753
|
+
text("role")
|
|
1754
|
+
], Message.prototype, "role", 2);
|
|
1755
|
+
__decorateClass([
|
|
1756
|
+
text("content")
|
|
1757
|
+
], Message.prototype, "content", 2);
|
|
1758
|
+
__decorateClass([
|
|
1759
|
+
text("model")
|
|
1760
|
+
], Message.prototype, "model", 2);
|
|
1761
|
+
__decorateClass([
|
|
1762
|
+
json("files", (json3) => json3)
|
|
1763
|
+
], Message.prototype, "files", 2);
|
|
1764
|
+
__decorateClass([
|
|
1765
|
+
date("created_at")
|
|
1766
|
+
], Message.prototype, "createdAt", 2);
|
|
1767
|
+
__decorateClass([
|
|
1768
|
+
date("updated_at")
|
|
1769
|
+
], Message.prototype, "updatedAt", 2);
|
|
1770
|
+
__decorateClass([
|
|
1771
|
+
json("vector", (json3) => json3)
|
|
1772
|
+
], Message.prototype, "vector", 2);
|
|
1773
|
+
__decorateClass([
|
|
1774
|
+
text("embedding_model")
|
|
1775
|
+
], Message.prototype, "embeddingModel", 2);
|
|
1776
|
+
__decorateClass([
|
|
1777
|
+
json("usage", (json3) => json3)
|
|
1778
|
+
], Message.prototype, "usage", 2);
|
|
1779
|
+
__decorateClass([
|
|
1780
|
+
json("sources", (json3) => json3)
|
|
1781
|
+
], Message.prototype, "sources", 2);
|
|
1782
|
+
__decorateClass([
|
|
1783
|
+
field("response_duration")
|
|
1784
|
+
], Message.prototype, "responseDuration", 2);
|
|
1785
|
+
__decorateClass([
|
|
1786
|
+
field("was_stopped")
|
|
1787
|
+
], Message.prototype, "wasStopped", 2);
|
|
1788
|
+
var Conversation = class extends Model {
|
|
1789
|
+
};
|
|
1790
|
+
Conversation.table = "conversations";
|
|
1791
|
+
Conversation.associations = {
|
|
1792
|
+
history: { type: "has_many", foreignKey: "conversation_id" }
|
|
1793
|
+
};
|
|
1794
|
+
__decorateClass([
|
|
1795
|
+
text("conversation_id")
|
|
1796
|
+
], Conversation.prototype, "conversationId", 2);
|
|
1797
|
+
__decorateClass([
|
|
1798
|
+
text("title")
|
|
1799
|
+
], Conversation.prototype, "title", 2);
|
|
1800
|
+
__decorateClass([
|
|
1801
|
+
date("created_at")
|
|
1802
|
+
], Conversation.prototype, "createdAt", 2);
|
|
1803
|
+
__decorateClass([
|
|
1804
|
+
date("updated_at")
|
|
1805
|
+
], Conversation.prototype, "updatedAt", 2);
|
|
1806
|
+
__decorateClass([
|
|
1807
|
+
field("is_deleted")
|
|
1808
|
+
], Conversation.prototype, "isDeleted", 2);
|
|
1809
|
+
|
|
1810
|
+
// src/lib/db/chat/types.ts
|
|
1811
|
+
function generateConversationId() {
|
|
1812
|
+
return `conv_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`;
|
|
1813
|
+
}
|
|
1673
1814
|
function convertUsageToStored(usage) {
|
|
1674
1815
|
if (!usage) return void 0;
|
|
1675
1816
|
return {
|
|
@@ -1679,11 +1820,8 @@ function convertUsageToStored(usage) {
|
|
|
1679
1820
|
costMicroUsd: usage.cost_micro_usd
|
|
1680
1821
|
};
|
|
1681
1822
|
}
|
|
1682
|
-
function generateConversationId() {
|
|
1683
|
-
return `conv_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`;
|
|
1684
|
-
}
|
|
1685
1823
|
|
|
1686
|
-
// src/lib/
|
|
1824
|
+
// src/lib/db/chat/operations.ts
|
|
1687
1825
|
import { Q } from "@nozbe/watermelondb";
|
|
1688
1826
|
function messageToStored(message) {
|
|
1689
1827
|
return {
|
|
@@ -2189,191 +2327,87 @@ function useChatStorage(options) {
|
|
|
2189
2327
|
};
|
|
2190
2328
|
}
|
|
2191
2329
|
|
|
2192
|
-
// src/
|
|
2193
|
-
import {
|
|
2194
|
-
import {
|
|
2195
|
-
|
|
2196
|
-
|
|
2330
|
+
// src/react/useMemoryStorage.ts
|
|
2331
|
+
import { useCallback as useCallback3, useState as useState3, useMemo as useMemo2, useRef as useRef2 } from "react";
|
|
2332
|
+
import { postApiV1ChatCompletions } from "@reverbia/sdk";
|
|
2333
|
+
|
|
2334
|
+
// src/lib/db/memory/schema.ts
|
|
2335
|
+
import { appSchema as appSchema2, tableSchema as tableSchema2 } from "@nozbe/watermelondb";
|
|
2336
|
+
var memoryStorageSchema = appSchema2({
|
|
2337
|
+
version: 1,
|
|
2197
2338
|
tables: [
|
|
2198
|
-
|
|
2199
|
-
name: "
|
|
2339
|
+
tableSchema2({
|
|
2340
|
+
name: "memories",
|
|
2200
2341
|
columns: [
|
|
2201
|
-
{ name: "
|
|
2202
|
-
|
|
2203
|
-
{ name: "
|
|
2204
|
-
{ name: "
|
|
2205
|
-
|
|
2206
|
-
{ name: "
|
|
2207
|
-
{ name: "
|
|
2208
|
-
{ name: "
|
|
2209
|
-
|
|
2342
|
+
{ name: "type", type: "string", isIndexed: true },
|
|
2343
|
+
{ name: "namespace", type: "string", isIndexed: true },
|
|
2344
|
+
{ name: "key", type: "string", isIndexed: true },
|
|
2345
|
+
{ name: "value", type: "string" },
|
|
2346
|
+
{ name: "raw_evidence", type: "string" },
|
|
2347
|
+
{ name: "confidence", type: "number" },
|
|
2348
|
+
{ name: "pii", type: "boolean", isIndexed: true },
|
|
2349
|
+
{ name: "composite_key", type: "string", isIndexed: true },
|
|
2350
|
+
{ name: "unique_key", type: "string", isIndexed: true },
|
|
2210
2351
|
{ name: "created_at", type: "number", isIndexed: true },
|
|
2211
2352
|
{ name: "updated_at", type: "number" },
|
|
2212
|
-
{ name: "
|
|
2213
|
-
// JSON stringified number[]
|
|
2353
|
+
{ name: "embedding", type: "string", isOptional: true },
|
|
2214
2354
|
{ name: "embedding_model", type: "string", isOptional: true },
|
|
2215
|
-
{ name: "usage", type: "string", isOptional: true },
|
|
2216
|
-
// JSON stringified ChatCompletionUsage
|
|
2217
|
-
{ name: "sources", type: "string", isOptional: true },
|
|
2218
|
-
// JSON stringified SearchSource[]
|
|
2219
|
-
{ name: "response_duration", type: "number", isOptional: true },
|
|
2220
|
-
{ name: "was_stopped", type: "boolean", isOptional: true }
|
|
2221
|
-
]
|
|
2222
|
-
}),
|
|
2223
|
-
tableSchema({
|
|
2224
|
-
name: "conversations",
|
|
2225
|
-
columns: [
|
|
2226
|
-
{ name: "conversation_id", type: "string", isIndexed: true },
|
|
2227
|
-
{ name: "title", type: "string" },
|
|
2228
|
-
{ name: "created_at", type: "number" },
|
|
2229
|
-
{ name: "updated_at", type: "number" },
|
|
2230
2355
|
{ name: "is_deleted", type: "boolean", isIndexed: true }
|
|
2231
2356
|
]
|
|
2232
2357
|
})
|
|
2233
2358
|
]
|
|
2234
2359
|
});
|
|
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
|
-
});
|
|
2250
2360
|
|
|
2251
|
-
// src/lib/
|
|
2252
|
-
import { Model } from "@nozbe/watermelondb";
|
|
2253
|
-
|
|
2254
|
-
|
|
2255
|
-
get messageId() {
|
|
2256
|
-
return this._getRaw("message_id");
|
|
2257
|
-
}
|
|
2258
|
-
/** Links message to its conversation */
|
|
2259
|
-
get conversationId() {
|
|
2260
|
-
return this._getRaw("conversation_id");
|
|
2261
|
-
}
|
|
2262
|
-
/** Who sent the message: 'user' | 'assistant' | 'system' */
|
|
2263
|
-
get role() {
|
|
2264
|
-
return this._getRaw("role");
|
|
2265
|
-
}
|
|
2266
|
-
/** The message text content */
|
|
2267
|
-
get content() {
|
|
2268
|
-
return this._getRaw("content");
|
|
2269
|
-
}
|
|
2270
|
-
/** LLM model used (e.g., GPT-4, Claude) */
|
|
2271
|
-
get model() {
|
|
2272
|
-
const value = this._getRaw("model");
|
|
2273
|
-
return value ? value : void 0;
|
|
2274
|
-
}
|
|
2275
|
-
/** Optional attached files */
|
|
2276
|
-
get files() {
|
|
2277
|
-
const raw = this._getRaw("files");
|
|
2278
|
-
if (!raw) return void 0;
|
|
2279
|
-
try {
|
|
2280
|
-
return JSON.parse(raw);
|
|
2281
|
-
} catch {
|
|
2282
|
-
return void 0;
|
|
2283
|
-
}
|
|
2284
|
-
}
|
|
2285
|
-
/** Created timestamp */
|
|
2286
|
-
get createdAt() {
|
|
2287
|
-
return new Date(this._getRaw("created_at"));
|
|
2288
|
-
}
|
|
2289
|
-
/** Updated timestamp */
|
|
2290
|
-
get updatedAt() {
|
|
2291
|
-
return new Date(this._getRaw("updated_at"));
|
|
2292
|
-
}
|
|
2293
|
-
/** Embedding vector for semantic search */
|
|
2294
|
-
get vector() {
|
|
2295
|
-
const raw = this._getRaw("vector");
|
|
2296
|
-
if (!raw) return void 0;
|
|
2297
|
-
try {
|
|
2298
|
-
return JSON.parse(raw);
|
|
2299
|
-
} catch {
|
|
2300
|
-
return void 0;
|
|
2301
|
-
}
|
|
2302
|
-
}
|
|
2303
|
-
/** Model used to generate embedding */
|
|
2304
|
-
get embeddingModel() {
|
|
2305
|
-
const value = this._getRaw("embedding_model");
|
|
2306
|
-
return value ? value : void 0;
|
|
2307
|
-
}
|
|
2308
|
-
/** Token counts and cost */
|
|
2309
|
-
get usage() {
|
|
2310
|
-
const raw = this._getRaw("usage");
|
|
2311
|
-
if (!raw) return void 0;
|
|
2312
|
-
try {
|
|
2313
|
-
return JSON.parse(raw);
|
|
2314
|
-
} catch {
|
|
2315
|
-
return void 0;
|
|
2316
|
-
}
|
|
2317
|
-
}
|
|
2318
|
-
/** Web search sources */
|
|
2319
|
-
get sources() {
|
|
2320
|
-
const raw = this._getRaw("sources");
|
|
2321
|
-
if (!raw) return void 0;
|
|
2322
|
-
try {
|
|
2323
|
-
return JSON.parse(raw);
|
|
2324
|
-
} catch {
|
|
2325
|
-
return void 0;
|
|
2326
|
-
}
|
|
2327
|
-
}
|
|
2328
|
-
/** Response time in seconds */
|
|
2329
|
-
get responseDuration() {
|
|
2330
|
-
const value = this._getRaw("response_duration");
|
|
2331
|
-
return value !== null && value !== void 0 ? value : void 0;
|
|
2332
|
-
}
|
|
2333
|
-
/** Whether the message generation was stopped by the user */
|
|
2334
|
-
get wasStopped() {
|
|
2335
|
-
return this._getRaw("was_stopped");
|
|
2336
|
-
}
|
|
2337
|
-
};
|
|
2338
|
-
Message.table = "history";
|
|
2339
|
-
Message.associations = {
|
|
2340
|
-
conversations: { type: "belongs_to", key: "conversation_id" }
|
|
2341
|
-
};
|
|
2342
|
-
var Conversation = class extends Model {
|
|
2343
|
-
/** Unique conversation identifier */
|
|
2344
|
-
get conversationId() {
|
|
2345
|
-
return this._getRaw("conversation_id");
|
|
2346
|
-
}
|
|
2347
|
-
/** Conversation title */
|
|
2348
|
-
get title() {
|
|
2349
|
-
return this._getRaw("title");
|
|
2350
|
-
}
|
|
2351
|
-
/** Created timestamp */
|
|
2352
|
-
get createdAt() {
|
|
2353
|
-
return new Date(this._getRaw("created_at"));
|
|
2354
|
-
}
|
|
2355
|
-
/** Updated timestamp */
|
|
2356
|
-
get updatedAt() {
|
|
2357
|
-
return new Date(this._getRaw("updated_at"));
|
|
2358
|
-
}
|
|
2359
|
-
/** Soft delete flag */
|
|
2360
|
-
get isDeleted() {
|
|
2361
|
-
return this._getRaw("is_deleted");
|
|
2362
|
-
}
|
|
2363
|
-
};
|
|
2364
|
-
Conversation.table = "conversations";
|
|
2365
|
-
Conversation.associations = {
|
|
2366
|
-
history: { type: "has_many", foreignKey: "conversation_id" }
|
|
2361
|
+
// src/lib/db/memory/models.ts
|
|
2362
|
+
import { Model as Model2 } from "@nozbe/watermelondb";
|
|
2363
|
+
import { field as field2, text as text2, date as date2, json as json2 } from "@nozbe/watermelondb/decorators";
|
|
2364
|
+
var Memory = class extends Model2 {
|
|
2367
2365
|
};
|
|
2366
|
+
Memory.table = "memories";
|
|
2367
|
+
__decorateClass([
|
|
2368
|
+
text2("type")
|
|
2369
|
+
], Memory.prototype, "type", 2);
|
|
2370
|
+
__decorateClass([
|
|
2371
|
+
text2("namespace")
|
|
2372
|
+
], Memory.prototype, "namespace", 2);
|
|
2373
|
+
__decorateClass([
|
|
2374
|
+
text2("key")
|
|
2375
|
+
], Memory.prototype, "key", 2);
|
|
2376
|
+
__decorateClass([
|
|
2377
|
+
text2("value")
|
|
2378
|
+
], Memory.prototype, "value", 2);
|
|
2379
|
+
__decorateClass([
|
|
2380
|
+
text2("raw_evidence")
|
|
2381
|
+
], Memory.prototype, "rawEvidence", 2);
|
|
2382
|
+
__decorateClass([
|
|
2383
|
+
field2("confidence")
|
|
2384
|
+
], Memory.prototype, "confidence", 2);
|
|
2385
|
+
__decorateClass([
|
|
2386
|
+
field2("pii")
|
|
2387
|
+
], Memory.prototype, "pii", 2);
|
|
2388
|
+
__decorateClass([
|
|
2389
|
+
text2("composite_key")
|
|
2390
|
+
], Memory.prototype, "compositeKey", 2);
|
|
2391
|
+
__decorateClass([
|
|
2392
|
+
text2("unique_key")
|
|
2393
|
+
], Memory.prototype, "uniqueKey", 2);
|
|
2394
|
+
__decorateClass([
|
|
2395
|
+
date2("created_at")
|
|
2396
|
+
], Memory.prototype, "createdAt", 2);
|
|
2397
|
+
__decorateClass([
|
|
2398
|
+
date2("updated_at")
|
|
2399
|
+
], Memory.prototype, "updatedAt", 2);
|
|
2400
|
+
__decorateClass([
|
|
2401
|
+
json2("embedding", (json3) => json3)
|
|
2402
|
+
], Memory.prototype, "embedding", 2);
|
|
2403
|
+
__decorateClass([
|
|
2404
|
+
text2("embedding_model")
|
|
2405
|
+
], Memory.prototype, "embeddingModel", 2);
|
|
2406
|
+
__decorateClass([
|
|
2407
|
+
field2("is_deleted")
|
|
2408
|
+
], Memory.prototype, "isDeleted", 2);
|
|
2368
2409
|
|
|
2369
|
-
// src/
|
|
2370
|
-
import { useCallback as useCallback3, useState as useState3, useMemo as useMemo2, useRef as useRef2 } from "react";
|
|
2371
|
-
import { postApiV1ChatCompletions } from "@reverbia/sdk";
|
|
2372
|
-
|
|
2373
|
-
// src/lib/memoryStorage/operations.ts
|
|
2374
|
-
import { Q as Q2 } from "@nozbe/watermelondb";
|
|
2375
|
-
|
|
2376
|
-
// src/lib/memoryStorage/types.ts
|
|
2410
|
+
// src/lib/db/memory/types.ts
|
|
2377
2411
|
function generateCompositeKey(namespace, key) {
|
|
2378
2412
|
return `${namespace}:${key}`;
|
|
2379
2413
|
}
|
|
@@ -2399,7 +2433,8 @@ function cosineSimilarity2(a, b) {
|
|
|
2399
2433
|
return dotProduct / denominator;
|
|
2400
2434
|
}
|
|
2401
2435
|
|
|
2402
|
-
// src/lib/
|
|
2436
|
+
// src/lib/db/memory/operations.ts
|
|
2437
|
+
import { Q as Q2 } from "@nozbe/watermelondb";
|
|
2403
2438
|
function memoryToStored(memory) {
|
|
2404
2439
|
return {
|
|
2405
2440
|
uniqueId: memory.id,
|
|
@@ -2816,7 +2851,7 @@ var DEFAULT_COMPLETION_MODEL = "openai/gpt-4o";
|
|
|
2816
2851
|
|
|
2817
2852
|
// src/lib/memory/embeddings.ts
|
|
2818
2853
|
var embeddingPipeline = null;
|
|
2819
|
-
var generateEmbeddingForText = async (
|
|
2854
|
+
var generateEmbeddingForText = async (text4, options = {}) => {
|
|
2820
2855
|
const { baseUrl = BASE_URL, provider = "local" } = options;
|
|
2821
2856
|
if (provider === "api") {
|
|
2822
2857
|
const { getToken, model: model2 } = options;
|
|
@@ -2830,7 +2865,7 @@ var generateEmbeddingForText = async (text, options = {}) => {
|
|
|
2830
2865
|
const response = await postApiV1Embeddings({
|
|
2831
2866
|
baseUrl,
|
|
2832
2867
|
body: {
|
|
2833
|
-
input:
|
|
2868
|
+
input: text4,
|
|
2834
2869
|
model: model2 ?? DEFAULT_API_EMBEDDING_MODEL
|
|
2835
2870
|
},
|
|
2836
2871
|
headers: {
|
|
@@ -2856,7 +2891,7 @@ var generateEmbeddingForText = async (text, options = {}) => {
|
|
|
2856
2891
|
const { pipeline } = await import("@huggingface/transformers");
|
|
2857
2892
|
embeddingPipeline = await pipeline("feature-extraction", model);
|
|
2858
2893
|
}
|
|
2859
|
-
const output = await embeddingPipeline(
|
|
2894
|
+
const output = await embeddingPipeline(text4, {
|
|
2860
2895
|
pooling: "cls",
|
|
2861
2896
|
normalize: true
|
|
2862
2897
|
});
|
|
@@ -2870,14 +2905,14 @@ var generateEmbeddingForText = async (text, options = {}) => {
|
|
|
2870
2905
|
}
|
|
2871
2906
|
};
|
|
2872
2907
|
var generateEmbeddingForMemory = async (memory, options = {}) => {
|
|
2873
|
-
const
|
|
2908
|
+
const text4 = [
|
|
2874
2909
|
memory.rawEvidence,
|
|
2875
2910
|
memory.type,
|
|
2876
2911
|
memory.namespace,
|
|
2877
2912
|
memory.key,
|
|
2878
2913
|
memory.value
|
|
2879
2914
|
].filter(Boolean).join(" ");
|
|
2880
|
-
return generateEmbeddingForText(
|
|
2915
|
+
return generateEmbeddingForText(text4, options);
|
|
2881
2916
|
};
|
|
2882
2917
|
|
|
2883
2918
|
// src/react/useMemoryStorage.ts
|
|
@@ -3429,117 +3464,38 @@ function useMemoryStorage(options) {
|
|
|
3429
3464
|
};
|
|
3430
3465
|
}
|
|
3431
3466
|
|
|
3432
|
-
// src/
|
|
3433
|
-
import {
|
|
3434
|
-
|
|
3467
|
+
// src/react/useSettings.ts
|
|
3468
|
+
import { useCallback as useCallback4, useState as useState4, useMemo as useMemo3, useEffect as useEffect2 } from "react";
|
|
3469
|
+
|
|
3470
|
+
// src/lib/db/settings/schema.ts
|
|
3471
|
+
import { appSchema as appSchema3, tableSchema as tableSchema3 } from "@nozbe/watermelondb";
|
|
3472
|
+
var settingsStorageSchema = appSchema3({
|
|
3435
3473
|
version: 1,
|
|
3436
3474
|
tables: [
|
|
3437
|
-
|
|
3438
|
-
name: "
|
|
3475
|
+
tableSchema3({
|
|
3476
|
+
name: "modelPreferences",
|
|
3439
3477
|
columns: [
|
|
3440
|
-
|
|
3441
|
-
{ name: "
|
|
3442
|
-
// 'identity' | 'preference' | 'project' | 'skill' | 'constraint'
|
|
3443
|
-
// Hierarchical key structure
|
|
3444
|
-
{ name: "namespace", type: "string", isIndexed: true },
|
|
3445
|
-
{ name: "key", type: "string", isIndexed: true },
|
|
3446
|
-
{ name: "value", type: "string" },
|
|
3447
|
-
// Evidence and confidence
|
|
3448
|
-
{ name: "raw_evidence", type: "string" },
|
|
3449
|
-
{ name: "confidence", type: "number" },
|
|
3450
|
-
{ name: "pii", type: "boolean", isIndexed: true },
|
|
3451
|
-
// Composite keys for efficient lookups
|
|
3452
|
-
{ name: "composite_key", type: "string", isIndexed: true },
|
|
3453
|
-
// namespace:key
|
|
3454
|
-
{ name: "unique_key", type: "string", isIndexed: true },
|
|
3455
|
-
// namespace:key:value
|
|
3456
|
-
// Timestamps
|
|
3457
|
-
{ name: "created_at", type: "number", isIndexed: true },
|
|
3458
|
-
{ name: "updated_at", type: "number" },
|
|
3459
|
-
// Vector embeddings for semantic search
|
|
3460
|
-
{ name: "embedding", type: "string", isOptional: true },
|
|
3461
|
-
// JSON stringified number[]
|
|
3462
|
-
{ name: "embedding_model", type: "string", isOptional: true },
|
|
3463
|
-
// Soft delete flag
|
|
3464
|
-
{ name: "is_deleted", type: "boolean", isIndexed: true }
|
|
3478
|
+
{ name: "wallet_address", type: "string", isIndexed: true },
|
|
3479
|
+
{ name: "models", type: "string", isOptional: true }
|
|
3465
3480
|
]
|
|
3466
3481
|
})
|
|
3467
3482
|
]
|
|
3468
3483
|
});
|
|
3469
3484
|
|
|
3470
|
-
// src/lib/
|
|
3471
|
-
import { Model as
|
|
3472
|
-
|
|
3473
|
-
|
|
3474
|
-
get type() {
|
|
3475
|
-
return this._getRaw("type");
|
|
3476
|
-
}
|
|
3477
|
-
/** Namespace for grouping related memories */
|
|
3478
|
-
get namespace() {
|
|
3479
|
-
return this._getRaw("namespace");
|
|
3480
|
-
}
|
|
3481
|
-
/** Key within the namespace */
|
|
3482
|
-
get key() {
|
|
3483
|
-
return this._getRaw("key");
|
|
3484
|
-
}
|
|
3485
|
-
/** The memory value/content */
|
|
3486
|
-
get value() {
|
|
3487
|
-
return this._getRaw("value");
|
|
3488
|
-
}
|
|
3489
|
-
/** Raw evidence from which this memory was extracted */
|
|
3490
|
-
get rawEvidence() {
|
|
3491
|
-
return this._getRaw("raw_evidence");
|
|
3492
|
-
}
|
|
3493
|
-
/** Confidence score (0-1) */
|
|
3494
|
-
get confidence() {
|
|
3495
|
-
return this._getRaw("confidence");
|
|
3496
|
-
}
|
|
3497
|
-
/** Whether this memory contains PII */
|
|
3498
|
-
get pii() {
|
|
3499
|
-
return this._getRaw("pii");
|
|
3500
|
-
}
|
|
3501
|
-
/** Composite key (namespace:key) for efficient lookups */
|
|
3502
|
-
get compositeKey() {
|
|
3503
|
-
return this._getRaw("composite_key");
|
|
3504
|
-
}
|
|
3505
|
-
/** Unique key (namespace:key:value) for deduplication */
|
|
3506
|
-
get uniqueKey() {
|
|
3507
|
-
return this._getRaw("unique_key");
|
|
3508
|
-
}
|
|
3509
|
-
/** Created timestamp */
|
|
3510
|
-
get createdAt() {
|
|
3511
|
-
return new Date(this._getRaw("created_at"));
|
|
3512
|
-
}
|
|
3513
|
-
/** Updated timestamp */
|
|
3514
|
-
get updatedAt() {
|
|
3515
|
-
return new Date(this._getRaw("updated_at"));
|
|
3516
|
-
}
|
|
3517
|
-
/** Embedding vector for semantic search */
|
|
3518
|
-
get embedding() {
|
|
3519
|
-
const raw = this._getRaw("embedding");
|
|
3520
|
-
if (!raw) return void 0;
|
|
3521
|
-
try {
|
|
3522
|
-
return JSON.parse(raw);
|
|
3523
|
-
} catch {
|
|
3524
|
-
return void 0;
|
|
3525
|
-
}
|
|
3526
|
-
}
|
|
3527
|
-
/** Model used to generate embedding */
|
|
3528
|
-
get embeddingModel() {
|
|
3529
|
-
const value = this._getRaw("embedding_model");
|
|
3530
|
-
return value ? value : void 0;
|
|
3531
|
-
}
|
|
3532
|
-
/** Soft delete flag */
|
|
3533
|
-
get isDeleted() {
|
|
3534
|
-
return this._getRaw("is_deleted");
|
|
3535
|
-
}
|
|
3485
|
+
// src/lib/db/settings/models.ts
|
|
3486
|
+
import { Model as Model3 } from "@nozbe/watermelondb";
|
|
3487
|
+
import { text as text3 } from "@nozbe/watermelondb/decorators";
|
|
3488
|
+
var ModelPreference = class extends Model3 {
|
|
3536
3489
|
};
|
|
3537
|
-
|
|
3538
|
-
|
|
3539
|
-
|
|
3540
|
-
|
|
3490
|
+
ModelPreference.table = "modelPreferences";
|
|
3491
|
+
__decorateClass([
|
|
3492
|
+
text3("wallet_address")
|
|
3493
|
+
], ModelPreference.prototype, "walletAddress", 2);
|
|
3494
|
+
__decorateClass([
|
|
3495
|
+
text3("models")
|
|
3496
|
+
], ModelPreference.prototype, "models", 2);
|
|
3541
3497
|
|
|
3542
|
-
// src/lib/
|
|
3498
|
+
// src/lib/db/settings/operations.ts
|
|
3543
3499
|
import { Q as Q3 } from "@nozbe/watermelondb";
|
|
3544
3500
|
function modelPreferenceToStored(preference) {
|
|
3545
3501
|
return {
|
|
@@ -3677,37 +3633,6 @@ function useSettings(options) {
|
|
|
3677
3633
|
};
|
|
3678
3634
|
}
|
|
3679
3635
|
|
|
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
|
-
|
|
3711
3636
|
// src/react/usePdf.ts
|
|
3712
3637
|
import { useCallback as useCallback5, useState as useState5 } from "react";
|
|
3713
3638
|
|
|
@@ -3778,13 +3703,13 @@ function usePdf() {
|
|
|
3778
3703
|
const contexts = await Promise.all(
|
|
3779
3704
|
pdfFiles.map(async (file) => {
|
|
3780
3705
|
try {
|
|
3781
|
-
const
|
|
3782
|
-
if (!
|
|
3706
|
+
const text4 = await extractTextFromPdf(file.url);
|
|
3707
|
+
if (!text4.trim()) {
|
|
3783
3708
|
console.warn(`No text found in PDF ${file.filename}`);
|
|
3784
3709
|
return null;
|
|
3785
3710
|
}
|
|
3786
3711
|
return `[Context from PDF attachment ${file.filename}]:
|
|
3787
|
-
${
|
|
3712
|
+
${text4}`;
|
|
3788
3713
|
} catch (err) {
|
|
3789
3714
|
console.error(`Failed to process PDF ${file.filename}:`, err);
|
|
3790
3715
|
return null;
|
|
@@ -3864,15 +3789,15 @@ function useOCR() {
|
|
|
3864
3789
|
const result = await Tesseract.recognize(image, language);
|
|
3865
3790
|
pageTexts.push(result.data.text);
|
|
3866
3791
|
}
|
|
3867
|
-
const
|
|
3868
|
-
if (!
|
|
3792
|
+
const text4 = pageTexts.join("\n\n");
|
|
3793
|
+
if (!text4.trim()) {
|
|
3869
3794
|
console.warn(
|
|
3870
3795
|
`No text found in OCR source ${filename || "unknown"}`
|
|
3871
3796
|
);
|
|
3872
3797
|
return null;
|
|
3873
3798
|
}
|
|
3874
3799
|
return `[Context from OCR attachment ${filename || "unknown"}]:
|
|
3875
|
-
${
|
|
3800
|
+
${text4}`;
|
|
3876
3801
|
} catch (err) {
|
|
3877
3802
|
console.error(
|
|
3878
3803
|
`Failed to process OCR for ${file.filename || "unknown"}:`,
|
|
@@ -4225,14 +4150,940 @@ var extractConversationContext = (messages, maxMessages = 3) => {
|
|
|
4225
4150
|
const userMessages = messages.filter((msg) => msg.role === "user").slice(-maxMessages).map((msg) => msg.content).join(" ");
|
|
4226
4151
|
return userMessages.trim();
|
|
4227
4152
|
};
|
|
4153
|
+
|
|
4154
|
+
// src/react/useDropboxBackup.ts
|
|
4155
|
+
import { useCallback as useCallback11, useMemo as useMemo4 } from "react";
|
|
4156
|
+
|
|
4157
|
+
// src/lib/backup/dropbox/api.ts
|
|
4158
|
+
var DROPBOX_API_URL = "https://api.dropboxapi.com/2";
|
|
4159
|
+
var DROPBOX_CONTENT_URL = "https://content.dropboxapi.com/2";
|
|
4160
|
+
var DEFAULT_BACKUP_FOLDER = "/ai-chat-app/conversations";
|
|
4161
|
+
async function ensureBackupFolder(accessToken, folder = DEFAULT_BACKUP_FOLDER) {
|
|
4162
|
+
try {
|
|
4163
|
+
await fetch(`${DROPBOX_API_URL}/files/create_folder_v2`, {
|
|
4164
|
+
method: "POST",
|
|
4165
|
+
headers: {
|
|
4166
|
+
Authorization: `Bearer ${accessToken}`,
|
|
4167
|
+
"Content-Type": "application/json"
|
|
4168
|
+
},
|
|
4169
|
+
body: JSON.stringify({
|
|
4170
|
+
path: folder,
|
|
4171
|
+
autorename: false
|
|
4172
|
+
})
|
|
4173
|
+
});
|
|
4174
|
+
} catch {
|
|
4175
|
+
}
|
|
4176
|
+
}
|
|
4177
|
+
async function uploadFileToDropbox(accessToken, filename, content, folder = DEFAULT_BACKUP_FOLDER) {
|
|
4178
|
+
await ensureBackupFolder(accessToken, folder);
|
|
4179
|
+
const path = `${folder}/${filename}`;
|
|
4180
|
+
const response = await fetch(`${DROPBOX_CONTENT_URL}/files/upload`, {
|
|
4181
|
+
method: "POST",
|
|
4182
|
+
headers: {
|
|
4183
|
+
Authorization: `Bearer ${accessToken}`,
|
|
4184
|
+
"Content-Type": "application/octet-stream",
|
|
4185
|
+
"Dropbox-API-Arg": JSON.stringify({
|
|
4186
|
+
path,
|
|
4187
|
+
mode: "overwrite",
|
|
4188
|
+
autorename: false,
|
|
4189
|
+
mute: true
|
|
4190
|
+
})
|
|
4191
|
+
},
|
|
4192
|
+
body: content
|
|
4193
|
+
});
|
|
4194
|
+
if (!response.ok) {
|
|
4195
|
+
const errorText = await response.text();
|
|
4196
|
+
throw new Error(`Dropbox upload failed: ${response.status} - ${errorText}`);
|
|
4197
|
+
}
|
|
4198
|
+
return response.json();
|
|
4199
|
+
}
|
|
4200
|
+
async function listDropboxFiles(accessToken, folder = DEFAULT_BACKUP_FOLDER) {
|
|
4201
|
+
await ensureBackupFolder(accessToken, folder);
|
|
4202
|
+
const response = await fetch(`${DROPBOX_API_URL}/files/list_folder`, {
|
|
4203
|
+
method: "POST",
|
|
4204
|
+
headers: {
|
|
4205
|
+
Authorization: `Bearer ${accessToken}`,
|
|
4206
|
+
"Content-Type": "application/json"
|
|
4207
|
+
},
|
|
4208
|
+
body: JSON.stringify({
|
|
4209
|
+
path: folder,
|
|
4210
|
+
recursive: false,
|
|
4211
|
+
include_deleted: false
|
|
4212
|
+
})
|
|
4213
|
+
});
|
|
4214
|
+
if (!response.ok) {
|
|
4215
|
+
const error = await response.json();
|
|
4216
|
+
if (error.error?.path?.[".tag"] === "not_found") {
|
|
4217
|
+
return [];
|
|
4218
|
+
}
|
|
4219
|
+
throw new Error(`Dropbox list failed: ${error.error_summary}`);
|
|
4220
|
+
}
|
|
4221
|
+
let data = await response.json();
|
|
4222
|
+
const allEntries = [...data.entries];
|
|
4223
|
+
while (data.has_more) {
|
|
4224
|
+
const continueResponse = await fetch(`${DROPBOX_API_URL}/files/list_folder/continue`, {
|
|
4225
|
+
method: "POST",
|
|
4226
|
+
headers: {
|
|
4227
|
+
Authorization: `Bearer ${accessToken}`,
|
|
4228
|
+
"Content-Type": "application/json"
|
|
4229
|
+
},
|
|
4230
|
+
body: JSON.stringify({
|
|
4231
|
+
cursor: data.cursor
|
|
4232
|
+
})
|
|
4233
|
+
});
|
|
4234
|
+
if (!continueResponse.ok) {
|
|
4235
|
+
const errorText = await continueResponse.text();
|
|
4236
|
+
throw new Error(`Dropbox list continue failed: ${continueResponse.status} - ${errorText}`);
|
|
4237
|
+
}
|
|
4238
|
+
data = await continueResponse.json();
|
|
4239
|
+
allEntries.push(...data.entries);
|
|
4240
|
+
}
|
|
4241
|
+
const files = allEntries.filter((entry) => entry[".tag"] === "file").map((entry) => ({
|
|
4242
|
+
id: entry.id,
|
|
4243
|
+
name: entry.name,
|
|
4244
|
+
path_lower: entry.path_lower,
|
|
4245
|
+
path_display: entry.path_display,
|
|
4246
|
+
client_modified: entry.client_modified || "",
|
|
4247
|
+
server_modified: entry.server_modified || "",
|
|
4248
|
+
size: entry.size || 0
|
|
4249
|
+
}));
|
|
4250
|
+
return files;
|
|
4251
|
+
}
|
|
4252
|
+
async function downloadDropboxFile(accessToken, path) {
|
|
4253
|
+
const response = await fetch(`${DROPBOX_CONTENT_URL}/files/download`, {
|
|
4254
|
+
method: "POST",
|
|
4255
|
+
headers: {
|
|
4256
|
+
Authorization: `Bearer ${accessToken}`,
|
|
4257
|
+
"Dropbox-API-Arg": JSON.stringify({ path })
|
|
4258
|
+
}
|
|
4259
|
+
});
|
|
4260
|
+
if (!response.ok) {
|
|
4261
|
+
throw new Error(`Dropbox download failed: ${response.status}`);
|
|
4262
|
+
}
|
|
4263
|
+
return response.blob();
|
|
4264
|
+
}
|
|
4265
|
+
async function findDropboxFile(accessToken, filename, folder = DEFAULT_BACKUP_FOLDER) {
|
|
4266
|
+
const files = await listDropboxFiles(accessToken, folder);
|
|
4267
|
+
return files.find((f) => f.name === filename) || null;
|
|
4268
|
+
}
|
|
4269
|
+
|
|
4270
|
+
// src/lib/backup/dropbox/backup.ts
|
|
4271
|
+
var isAuthError = (err) => err instanceof Error && (err.message.includes("401") || err.message.includes("invalid_access_token"));
|
|
4272
|
+
async function pushConversationToDropbox(database, conversationId, userAddress, token, deps, backupFolder = DEFAULT_BACKUP_FOLDER, _retried = false) {
|
|
4273
|
+
try {
|
|
4274
|
+
await deps.requestEncryptionKey(userAddress);
|
|
4275
|
+
const filename = `${conversationId}.json`;
|
|
4276
|
+
const existingFile = await findDropboxFile(token, filename, backupFolder);
|
|
4277
|
+
if (existingFile) {
|
|
4278
|
+
const { Q: Q4 } = await import("@nozbe/watermelondb");
|
|
4279
|
+
const conversationsCollection = database.get("conversations");
|
|
4280
|
+
const records = await conversationsCollection.query(Q4.where("conversation_id", conversationId)).fetch();
|
|
4281
|
+
if (records.length > 0) {
|
|
4282
|
+
const conversation = conversationToStored(records[0]);
|
|
4283
|
+
const localUpdated = conversation.updatedAt.getTime();
|
|
4284
|
+
const remoteModified = new Date(existingFile.server_modified).getTime();
|
|
4285
|
+
if (localUpdated <= remoteModified) {
|
|
4286
|
+
return "skipped";
|
|
4287
|
+
}
|
|
4288
|
+
}
|
|
4289
|
+
}
|
|
4290
|
+
const exportResult = await deps.exportConversation(
|
|
4291
|
+
conversationId,
|
|
4292
|
+
userAddress
|
|
4293
|
+
);
|
|
4294
|
+
if (!exportResult.success || !exportResult.blob) {
|
|
4295
|
+
return "failed";
|
|
4296
|
+
}
|
|
4297
|
+
await uploadFileToDropbox(token, filename, exportResult.blob, backupFolder);
|
|
4298
|
+
return "uploaded";
|
|
4299
|
+
} catch (err) {
|
|
4300
|
+
if (isAuthError(err) && !_retried) {
|
|
4301
|
+
try {
|
|
4302
|
+
const newToken = await deps.requestDropboxAccess();
|
|
4303
|
+
return pushConversationToDropbox(
|
|
4304
|
+
database,
|
|
4305
|
+
conversationId,
|
|
4306
|
+
userAddress,
|
|
4307
|
+
newToken,
|
|
4308
|
+
deps,
|
|
4309
|
+
backupFolder,
|
|
4310
|
+
true
|
|
4311
|
+
);
|
|
4312
|
+
} catch {
|
|
4313
|
+
return "failed";
|
|
4314
|
+
}
|
|
4315
|
+
}
|
|
4316
|
+
return "failed";
|
|
4317
|
+
}
|
|
4318
|
+
}
|
|
4319
|
+
async function performDropboxExport(database, userAddress, token, deps, onProgress, backupFolder = DEFAULT_BACKUP_FOLDER) {
|
|
4320
|
+
await deps.requestEncryptionKey(userAddress);
|
|
4321
|
+
const { Q: Q4 } = await import("@nozbe/watermelondb");
|
|
4322
|
+
const conversationsCollection = database.get("conversations");
|
|
4323
|
+
const records = await conversationsCollection.query(Q4.where("is_deleted", false)).fetch();
|
|
4324
|
+
const conversations = records.map(conversationToStored);
|
|
4325
|
+
const total = conversations.length;
|
|
4326
|
+
if (total === 0) {
|
|
4327
|
+
return { success: true, uploaded: 0, skipped: 0, total: 0 };
|
|
4328
|
+
}
|
|
4329
|
+
let uploaded = 0;
|
|
4330
|
+
let skipped = 0;
|
|
4331
|
+
for (let i = 0; i < conversations.length; i++) {
|
|
4332
|
+
const conv = conversations[i];
|
|
4333
|
+
onProgress?.(i + 1, total);
|
|
4334
|
+
const result = await pushConversationToDropbox(
|
|
4335
|
+
database,
|
|
4336
|
+
conv.conversationId,
|
|
4337
|
+
userAddress,
|
|
4338
|
+
token,
|
|
4339
|
+
deps,
|
|
4340
|
+
backupFolder
|
|
4341
|
+
);
|
|
4342
|
+
if (result === "uploaded") uploaded++;
|
|
4343
|
+
if (result === "skipped") skipped++;
|
|
4344
|
+
}
|
|
4345
|
+
return { success: true, uploaded, skipped, total };
|
|
4346
|
+
}
|
|
4347
|
+
async function performDropboxImport(userAddress, token, deps, onProgress, backupFolder = DEFAULT_BACKUP_FOLDER) {
|
|
4348
|
+
await deps.requestEncryptionKey(userAddress);
|
|
4349
|
+
const remoteFiles = await listDropboxFiles(token, backupFolder);
|
|
4350
|
+
if (remoteFiles.length === 0) {
|
|
4351
|
+
return {
|
|
4352
|
+
success: false,
|
|
4353
|
+
restored: 0,
|
|
4354
|
+
failed: 0,
|
|
4355
|
+
total: 0,
|
|
4356
|
+
noBackupsFound: true
|
|
4357
|
+
};
|
|
4358
|
+
}
|
|
4359
|
+
const jsonFiles = remoteFiles.filter(
|
|
4360
|
+
(file) => file.name.endsWith(".json")
|
|
4361
|
+
);
|
|
4362
|
+
const total = jsonFiles.length;
|
|
4363
|
+
let restored = 0;
|
|
4364
|
+
let failed = 0;
|
|
4365
|
+
let currentToken = token;
|
|
4366
|
+
for (let i = 0; i < jsonFiles.length; i++) {
|
|
4367
|
+
const file = jsonFiles[i];
|
|
4368
|
+
onProgress?.(i + 1, total);
|
|
4369
|
+
try {
|
|
4370
|
+
const blob = await downloadDropboxFile(currentToken, file.path_lower);
|
|
4371
|
+
const result = await deps.importConversation(blob, userAddress);
|
|
4372
|
+
if (result.success) {
|
|
4373
|
+
restored++;
|
|
4374
|
+
} else {
|
|
4375
|
+
failed++;
|
|
4376
|
+
}
|
|
4377
|
+
} catch (err) {
|
|
4378
|
+
if (isAuthError(err)) {
|
|
4379
|
+
try {
|
|
4380
|
+
currentToken = await deps.requestDropboxAccess();
|
|
4381
|
+
const blob = await downloadDropboxFile(currentToken, file.path_lower);
|
|
4382
|
+
const result = await deps.importConversation(blob, userAddress);
|
|
4383
|
+
if (result.success) {
|
|
4384
|
+
restored++;
|
|
4385
|
+
} else {
|
|
4386
|
+
failed++;
|
|
4387
|
+
}
|
|
4388
|
+
} catch {
|
|
4389
|
+
failed++;
|
|
4390
|
+
}
|
|
4391
|
+
} else {
|
|
4392
|
+
failed++;
|
|
4393
|
+
}
|
|
4394
|
+
}
|
|
4395
|
+
}
|
|
4396
|
+
return { success: true, restored, failed, total };
|
|
4397
|
+
}
|
|
4398
|
+
|
|
4399
|
+
// src/react/useDropboxAuth.ts
|
|
4400
|
+
import {
|
|
4401
|
+
createContext,
|
|
4402
|
+
createElement,
|
|
4403
|
+
useCallback as useCallback10,
|
|
4404
|
+
useContext,
|
|
4405
|
+
useEffect as useEffect6,
|
|
4406
|
+
useState as useState10
|
|
4407
|
+
} from "react";
|
|
4408
|
+
|
|
4409
|
+
// src/lib/backup/dropbox/auth.ts
|
|
4410
|
+
var DROPBOX_AUTH_URL = "https://www.dropbox.com/oauth2/authorize";
|
|
4411
|
+
var DROPBOX_TOKEN_URL = "https://api.dropboxapi.com/oauth2/token";
|
|
4412
|
+
var TOKEN_STORAGE_KEY = "dropbox_access_token";
|
|
4413
|
+
var VERIFIER_STORAGE_KEY = "dropbox_code_verifier";
|
|
4414
|
+
function generateCodeVerifier() {
|
|
4415
|
+
const array = new Uint8Array(32);
|
|
4416
|
+
crypto.getRandomValues(array);
|
|
4417
|
+
return Array.from(array, (byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
4418
|
+
}
|
|
4419
|
+
async function generateCodeChallenge(verifier) {
|
|
4420
|
+
const encoder = new TextEncoder();
|
|
4421
|
+
const data = encoder.encode(verifier);
|
|
4422
|
+
const hash = await crypto.subtle.digest("SHA-256", data);
|
|
4423
|
+
const base64 = btoa(String.fromCharCode(...new Uint8Array(hash)));
|
|
4424
|
+
return base64.replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
4425
|
+
}
|
|
4426
|
+
function getStoredToken() {
|
|
4427
|
+
if (typeof window === "undefined") return null;
|
|
4428
|
+
return sessionStorage.getItem(TOKEN_STORAGE_KEY);
|
|
4429
|
+
}
|
|
4430
|
+
function storeToken(token) {
|
|
4431
|
+
if (typeof window === "undefined") return;
|
|
4432
|
+
sessionStorage.setItem(TOKEN_STORAGE_KEY, token);
|
|
4433
|
+
}
|
|
4434
|
+
function clearToken() {
|
|
4435
|
+
if (typeof window === "undefined") return;
|
|
4436
|
+
sessionStorage.removeItem(TOKEN_STORAGE_KEY);
|
|
4437
|
+
}
|
|
4438
|
+
function getStoredVerifier() {
|
|
4439
|
+
if (typeof window === "undefined") return null;
|
|
4440
|
+
return sessionStorage.getItem(VERIFIER_STORAGE_KEY);
|
|
4441
|
+
}
|
|
4442
|
+
function storeVerifier(verifier) {
|
|
4443
|
+
if (typeof window === "undefined") return;
|
|
4444
|
+
sessionStorage.setItem(VERIFIER_STORAGE_KEY, verifier);
|
|
4445
|
+
}
|
|
4446
|
+
function clearVerifier() {
|
|
4447
|
+
if (typeof window === "undefined") return;
|
|
4448
|
+
sessionStorage.removeItem(VERIFIER_STORAGE_KEY);
|
|
4449
|
+
}
|
|
4450
|
+
function getRedirectUri(callbackPath) {
|
|
4451
|
+
if (typeof window === "undefined") return "";
|
|
4452
|
+
return `${window.location.origin}${callbackPath}`;
|
|
4453
|
+
}
|
|
4454
|
+
async function handleDropboxCallback(appKey, callbackPath) {
|
|
4455
|
+
if (typeof window === "undefined") return null;
|
|
4456
|
+
const url = new URL(window.location.href);
|
|
4457
|
+
const code = url.searchParams.get("code");
|
|
4458
|
+
const state = url.searchParams.get("state");
|
|
4459
|
+
if (!code || state !== "dropbox_auth") return null;
|
|
4460
|
+
const verifier = getStoredVerifier();
|
|
4461
|
+
if (!verifier) return null;
|
|
4462
|
+
try {
|
|
4463
|
+
const response = await fetch(DROPBOX_TOKEN_URL, {
|
|
4464
|
+
method: "POST",
|
|
4465
|
+
headers: {
|
|
4466
|
+
"Content-Type": "application/x-www-form-urlencoded"
|
|
4467
|
+
},
|
|
4468
|
+
body: new URLSearchParams({
|
|
4469
|
+
code,
|
|
4470
|
+
grant_type: "authorization_code",
|
|
4471
|
+
client_id: appKey,
|
|
4472
|
+
redirect_uri: getRedirectUri(callbackPath),
|
|
4473
|
+
code_verifier: verifier
|
|
4474
|
+
})
|
|
4475
|
+
});
|
|
4476
|
+
if (!response.ok) {
|
|
4477
|
+
throw new Error("Token exchange failed");
|
|
4478
|
+
}
|
|
4479
|
+
const data = await response.json();
|
|
4480
|
+
const token = data.access_token;
|
|
4481
|
+
if (typeof token !== "string" || token.trim() === "") {
|
|
4482
|
+
throw new Error("Invalid token response: access_token is missing or empty");
|
|
4483
|
+
}
|
|
4484
|
+
storeToken(token);
|
|
4485
|
+
clearVerifier();
|
|
4486
|
+
window.history.replaceState({}, "", window.location.pathname);
|
|
4487
|
+
return token;
|
|
4488
|
+
} catch {
|
|
4489
|
+
clearVerifier();
|
|
4490
|
+
return null;
|
|
4491
|
+
}
|
|
4492
|
+
}
|
|
4493
|
+
async function startDropboxAuth(appKey, callbackPath) {
|
|
4494
|
+
const verifier = generateCodeVerifier();
|
|
4495
|
+
const challenge = await generateCodeChallenge(verifier);
|
|
4496
|
+
storeVerifier(verifier);
|
|
4497
|
+
const params = new URLSearchParams({
|
|
4498
|
+
client_id: appKey,
|
|
4499
|
+
redirect_uri: getRedirectUri(callbackPath),
|
|
4500
|
+
response_type: "code",
|
|
4501
|
+
code_challenge: challenge,
|
|
4502
|
+
code_challenge_method: "S256",
|
|
4503
|
+
state: "dropbox_auth",
|
|
4504
|
+
token_access_type: "offline"
|
|
4505
|
+
});
|
|
4506
|
+
window.location.href = `${DROPBOX_AUTH_URL}?${params.toString()}`;
|
|
4507
|
+
return new Promise(() => {
|
|
4508
|
+
});
|
|
4509
|
+
}
|
|
4510
|
+
async function requestDropboxAccess(appKey, callbackPath) {
|
|
4511
|
+
if (!appKey) {
|
|
4512
|
+
throw new Error("Dropbox is not configured");
|
|
4513
|
+
}
|
|
4514
|
+
const storedToken = getStoredToken();
|
|
4515
|
+
if (storedToken) {
|
|
4516
|
+
return storedToken;
|
|
4517
|
+
}
|
|
4518
|
+
return startDropboxAuth(appKey, callbackPath);
|
|
4519
|
+
}
|
|
4520
|
+
|
|
4521
|
+
// src/react/useDropboxAuth.ts
|
|
4522
|
+
var DropboxAuthContext = createContext(null);
|
|
4523
|
+
function DropboxAuthProvider({
|
|
4524
|
+
appKey,
|
|
4525
|
+
callbackPath = "/auth/dropbox/callback",
|
|
4526
|
+
children
|
|
4527
|
+
}) {
|
|
4528
|
+
const [accessToken, setAccessToken] = useState10(null);
|
|
4529
|
+
const isConfigured = !!appKey;
|
|
4530
|
+
useEffect6(() => {
|
|
4531
|
+
const storedToken = getStoredToken();
|
|
4532
|
+
if (storedToken) {
|
|
4533
|
+
setAccessToken(storedToken);
|
|
4534
|
+
}
|
|
4535
|
+
}, []);
|
|
4536
|
+
useEffect6(() => {
|
|
4537
|
+
if (!isConfigured || !appKey) return;
|
|
4538
|
+
const handleCallback = async () => {
|
|
4539
|
+
const token = await handleDropboxCallback(appKey, callbackPath);
|
|
4540
|
+
if (token) {
|
|
4541
|
+
setAccessToken(token);
|
|
4542
|
+
}
|
|
4543
|
+
};
|
|
4544
|
+
handleCallback();
|
|
4545
|
+
}, [appKey, callbackPath, isConfigured]);
|
|
4546
|
+
const requestAccess = useCallback10(async () => {
|
|
4547
|
+
if (!isConfigured || !appKey) {
|
|
4548
|
+
throw new Error("Dropbox is not configured");
|
|
4549
|
+
}
|
|
4550
|
+
if (accessToken) {
|
|
4551
|
+
return accessToken;
|
|
4552
|
+
}
|
|
4553
|
+
const storedToken = getStoredToken();
|
|
4554
|
+
if (storedToken) {
|
|
4555
|
+
setAccessToken(storedToken);
|
|
4556
|
+
return storedToken;
|
|
4557
|
+
}
|
|
4558
|
+
return requestDropboxAccess(appKey, callbackPath);
|
|
4559
|
+
}, [accessToken, appKey, callbackPath, isConfigured]);
|
|
4560
|
+
const logout = useCallback10(() => {
|
|
4561
|
+
clearToken();
|
|
4562
|
+
setAccessToken(null);
|
|
4563
|
+
}, []);
|
|
4564
|
+
return createElement(
|
|
4565
|
+
DropboxAuthContext.Provider,
|
|
4566
|
+
{
|
|
4567
|
+
value: {
|
|
4568
|
+
accessToken,
|
|
4569
|
+
isAuthenticated: !!accessToken,
|
|
4570
|
+
isConfigured,
|
|
4571
|
+
requestAccess,
|
|
4572
|
+
logout
|
|
4573
|
+
}
|
|
4574
|
+
},
|
|
4575
|
+
children
|
|
4576
|
+
);
|
|
4577
|
+
}
|
|
4578
|
+
function useDropboxAuth() {
|
|
4579
|
+
const context = useContext(DropboxAuthContext);
|
|
4580
|
+
if (!context) {
|
|
4581
|
+
throw new Error("useDropboxAuth must be used within DropboxAuthProvider");
|
|
4582
|
+
}
|
|
4583
|
+
return context;
|
|
4584
|
+
}
|
|
4585
|
+
|
|
4586
|
+
// src/react/useDropboxBackup.ts
|
|
4587
|
+
function useDropboxBackup(options) {
|
|
4588
|
+
const {
|
|
4589
|
+
database,
|
|
4590
|
+
userAddress,
|
|
4591
|
+
requestEncryptionKey: requestEncryptionKey2,
|
|
4592
|
+
exportConversation,
|
|
4593
|
+
importConversation,
|
|
4594
|
+
backupFolder = DEFAULT_BACKUP_FOLDER
|
|
4595
|
+
} = options;
|
|
4596
|
+
const {
|
|
4597
|
+
accessToken: dropboxToken,
|
|
4598
|
+
isConfigured: isDropboxConfigured,
|
|
4599
|
+
requestAccess: requestDropboxAccess2
|
|
4600
|
+
} = useDropboxAuth();
|
|
4601
|
+
const deps = useMemo4(
|
|
4602
|
+
() => ({
|
|
4603
|
+
requestDropboxAccess: requestDropboxAccess2,
|
|
4604
|
+
requestEncryptionKey: requestEncryptionKey2,
|
|
4605
|
+
exportConversation,
|
|
4606
|
+
importConversation
|
|
4607
|
+
}),
|
|
4608
|
+
[requestDropboxAccess2, requestEncryptionKey2, exportConversation, importConversation]
|
|
4609
|
+
);
|
|
4610
|
+
const ensureToken = useCallback11(async () => {
|
|
4611
|
+
if (dropboxToken) return dropboxToken;
|
|
4612
|
+
try {
|
|
4613
|
+
return await requestDropboxAccess2();
|
|
4614
|
+
} catch {
|
|
4615
|
+
return null;
|
|
4616
|
+
}
|
|
4617
|
+
}, [dropboxToken, requestDropboxAccess2]);
|
|
4618
|
+
const backup = useCallback11(
|
|
4619
|
+
async (backupOptions) => {
|
|
4620
|
+
if (!userAddress) {
|
|
4621
|
+
return { error: "Please sign in to backup to Dropbox" };
|
|
4622
|
+
}
|
|
4623
|
+
const token = await ensureToken();
|
|
4624
|
+
if (!token) {
|
|
4625
|
+
return { error: "Dropbox access denied" };
|
|
4626
|
+
}
|
|
4627
|
+
try {
|
|
4628
|
+
return await performDropboxExport(
|
|
4629
|
+
database,
|
|
4630
|
+
userAddress,
|
|
4631
|
+
token,
|
|
4632
|
+
deps,
|
|
4633
|
+
backupOptions?.onProgress,
|
|
4634
|
+
backupFolder
|
|
4635
|
+
);
|
|
4636
|
+
} catch (err) {
|
|
4637
|
+
return {
|
|
4638
|
+
error: err instanceof Error ? err.message : "Failed to backup to Dropbox"
|
|
4639
|
+
};
|
|
4640
|
+
}
|
|
4641
|
+
},
|
|
4642
|
+
[database, userAddress, ensureToken, deps, backupFolder]
|
|
4643
|
+
);
|
|
4644
|
+
const restore = useCallback11(
|
|
4645
|
+
async (restoreOptions) => {
|
|
4646
|
+
if (!userAddress) {
|
|
4647
|
+
return { error: "Please sign in to restore from Dropbox" };
|
|
4648
|
+
}
|
|
4649
|
+
const token = await ensureToken();
|
|
4650
|
+
if (!token) {
|
|
4651
|
+
return { error: "Dropbox access denied" };
|
|
4652
|
+
}
|
|
4653
|
+
try {
|
|
4654
|
+
return await performDropboxImport(
|
|
4655
|
+
userAddress,
|
|
4656
|
+
token,
|
|
4657
|
+
deps,
|
|
4658
|
+
restoreOptions?.onProgress,
|
|
4659
|
+
backupFolder
|
|
4660
|
+
);
|
|
4661
|
+
} catch (err) {
|
|
4662
|
+
return {
|
|
4663
|
+
error: err instanceof Error ? err.message : "Failed to restore from Dropbox"
|
|
4664
|
+
};
|
|
4665
|
+
}
|
|
4666
|
+
},
|
|
4667
|
+
[userAddress, ensureToken, deps, backupFolder]
|
|
4668
|
+
);
|
|
4669
|
+
return {
|
|
4670
|
+
backup,
|
|
4671
|
+
restore,
|
|
4672
|
+
isConfigured: isDropboxConfigured,
|
|
4673
|
+
isAuthenticated: !!dropboxToken
|
|
4674
|
+
};
|
|
4675
|
+
}
|
|
4676
|
+
|
|
4677
|
+
// src/react/useGoogleDriveBackup.ts
|
|
4678
|
+
import { useCallback as useCallback12, useMemo as useMemo5 } from "react";
|
|
4679
|
+
|
|
4680
|
+
// src/lib/backup/google/api.ts
|
|
4681
|
+
var DRIVE_API_URL = "https://www.googleapis.com/drive/v3";
|
|
4682
|
+
var DRIVE_UPLOAD_URL = "https://www.googleapis.com/upload/drive/v3";
|
|
4683
|
+
var FOLDER_MIME_TYPE = "application/vnd.google-apps.folder";
|
|
4684
|
+
function escapeQueryValue(value) {
|
|
4685
|
+
return value.replace(/'/g, "''");
|
|
4686
|
+
}
|
|
4687
|
+
var DEFAULT_ROOT_FOLDER = "ai-chat-app";
|
|
4688
|
+
var DEFAULT_CONVERSATIONS_FOLDER = "conversations";
|
|
4689
|
+
async function ensureFolder(accessToken, name, parentId) {
|
|
4690
|
+
const parentQuery = parentId ? `'${escapeQueryValue(parentId)}' in parents and ` : "";
|
|
4691
|
+
const query = `${parentQuery}mimeType='${FOLDER_MIME_TYPE}' and name='${escapeQueryValue(name)}' and trashed=false`;
|
|
4692
|
+
const response = await fetch(
|
|
4693
|
+
`${DRIVE_API_URL}/files?q=${encodeURIComponent(query)}&fields=files(id)`,
|
|
4694
|
+
{
|
|
4695
|
+
headers: { Authorization: `Bearer ${accessToken}` }
|
|
4696
|
+
}
|
|
4697
|
+
);
|
|
4698
|
+
if (!response.ok) {
|
|
4699
|
+
throw new Error(`Failed to search for folder ${name}: ${response.status}`);
|
|
4700
|
+
}
|
|
4701
|
+
const data = await response.json();
|
|
4702
|
+
if (data.files && data.files.length > 0) {
|
|
4703
|
+
return data.files[0].id;
|
|
4704
|
+
}
|
|
4705
|
+
const body = {
|
|
4706
|
+
name,
|
|
4707
|
+
mimeType: FOLDER_MIME_TYPE
|
|
4708
|
+
};
|
|
4709
|
+
if (parentId) {
|
|
4710
|
+
body.parents = [parentId];
|
|
4711
|
+
}
|
|
4712
|
+
const createResponse = await fetch(`${DRIVE_API_URL}/files`, {
|
|
4713
|
+
method: "POST",
|
|
4714
|
+
headers: {
|
|
4715
|
+
Authorization: `Bearer ${accessToken}`,
|
|
4716
|
+
"Content-Type": "application/json"
|
|
4717
|
+
},
|
|
4718
|
+
body: JSON.stringify(body)
|
|
4719
|
+
});
|
|
4720
|
+
if (!createResponse.ok) {
|
|
4721
|
+
throw new Error(`Failed to create folder ${name}: ${createResponse.status}`);
|
|
4722
|
+
}
|
|
4723
|
+
const folderData = await createResponse.json();
|
|
4724
|
+
return folderData.id;
|
|
4725
|
+
}
|
|
4726
|
+
async function getBackupFolder(accessToken, rootFolder = DEFAULT_ROOT_FOLDER, subfolder = DEFAULT_CONVERSATIONS_FOLDER) {
|
|
4727
|
+
const rootId = await ensureFolder(accessToken, rootFolder);
|
|
4728
|
+
return ensureFolder(accessToken, subfolder, rootId);
|
|
4729
|
+
}
|
|
4730
|
+
async function uploadFileToDrive(accessToken, folderId, content, filename) {
|
|
4731
|
+
const metadata = {
|
|
4732
|
+
name: filename,
|
|
4733
|
+
parents: [folderId],
|
|
4734
|
+
mimeType: "application/json"
|
|
4735
|
+
};
|
|
4736
|
+
const form = new FormData();
|
|
4737
|
+
form.append("metadata", new Blob([JSON.stringify(metadata)], { type: "application/json" }));
|
|
4738
|
+
form.append("file", content);
|
|
4739
|
+
const response = await fetch(`${DRIVE_UPLOAD_URL}/files?uploadType=multipart`, {
|
|
4740
|
+
method: "POST",
|
|
4741
|
+
headers: { Authorization: `Bearer ${accessToken}` },
|
|
4742
|
+
body: form
|
|
4743
|
+
});
|
|
4744
|
+
if (!response.ok) {
|
|
4745
|
+
const errorText = await response.text();
|
|
4746
|
+
throw new Error(`Drive upload failed: ${response.status} - ${errorText}`);
|
|
4747
|
+
}
|
|
4748
|
+
return response.json();
|
|
4749
|
+
}
|
|
4750
|
+
async function updateDriveFile(accessToken, fileId, content) {
|
|
4751
|
+
const response = await fetch(`${DRIVE_UPLOAD_URL}/files/${fileId}?uploadType=media`, {
|
|
4752
|
+
method: "PATCH",
|
|
4753
|
+
headers: {
|
|
4754
|
+
Authorization: `Bearer ${accessToken}`,
|
|
4755
|
+
"Content-Type": "application/json"
|
|
4756
|
+
},
|
|
4757
|
+
body: content
|
|
4758
|
+
});
|
|
4759
|
+
if (!response.ok) {
|
|
4760
|
+
const errorText = await response.text();
|
|
4761
|
+
throw new Error(`Drive update failed: ${response.status} - ${errorText}`);
|
|
4762
|
+
}
|
|
4763
|
+
return response.json();
|
|
4764
|
+
}
|
|
4765
|
+
async function listDriveFiles(accessToken, folderId) {
|
|
4766
|
+
const query = `'${escapeQueryValue(folderId)}' in parents and mimeType='application/json' and trashed=false`;
|
|
4767
|
+
const fields = "files(id,name,createdTime,modifiedTime,size)";
|
|
4768
|
+
const response = await fetch(
|
|
4769
|
+
`${DRIVE_API_URL}/files?q=${encodeURIComponent(query)}&fields=${fields}&pageSize=1000`,
|
|
4770
|
+
{
|
|
4771
|
+
headers: { Authorization: `Bearer ${accessToken}` }
|
|
4772
|
+
}
|
|
4773
|
+
);
|
|
4774
|
+
if (!response.ok) {
|
|
4775
|
+
throw new Error(`Failed to list files: ${response.status}`);
|
|
4776
|
+
}
|
|
4777
|
+
const data = await response.json();
|
|
4778
|
+
return data.files ?? [];
|
|
4779
|
+
}
|
|
4780
|
+
async function downloadDriveFile(accessToken, fileId) {
|
|
4781
|
+
const response = await fetch(`${DRIVE_API_URL}/files/${fileId}?alt=media`, {
|
|
4782
|
+
headers: { Authorization: `Bearer ${accessToken}` }
|
|
4783
|
+
});
|
|
4784
|
+
if (!response.ok) {
|
|
4785
|
+
throw new Error(`Failed to download file: ${response.status}`);
|
|
4786
|
+
}
|
|
4787
|
+
return response.blob();
|
|
4788
|
+
}
|
|
4789
|
+
async function findDriveFile(accessToken, folderId, filename) {
|
|
4790
|
+
const query = `'${escapeQueryValue(folderId)}' in parents and name='${escapeQueryValue(filename)}' and trashed=false`;
|
|
4791
|
+
const fields = "files(id,name,createdTime,modifiedTime,size)";
|
|
4792
|
+
const response = await fetch(
|
|
4793
|
+
`${DRIVE_API_URL}/files?q=${encodeURIComponent(query)}&fields=${fields}&pageSize=1`,
|
|
4794
|
+
{
|
|
4795
|
+
headers: { Authorization: `Bearer ${accessToken}` }
|
|
4796
|
+
}
|
|
4797
|
+
);
|
|
4798
|
+
if (!response.ok) {
|
|
4799
|
+
throw new Error(`Failed to find file: ${response.status}`);
|
|
4800
|
+
}
|
|
4801
|
+
const data = await response.json();
|
|
4802
|
+
return data.files?.[0] ?? null;
|
|
4803
|
+
}
|
|
4804
|
+
|
|
4805
|
+
// src/lib/backup/google/backup.ts
|
|
4806
|
+
var isAuthError2 = (err) => err instanceof Error && (err.message.includes("401") || err.message.includes("403"));
|
|
4807
|
+
async function getConversationsFolder(token, requestDriveAccess, rootFolder, subfolder) {
|
|
4808
|
+
try {
|
|
4809
|
+
const folderId = await getBackupFolder(token, rootFolder, subfolder);
|
|
4810
|
+
return { folderId, token };
|
|
4811
|
+
} catch (err) {
|
|
4812
|
+
if (isAuthError2(err)) {
|
|
4813
|
+
try {
|
|
4814
|
+
const newToken = await requestDriveAccess();
|
|
4815
|
+
const folderId = await getBackupFolder(newToken, rootFolder, subfolder);
|
|
4816
|
+
return { folderId, token: newToken };
|
|
4817
|
+
} catch {
|
|
4818
|
+
return null;
|
|
4819
|
+
}
|
|
4820
|
+
}
|
|
4821
|
+
throw err;
|
|
4822
|
+
}
|
|
4823
|
+
}
|
|
4824
|
+
async function pushConversationToDrive(database, conversationId, userAddress, token, deps, rootFolder = DEFAULT_ROOT_FOLDER, subfolder = DEFAULT_CONVERSATIONS_FOLDER, _retried = false) {
|
|
4825
|
+
try {
|
|
4826
|
+
await deps.requestEncryptionKey(userAddress);
|
|
4827
|
+
const folderResult = await getConversationsFolder(
|
|
4828
|
+
token,
|
|
4829
|
+
deps.requestDriveAccess,
|
|
4830
|
+
rootFolder,
|
|
4831
|
+
subfolder
|
|
4832
|
+
);
|
|
4833
|
+
if (!folderResult) return "failed";
|
|
4834
|
+
const { folderId, token: activeToken } = folderResult;
|
|
4835
|
+
const filename = `${conversationId}.json`;
|
|
4836
|
+
const existingFile = await findDriveFile(activeToken, folderId, filename);
|
|
4837
|
+
if (existingFile) {
|
|
4838
|
+
const { Q: Q4 } = await import("@nozbe/watermelondb");
|
|
4839
|
+
const conversationsCollection = database.get("conversations");
|
|
4840
|
+
const records = await conversationsCollection.query(Q4.where("conversation_id", conversationId)).fetch();
|
|
4841
|
+
if (records.length > 0) {
|
|
4842
|
+
const conversation = conversationToStored(records[0]);
|
|
4843
|
+
const localUpdated = conversation.updatedAt.getTime();
|
|
4844
|
+
const remoteModified = new Date(existingFile.modifiedTime).getTime();
|
|
4845
|
+
if (localUpdated <= remoteModified) {
|
|
4846
|
+
return "skipped";
|
|
4847
|
+
}
|
|
4848
|
+
}
|
|
4849
|
+
}
|
|
4850
|
+
const exportResult = await deps.exportConversation(
|
|
4851
|
+
conversationId,
|
|
4852
|
+
userAddress
|
|
4853
|
+
);
|
|
4854
|
+
if (!exportResult.success || !exportResult.blob) {
|
|
4855
|
+
return "failed";
|
|
4856
|
+
}
|
|
4857
|
+
if (existingFile) {
|
|
4858
|
+
await updateDriveFile(activeToken, existingFile.id, exportResult.blob);
|
|
4859
|
+
} else {
|
|
4860
|
+
await uploadFileToDrive(
|
|
4861
|
+
activeToken,
|
|
4862
|
+
folderId,
|
|
4863
|
+
exportResult.blob,
|
|
4864
|
+
filename
|
|
4865
|
+
);
|
|
4866
|
+
}
|
|
4867
|
+
return "uploaded";
|
|
4868
|
+
} catch (err) {
|
|
4869
|
+
if (isAuthError2(err) && !_retried) {
|
|
4870
|
+
try {
|
|
4871
|
+
const newToken = await deps.requestDriveAccess();
|
|
4872
|
+
return pushConversationToDrive(
|
|
4873
|
+
database,
|
|
4874
|
+
conversationId,
|
|
4875
|
+
userAddress,
|
|
4876
|
+
newToken,
|
|
4877
|
+
deps,
|
|
4878
|
+
rootFolder,
|
|
4879
|
+
subfolder,
|
|
4880
|
+
true
|
|
4881
|
+
);
|
|
4882
|
+
} catch {
|
|
4883
|
+
return "failed";
|
|
4884
|
+
}
|
|
4885
|
+
}
|
|
4886
|
+
return "failed";
|
|
4887
|
+
}
|
|
4888
|
+
}
|
|
4889
|
+
async function performGoogleDriveExport(database, userAddress, token, deps, onProgress, rootFolder = DEFAULT_ROOT_FOLDER, subfolder = DEFAULT_CONVERSATIONS_FOLDER) {
|
|
4890
|
+
await deps.requestEncryptionKey(userAddress);
|
|
4891
|
+
const folderResult = await getConversationsFolder(
|
|
4892
|
+
token,
|
|
4893
|
+
deps.requestDriveAccess,
|
|
4894
|
+
rootFolder,
|
|
4895
|
+
subfolder
|
|
4896
|
+
);
|
|
4897
|
+
if (!folderResult) {
|
|
4898
|
+
return { success: false, uploaded: 0, skipped: 0, total: 0 };
|
|
4899
|
+
}
|
|
4900
|
+
const { token: activeToken } = folderResult;
|
|
4901
|
+
const { Q: Q4 } = await import("@nozbe/watermelondb");
|
|
4902
|
+
const conversationsCollection = database.get("conversations");
|
|
4903
|
+
const records = await conversationsCollection.query(Q4.where("is_deleted", false)).fetch();
|
|
4904
|
+
const conversations = records.map(conversationToStored);
|
|
4905
|
+
const total = conversations.length;
|
|
4906
|
+
if (total === 0) {
|
|
4907
|
+
return { success: true, uploaded: 0, skipped: 0, total: 0 };
|
|
4908
|
+
}
|
|
4909
|
+
let uploaded = 0;
|
|
4910
|
+
let skipped = 0;
|
|
4911
|
+
for (let i = 0; i < conversations.length; i++) {
|
|
4912
|
+
const conv = conversations[i];
|
|
4913
|
+
onProgress?.(i + 1, total);
|
|
4914
|
+
const result = await pushConversationToDrive(
|
|
4915
|
+
database,
|
|
4916
|
+
conv.conversationId,
|
|
4917
|
+
userAddress,
|
|
4918
|
+
activeToken,
|
|
4919
|
+
deps,
|
|
4920
|
+
rootFolder,
|
|
4921
|
+
subfolder
|
|
4922
|
+
);
|
|
4923
|
+
if (result === "uploaded") uploaded++;
|
|
4924
|
+
if (result === "skipped") skipped++;
|
|
4925
|
+
}
|
|
4926
|
+
return { success: true, uploaded, skipped, total };
|
|
4927
|
+
}
|
|
4928
|
+
async function performGoogleDriveImport(userAddress, token, deps, onProgress, rootFolder = DEFAULT_ROOT_FOLDER, subfolder = DEFAULT_CONVERSATIONS_FOLDER) {
|
|
4929
|
+
await deps.requestEncryptionKey(userAddress);
|
|
4930
|
+
const folderResult = await getConversationsFolder(
|
|
4931
|
+
token,
|
|
4932
|
+
deps.requestDriveAccess,
|
|
4933
|
+
rootFolder,
|
|
4934
|
+
subfolder
|
|
4935
|
+
);
|
|
4936
|
+
if (!folderResult) {
|
|
4937
|
+
return {
|
|
4938
|
+
success: false,
|
|
4939
|
+
restored: 0,
|
|
4940
|
+
failed: 0,
|
|
4941
|
+
total: 0,
|
|
4942
|
+
noBackupsFound: true
|
|
4943
|
+
};
|
|
4944
|
+
}
|
|
4945
|
+
const { folderId, token: activeToken } = folderResult;
|
|
4946
|
+
const remoteFiles = await listDriveFiles(activeToken, folderId);
|
|
4947
|
+
if (remoteFiles.length === 0) {
|
|
4948
|
+
return {
|
|
4949
|
+
success: false,
|
|
4950
|
+
restored: 0,
|
|
4951
|
+
failed: 0,
|
|
4952
|
+
total: 0,
|
|
4953
|
+
noBackupsFound: true
|
|
4954
|
+
};
|
|
4955
|
+
}
|
|
4956
|
+
const jsonFiles = remoteFiles.filter(
|
|
4957
|
+
(file) => file.name.endsWith(".json")
|
|
4958
|
+
);
|
|
4959
|
+
const total = jsonFiles.length;
|
|
4960
|
+
let restored = 0;
|
|
4961
|
+
let failed = 0;
|
|
4962
|
+
for (let i = 0; i < jsonFiles.length; i++) {
|
|
4963
|
+
const file = jsonFiles[i];
|
|
4964
|
+
onProgress?.(i + 1, total);
|
|
4965
|
+
try {
|
|
4966
|
+
const blob = await downloadDriveFile(activeToken, file.id);
|
|
4967
|
+
const result = await deps.importConversation(blob, userAddress);
|
|
4968
|
+
if (result.success) {
|
|
4969
|
+
restored++;
|
|
4970
|
+
} else {
|
|
4971
|
+
failed++;
|
|
4972
|
+
}
|
|
4973
|
+
} catch {
|
|
4974
|
+
failed++;
|
|
4975
|
+
}
|
|
4976
|
+
}
|
|
4977
|
+
return { success: true, restored, failed, total };
|
|
4978
|
+
}
|
|
4979
|
+
|
|
4980
|
+
// src/react/useGoogleDriveBackup.ts
|
|
4981
|
+
function useGoogleDriveBackup(options) {
|
|
4982
|
+
const {
|
|
4983
|
+
database,
|
|
4984
|
+
userAddress,
|
|
4985
|
+
accessToken,
|
|
4986
|
+
requestDriveAccess,
|
|
4987
|
+
requestEncryptionKey: requestEncryptionKey2,
|
|
4988
|
+
exportConversation,
|
|
4989
|
+
importConversation,
|
|
4990
|
+
rootFolder = DEFAULT_ROOT_FOLDER,
|
|
4991
|
+
conversationsFolder = DEFAULT_CONVERSATIONS_FOLDER
|
|
4992
|
+
} = options;
|
|
4993
|
+
const deps = useMemo5(
|
|
4994
|
+
() => ({
|
|
4995
|
+
requestDriveAccess,
|
|
4996
|
+
requestEncryptionKey: requestEncryptionKey2,
|
|
4997
|
+
exportConversation,
|
|
4998
|
+
importConversation
|
|
4999
|
+
}),
|
|
5000
|
+
[
|
|
5001
|
+
requestDriveAccess,
|
|
5002
|
+
requestEncryptionKey2,
|
|
5003
|
+
exportConversation,
|
|
5004
|
+
importConversation
|
|
5005
|
+
]
|
|
5006
|
+
);
|
|
5007
|
+
const ensureToken = useCallback12(async () => {
|
|
5008
|
+
if (accessToken) return accessToken;
|
|
5009
|
+
try {
|
|
5010
|
+
return await requestDriveAccess();
|
|
5011
|
+
} catch {
|
|
5012
|
+
return null;
|
|
5013
|
+
}
|
|
5014
|
+
}, [accessToken, requestDriveAccess]);
|
|
5015
|
+
const backup = useCallback12(
|
|
5016
|
+
async (backupOptions) => {
|
|
5017
|
+
if (!userAddress) {
|
|
5018
|
+
return { error: "Please sign in to backup to Google Drive" };
|
|
5019
|
+
}
|
|
5020
|
+
const token = await ensureToken();
|
|
5021
|
+
if (!token) {
|
|
5022
|
+
return { error: "Google Drive access denied" };
|
|
5023
|
+
}
|
|
5024
|
+
try {
|
|
5025
|
+
return await performGoogleDriveExport(
|
|
5026
|
+
database,
|
|
5027
|
+
userAddress,
|
|
5028
|
+
token,
|
|
5029
|
+
deps,
|
|
5030
|
+
backupOptions?.onProgress,
|
|
5031
|
+
rootFolder,
|
|
5032
|
+
conversationsFolder
|
|
5033
|
+
);
|
|
5034
|
+
} catch (err) {
|
|
5035
|
+
return {
|
|
5036
|
+
error: err instanceof Error ? err.message : "Failed to backup to Google Drive"
|
|
5037
|
+
};
|
|
5038
|
+
}
|
|
5039
|
+
},
|
|
5040
|
+
[database, userAddress, ensureToken, deps, rootFolder, conversationsFolder]
|
|
5041
|
+
);
|
|
5042
|
+
const restore = useCallback12(
|
|
5043
|
+
async (restoreOptions) => {
|
|
5044
|
+
if (!userAddress) {
|
|
5045
|
+
return { error: "Please sign in to restore from Google Drive" };
|
|
5046
|
+
}
|
|
5047
|
+
const token = await ensureToken();
|
|
5048
|
+
if (!token) {
|
|
5049
|
+
return { error: "Google Drive access denied" };
|
|
5050
|
+
}
|
|
5051
|
+
try {
|
|
5052
|
+
return await performGoogleDriveImport(
|
|
5053
|
+
userAddress,
|
|
5054
|
+
token,
|
|
5055
|
+
deps,
|
|
5056
|
+
restoreOptions?.onProgress,
|
|
5057
|
+
rootFolder,
|
|
5058
|
+
conversationsFolder
|
|
5059
|
+
);
|
|
5060
|
+
} catch (err) {
|
|
5061
|
+
return {
|
|
5062
|
+
error: err instanceof Error ? err.message : "Failed to restore from Google Drive"
|
|
5063
|
+
};
|
|
5064
|
+
}
|
|
5065
|
+
},
|
|
5066
|
+
[userAddress, ensureToken, deps, rootFolder, conversationsFolder]
|
|
5067
|
+
);
|
|
5068
|
+
return {
|
|
5069
|
+
backup,
|
|
5070
|
+
restore,
|
|
5071
|
+
isAuthenticated: !!accessToken
|
|
5072
|
+
};
|
|
5073
|
+
}
|
|
4228
5074
|
export {
|
|
4229
5075
|
Conversation as ChatConversation,
|
|
4230
5076
|
Message as ChatMessage,
|
|
5077
|
+
DEFAULT_BACKUP_FOLDER,
|
|
5078
|
+
DEFAULT_CONVERSATIONS_FOLDER as DEFAULT_DRIVE_CONVERSATIONS_FOLDER,
|
|
5079
|
+
DEFAULT_ROOT_FOLDER as DEFAULT_DRIVE_ROOT_FOLDER,
|
|
4231
5080
|
DEFAULT_TOOL_SELECTOR_MODEL,
|
|
5081
|
+
DropboxAuthProvider,
|
|
4232
5082
|
Memory as StoredMemoryModel,
|
|
4233
5083
|
ModelPreference as StoredModelPreferenceModel,
|
|
4234
5084
|
chatStorageMigrations,
|
|
4235
5085
|
chatStorageSchema,
|
|
5086
|
+
clearToken as clearDropboxToken,
|
|
4236
5087
|
createMemoryContextSystemMessage,
|
|
4237
5088
|
decryptData,
|
|
4238
5089
|
decryptDataBytes,
|
|
@@ -4243,14 +5094,19 @@ export {
|
|
|
4243
5094
|
generateCompositeKey,
|
|
4244
5095
|
generateConversationId,
|
|
4245
5096
|
generateUniqueKey,
|
|
5097
|
+
getStoredToken as getDropboxToken,
|
|
4246
5098
|
hasEncryptionKey,
|
|
4247
5099
|
memoryStorageSchema,
|
|
4248
5100
|
requestEncryptionKey,
|
|
4249
5101
|
selectTool,
|
|
4250
5102
|
settingsStorageSchema,
|
|
5103
|
+
storeToken as storeDropboxToken,
|
|
4251
5104
|
useChat,
|
|
4252
5105
|
useChatStorage,
|
|
5106
|
+
useDropboxAuth,
|
|
5107
|
+
useDropboxBackup,
|
|
4253
5108
|
useEncryption,
|
|
5109
|
+
useGoogleDriveBackup,
|
|
4254
5110
|
useImageGeneration,
|
|
4255
5111
|
useMemoryStorage,
|
|
4256
5112
|
useModels,
|