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,220 @@
1
+ /**
2
+ * Módulo de Gestión de Adjuntos (ChatAttachments) para ZeroChat.
3
+ * Gestiona la lista de archivos adjuntos (texto, código, imágenes, PDF),
4
+ * su renderizado visual en chips y el ensamblado del prompt enriquecido con adjuntos.
5
+ */
6
+ (function (root, factory) {
7
+ if (typeof exports === 'object' && typeof module !== 'undefined') {
8
+ module.exports = factory();
9
+ } else {
10
+ root.ChatAttachments = factory();
11
+ }
12
+ }(typeof self !== 'undefined' ? self : this, function () {
13
+ 'use strict';
14
+
15
+ function getState() {
16
+ if (typeof window !== 'undefined' && window.ChatState) return window.ChatState;
17
+ if (typeof global !== 'undefined' && global.ChatState) return global.ChatState;
18
+ try {
19
+ return require('./state.js');
20
+ } catch (_) {
21
+ return null;
22
+ }
23
+ }
24
+
25
+ function getFileParser() {
26
+ return (typeof window !== 'undefined' && window.ChatFileParser) ? window.ChatFileParser : {
27
+ formatBytes: (bytes) => `${bytes} B`
28
+ };
29
+ }
30
+
31
+ function getUtils() {
32
+ if (typeof window !== 'undefined' && window.ChatUtils) return window.ChatUtils;
33
+ if (typeof global !== 'undefined' && global.ChatUtils) return global.ChatUtils;
34
+ try {
35
+ return require('./utils.js');
36
+ } catch (_) {
37
+ return null;
38
+ }
39
+ }
40
+
41
+ function getFiles() {
42
+ const State = getState();
43
+ if (State && typeof State.get === 'function') {
44
+ const ui = State.get('ui');
45
+ return Array.isArray(ui?.attachedFiles) ? ui.attachedFiles : [];
46
+ }
47
+ return [];
48
+ }
49
+
50
+ function setFiles(files) {
51
+ const State = getState();
52
+ const cleanFiles = Array.isArray(files) ? [...files] : [];
53
+ if (State && typeof State.setAttachments === 'function') {
54
+ State.setAttachments(cleanFiles);
55
+ } else if (State && typeof State.set === 'function') {
56
+ State.set('ui', { attachedFiles: cleanFiles });
57
+ }
58
+ }
59
+
60
+ function clearFiles() {
61
+ const State = getState();
62
+ if (State && typeof State.clearAttachments === 'function') {
63
+ State.clearAttachments();
64
+ } else {
65
+ setFiles([]);
66
+ }
67
+ }
68
+
69
+ function removeFileAt(index) {
70
+ const files = getFiles();
71
+ if (index >= 0 && index < files.length) {
72
+ files.splice(index, 1);
73
+ setFiles(files);
74
+ }
75
+ }
76
+
77
+ function addFile(fileObj) {
78
+ if (fileObj && fileObj.name) {
79
+ const files = getFiles();
80
+ files.push(fileObj);
81
+ setFiles(files);
82
+ }
83
+ }
84
+
85
+ function renderChips(container, onRemoveCallback) {
86
+ if (!container) return;
87
+
88
+ const files = getFiles();
89
+ if (files.length === 0) {
90
+ container.innerHTML = '';
91
+ container.style.display = 'none';
92
+ return;
93
+ }
94
+
95
+ container.style.display = 'flex';
96
+ container.innerHTML = '';
97
+
98
+ const FileParser = getFileParser();
99
+
100
+ files.forEach((file, index) => {
101
+ const chip = document.createElement('div');
102
+ chip.className = 'file-chip';
103
+
104
+ let iconName = 'file-text';
105
+ if (file.type === 'pdf') iconName = 'file-text';
106
+ else if (file.type === 'image') iconName = 'image';
107
+
108
+ const Icons = typeof window !== 'undefined' ? window.ChatIcons : null;
109
+ const I18n = typeof window !== 'undefined' ? window.ChatI18n : null;
110
+ const iconSvg = Icons ? Icons.get(iconName, { size: 14 }) : '';
111
+ const closeSvg = Icons ? Icons.get('close', { size: 12 }) : '<svg class="ui-icon" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor"><use href="#icon-close"></use></svg>';
112
+ const removeTitle = (I18n && typeof I18n.t === 'function') ? I18n.t('btn_delete') : 'Eliminar';
113
+ const icon = document.createElement('span');
114
+ icon.className = 'file-chip-icon';
115
+ // Los SVG proceden exclusivamente de ChatIcons o de esta plantilla estática.
116
+ const Utils = getUtils();
117
+ if (Utils?.setTrustedHtml) Utils.setTrustedHtml(icon, iconSvg);
118
+ else icon.innerHTML = iconSvg;
119
+
120
+ const name = document.createElement('span');
121
+ name.className = 'file-chip-name';
122
+ name.textContent = String(file.name || '');
123
+ name.title = String(file.name || '');
124
+
125
+ const size = document.createElement('span');
126
+ size.className = 'file-chip-size';
127
+ size.textContent = `(${FileParser.formatBytes(file.size)})`;
128
+
129
+ const removeButton = document.createElement('button');
130
+ removeButton.type = 'button';
131
+ removeButton.className = 'btn-remove-chip file-chip-remove';
132
+ removeButton.setAttribute('data-index', String(index));
133
+ removeButton.title = String(removeTitle);
134
+ removeButton.setAttribute('aria-label', String(removeTitle));
135
+ if (Utils?.setTrustedHtml) Utils.setTrustedHtml(removeButton, closeSvg);
136
+ else removeButton.innerHTML = closeSvg;
137
+
138
+ removeButton.addEventListener('click', () => {
139
+ removeFileAt(index);
140
+ renderChips(container, onRemoveCallback);
141
+ if (typeof onRemoveCallback === 'function') {
142
+ onRemoveCallback(index);
143
+ }
144
+ });
145
+
146
+ chip.appendChild(icon);
147
+ chip.appendChild(name);
148
+ chip.appendChild(size);
149
+ chip.appendChild(removeButton);
150
+ container.appendChild(chip);
151
+ });
152
+ }
153
+
154
+ /**
155
+ * Construye el prompt completo y el texto visual a partir del texto del usuario y los adjuntos.
156
+ */
157
+ function buildAttachmentsPayload(rawText = '', files = null) {
158
+ const effectiveFiles = Array.isArray(files) ? files : getFiles();
159
+ let fullPrompt = rawText;
160
+ let displayText = rawText;
161
+ let imageAttachments = [];
162
+
163
+ if (effectiveFiles.length > 0) {
164
+ const FileParser = getFileParser();
165
+
166
+ imageAttachments = effectiveFiles.filter(f => f.type === 'image' && f.dataUrl).map(f => ({
167
+ name: f.name,
168
+ dataUrl: f.dataUrl,
169
+ mimeType: f.mimeType || 'image/jpeg'
170
+ }));
171
+
172
+ const attachmentsText = effectiveFiles.map(file => {
173
+ if (file.type === 'pdf') {
174
+ return `\n\n--- PDF Document: ${file.name} (${FileParser.formatBytes(file.size)}) ---\n\`\`\`text\n${file.content}\n\`\`\``;
175
+ } else if (file.type === 'image') {
176
+ return `\n\n--- Image: ${file.name} (${FileParser.formatBytes(file.size)}) ---`;
177
+ }
178
+ return `\n\n--- File: ${file.name} (${FileParser.formatBytes(file.size)}) ---\n\`\`\`\n${file.content}\n\`\`\``;
179
+ }).join('');
180
+
181
+ fullPrompt = rawText ? `${rawText}\n${attachmentsText}` : `Attached files for analysis:${attachmentsText}`;
182
+
183
+ const fileNamesList = effectiveFiles.map(f => {
184
+ const icon = f.type === 'pdf' ? '📕' : f.type === 'image' ? '🖼️' : '📎';
185
+ return `${icon} ${f.name}`;
186
+ }).join(', ');
187
+
188
+ displayText = rawText ? `${rawText}\n\n[${fileNamesList}]` : `[${fileNamesList}]`;
189
+ }
190
+
191
+ return {
192
+ fullPrompt,
193
+ displayText,
194
+ imageAttachments
195
+ };
196
+ }
197
+
198
+ const MAX_FILE_SIZE = 50 * 1024 * 1024; // 50 MB límite por archivo
199
+
200
+ function validateFileSize(file, maxBytes = MAX_FILE_SIZE) {
201
+ if (!file) return { valid: false, reason: 'empty' };
202
+ const size = typeof file.size === 'number' ? file.size : 0;
203
+ if (size > maxBytes) {
204
+ return { valid: false, size, maxBytes, reason: 'too_large' };
205
+ }
206
+ return { valid: true, size, maxBytes };
207
+ }
208
+
209
+ return {
210
+ MAX_FILE_SIZE,
211
+ validateFileSize,
212
+ getFiles,
213
+ setFiles,
214
+ clearFiles,
215
+ removeFileAt,
216
+ addFile,
217
+ renderChips,
218
+ buildAttachmentsPayload
219
+ };
220
+ }));
@@ -0,0 +1,338 @@
1
+ /**
2
+ * Módulo de Visualización y Gráficos Nativos en SVG (ChatCharts) para ZeroChat.
3
+ * - Soporta gráficos de Barras, Líneas, Donut y Sectores (Pie) con SVG puro sin dependencias externas.
4
+ * - Totalmente interactivo, accesible y adaptado a temas claro y oscuro.
5
+ * - Compatible con file:// y http://.
6
+ */
7
+
8
+ (function (root, factory) {
9
+ if (typeof exports === 'object' && typeof module !== 'undefined') {
10
+ module.exports = factory();
11
+ } else {
12
+ root.ChatCharts = factory();
13
+ }
14
+ })(typeof self !== 'undefined' ? self : this, function () {
15
+ 'use strict';
16
+
17
+ const COLOR_PALETTE = [
18
+ '#3b82f6', // blue
19
+ '#10b981', // emerald
20
+ '#f59e0b', // amber
21
+ '#8b5cf6', // purple
22
+ '#ec4899', // pink
23
+ '#06b6d4', // cyan
24
+ '#f97316', // orange
25
+ '#6366f1', // indigo
26
+ '#14b8a6', // teal
27
+ '#84cc16' // lime
28
+ ];
29
+
30
+ function getUtils() {
31
+ if (typeof window !== 'undefined' && window.ChatUtils) return window.ChatUtils;
32
+ if (typeof require !== 'undefined') {
33
+ try { return require('./utils.js'); } catch (e) { return null; }
34
+ }
35
+ return null;
36
+ }
37
+
38
+ function escapeHtml(str) {
39
+ const Utils = getUtils();
40
+ if (Utils && typeof Utils.escapeHtml === 'function') return Utils.escapeHtml(str);
41
+ if (!str) return '';
42
+ return String(str)
43
+ .replace(/&/g, '&amp;')
44
+ .replace(/</g, '&lt;')
45
+ .replace(/>/g, '&gt;')
46
+ .replace(/"/g, '&quot;')
47
+ .replace(/'/g, '&#39;');
48
+ }
49
+
50
+ /**
51
+ * Genera un gráfico de barras verticales en SVG.
52
+ */
53
+ function renderBarChart(labels, datasets, width = 640, height = 340) {
54
+ const padding = { top: 40, right: 30, bottom: 60, left: 60 };
55
+ const chartWidth = width - padding.left - padding.right;
56
+ const chartHeight = height - padding.top - padding.bottom;
57
+
58
+ const allValues = datasets.flatMap(d => d.data || []);
59
+ const maxVal = Math.max(...allValues, 1);
60
+ const minVal = Math.min(0, ...allValues);
61
+ const valRange = maxVal - minVal;
62
+
63
+ // Ejes y cuadrícula horizontal
64
+ const numTicks = 5;
65
+ let gridLines = '';
66
+ for (let i = 0; i <= numTicks; i++) {
67
+ const val = minVal + (valRange * (numTicks - i)) / numTicks;
68
+ const y = padding.top + (chartHeight * i) / numTicks;
69
+ gridLines += `
70
+ <line x1="${padding.left}" y1="${y}" x2="${width - padding.right}" y2="${y}" stroke="currentColor" stroke-opacity="0.12" stroke-dasharray="3,3"/>
71
+ <text x="${padding.left - 8}" y="${y + 4}" font-size="11" text-anchor="end" fill="currentColor" opacity="0.65">${val.toLocaleString()}</text>
72
+ `;
73
+ }
74
+
75
+ // Barras
76
+ const groupWidth = chartWidth / labels.length;
77
+ const barPadding = groupWidth * 0.2;
78
+ const availableBarWidth = groupWidth - barPadding;
79
+ const singleBarWidth = availableBarWidth / datasets.length;
80
+
81
+ let bars = '';
82
+ let xLabels = '';
83
+
84
+ labels.forEach((label, groupIdx) => {
85
+ const groupX = padding.left + groupIdx * groupWidth + barPadding / 2;
86
+ const labelX = groupX + availableBarWidth / 2;
87
+
88
+ xLabels += `
89
+ <text x="${labelX}" y="${height - padding.bottom + 20}" font-size="11" text-anchor="middle" fill="currentColor" opacity="0.8" transform="rotate(-15, ${labelX}, ${height - padding.bottom + 20})">
90
+ ${escapeHtml(label.length > 14 ? label.slice(0, 12) + '…' : label)}
91
+ </text>
92
+ `;
93
+
94
+ datasets.forEach((ds, dsIdx) => {
95
+ const val = (ds.data && ds.data[groupIdx] !== undefined) ? ds.data[groupIdx] : 0;
96
+ const barH = (val / (maxVal || 1)) * chartHeight;
97
+ const barX = groupX + dsIdx * singleBarWidth;
98
+ const barY = padding.top + chartHeight - barH;
99
+ const color = ds.color || COLOR_PALETTE[dsIdx % COLOR_PALETTE.length];
100
+
101
+ bars += `
102
+ <g class="chart-bar-group">
103
+ <rect x="${barX}" y="${barY}" width="${Math.max(singleBarWidth - 2, 2)}" height="${Math.max(barH, 2)}" fill="${color}" rx="3">
104
+ <title>${escapeHtml(label)} (${escapeHtml(ds.label || 'Valor')}): ${val.toLocaleString()}</title>
105
+ </rect>
106
+ <text x="${barX + singleBarWidth / 2}" y="${barY - 5}" font-size="10" font-weight="600" text-anchor="middle" fill="${color}">
107
+ ${val >= 1000 ? (val / 1000).toFixed(1) + 'k' : val}
108
+ </text>
109
+ </g>
110
+ `;
111
+ });
112
+ });
113
+
114
+ return `
115
+ <svg viewBox="0 0 ${width} ${height}" class="chat-chart-svg" style="width:100%; height:auto; max-height:360px;">
116
+ ${gridLines}
117
+ ${bars}
118
+ ${xLabels}
119
+ </svg>
120
+ `;
121
+ }
122
+
123
+ /**
124
+ * Genera un gráfico de líneas en SVG.
125
+ */
126
+ function renderLineChart(labels, datasets, width = 640, height = 340) {
127
+ const padding = { top: 40, right: 30, bottom: 60, left: 60 };
128
+ const chartWidth = width - padding.left - padding.right;
129
+ const chartHeight = height - padding.top - padding.bottom;
130
+
131
+ const allValues = datasets.flatMap(d => d.data || []);
132
+ const maxVal = Math.max(...allValues, 1);
133
+ const minVal = Math.min(0, ...allValues);
134
+ const valRange = maxVal - minVal;
135
+
136
+ let gridLines = '';
137
+ const numTicks = 5;
138
+ for (let i = 0; i <= numTicks; i++) {
139
+ const val = minVal + (valRange * (numTicks - i)) / numTicks;
140
+ const y = padding.top + (chartHeight * i) / numTicks;
141
+ gridLines += `
142
+ <line x1="${padding.left}" y1="${y}" x2="${width - padding.right}" y2="${y}" stroke="currentColor" stroke-opacity="0.12" stroke-dasharray="3,3"/>
143
+ <text x="${padding.left - 8}" y="${y + 4}" font-size="11" text-anchor="end" fill="currentColor" opacity="0.65">${val.toLocaleString()}</text>
144
+ `;
145
+ }
146
+
147
+ let paths = '';
148
+ let dots = '';
149
+ let xLabels = '';
150
+
151
+ const stepX = labels.length > 1 ? chartWidth / (labels.length - 1) : chartWidth / 2;
152
+
153
+ labels.forEach((label, idx) => {
154
+ const x = labels.length > 1 ? padding.left + idx * stepX : padding.left + chartWidth / 2;
155
+ xLabels += `
156
+ <text x="${x}" y="${height - padding.bottom + 20}" font-size="11" text-anchor="middle" fill="currentColor" opacity="0.8" transform="rotate(-15, ${x}, ${height - padding.bottom + 20})">
157
+ ${escapeHtml(label.length > 14 ? label.slice(0, 12) + '…' : label)}
158
+ </text>
159
+ `;
160
+ });
161
+
162
+ datasets.forEach((ds, dsIdx) => {
163
+ const color = ds.color || COLOR_PALETTE[dsIdx % COLOR_PALETTE.length];
164
+ const points = (ds.data || []).map((val, idx) => {
165
+ const x = labels.length > 1 ? padding.left + idx * stepX : padding.left + chartWidth / 2;
166
+ const y = padding.top + chartHeight - (val / (maxVal || 1)) * chartHeight;
167
+ return { x, y, val, label: labels[idx] };
168
+ });
169
+
170
+ if (points.length === 0) return;
171
+
172
+ const pathData = points.reduce((acc, pt, i) => `${acc} ${i === 0 ? 'M' : 'L'} ${pt.x} ${pt.y}`, '');
173
+ const areaData = `${pathData} L ${points[points.length - 1].x} ${padding.top + chartHeight} L ${points[0].x} ${padding.top + chartHeight} Z`;
174
+
175
+ paths += `
176
+ <path d="${areaData}" fill="${color}" fill-opacity="0.12" />
177
+ <path d="${pathData}" fill="none" stroke="${color}" stroke-width="3" stroke-linecap="round" stroke-linejoin="round" />
178
+ `;
179
+
180
+ points.forEach(pt => {
181
+ dots += `
182
+ <circle cx="${pt.x}" cy="${pt.y}" r="4.5" fill="${color}" stroke="#ffffff" stroke-width="1.5">
183
+ <title>${escapeHtml(pt.label)}: ${pt.val.toLocaleString()}</title>
184
+ </circle>
185
+ <text x="${pt.x}" y="${pt.y - 8}" font-size="10" font-weight="600" text-anchor="middle" fill="${color}">
186
+ ${pt.val >= 1000 ? (pt.val / 1000).toFixed(1) + 'k' : pt.val}
187
+ </text>
188
+ `;
189
+ });
190
+ });
191
+
192
+ return `
193
+ <svg viewBox="0 0 ${width} ${height}" class="chat-chart-svg" style="width:100%; height:auto; max-height:360px;">
194
+ ${gridLines}
195
+ ${paths}
196
+ ${dots}
197
+ ${xLabels}
198
+ </svg>
199
+ `;
200
+ }
201
+
202
+ /**
203
+ * Genera un gráfico de donut o sectores (Pie) en SVG.
204
+ */
205
+ function renderPieOrDonutChart(labels, dataset, isDonut = true, width = 500, height = 300) {
206
+ const data = dataset.data || [];
207
+ const total = data.reduce((acc, v) => acc + (Number(v) || 0), 0) || 1;
208
+
209
+ const centerX = 160;
210
+ const centerY = height / 2;
211
+ const outerRadius = 110;
212
+ const innerRadius = isDonut ? 60 : 0;
213
+
214
+ let startAngle = 0;
215
+ let slices = '';
216
+ let legendItems = '';
217
+
218
+ data.forEach((val, idx) => {
219
+ const percentage = (val / total) * 100;
220
+ const angle = (val / total) * 2 * Math.PI;
221
+ const endAngle = startAngle + angle;
222
+ const color = COLOR_PALETTE[idx % COLOR_PALETTE.length];
223
+ const label = labels[idx] || `Item ${idx + 1}`;
224
+
225
+ // Coordenadas del arco
226
+ const x1 = centerX + outerRadius * Math.sin(startAngle);
227
+ const y1 = centerY - outerRadius * Math.cos(startAngle);
228
+ const x2 = centerX + outerRadius * Math.sin(endAngle);
229
+ const y2 = centerY - outerRadius * Math.cos(endAngle);
230
+
231
+ const largeArcFlag = angle > Math.PI ? 1 : 0;
232
+
233
+ let pathD = '';
234
+ if (isDonut) {
235
+ const ix1 = centerX + innerRadius * Math.sin(startAngle);
236
+ const iy1 = centerY - innerRadius * Math.cos(startAngle);
237
+ const ix2 = centerX + innerRadius * Math.sin(endAngle);
238
+ const iy2 = centerY - innerRadius * Math.cos(endAngle);
239
+
240
+ pathD = `M ${x1} ${y1} A ${outerRadius} ${outerRadius} 0 ${largeArcFlag} 1 ${x2} ${y2} L ${ix2} ${iy2} A ${innerRadius} ${innerRadius} 0 ${largeArcFlag} 0 ${ix1} ${iy1} Z`;
241
+ } else {
242
+ pathD = `M ${centerX} ${centerY} L ${x1} ${y1} A ${outerRadius} ${outerRadius} 0 ${largeArcFlag} 1 ${x2} ${y2} Z`;
243
+ }
244
+
245
+ slices += `
246
+ <path d="${pathD}" fill="${color}" stroke="#ffffff" stroke-width="1.5" opacity="0.95">
247
+ <title>${escapeHtml(label)}: ${val.toLocaleString()} (${percentage.toFixed(1)}%)</title>
248
+ </path>
249
+ `;
250
+
251
+ // Leyenda
252
+ const legendY = 40 + idx * 24;
253
+ if (legendY < height - 10) {
254
+ legendItems += `
255
+ <g transform="translate(300, ${legendY})">
256
+ <rect x="0" y="-8" width="12" height="12" rx="3" fill="${color}"/>
257
+ <text x="18" y="2" font-size="11" fill="currentColor" opacity="0.9">${escapeHtml(label.slice(0, 16))}</text>
258
+ <text x="180" y="2" font-size="11" font-weight="600" text-anchor="end" fill="${color}">${percentage.toFixed(1)}%</text>
259
+ </g>
260
+ `;
261
+ }
262
+
263
+ startAngle = endAngle;
264
+ });
265
+
266
+ return `
267
+ <svg viewBox="0 0 ${width} ${height}" class="chat-chart-svg" style="width:100%; height:auto; max-height:320px;">
268
+ <g class="pie-slices">${slices}</g>
269
+ <g class="pie-legend">${legendItems}</g>
270
+ </svg>
271
+ `;
272
+ }
273
+
274
+ /**
275
+ * Renderiza el contenedor completo de la tarjeta de gráfico interactivo.
276
+ */
277
+ function renderChartCard(config) {
278
+ const type = (config.type || 'bar').toLowerCase();
279
+ const title = config.title || 'Visualización de Datos';
280
+ const description = config.description || '';
281
+ const labels = config.labels || [];
282
+ const datasets = config.datasets || (config.data ? [{ label: title, data: config.data }] : []);
283
+
284
+ let chartSvg = '';
285
+ if (type === 'line') {
286
+ chartSvg = renderLineChart(labels, datasets);
287
+ } else if (type === 'pie') {
288
+ chartSvg = renderPieOrDonutChart(labels, datasets[0] || { data: [] }, false);
289
+ } else if (type === 'donut' || type === 'doughnut') {
290
+ chartSvg = renderPieOrDonutChart(labels, datasets[0] || { data: [] }, true);
291
+ } else {
292
+ chartSvg = renderBarChart(labels, datasets);
293
+ }
294
+
295
+ // Leyendas superiores para barras y líneas si hay múltiples datasets
296
+ let topLegend = '';
297
+ if ((type === 'bar' || type === 'line') && datasets.length > 1) {
298
+ topLegend = '<div class="chart-legend-top">' + datasets.map((ds, idx) => `
299
+ <span class="chart-legend-item">
300
+ <span class="chart-legend-dot" style="background-color:${ds.color || COLOR_PALETTE[idx % COLOR_PALETTE.length]}"></span>
301
+ ${escapeHtml(ds.label || `Serie ${idx + 1}`)}
302
+ </span>
303
+ `).join('') + '</div>';
304
+ }
305
+
306
+ const chartIconSvg = '<svg class="ui-icon" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="12" y1="20" x2="12" y2="10"></line><line x1="18" y1="20" x2="18" y2="4"></line><line x1="6" y1="20" x2="6" y2="16"></line></svg>';
307
+ const chevronSvg = '<svg class="ui-icon" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="6 9 12 15 18 9"></polyline></svg>';
308
+
309
+ return `
310
+ <div class="chat-chart-card">
311
+ <div class="chat-chart-header">
312
+ <div class="chat-chart-title">
313
+ <span>${chartIconSvg}</span>
314
+ <strong>${escapeHtml(title)}</strong>
315
+ </div>
316
+ <div class="tool-card-header-actions">
317
+ <span class="chat-chart-badge">${escapeHtml(type.toUpperCase())}</span>
318
+ <button type="button" class="btn-tool-collapse" title="Minimizar / Expandir gráfico">${chevronSvg}</button>
319
+ </div>
320
+ </div>
321
+ <div class="tool-card-collapsible-body">
322
+ ${description ? `<div class="chat-chart-description">${escapeHtml(description)}</div>` : ''}
323
+ ${topLegend}
324
+ <div class="chat-chart-body">
325
+ ${chartSvg}
326
+ </div>
327
+ </div>
328
+ </div>
329
+ `;
330
+ }
331
+
332
+ return {
333
+ renderBarChart,
334
+ renderLineChart,
335
+ renderPieOrDonutChart,
336
+ renderChartCard
337
+ };
338
+ });