zerochat 7.3.0__py3-none-any.whl

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.
Files changed (130) hide show
  1. zerochat-7.3.0.dist-info/METADATA +70 -0
  2. zerochat-7.3.0.dist-info/RECORD +130 -0
  3. zerochat-7.3.0.dist-info/WHEEL +5 -0
  4. zerochat-7.3.0.dist-info/entry_points.txt +2 -0
  5. zerochat-7.3.0.dist-info/licenses/LICENSE +34 -0
  6. zerochat-7.3.0.dist-info/top_level.txt +2 -0
  7. zerochat.py +2004 -0
  8. zerochat_runtime/__init__.py +1 -0
  9. zerochat_runtime/assets/css/base.css +199 -0
  10. zerochat_runtime/assets/css/components/composer.css +1258 -0
  11. zerochat_runtime/assets/css/components/debug.css +777 -0
  12. zerochat_runtime/assets/css/components/header.css +97 -0
  13. zerochat_runtime/assets/css/components/icons.css +39 -0
  14. zerochat_runtime/assets/css/components/markdown.css +685 -0
  15. zerochat_runtime/assets/css/components/messages.css +473 -0
  16. zerochat_runtime/assets/css/components/modals.css +1319 -0
  17. zerochat_runtime/assets/css/components/sidebar.css +622 -0
  18. zerochat_runtime/assets/css/components/tools.css +1223 -0
  19. zerochat_runtime/assets/css/layout.css +171 -0
  20. zerochat_runtime/assets/css/print.css +120 -0
  21. zerochat_runtime/assets/css/styles.css +18 -0
  22. zerochat_runtime/assets/css/theme-overrides.css +555 -0
  23. zerochat_runtime/assets/css/tokens.css +165 -0
  24. zerochat_runtime/assets/help/architecture.html +247 -0
  25. zerochat_runtime/assets/help/composer.html +134 -0
  26. zerochat_runtime/assets/help/conversations.html +105 -0
  27. zerochat_runtime/assets/help/debug.html +108 -0
  28. zerochat_runtime/assets/help/en/architecture.html +247 -0
  29. zerochat_runtime/assets/help/en/composer.html +134 -0
  30. zerochat_runtime/assets/help/en/conversations.html +104 -0
  31. zerochat_runtime/assets/help/en/debug.html +105 -0
  32. zerochat_runtime/assets/help/en/gemini-free.html +39 -0
  33. zerochat_runtime/assets/help/en/index.html +191 -0
  34. zerochat_runtime/assets/help/en/learning.html +71 -0
  35. zerochat_runtime/assets/help/en/mcp.html +143 -0
  36. zerochat_runtime/assets/help/en/openrouter-free.html +39 -0
  37. zerochat_runtime/assets/help/en/profiles.html +173 -0
  38. zerochat_runtime/assets/help/en/rag.html +130 -0
  39. zerochat_runtime/assets/help/en/reasoning-telemetry.html +108 -0
  40. zerochat_runtime/assets/help/en/tools-agent.html +133 -0
  41. zerochat_runtime/assets/help/en/webllm.html +355 -0
  42. zerochat_runtime/assets/help/gemini-free.html +39 -0
  43. zerochat_runtime/assets/help/help.css +604 -0
  44. zerochat_runtime/assets/help/help.js +51 -0
  45. zerochat_runtime/assets/help/index.html +194 -0
  46. zerochat_runtime/assets/help/learning.html +71 -0
  47. zerochat_runtime/assets/help/mcp.html +143 -0
  48. zerochat_runtime/assets/help/openrouter-free.html +39 -0
  49. zerochat_runtime/assets/help/profiles.html +174 -0
  50. zerochat_runtime/assets/help/rag.html +130 -0
  51. zerochat_runtime/assets/help/reasoning-telemetry.html +111 -0
  52. zerochat_runtime/assets/help/tools-agent.html +133 -0
  53. zerochat_runtime/assets/help/webllm.html +359 -0
  54. zerochat_runtime/assets/js/agent-core.js +1553 -0
  55. zerochat_runtime/assets/js/api.js +844 -0
  56. zerochat_runtime/assets/js/app.js +2558 -0
  57. zerochat_runtime/assets/js/attachments.js +220 -0
  58. zerochat_runtime/assets/js/charts.js +338 -0
  59. zerochat_runtime/assets/js/chat-engine.js +566 -0
  60. zerochat_runtime/assets/js/config-store.js +207 -0
  61. zerochat_runtime/assets/js/context-manager.js +584 -0
  62. zerochat_runtime/assets/js/conversation-service.js +484 -0
  63. zerochat_runtime/assets/js/cookies.js +688 -0
  64. zerochat_runtime/assets/js/data-reset-service.js +136 -0
  65. zerochat_runtime/assets/js/debug.js +508 -0
  66. zerochat_runtime/assets/js/defaults.js +16 -0
  67. zerochat_runtime/assets/js/export.js +179 -0
  68. zerochat_runtime/assets/js/file-parser.js +2088 -0
  69. zerochat_runtime/assets/js/generation-controller.js +417 -0
  70. zerochat_runtime/assets/js/i18n.js +1483 -0
  71. zerochat_runtime/assets/js/icons.js +156 -0
  72. zerochat_runtime/assets/js/ingestionEngine.js +436 -0
  73. zerochat_runtime/assets/js/markdown.js +588 -0
  74. zerochat_runtime/assets/js/mcp.js +1271 -0
  75. zerochat_runtime/assets/js/message-turns.js +62 -0
  76. zerochat_runtime/assets/js/profile-backup.js +118 -0
  77. zerochat_runtime/assets/js/profile-export-bundle.js +481 -0
  78. zerochat_runtime/assets/js/profile-repository.js +213 -0
  79. zerochat_runtime/assets/js/providers-webllm.js +486 -0
  80. zerochat_runtime/assets/js/providers.js +1560 -0
  81. zerochat_runtime/assets/js/rag-index.js +295 -0
  82. zerochat_runtime/assets/js/rag-service.js +530 -0
  83. zerochat_runtime/assets/js/rag-ui.js +997 -0
  84. zerochat_runtime/assets/js/ragStorage.js +676 -0
  85. zerochat_runtime/assets/js/sandbox.js +449 -0
  86. zerochat_runtime/assets/js/state.js +704 -0
  87. zerochat_runtime/assets/js/storage-db.js +144 -0
  88. zerochat_runtime/assets/js/tool-cards.js +330 -0
  89. zerochat_runtime/assets/js/tool-security.js +653 -0
  90. zerochat_runtime/assets/js/tools/README.md +50 -0
  91. zerochat_runtime/assets/js/tools/builtin/agent-checkpoint.tool.js +212 -0
  92. zerochat_runtime/assets/js/tools/builtin/download-pdf.tool.js +111 -0
  93. zerochat_runtime/assets/js/tools/builtin/execute-javascript.tool.js +219 -0
  94. zerochat_runtime/assets/js/tools/builtin/fetch-web-page.tool.js +112 -0
  95. zerochat_runtime/assets/js/tools/builtin/list-documents.tool.js +117 -0
  96. zerochat_runtime/assets/js/tools/builtin/read-knowledge-chunk.tool.js +129 -0
  97. zerochat_runtime/assets/js/tools/builtin/read-knowledge-image.tool.js +95 -0
  98. zerochat_runtime/assets/js/tools/builtin/render-chart.tool.js +90 -0
  99. zerochat_runtime/assets/js/tools/builtin/search-knowledge-base.tool.js +124 -0
  100. zerochat_runtime/assets/js/tools/builtin/search-web.tool.js +125 -0
  101. zerochat_runtime/assets/js/tools/tool-manifest.js +53 -0
  102. zerochat_runtime/assets/js/tools/tool-runtime.js +77 -0
  103. zerochat_runtime/assets/js/ui-composer.js +319 -0
  104. zerochat_runtime/assets/js/ui-conversation.js +655 -0
  105. zerochat_runtime/assets/js/ui-dialogs.js +132 -0
  106. zerochat_runtime/assets/js/ui-generation-status.js +119 -0
  107. zerochat_runtime/assets/js/ui-inspector.js +804 -0
  108. zerochat_runtime/assets/js/ui-mcp.js +604 -0
  109. zerochat_runtime/assets/js/ui-profiles.js +824 -0
  110. zerochat_runtime/assets/js/ui-reasoning.js +146 -0
  111. zerochat_runtime/assets/js/ui-settings.js +825 -0
  112. zerochat_runtime/assets/js/ui-shell.js +215 -0
  113. zerochat_runtime/assets/js/ui-sidebar.js +429 -0
  114. zerochat_runtime/assets/js/ui-telemetry.js +404 -0
  115. zerochat_runtime/assets/js/ui-transfer.js +262 -0
  116. zerochat_runtime/assets/js/utils.js +216 -0
  117. zerochat_runtime/assets/js/vendor/orama.browser.js +11 -0
  118. zerochat_runtime/assets/js/web-browser.js +569 -0
  119. zerochat_runtime/assets/js/web-search.js +519 -0
  120. zerochat_runtime/assets/manifest.webmanifest +20 -0
  121. zerochat_runtime/assets/services/dummy_mcp/dummy_mcp_server.py +47 -0
  122. zerochat_runtime/assets/services/dummy_mcp/service.json +22 -0
  123. zerochat_runtime/assets/services/lsp/installer.json +9 -0
  124. zerochat_runtime/assets/services/lsp/service.json +22 -0
  125. zerochat_runtime/assets/services/memory/installer.json +9 -0
  126. zerochat_runtime/assets/services/memory/service.json +24 -0
  127. zerochat_runtime/assets/services/playwright/installer.json +10 -0
  128. zerochat_runtime/assets/services/playwright/service.json +39 -0
  129. zerochat_runtime/assets/sw.js +164 -0
  130. zerochat_runtime/assets/zerochat.html +671 -0
@@ -0,0 +1,676 @@
1
+ /**
2
+ * IndexedDB persistence for the ZeroChat local knowledge base.
3
+ * Documents, extracted images and retrieval chunks are stored independently in
4
+ * ZeroChatDB. Search indexes are derived data and never the source of truth.
5
+ */
6
+ (function (root, factory) {
7
+ if (typeof exports === 'object' && typeof module !== 'undefined') {
8
+ module.exports = factory(require('./storage-db.js'));
9
+ } else {
10
+ root.ChatRagStorage = factory(root.ZeroChatDB);
11
+ }
12
+ })(typeof self !== 'undefined' ? self : this, function (Database) {
13
+ 'use strict';
14
+
15
+ const STORES = Database ? Database.STORES : {
16
+ ragBranches: 'rag_branches', ragDocuments: 'rag_documents',
17
+ ragImages: 'rag_images', ragChunks: 'rag_chunks', ragMeta: 'rag_meta'
18
+ };
19
+ const memory = {
20
+ branches: new Map(), documents: new Map(), images: new Map(), chunks: new Map(), meta: new Map()
21
+ };
22
+
23
+ class RagStorageError extends Error {
24
+ constructor(message, details = {}) { super(message); this.name = 'RagStorageError'; this.details = details; }
25
+ }
26
+ class ValidationError extends RagStorageError {
27
+ constructor(message, details = {}) { super(message, details); this.name = 'ValidationError'; }
28
+ }
29
+ class QuotaExceededError extends RagStorageError {
30
+ constructor(message = 'Se ha superado la cuota de almacenamiento del navegador.', details = {}) {
31
+ super(message, details); this.name = 'QuotaExceededError';
32
+ }
33
+ }
34
+ class NotFoundError extends RagStorageError {
35
+ constructor(message, details = {}) { super(message, details); this.name = 'NotFoundError'; }
36
+ }
37
+
38
+ function generateId(prefix) {
39
+ if (typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function') return `${prefix}_${crypto.randomUUID()}`;
40
+ return `${prefix}_${Date.now()}_${Math.random().toString(36).slice(2, 10)}`;
41
+ }
42
+
43
+ function isQuotaError(error) {
44
+ return Boolean(error && (error.name === 'QuotaExceededError' || error.name === 'NS_ERROR_DOM_QUOTA_REACHED' || error.code === 22 || error.code === 1014));
45
+ }
46
+
47
+ async function openDatabase() {
48
+ return Database && Database.openDatabase ? Database.openDatabase() : null;
49
+ }
50
+
51
+ function requestResult(request) {
52
+ return new Promise((resolve, reject) => {
53
+ request.onsuccess = () => resolve(request.result);
54
+ request.onerror = () => reject(request.error);
55
+ });
56
+ }
57
+
58
+ function transactionDone(transaction) {
59
+ return new Promise((resolve, reject) => {
60
+ transaction.oncomplete = () => resolve(true);
61
+ transaction.onerror = () => reject(transaction.error);
62
+ transaction.onabort = () => reject(transaction.error || new RagStorageError('Transacción cancelada.'));
63
+ });
64
+ }
65
+
66
+ async function requestPersistentStorage() {
67
+ try {
68
+ if (typeof navigator !== 'undefined' && navigator.storage && typeof navigator.storage.persist === 'function') {
69
+ return navigator.storage.persist();
70
+ }
71
+ } catch (_) {}
72
+ return false;
73
+ }
74
+
75
+ function validateBranch(input) {
76
+ const data = typeof input === 'string' ? { name: input } : input;
77
+ if (!data || typeof data !== 'object' || !String(data.name || '').trim()) {
78
+ throw new ValidationError('El nombre de la rama es obligatorio.');
79
+ }
80
+ const now = Date.now();
81
+ return {
82
+ id: String(data.id || generateId('branch')),
83
+ name: String(data.name).trim(),
84
+ description: String(data.description || '').trim(),
85
+ language: String(data.language || 'spanish').trim().toLowerCase(),
86
+ createdAt: Number(data.createdAt) || now,
87
+ updatedAt: Number(data.updatedAt) || now
88
+ };
89
+ }
90
+
91
+ function validateChunk(chunk, index, document) {
92
+ if (!chunk || typeof chunk !== 'object' || !String(chunk.content || '').trim()) {
93
+ throw new ValidationError(`El fragmento ${index + 1} no contiene texto.`);
94
+ }
95
+ const order = Number.isInteger(chunk.order) ? chunk.order : index;
96
+ return {
97
+ id: String(chunk.id || `${document.id}:chunk:${order}`),
98
+ branchId: document.branchId,
99
+ documentId: document.id,
100
+ order,
101
+ title: String(chunk.title || `Fragmento ${order + 1}`).trim(),
102
+ content: String(chunk.content),
103
+ charCount: String(chunk.content).length,
104
+ pageStart: Number.isFinite(chunk.pageStart) ? chunk.pageStart : null,
105
+ pageEnd: Number.isFinite(chunk.pageEnd) ? chunk.pageEnd : null,
106
+ createdAt: Number(chunk.createdAt) || document.createdAt
107
+ };
108
+ }
109
+
110
+ function validateDocument(data) {
111
+ if (!data || typeof data !== 'object') throw new ValidationError('El documento debe ser un objeto.');
112
+ if (!String(data.branchId || '').trim()) throw new ValidationError('El documento requiere una rama.');
113
+ if (!String(data.title || '').trim()) throw new ValidationError('El título del documento es obligatorio.');
114
+ const fileType = String(data.fileType || 'txt').toLowerCase();
115
+ if (!['pdf', 'txt', 'md'].includes(fileType)) throw new ValidationError(`Tipo de archivo no soportado: ${fileType}.`);
116
+ const now = Date.now();
117
+ const document = {
118
+ id: String(data.id || generateId('doc')),
119
+ branchId: String(data.branchId).trim(),
120
+ title: String(data.title).trim(),
121
+ path: String(data.path || data.title).trim(),
122
+ fileType,
123
+ mimeType: String(data.mimeType || ''),
124
+ fileSize: Number(data.fileSize) || 0,
125
+ chunkCount: 0,
126
+ imageCount: Math.max(0, Math.floor(Number(data.imageCount) || 0)),
127
+ createdAt: Number(data.createdAt) || now,
128
+ updatedAt: Number(data.updatedAt) || now
129
+ };
130
+ const sourceChunks = Array.isArray(data.chunks) ? data.chunks : [];
131
+ const chunks = sourceChunks.map((chunk, index) => validateChunk(chunk, index, document));
132
+ document.chunkCount = chunks.length;
133
+ return { document, chunks };
134
+ }
135
+
136
+ async function getByIndex(storeName, indexName, value) {
137
+ const db = await openDatabase();
138
+ if (!db) return null;
139
+ const tx = db.transaction(storeName, 'readonly');
140
+ const index = tx.objectStore(storeName).index(indexName);
141
+ // Usar openCursor en streaming para evitar el límite de IPC de Chromium
142
+ // ("The serialized value is too large: max=257949696 bytes") en ramas con cientos de documentos
143
+ if (typeof index.openCursor === 'function') {
144
+ return new Promise((resolve, reject) => {
145
+ const results = [];
146
+ try {
147
+ const keyRange = (typeof IDBKeyRange !== 'undefined' && IDBKeyRange && typeof IDBKeyRange.only === 'function')
148
+ ? IDBKeyRange.only(value)
149
+ : value;
150
+ const request = index.openCursor(keyRange);
151
+ request.onsuccess = (event) => {
152
+ const cursor = event.target.result;
153
+ if (cursor) {
154
+ results.push(cursor.value);
155
+ cursor.continue();
156
+ } else {
157
+ resolve(results);
158
+ }
159
+ };
160
+ request.onerror = () => reject(request.error || new Error(`Error en cursor de ${storeName}.${indexName}`));
161
+ } catch (err) {
162
+ if (typeof index.getAll === 'function') {
163
+ requestResult(index.getAll(value)).then(resolve, reject);
164
+ } else {
165
+ reject(err);
166
+ }
167
+ }
168
+ });
169
+ }
170
+ if (typeof index.getAll === 'function') {
171
+ return requestResult(index.getAll(value));
172
+ }
173
+ return [];
174
+ }
175
+
176
+ async function createBranch(nameOrData, description = '', language = '') {
177
+ const input = typeof nameOrData === 'string'
178
+ ? { name: nameOrData, description, ...(language ? { language } : {}) }
179
+ : nameOrData;
180
+ const branch = validateBranch(input);
181
+ const db = await openDatabase();
182
+ if (!db) { memory.branches.set(branch.id, branch); return { ...branch }; }
183
+ try {
184
+ const tx = db.transaction(STORES.ragBranches, 'readwrite');
185
+ tx.objectStore(STORES.ragBranches).add(branch);
186
+ await transactionDone(tx);
187
+ return { ...branch };
188
+ } catch (error) {
189
+ if (isQuotaError(error)) throw new QuotaExceededError(undefined, { error });
190
+ throw new RagStorageError(`No se pudo crear la rama: ${error.message || error}`, { error });
191
+ }
192
+ }
193
+
194
+ async function getBranches() {
195
+ const db = await openDatabase();
196
+ if (!db) return Array.from(memory.branches.values()).map(v => ({ ...v })).sort((a, b) => b.createdAt - a.createdAt);
197
+ const tx = db.transaction(STORES.ragBranches, 'readonly');
198
+ const values = await requestResult(tx.objectStore(STORES.ragBranches).getAll());
199
+ return values.sort((a, b) => b.createdAt - a.createdAt);
200
+ }
201
+
202
+ async function getBranchById(id) {
203
+ if (!id) return null;
204
+ const db = await openDatabase();
205
+ if (!db) return memory.branches.has(id) ? { ...memory.branches.get(id) } : null;
206
+ const tx = db.transaction(STORES.ragBranches, 'readonly');
207
+ return (await requestResult(tx.objectStore(STORES.ragBranches).get(String(id)))) || null;
208
+ }
209
+
210
+ async function updateBranch(id, updates = {}) {
211
+ const current = await getBranchById(id);
212
+ if (!current) throw new NotFoundError(`No existe la rama ${id}.`);
213
+ const next = validateBranch({ ...current, ...updates, id: current.id, createdAt: current.createdAt, updatedAt: Date.now() });
214
+ const db = await openDatabase();
215
+ if (!db) { memory.branches.set(next.id, next); return { ...next }; }
216
+ const tx = db.transaction(STORES.ragBranches, 'readwrite');
217
+ tx.objectStore(STORES.ragBranches).put(next);
218
+ await transactionDone(tx);
219
+ return next;
220
+ }
221
+
222
+ async function deleteByIndex(store, index, value) {
223
+ const request = store.index(index).openKeyCursor(value);
224
+ return new Promise((resolve, reject) => {
225
+ request.onsuccess = (event) => {
226
+ const cursor = event.target.result;
227
+ if (!cursor) { resolve(); return; }
228
+ store.delete(cursor.primaryKey);
229
+ cursor.continue();
230
+ };
231
+ request.onerror = () => reject(request.error);
232
+ });
233
+ }
234
+
235
+ async function deleteBranch(id) {
236
+ const branch = await getBranchById(id);
237
+ if (!branch) throw new NotFoundError(`No existe la rama ${id}.`);
238
+ const docs = await getDocumentsByBranch(id);
239
+ const db = await openDatabase();
240
+ if (!db) {
241
+ memory.branches.delete(id);
242
+ for (const doc of docs) await deleteDocument(doc.id);
243
+ return { success: true, deletedBranchId: id, deletedDocumentsCount: docs.length };
244
+ }
245
+ const tx = db.transaction([STORES.ragBranches, STORES.ragDocuments, STORES.ragImages, STORES.ragChunks], 'readwrite');
246
+ tx.objectStore(STORES.ragBranches).delete(id);
247
+ await Promise.all([
248
+ deleteByIndex(tx.objectStore(STORES.ragDocuments), 'by_branchId', id),
249
+ deleteByIndex(tx.objectStore(STORES.ragImages), 'by_branchId', id),
250
+ deleteByIndex(tx.objectStore(STORES.ragChunks), 'by_branchId', id)
251
+ ]);
252
+ await transactionDone(tx);
253
+ return { success: true, deletedBranchId: id, deletedDocumentsCount: docs.length };
254
+ }
255
+
256
+ async function saveDocument(data, images = []) {
257
+ const docImages = Array.isArray(images) ? images : [];
258
+ const { document, chunks } = validateDocument({ ...data, imageCount: docImages.length });
259
+ if (!(await getBranchById(document.branchId))) throw new NotFoundError(`No existe la rama ${document.branchId}.`);
260
+ const db = await openDatabase();
261
+ if (!db) {
262
+ memory.documents.set(document.id, document);
263
+ if (docImages.length > 0) {
264
+ memory.images.set(document.id, { documentId: document.id, branchId: document.branchId, images: docImages });
265
+ }
266
+ chunks.forEach(chunk => memory.chunks.set(chunk.id, chunk));
267
+ return { ...document };
268
+ }
269
+ try {
270
+ const tx = db.transaction([STORES.ragDocuments, STORES.ragImages, STORES.ragChunks], 'readwrite');
271
+ tx.objectStore(STORES.ragDocuments).add(document);
272
+ if (docImages.length > 0) {
273
+ tx.objectStore(STORES.ragImages).put({
274
+ documentId: document.id,
275
+ branchId: document.branchId,
276
+ images: docImages
277
+ });
278
+ }
279
+ for (const chunk of chunks) tx.objectStore(STORES.ragChunks).add(chunk);
280
+ await transactionDone(tx);
281
+ await updateBranch(document.branchId, { updatedAt: Date.now() });
282
+ return document;
283
+ } catch (error) {
284
+ if (isQuotaError(error)) throw new QuotaExceededError(undefined, { error });
285
+ throw new RagStorageError(`No se pudo guardar el documento: ${error.message || error}`, { error });
286
+ }
287
+ }
288
+
289
+ async function replaceDocument(oldDocId, data, images = []) {
290
+ const existing = await getDocumentById(oldDocId);
291
+ if (!existing) throw new NotFoundError(`No existe el documento ${oldDocId}.`);
292
+ const docImages = Array.isArray(images) ? images : [];
293
+ const { document, chunks } = validateDocument({ ...data, branchId: existing.branchId, imageCount: docImages.length });
294
+ const db = await openDatabase();
295
+ if (!db) {
296
+ await deleteDocument(oldDocId);
297
+ memory.documents.set(document.id, document);
298
+ if (docImages.length > 0) {
299
+ memory.images.set(document.id, { documentId: document.id, branchId: document.branchId, images: docImages });
300
+ }
301
+ chunks.forEach(chunk => memory.chunks.set(chunk.id, chunk));
302
+ return { ...document };
303
+ }
304
+ try {
305
+ const tx = db.transaction([STORES.ragDocuments, STORES.ragImages, STORES.ragChunks], 'readwrite');
306
+ tx.objectStore(STORES.ragDocuments).delete(oldDocId);
307
+ tx.objectStore(STORES.ragImages).delete(oldDocId);
308
+ await deleteByIndex(tx.objectStore(STORES.ragChunks), 'by_documentId', oldDocId);
309
+
310
+ tx.objectStore(STORES.ragDocuments).add(document);
311
+ if (docImages.length > 0) {
312
+ tx.objectStore(STORES.ragImages).put({
313
+ documentId: document.id,
314
+ branchId: document.branchId,
315
+ images: docImages
316
+ });
317
+ }
318
+ for (const chunk of chunks) tx.objectStore(STORES.ragChunks).add(chunk);
319
+ await transactionDone(tx);
320
+ await updateBranch(document.branchId, { updatedAt: Date.now() });
321
+ return document;
322
+ } catch (error) {
323
+ if (isQuotaError(error)) throw new QuotaExceededError(undefined, { error });
324
+ throw new RagStorageError(`No se pudo reemplazar el documento: ${error.message || error}`, { error });
325
+ }
326
+ }
327
+
328
+ async function findDocumentByPath(branchId, path) {
329
+ if (!branchId || !path) return null;
330
+ const cleanPath = String(path).trim();
331
+ const docs = await getDocumentsByBranch(branchId);
332
+ return docs.find(doc => (doc.path && doc.path === cleanPath) || doc.title === cleanPath) || null;
333
+ }
334
+
335
+ async function getDocumentImages(documentId) {
336
+ if (!documentId) return [];
337
+ const db = await openDatabase();
338
+ if (!db) {
339
+ const rec = memory.images.get(String(documentId));
340
+ return rec?.images || [];
341
+ }
342
+ const tx = db.transaction(STORES.ragImages, 'readonly');
343
+ const rec = await requestResult(tx.objectStore(STORES.ragImages).get(String(documentId)));
344
+ return rec?.images || [];
345
+ }
346
+
347
+ async function getDocumentImage(documentId, imageId) {
348
+ const images = await getDocumentImages(documentId);
349
+ if (!images || images.length === 0) return null;
350
+ const cleanId = String(imageId || '').trim();
351
+ return images.find(img => img.id === cleanId || `${documentId}:${img.id}` === cleanId) || null;
352
+ }
353
+
354
+ async function getDocumentsByBranch(branchId) {
355
+ if (!branchId) return [];
356
+ const fromDb = await getByIndex(STORES.ragDocuments, 'by_branchId', branchId);
357
+ const values = fromDb || Array.from(memory.documents.values()).filter(doc => doc.branchId === branchId).map(v => ({ ...v }));
358
+ return values.sort((a, b) => b.createdAt - a.createdAt);
359
+ }
360
+
361
+ async function getDocumentById(id) {
362
+ if (!id) return null;
363
+ const db = await openDatabase();
364
+ if (!db) return memory.documents.has(id) ? { ...memory.documents.get(id) } : null;
365
+ const tx = db.transaction(STORES.ragDocuments, 'readonly');
366
+ return (await requestResult(tx.objectStore(STORES.ragDocuments).get(String(id)))) || null;
367
+ }
368
+
369
+ async function getChunksByBranch(branchId) {
370
+ if (!branchId) return [];
371
+ const fromDb = await getByIndex(STORES.ragChunks, 'by_branchId', branchId);
372
+ const values = fromDb || Array.from(memory.chunks.values()).filter(chunk => chunk.branchId === branchId).map(v => ({ ...v }));
373
+ return values.sort((a, b) => a.createdAt - b.createdAt || a.order - b.order);
374
+ }
375
+
376
+ async function getChunksByDocument(documentId) {
377
+ if (!documentId) return [];
378
+ const fromDb = await getByIndex(STORES.ragChunks, 'by_documentId', documentId);
379
+ const values = fromDb || Array.from(memory.chunks.values()).filter(chunk => chunk.documentId === documentId).map(v => ({ ...v }));
380
+ return values.sort((a, b) => a.order - b.order);
381
+ }
382
+
383
+ function resolveChunkIdAlias(id) {
384
+ if (!id || typeof id !== 'string') return null;
385
+ const m = id.match(/^([a-zA-Z0-9_-]+)[#:/](?:chunk:?)?(\d+)$/i);
386
+ if (m) {
387
+ return { docId: m[1], order: parseInt(m[2], 10), canonicalId: `${m[1]}:chunk:${m[2]}` };
388
+ }
389
+ return null;
390
+ }
391
+
392
+ async function getChunkById(id) {
393
+ if (!id) return null;
394
+ const strId = String(id).trim();
395
+ const db = await openDatabase();
396
+ if (!db) {
397
+ if (memory.chunks.has(strId)) return { ...memory.chunks.get(strId) };
398
+ const alias = resolveChunkIdAlias(strId);
399
+ if (alias && memory.chunks.has(alias.canonicalId)) return { ...memory.chunks.get(alias.canonicalId) };
400
+ if (alias) {
401
+ for (const chunk of memory.chunks.values()) {
402
+ if (chunk.documentId === alias.docId && chunk.order === alias.order) return { ...chunk };
403
+ }
404
+ }
405
+ for (const chunk of memory.chunks.values()) {
406
+ if (chunk.documentId === strId && chunk.order === 0) return { ...chunk };
407
+ }
408
+ return null;
409
+ }
410
+
411
+ const tx = db.transaction(STORES.ragChunks, 'readonly');
412
+ const store = tx.objectStore(STORES.ragChunks);
413
+ let record = (await requestResult(store.get(strId))) || null;
414
+ if (record) return record;
415
+
416
+ const alias = resolveChunkIdAlias(strId);
417
+ if (alias) {
418
+ record = (await requestResult(store.get(alias.canonicalId))) || null;
419
+ if (record) return record;
420
+
421
+ const docChunks = await getChunksByDocument(alias.docId);
422
+ if (docChunks && docChunks.length > 0) {
423
+ const found = docChunks.find(c => c.order === alias.order) || docChunks[alias.order];
424
+ if (found) return found;
425
+ }
426
+ } else {
427
+ const docChunks = await getChunksByDocument(strId);
428
+ if (docChunks && docChunks.length > 0) {
429
+ return docChunks[0];
430
+ }
431
+ }
432
+
433
+ return null;
434
+ }
435
+
436
+ async function deleteDocument(id) {
437
+ const document = await getDocumentById(id);
438
+ if (!document) return false;
439
+ const db = await openDatabase();
440
+ if (!db) {
441
+ memory.documents.delete(id); memory.images.delete(id);
442
+ for (const [chunkId, chunk] of memory.chunks) if (chunk.documentId === id) memory.chunks.delete(chunkId);
443
+ return true;
444
+ }
445
+ const tx = db.transaction([STORES.ragDocuments, STORES.ragImages, STORES.ragChunks], 'readwrite');
446
+ tx.objectStore(STORES.ragDocuments).delete(id);
447
+ tx.objectStore(STORES.ragImages).delete(id);
448
+ await deleteByIndex(tx.objectStore(STORES.ragChunks), 'by_documentId', id);
449
+ await transactionDone(tx);
450
+ return true;
451
+ }
452
+
453
+ async function getStorageEstimate() {
454
+ let usage = 0, quota = 0, persisted = false;
455
+ try {
456
+ if (typeof navigator !== 'undefined' && navigator.storage) {
457
+ if (typeof navigator.storage.estimate === 'function') ({ usage = 0, quota = 0 } = await navigator.storage.estimate());
458
+ if (typeof navigator.storage.persisted === 'function') persisted = await navigator.storage.persisted();
459
+ }
460
+ } catch (_) {}
461
+ const percentUsed = quota > 0 ? (usage / quota) * 100 : 0;
462
+ return { usage, quota, percentUsed, usagePercent: percentUsed.toFixed(2), isPersisted: persisted };
463
+ }
464
+
465
+ async function clearAllData() {
466
+ const db = await openDatabase();
467
+ if (!db) {
468
+ Object.values(memory).forEach(store => store.clear());
469
+ return true;
470
+ }
471
+ const names = [STORES.ragBranches, STORES.ragDocuments, STORES.ragImages, STORES.ragChunks, STORES.ragMeta];
472
+ const tx = db.transaction(names, 'readwrite');
473
+ names.forEach(name => tx.objectStore(name).clear());
474
+ await transactionDone(tx);
475
+ return true;
476
+ }
477
+
478
+ async function exportBranch(branchId, options = {}) {
479
+ const branch = await getBranchById(branchId);
480
+ if (!branch) throw new NotFoundError(`No existe la rama ${branchId}.`);
481
+ const documents = await getDocumentsByBranch(branchId);
482
+ const onProgress = typeof options.onProgress === 'function' ? options.onProgress : null;
483
+ const total = documents.length;
484
+ let current = 0;
485
+ const exportedDocuments = [];
486
+ for (const document of documents) {
487
+ const chunks = await getChunksByDocument(document.id);
488
+ const images = await getDocumentImages(document.id);
489
+ exportedDocuments.push({
490
+ id: document.id,
491
+ title: document.title,
492
+ fileType: document.fileType,
493
+ mimeType: document.mimeType,
494
+ fileSize: document.fileSize,
495
+ imageCount: document.imageCount || (images ? images.length : 0),
496
+ images: (images && images.length > 0) ? images : undefined,
497
+ chunks: chunks.map(chunk => ({
498
+ order: chunk.order,
499
+ title: chunk.title,
500
+ content: chunk.content,
501
+ pageStart: chunk.pageStart,
502
+ pageEnd: chunk.pageEnd
503
+ }))
504
+ });
505
+ current++;
506
+ if (onProgress) {
507
+ onProgress({ current, total, percent: Math.round((current / total) * 100), docTitle: document.title });
508
+ if (current % 10 === 0) {
509
+ await new Promise(resolve => setTimeout(resolve, 0));
510
+ }
511
+ }
512
+ }
513
+ return {
514
+ schema: 'zerochat-knowledge',
515
+ version: 2,
516
+ exportedAt: new Date().toISOString(),
517
+ branch: { name: branch.name, description: branch.description, language: branch.language },
518
+ documents: exportedDocuments
519
+ };
520
+ }
521
+
522
+ async function exportBranchBlob(branchId, options = {}) {
523
+ const branch = await getBranchById(branchId);
524
+ if (!branch) throw new NotFoundError(`No existe la rama ${branchId}.`);
525
+ const documents = await getDocumentsByBranch(branchId);
526
+ const safeName = String(branch.name || 'conocimiento').normalize('NFKD')
527
+ .replace(/[\u0300-\u036f]/g, '').replace(/[^a-zA-Z0-9_-]+/g, '-').replace(/^-+|-+$/g, '') || 'conocimiento';
528
+
529
+ const compress = options.compress !== false && typeof CompressionStream !== 'undefined';
530
+ const onProgress = typeof options.onProgress === 'function' ? options.onProgress : null;
531
+ const total = documents.length;
532
+ let current = 0;
533
+
534
+ if (typeof TransformStream !== 'undefined' && compress) {
535
+ const ts = new TransformStream();
536
+ const writer = ts.writable.getWriter();
537
+ const cs = new CompressionStream('gzip');
538
+ const compressedStream = ts.readable.pipeThrough(cs);
539
+ const responsePromise = new Response(compressedStream).blob();
540
+ const encoder = new TextEncoder();
541
+
542
+ const headerObj = {
543
+ schema: 'zerochat-knowledge',
544
+ version: 2,
545
+ exportedAt: new Date().toISOString(),
546
+ branch: { name: branch.name, description: branch.description, language: branch.language }
547
+ };
548
+ const headerStr = JSON.stringify(headerObj);
549
+ await writer.write(encoder.encode(headerStr.slice(0, -1) + ',"documents":[\n'));
550
+
551
+ let first = true;
552
+ for (const document of documents) {
553
+ const chunks = await getChunksByDocument(document.id);
554
+ const images = await getDocumentImages(document.id);
555
+ const docRecord = {
556
+ id: document.id,
557
+ title: document.title,
558
+ fileType: document.fileType,
559
+ mimeType: document.mimeType,
560
+ fileSize: document.fileSize,
561
+ imageCount: document.imageCount || (images ? images.length : 0),
562
+ images: (images && images.length > 0) ? images : undefined,
563
+ chunks: chunks.map(chunk => ({
564
+ order: chunk.order,
565
+ title: chunk.title,
566
+ content: chunk.content,
567
+ pageStart: chunk.pageStart,
568
+ pageEnd: chunk.pageEnd
569
+ }))
570
+ };
571
+ const chunkJson = (first ? '' : ',\n') + JSON.stringify(docRecord);
572
+ first = false;
573
+ await writer.write(encoder.encode(chunkJson));
574
+
575
+ current++;
576
+ if (onProgress) {
577
+ onProgress({ current, total, percent: Math.round((current / total) * 100), docTitle: document.title });
578
+ if (current % 10 === 0) {
579
+ await new Promise(resolve => setTimeout(resolve, 0));
580
+ }
581
+ }
582
+ }
583
+
584
+ await writer.write(encoder.encode('\n]}'));
585
+ await writer.close();
586
+ const blob = await responsePromise;
587
+ return {
588
+ blob,
589
+ compressed: true,
590
+ filename: `${safeName}.zerochat-knowledge.json.gz`
591
+ };
592
+ }
593
+
594
+ const backup = await exportBranch(branchId, { onProgress });
595
+ const jsonStr = JSON.stringify(backup);
596
+ const plainBlob = new Blob([jsonStr], { type: 'application/json' });
597
+ return {
598
+ blob: plainBlob,
599
+ compressed: false,
600
+ filename: `${safeName}.zerochat-knowledge.json`
601
+ };
602
+ }
603
+
604
+ async function importBranch(backup, onProgress) {
605
+ let data = backup;
606
+ if (typeof data === 'string') {
607
+ try { data = JSON.parse(data); }
608
+ catch (error) { throw new ValidationError('El respaldo no contiene JSON válido.', { error }); }
609
+ }
610
+ if (!data || data.schema !== 'zerochat-knowledge' || data.version !== 2 || !data.branch || !Array.isArray(data.documents)) {
611
+ throw new ValidationError('Formato de respaldo de conocimiento no compatible.');
612
+ }
613
+ const branch = await createBranch(data.branch);
614
+ try {
615
+ const total = data.documents.length;
616
+ let current = 0;
617
+ for (const document of data.documents) {
618
+ const oldDocId = document.id;
619
+ let targetDocId = oldDocId;
620
+ if (!targetDocId || (await getDocumentById(targetDocId))) {
621
+ targetDocId = generateId('doc');
622
+ }
623
+
624
+ // Si cambia el ID del documento, actualizar las referencias a imágenes en los chunks
625
+ const sourceChunks = Array.isArray(document.chunks) ? document.chunks : [];
626
+ const remappedChunks = sourceChunks.map(chunk => {
627
+ if (oldDocId && oldDocId !== targetDocId && chunk.content && chunk.content.includes(`rag-image://${oldDocId}:`)) {
628
+ return {
629
+ ...chunk,
630
+ content: chunk.content.split(`rag-image://${oldDocId}:`).join(`rag-image://${targetDocId}:`)
631
+ };
632
+ }
633
+ return chunk;
634
+ });
635
+
636
+ await saveDocument({
637
+ id: targetDocId,
638
+ branchId: branch.id,
639
+ title: document.title,
640
+ fileType: document.fileType,
641
+ mimeType: document.mimeType,
642
+ fileSize: document.fileSize,
643
+ imageCount: document.imageCount || (document.images ? document.images.length : 0),
644
+ chunks: remappedChunks
645
+ }, document.images || []);
646
+ current++;
647
+ if (typeof onProgress === 'function') {
648
+ onProgress({ current, total, percent: Math.round((current / total) * 100), docTitle: document.title });
649
+ if (current % 10 === 0) {
650
+ await new Promise(resolve => setTimeout(resolve, 0));
651
+ }
652
+ }
653
+ }
654
+ return branch;
655
+ } catch (error) {
656
+ try {
657
+ await deleteBranch(branch.id);
658
+ } catch (rollbackError) {
659
+ console.warn('[RagStorage] Error durante el rollback de importación:', rollbackError);
660
+ }
661
+ throw error;
662
+ }
663
+ }
664
+
665
+ return {
666
+ createBranch, getBranches, getBranchById, updateBranch, deleteBranch,
667
+ saveDocument, replaceDocument, findDocumentByPath, getDocumentsByBranch, getDocumentById,
668
+ getChunksByBranch, getChunksByDocument, getChunkById, deleteDocument,
669
+ getDocumentImages, getDocumentImage,
670
+ getStorageEstimate, requestPersistentStorage, clearAllData, exportBranch, exportBranchBlob, importBranch, openDatabase,
671
+ RagStorageError, ValidationError, QuotaExceededError, NotFoundError,
672
+ DB_NAME: Database?.DB_NAME || 'ZeroChatDB', DB_VERSION: Database?.DB_VERSION || 2,
673
+ STORE_BRANCHES: STORES.ragBranches, STORE_DOCUMENTS: STORES.ragDocuments,
674
+ STORE_IMAGES: STORES.ragImages, STORE_CHUNKS: STORES.ragChunks
675
+ };
676
+ });