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,997 @@
1
+ /** Minimal UI for IndexedDB-backed local knowledge. */
2
+ (function (root, factory) {
3
+ if (typeof exports === 'object' && typeof module !== 'undefined') module.exports = factory();
4
+ else root.ChatRagUI = factory();
5
+ })(typeof self !== 'undefined' ? self : this, function () {
6
+ 'use strict';
7
+
8
+ let activeBranchIds = new Set();
9
+ let initialized = false;
10
+
11
+ function storage() {
12
+ if (typeof window !== 'undefined') return window.ChatRagStorage;
13
+ try { return require('./ragStorage.js'); } catch (_) { return null; }
14
+ }
15
+ function ingestion() {
16
+ if (typeof window !== 'undefined') return window.ChatIngestionEngine || window.IngestionEngine;
17
+ try { return require('./ingestionEngine.js'); } catch (_) { return null; }
18
+ }
19
+ function indexer() {
20
+ if (typeof window !== 'undefined') return window.ChatRagIndex;
21
+ try { return require('./rag-index.js'); } catch (_) { return null; }
22
+ }
23
+ function runtimeConfig() {
24
+ return typeof window !== 'undefined' ? window.ChatConfig : null;
25
+ }
26
+ function escapeHtml(value) {
27
+ const markdown = typeof window !== 'undefined' ? window.ChatMarkdown : null;
28
+ return markdown?.escapeHtml ? markdown.escapeHtml(String(value || '')) : String(value || '').replace(/[&<>"']/g, char => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }[char]));
29
+ }
30
+ function formatBytes(bytes) {
31
+ if (!bytes) return '0 B';
32
+ const units = ['B', 'KB', 'MB', 'GB'];
33
+ const exponent = Math.min(units.length - 1, Math.floor(Math.log(bytes) / Math.log(1024)));
34
+ return `${(bytes / Math.pow(1024, exponent)).toFixed(exponent ? 1 : 0)} ${units[exponent]}`;
35
+ }
36
+
37
+ function t(key, params) {
38
+ if (typeof window !== 'undefined' && window.ChatI18n?.t) {
39
+ return window.ChatI18n.t(key, params);
40
+ }
41
+ return '';
42
+ }
43
+
44
+ function getIcon(name, options) {
45
+ if (typeof window !== 'undefined' && window.ChatIcons?.get) {
46
+ return window.ChatIcons.get(name, options);
47
+ }
48
+ try {
49
+ const icons = require('./icons.js');
50
+ return icons?.get?.(name, options) || '';
51
+ } catch (_) {
52
+ return '';
53
+ }
54
+ }
55
+
56
+ async function getBranchMetrics(branches) {
57
+ const metrics = await Promise.all((branches || []).map(async branch => {
58
+ const documents = await storage().getDocumentsByBranch(branch.id);
59
+ return [branch.id, {
60
+ documentCount: documents.length,
61
+ totalBytes: documents.reduce((sum, document) => sum + (Number(document.fileSize) || 0), 0)
62
+ }];
63
+ }));
64
+ return new Map(metrics);
65
+ }
66
+
67
+ function formatBranchMetrics(metrics) {
68
+ const count = Number(metrics?.documentCount) || 0;
69
+ const bytes = formatBytes(metrics?.totalBytes || 0);
70
+ return t('rag_branch_summary_format', {
71
+ count,
72
+ plural: count === 1 ? '' : 's',
73
+ bytes
74
+ }) || `${count} documento${count === 1 ? '' : 's'} de ${bytes}`;
75
+ }
76
+
77
+ function formatBranchLanguage(lang) {
78
+ const clean = String(lang || 'spanish').toLowerCase().trim();
79
+ const map = {
80
+ spanish: t('rag_lang_spanish') || 'Español',
81
+ english: t('rag_lang_english') || 'Inglés',
82
+ french: t('rag_lang_french') || 'Francés',
83
+ german: t('rag_lang_german') || 'Alemán',
84
+ italian: t('rag_lang_italian') || 'Italiano',
85
+ portuguese: t('rag_lang_portuguese') || 'Portugués'
86
+ };
87
+ return map[clean] || (clean.charAt(0).toUpperCase() + clean.slice(1));
88
+ }
89
+
90
+ function formatDocumentMetrics(document) {
91
+ const hasImageCount = Number.isInteger(document?.imageCount) && document.imageCount >= 0;
92
+ let images;
93
+ if (hasImageCount) {
94
+ images = document.imageCount === 1
95
+ ? (t('rag_image_count_singular', { count: 1 }) || '1 imagen')
96
+ : (t('rag_image_count_plural', { count: document.imageCount }) || `${document.imageCount} imágenes`);
97
+ } else {
98
+ images = t('rag_image_count_reload') || 'imágenes: recarga necesaria';
99
+ }
100
+ const chunks = document?.chunkCount || 0;
101
+ const size = formatBytes(document?.fileSize);
102
+ return t('rag_doc_metrics_format', { chunks, size, images }) || `${chunks} fragmentos · ${size} · ${images}`;
103
+ }
104
+
105
+ function getActiveBranchIds() {
106
+ return Array.from(activeBranchIds);
107
+ }
108
+
109
+ function getActiveBranchId() {
110
+ return activeBranchIds.values().next().value || '';
111
+ }
112
+
113
+ function setActiveBranchIds(ids) {
114
+ const list = Array.isArray(ids) ? ids : (ids ? [ids] : []);
115
+ activeBranchIds = new Set(list.map(id => String(id || '').trim()).filter(Boolean));
116
+ const config = runtimeConfig();
117
+ if (config?.updateGeneral) {
118
+ config.updateGeneral({
119
+ activeRagBranchIds: Array.from(activeBranchIds),
120
+ activeRagBranchId: getActiveBranchId()
121
+ });
122
+ }
123
+ updateToolbarStatus();
124
+ return Array.from(activeBranchIds);
125
+ }
126
+
127
+ function setActiveBranchId(branchId) {
128
+ if (!branchId) return setActiveBranchIds([]);
129
+ return setActiveBranchIds([branchId]);
130
+ }
131
+
132
+ function toggleBranchActive(branchId) {
133
+ const cleanId = String(branchId || '').trim();
134
+ if (!cleanId) return getActiveBranchIds();
135
+ if (activeBranchIds.has(cleanId)) {
136
+ activeBranchIds.delete(cleanId);
137
+ } else {
138
+ activeBranchIds.add(cleanId);
139
+ }
140
+ return setActiveBranchIds(Array.from(activeBranchIds));
141
+ }
142
+
143
+ async function warnAboutMixedBranchLanguages(branchIds) {
144
+ const ids = Array.from(new Set((branchIds || []).map(id => String(id || '').trim()).filter(Boolean)));
145
+ if (ids.length < 2 || !storage()?.getBranches) return;
146
+
147
+ const branches = await storage().getBranches();
148
+ const languages = Array.from(new Set(branches
149
+ .filter(branch => ids.includes(branch.id))
150
+ .map(branch => String(branch.language || 'spanish').trim().toLowerCase())));
151
+ if (languages.length < 2) return;
152
+
153
+ const Dialogs = typeof window !== 'undefined' ? window.ChatDialogs : null;
154
+ if (!Dialogs?.alert) return;
155
+ await Dialogs.alert(t('rag_mixed_languages_warning', {
156
+ languages: languages.map(formatBranchLanguage).join(', ')
157
+ }), { type: 'info' });
158
+ }
159
+
160
+ async function toggleBranchActiveWithLanguageNotice(branchId) {
161
+ const activeIds = toggleBranchActive(branchId);
162
+ await warnAboutMixedBranchLanguages(activeIds);
163
+ return activeIds;
164
+ }
165
+
166
+ function isBranchActive(branchId) {
167
+ return activeBranchIds.has(String(branchId || '').trim());
168
+ }
169
+
170
+ async function updateToolbarStatus() {
171
+ if (typeof document === 'undefined') return;
172
+ const button = document.getElementById('btn-open-rag');
173
+ if (!button) return;
174
+ const count = activeBranchIds.size;
175
+ button.classList.toggle('active', count > 0);
176
+ button.title = count === 0
177
+ ? (t('btn_rag_title') || 'Gestionar conocimiento local')
178
+ : (count === 1
179
+ ? (t('rag_toolbar_active_single') || 'Conocimiento local activo (1 rama)')
180
+ : (t('rag_toolbar_active_multi', { count }) || `Conocimiento local activo (${count} ramas)`));
181
+ }
182
+
183
+ async function renderActiveTab() {
184
+ if (typeof document === 'undefined') return;
185
+ const branches = await storage().getBranches();
186
+ const branchMetrics = await getBranchMetrics(branches);
187
+ const list = document.getElementById('rag-active-branch-list');
188
+ const title = document.getElementById('rag-active-status-title');
189
+ const description = document.getElementById('rag-active-status-desc');
190
+ const toggle = document.getElementById('btn-rag-toggle-master');
191
+ const activeList = branches.filter(branch => activeBranchIds.has(branch.id));
192
+ const activeCount = activeList.length;
193
+
194
+ if (title) {
195
+ if (activeCount === 0) title.textContent = t('rag_status_disabled') || 'Conocimiento desactivado';
196
+ else if (activeCount === 1) title.textContent = activeList[0].name;
197
+ else title.textContent = t('rag_status_active_count', { count: activeCount, list: activeList.map(b => b.name).join(', ') }) || `${activeCount} ramas activas (${activeList.map(b => b.name).join(', ')})`;
198
+ }
199
+ if (description) {
200
+ if (activeCount === 0) description.textContent = t('rag_status_disabled_desc') || 'Selecciona una o varias ramas para que el agente pueda buscar en tus documentos.';
201
+ else if (activeCount === 1) description.textContent = t('rag_status_desc_single') || 'El agente puede buscar fragmentos de esta rama mediante Orama.';
202
+ else description.textContent = t('rag_status_desc_multi', { count: activeCount }) || `El agente consultará en paralelo las ${activeCount} ramas activas en cada búsqueda.`;
203
+ }
204
+ if (toggle) {
205
+ toggle.disabled = activeCount === 0;
206
+ toggle.textContent = t('rag_disable_all') || 'Desactivar todas';
207
+ }
208
+ if (!list) return;
209
+ if (!branches.length) {
210
+ list.innerHTML = `<div class="rag-empty-state">${t('rag_no_branches_active') || 'No hay ramas. Crea la primera desde RAG en Configuración.'}</div>`;
211
+ return;
212
+ }
213
+ list.innerHTML = branches.map(branch => {
214
+ const isActive = activeBranchIds.has(branch.id);
215
+ const metrics = branchMetrics.get(branch.id);
216
+ const formatted = formatBranchMetrics(metrics);
217
+ const loadedText = t('rag_branch_loaded', { summary: formatted }) || `Esta rama cargó ${formatted}`;
218
+ const descText = branch.description || t('rag_branch_no_desc') || 'Sin descripción';
219
+ const badgeIcon = isActive ? getIcon('check', { size: 12 }) : getIcon('plus', { size: 12 });
220
+ const badgeText = isActive ? (t('rag_branch_active_badge') || 'Activa') : (t('rag_branch_activate_badge') || 'Activar');
221
+ const langLabel = formatBranchLanguage(branch.language);
222
+ return `
223
+ <button type="button" class="setting-toggle-card rag-branch-select-card${isActive ? ' active' : ''}" data-branch-id="${escapeHtml(branch.id)}">
224
+ <span class="toggle-card-info"><strong>${escapeHtml(branch.name)}</strong><span class="toggle-card-desc">${escapeHtml(descText)}</span><span class="rag-branch-metrics">${escapeHtml(loadedText)} · <span class="rag-branch-lang-inline" style="display:inline-flex; align-items:center; gap:0.25rem;">${getIcon('globe', { size: 12 })} <span>${escapeHtml(langLabel)}</span></span></span></span>
225
+ <span class="rag-branch-badge-status" style="display:inline-flex; align-items:center; gap:0.25rem;">${badgeIcon} <span>${escapeHtml(badgeText)}</span></span>
226
+ </button>`;
227
+ }).join('');
228
+ list.querySelectorAll('[data-branch-id]').forEach(button => button.addEventListener('click', async () => {
229
+ await toggleBranchActiveWithLanguageNotice(button.dataset.branchId);
230
+ await renderActiveTab();
231
+ }));
232
+ }
233
+
234
+ function progressMarkup(event) {
235
+ const isError = event.status === 'error';
236
+ const isCancelled = event.status === 'cancelled';
237
+ const isSkipped = event.status === 'skipped';
238
+ const itemClass = isError ? 'error' : (isCancelled || isSkipped) ? 'skipped' : '';
239
+ return `<div class="rag-ingestion-progress-item ${itemClass}"><strong>${escapeHtml(event.fileName)}</strong><span>${escapeHtml(event.message)}</span><progress max="100" value="${Number(event.percent) || 0}"></progress></div>`;
240
+ }
241
+
242
+ function globalProgressMarkup(event, isRunning = true) {
243
+ const total = Number(event.totalFiles) || 0;
244
+ const finished = Number(event.finishedFiles) || 0;
245
+ const processed = Number(event.processedFiles) || 0;
246
+ const replaced = Number(event.replacedFiles) || 0;
247
+ const skipped = Number(event.skippedFiles) || 0;
248
+ const failed = Number(event.failedFiles) || 0;
249
+ const overallPercent = Math.round(Number(event.overallPercent) || 0);
250
+
251
+ const parts = [];
252
+ if (processed > 0) parts.push(`${processed} nuevos`);
253
+ if (replaced > 0) parts.push(`${replaced} reemplazados`);
254
+ if (skipped > 0) parts.push(`${skipped} omitidos`);
255
+ if (failed > 0) parts.push(`${failed} con error`);
256
+ const status = parts.length
257
+ ? parts.join(' · ')
258
+ : (t('rag_ingestion_status', { processed }) || `${processed} indexados`);
259
+
260
+ const header = t('rag_ingestion_global', { finished, total }) || `Carga global: ${finished} de ${total}`;
261
+ const stopIcon = '<svg class="ui-icon" width="11" height="11" viewBox="0 0 24 24" fill="currentColor"><rect x="4" y="4" width="16" height="16" rx="2"></rect></svg>';
262
+ const stopBtn = isRunning
263
+ ? `<button type="button" id="btn-rag-stop-ingestion" class="btn-secondary btn-danger-hover rag-stop-ingestion-btn" title="${escapeHtml(t('rag_btn_stop_ingestion') || 'Detener')}">${stopIcon} <span>${escapeHtml(t('rag_btn_stop_ingestion') || 'Detener')}</span></button>`
264
+ : '';
265
+
266
+ return `<div class="rag-ingestion-global-progress"><div><strong>${escapeHtml(header)}</strong><span>${escapeHtml(status)}</span></div><progress max="100" value="${overallPercent}"></progress><div style="display:inline-flex; align-items:center; gap:0.5rem;"><span>${overallPercent}%</span>${stopBtn}</div></div>`;
267
+ }
268
+
269
+ function ingestionResultMarkup(result) {
270
+ const processed = Number(result?.processed) || 0;
271
+ const replaced = Number(result?.replaced) || 0;
272
+ const skipped = Number(result?.skipped) || 0;
273
+ const cancelled = Number(result?.cancelled) || 0;
274
+ const failed = Number(result?.failed) || 0;
275
+ const total = Number(result?.total) || 0;
276
+ const totalIndexed = processed + replaced;
277
+
278
+ let summaryText = `${totalIndexed} indexados · ${failed} no indexados`;
279
+ if (replaced > 0 || skipped > 0 || cancelled > 0) {
280
+ const extra = [];
281
+ if (replaced > 0) extra.push(`${replaced} reemplazados`);
282
+ if (skipped > 0) extra.push(`${skipped} omitidos`);
283
+ if (cancelled > 0) extra.push(`${cancelled} cancelados`);
284
+ summaryText += ` (${extra.join(', ')})`;
285
+ }
286
+
287
+ const header = cancelled > 0
288
+ ? (t('rag_ingestion_cancelled') || 'Ingesta detenida por el usuario') + `: ${summaryText}`
289
+ : (t('rag_ingestion_complete', { processed: totalIndexed, failed, total }) || `Ingesta completada: ${summaryText}`);
290
+
291
+ const errors = Array.isArray(result?.errors) ? result.errors : [];
292
+ return `<div class="rag-ingestion-global-progress${failed ? ' error' : ''}"><div><strong>${escapeHtml(header)}</strong></div>${errors.length ? `<div class="rag-ingestion-progress-recent">${errors.map(error => `<div class="rag-ingestion-progress-item error"><strong>${escapeHtml(error.fileName)}</strong><span>${escapeHtml(error.error)}</span></div>`).join('')}</div>` : ''}</div>`;
293
+ }
294
+
295
+ async function renderWorkspace(branchId, ingestionResult) {
296
+ if (typeof document === 'undefined') return;
297
+ await updateBranchFields(branchId);
298
+ const workspace = document.getElementById('rag-manage-workspace');
299
+ if (!workspace) return;
300
+ if (!branchId) {
301
+ workspace.innerHTML = `<div class="rag-empty-state">${t('rag_workspace_empty') || 'Escribe un nombre arriba y pulsa "Crear rama" para empezar.'}</div>`;
302
+ return;
303
+ }
304
+ const documents = await storage().getDocumentsByBranch(branchId);
305
+ const branch = await storage().getBranchById(branchId);
306
+ const dropzoneTitle = t('rag_dropzone_title') || 'Arrastra o selecciona archivos';
307
+ const dropzoneHint = t('rag_dropzone_hint') || 'PDF o archivos de texto · guardado privado en IndexedDB';
308
+ const deleteDocTitle = t('rag_delete_doc_title') || 'Eliminar documento';
309
+ const emptyDocsText = t('rag_branch_empty_docs') || 'La rama todavía no contiene documentos.';
310
+ const langLabel = formatBranchLanguage(branch?.language);
311
+
312
+ workspace.innerHTML = `
313
+ <div class="rag-workspace-header-bar" style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 0.5rem; font-size: 0.85rem; color: var(--text-muted);">
314
+ <span>${escapeHtml(t('rag_branch_label') || 'Rama:')} <strong>${escapeHtml(branch?.name || '')}</strong></span>
315
+ <span style="display:inline-flex; align-items:center; gap:0.35rem;">${getIcon('globe', { size: 14 })} ${escapeHtml(t('rag_branch_lang') || 'Idioma de la documentación:')} <strong>${escapeHtml(langLabel)}</strong></span>
316
+ </div>
317
+ <label class="rag-dropzone" id="rag-dropzone">
318
+ <strong>${escapeHtml(dropzoneTitle)}</strong>
319
+ <span>${escapeHtml(dropzoneHint)}</span>
320
+ <input id="rag-file-input" type="file" multiple hidden>
321
+ </label>
322
+ <div id="rag-ingestion-progress"></div>
323
+ <div class="rag-documents-list">${documents.length ? documents.map(document => `
324
+ <div class="rag-document-card" data-document-id="${escapeHtml(document.id)}">
325
+ <div><strong>${escapeHtml(document.title)}</strong><div class="toggle-card-desc">${formatDocumentMetrics(document)}</div></div>
326
+ <button type="button" class="btn-secondary btn-danger-hover" data-delete-document="${escapeHtml(document.id)}" title="${escapeHtml(deleteDocTitle)}"><svg class="ui-icon" width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="3 6 5 6 21 6"></polyline><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"></path></svg></button>
327
+ </div>`).join('') : `<div class="rag-empty-state">${escapeHtml(emptyDocsText)}</div>`}</div>`;
328
+
329
+ const input = document.getElementById('rag-file-input');
330
+ const dropzone = document.getElementById('rag-dropzone');
331
+ const progress = document.getElementById('rag-ingestion-progress');
332
+ if (progress && ingestionResult) progress.innerHTML = ingestionResultMarkup(ingestionResult);
333
+ const handleFiles = files => {
334
+ ingestFiles(Array.from(files || []), branchId);
335
+ if (input) input.value = '';
336
+ };
337
+ if (input) input.addEventListener('change', () => handleFiles(input.files));
338
+ if (dropzone) {
339
+ dropzone.addEventListener('dragover', event => { event.preventDefault(); dropzone.classList.add('drag-over'); });
340
+ dropzone.addEventListener('dragleave', () => dropzone.classList.remove('drag-over'));
341
+ dropzone.addEventListener('drop', event => { event.preventDefault(); dropzone.classList.remove('drag-over'); handleFiles(event.dataTransfer.files); });
342
+ }
343
+ workspace.querySelectorAll('[data-delete-document]').forEach(button => button.addEventListener('click', async () => {
344
+ const documentId = button.dataset.deleteDocument;
345
+ const confirmMsg = t('rag_delete_doc_confirm') || '¿Eliminar este documento y todos sus fragmentos?';
346
+ if (!await ChatDialogs.confirm(confirmMsg)) return;
347
+ const document = await storage().getDocumentById(documentId);
348
+ if (!document || document.branchId !== branchId || !(await storage().getBranchById(branchId))) return;
349
+ await storage().deleteDocument(documentId);
350
+ indexer()?.invalidateBranch(branchId);
351
+ await renderWorkspace(branchId);
352
+ await updateQuota();
353
+ }));
354
+ }
355
+
356
+ let currentIngestionController = null;
357
+
358
+ async function ingestFiles(files, branchId) {
359
+ if (!files.length) return;
360
+ if (currentIngestionController) return;
361
+
362
+ const maxBytes = 50 * 1024 * 1024;
363
+ const validFiles = [];
364
+ const oversizedFiles = [];
365
+ for (const f of files) {
366
+ if (f && typeof f.size === 'number' && f.size > maxBytes) {
367
+ oversizedFiles.push(f);
368
+ } else {
369
+ validFiles.push(f);
370
+ }
371
+ }
372
+ if (oversizedFiles.length > 0) {
373
+ const Dialogs = typeof window !== 'undefined' ? window.ChatDialogs : null;
374
+ if (Dialogs?.alert) {
375
+ const names = oversizedFiles.map(f => f.name || 'documento').join(', ');
376
+ await Dialogs.alert(t('err_file_too_large', { name: names, max: '50 MB' }), { type: 'error' });
377
+ }
378
+ }
379
+ if (!validFiles.length) return;
380
+
381
+ const container = document.getElementById('rag-ingestion-progress');
382
+ const events = new Map();
383
+ const abortController = new AbortController();
384
+ currentIngestionController = abortController;
385
+
386
+ const attachStopListener = () => {
387
+ const stopBtn = document.getElementById('btn-rag-stop-ingestion');
388
+ if (stopBtn && !stopBtn.dataset.bound) {
389
+ stopBtn.dataset.bound = 'true';
390
+ stopBtn.addEventListener('click', () => {
391
+ stopBtn.disabled = true;
392
+ stopBtn.textContent = t('rag_ingestion_stopping') || 'Deteniendo...';
393
+ abortController.abort();
394
+ });
395
+ }
396
+ };
397
+
398
+ const onDuplicateConflict = async ({ fullPath }) => {
399
+ const Dialogs = typeof window !== 'undefined' ? window.ChatDialogs : null;
400
+ if (!Dialogs?.askDuplicate && !Dialogs?.confirm) {
401
+ return { action: 'ignore', applyToAll: false };
402
+ }
403
+ const promptMsg = t('rag_duplicate_prompt', { name: fullPath }) ||
404
+ `Ya existe un documento con el mismo nombre y ruta en esta rama ("${fullPath}"). ¿Deseas reemplazarlo o ignorarlo?`;
405
+ const options = {
406
+ title: t('rag_duplicate_title') || 'Documento duplicado',
407
+ acceptText: t('rag_btn_replace') || 'Reemplazar',
408
+ cancelText: t('rag_btn_ignore') || 'Ignorar',
409
+ checkbox: t('rag_duplicate_apply_all') || 'Aplicar a todos los duplicados restantes de esta carga'
410
+ };
411
+ const res = Dialogs.askDuplicate
412
+ ? await Dialogs.askDuplicate(promptMsg, options)
413
+ : await Dialogs.confirm(promptMsg, options);
414
+ const accepted = (typeof res === 'object' && res !== null) ? res.accepted : Boolean(res);
415
+ const applyToAll = (typeof res === 'object' && res !== null) ? Boolean(res.applyToAll || res.checkboxChecked) : false;
416
+ return {
417
+ action: accepted ? 'replace' : 'ignore',
418
+ applyToAll
419
+ };
420
+ };
421
+
422
+ try {
423
+ const result = await ingestion().processDocumentQueue(validFiles, branchId, event => {
424
+ events.set(event.fileIndex, event);
425
+ if (container) {
426
+ const recentEvents = Array.from(events.values()).slice(-12).reverse();
427
+ container.innerHTML = `${globalProgressMarkup(event, true)}<div class="rag-ingestion-progress-recent">${recentEvents.map(progressMarkup).join('')}</div>`;
428
+ attachStopListener();
429
+ }
430
+ }, {
431
+ signal: abortController.signal,
432
+ onDuplicateConflict
433
+ });
434
+ await renderWorkspace(branchId, result);
435
+ await updateQuota();
436
+ } finally {
437
+ currentIngestionController = null;
438
+ }
439
+ }
440
+
441
+ async function renderManageTab(preferredBranchId) {
442
+ if (typeof document === 'undefined') return;
443
+ const branches = await storage().getBranches();
444
+ const branchMetrics = await getBranchMetrics(branches);
445
+ const select = document.getElementById('rag-manage-branch-select');
446
+ if (!select) return;
447
+ const selected = preferredBranchId || select.value || branches[0]?.id || '';
448
+ select.innerHTML = branches.map(branch => `<option value="${escapeHtml(branch.id)}"${branch.id === selected ? ' selected' : ''}>${escapeHtml(branch.name)} (${escapeHtml(formatBranchMetrics(branchMetrics.get(branch.id)))})</option>`).join('');
449
+ select.disabled = !branches.length;
450
+ await renderWorkspace(selected);
451
+ }
452
+
453
+ const SVG_PLUS = `<svg class="ui-icon" width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="12" y1="5" x2="12" y2="19"></line><line x1="5" y1="12" x2="19" y2="12"></line></svg>`;
454
+ const SVG_SAVE = `<svg class="ui-icon" width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M19 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11l5 5v11a2 2 0 0 1-2 2z"></path><polyline points="17 21 17 13 7 13 7 21"></polyline><polyline points="7 3 7 8 15 8"></polyline></svg>`;
455
+
456
+ let isCreatingBranch = false;
457
+ let loadedBranchName = '';
458
+ let loadedBranchDesc = '';
459
+ let loadedBranchLang = 'spanish';
460
+ let currentButtonMode = 'new';
461
+
462
+ function setNewBranchButtonMode(mode) {
463
+ currentButtonMode = mode;
464
+ const btn = document.getElementById('btn-rag-new-branch');
465
+ const icon = document.getElementById('rag-new-branch-icon');
466
+ const text = document.getElementById('rag-new-branch-text');
467
+ if (!btn || !text) return;
468
+
469
+ if (mode === 'save') {
470
+ if (icon) icon.innerHTML = SVG_SAVE;
471
+ text.textContent = t('rag_btn_save') || 'Guardar';
472
+ btn.setAttribute('title', t('rag_btn_save') || 'Guardar');
473
+ } else {
474
+ if (icon) icon.innerHTML = SVG_PLUS;
475
+ text.textContent = t('rag_new_branch') || 'Nueva rama';
476
+ btn.setAttribute('title', t('rag_new_branch') || 'Nueva rama');
477
+ }
478
+ }
479
+
480
+ function checkBranchInputsChanged() {
481
+ const nameInput = document.getElementById('rag-branch-name-input');
482
+ const descInput = document.getElementById('rag-branch-desc-input');
483
+ const langSelect = document.getElementById('rag-branch-lang-select');
484
+ const currentName = nameInput?.value?.trim() || '';
485
+ const currentDesc = descInput?.value?.trim() || '';
486
+ const currentLang = (langSelect?.value || 'spanish').trim().toLowerCase();
487
+
488
+ if (isCreatingBranch) {
489
+ setNewBranchButtonMode('save');
490
+ return;
491
+ }
492
+
493
+ const hasChanged = currentName !== loadedBranchName.trim() ||
494
+ currentDesc !== (loadedBranchDesc || '').trim() ||
495
+ currentLang !== (loadedBranchLang || 'spanish').trim().toLowerCase();
496
+ setNewBranchButtonMode(hasChanged ? 'save' : 'new');
497
+ }
498
+
499
+ function hasPendingBranchChanges() {
500
+ const name = document.getElementById('rag-branch-name-input')?.value?.trim() || '';
501
+ const description = document.getElementById('rag-branch-desc-input')?.value?.trim() || '';
502
+ const language = (document.getElementById('rag-branch-lang-select')?.value || 'spanish').trim().toLowerCase();
503
+ if (isCreatingBranch) return Boolean(name || description || language !== 'spanish');
504
+ return name !== loadedBranchName.trim() ||
505
+ description !== (loadedBranchDesc || '').trim() ||
506
+ language !== (loadedBranchLang || 'spanish').trim().toLowerCase();
507
+ }
508
+
509
+ async function closeManageDialog() {
510
+ const modal = document.getElementById('rag-manage-modal');
511
+ if (!modal) return false;
512
+ if (hasPendingBranchChanges()) {
513
+ const Dialogs = typeof window !== 'undefined' ? window.ChatDialogs : null;
514
+ if (!Dialogs?.confirm || !await Dialogs.confirm(t('confirm_rag_branch_unsaved_changes'))) return false;
515
+ }
516
+ modal.close();
517
+ return true;
518
+ }
519
+
520
+ async function updateBranchFields(branchId) {
521
+ if (typeof document === 'undefined') return;
522
+ const nameInput = document.getElementById('rag-branch-name-input');
523
+ const descInput = document.getElementById('rag-branch-desc-input');
524
+ const langSelect = document.getElementById('rag-branch-lang-select');
525
+ const feedback = document.getElementById('rag-branch-feedback');
526
+ if (feedback) feedback.style.display = 'none';
527
+
528
+ if (!branchId) {
529
+ isCreatingBranch = true;
530
+ loadedBranchName = '';
531
+ loadedBranchDesc = '';
532
+ loadedBranchLang = 'spanish';
533
+ if (nameInput) nameInput.value = '';
534
+ if (descInput) descInput.value = '';
535
+ if (langSelect) langSelect.value = 'spanish';
536
+ setNewBranchButtonMode('new');
537
+ return;
538
+ }
539
+
540
+ isCreatingBranch = false;
541
+ const branch = await storage().getBranchById(branchId);
542
+ loadedBranchName = branch?.name || '';
543
+ loadedBranchDesc = branch?.description || '';
544
+ loadedBranchLang = branch?.language || 'spanish';
545
+ if (nameInput) nameInput.value = loadedBranchName;
546
+ if (descInput) descInput.value = loadedBranchDesc;
547
+ if (langSelect) langSelect.value = loadedBranchLang;
548
+ setNewBranchButtonMode('new');
549
+ }
550
+
551
+ async function handleNewBranchButtonClick() {
552
+ if (currentButtonMode === 'save') {
553
+ await saveOrUpdateBranch();
554
+ } else {
555
+ prepareNewBranch();
556
+ }
557
+ }
558
+
559
+ function prepareNewBranch() {
560
+ isCreatingBranch = true;
561
+ loadedBranchName = '';
562
+ loadedBranchDesc = '';
563
+ loadedBranchLang = 'spanish';
564
+ const nameInput = document.getElementById('rag-branch-name-input');
565
+ const descInput = document.getElementById('rag-branch-desc-input');
566
+ const langSelect = document.getElementById('rag-branch-lang-select');
567
+ const feedback = document.getElementById('rag-branch-feedback');
568
+ if (feedback) feedback.style.display = 'none';
569
+
570
+ if (nameInput) {
571
+ nameInput.value = '';
572
+ nameInput.focus();
573
+ }
574
+ if (descInput) descInput.value = '';
575
+ if (langSelect) langSelect.value = 'spanish';
576
+ setNewBranchButtonMode('save');
577
+ }
578
+
579
+ function showBranchFeedback(msg, type = 'success') {
580
+ const el = document.getElementById('rag-branch-feedback');
581
+ if (!el) return;
582
+ el.style.display = 'block';
583
+ el.className = `server-query-status status-${type}`;
584
+ el.textContent = msg;
585
+ setTimeout(() => {
586
+ if (el) el.style.display = 'none';
587
+ }, 4000);
588
+ }
589
+
590
+ async function saveOrUpdateBranch() {
591
+ const nameInput = document.getElementById('rag-branch-name-input');
592
+ const descInput = document.getElementById('rag-branch-desc-input');
593
+ const langSelect = document.getElementById('rag-branch-lang-select');
594
+ const name = nameInput?.value?.trim();
595
+ const description = descInput?.value?.trim() || '';
596
+ const language = (langSelect?.value || 'spanish').trim().toLowerCase();
597
+
598
+ if (!name) {
599
+ showBranchFeedback(t('rag_branch_name_empty') || 'Por favor, escribe un nombre para la rama.', 'error');
600
+ nameInput?.focus();
601
+ return;
602
+ }
603
+
604
+ if (isCreatingBranch) {
605
+ const branch = await storage().createBranch({ name, description, language });
606
+ isCreatingBranch = false;
607
+ await renderManageTab(branch.id);
608
+ await renderActiveTab();
609
+ setNewBranchButtonMode('new');
610
+ showBranchFeedback(t('rag_branch_created', { name }) || `Rama "${name}" creada con éxito.`, 'success');
611
+ } else {
612
+ const select = document.getElementById('rag-manage-branch-select');
613
+ const id = select?.value;
614
+ if (!id) {
615
+ const branch = await storage().createBranch({ name, description, language });
616
+ isCreatingBranch = false;
617
+ await renderManageTab(branch.id);
618
+ await renderActiveTab();
619
+ setNewBranchButtonMode('new');
620
+ showBranchFeedback(t('rag_branch_created', { name }) || `Rama "${name}" creada con éxito.`, 'success');
621
+ return;
622
+ }
623
+ await storage().updateBranch(id, { name, description, language });
624
+ indexer()?.invalidateBranch(id);
625
+ loadedBranchName = name;
626
+ loadedBranchDesc = description;
627
+ loadedBranchLang = language;
628
+ await renderManageTab(id);
629
+ await renderActiveTab();
630
+ setNewBranchButtonMode('new');
631
+ showBranchFeedback(t('rag_branch_updated', { name }) || `Rama "${name}" guardada con éxito.`, 'success');
632
+ }
633
+ }
634
+
635
+ function editBranch() {
636
+ const nameInput = document.getElementById('rag-branch-name-input');
637
+ if (nameInput) {
638
+ nameInput.focus();
639
+ nameInput.select();
640
+ }
641
+ }
642
+
643
+ async function deleteBranch() {
644
+ const select = document.getElementById('rag-manage-branch-select');
645
+ const id = select?.value;
646
+ const confirmMsg = t('rag_delete_branch_confirm') || '¿Eliminar la rama y todos sus documentos?';
647
+ if (!id || !await ChatDialogs.confirm(confirmMsg)) return;
648
+ if (!(await storage().getBranchById(id))) return;
649
+ await storage().deleteBranch(id);
650
+ indexer()?.invalidateBranch(id);
651
+ if (activeBranchIds.has(id)) {
652
+ activeBranchIds.delete(id);
653
+ setActiveBranchIds(Array.from(activeBranchIds));
654
+ }
655
+ await renderManageTab();
656
+ await renderActiveTab();
657
+ await updateQuota();
658
+ }
659
+
660
+ async function isGzipBlob(blob) {
661
+ try {
662
+ if (!blob || blob.size < 2) return false;
663
+ const slice = blob.slice(0, 2);
664
+ const buf = await slice.arrayBuffer();
665
+ const bytes = new Uint8Array(buf);
666
+ return bytes[0] === 0x1F && bytes[1] === 0x8B;
667
+ } catch (_) {
668
+ return false;
669
+ }
670
+ }
671
+
672
+ async function decompressFileIfNeeded(file) {
673
+ if (!file) return '';
674
+ const isGz = file.name?.toLowerCase().endsWith('.gz') || await isGzipBlob(file);
675
+ if (isGz && typeof DecompressionStream !== 'undefined') {
676
+ const stream = file.stream().pipeThrough(new DecompressionStream('gzip'));
677
+ return await new Response(stream).text();
678
+ }
679
+ return await file.text();
680
+ }
681
+
682
+ async function exportBranch() {
683
+ const select = document.getElementById('rag-manage-branch-select');
684
+ const branchId = select?.value;
685
+ if (!branchId) return;
686
+
687
+ const btnExport = document.getElementById('btn-rag-export-branch');
688
+ const prevHtml = btnExport?.innerHTML;
689
+ try {
690
+ if (btnExport) {
691
+ btnExport.disabled = true;
692
+ btnExport.textContent = 'Exportando 0%...';
693
+ }
694
+ const { blob, filename } = await storage().exportBranchBlob(branchId, {
695
+ compress: true,
696
+ onProgress: ({ current, total, percent }) => {
697
+ if (btnExport) btnExport.textContent = `Exportando ${percent}% (${current}/${total})...`;
698
+ }
699
+ });
700
+ const url = URL.createObjectURL(blob);
701
+ const anchor = document.createElement('a');
702
+ anchor.href = url;
703
+ anchor.download = filename;
704
+ anchor.click();
705
+ setTimeout(() => URL.revokeObjectURL(url), 10000);
706
+ if (btnExport) btnExport.textContent = '¡Exportado!';
707
+ await new Promise(resolve => setTimeout(resolve, 1500));
708
+ } catch (error) {
709
+ ChatDialogs.alert(t('notice_export_error', { err: error.message || error }), { type: 'error' });
710
+ } finally {
711
+ if (btnExport) {
712
+ btnExport.disabled = false;
713
+ if (prevHtml) btnExport.innerHTML = prevHtml;
714
+ }
715
+ }
716
+ }
717
+
718
+ async function importBranchFile(file) {
719
+ if (!file) return null;
720
+ const btnImport = document.getElementById('btn-rag-import-branch');
721
+ const prevHtml = btnImport?.innerHTML;
722
+ try {
723
+ if (btnImport) {
724
+ btnImport.disabled = true;
725
+ btnImport.textContent = 'Descomprimiendo...';
726
+ }
727
+ let text;
728
+ try {
729
+ text = await decompressFileIfNeeded(file);
730
+ } catch (err) {
731
+ if (err.name === 'RangeError' || err.code === 'ERR_STRING_TOO_LONG' || String(err).includes('string')) {
732
+ throw new Error('El archivo supera el límite de memoria del navegador (512 MB). Utiliza el respaldo ligero optimizado.');
733
+ }
734
+ throw err;
735
+ }
736
+ if (!text) throw new Error('El archivo de respaldo está vacío o no se pudo leer.');
737
+ if (btnImport) btnImport.textContent = 'Restaurando 0%...';
738
+
739
+ const branch = await storage().importBranch(text, ({ current, total, percent }) => {
740
+ if (btnImport) btnImport.textContent = `Restaurando ${percent}% (${current}/${total})...`;
741
+ });
742
+ indexer()?.invalidateBranch(branch.id);
743
+ await renderManageTab(branch.id);
744
+ await renderActiveTab();
745
+ await updateQuota();
746
+ ChatDialogs.alert(t('notice_branch_restored', { name: branch.name }), { type: 'success' });
747
+ return branch;
748
+ } finally {
749
+ if (btnImport) {
750
+ btnImport.disabled = false;
751
+ if (prevHtml) btnImport.innerHTML = prevHtml;
752
+ }
753
+ }
754
+ }
755
+
756
+ async function updateQuota() {
757
+ if (typeof document === 'undefined') return;
758
+ const node = document.getElementById('rag-storage-quota-info');
759
+ const estimate = await storage().getStorageEstimate();
760
+ const dbIcon = '<svg class="ui-icon" width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><ellipse cx="12" cy="5" rx="9" ry="3"></ellipse><path d="M21 12c0 1.66-4 3-9 3s-9-1.34-9-3"></path><path d="M3 5v14c0 1.66 4 3 9 3s9-1.34 9-3V5"></path></svg>';
761
+ const quotaText = estimate.quota
762
+ ? (t('rag_quota_indexeddb', { usage: formatBytes(estimate.usage), quota: formatBytes(estimate.quota) }) || `IndexedDB: ${formatBytes(estimate.usage)} de ${formatBytes(estimate.quota)}`)
763
+ : (t('rag_quota_indexeddb_local') || 'IndexedDB local');
764
+ if (node) node.innerHTML = `${dbIcon} <span>${escapeHtml(quotaText)}</span>`;
765
+ }
766
+
767
+ async function refresh() {
768
+ await renderActiveTab();
769
+ await renderManageTab();
770
+ await updateQuota();
771
+ await updateToolbarStatus();
772
+ }
773
+
774
+ function openRagModal(mode = 'activate') {
775
+ if (typeof document === 'undefined') return;
776
+ const isManage = mode === 'manage';
777
+ const modal = document.getElementById(isManage ? 'rag-manage-modal' : 'rag-modal');
778
+ if (!modal) return;
779
+
780
+ (isManage ? renderManageTab() : renderActiveTab())
781
+ .then(() => updateQuota())
782
+ .catch(() => {});
783
+ modal.showModal();
784
+ }
785
+
786
+ function bindActivationDialog() {
787
+ const modal = document.getElementById('rag-modal');
788
+ if (!modal) return;
789
+
790
+ document.getElementById('btn-close-rag')?.addEventListener('click', () => modal?.close());
791
+ document.getElementById('btn-rag-toggle-master')?.addEventListener('click', async () => { setActiveBranchIds([]); await renderActiveTab(); });
792
+ document.getElementById('btn-rag-activate-all')?.addEventListener('click', async () => {
793
+ const branches = await storage().getBranches();
794
+ const activeIds = setActiveBranchIds(branches.map(b => b.id));
795
+ await warnAboutMixedBranchLanguages(activeIds);
796
+ await renderActiveTab();
797
+ });
798
+ }
799
+
800
+ function bindManageDialog() {
801
+ const modal = document.getElementById('rag-manage-modal');
802
+ if (!modal) return;
803
+
804
+ document.getElementById('btn-close-rag-manage')?.addEventListener('click', () => { closeManageDialog(); });
805
+ modal.addEventListener('cancel', event => {
806
+ if (!hasPendingBranchChanges()) return;
807
+ event.preventDefault();
808
+ closeManageDialog();
809
+ });
810
+ document.getElementById('btn-rag-new-branch')?.addEventListener('click', handleNewBranchButtonClick);
811
+ document.getElementById('btn-rag-edit-branch')?.addEventListener('click', editBranch);
812
+
813
+ document.getElementById('rag-branch-name-input')?.addEventListener('input', checkBranchInputsChanged);
814
+ document.getElementById('rag-branch-desc-input')?.addEventListener('input', checkBranchInputsChanged);
815
+ document.getElementById('rag-branch-lang-select')?.addEventListener('change', checkBranchInputsChanged);
816
+
817
+ const handleBranchKeyEnter = (e) => {
818
+ if (e.key === 'Enter') {
819
+ e.preventDefault();
820
+ saveOrUpdateBranch();
821
+ }
822
+ };
823
+ document.getElementById('rag-branch-name-input')?.addEventListener('keydown', handleBranchKeyEnter);
824
+ document.getElementById('rag-branch-desc-input')?.addEventListener('keydown', handleBranchKeyEnter);
825
+
826
+ document.getElementById('btn-rag-delete-branch')?.addEventListener('click', deleteBranch);
827
+ document.getElementById('btn-rag-export-branch')?.addEventListener('click', () => exportBranch().catch(error => ChatDialogs.alert(t('notice_export_error', { err: error.message || error }), { type: 'error' })));
828
+ const importInput = document.getElementById('rag-import-input');
829
+ document.getElementById('btn-rag-import-branch')?.addEventListener('click', () => importInput?.click());
830
+ importInput?.addEventListener('change', async () => {
831
+ try { await importBranchFile(importInput.files?.[0]); }
832
+ catch (error) { ChatDialogs.alert(t('notice_import_error', { err: error.message || error }), { type: 'error' }); }
833
+ finally { importInput.value = ''; }
834
+ });
835
+ document.getElementById('rag-manage-branch-select')?.addEventListener('change', event => renderWorkspace(event.target.value));
836
+ }
837
+
838
+ function initRagUI() {
839
+ if (initialized || typeof document === 'undefined') return;
840
+ initialized = true;
841
+ const cfg = runtimeConfig()?.getActive?.() || {};
842
+ if (Array.isArray(cfg.activeRagBranchIds) && cfg.activeRagBranchIds.length > 0) {
843
+ activeBranchIds = new Set(cfg.activeRagBranchIds.map(String).filter(Boolean));
844
+ } else if (cfg.activeRagBranchId) {
845
+ activeBranchIds = new Set([String(cfg.activeRagBranchId)]);
846
+ } else {
847
+ activeBranchIds = new Set();
848
+ }
849
+ ensureDialogMarkup();
850
+
851
+ document.getElementById('btn-open-rag')?.addEventListener('click', () => openRagModal('activate'));
852
+ bindActivationDialog();
853
+ bindManageDialog();
854
+
855
+ updateToolbarStatus();
856
+
857
+ if (typeof window !== 'undefined') {
858
+ if (window.ChatI18n?.onChange) {
859
+ window.ChatI18n.onChange(() => {
860
+ refresh().catch(() => {});
861
+ });
862
+ } else {
863
+ window.addEventListener('zerochat:languagechange', () => {
864
+ refresh().catch(() => {});
865
+ });
866
+ }
867
+ }
868
+ }
869
+
870
+ function getRagModalHTML(mode = 'activate') {
871
+ const isActivateMode = mode === 'activate';
872
+ const titleKey = isActivateMode ? 'rag_modal_title_activate' : 'rag_modal_title_manage';
873
+ const titleFallback = isActivateMode ? 'RAG' : 'RAG. Gestionar';
874
+
875
+ return `<div class="modal-header">
876
+ <div class="modal-title">
877
+ <h3 data-i18n="${titleKey}">${titleFallback}</h3>
878
+ </div>
879
+ <div class="${isActivateMode ? 'rag-activation-header-actions' : 'rag-manage-header-actions'}">
880
+ ${isActivateMode ? `<button type="button" id="btn-rag-activate-all" class="btn-secondary" data-i18n="rag_activate_all">Activar todas</button>
881
+ <button type="button" id="btn-rag-toggle-master" class="btn-secondary" data-i18n="rag_disable_all">Desactivar todas</button>` : `<button type="button" id="btn-rag-new-branch" class="btn-primary">
882
+ <span id="rag-new-branch-icon"><svg class="ui-icon" width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor"><use href="#icon-plus"></use></svg></span>
883
+ <span id="rag-new-branch-text" data-i18n="rag_new_branch">Nueva rama</span>
884
+ </button>
885
+ <button type="button" id="btn-rag-export-branch" class="btn-secondary" data-i18n-title="rag_export_branch">
886
+ <svg class="ui-icon" width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor"><use href="#icon-download"></use></svg>
887
+ <span data-i18n="rag_export_branch">Respaldo</span>
888
+ </button>
889
+ <button type="button" id="btn-rag-import-branch" class="btn-secondary" data-i18n-title="rag_import_branch">
890
+ <svg class="ui-icon" width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor"><use href="#icon-upload"></use></svg>
891
+ <span data-i18n="rag_import_branch">Restaurar</span>
892
+ </button>
893
+ <input id="rag-import-input" type="file" accept="application/json,.json,.gz,.json.gz,application/gzip" hidden>`}
894
+ <button type="button" id="${isActivateMode ? 'btn-close-rag' : 'btn-close-rag-manage'}" class="btn-close" data-i18n-aria="modal_close_aria" aria-label="Cerrar modal">
895
+ <svg class="ui-icon" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor"><use href="#icon-close"></use></svg>
896
+ </button>
897
+ </div>
898
+ </div>
899
+ <div class="modal-body rag-modal-body">
900
+ ${isActivateMode ? `<div class="rag-modal-content">
901
+ <div class="setting-toggle-card rag-master-toggle-card">
902
+ <div class="toggle-card-info">
903
+ <div class="toggle-card-title"><span id="rag-active-status-title" data-i18n="rag_status_disabled">Conocimiento desactivado</span></div>
904
+ <p class="toggle-card-desc" id="rag-active-status-desc" data-i18n="rag_status_disabled_desc">Selecciona una o varias ramas para que el agente pueda buscar en tus documentos.</p>
905
+ </div>
906
+ <div id="rag-storage-quota-info" class="rag-storage-quota-info rag-master-storage">
907
+ <svg class="ui-icon" width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor"><use href="#icon-database"></use></svg>
908
+ <span>IndexedDB</span>
909
+ </div>
910
+ </div>
911
+ <div class="form-field">
912
+ <label><strong data-i18n="rag_available_branches">Ramas disponibles</strong><span class="label-hint" data-i18n="rag_available_branches_hint">Puedes activar una o varias ramas simultáneamente para búsquedas cruzadas.</span></label>
913
+ <div id="rag-active-branch-list" class="rag-active-branch-list"></div>
914
+ </div>
915
+ <div class="rag-help-link-card">
916
+ <svg class="ui-icon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor"><use href="#icon-help-circle"></use></svg>
917
+ <span data-i18n="rag_help_link_text">¿Necesitas ayuda con RAG?</span>
918
+ <a href="help/rag.html" target="_blank" rel="noopener noreferrer" class="rag-help-external-link" data-i18n="rag_help_link_label">Ver guía completa</a>
919
+ </div>
920
+ <div class="rag-active-tip-card">
921
+ <span class="rag-active-tip-icon" style="color: var(--accent, #f59e0b); display: inline-flex; align-items: center;">${getIcon('lightbulb', { size: 18 })}</span>
922
+ <div class="rag-active-tip-content">
923
+ <strong data-i18n="rag_active_tip_title">Eficacia del RAG y modelo:</strong>
924
+ <span data-i18n-html="rag_active_tip_desc">La eficacia del RAG se basa en gran medida en la <strong>inteligencia, visión multimodal</strong> (para interpretar tablas, gráficos e imágenes) y la <strong>capacidad de razonamiento agéntico</strong> del modelo elegido: es clave para formular búsquedas precisas, examinar fragmentos contiguos y contrastar evidencias sin desorientarse. Si utilizas modelos compactos o con menor autonomía agéntica, activa el <strong>Punto de Control agéntico (agent_checkpoint)</strong> desde el menú de Razonamiento para consolidar hallazgos y mantener un plan de investigación claro.</span>
925
+ </div>
926
+ </div>
927
+ </div>` : `<div class="rag-modal-content">
928
+ <div class="rag-manage-toolbar">
929
+ <div class="form-field rag-manage-branch-field">
930
+ <label for="rag-manage-branch-select"><strong data-i18n="rag_branch_label">Rama:</strong></label>
931
+ <select id="rag-manage-branch-select" class="combobox-select-helper"></select>
932
+ </div>
933
+ <div class="rag-manage-toolbar-actions">
934
+ <button type="button" id="btn-rag-edit-branch" class="btn-secondary" data-i18n-title="rag_edit_branch">
935
+ <svg class="ui-icon" width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor"><use href="#icon-edit"></use></svg>
936
+ <span data-i18n="rag_edit_branch">Editar</span>
937
+ </button>
938
+ <button type="button" id="btn-rag-delete-branch" class="btn-secondary btn-danger-hover" data-i18n-title="rag_delete_branch">
939
+ <svg class="ui-icon" width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor"><use href="#icon-trash"></use></svg>
940
+ <span data-i18n="rag_delete_branch">Eliminar</span>
941
+ </button>
942
+ </div>
943
+ </div>
944
+ <div class="rag-branch-details-card" id="rag-branch-details-card">
945
+ <div class="rag-branch-fields-grid">
946
+ <div class="form-field" style="margin-bottom: 0;">
947
+ <label for="rag-branch-name-input"><strong data-i18n="rag_branch_name">Nombre de la rama:</strong></label>
948
+ <input type="text" id="rag-branch-name-input" data-i18n-placeholder="rag_branch_name_placeholder" placeholder="Nombre de la rama (ej: Manuales)" autocomplete="off">
949
+ </div>
950
+ <div class="form-field" style="margin-bottom: 0;">
951
+ <label for="rag-branch-lang-select"><strong data-i18n="rag_branch_lang">Idioma de la documentación:</strong></label>
952
+ <select id="rag-branch-lang-select" class="combobox-select-helper">
953
+ <option value="spanish" selected data-i18n="rag_lang_spanish">Español</option>
954
+ <option value="english" data-i18n="rag_lang_english">Inglés</option>
955
+ <option value="french" data-i18n="rag_lang_french">Francés</option>
956
+ <option value="german" data-i18n="rag_lang_german">Alemán</option>
957
+ <option value="italian" data-i18n="rag_lang_italian">Italiano</option>
958
+ <option value="portuguese" data-i18n="rag_lang_portuguese">Portugués</option>
959
+ </select>
960
+ </div>
961
+ <div class="form-field" style="margin-bottom: 0;">
962
+ <label for="rag-branch-desc-input"><strong data-i18n="rag_branch_desc">Descripción (opcional):</strong></label>
963
+ <input type="text" id="rag-branch-desc-input" data-i18n-placeholder="rag_branch_desc_placeholder" placeholder="Descripción sobre el contenido de esta rama" autocomplete="off">
964
+ </div>
965
+ </div>
966
+ <div id="rag-branch-feedback" class="server-query-status" style="display: none; margin-top: 0.5rem;"></div>
967
+ </div>
968
+ <div id="rag-manage-workspace" class="rag-manage-workspace"></div>
969
+ </div>`}
970
+ </div>
971
+ `;
972
+ }
973
+
974
+ function ensureDialogMarkup() {
975
+ if (typeof document === 'undefined') return;
976
+ const activationDialog = document.getElementById('rag-modal');
977
+ const manageDialog = document.getElementById('rag-manage-modal');
978
+ if (activationDialog && !activationDialog.firstElementChild) activationDialog.innerHTML = getRagModalHTML('activate');
979
+ if (manageDialog && !manageDialog.firstElementChild) manageDialog.innerHTML = getRagModalHTML('manage');
980
+ }
981
+
982
+ if (typeof document !== 'undefined') {
983
+ if (document.readyState === 'loading') {
984
+ document.addEventListener('DOMContentLoaded', ensureDialogMarkup);
985
+ } else {
986
+ ensureDialogMarkup();
987
+ }
988
+ }
989
+
990
+ return {
991
+ initRagUI, refresh, renderActiveTab, renderManageTab,
992
+ getActiveBranchId, setActiveBranchId,
993
+ getActiveBranchIds, setActiveBranchIds, toggleBranchActive, isBranchActive,
994
+ updateToolbarStatus, exportBranch, importBranchFile, ingestionResultMarkup,
995
+ ensureDialogMarkup, getRagModalHTML, openRagModal
996
+ };
997
+ });