@reverbia/sdk 1.0.0-next.20260110032702 → 1.0.0-next.20260110210906
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.d.mts +26 -0
- package/dist/expo/index.d.ts +26 -0
- package/dist/react/index.cjs +418 -19
- package/dist/react/index.d.mts +159 -1
- package/dist/react/index.d.ts +159 -1
- package/dist/react/index.mjs +442 -52
- package/package.json +1 -1
package/dist/expo/index.d.mts
CHANGED
|
@@ -777,12 +777,38 @@ declare function generateCompositeKey(namespace: string, key: string): string;
|
|
|
777
777
|
declare function generateUniqueKey(namespace: string, key: string, value: string): string;
|
|
778
778
|
|
|
779
779
|
type ChatRole = "user" | "assistant" | "system";
|
|
780
|
+
/**
|
|
781
|
+
* Metadata for files attached to messages.
|
|
782
|
+
*
|
|
783
|
+
* Note the distinction between `url` and `sourceUrl`:
|
|
784
|
+
* - `url`: Content URL that gets sent to the AI as part of the message (e.g., data URIs for user uploads)
|
|
785
|
+
* - `sourceUrl`: Original external URL for locally-cached files (for lookup only, never sent to AI)
|
|
786
|
+
*/
|
|
780
787
|
interface FileMetadata {
|
|
788
|
+
/** Unique identifier for the file (used as OPFS key for cached files) */
|
|
781
789
|
id: string;
|
|
790
|
+
/** Display name of the file */
|
|
782
791
|
name: string;
|
|
792
|
+
/** MIME type (e.g., "image/png") */
|
|
783
793
|
type: string;
|
|
794
|
+
/** File size in bytes */
|
|
784
795
|
size: number;
|
|
796
|
+
/**
|
|
797
|
+
* Content URL to include when sending this message to the AI.
|
|
798
|
+
* When present, this URL is added as an `image_url` content part.
|
|
799
|
+
* Typically used for user-uploaded files (data URIs) that should be sent with the message.
|
|
800
|
+
*
|
|
801
|
+
* NOT used for MCP-cached files - those use `sourceUrl` for lookup and render from OPFS.
|
|
802
|
+
*/
|
|
785
803
|
url?: string;
|
|
804
|
+
/**
|
|
805
|
+
* Original external URL for files downloaded and cached locally (e.g., from MCP R2).
|
|
806
|
+
* Used purely for URL→OPFS mapping to enable fallback when the source returns 404.
|
|
807
|
+
*
|
|
808
|
+
* This is metadata for local lookup only - it is NOT sent to the AI or rendered directly.
|
|
809
|
+
* The file content is served from OPFS using the `id` field.
|
|
810
|
+
*/
|
|
811
|
+
sourceUrl?: string;
|
|
786
812
|
}
|
|
787
813
|
interface ChatCompletionUsage {
|
|
788
814
|
promptTokens?: number;
|
package/dist/expo/index.d.ts
CHANGED
|
@@ -777,12 +777,38 @@ declare function generateCompositeKey(namespace: string, key: string): string;
|
|
|
777
777
|
declare function generateUniqueKey(namespace: string, key: string, value: string): string;
|
|
778
778
|
|
|
779
779
|
type ChatRole = "user" | "assistant" | "system";
|
|
780
|
+
/**
|
|
781
|
+
* Metadata for files attached to messages.
|
|
782
|
+
*
|
|
783
|
+
* Note the distinction between `url` and `sourceUrl`:
|
|
784
|
+
* - `url`: Content URL that gets sent to the AI as part of the message (e.g., data URIs for user uploads)
|
|
785
|
+
* - `sourceUrl`: Original external URL for locally-cached files (for lookup only, never sent to AI)
|
|
786
|
+
*/
|
|
780
787
|
interface FileMetadata {
|
|
788
|
+
/** Unique identifier for the file (used as OPFS key for cached files) */
|
|
781
789
|
id: string;
|
|
790
|
+
/** Display name of the file */
|
|
782
791
|
name: string;
|
|
792
|
+
/** MIME type (e.g., "image/png") */
|
|
783
793
|
type: string;
|
|
794
|
+
/** File size in bytes */
|
|
784
795
|
size: number;
|
|
796
|
+
/**
|
|
797
|
+
* Content URL to include when sending this message to the AI.
|
|
798
|
+
* When present, this URL is added as an `image_url` content part.
|
|
799
|
+
* Typically used for user-uploaded files (data URIs) that should be sent with the message.
|
|
800
|
+
*
|
|
801
|
+
* NOT used for MCP-cached files - those use `sourceUrl` for lookup and render from OPFS.
|
|
802
|
+
*/
|
|
785
803
|
url?: string;
|
|
804
|
+
/**
|
|
805
|
+
* Original external URL for files downloaded and cached locally (e.g., from MCP R2).
|
|
806
|
+
* Used purely for URL→OPFS mapping to enable fallback when the source returns 404.
|
|
807
|
+
*
|
|
808
|
+
* This is metadata for local lookup only - it is NOT sent to the AI or rendered directly.
|
|
809
|
+
* The file content is served from OPFS using the `id` field.
|
|
810
|
+
*/
|
|
811
|
+
sourceUrl?: string;
|
|
786
812
|
}
|
|
787
813
|
interface ChatCompletionUsage {
|
|
788
814
|
promptTokens?: number;
|
package/dist/react/index.cjs
CHANGED
|
@@ -42,6 +42,7 @@ __export(index_exports, {
|
|
|
42
42
|
BACKUP_DRIVE_ROOT_FOLDER: () => DEFAULT_ROOT_FOLDER,
|
|
43
43
|
BACKUP_ICLOUD_FOLDER: () => DEFAULT_BACKUP_FOLDER2,
|
|
44
44
|
BackupAuthProvider: () => BackupAuthProvider,
|
|
45
|
+
BlobUrlManager: () => BlobUrlManager,
|
|
45
46
|
ChatConversation: () => Conversation,
|
|
46
47
|
ChatMessage: () => Message,
|
|
47
48
|
DEFAULT_BACKUP_FOLDER: () => DEFAULT_BACKUP_FOLDER,
|
|
@@ -71,9 +72,12 @@ __export(index_exports, {
|
|
|
71
72
|
createMemoryContextSystemMessage: () => createMemoryContextSystemMessage,
|
|
72
73
|
decryptData: () => decryptData,
|
|
73
74
|
decryptDataBytes: () => decryptDataBytes,
|
|
75
|
+
deleteEncryptedFile: () => deleteEncryptedFile,
|
|
74
76
|
encryptData: () => encryptData,
|
|
75
77
|
exportPublicKey: () => exportPublicKey,
|
|
76
78
|
extractConversationContext: () => extractConversationContext,
|
|
79
|
+
fileExists: () => fileExists,
|
|
80
|
+
findFileIdBySourceUrl: () => findFileIdBySourceUrl,
|
|
77
81
|
formatMemoriesForChat: () => formatMemoriesForChat,
|
|
78
82
|
generateCompositeKey: () => generateCompositeKey,
|
|
79
83
|
generateConversationId: () => generateConversationId,
|
|
@@ -84,6 +88,7 @@ __export(index_exports, {
|
|
|
84
88
|
getAndClearDriveReturnUrl: () => getAndClearDriveReturnUrl,
|
|
85
89
|
getCalendarAccessToken: () => getCalendarAccessToken,
|
|
86
90
|
getDriveAccessToken: () => getDriveAccessToken,
|
|
91
|
+
getEncryptionKey: () => getEncryptionKey,
|
|
87
92
|
getGoogleDriveStoredToken: () => getGoogleDriveStoredToken,
|
|
88
93
|
getValidCalendarToken: () => getValidCalendarToken,
|
|
89
94
|
getValidDriveToken: () => getValidDriveToken,
|
|
@@ -98,9 +103,12 @@ __export(index_exports, {
|
|
|
98
103
|
hasKeyPair: () => hasKeyPair,
|
|
99
104
|
isCalendarCallback: () => isCalendarCallback,
|
|
100
105
|
isDriveCallback: () => isDriveCallback,
|
|
106
|
+
isOPFSSupported: () => isOPFSSupported,
|
|
101
107
|
memoryStorageSchema: () => memoryStorageSchema,
|
|
108
|
+
readEncryptedFile: () => readEncryptedFile,
|
|
102
109
|
refreshCalendarToken: () => refreshCalendarToken,
|
|
103
110
|
refreshDriveToken: () => refreshDriveToken,
|
|
111
|
+
replaceUrlWithMCPPlaceholder: () => replaceUrlWithMCPPlaceholder,
|
|
104
112
|
requestEncryptionKey: () => requestEncryptionKey,
|
|
105
113
|
requestKeyPair: () => requestKeyPair,
|
|
106
114
|
revokeCalendarToken: () => revokeCalendarToken,
|
|
@@ -135,7 +143,8 @@ __export(index_exports, {
|
|
|
135
143
|
usePdf: () => usePdf,
|
|
136
144
|
useSearch: () => useSearch,
|
|
137
145
|
useSettings: () => useSettings,
|
|
138
|
-
userPreferencesStorageSchema: () => userPreferencesStorageSchema
|
|
146
|
+
userPreferencesStorageSchema: () => userPreferencesStorageSchema,
|
|
147
|
+
writeEncryptedFile: () => writeEncryptedFile
|
|
139
148
|
});
|
|
140
149
|
module.exports = __toCommonJS(index_exports);
|
|
141
150
|
|
|
@@ -2827,21 +2836,262 @@ async function searchMessagesOp(ctx, queryVector, options) {
|
|
|
2827
2836
|
return resultsWithSimilarity.sort((a, b) => b.similarity - a.similarity).slice(0, limit);
|
|
2828
2837
|
}
|
|
2829
2838
|
|
|
2839
|
+
// src/lib/storage/opfs.ts
|
|
2840
|
+
var FILE_PLACEHOLDER_PREFIX = "__SDKFILE__";
|
|
2841
|
+
var FILE_PLACEHOLDER_SUFFIX = "__";
|
|
2842
|
+
var FILE_PLACEHOLDER_REGEX = /__SDKFILE__([a-f0-9-]+)__/g;
|
|
2843
|
+
function createFilePlaceholder(fileId) {
|
|
2844
|
+
return `${FILE_PLACEHOLDER_PREFIX}${fileId}${FILE_PLACEHOLDER_SUFFIX}`;
|
|
2845
|
+
}
|
|
2846
|
+
function extractFileIds(content) {
|
|
2847
|
+
const matches = content.matchAll(FILE_PLACEHOLDER_REGEX);
|
|
2848
|
+
return Array.from(matches, (m) => m[1]);
|
|
2849
|
+
}
|
|
2850
|
+
function isOPFSSupported() {
|
|
2851
|
+
return typeof navigator !== "undefined" && "storage" in navigator && "getDirectory" in navigator.storage;
|
|
2852
|
+
}
|
|
2853
|
+
async function getSDKDirectory() {
|
|
2854
|
+
const root = await navigator.storage.getDirectory();
|
|
2855
|
+
return root.getDirectoryHandle("reverbia-sdk-files", { create: true });
|
|
2856
|
+
}
|
|
2857
|
+
function bytesToHex2(bytes) {
|
|
2858
|
+
return Array.from(bytes).map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
2859
|
+
}
|
|
2860
|
+
function hexToBytes2(hex) {
|
|
2861
|
+
const bytes = new Uint8Array(hex.length / 2);
|
|
2862
|
+
for (let i = 0; i < hex.length; i += 2) {
|
|
2863
|
+
bytes[i / 2] = parseInt(hex.slice(i, i + 2), 16);
|
|
2864
|
+
}
|
|
2865
|
+
return bytes;
|
|
2866
|
+
}
|
|
2867
|
+
async function encryptBlob(blob, encryptionKey) {
|
|
2868
|
+
const arrayBuffer = await blob.arrayBuffer();
|
|
2869
|
+
const plaintext = new Uint8Array(arrayBuffer);
|
|
2870
|
+
const iv = crypto.getRandomValues(new Uint8Array(12));
|
|
2871
|
+
const ciphertext = await crypto.subtle.encrypt(
|
|
2872
|
+
{ name: "AES-GCM", iv },
|
|
2873
|
+
encryptionKey,
|
|
2874
|
+
plaintext
|
|
2875
|
+
);
|
|
2876
|
+
const combined = new Uint8Array(iv.length + ciphertext.byteLength);
|
|
2877
|
+
combined.set(iv, 0);
|
|
2878
|
+
combined.set(new Uint8Array(ciphertext), iv.length);
|
|
2879
|
+
return bytesToHex2(combined);
|
|
2880
|
+
}
|
|
2881
|
+
async function decryptToBytes(encryptedHex, encryptionKey) {
|
|
2882
|
+
const combined = hexToBytes2(encryptedHex);
|
|
2883
|
+
const iv = combined.slice(0, 12);
|
|
2884
|
+
const ciphertext = combined.slice(12);
|
|
2885
|
+
const decrypted = await crypto.subtle.decrypt(
|
|
2886
|
+
{ name: "AES-GCM", iv },
|
|
2887
|
+
encryptionKey,
|
|
2888
|
+
ciphertext
|
|
2889
|
+
);
|
|
2890
|
+
return new Uint8Array(decrypted);
|
|
2891
|
+
}
|
|
2892
|
+
async function writeEncryptedFile(fileId, blob, encryptionKey, metadata) {
|
|
2893
|
+
if (!isOPFSSupported()) {
|
|
2894
|
+
throw new Error("OPFS is not supported in this browser");
|
|
2895
|
+
}
|
|
2896
|
+
const dir = await getSDKDirectory();
|
|
2897
|
+
const encryptedHex = await encryptBlob(blob, encryptionKey);
|
|
2898
|
+
const contentHandle = await dir.getFileHandle(`${fileId}.enc`, {
|
|
2899
|
+
create: true
|
|
2900
|
+
});
|
|
2901
|
+
const contentWritable = await contentHandle.createWritable();
|
|
2902
|
+
await contentWritable.write(encryptedHex);
|
|
2903
|
+
await contentWritable.close();
|
|
2904
|
+
const fileMetadata = {
|
|
2905
|
+
id: fileId,
|
|
2906
|
+
name: metadata?.name || `file-${fileId}`,
|
|
2907
|
+
type: blob.type || "application/octet-stream",
|
|
2908
|
+
size: blob.size,
|
|
2909
|
+
sourceUrl: metadata?.sourceUrl,
|
|
2910
|
+
createdAt: Date.now()
|
|
2911
|
+
};
|
|
2912
|
+
const metaHandle = await dir.getFileHandle(`${fileId}.meta.json`, {
|
|
2913
|
+
create: true
|
|
2914
|
+
});
|
|
2915
|
+
const metaWritable = await metaHandle.createWritable();
|
|
2916
|
+
await metaWritable.write(JSON.stringify(fileMetadata));
|
|
2917
|
+
await metaWritable.close();
|
|
2918
|
+
}
|
|
2919
|
+
async function readEncryptedFile(fileId, encryptionKey) {
|
|
2920
|
+
if (!isOPFSSupported()) {
|
|
2921
|
+
throw new Error("OPFS is not supported in this browser");
|
|
2922
|
+
}
|
|
2923
|
+
const dir = await getSDKDirectory();
|
|
2924
|
+
try {
|
|
2925
|
+
const contentHandle = await dir.getFileHandle(`${fileId}.enc`);
|
|
2926
|
+
const contentFile = await contentHandle.getFile();
|
|
2927
|
+
const encryptedHex = await contentFile.text();
|
|
2928
|
+
const metaHandle = await dir.getFileHandle(`${fileId}.meta.json`);
|
|
2929
|
+
const metaFile = await metaHandle.getFile();
|
|
2930
|
+
const metadata = JSON.parse(await metaFile.text());
|
|
2931
|
+
const decryptedBytes = await decryptToBytes(encryptedHex, encryptionKey);
|
|
2932
|
+
const blob = new Blob([decryptedBytes.buffer], { type: metadata.type });
|
|
2933
|
+
return { blob, metadata };
|
|
2934
|
+
} catch (error) {
|
|
2935
|
+
if (error instanceof DOMException && error.name === "NotFoundError") {
|
|
2936
|
+
return null;
|
|
2937
|
+
}
|
|
2938
|
+
throw error;
|
|
2939
|
+
}
|
|
2940
|
+
}
|
|
2941
|
+
async function deleteEncryptedFile(fileId) {
|
|
2942
|
+
if (!isOPFSSupported()) {
|
|
2943
|
+
throw new Error("OPFS is not supported in this browser");
|
|
2944
|
+
}
|
|
2945
|
+
const dir = await getSDKDirectory();
|
|
2946
|
+
try {
|
|
2947
|
+
await dir.removeEntry(`${fileId}.enc`);
|
|
2948
|
+
await dir.removeEntry(`${fileId}.meta.json`);
|
|
2949
|
+
} catch {
|
|
2950
|
+
}
|
|
2951
|
+
}
|
|
2952
|
+
async function fileExists(fileId) {
|
|
2953
|
+
if (!isOPFSSupported()) {
|
|
2954
|
+
return false;
|
|
2955
|
+
}
|
|
2956
|
+
const dir = await getSDKDirectory();
|
|
2957
|
+
try {
|
|
2958
|
+
await dir.getFileHandle(`${fileId}.enc`);
|
|
2959
|
+
return true;
|
|
2960
|
+
} catch {
|
|
2961
|
+
return false;
|
|
2962
|
+
}
|
|
2963
|
+
}
|
|
2964
|
+
var BlobUrlManager = class {
|
|
2965
|
+
constructor() {
|
|
2966
|
+
this.activeUrls = /* @__PURE__ */ new Map();
|
|
2967
|
+
}
|
|
2968
|
+
// fileId -> blobUrl
|
|
2969
|
+
/**
|
|
2970
|
+
* Creates a blob URL for a file and tracks it.
|
|
2971
|
+
*/
|
|
2972
|
+
createUrl(fileId, blob) {
|
|
2973
|
+
this.revokeUrl(fileId);
|
|
2974
|
+
const url = URL.createObjectURL(blob);
|
|
2975
|
+
this.activeUrls.set(fileId, url);
|
|
2976
|
+
return url;
|
|
2977
|
+
}
|
|
2978
|
+
/**
|
|
2979
|
+
* Gets the active blob URL for a file, if any.
|
|
2980
|
+
*/
|
|
2981
|
+
getUrl(fileId) {
|
|
2982
|
+
return this.activeUrls.get(fileId);
|
|
2983
|
+
}
|
|
2984
|
+
/**
|
|
2985
|
+
* Revokes a blob URL and removes it from tracking.
|
|
2986
|
+
*/
|
|
2987
|
+
revokeUrl(fileId) {
|
|
2988
|
+
const url = this.activeUrls.get(fileId);
|
|
2989
|
+
if (url) {
|
|
2990
|
+
URL.revokeObjectURL(url);
|
|
2991
|
+
this.activeUrls.delete(fileId);
|
|
2992
|
+
}
|
|
2993
|
+
}
|
|
2994
|
+
/**
|
|
2995
|
+
* Revokes all tracked blob URLs.
|
|
2996
|
+
*/
|
|
2997
|
+
revokeAll() {
|
|
2998
|
+
for (const url of this.activeUrls.values()) {
|
|
2999
|
+
URL.revokeObjectURL(url);
|
|
3000
|
+
}
|
|
3001
|
+
this.activeUrls.clear();
|
|
3002
|
+
}
|
|
3003
|
+
/**
|
|
3004
|
+
* Gets the count of active blob URLs.
|
|
3005
|
+
*/
|
|
3006
|
+
get size() {
|
|
3007
|
+
return this.activeUrls.size;
|
|
3008
|
+
}
|
|
3009
|
+
};
|
|
3010
|
+
|
|
2830
3011
|
// src/react/useChatStorage.ts
|
|
2831
|
-
function
|
|
2832
|
-
const
|
|
2833
|
-
|
|
2834
|
-
|
|
3012
|
+
function replaceUrlWithMCPPlaceholder(content, url, fileId) {
|
|
3013
|
+
const escapedUrl = url.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
3014
|
+
const placeholder = `![MCP_IMAGE:${fileId}]`;
|
|
3015
|
+
let result = content;
|
|
3016
|
+
const markdownImagePattern = new RegExp(
|
|
3017
|
+
`!\\[[^\\]]*\\]\\([\\s]*${escapedUrl}[\\s]*\\)`,
|
|
3018
|
+
"g"
|
|
3019
|
+
);
|
|
3020
|
+
result = result.replace(markdownImagePattern, placeholder);
|
|
3021
|
+
result = result.replace(new RegExp(escapedUrl, "g"), placeholder);
|
|
3022
|
+
const orphanedMarkdownPattern = new RegExp(
|
|
3023
|
+
`!\\[[^\\]]*\\]\\([\\s]*\\!\\[MCP_IMAGE:${fileId}\\][\\s]*\\)`,
|
|
3024
|
+
"g"
|
|
3025
|
+
);
|
|
3026
|
+
result = result.replace(orphanedMarkdownPattern, placeholder);
|
|
3027
|
+
return result;
|
|
3028
|
+
}
|
|
3029
|
+
function findFileIdBySourceUrl(files, sourceUrl) {
|
|
3030
|
+
return files?.find((f) => f.sourceUrl === sourceUrl)?.id;
|
|
3031
|
+
}
|
|
3032
|
+
async function isUrlValid(url, timeoutMs = 5e3) {
|
|
3033
|
+
try {
|
|
3034
|
+
const controller = new AbortController();
|
|
3035
|
+
const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
|
|
3036
|
+
const response = await fetch(url, {
|
|
3037
|
+
method: "GET",
|
|
3038
|
+
headers: { Range: "bytes=0-0" },
|
|
3039
|
+
signal: controller.signal
|
|
3040
|
+
});
|
|
3041
|
+
clearTimeout(timeoutId);
|
|
3042
|
+
return response.ok || response.status === 206;
|
|
3043
|
+
} catch {
|
|
3044
|
+
return false;
|
|
3045
|
+
}
|
|
3046
|
+
}
|
|
3047
|
+
async function storedToLlmapiMessage(stored) {
|
|
3048
|
+
let textContent = stored.content;
|
|
3049
|
+
const fileUrlMap = /* @__PURE__ */ new Map();
|
|
3050
|
+
const imageParts = [];
|
|
2835
3051
|
if (stored.files?.length) {
|
|
2836
3052
|
for (const file of stored.files) {
|
|
2837
3053
|
if (file.url) {
|
|
2838
|
-
|
|
3054
|
+
imageParts.push({
|
|
2839
3055
|
type: "image_url",
|
|
2840
3056
|
image_url: { url: file.url }
|
|
2841
3057
|
});
|
|
3058
|
+
} else if (file.sourceUrl) {
|
|
3059
|
+
const isValid = await isUrlValid(file.sourceUrl);
|
|
3060
|
+
if (isValid) {
|
|
3061
|
+
imageParts.push({
|
|
3062
|
+
type: "image_url",
|
|
3063
|
+
image_url: { url: file.sourceUrl }
|
|
3064
|
+
});
|
|
3065
|
+
fileUrlMap.set(file.id, file.sourceUrl);
|
|
3066
|
+
}
|
|
2842
3067
|
}
|
|
2843
3068
|
}
|
|
2844
3069
|
}
|
|
3070
|
+
textContent = textContent.replace(
|
|
3071
|
+
/__SDKFILE__([a-f0-9-]+)__/g,
|
|
3072
|
+
(match, fileId) => {
|
|
3073
|
+
const sourceUrl = fileUrlMap.get(fileId);
|
|
3074
|
+
if (sourceUrl) {
|
|
3075
|
+
return ``;
|
|
3076
|
+
}
|
|
3077
|
+
return "";
|
|
3078
|
+
}
|
|
3079
|
+
);
|
|
3080
|
+
textContent = textContent.replace(
|
|
3081
|
+
/!\[MCP_IMAGE:([a-f0-9-]+)\]/g,
|
|
3082
|
+
(match, fileId) => {
|
|
3083
|
+
const sourceUrl = fileUrlMap.get(fileId);
|
|
3084
|
+
if (sourceUrl) {
|
|
3085
|
+
return ``;
|
|
3086
|
+
}
|
|
3087
|
+
return "";
|
|
3088
|
+
}
|
|
3089
|
+
);
|
|
3090
|
+
textContent = textContent.replace(/\n{3,}/g, "\n\n").trim();
|
|
3091
|
+
const content = [
|
|
3092
|
+
{ type: "text", text: textContent },
|
|
3093
|
+
...imageParts
|
|
3094
|
+
];
|
|
2845
3095
|
return {
|
|
2846
3096
|
role: stored.role,
|
|
2847
3097
|
content
|
|
@@ -2858,9 +3108,17 @@ function useChatStorage(options) {
|
|
|
2858
3108
|
onData,
|
|
2859
3109
|
onFinish,
|
|
2860
3110
|
onError,
|
|
2861
|
-
apiType
|
|
3111
|
+
apiType,
|
|
3112
|
+
walletAddress
|
|
2862
3113
|
} = options;
|
|
2863
3114
|
const [currentConversationId, setCurrentConversationId] = (0, import_react2.useState)(initialConversationId || null);
|
|
3115
|
+
const blobManagerRef = (0, import_react2.useRef)(new BlobUrlManager());
|
|
3116
|
+
(0, import_react2.useEffect)(() => {
|
|
3117
|
+
const manager = blobManagerRef.current;
|
|
3118
|
+
return () => {
|
|
3119
|
+
manager.revokeAll();
|
|
3120
|
+
};
|
|
3121
|
+
}, []);
|
|
2864
3122
|
const messagesCollection = (0, import_react2.useMemo)(
|
|
2865
3123
|
() => database.get("history"),
|
|
2866
3124
|
[database]
|
|
@@ -2928,9 +3186,49 @@ function useChatStorage(options) {
|
|
|
2928
3186
|
);
|
|
2929
3187
|
const getMessages = (0, import_react2.useCallback)(
|
|
2930
3188
|
async (convId) => {
|
|
2931
|
-
|
|
3189
|
+
const messages = await getMessagesOp(storageCtx, convId);
|
|
3190
|
+
if (walletAddress && hasEncryptionKey(walletAddress) && isOPFSSupported()) {
|
|
3191
|
+
try {
|
|
3192
|
+
const encryptionKey = await getEncryptionKey(walletAddress);
|
|
3193
|
+
const blobManager = blobManagerRef.current;
|
|
3194
|
+
const resolvedMessages = await Promise.all(
|
|
3195
|
+
messages.map(async (msg) => {
|
|
3196
|
+
const fileIds = extractFileIds(msg.content);
|
|
3197
|
+
if (fileIds.length === 0) {
|
|
3198
|
+
return msg;
|
|
3199
|
+
}
|
|
3200
|
+
let resolvedContent = msg.content;
|
|
3201
|
+
for (const fileId of fileIds) {
|
|
3202
|
+
let url = blobManager.getUrl(fileId);
|
|
3203
|
+
if (!url) {
|
|
3204
|
+
const result = await readEncryptedFile(fileId, encryptionKey);
|
|
3205
|
+
if (result) {
|
|
3206
|
+
url = blobManager.createUrl(fileId, result.blob);
|
|
3207
|
+
}
|
|
3208
|
+
}
|
|
3209
|
+
if (url) {
|
|
3210
|
+
const placeholder = createFilePlaceholder(fileId);
|
|
3211
|
+
resolvedContent = resolvedContent.replace(
|
|
3212
|
+
new RegExp(
|
|
3213
|
+
placeholder.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"),
|
|
3214
|
+
"g"
|
|
3215
|
+
),
|
|
3216
|
+
``
|
|
3217
|
+
);
|
|
3218
|
+
}
|
|
3219
|
+
}
|
|
3220
|
+
return { ...msg, content: resolvedContent };
|
|
3221
|
+
})
|
|
3222
|
+
);
|
|
3223
|
+
return resolvedMessages;
|
|
3224
|
+
} catch (error) {
|
|
3225
|
+
console.error("[useChatStorage] Failed to resolve file placeholders:", error);
|
|
3226
|
+
return messages;
|
|
3227
|
+
}
|
|
3228
|
+
}
|
|
3229
|
+
return messages;
|
|
2932
3230
|
},
|
|
2933
|
-
[storageCtx]
|
|
3231
|
+
[storageCtx, walletAddress]
|
|
2934
3232
|
);
|
|
2935
3233
|
const getMessageCount = (0, import_react2.useCallback)(
|
|
2936
3234
|
async (convId) => {
|
|
@@ -3054,7 +3352,8 @@ function useChatStorage(options) {
|
|
|
3054
3352
|
[]
|
|
3055
3353
|
);
|
|
3056
3354
|
const extractAndStoreMCPImages = (0, import_react2.useCallback)(
|
|
3057
|
-
async (content, writeFile) => {
|
|
3355
|
+
async (content, writeFile, options2) => {
|
|
3356
|
+
const { replaceUrls = true } = options2 ?? {};
|
|
3058
3357
|
try {
|
|
3059
3358
|
const MCP_IMAGE_URL_PATTERN = new RegExp(
|
|
3060
3359
|
`https://${MCP_R2_DOMAIN.replace(/\./g, "\\.")}[^\\s)]*`,
|
|
@@ -3128,9 +3427,10 @@ function useChatStorage(options) {
|
|
|
3128
3427
|
id: fileId,
|
|
3129
3428
|
name: fileName,
|
|
3130
3429
|
type: mimeType,
|
|
3131
|
-
size
|
|
3430
|
+
size,
|
|
3431
|
+
sourceUrl: imageUrl
|
|
3132
3432
|
});
|
|
3133
|
-
if (imageUrl) {
|
|
3433
|
+
if (replaceUrls && imageUrl) {
|
|
3134
3434
|
replaceUrlWithPlaceholder(imageUrl, fileId);
|
|
3135
3435
|
}
|
|
3136
3436
|
} else {
|
|
@@ -3152,6 +3452,89 @@ function useChatStorage(options) {
|
|
|
3152
3452
|
},
|
|
3153
3453
|
[]
|
|
3154
3454
|
);
|
|
3455
|
+
const extractAndStoreEncryptedMCPImages = (0, import_react2.useCallback)(
|
|
3456
|
+
async (content, address) => {
|
|
3457
|
+
try {
|
|
3458
|
+
if (!isOPFSSupported()) {
|
|
3459
|
+
console.warn("[extractAndStoreEncryptedMCPImages] OPFS not supported");
|
|
3460
|
+
return { processedFiles: [], cleanedContent: content };
|
|
3461
|
+
}
|
|
3462
|
+
if (!hasEncryptionKey(address)) {
|
|
3463
|
+
console.warn("[extractAndStoreEncryptedMCPImages] Encryption key not available");
|
|
3464
|
+
return { processedFiles: [], cleanedContent: content };
|
|
3465
|
+
}
|
|
3466
|
+
const MCP_IMAGE_URL_PATTERN = new RegExp(
|
|
3467
|
+
`https://${MCP_R2_DOMAIN.replace(/\./g, "\\.")}[^\\s)]*`,
|
|
3468
|
+
"g"
|
|
3469
|
+
);
|
|
3470
|
+
const urlMatches = content.match(MCP_IMAGE_URL_PATTERN);
|
|
3471
|
+
if (!urlMatches || urlMatches.length === 0) {
|
|
3472
|
+
return { processedFiles: [], cleanedContent: content };
|
|
3473
|
+
}
|
|
3474
|
+
const uniqueUrls = [...new Set(urlMatches)];
|
|
3475
|
+
const encryptionKey = await getEncryptionKey(address);
|
|
3476
|
+
const processedFiles = [];
|
|
3477
|
+
let cleanedContent = content;
|
|
3478
|
+
const results = await Promise.allSettled(
|
|
3479
|
+
uniqueUrls.map(async (imageUrl) => {
|
|
3480
|
+
const controller = new AbortController();
|
|
3481
|
+
const timeoutId = setTimeout(() => controller.abort(), 3e4);
|
|
3482
|
+
try {
|
|
3483
|
+
const response = await fetch(imageUrl, {
|
|
3484
|
+
signal: controller.signal,
|
|
3485
|
+
cache: "no-store"
|
|
3486
|
+
});
|
|
3487
|
+
if (!response.ok) {
|
|
3488
|
+
throw new Error(`Failed to fetch image: ${response.status}`);
|
|
3489
|
+
}
|
|
3490
|
+
const blob = await response.blob();
|
|
3491
|
+
const fileId = crypto.randomUUID();
|
|
3492
|
+
const urlPath = imageUrl.split("?")[0] ?? imageUrl;
|
|
3493
|
+
const extension = urlPath.match(/\.([a-zA-Z0-9]+)$/)?.[1] || "png";
|
|
3494
|
+
const mimeType = blob.type || `image/${extension}`;
|
|
3495
|
+
const fileName = `mcp-image-${Date.now()}-${fileId.slice(0, 8)}.${extension}`;
|
|
3496
|
+
await writeEncryptedFile(fileId, blob, encryptionKey, {
|
|
3497
|
+
name: fileName,
|
|
3498
|
+
sourceUrl: imageUrl
|
|
3499
|
+
});
|
|
3500
|
+
return { fileId, fileName, mimeType, size: blob.size, imageUrl };
|
|
3501
|
+
} finally {
|
|
3502
|
+
clearTimeout(timeoutId);
|
|
3503
|
+
}
|
|
3504
|
+
})
|
|
3505
|
+
);
|
|
3506
|
+
results.forEach((result, i) => {
|
|
3507
|
+
const imageUrl = uniqueUrls[i];
|
|
3508
|
+
if (result.status === "fulfilled") {
|
|
3509
|
+
const { fileId, fileName, mimeType, size } = result.value;
|
|
3510
|
+
processedFiles.push({
|
|
3511
|
+
id: fileId,
|
|
3512
|
+
name: fileName,
|
|
3513
|
+
type: mimeType,
|
|
3514
|
+
size,
|
|
3515
|
+
sourceUrl: imageUrl
|
|
3516
|
+
});
|
|
3517
|
+
const placeholder = createFilePlaceholder(fileId);
|
|
3518
|
+
const escapedUrl = imageUrl.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
3519
|
+
const markdownImagePattern = new RegExp(
|
|
3520
|
+
`!\\[[^\\]]*\\]\\([\\s]*${escapedUrl}[\\s]*\\)`,
|
|
3521
|
+
"g"
|
|
3522
|
+
);
|
|
3523
|
+
cleanedContent = cleanedContent.replace(markdownImagePattern, placeholder);
|
|
3524
|
+
cleanedContent = cleanedContent.replace(new RegExp(escapedUrl, "g"), placeholder);
|
|
3525
|
+
} else {
|
|
3526
|
+
console.error("[extractAndStoreEncryptedMCPImages] Failed:", result.reason);
|
|
3527
|
+
}
|
|
3528
|
+
});
|
|
3529
|
+
cleanedContent = cleanedContent.replace(/\n{3,}/g, "\n\n").trim();
|
|
3530
|
+
return { processedFiles, cleanedContent };
|
|
3531
|
+
} catch (err) {
|
|
3532
|
+
console.error("[extractAndStoreEncryptedMCPImages] Unexpected error:", err);
|
|
3533
|
+
return { processedFiles: [], cleanedContent: content };
|
|
3534
|
+
}
|
|
3535
|
+
},
|
|
3536
|
+
[]
|
|
3537
|
+
);
|
|
3155
3538
|
const sendMessage = (0, import_react2.useCallback)(
|
|
3156
3539
|
async (args) => {
|
|
3157
3540
|
const {
|
|
@@ -3203,10 +3586,10 @@ function useChatStorage(options) {
|
|
|
3203
3586
|
const storedMessages = await getMessages(convId);
|
|
3204
3587
|
const validMessages = storedMessages.filter((msg) => !msg.error);
|
|
3205
3588
|
const limitedMessages = validMessages.slice(-maxHistoryMessages);
|
|
3206
|
-
|
|
3207
|
-
|
|
3208
|
-
|
|
3209
|
-
];
|
|
3589
|
+
const historyMessages = await Promise.all(
|
|
3590
|
+
limitedMessages.map(storedToLlmapiMessage)
|
|
3591
|
+
);
|
|
3592
|
+
messagesToSend = [...historyMessages, ...messages];
|
|
3210
3593
|
} else {
|
|
3211
3594
|
messagesToSend = [...messages];
|
|
3212
3595
|
}
|
|
@@ -3344,7 +3727,14 @@ function useChatStorage(options) {
|
|
|
3344
3727
|
let cleanedContent = assistantContent.replace(jsonSourcesBlockRegex, "").trim();
|
|
3345
3728
|
cleanedContent = cleanedContent.replace(/\n{3,}/g, "\n\n");
|
|
3346
3729
|
let processedFiles = [];
|
|
3347
|
-
if (
|
|
3730
|
+
if (walletAddress) {
|
|
3731
|
+
const result2 = await extractAndStoreEncryptedMCPImages(
|
|
3732
|
+
cleanedContent,
|
|
3733
|
+
walletAddress
|
|
3734
|
+
);
|
|
3735
|
+
processedFiles = result2.processedFiles;
|
|
3736
|
+
cleanedContent = result2.cleanedContent;
|
|
3737
|
+
} else if (writeFile) {
|
|
3348
3738
|
const result2 = await extractAndStoreMCPImages(
|
|
3349
3739
|
cleanedContent,
|
|
3350
3740
|
writeFile
|
|
@@ -3380,7 +3770,7 @@ function useChatStorage(options) {
|
|
|
3380
3770
|
assistantMessage: storedAssistantMessage
|
|
3381
3771
|
};
|
|
3382
3772
|
},
|
|
3383
|
-
[ensureConversation, getMessages, storageCtx, baseSendMessage]
|
|
3773
|
+
[ensureConversation, getMessages, storageCtx, baseSendMessage, walletAddress, extractAndStoreEncryptedMCPImages]
|
|
3384
3774
|
);
|
|
3385
3775
|
const searchMessages = (0, import_react2.useCallback)(
|
|
3386
3776
|
async (queryVector, options2) => {
|
|
@@ -9155,6 +9545,7 @@ function hasDriveCredentials() {
|
|
|
9155
9545
|
BACKUP_DRIVE_ROOT_FOLDER,
|
|
9156
9546
|
BACKUP_ICLOUD_FOLDER,
|
|
9157
9547
|
BackupAuthProvider,
|
|
9548
|
+
BlobUrlManager,
|
|
9158
9549
|
ChatConversation,
|
|
9159
9550
|
ChatMessage,
|
|
9160
9551
|
DEFAULT_BACKUP_FOLDER,
|
|
@@ -9184,9 +9575,12 @@ function hasDriveCredentials() {
|
|
|
9184
9575
|
createMemoryContextSystemMessage,
|
|
9185
9576
|
decryptData,
|
|
9186
9577
|
decryptDataBytes,
|
|
9578
|
+
deleteEncryptedFile,
|
|
9187
9579
|
encryptData,
|
|
9188
9580
|
exportPublicKey,
|
|
9189
9581
|
extractConversationContext,
|
|
9582
|
+
fileExists,
|
|
9583
|
+
findFileIdBySourceUrl,
|
|
9190
9584
|
formatMemoriesForChat,
|
|
9191
9585
|
generateCompositeKey,
|
|
9192
9586
|
generateConversationId,
|
|
@@ -9197,6 +9591,7 @@ function hasDriveCredentials() {
|
|
|
9197
9591
|
getAndClearDriveReturnUrl,
|
|
9198
9592
|
getCalendarAccessToken,
|
|
9199
9593
|
getDriveAccessToken,
|
|
9594
|
+
getEncryptionKey,
|
|
9200
9595
|
getGoogleDriveStoredToken,
|
|
9201
9596
|
getValidCalendarToken,
|
|
9202
9597
|
getValidDriveToken,
|
|
@@ -9211,9 +9606,12 @@ function hasDriveCredentials() {
|
|
|
9211
9606
|
hasKeyPair,
|
|
9212
9607
|
isCalendarCallback,
|
|
9213
9608
|
isDriveCallback,
|
|
9609
|
+
isOPFSSupported,
|
|
9214
9610
|
memoryStorageSchema,
|
|
9611
|
+
readEncryptedFile,
|
|
9215
9612
|
refreshCalendarToken,
|
|
9216
9613
|
refreshDriveToken,
|
|
9614
|
+
replaceUrlWithMCPPlaceholder,
|
|
9217
9615
|
requestEncryptionKey,
|
|
9218
9616
|
requestKeyPair,
|
|
9219
9617
|
revokeCalendarToken,
|
|
@@ -9248,5 +9646,6 @@ function hasDriveCredentials() {
|
|
|
9248
9646
|
usePdf,
|
|
9249
9647
|
useSearch,
|
|
9250
9648
|
useSettings,
|
|
9251
|
-
userPreferencesStorageSchema
|
|
9649
|
+
userPreferencesStorageSchema,
|
|
9650
|
+
writeEncryptedFile
|
|
9252
9651
|
});
|