@caribeng/ui 22.2.0 → 22.4.0

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 (121) hide show
  1. package/README.md +93 -11
  2. package/fesm2022/caribeng-ui-accordion.mjs +2 -2
  3. package/fesm2022/caribeng-ui-avatar.mjs +2 -2
  4. package/fesm2022/caribeng-ui-badge.mjs +8 -2
  5. package/fesm2022/caribeng-ui-breadcrumb.mjs +13 -6
  6. package/fesm2022/caribeng-ui-chart.mjs +8 -5
  7. package/fesm2022/caribeng-ui-chat.mjs +1303 -0
  8. package/fesm2022/caribeng-ui-code-block.mjs +332 -0
  9. package/fesm2022/caribeng-ui-colorpicker.mjs +99 -5
  10. package/fesm2022/caribeng-ui-command-palette.mjs +476 -0
  11. package/fesm2022/caribeng-ui-confirmdialog.mjs +78 -13
  12. package/fesm2022/caribeng-ui-confirmpopup.mjs +91 -16
  13. package/fesm2022/caribeng-ui-contextmenu.mjs +8 -4
  14. package/fesm2022/caribeng-ui-copy-button.mjs +230 -0
  15. package/fesm2022/caribeng-ui-dataview.mjs +4 -1
  16. package/fesm2022/caribeng-ui-dock.mjs +52 -12
  17. package/fesm2022/caribeng-ui-drawer.mjs +9 -4
  18. package/fesm2022/caribeng-ui-editor.mjs +21 -0
  19. package/fesm2022/caribeng-ui-fieldset.mjs +2 -2
  20. package/fesm2022/caribeng-ui-file-card.mjs +134 -17
  21. package/fesm2022/caribeng-ui-folder-card.mjs +122 -12
  22. package/fesm2022/caribeng-ui-inlinemessage.mjs +2 -2
  23. package/fesm2022/caribeng-ui-inputmask.mjs +8 -2
  24. package/fesm2022/caribeng-ui-kanban.mjs +4612 -0
  25. package/fesm2022/caribeng-ui-kbd.mjs +122 -0
  26. package/fesm2022/caribeng-ui-megamenu.mjs +13 -9
  27. package/fesm2022/caribeng-ui-mentions.mjs +495 -0
  28. package/fesm2022/caribeng-ui-menu.mjs +8 -4
  29. package/fesm2022/caribeng-ui-menubar.mjs +9 -5
  30. package/fesm2022/caribeng-ui-message.mjs +6 -3
  31. package/fesm2022/caribeng-ui-organizationchart.mjs +6 -10
  32. package/fesm2022/caribeng-ui-panel.mjs +6 -6
  33. package/fesm2022/caribeng-ui-panelmenu.mjs +9 -6
  34. package/fesm2022/caribeng-ui-phone-input.mjs +688 -0
  35. package/fesm2022/caribeng-ui-pricing.mjs +263 -69
  36. package/fesm2022/caribeng-ui-qrcode.mjs +564 -0
  37. package/fesm2022/caribeng-ui-ripple.mjs +2 -0
  38. package/fesm2022/caribeng-ui-scrolltop.mjs +3 -1
  39. package/fesm2022/caribeng-ui-sidebar.mjs +573 -0
  40. package/fesm2022/caribeng-ui-stepper.mjs +10 -3
  41. package/fesm2022/caribeng-ui-table.mjs +10 -4
  42. package/fesm2022/caribeng-ui-tabmenu.mjs +8 -4
  43. package/fesm2022/caribeng-ui-tieredmenu.mjs +109 -19
  44. package/fesm2022/caribeng-ui.mjs +388 -1
  45. package/package.json +42 -2
  46. package/schematics/collection.json +5 -0
  47. package/schematics/migrate-from-primeng/core-icons.json +1 -0
  48. package/schematics/migrate-from-primeng/icons.js +269 -0
  49. package/schematics/migrate-from-primeng/index.js +290 -0
  50. package/schematics/migrate-from-primeng/mappings/button.js +93 -0
  51. package/schematics/migrate-from-primeng/mappings/card.js +33 -0
  52. package/schematics/migrate-from-primeng/mappings/checkbox.js +44 -0
  53. package/schematics/migrate-from-primeng/mappings/confirm.js +221 -0
  54. package/schematics/migrate-from-primeng/mappings/datepicker.js +121 -0
  55. package/schematics/migrate-from-primeng/mappings/dialog.js +84 -0
  56. package/schematics/migrate-from-primeng/mappings/index.js +88 -0
  57. package/schematics/migrate-from-primeng/mappings/inputnumber.js +62 -0
  58. package/schematics/migrate-from-primeng/mappings/inputtext.js +60 -0
  59. package/schematics/migrate-from-primeng/mappings/menu.js +468 -0
  60. package/schematics/migrate-from-primeng/mappings/multiselect.js +49 -0
  61. package/schematics/migrate-from-primeng/mappings/radiobutton.js +22 -0
  62. package/schematics/migrate-from-primeng/mappings/rules.js +206 -0
  63. package/schematics/migrate-from-primeng/mappings/select.js +97 -0
  64. package/schematics/migrate-from-primeng/mappings/service-calls.js +100 -0
  65. package/schematics/migrate-from-primeng/mappings/table.js +613 -0
  66. package/schematics/migrate-from-primeng/mappings/tabs.js +225 -0
  67. package/schematics/migrate-from-primeng/mappings/tag.js +29 -0
  68. package/schematics/migrate-from-primeng/mappings/textarea.js +44 -0
  69. package/schematics/migrate-from-primeng/mappings/toast.js +173 -0
  70. package/schematics/migrate-from-primeng/mappings/toggleswitch.js +37 -0
  71. package/schematics/migrate-from-primeng/mappings/tooltip.js +44 -0
  72. package/schematics/migrate-from-primeng/mappings/types.js +10 -0
  73. package/schematics/migrate-from-primeng/report.js +79 -0
  74. package/schematics/migrate-from-primeng/schema.js +2 -0
  75. package/schematics/migrate-from-primeng/schema.json +34 -0
  76. package/schematics/migrate-from-primeng/template.js +387 -0
  77. package/schematics/migrate-from-primeng/todo.js +5 -0
  78. package/schematics/migrate-from-primeng/typescript.js +442 -0
  79. package/schematics/migrate-from-primeng/workspace.js +117 -0
  80. package/schematics/ng-add/index.js +16 -1
  81. package/types/caribeng-ui-badge.d.ts +3 -1
  82. package/types/caribeng-ui-breadcrumb.d.ts +8 -0
  83. package/types/caribeng-ui-chart.d.ts +3 -1
  84. package/types/caribeng-ui-chat.d.ts +299 -0
  85. package/types/caribeng-ui-code-block.d.ts +65 -0
  86. package/types/caribeng-ui-colorpicker.d.ts +5 -1
  87. package/types/caribeng-ui-command-palette.d.ts +133 -0
  88. package/types/caribeng-ui-confirmdialog.d.ts +34 -2
  89. package/types/caribeng-ui-confirmpopup.d.ts +39 -2
  90. package/types/caribeng-ui-contextmenu.d.ts +7 -0
  91. package/types/caribeng-ui-copy-button.d.ts +90 -0
  92. package/types/caribeng-ui-dataview.d.ts +2 -0
  93. package/types/caribeng-ui-dock.d.ts +13 -1
  94. package/types/caribeng-ui-drawer.d.ts +3 -1
  95. package/types/caribeng-ui-file-card.d.ts +27 -2
  96. package/types/caribeng-ui-folder-card.d.ts +27 -5
  97. package/types/caribeng-ui-inputmask.d.ts +3 -1
  98. package/types/caribeng-ui-kanban.d.ts +783 -0
  99. package/types/caribeng-ui-kbd.d.ts +36 -0
  100. package/types/caribeng-ui-megamenu.d.ts +11 -0
  101. package/types/caribeng-ui-mentions.d.ts +127 -0
  102. package/types/caribeng-ui-menu.d.ts +7 -0
  103. package/types/caribeng-ui-menubar.d.ts +7 -0
  104. package/types/caribeng-ui-message.d.ts +2 -0
  105. package/types/caribeng-ui-panelmenu.d.ts +7 -0
  106. package/types/caribeng-ui-phone-input.d.ts +168 -0
  107. package/types/caribeng-ui-pricing.d.ts +41 -4
  108. package/types/caribeng-ui-qrcode.d.ts +88 -0
  109. package/types/caribeng-ui-sidebar.d.ts +167 -0
  110. package/types/caribeng-ui-stepper.d.ts +2 -0
  111. package/types/caribeng-ui-table.d.ts +1 -1
  112. package/types/caribeng-ui-tabmenu.d.ts +7 -0
  113. package/types/caribeng-ui-tieredmenu.d.ts +17 -1
  114. package/types/caribeng-ui.d.ts +276 -2
  115. package/styles/caribeng.css +0 -380
  116. package/styles/scss/_mixins.scss +0 -46
  117. package/styles/scss/_overlay.scss +0 -15
  118. package/styles/scss/_tokens.scss +0 -267
  119. package/styles/scss/_utilities.scss +0 -73
  120. package/styles/scss/_variables.scss +0 -33
  121. package/styles/scss/index.scss +0 -16
@@ -0,0 +1,1303 @@
1
+ import * as i0 from '@angular/core';
2
+ import { input, booleanAttribute, model, output, computed, ViewEncapsulation, ChangeDetectionStrategy, Component, inject, DestroyRef, viewChild, signal, afterNextRender, effect, untracked, Injector } from '@angular/core';
3
+ import { injectCbTranslation, cbFormat } from '@caribeng/ui';
4
+ import { CbCodeBlock } from '@caribeng/ui/code-block';
5
+ import { CbCopyButton } from '@caribeng/ui/copy-button';
6
+ import { CbIcon } from '@caribeng/ui/icon';
7
+ import { DOCUMENT } from '@angular/common';
8
+
9
+ /*
10
+ * Markdown ligero y seguro de CaribeNG para mensajes de chat.
11
+ *
12
+ * Todo el texto de entrada se escapa: el HTML que llega se muestra como texto,
13
+ * nunca se interpreta. Solo se generan etiquetas conocidas y enlaces a
14
+ * http(s), mailto, rutas relativas o anclas. Tolera markdown incompleto (lo
15
+ * habitual mientras llega una respuesta en streaming): un bloque de código sin
16
+ * cerrar se pinta igualmente como código.
17
+ *
18
+ * Soporta: párrafos (un salto de línea = <br>), encabezados, negrita, cursiva,
19
+ * tachado, código en línea y en bloque (con lenguaje), enlaces, URLs sueltas,
20
+ * listas con viñetas, numeradas y de tareas, citas, separadores y tablas.
21
+ *
22
+ * Solo emite etiquetas y atributos que conserva el sanitizador de Angular
23
+ * (`[innerHTML]`): nada de `style` ni `<input>`.
24
+ */
25
+ function escapeHtml(text) {
26
+ return text.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;').replace(/'/g, '&#39;');
27
+ }
28
+ /** URL permitida en un enlace, o `null`. */
29
+ function cbSafeUrl(url) {
30
+ const trimmed = url.trim();
31
+ if (/^(https?:|mailto:)/i.test(trimmed))
32
+ return trimmed;
33
+ if (/^[a-z][a-z\d+.-]*:/i.test(trimmed))
34
+ return null;
35
+ // Relativas, anclas y consultas; nunca "//dominio" (abriría otro origen sin esquema explícito).
36
+ if (/^(\/(?!\/)|#|\?|\.{1,2}\/)/.test(trimmed) || /^[\w-][\w./-]*$/.test(trimmed))
37
+ return trimmed;
38
+ return null;
39
+ }
40
+ function link(href, label) {
41
+ const external = /^(https?:)?\/\//i.test(href);
42
+ const attrs = external ? ' target="_blank" rel="noopener noreferrer"' : '';
43
+ return `<a href="${escapeHtml(href)}"${attrs}>${label}</a>`;
44
+ }
45
+ /** Formato en línea sobre texto aún sin escapar. */
46
+ function inline(text) {
47
+ const tokens = [];
48
+ const stash = (html) => `\u0000${tokens.push(html) - 1}\u0000`;
49
+ // 1) Código en línea: su contenido no se formatea.
50
+ let out = text.replace(/(`+)([\s\S]*?[^`])\1(?!`)/g, (_m, _ticks, code) => stash(`<code>${escapeHtml(code.trim() === '' ? code : code.replace(/^ (.*) $/, '$1'))}</code>`));
51
+ // 2) Enlaces [texto](url).
52
+ out = out.replace(/\[([^\]\n]+)\]\(([^)\s]+)(?:\s+"[^"]*")?\)/g, (m, label, url) => {
53
+ const safe = cbSafeUrl(url);
54
+ return safe ? stash(link(safe, formatEmphasis(escapeHtml(label)))) : m;
55
+ });
56
+ // 3) URLs sueltas.
57
+ out = out.replace(/\bhttps?:\/\/[^\s<>()]+[^\s<>().,;:!?'"\]]/g, (url) => stash(link(url, escapeHtml(url))));
58
+ // 4) Resto: escapar y aplicar énfasis.
59
+ out = formatEmphasis(escapeHtml(out));
60
+ // Los tokens pueden contener otros (código dentro del texto de un enlace).
61
+ for (let depth = 0; depth < 4 && out.includes('\u0000'); depth++) {
62
+ out = out.replace(/\u0000(\d+)\u0000/g, (_m, i) => tokens[Number(i)]);
63
+ }
64
+ return out;
65
+ }
66
+ /** Negrita, cursiva y tachado sobre texto ya escapado. */
67
+ function formatEmphasis(html) {
68
+ return html
69
+ .replace(/\*\*\*(?=\S)([\s\S]*?\S)\*\*\*/g, '<strong><em>$1</em></strong>')
70
+ .replace(/\*\*(?=\S)([\s\S]*?\S)\*\*/g, '<strong>$1</strong>')
71
+ .replace(/(^|[^\w])__(?=\S)([\s\S]*?\S)__(?!\w)/g, '$1<strong>$2</strong>')
72
+ .replace(/(^|[^*\w])\*([^\s*](?:[^*\n]*?[^\s*])?)\*(?!\*)/g, '$1<em>$2</em>')
73
+ .replace(/(^|[^\w])_([^\s_](?:[^_\n]*?[^\s_])?)_(?!\w)/g, '$1<em>$2</em>')
74
+ .replace(/~~(?=\S)([\s\S]*?\S)~~/g, '<del>$1</del>');
75
+ }
76
+ const FENCE = /^ {0,3}(`{3,}|~{3,})\s*([\w#+.-]*)\s*$/;
77
+ const HEADING = /^ {0,3}(#{1,6})\s+(.*?)\s*#*\s*$/;
78
+ const HR = /^ {0,3}([-*_])(\s*\1){2,}\s*$/;
79
+ const QUOTE = /^ {0,3}>\s?/;
80
+ const BULLET = /^(\s*)([-*+])\s+(.*)$/;
81
+ const ORDERED = /^(\s*)(\d{1,9})[.)]\s+(.*)$/;
82
+ const TABLE_SEP = /^\s*\|?\s*:?-{2,}:?\s*(\|\s*:?-{2,}:?\s*)*\|?\s*$/;
83
+ function splitRow(line) {
84
+ let row = line.trim();
85
+ if (row.startsWith('|'))
86
+ row = row.slice(1);
87
+ if (row.endsWith('|') && !row.endsWith('\\|'))
88
+ row = row.slice(0, -1);
89
+ return row.split(/(?<!\\)\|/).map((cell) => cell.trim().replace(/\\\|/g, '|'));
90
+ }
91
+ function isBlockStart(line) {
92
+ return FENCE.test(line) || HEADING.test(line) || HR.test(line) || QUOTE.test(line) || BULLET.test(line) || ORDERED.test(line);
93
+ }
94
+ function renderList(lines, start) {
95
+ const ordered = ORDERED.test(lines[start]);
96
+ const pattern = ordered ? ORDERED : BULLET;
97
+ const first = lines[start].match(pattern);
98
+ const indent = first[1].length;
99
+ const items = [];
100
+ let i = start;
101
+ while (i < lines.length) {
102
+ const m = lines[i].match(pattern);
103
+ if (!m || m[1].length !== indent)
104
+ break;
105
+ let body = m[3];
106
+ i++;
107
+ // Líneas de continuación y sublistas más indentadas.
108
+ const nested = [];
109
+ while (i < lines.length && lines[i].trim() !== '' && !(lines[i].match(pattern)?.[1].length === indent) && (/^\s+/.test(lines[i]) || !isBlockStart(lines[i]))) {
110
+ nested.push(lines[i]);
111
+ i++;
112
+ }
113
+ let sub = '';
114
+ const subStart = nested.findIndex((l) => BULLET.test(l) || ORDERED.test(l));
115
+ if (subStart >= 0) {
116
+ body += nested.slice(0, subStart).map((l) => '\n' + l.trim()).join('');
117
+ sub = renderBlocks(nested.slice(subStart).map((l) => l.replace(new RegExp(`^\\s{0,${indent + 4}}`), '')));
118
+ }
119
+ else {
120
+ body += nested.map((l) => '\n' + l.trim()).join('');
121
+ }
122
+ const task = !ordered ? body.match(/^\[([ xX])\]\s+(.*)$/s) : null;
123
+ const content = task
124
+ ? `<span class="cb-md-check">${task[1].trim() ? '☑' : '☐'}</span> ${inline(task[2]).replace(/\n/g, '<br>')}`
125
+ : inline(body).replace(/\n/g, '<br>');
126
+ items.push(`<li${task ? ' class="cb-md-task"' : ''}>${content}${sub}</li>`);
127
+ if (i < lines.length && lines[i].trim() === '' && lines[i + 1]?.match(pattern)?.[1].length === indent)
128
+ i++;
129
+ }
130
+ const startAttr = ordered && first[2] !== '1' ? ` start="${Number(first[2])}"` : '';
131
+ return { html: ordered ? `<ol${startAttr}>${items.join('')}</ol>` : `<ul>${items.join('')}</ul>`, next: i };
132
+ }
133
+ /**
134
+ * Pinta los bloques. Con `sink`, los bloques de código del nivel superior no se
135
+ * pintan como HTML sino que se entregan aparte (para un componente de código).
136
+ */
137
+ function renderBlocks(lines, sink) {
138
+ const out = [];
139
+ const flush = () => {
140
+ if (sink && out.length)
141
+ sink.push({ kind: 'html', html: out.join('') });
142
+ out.length = 0;
143
+ };
144
+ let i = 0;
145
+ while (i < lines.length) {
146
+ const line = lines[i];
147
+ if (line.trim() === '') {
148
+ i++;
149
+ continue;
150
+ }
151
+ const fence = line.match(FENCE);
152
+ if (fence) {
153
+ const marker = fence[1];
154
+ const lang = fence[2];
155
+ const code = [];
156
+ i++;
157
+ while (i < lines.length && !new RegExp(`^ {0,3}${marker[0]}{${marker.length},}\\s*$`).test(lines[i]))
158
+ code.push(lines[i++]);
159
+ i++; // cierre (o fin del texto si llega incompleto)
160
+ if (sink) {
161
+ flush();
162
+ sink.push({ kind: 'code', code: code.join('\n'), language: lang });
163
+ continue;
164
+ }
165
+ const cls = lang ? ` class="language-${escapeHtml(lang)}"` : '';
166
+ const label = lang ? `<span class="cb-md-lang">${escapeHtml(lang)}</span>` : '';
167
+ out.push(`<pre>${label}<code${cls}>${escapeHtml(code.join('\n'))}</code></pre>`);
168
+ continue;
169
+ }
170
+ const heading = line.match(HEADING);
171
+ if (heading) {
172
+ const level = heading[1].length;
173
+ out.push(`<h${level}>${inline(heading[2])}</h${level}>`);
174
+ i++;
175
+ continue;
176
+ }
177
+ if (HR.test(line)) {
178
+ out.push('<hr />');
179
+ i++;
180
+ continue;
181
+ }
182
+ if (QUOTE.test(line)) {
183
+ const quoted = [];
184
+ while (i < lines.length && (QUOTE.test(lines[i]) || (lines[i].trim() !== '' && !isBlockStart(lines[i]) && quoted.length))) {
185
+ quoted.push(lines[i].replace(QUOTE, ''));
186
+ i++;
187
+ }
188
+ out.push(`<blockquote>${renderBlocks(quoted)}</blockquote>`);
189
+ continue;
190
+ }
191
+ if (BULLET.test(line) || ORDERED.test(line)) {
192
+ const list = renderList(lines, i);
193
+ out.push(list.html);
194
+ i = list.next;
195
+ continue;
196
+ }
197
+ if (line.includes('|') && i + 1 < lines.length && TABLE_SEP.test(lines[i + 1]) && lines[i + 1].includes('-')) {
198
+ const header = splitRow(line);
199
+ const aligns = splitRow(lines[i + 1]).map((c) => (c.startsWith(':') && c.endsWith(':') ? 'center' : c.endsWith(':') ? 'right' : c.startsWith(':') ? 'left' : ''));
200
+ // Alineación con clases: el sanitizador de Angular elimina los atributos style.
201
+ const cell = (tag, text, col) => `<${tag}${aligns[col] ? ` class="cb-md-${aligns[col]}"` : ''}>${inline(text)}</${tag}>`;
202
+ const rows = [];
203
+ i += 2;
204
+ while (i < lines.length && lines[i].includes('|') && lines[i].trim() !== '') {
205
+ const cells = splitRow(lines[i]);
206
+ rows.push(`<tr>${header.map((_h, c) => cell('td', cells[c] ?? '', c)).join('')}</tr>`);
207
+ i++;
208
+ }
209
+ out.push(`<div class="cb-md-table"><table><thead><tr>${header.map((h, c) => cell('th', h, c)).join('')}</tr></thead>` +
210
+ `<tbody>${rows.join('')}</tbody></table></div>`);
211
+ continue;
212
+ }
213
+ // Párrafo: hasta una línea vacía o el inicio de otro bloque.
214
+ const para = [];
215
+ while (i < lines.length && lines[i].trim() !== '' && (para.length === 0 || !isBlockStart(lines[i])))
216
+ para.push(lines[i++].trim());
217
+ out.push(`<p>${inline(para.join('\n')).replace(/\n/g, '<br>')}</p>`);
218
+ }
219
+ const html = out.join('');
220
+ flush();
221
+ return html;
222
+ }
223
+ function toLines(markdown) {
224
+ return (markdown ?? '').replace(/\r\n?/g, '\n').split('\n');
225
+ }
226
+ /** Convierte markdown en HTML seguro (todo el HTML de entrada se escapa). */
227
+ function cbMarkdownToHtml(markdown) {
228
+ return renderBlocks(toLines(markdown));
229
+ }
230
+ /**
231
+ * Como `cbMarkdownToHtml`, pero separa los bloques de código del nivel
232
+ * superior para pintarlos con un componente (resaltado, copiar).
233
+ */
234
+ function cbMarkdownToBlocks(markdown) {
235
+ const blocks = [];
236
+ renderBlocks(toLines(markdown), blocks);
237
+ return blocks;
238
+ }
239
+
240
+ function formatSize$1(bytes) {
241
+ if (bytes === undefined)
242
+ return '';
243
+ if (bytes < 1024)
244
+ return `${bytes} B`;
245
+ if (bytes < 1024 * 1024)
246
+ return `${(bytes / 1024).toFixed(0)} KB`;
247
+ return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
248
+ }
249
+ /**
250
+ * Mensaje de chat de CaribeNG: burbuja con autor, hora, avatar y estado de
251
+ * envío. `text` se pinta como markdown seguro y sus bloques de código con
252
+ * `cb-code-block` (resaltado y copiar). Admite adjuntos, valoración 👍/👎 y
253
+ * "Regenerar"; con `streaming` muestra un cursor mientras llega la respuesta.
254
+ * El contenido proyectado (p. ej. `cb-chat-reasoning`, `cb-chat-tool`) va
255
+ * antes del texto.
256
+ *
257
+ * @example
258
+ * ```html
259
+ * <cb-chat-message role="user" text="¿Cuál es mi saldo?" time="10:30" />
260
+ * <cb-chat-message role="assistant" [text]="respuesta" [streaming]="generando" copyable showFeedback [(feedback)]="voto" />
261
+ * ```
262
+ */
263
+ class CbChatMessage {
264
+ constructor() {
265
+ /** Textos de interfaz en el idioma activo (`provideCaribeNG({ locale })`). */
266
+ this.t = injectCbTranslation();
267
+ this.role = input('assistant', /* @ts-ignore */
268
+ ...(ngDevMode ? [{ debugName: "role" }] : /* istanbul ignore next */ []));
269
+ /** Contenido del mensaje (markdown por defecto). */
270
+ this.text = input('', /* @ts-ignore */
271
+ ...(ngDevMode ? [{ debugName: "text" }] : /* istanbul ignore next */ []));
272
+ /** Interpreta `text` como markdown seguro. Con `false` se muestra tal cual. */
273
+ this.markdown = input(true, { ...(ngDevMode ? { debugName: "markdown" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
274
+ /** Nombre del autor. Por defecto, "Tú" o "Asistente". */
275
+ this.author = input(undefined, /* @ts-ignore */
276
+ ...(ngDevMode ? [{ debugName: "author" }] : /* istanbul ignore next */ []));
277
+ /** Hora o fecha ya formateada ("10:30"). */
278
+ this.time = input('', /* @ts-ignore */
279
+ ...(ngDevMode ? [{ debugName: "time" }] : /* istanbul ignore next */ []));
280
+ /** Imagen del avatar. Sin ella, iniciales (usuario) o un icono (asistente). */
281
+ this.avatar = input('', /* @ts-ignore */
282
+ ...(ngDevMode ? [{ debugName: "avatar" }] : /* istanbul ignore next */ []));
283
+ this.showAvatar = input(true, { ...(ngDevMode ? { debugName: "showAvatar" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
284
+ this.showAuthor = input(false, { ...(ngDevMode ? { debugName: "showAuthor" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
285
+ /** Estado de envío (mensajes propios). `error` muestra "Reintentar". */
286
+ this.status = input(undefined, /* @ts-ignore */
287
+ ...(ngDevMode ? [{ debugName: "status" }] : /* istanbul ignore next */ []));
288
+ /** La respuesta sigue llegando: muestra un cursor y marca el mensaje como ocupado. */
289
+ this.streaming = input(false, { ...(ngDevMode ? { debugName: "streaming" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
290
+ /** Botón para copiar el texto (cuando termina de llegar). */
291
+ this.copyable = input(false, { ...(ngDevMode ? { debugName: "copyable" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
292
+ /** Archivos adjuntos (las imágenes con URL, en miniatura). */
293
+ this.attachments = input([], /* @ts-ignore */
294
+ ...(ngDevMode ? [{ debugName: "attachments" }] : /* istanbul ignore next */ []));
295
+ /** Muestra los botones 👍 / 👎. */
296
+ this.showFeedback = input(false, { ...(ngDevMode ? { debugName: "showFeedback" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
297
+ /** Valoración elegida (bidireccional). Pulsar la misma la quita. */
298
+ this.feedback = model(null, /* @ts-ignore */
299
+ ...(ngDevMode ? [{ debugName: "feedback" }] : /* istanbul ignore next */ []));
300
+ /** Muestra el botón "Regenerar respuesta". */
301
+ this.regenerable = input(false, { ...(ngDevMode ? { debugName: "regenerable" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
302
+ /** Se pulsó "Reintentar" en un mensaje con error. */
303
+ this.retry = output();
304
+ /** Se pulsó "Regenerar respuesta". */
305
+ this.regenerate = output();
306
+ this.blocks = computed(() => cbMarkdownToBlocks(this.text()), /* @ts-ignore */
307
+ ...(ngDevMode ? [{ debugName: "blocks" }] : /* istanbul ignore next */ []));
308
+ this.authorName = computed(() => this.author() ?? (this.role() === 'user' ? this.t().chatYou : this.t().chatAssistant), /* @ts-ignore */
309
+ ...(ngDevMode ? [{ debugName: "authorName" }] : /* istanbul ignore next */ []));
310
+ this.initials = computed(() => this.authorName()
311
+ .split(/\s+/)
312
+ .filter(Boolean)
313
+ .slice(0, 2)
314
+ .map((w) => w.charAt(0).toUpperCase())
315
+ .join(''), /* @ts-ignore */
316
+ ...(ngDevMode ? [{ debugName: "initials" }] : /* istanbul ignore next */ []));
317
+ this.label = computed(() => {
318
+ if (this.role() === 'system')
319
+ return null;
320
+ return this.time() ? `${this.authorName()}, ${this.time()}` : this.authorName();
321
+ }, /* @ts-ignore */
322
+ ...(ngDevMode ? [{ debugName: "label" }] : /* istanbul ignore next */ []));
323
+ }
324
+ isImage(a) {
325
+ return !!a.type?.startsWith('image/');
326
+ }
327
+ size(a) {
328
+ return formatSize$1(a.size);
329
+ }
330
+ vote(value) {
331
+ this.feedback.set(this.feedback() === value ? null : value);
332
+ }
333
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.5", ngImport: i0, type: CbChatMessage, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
334
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.0.5", type: CbChatMessage, isStandalone: true, selector: "cb-chat-message", inputs: { role: { classPropertyName: "role", publicName: "role", isSignal: true, isRequired: false, transformFunction: null }, text: { classPropertyName: "text", publicName: "text", isSignal: true, isRequired: false, transformFunction: null }, markdown: { classPropertyName: "markdown", publicName: "markdown", isSignal: true, isRequired: false, transformFunction: null }, author: { classPropertyName: "author", publicName: "author", isSignal: true, isRequired: false, transformFunction: null }, time: { classPropertyName: "time", publicName: "time", isSignal: true, isRequired: false, transformFunction: null }, avatar: { classPropertyName: "avatar", publicName: "avatar", isSignal: true, isRequired: false, transformFunction: null }, showAvatar: { classPropertyName: "showAvatar", publicName: "showAvatar", isSignal: true, isRequired: false, transformFunction: null }, showAuthor: { classPropertyName: "showAuthor", publicName: "showAuthor", isSignal: true, isRequired: false, transformFunction: null }, status: { classPropertyName: "status", publicName: "status", isSignal: true, isRequired: false, transformFunction: null }, streaming: { classPropertyName: "streaming", publicName: "streaming", isSignal: true, isRequired: false, transformFunction: null }, copyable: { classPropertyName: "copyable", publicName: "copyable", isSignal: true, isRequired: false, transformFunction: null }, attachments: { classPropertyName: "attachments", publicName: "attachments", isSignal: true, isRequired: false, transformFunction: null }, showFeedback: { classPropertyName: "showFeedback", publicName: "showFeedback", isSignal: true, isRequired: false, transformFunction: null }, feedback: { classPropertyName: "feedback", publicName: "feedback", isSignal: true, isRequired: false, transformFunction: null }, regenerable: { classPropertyName: "regenerable", publicName: "regenerable", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { feedback: "feedbackChange", retry: "retry", regenerate: "regenerate" }, host: { attributes: { "role": "article" }, properties: { "class.cb-chat-message--user": "role() === 'user'", "class.cb-chat-message--assistant": "role() === 'assistant'", "class.cb-chat-message--system": "role() === 'system'", "class.cb-chat-message--error": "status() === 'error'", "attr.aria-label": "label()", "attr.aria-busy": "streaming() ? \"true\" : null" }, classAttribute: "cb-chat-message" }, ngImport: i0, template: `
335
+ @if (role() !== 'system' && showAvatar()) {
336
+ <span class="cb-chat-message__avatar" aria-hidden="true">
337
+ @if (avatar()) {
338
+ <img [src]="avatar()" alt="" />
339
+ } @else if (role() === 'assistant') {
340
+ <cb-icon name="sparkles" [size]="16" />
341
+ } @else {
342
+ {{ initials() }}
343
+ }
344
+ </span>
345
+ }
346
+ <div class="cb-chat-message__body">
347
+ @if (role() !== 'system' && (showAuthor() || time())) {
348
+ <div class="cb-chat-message__meta" aria-hidden="true">
349
+ @if (showAuthor()) {
350
+ <span class="cb-chat-message__author">{{ authorName() }}</span>
351
+ }
352
+ @if (time()) {
353
+ <span class="cb-chat-message__time">{{ time() }}</span>
354
+ }
355
+ </div>
356
+ }
357
+ <ng-content select="cb-chat-reasoning, cb-chat-tool, [cb-chat-message-before]" />
358
+ @if (attachments().length) {
359
+ <ul class="cb-chat-message__attachments" role="list" [attr.aria-label]="t().chatAttachments">
360
+ @for (a of attachments(); track $index) {
361
+ <li class="cb-chat-message__attachment">
362
+ @if (isImage(a) && a.url) {
363
+ <a class="cb-chat-message__thumb" [href]="a.url" target="_blank" rel="noopener noreferrer">
364
+ <img [src]="a.url" [alt]="a.name" loading="lazy" />
365
+ </a>
366
+ } @else {
367
+ <a class="cb-chat-message__file" [attr.href]="a.url || null" [attr.target]="a.url ? '_blank' : null" [attr.rel]="a.url ? 'noopener noreferrer' : null">
368
+ <cb-icon [name]="isImage(a) ? 'image' : 'file'" [size]="18" aria-hidden="true" />
369
+ <span class="cb-chat-message__file-text">
370
+ <span class="cb-chat-message__file-name">{{ a.name }}</span>
371
+ @if (a.size !== undefined) {
372
+ <span class="cb-chat-message__file-size">{{ size(a) }}</span>
373
+ }
374
+ </span>
375
+ </a>
376
+ }
377
+ </li>
378
+ }
379
+ </ul>
380
+ }
381
+ <!-- Sin texto ni contenido proyectado queda vacía y el CSS la oculta (:empty). -->
382
+ <div class="cb-chat-message__bubble">
383
+ @if (text()) {
384
+ @if (markdown()) {
385
+ @for (block of blocks(); track $index) {
386
+ @if (block.kind === 'html') {
387
+ <div class="cb-chat-message__markdown" [innerHTML]="block.html"></div>
388
+ } @else {
389
+ <cb-code-block class="cb-chat-message__code" [code]="block.code" [language]="block.language" />
390
+ }
391
+ }
392
+ } @else {
393
+ <p class="cb-chat-message__plain">{{ text() }}</p>
394
+ }
395
+ }
396
+ <ng-content />
397
+ @if (streaming()) {
398
+ <span class="cb-chat-message__caret" aria-hidden="true"></span>
399
+ }
400
+ </div>
401
+ <div class="cb-chat-message__footer">
402
+ @switch (status()) {
403
+ @case ('sending') {
404
+ <span class="cb-chat-message__status">{{ t().chatSending }}</span>
405
+ }
406
+ @case ('error') {
407
+ <span class="cb-chat-message__status cb-chat-message__status--error">
408
+ <cb-icon name="circle-alert" [size]="14" aria-hidden="true" /> {{ t().chatError }}
409
+ </span>
410
+ <button type="button" class="cb-chat-message__action" (click)="retry.emit()">
411
+ <cb-icon name="rotate-ccw" [size]="14" aria-hidden="true" /> {{ t().chatRetry }}
412
+ </button>
413
+ }
414
+ }
415
+ <ng-content select="[cb-chat-message-actions]" />
416
+ @if (!streaming()) {
417
+ @if (copyable() && text()) {
418
+ <cb-copy-button [value]="text()" size="sm" />
419
+ }
420
+ @if (showFeedback()) {
421
+ <button type="button" class="cb-chat-message__icon-btn" [attr.aria-pressed]="feedback() === 'up'" [attr.aria-label]="t().chatHelpful" (click)="vote('up')">
422
+ <svg viewBox="0 0 24 24" width="15" height="15" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M7 10v11" /><path d="M15 5.9 14 10h5.8a2 2 0 0 1 1.9 2.6l-2.3 7A2 2 0 0 1 17.5 21H7V10l4.6-7.5a1.9 1.9 0 0 1 3.4 1.5z" /></svg>
423
+ </button>
424
+ <button type="button" class="cb-chat-message__icon-btn" [attr.aria-pressed]="feedback() === 'down'" [attr.aria-label]="t().chatNotHelpful" (click)="vote('down')">
425
+ <svg viewBox="0 0 24 24" width="15" height="15" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M17 14V3" /><path d="M9 18.1 10 14H4.2a2 2 0 0 1-1.9-2.6l2.3-7A2 2 0 0 1 6.5 3H17v11l-4.6 7.5a1.9 1.9 0 0 1-3.4-1.5z" /></svg>
426
+ </button>
427
+ }
428
+ @if (regenerable()) {
429
+ <button type="button" class="cb-chat-message__icon-btn" [attr.aria-label]="t().chatRegenerate" (click)="regenerate.emit()">
430
+ <cb-icon name="refresh-cw" [size]="15" aria-hidden="true" />
431
+ </button>
432
+ }
433
+ }
434
+ </div>
435
+ </div>
436
+ `, isInline: true, styles: [":host{display:flex;align-items:flex-start;gap:.625rem;max-width:100%;color:var(--cb-color-text);font-family:var(--cb-font-sans)}:host(.cb-chat-message--user){flex-direction:row-reverse}:host(.cb-chat-message--system){justify-content:center}.cb-chat-message__avatar{display:inline-flex;flex:none;align-items:center;justify-content:center;width:2rem;height:2rem;overflow:hidden;border-radius:50%;background:var(--cb-color-surface-hover);color:var(--cb-color-text);font-size:.75rem;font-weight:var(--cb-font-weight-semibold, 600)}:host(.cb-chat-message--assistant) .cb-chat-message__avatar{background:color-mix(in srgb,var(--cb-color-primary) 14%,transparent);color:var(--cb-color-primary)}.cb-chat-message__avatar img{width:100%;height:100%;object-fit:cover}.cb-chat-message__body{display:flex;flex-direction:column;gap:.25rem;min-width:0;max-width:min(46rem,85%)}:host(.cb-chat-message--user) .cb-chat-message__body{align-items:flex-end}.cb-chat-message__meta{display:flex;gap:.5rem;color:var(--cb-color-text-muted);font-size:var(--cb-text-xs, .75rem)}.cb-chat-message__author{font-weight:var(--cb-font-weight-semibold, 600)}.cb-chat-message__bubble{max-width:100%;padding:.625rem .875rem;border-radius:var(--cb-radius-lg, 12px);border-top-left-radius:var(--cb-radius-sm, 4px);background:var(--cb-color-surface-subtle);font-size:var(--cb-text-sm, .875rem);line-height:1.55;overflow-wrap:anywhere}:host(.cb-chat-message--user) .cb-chat-message__bubble{border-top-left-radius:var(--cb-radius-lg, 12px);border-top-right-radius:var(--cb-radius-sm, 4px);background:var(--cb-color-primary);color:var(--cb-color-primary-contrast)}:host(.cb-chat-message--system) .cb-chat-message__bubble{padding:.25rem .75rem;border-radius:var(--cb-radius-full, 999px);background:transparent;color:var(--cb-color-text-muted);font-size:var(--cb-text-xs, .75rem);text-align:center}.cb-chat-message__bubble:empty{display:none}.cb-chat-message__bubble>.cb-chat-message__code{margin:.5rem 0}.cb-chat-message__bubble>.cb-chat-message__code:first-child{margin-top:0}.cb-chat-message__bubble>.cb-chat-message__code:last-child{margin-bottom:0}.cb-chat-message__attachments{display:flex;flex-wrap:wrap;gap:.5rem;margin:0;padding:0;list-style:none}:host(.cb-chat-message--user) .cb-chat-message__attachments{justify-content:flex-end}.cb-chat-message__thumb{display:block;overflow:hidden;border:1px solid var(--cb-color-border);border-radius:var(--cb-radius-md, 8px)}.cb-chat-message__thumb img{display:block;width:7.5rem;height:7.5rem;object-fit:cover}.cb-chat-message__file{display:inline-flex;align-items:center;gap:.5rem;max-width:16rem;padding:.5rem .75rem;border:1px solid var(--cb-color-border);border-radius:var(--cb-radius-md, 8px);background:var(--cb-color-surface-raised);color:inherit;text-decoration:none}a.cb-chat-message__file[href]:hover{background:var(--cb-color-surface-hover)}.cb-chat-message__thumb:focus-visible,.cb-chat-message__file:focus-visible,.cb-chat-message__icon-btn:focus-visible{outline:2px solid var(--cb-color-focus-ring);outline-offset:2px}.cb-chat-message__file-text{display:flex;flex-direction:column;min-width:0}.cb-chat-message__file-name{overflow:hidden;font-size:var(--cb-text-xs, .75rem);font-weight:var(--cb-font-weight-medium, 500);text-overflow:ellipsis;white-space:nowrap}.cb-chat-message__file-size{color:var(--cb-color-text-muted);font-size:.6875rem}.cb-chat-message__icon-btn{display:inline-flex;align-items:center;justify-content:center;width:1.75rem;height:1.75rem;padding:0;border:0;border-radius:var(--cb-radius-sm, 4px);background:transparent;color:var(--cb-color-text-muted);cursor:pointer}.cb-chat-message__icon-btn:hover{background:var(--cb-color-surface-hover);color:var(--cb-color-text)}.cb-chat-message__icon-btn[aria-pressed=true]{color:var(--cb-color-primary)}.cb-chat-message__icon-btn[aria-pressed=true] svg{fill:color-mix(in srgb,currentColor 22%,transparent)}:host(.cb-chat-message--error) .cb-chat-message__bubble{opacity:.75}.cb-chat-message__plain{margin:0;white-space:pre-wrap}.cb-chat-message__caret{display:inline-block;width:.5em;height:1em;margin-left:2px;vertical-align:text-bottom;background:currentColor;animation:cb-chat-caret 1s steps(1) infinite}@keyframes cb-chat-caret{50%{opacity:0}}.cb-chat-message__footer{display:flex;flex-wrap:wrap;align-items:center;gap:.5rem;min-height:0;color:var(--cb-color-text-muted);font-size:var(--cb-text-xs, .75rem)}.cb-chat-message__footer:empty{display:none}.cb-chat-message__status{display:inline-flex;align-items:center;gap:.25rem}.cb-chat-message__status--error{color:var(--cb-color-danger-700)}:host-context([data-theme=\"dark\"]) .cb-chat-message__status--error{color:var(--cb-color-danger-400)}.cb-chat-message__action{display:inline-flex;align-items:center;gap:.25rem;padding:.125rem .375rem;border:0;border-radius:var(--cb-radius-sm, 4px);background:transparent;color:var(--cb-color-text);font:inherit;text-decoration:underline;cursor:pointer}.cb-chat-message__action:focus-visible{outline:2px solid var(--cb-color-focus-ring);outline-offset:2px}.cb-chat-message__markdown>:first-child{margin-top:0}.cb-chat-message__markdown>:last-child{margin-bottom:0}.cb-chat-message__markdown ::ng-deep p,.cb-chat-message__markdown ::ng-deep ul,.cb-chat-message__markdown ::ng-deep ol,.cb-chat-message__markdown ::ng-deep blockquote,.cb-chat-message__markdown ::ng-deep pre,.cb-chat-message__markdown ::ng-deep .cb-md-table{margin:0 0 .625rem}.cb-chat-message__markdown ::ng-deep :is(p,ul,ol,pre,blockquote,.cb-md-table):last-child{margin-bottom:0}.cb-chat-message__markdown ::ng-deep :is(h1,h2,h3,h4,h5,h6){margin:.75rem 0 .375rem;font-size:1em;font-weight:var(--cb-font-weight-bold, 700)}.cb-chat-message__markdown ::ng-deep h1{font-size:1.2em}.cb-chat-message__markdown ::ng-deep h2{font-size:1.1em}.cb-chat-message__markdown ::ng-deep :is(ul,ol){padding-left:1.25rem}.cb-chat-message__markdown ::ng-deep li+li{margin-top:.125rem}.cb-chat-message__markdown ::ng-deep .cb-md-task{list-style:none;margin-left:-1.25rem}.cb-chat-message__markdown ::ng-deep a{color:inherit;text-decoration:underline}.cb-chat-message__markdown ::ng-deep code{padding:.0625rem .3125rem;border-radius:var(--cb-radius-sm, 4px);background:color-mix(in srgb,currentColor 10%,transparent);font-family:var(--cb-font-mono);font-size:.9em}.cb-chat-message__markdown ::ng-deep pre{position:relative;overflow-x:auto;padding:.75rem;border-radius:var(--cb-radius-md, 8px);background:var(--cb-color-surface-950, #0b1220);color:#e5e7eb}.cb-chat-message__markdown ::ng-deep pre code{padding:0;background:transparent;font-size:.85em;white-space:pre}.cb-chat-message__markdown ::ng-deep .cb-md-lang{display:block;margin-bottom:.375rem;color:#9ca3af;font-family:var(--cb-font-sans);font-size:.7rem;text-transform:uppercase}.cb-chat-message__markdown ::ng-deep blockquote{padding-left:.75rem;border-left:3px solid color-mix(in srgb,currentColor 30%,transparent);opacity:.9}.cb-chat-message__markdown ::ng-deep hr{border:0;border-top:1px solid color-mix(in srgb,currentColor 20%,transparent)}.cb-chat-message__markdown ::ng-deep .cb-md-table{overflow-x:auto}.cb-chat-message__markdown ::ng-deep table{border-collapse:collapse;font-size:.95em}.cb-chat-message__markdown ::ng-deep .cb-md-left{text-align:left}.cb-chat-message__markdown ::ng-deep .cb-md-center{text-align:center}.cb-chat-message__markdown ::ng-deep .cb-md-right{text-align:right}.cb-chat-message__markdown ::ng-deep .cb-md-check{margin-right:.125rem}.cb-chat-message__markdown ::ng-deep :is(th,td){padding:.25rem .625rem;border:1px solid color-mix(in srgb,currentColor 18%,transparent)}@media(prefers-reduced-motion:reduce){.cb-chat-message__caret{animation:none}}\n"], dependencies: [{ kind: "component", type: CbIcon, selector: "cb-icon", inputs: ["name", "size", "spin", "ariaLabel"] }, { kind: "component", type: CbCopyButton, selector: "cb-copy-button", inputs: ["value", "showLabel", "label", "copiedLabel", "timeout", "variant", "severity", "size", "disabled"], outputs: ["copied", "copyError"] }, { kind: "component", type: CbCodeBlock, selector: "cb-code-block", inputs: ["code", "language", "filename", "lineNumbers", "startLine", "highlightLines", "wrap", "copyable", "header", "maxHeight", "ariaLabel"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
437
+ }
438
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.5", ngImport: i0, type: CbChatMessage, decorators: [{
439
+ type: Component,
440
+ args: [{ selector: 'cb-chat-message', changeDetection: ChangeDetectionStrategy.OnPush, encapsulation: ViewEncapsulation.Emulated, imports: [CbIcon, CbCopyButton, CbCodeBlock], host: {
441
+ class: 'cb-chat-message',
442
+ role: 'article',
443
+ '[class.cb-chat-message--user]': "role() === 'user'",
444
+ '[class.cb-chat-message--assistant]': "role() === 'assistant'",
445
+ '[class.cb-chat-message--system]': "role() === 'system'",
446
+ '[class.cb-chat-message--error]': "status() === 'error'",
447
+ '[attr.aria-label]': 'label()',
448
+ '[attr.aria-busy]': 'streaming() ? "true" : null',
449
+ }, template: `
450
+ @if (role() !== 'system' && showAvatar()) {
451
+ <span class="cb-chat-message__avatar" aria-hidden="true">
452
+ @if (avatar()) {
453
+ <img [src]="avatar()" alt="" />
454
+ } @else if (role() === 'assistant') {
455
+ <cb-icon name="sparkles" [size]="16" />
456
+ } @else {
457
+ {{ initials() }}
458
+ }
459
+ </span>
460
+ }
461
+ <div class="cb-chat-message__body">
462
+ @if (role() !== 'system' && (showAuthor() || time())) {
463
+ <div class="cb-chat-message__meta" aria-hidden="true">
464
+ @if (showAuthor()) {
465
+ <span class="cb-chat-message__author">{{ authorName() }}</span>
466
+ }
467
+ @if (time()) {
468
+ <span class="cb-chat-message__time">{{ time() }}</span>
469
+ }
470
+ </div>
471
+ }
472
+ <ng-content select="cb-chat-reasoning, cb-chat-tool, [cb-chat-message-before]" />
473
+ @if (attachments().length) {
474
+ <ul class="cb-chat-message__attachments" role="list" [attr.aria-label]="t().chatAttachments">
475
+ @for (a of attachments(); track $index) {
476
+ <li class="cb-chat-message__attachment">
477
+ @if (isImage(a) && a.url) {
478
+ <a class="cb-chat-message__thumb" [href]="a.url" target="_blank" rel="noopener noreferrer">
479
+ <img [src]="a.url" [alt]="a.name" loading="lazy" />
480
+ </a>
481
+ } @else {
482
+ <a class="cb-chat-message__file" [attr.href]="a.url || null" [attr.target]="a.url ? '_blank' : null" [attr.rel]="a.url ? 'noopener noreferrer' : null">
483
+ <cb-icon [name]="isImage(a) ? 'image' : 'file'" [size]="18" aria-hidden="true" />
484
+ <span class="cb-chat-message__file-text">
485
+ <span class="cb-chat-message__file-name">{{ a.name }}</span>
486
+ @if (a.size !== undefined) {
487
+ <span class="cb-chat-message__file-size">{{ size(a) }}</span>
488
+ }
489
+ </span>
490
+ </a>
491
+ }
492
+ </li>
493
+ }
494
+ </ul>
495
+ }
496
+ <!-- Sin texto ni contenido proyectado queda vacía y el CSS la oculta (:empty). -->
497
+ <div class="cb-chat-message__bubble">
498
+ @if (text()) {
499
+ @if (markdown()) {
500
+ @for (block of blocks(); track $index) {
501
+ @if (block.kind === 'html') {
502
+ <div class="cb-chat-message__markdown" [innerHTML]="block.html"></div>
503
+ } @else {
504
+ <cb-code-block class="cb-chat-message__code" [code]="block.code" [language]="block.language" />
505
+ }
506
+ }
507
+ } @else {
508
+ <p class="cb-chat-message__plain">{{ text() }}</p>
509
+ }
510
+ }
511
+ <ng-content />
512
+ @if (streaming()) {
513
+ <span class="cb-chat-message__caret" aria-hidden="true"></span>
514
+ }
515
+ </div>
516
+ <div class="cb-chat-message__footer">
517
+ @switch (status()) {
518
+ @case ('sending') {
519
+ <span class="cb-chat-message__status">{{ t().chatSending }}</span>
520
+ }
521
+ @case ('error') {
522
+ <span class="cb-chat-message__status cb-chat-message__status--error">
523
+ <cb-icon name="circle-alert" [size]="14" aria-hidden="true" /> {{ t().chatError }}
524
+ </span>
525
+ <button type="button" class="cb-chat-message__action" (click)="retry.emit()">
526
+ <cb-icon name="rotate-ccw" [size]="14" aria-hidden="true" /> {{ t().chatRetry }}
527
+ </button>
528
+ }
529
+ }
530
+ <ng-content select="[cb-chat-message-actions]" />
531
+ @if (!streaming()) {
532
+ @if (copyable() && text()) {
533
+ <cb-copy-button [value]="text()" size="sm" />
534
+ }
535
+ @if (showFeedback()) {
536
+ <button type="button" class="cb-chat-message__icon-btn" [attr.aria-pressed]="feedback() === 'up'" [attr.aria-label]="t().chatHelpful" (click)="vote('up')">
537
+ <svg viewBox="0 0 24 24" width="15" height="15" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M7 10v11" /><path d="M15 5.9 14 10h5.8a2 2 0 0 1 1.9 2.6l-2.3 7A2 2 0 0 1 17.5 21H7V10l4.6-7.5a1.9 1.9 0 0 1 3.4 1.5z" /></svg>
538
+ </button>
539
+ <button type="button" class="cb-chat-message__icon-btn" [attr.aria-pressed]="feedback() === 'down'" [attr.aria-label]="t().chatNotHelpful" (click)="vote('down')">
540
+ <svg viewBox="0 0 24 24" width="15" height="15" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M17 14V3" /><path d="M9 18.1 10 14H4.2a2 2 0 0 1-1.9-2.6l2.3-7A2 2 0 0 1 6.5 3H17v11l-4.6 7.5a1.9 1.9 0 0 1-3.4-1.5z" /></svg>
541
+ </button>
542
+ }
543
+ @if (regenerable()) {
544
+ <button type="button" class="cb-chat-message__icon-btn" [attr.aria-label]="t().chatRegenerate" (click)="regenerate.emit()">
545
+ <cb-icon name="refresh-cw" [size]="15" aria-hidden="true" />
546
+ </button>
547
+ }
548
+ }
549
+ </div>
550
+ </div>
551
+ `, styles: [":host{display:flex;align-items:flex-start;gap:.625rem;max-width:100%;color:var(--cb-color-text);font-family:var(--cb-font-sans)}:host(.cb-chat-message--user){flex-direction:row-reverse}:host(.cb-chat-message--system){justify-content:center}.cb-chat-message__avatar{display:inline-flex;flex:none;align-items:center;justify-content:center;width:2rem;height:2rem;overflow:hidden;border-radius:50%;background:var(--cb-color-surface-hover);color:var(--cb-color-text);font-size:.75rem;font-weight:var(--cb-font-weight-semibold, 600)}:host(.cb-chat-message--assistant) .cb-chat-message__avatar{background:color-mix(in srgb,var(--cb-color-primary) 14%,transparent);color:var(--cb-color-primary)}.cb-chat-message__avatar img{width:100%;height:100%;object-fit:cover}.cb-chat-message__body{display:flex;flex-direction:column;gap:.25rem;min-width:0;max-width:min(46rem,85%)}:host(.cb-chat-message--user) .cb-chat-message__body{align-items:flex-end}.cb-chat-message__meta{display:flex;gap:.5rem;color:var(--cb-color-text-muted);font-size:var(--cb-text-xs, .75rem)}.cb-chat-message__author{font-weight:var(--cb-font-weight-semibold, 600)}.cb-chat-message__bubble{max-width:100%;padding:.625rem .875rem;border-radius:var(--cb-radius-lg, 12px);border-top-left-radius:var(--cb-radius-sm, 4px);background:var(--cb-color-surface-subtle);font-size:var(--cb-text-sm, .875rem);line-height:1.55;overflow-wrap:anywhere}:host(.cb-chat-message--user) .cb-chat-message__bubble{border-top-left-radius:var(--cb-radius-lg, 12px);border-top-right-radius:var(--cb-radius-sm, 4px);background:var(--cb-color-primary);color:var(--cb-color-primary-contrast)}:host(.cb-chat-message--system) .cb-chat-message__bubble{padding:.25rem .75rem;border-radius:var(--cb-radius-full, 999px);background:transparent;color:var(--cb-color-text-muted);font-size:var(--cb-text-xs, .75rem);text-align:center}.cb-chat-message__bubble:empty{display:none}.cb-chat-message__bubble>.cb-chat-message__code{margin:.5rem 0}.cb-chat-message__bubble>.cb-chat-message__code:first-child{margin-top:0}.cb-chat-message__bubble>.cb-chat-message__code:last-child{margin-bottom:0}.cb-chat-message__attachments{display:flex;flex-wrap:wrap;gap:.5rem;margin:0;padding:0;list-style:none}:host(.cb-chat-message--user) .cb-chat-message__attachments{justify-content:flex-end}.cb-chat-message__thumb{display:block;overflow:hidden;border:1px solid var(--cb-color-border);border-radius:var(--cb-radius-md, 8px)}.cb-chat-message__thumb img{display:block;width:7.5rem;height:7.5rem;object-fit:cover}.cb-chat-message__file{display:inline-flex;align-items:center;gap:.5rem;max-width:16rem;padding:.5rem .75rem;border:1px solid var(--cb-color-border);border-radius:var(--cb-radius-md, 8px);background:var(--cb-color-surface-raised);color:inherit;text-decoration:none}a.cb-chat-message__file[href]:hover{background:var(--cb-color-surface-hover)}.cb-chat-message__thumb:focus-visible,.cb-chat-message__file:focus-visible,.cb-chat-message__icon-btn:focus-visible{outline:2px solid var(--cb-color-focus-ring);outline-offset:2px}.cb-chat-message__file-text{display:flex;flex-direction:column;min-width:0}.cb-chat-message__file-name{overflow:hidden;font-size:var(--cb-text-xs, .75rem);font-weight:var(--cb-font-weight-medium, 500);text-overflow:ellipsis;white-space:nowrap}.cb-chat-message__file-size{color:var(--cb-color-text-muted);font-size:.6875rem}.cb-chat-message__icon-btn{display:inline-flex;align-items:center;justify-content:center;width:1.75rem;height:1.75rem;padding:0;border:0;border-radius:var(--cb-radius-sm, 4px);background:transparent;color:var(--cb-color-text-muted);cursor:pointer}.cb-chat-message__icon-btn:hover{background:var(--cb-color-surface-hover);color:var(--cb-color-text)}.cb-chat-message__icon-btn[aria-pressed=true]{color:var(--cb-color-primary)}.cb-chat-message__icon-btn[aria-pressed=true] svg{fill:color-mix(in srgb,currentColor 22%,transparent)}:host(.cb-chat-message--error) .cb-chat-message__bubble{opacity:.75}.cb-chat-message__plain{margin:0;white-space:pre-wrap}.cb-chat-message__caret{display:inline-block;width:.5em;height:1em;margin-left:2px;vertical-align:text-bottom;background:currentColor;animation:cb-chat-caret 1s steps(1) infinite}@keyframes cb-chat-caret{50%{opacity:0}}.cb-chat-message__footer{display:flex;flex-wrap:wrap;align-items:center;gap:.5rem;min-height:0;color:var(--cb-color-text-muted);font-size:var(--cb-text-xs, .75rem)}.cb-chat-message__footer:empty{display:none}.cb-chat-message__status{display:inline-flex;align-items:center;gap:.25rem}.cb-chat-message__status--error{color:var(--cb-color-danger-700)}:host-context([data-theme=\"dark\"]) .cb-chat-message__status--error{color:var(--cb-color-danger-400)}.cb-chat-message__action{display:inline-flex;align-items:center;gap:.25rem;padding:.125rem .375rem;border:0;border-radius:var(--cb-radius-sm, 4px);background:transparent;color:var(--cb-color-text);font:inherit;text-decoration:underline;cursor:pointer}.cb-chat-message__action:focus-visible{outline:2px solid var(--cb-color-focus-ring);outline-offset:2px}.cb-chat-message__markdown>:first-child{margin-top:0}.cb-chat-message__markdown>:last-child{margin-bottom:0}.cb-chat-message__markdown ::ng-deep p,.cb-chat-message__markdown ::ng-deep ul,.cb-chat-message__markdown ::ng-deep ol,.cb-chat-message__markdown ::ng-deep blockquote,.cb-chat-message__markdown ::ng-deep pre,.cb-chat-message__markdown ::ng-deep .cb-md-table{margin:0 0 .625rem}.cb-chat-message__markdown ::ng-deep :is(p,ul,ol,pre,blockquote,.cb-md-table):last-child{margin-bottom:0}.cb-chat-message__markdown ::ng-deep :is(h1,h2,h3,h4,h5,h6){margin:.75rem 0 .375rem;font-size:1em;font-weight:var(--cb-font-weight-bold, 700)}.cb-chat-message__markdown ::ng-deep h1{font-size:1.2em}.cb-chat-message__markdown ::ng-deep h2{font-size:1.1em}.cb-chat-message__markdown ::ng-deep :is(ul,ol){padding-left:1.25rem}.cb-chat-message__markdown ::ng-deep li+li{margin-top:.125rem}.cb-chat-message__markdown ::ng-deep .cb-md-task{list-style:none;margin-left:-1.25rem}.cb-chat-message__markdown ::ng-deep a{color:inherit;text-decoration:underline}.cb-chat-message__markdown ::ng-deep code{padding:.0625rem .3125rem;border-radius:var(--cb-radius-sm, 4px);background:color-mix(in srgb,currentColor 10%,transparent);font-family:var(--cb-font-mono);font-size:.9em}.cb-chat-message__markdown ::ng-deep pre{position:relative;overflow-x:auto;padding:.75rem;border-radius:var(--cb-radius-md, 8px);background:var(--cb-color-surface-950, #0b1220);color:#e5e7eb}.cb-chat-message__markdown ::ng-deep pre code{padding:0;background:transparent;font-size:.85em;white-space:pre}.cb-chat-message__markdown ::ng-deep .cb-md-lang{display:block;margin-bottom:.375rem;color:#9ca3af;font-family:var(--cb-font-sans);font-size:.7rem;text-transform:uppercase}.cb-chat-message__markdown ::ng-deep blockquote{padding-left:.75rem;border-left:3px solid color-mix(in srgb,currentColor 30%,transparent);opacity:.9}.cb-chat-message__markdown ::ng-deep hr{border:0;border-top:1px solid color-mix(in srgb,currentColor 20%,transparent)}.cb-chat-message__markdown ::ng-deep .cb-md-table{overflow-x:auto}.cb-chat-message__markdown ::ng-deep table{border-collapse:collapse;font-size:.95em}.cb-chat-message__markdown ::ng-deep .cb-md-left{text-align:left}.cb-chat-message__markdown ::ng-deep .cb-md-center{text-align:center}.cb-chat-message__markdown ::ng-deep .cb-md-right{text-align:right}.cb-chat-message__markdown ::ng-deep .cb-md-check{margin-right:.125rem}.cb-chat-message__markdown ::ng-deep :is(th,td){padding:.25rem .625rem;border:1px solid color-mix(in srgb,currentColor 18%,transparent)}@media(prefers-reduced-motion:reduce){.cb-chat-message__caret{animation:none}}\n"] }]
552
+ }], propDecorators: { role: [{ type: i0.Input, args: [{ isSignal: true, alias: "role", required: false }] }], text: [{ type: i0.Input, args: [{ isSignal: true, alias: "text", required: false }] }], markdown: [{ type: i0.Input, args: [{ isSignal: true, alias: "markdown", required: false }] }], author: [{ type: i0.Input, args: [{ isSignal: true, alias: "author", required: false }] }], time: [{ type: i0.Input, args: [{ isSignal: true, alias: "time", required: false }] }], avatar: [{ type: i0.Input, args: [{ isSignal: true, alias: "avatar", required: false }] }], showAvatar: [{ type: i0.Input, args: [{ isSignal: true, alias: "showAvatar", required: false }] }], showAuthor: [{ type: i0.Input, args: [{ isSignal: true, alias: "showAuthor", required: false }] }], status: [{ type: i0.Input, args: [{ isSignal: true, alias: "status", required: false }] }], streaming: [{ type: i0.Input, args: [{ isSignal: true, alias: "streaming", required: false }] }], copyable: [{ type: i0.Input, args: [{ isSignal: true, alias: "copyable", required: false }] }], attachments: [{ type: i0.Input, args: [{ isSignal: true, alias: "attachments", required: false }] }], showFeedback: [{ type: i0.Input, args: [{ isSignal: true, alias: "showFeedback", required: false }] }], feedback: [{ type: i0.Input, args: [{ isSignal: true, alias: "feedback", required: false }] }, { type: i0.Output, args: ["feedbackChange"] }], regenerable: [{ type: i0.Input, args: [{ isSignal: true, alias: "regenerable", required: false }] }], retry: [{ type: i0.Output, args: ["retry"] }], regenerate: [{ type: i0.Output, args: ["regenerate"] }] } });
553
+
554
+ /** Distancia (px) al final por debajo de la cual la lista sigue los mensajes nuevos. */
555
+ const PIN_THRESHOLD = 48;
556
+ /**
557
+ * Lista de mensajes de chat de CaribeNG (`role="log"`). Se mantiene pegada al
558
+ * final mientras llegan mensajes o se alarga una respuesta en streaming, pero
559
+ * no mueve el scroll si el usuario subió a leer: entonces ofrece "Ir al último
560
+ * mensaje". Con `typing` muestra el indicador de escritura.
561
+ *
562
+ * Accesibilidad: con `busy` (respuesta generándose) el log se marca
563
+ * `aria-busy` para que el lector no lea cada fragmento, y al terminar se
564
+ * anuncia "Respuesta lista".
565
+ *
566
+ * @example
567
+ * ```html
568
+ * <cb-chat-messages [busy]="generando()" [typing]="pensando()" style="height: 30rem">
569
+ * @for (m of mensajes(); track m.id) {
570
+ * <cb-chat-message [role]="m.role" [text]="m.text" />
571
+ * }
572
+ * </cb-chat-messages>
573
+ * ```
574
+ */
575
+ class CbChatMessages {
576
+ constructor() {
577
+ /** Textos de interfaz en el idioma activo (`provideCaribeNG({ locale })`). */
578
+ this.t = injectCbTranslation();
579
+ this.document = inject(DOCUMENT);
580
+ this.destroyRef = inject(DestroyRef);
581
+ this.scroller = viewChild.required('scroller');
582
+ this.content = viewChild.required('content');
583
+ /** Una respuesta se está generando (marca el log como ocupado). */
584
+ this.busy = input(false, { ...(ngDevMode ? { debugName: "busy" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
585
+ /** Muestra el indicador "Escribiendo…". */
586
+ this.typing = input(false, { ...(ngDevMode ? { debugName: "typing" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
587
+ /** Texto del indicador (p. ej. "Laura está escribiendo…"). */
588
+ this.typingLabel = input(undefined, /* @ts-ignore */
589
+ ...(ngDevMode ? [{ debugName: "typingLabel" }] : /* istanbul ignore next */ []));
590
+ /** Nombre del log. Por defecto, "Conversación". */
591
+ this.ariaLabel = input(undefined, /* @ts-ignore */
592
+ ...(ngDevMode ? [{ debugName: "ariaLabel" }] : /* istanbul ignore next */ []));
593
+ /** La vista está en el final (sigue los mensajes nuevos). */
594
+ this.pinned = signal(true, /* @ts-ignore */
595
+ ...(ngDevMode ? [{ debugName: "pinned" }] : /* istanbul ignore next */ []));
596
+ this.announcement = signal('', /* @ts-ignore */
597
+ ...(ngDevMode ? [{ debugName: "announcement" }] : /* istanbul ignore next */ []));
598
+ this.wasBusy = false;
599
+ afterNextRender(() => {
600
+ this.scrollToBottom(false);
601
+ const view = this.document.defaultView;
602
+ const follow = () => {
603
+ if (this.pinned())
604
+ this.scrollToBottom(false);
605
+ };
606
+ const content = this.content().nativeElement;
607
+ if (view?.ResizeObserver) {
608
+ const resize = new view.ResizeObserver(follow);
609
+ resize.observe(content);
610
+ this.destroyRef.onDestroy(() => resize.disconnect());
611
+ }
612
+ if (view?.MutationObserver) {
613
+ const mutation = new view.MutationObserver(follow);
614
+ mutation.observe(content, { childList: true, subtree: true, characterData: true });
615
+ this.destroyRef.onDestroy(() => mutation.disconnect());
616
+ }
617
+ });
618
+ // Al terminar una respuesta, se anuncia una sola vez (en vez de cada fragmento).
619
+ effect(() => {
620
+ const busy = this.busy();
621
+ untracked(() => {
622
+ if (this.wasBusy && !busy)
623
+ this.announcement.set(this.t().chatResponseReady);
624
+ else if (busy)
625
+ this.announcement.set('');
626
+ this.wasBusy = busy;
627
+ });
628
+ });
629
+ }
630
+ onScroll() {
631
+ const el = this.scroller().nativeElement;
632
+ this.pinned.set(el.scrollHeight - el.scrollTop - el.clientHeight <= PIN_THRESHOLD);
633
+ }
634
+ /** Lleva la vista al último mensaje y vuelve a seguir los nuevos. */
635
+ scrollToBottom(smooth = true) {
636
+ const el = this.scroller().nativeElement;
637
+ const reduce = this.document.defaultView?.matchMedia?.('(prefers-reduced-motion: reduce)').matches;
638
+ if (typeof el.scrollTo === 'function')
639
+ el.scrollTo({ top: el.scrollHeight, behavior: smooth && !reduce ? 'smooth' : 'auto' });
640
+ else
641
+ el.scrollTop = el.scrollHeight;
642
+ this.pinned.set(true);
643
+ }
644
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.5", ngImport: i0, type: CbChatMessages, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
645
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.0.5", type: CbChatMessages, isStandalone: true, selector: "cb-chat-messages", inputs: { busy: { classPropertyName: "busy", publicName: "busy", isSignal: true, isRequired: false, transformFunction: null }, typing: { classPropertyName: "typing", publicName: "typing", isSignal: true, isRequired: false, transformFunction: null }, typingLabel: { classPropertyName: "typingLabel", publicName: "typingLabel", isSignal: true, isRequired: false, transformFunction: null }, ariaLabel: { classPropertyName: "ariaLabel", publicName: "ariaLabel", isSignal: true, isRequired: false, transformFunction: null } }, host: { classAttribute: "cb-chat-messages" }, viewQueries: [{ propertyName: "scroller", first: true, predicate: ["scroller"], descendants: true, isSignal: true }, { propertyName: "content", first: true, predicate: ["content"], descendants: true, isSignal: true }], ngImport: i0, template: `
646
+ <div
647
+ #scroller
648
+ class="cb-chat-messages__scroller"
649
+ role="log"
650
+ tabindex="0"
651
+ [attr.aria-label]="ariaLabel() ?? t().chatConversation"
652
+ [attr.aria-busy]="busy() ? 'true' : null"
653
+ (scroll)="onScroll()"
654
+ >
655
+ <div #content class="cb-chat-messages__content">
656
+ <ng-content />
657
+ @if (typing()) {
658
+ <div class="cb-chat-messages__typing">
659
+ <span class="cb-chat-messages__dots" aria-hidden="true"><span></span><span></span><span></span></span>
660
+ <span class="cb-chat-messages__typing-label">{{ typingLabel() ?? t().chatTyping }}</span>
661
+ </div>
662
+ }
663
+ </div>
664
+ </div>
665
+ @if (!pinned()) {
666
+ <button type="button" class="cb-chat-messages__jump" (click)="scrollToBottom(true)">
667
+ <cb-icon name="arrow-down" [size]="16" aria-hidden="true" />
668
+ <span>{{ t().chatJumpToLatest }}</span>
669
+ </button>
670
+ }
671
+ <div class="cb-chat-messages__status" role="status">{{ announcement() }}</div>
672
+ `, isInline: true, styles: [":host{position:relative;display:flex;flex-direction:column;min-height:0}.cb-chat-messages__scroller{flex:1;min-height:0;overflow-y:auto;overscroll-behavior:contain}.cb-chat-messages__scroller:focus-visible{outline:2px solid var(--cb-color-focus-ring);outline-offset:-2px}.cb-chat-messages__content{display:flex;flex-direction:column;gap:1rem;padding:1rem}.cb-chat-messages__typing{display:flex;align-items:center;gap:.5rem;color:var(--cb-color-text-muted);font-size:var(--cb-text-xs, .75rem)}.cb-chat-messages__dots{display:inline-flex;gap:.25rem;padding:.625rem .75rem;border-radius:var(--cb-radius-lg, 12px);background:var(--cb-color-surface-subtle)}.cb-chat-messages__dots span{width:.375rem;height:.375rem;border-radius:50%;background:currentColor;animation:cb-chat-dot 1.2s ease-in-out infinite}.cb-chat-messages__dots span:nth-child(2){animation-delay:.15s}.cb-chat-messages__dots span:nth-child(3){animation-delay:.3s}@keyframes cb-chat-dot{0%,60%,to{opacity:.3;transform:none}30%{opacity:1;transform:translateY(-2px)}}.cb-chat-messages__jump{position:absolute;bottom:.75rem;left:50%;display:inline-flex;align-items:center;gap:.375rem;padding:.375rem .75rem;border:1px solid var(--cb-color-border);border-radius:var(--cb-radius-full, 999px);background:var(--cb-color-surface-raised);box-shadow:var(--cb-shadow-md);color:var(--cb-color-text);font:inherit;font-size:var(--cb-text-xs, .75rem);transform:translate(-50%);cursor:pointer}.cb-chat-messages__jump:focus-visible{outline:2px solid var(--cb-color-focus-ring);outline-offset:2px}.cb-chat-messages__status{position:absolute;width:1px;height:1px;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap}@media(prefers-reduced-motion:reduce){.cb-chat-messages__dots span{animation:none}}\n"], dependencies: [{ kind: "component", type: CbIcon, selector: "cb-icon", inputs: ["name", "size", "spin", "ariaLabel"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
673
+ }
674
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.5", ngImport: i0, type: CbChatMessages, decorators: [{
675
+ type: Component,
676
+ args: [{ selector: 'cb-chat-messages', changeDetection: ChangeDetectionStrategy.OnPush, encapsulation: ViewEncapsulation.Emulated, imports: [CbIcon], host: { class: 'cb-chat-messages' }, template: `
677
+ <div
678
+ #scroller
679
+ class="cb-chat-messages__scroller"
680
+ role="log"
681
+ tabindex="0"
682
+ [attr.aria-label]="ariaLabel() ?? t().chatConversation"
683
+ [attr.aria-busy]="busy() ? 'true' : null"
684
+ (scroll)="onScroll()"
685
+ >
686
+ <div #content class="cb-chat-messages__content">
687
+ <ng-content />
688
+ @if (typing()) {
689
+ <div class="cb-chat-messages__typing">
690
+ <span class="cb-chat-messages__dots" aria-hidden="true"><span></span><span></span><span></span></span>
691
+ <span class="cb-chat-messages__typing-label">{{ typingLabel() ?? t().chatTyping }}</span>
692
+ </div>
693
+ }
694
+ </div>
695
+ </div>
696
+ @if (!pinned()) {
697
+ <button type="button" class="cb-chat-messages__jump" (click)="scrollToBottom(true)">
698
+ <cb-icon name="arrow-down" [size]="16" aria-hidden="true" />
699
+ <span>{{ t().chatJumpToLatest }}</span>
700
+ </button>
701
+ }
702
+ <div class="cb-chat-messages__status" role="status">{{ announcement() }}</div>
703
+ `, styles: [":host{position:relative;display:flex;flex-direction:column;min-height:0}.cb-chat-messages__scroller{flex:1;min-height:0;overflow-y:auto;overscroll-behavior:contain}.cb-chat-messages__scroller:focus-visible{outline:2px solid var(--cb-color-focus-ring);outline-offset:-2px}.cb-chat-messages__content{display:flex;flex-direction:column;gap:1rem;padding:1rem}.cb-chat-messages__typing{display:flex;align-items:center;gap:.5rem;color:var(--cb-color-text-muted);font-size:var(--cb-text-xs, .75rem)}.cb-chat-messages__dots{display:inline-flex;gap:.25rem;padding:.625rem .75rem;border-radius:var(--cb-radius-lg, 12px);background:var(--cb-color-surface-subtle)}.cb-chat-messages__dots span{width:.375rem;height:.375rem;border-radius:50%;background:currentColor;animation:cb-chat-dot 1.2s ease-in-out infinite}.cb-chat-messages__dots span:nth-child(2){animation-delay:.15s}.cb-chat-messages__dots span:nth-child(3){animation-delay:.3s}@keyframes cb-chat-dot{0%,60%,to{opacity:.3;transform:none}30%{opacity:1;transform:translateY(-2px)}}.cb-chat-messages__jump{position:absolute;bottom:.75rem;left:50%;display:inline-flex;align-items:center;gap:.375rem;padding:.375rem .75rem;border:1px solid var(--cb-color-border);border-radius:var(--cb-radius-full, 999px);background:var(--cb-color-surface-raised);box-shadow:var(--cb-shadow-md);color:var(--cb-color-text);font:inherit;font-size:var(--cb-text-xs, .75rem);transform:translate(-50%);cursor:pointer}.cb-chat-messages__jump:focus-visible{outline:2px solid var(--cb-color-focus-ring);outline-offset:2px}.cb-chat-messages__status{position:absolute;width:1px;height:1px;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap}@media(prefers-reduced-motion:reduce){.cb-chat-messages__dots span{animation:none}}\n"] }]
704
+ }], ctorParameters: () => [], propDecorators: { scroller: [{ type: i0.ViewChild, args: ['scroller', { isSignal: true }] }], content: [{ type: i0.ViewChild, args: ['content', { isSignal: true }] }], busy: [{ type: i0.Input, args: [{ isSignal: true, alias: "busy", required: false }] }], typing: [{ type: i0.Input, args: [{ isSignal: true, alias: "typing", required: false }] }], typingLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "typingLabel", required: false }] }], ariaLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "ariaLabel", required: false }] }] } });
705
+
706
+ let nextUid$2 = 0;
707
+ function formatSize(bytes) {
708
+ if (bytes < 1024)
709
+ return `${bytes} B`;
710
+ if (bytes < 1024 * 1024)
711
+ return `${(bytes / 1024).toFixed(0)} KB`;
712
+ return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
713
+ }
714
+ /**
715
+ * Campo para escribir mensajes (prompt) de CaribeNG: crece con el texto hasta
716
+ * `maxRows`, envía con Enter (Shift + Enter = salto de línea, respetando la
717
+ * composición de teclados asiáticos), adjunta archivos (botón, pegar o
718
+ * arrastrar), muestra sugerencias y, mientras se genera una respuesta, cambia
719
+ * "Enviar" por "Detener".
720
+ *
721
+ * @example
722
+ * ```html
723
+ * <cb-chat-input [streaming]="generando()" attachments (send)="enviar($event)" (stop)="detener()" />
724
+ * ```
725
+ */
726
+ class CbChatInput {
727
+ constructor() {
728
+ /** Textos de interfaz en el idioma activo (`provideCaribeNG({ locale })`). */
729
+ this.t = injectCbTranslation();
730
+ this.fmt = cbFormat;
731
+ this.injector = inject(Injector);
732
+ this.textarea = viewChild.required('textarea');
733
+ this.inputId = `cb-chat-input-${++nextUid$2}`;
734
+ /** Texto escrito (bidireccional). */
735
+ this.value = model('', /* @ts-ignore */
736
+ ...(ngDevMode ? [{ debugName: "value" }] : /* istanbul ignore next */ []));
737
+ /** Archivos adjuntos (bidireccional). */
738
+ this.files = model([], /* @ts-ignore */
739
+ ...(ngDevMode ? [{ debugName: "files" }] : /* istanbul ignore next */ []));
740
+ this.placeholder = input(undefined, /* @ts-ignore */
741
+ ...(ngDevMode ? [{ debugName: "placeholder" }] : /* istanbul ignore next */ []));
742
+ /** Nombre del campo. Por defecto, "Mensaje". */
743
+ this.ariaLabel = input(undefined, /* @ts-ignore */
744
+ ...(ngDevMode ? [{ debugName: "ariaLabel" }] : /* istanbul ignore next */ []));
745
+ this.disabled = input(false, { ...(ngDevMode ? { debugName: "disabled" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
746
+ /** Se está generando una respuesta: muestra "Detener" y no deja enviar. */
747
+ this.streaming = input(false, { ...(ngDevMode ? { debugName: "streaming" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
748
+ /** Permite adjuntar archivos (botón, pegar o arrastrar). */
749
+ this.attachments = input(false, { ...(ngDevMode ? { debugName: "attachments" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
750
+ /** Tipos aceptados (`image/*,.pdf`). */
751
+ this.accept = input('', /* @ts-ignore */
752
+ ...(ngDevMode ? [{ debugName: "accept" }] : /* istanbul ignore next */ []));
753
+ this.multiple = input(true, { ...(ngDevMode ? { debugName: "multiple" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
754
+ /** Máximo de caracteres, con contador. 0 = sin límite. */
755
+ this.maxLength = input(0, /* @ts-ignore */
756
+ ...(ngDevMode ? [{ debugName: "maxLength" }] : /* istanbul ignore next */ []));
757
+ /** Filas máximas antes de hacer scroll. */
758
+ this.maxRows = input(8, /* @ts-ignore */
759
+ ...(ngDevMode ? [{ debugName: "maxRows" }] : /* istanbul ignore next */ []));
760
+ /** Enter envía (Shift + Enter, salto de línea). Con `false`, Ctrl/⌘ + Enter envía. */
761
+ this.submitOnEnter = input(true, { ...(ngDevMode ? { debugName: "submitOnEnter" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
762
+ /** Vacía el campo y los adjuntos al enviar. */
763
+ this.clearOnSend = input(true, { ...(ngDevMode ? { debugName: "clearOnSend" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
764
+ /** Sugerencias de inicio (se muestran con el campo vacío; al pulsar, se envían). */
765
+ this.suggestions = input([], /* @ts-ignore */
766
+ ...(ngDevMode ? [{ debugName: "suggestions" }] : /* istanbul ignore next */ []));
767
+ /** El usuario envió un mensaje. */
768
+ this.send = output();
769
+ /** El usuario pidió detener la respuesta. */
770
+ this.stop = output();
771
+ this.dragging = signal(false, /* @ts-ignore */
772
+ ...(ngDevMode ? [{ debugName: "dragging" }] : /* istanbul ignore next */ []));
773
+ this.canSend = computed(() => !this.disabled() && !this.streaming() && (this.value().trim().length > 0 || this.files().length > 0), /* @ts-ignore */
774
+ ...(ngDevMode ? [{ debugName: "canSend" }] : /* istanbul ignore next */ []));
775
+ /** URLs temporales de las miniaturas, por archivo. */
776
+ this.previews = new Map();
777
+ // Ajusta la altura cuando el valor cambia desde fuera (p. ej. al vaciarse tras enviar).
778
+ effect(() => {
779
+ this.value();
780
+ untracked(() => afterNextRender(() => this.resize(), { injector: this.injector }));
781
+ });
782
+ // Libera las miniaturas de los archivos que ya no están.
783
+ effect(() => {
784
+ const current = new Set(this.files());
785
+ untracked(() => {
786
+ for (const [file, url] of this.previews) {
787
+ if (!current.has(file)) {
788
+ URL.revokeObjectURL?.(url);
789
+ this.previews.delete(file);
790
+ }
791
+ }
792
+ });
793
+ });
794
+ inject(DestroyRef).onDestroy(() => {
795
+ for (const url of this.previews.values())
796
+ URL.revokeObjectURL?.(url);
797
+ this.previews.clear();
798
+ });
799
+ }
800
+ /** Miniatura de una imagen adjunta (`null` si no es imagen o el navegador no la admite). */
801
+ preview(file) {
802
+ if (!file.type.startsWith('image/') || typeof URL.createObjectURL !== 'function')
803
+ return null;
804
+ let url = this.previews.get(file);
805
+ if (!url) {
806
+ url = URL.createObjectURL(file);
807
+ this.previews.set(file, url);
808
+ }
809
+ return url;
810
+ }
811
+ /** Enfoca el campo. */
812
+ focus() {
813
+ this.textarea().nativeElement.focus();
814
+ }
815
+ /** Envía el mensaje si hay texto o adjuntos. */
816
+ submit() {
817
+ if (!this.canSend())
818
+ return;
819
+ this.send.emit({ text: this.value().trim(), files: [...this.files()] });
820
+ if (this.clearOnSend()) {
821
+ this.value.set('');
822
+ this.files.set([]);
823
+ }
824
+ }
825
+ size(file) {
826
+ return formatSize(file.size);
827
+ }
828
+ onInput(event) {
829
+ this.value.set(event.target.value);
830
+ this.resize();
831
+ }
832
+ onKeydown(event) {
833
+ if (event.key !== 'Enter' || event.isComposing || event.keyCode === 229)
834
+ return;
835
+ const mod = event.ctrlKey || event.metaKey;
836
+ const sends = this.submitOnEnter() ? !event.shiftKey : mod;
837
+ if (!sends)
838
+ return;
839
+ event.preventDefault();
840
+ this.submit();
841
+ }
842
+ useSuggestion(text) {
843
+ this.value.set(text);
844
+ this.submit();
845
+ }
846
+ onFilesChosen(event) {
847
+ const input = event.target;
848
+ this.addFiles(Array.from(input.files ?? []));
849
+ input.value = '';
850
+ }
851
+ onPaste(event) {
852
+ if (!this.attachments())
853
+ return;
854
+ const pasted = Array.from(event.clipboardData?.files ?? []);
855
+ if (!pasted.length)
856
+ return;
857
+ event.preventDefault();
858
+ this.addFiles(pasted);
859
+ }
860
+ onDragOver(event) {
861
+ if (!this.attachments() || this.disabled() || !event.dataTransfer?.types.includes('Files'))
862
+ return;
863
+ event.preventDefault();
864
+ this.dragging.set(true);
865
+ }
866
+ onDrop(event) {
867
+ this.dragging.set(false);
868
+ if (!this.attachments() || this.disabled())
869
+ return;
870
+ const dropped = Array.from(event.dataTransfer?.files ?? []);
871
+ if (!dropped.length)
872
+ return;
873
+ event.preventDefault();
874
+ this.addFiles(dropped);
875
+ }
876
+ removeFile(index) {
877
+ this.files.update((list) => list.filter((_f, i) => i !== index));
878
+ this.focus();
879
+ }
880
+ addFiles(incoming) {
881
+ const accepted = incoming.filter((file) => this.isAccepted(file));
882
+ if (!accepted.length)
883
+ return;
884
+ this.files.update((list) => (this.multiple() ? [...list, ...accepted] : [accepted[0]]));
885
+ }
886
+ isAccepted(file) {
887
+ const accept = this.accept().trim();
888
+ if (!accept)
889
+ return true;
890
+ return accept.split(',').some((rule) => {
891
+ const r = rule.trim().toLowerCase();
892
+ if (!r)
893
+ return false;
894
+ if (r.startsWith('.'))
895
+ return file.name.toLowerCase().endsWith(r);
896
+ if (r.endsWith('/*'))
897
+ return file.type.toLowerCase().startsWith(r.slice(0, -1));
898
+ return file.type.toLowerCase() === r;
899
+ });
900
+ }
901
+ resize() {
902
+ const el = this.textarea().nativeElement;
903
+ const style = el.ownerDocument.defaultView?.getComputedStyle(el);
904
+ const line = parseFloat(style?.lineHeight ?? '') || 20;
905
+ const padding = (parseFloat(style?.paddingTop ?? '') || 0) + (parseFloat(style?.paddingBottom ?? '') || 0);
906
+ el.style.height = 'auto';
907
+ const max = line * Math.max(1, this.maxRows()) + padding;
908
+ el.style.height = `${Math.min(el.scrollHeight, max)}px`;
909
+ el.style.overflowY = el.scrollHeight > max ? 'auto' : 'hidden';
910
+ }
911
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.5", ngImport: i0, type: CbChatInput, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
912
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.0.5", type: CbChatInput, isStandalone: true, selector: "cb-chat-input", inputs: { value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: false, transformFunction: null }, files: { classPropertyName: "files", publicName: "files", isSignal: true, isRequired: false, transformFunction: null }, placeholder: { classPropertyName: "placeholder", publicName: "placeholder", isSignal: true, isRequired: false, transformFunction: null }, ariaLabel: { classPropertyName: "ariaLabel", publicName: "ariaLabel", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, streaming: { classPropertyName: "streaming", publicName: "streaming", isSignal: true, isRequired: false, transformFunction: null }, attachments: { classPropertyName: "attachments", publicName: "attachments", isSignal: true, isRequired: false, transformFunction: null }, accept: { classPropertyName: "accept", publicName: "accept", isSignal: true, isRequired: false, transformFunction: null }, multiple: { classPropertyName: "multiple", publicName: "multiple", isSignal: true, isRequired: false, transformFunction: null }, maxLength: { classPropertyName: "maxLength", publicName: "maxLength", isSignal: true, isRequired: false, transformFunction: null }, maxRows: { classPropertyName: "maxRows", publicName: "maxRows", isSignal: true, isRequired: false, transformFunction: null }, submitOnEnter: { classPropertyName: "submitOnEnter", publicName: "submitOnEnter", isSignal: true, isRequired: false, transformFunction: null }, clearOnSend: { classPropertyName: "clearOnSend", publicName: "clearOnSend", isSignal: true, isRequired: false, transformFunction: null }, suggestions: { classPropertyName: "suggestions", publicName: "suggestions", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { value: "valueChange", files: "filesChange", send: "send", stop: "stop" }, host: { properties: { "class.cb-chat-input--disabled": "disabled()", "class.cb-chat-input--dragging": "dragging()" }, classAttribute: "cb-chat-input" }, viewQueries: [{ propertyName: "textarea", first: true, predicate: ["textarea"], descendants: true, isSignal: true }], ngImport: i0, template: `
913
+ @if (suggestions().length && !value() && !files().length) {
914
+ <div class="cb-chat-input__suggestions">
915
+ @for (s of suggestions(); track s) {
916
+ <button type="button" class="cb-chat-input__suggestion" [disabled]="disabled() || streaming()" (click)="useSuggestion(s)">{{ s }}</button>
917
+ }
918
+ </div>
919
+ }
920
+ <form
921
+ class="cb-chat-input__box"
922
+ (submit)="$event.preventDefault(); submit()"
923
+ (dragover)="onDragOver($event)"
924
+ (dragleave)="dragging.set(false)"
925
+ (drop)="onDrop($event)"
926
+ >
927
+ @if (files().length) {
928
+ <ul class="cb-chat-input__files" role="list">
929
+ @for (file of files(); track $index) {
930
+ <li class="cb-chat-input__file">
931
+ @if (preview(file); as src) {
932
+ <img class="cb-chat-input__file-thumb" [src]="src" alt="" />
933
+ } @else {
934
+ <cb-icon [name]="file.type.startsWith('image/') ? 'image' : 'file'" [size]="16" aria-hidden="true" />
935
+ }
936
+ <span class="cb-chat-input__file-name">{{ file.name }}</span>
937
+ <span class="cb-chat-input__file-size">{{ size(file) }}</span>
938
+ <button type="button" class="cb-chat-input__file-remove" [attr.aria-label]="fmt(t().removeItem, file.name)" (click)="removeFile($index)">
939
+ <cb-icon name="x" [size]="14" aria-hidden="true" />
940
+ </button>
941
+ </li>
942
+ }
943
+ </ul>
944
+ }
945
+ <div class="cb-chat-input__row">
946
+ @if (attachments()) {
947
+ <button type="button" class="cb-chat-input__icon-btn" [attr.aria-label]="t().chatAttach" [disabled]="disabled()" (click)="fileInput.click()">
948
+ <cb-icon name="plus" [size]="20" aria-hidden="true" />
949
+ </button>
950
+ <input #fileInput type="file" hidden [attr.accept]="accept() || null" [multiple]="multiple()" (change)="onFilesChosen($event)" />
951
+ }
952
+ <textarea
953
+ #textarea
954
+ class="cb-chat-input__textarea"
955
+ rows="1"
956
+ [id]="inputId"
957
+ [value]="value()"
958
+ [placeholder]="placeholder() ?? t().chatPlaceholder"
959
+ [disabled]="disabled()"
960
+ [attr.maxlength]="maxLength() || null"
961
+ [attr.aria-label]="ariaLabel() ?? t().chatMessage"
962
+ [attr.aria-describedby]="maxLength() ? inputId + '-count' : null"
963
+ (input)="onInput($event)"
964
+ (keydown)="onKeydown($event)"
965
+ (paste)="onPaste($event)"
966
+ ></textarea>
967
+ @if (streaming()) {
968
+ <button type="button" class="cb-chat-input__send cb-chat-input__send--stop" [attr.aria-label]="t().chatStop" (click)="stop.emit()">
969
+ <svg viewBox="0 0 24 24" width="14" height="14" aria-hidden="true"><rect x="5" y="5" width="14" height="14" rx="2" fill="currentColor" /></svg>
970
+ </button>
971
+ } @else {
972
+ <button type="submit" class="cb-chat-input__send" [attr.aria-label]="t().chatSend" [disabled]="!canSend()">
973
+ <cb-icon name="arrow-up" [size]="18" aria-hidden="true" />
974
+ </button>
975
+ }
976
+ </div>
977
+ </form>
978
+ @if (maxLength()) {
979
+ <div class="cb-chat-input__count" [id]="inputId + '-count'">{{ value().length }} / {{ maxLength() }}</div>
980
+ }
981
+ `, isInline: true, styles: [":host{display:flex;flex-direction:column;gap:.5rem;color:var(--cb-color-text);font-family:var(--cb-font-sans)}.cb-chat-input__suggestions{display:flex;flex-wrap:wrap;gap:.5rem}.cb-chat-input__suggestion{padding:.375rem .75rem;border:1px solid var(--cb-color-border);border-radius:var(--cb-radius-full, 999px);background:var(--cb-color-surface-raised);color:inherit;font:inherit;font-size:var(--cb-text-sm, .875rem);cursor:pointer}.cb-chat-input__suggestion:hover:not(:disabled){background:var(--cb-color-surface-hover)}.cb-chat-input__suggestion:focus-visible,.cb-chat-input__icon-btn:focus-visible,.cb-chat-input__send:focus-visible,.cb-chat-input__file-remove:focus-visible{outline:2px solid var(--cb-color-focus-ring);outline-offset:2px}.cb-chat-input__box{display:flex;flex-direction:column;gap:.5rem;margin:0;padding:.5rem;border:1px solid var(--cb-color-border-strong);border-radius:var(--cb-radius-lg, 12px);background:var(--cb-color-surface-raised);transition:border-color var(--cb-duration-fast, .15s) ease}.cb-chat-input__box:focus-within{border-color:var(--cb-color-primary);box-shadow:0 0 0 3px color-mix(in srgb,var(--cb-color-primary) 18%,transparent)}:host(.cb-chat-input--dragging) .cb-chat-input__box{border-style:dashed;border-color:var(--cb-color-primary);background:color-mix(in srgb,var(--cb-color-primary) 6%,var(--cb-color-surface-raised))}:host(.cb-chat-input--disabled) .cb-chat-input__box{opacity:.6}.cb-chat-input__files{display:flex;flex-wrap:wrap;gap:.375rem;margin:0;padding:0;list-style:none}.cb-chat-input__file{display:inline-flex;align-items:center;gap:.375rem;max-width:16rem;padding:.25rem .25rem .25rem .5rem;border-radius:var(--cb-radius-md, 8px);background:var(--cb-color-surface-subtle);font-size:var(--cb-text-xs, .75rem)}.cb-chat-input__file-thumb{flex:none;width:1.75rem;height:1.75rem;border-radius:var(--cb-radius-sm, 4px);object-fit:cover}.cb-chat-input__file-name{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.cb-chat-input__file-size{flex:none;color:var(--cb-color-text-muted)}.cb-chat-input__file-remove{display:inline-flex;flex:none;padding:.125rem;border:0;border-radius:var(--cb-radius-sm, 4px);background:transparent;color:var(--cb-color-text-muted);cursor:pointer}.cb-chat-input__row{display:flex;align-items:flex-end;gap:.375rem}.cb-chat-input__textarea{flex:1;min-width:0;max-height:none;margin:0;padding:.375rem .25rem;border:0;outline:none;resize:none;overflow-y:hidden;background:transparent;color:inherit;font:inherit;font-size:var(--cb-text-sm, .875rem);line-height:1.5}.cb-chat-input__textarea::placeholder{color:var(--cb-color-text-muted)}.cb-chat-input__icon-btn,.cb-chat-input__send{display:inline-flex;flex:none;align-items:center;justify-content:center;width:2.25rem;height:2.25rem;border:0;border-radius:50%;cursor:pointer}.cb-chat-input__icon-btn{background:transparent;color:var(--cb-color-text-muted)}.cb-chat-input__icon-btn:hover:not(:disabled){background:var(--cb-color-surface-hover);color:var(--cb-color-text)}.cb-chat-input__send{background:var(--cb-color-primary);color:var(--cb-color-primary-contrast)}.cb-chat-input__send:disabled{background:var(--cb-color-surface-hover);color:var(--cb-color-text-muted);cursor:not-allowed}.cb-chat-input__send--stop{background:var(--cb-color-text-strong);color:var(--cb-color-surface-raised)}.cb-chat-input__count{align-self:flex-end;color:var(--cb-color-text-muted);font-size:var(--cb-text-xs, .75rem)}\n"], dependencies: [{ kind: "component", type: CbIcon, selector: "cb-icon", inputs: ["name", "size", "spin", "ariaLabel"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
982
+ }
983
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.5", ngImport: i0, type: CbChatInput, decorators: [{
984
+ type: Component,
985
+ args: [{ selector: 'cb-chat-input', changeDetection: ChangeDetectionStrategy.OnPush, encapsulation: ViewEncapsulation.Emulated, imports: [CbIcon], host: {
986
+ class: 'cb-chat-input',
987
+ '[class.cb-chat-input--disabled]': 'disabled()',
988
+ '[class.cb-chat-input--dragging]': 'dragging()',
989
+ }, template: `
990
+ @if (suggestions().length && !value() && !files().length) {
991
+ <div class="cb-chat-input__suggestions">
992
+ @for (s of suggestions(); track s) {
993
+ <button type="button" class="cb-chat-input__suggestion" [disabled]="disabled() || streaming()" (click)="useSuggestion(s)">{{ s }}</button>
994
+ }
995
+ </div>
996
+ }
997
+ <form
998
+ class="cb-chat-input__box"
999
+ (submit)="$event.preventDefault(); submit()"
1000
+ (dragover)="onDragOver($event)"
1001
+ (dragleave)="dragging.set(false)"
1002
+ (drop)="onDrop($event)"
1003
+ >
1004
+ @if (files().length) {
1005
+ <ul class="cb-chat-input__files" role="list">
1006
+ @for (file of files(); track $index) {
1007
+ <li class="cb-chat-input__file">
1008
+ @if (preview(file); as src) {
1009
+ <img class="cb-chat-input__file-thumb" [src]="src" alt="" />
1010
+ } @else {
1011
+ <cb-icon [name]="file.type.startsWith('image/') ? 'image' : 'file'" [size]="16" aria-hidden="true" />
1012
+ }
1013
+ <span class="cb-chat-input__file-name">{{ file.name }}</span>
1014
+ <span class="cb-chat-input__file-size">{{ size(file) }}</span>
1015
+ <button type="button" class="cb-chat-input__file-remove" [attr.aria-label]="fmt(t().removeItem, file.name)" (click)="removeFile($index)">
1016
+ <cb-icon name="x" [size]="14" aria-hidden="true" />
1017
+ </button>
1018
+ </li>
1019
+ }
1020
+ </ul>
1021
+ }
1022
+ <div class="cb-chat-input__row">
1023
+ @if (attachments()) {
1024
+ <button type="button" class="cb-chat-input__icon-btn" [attr.aria-label]="t().chatAttach" [disabled]="disabled()" (click)="fileInput.click()">
1025
+ <cb-icon name="plus" [size]="20" aria-hidden="true" />
1026
+ </button>
1027
+ <input #fileInput type="file" hidden [attr.accept]="accept() || null" [multiple]="multiple()" (change)="onFilesChosen($event)" />
1028
+ }
1029
+ <textarea
1030
+ #textarea
1031
+ class="cb-chat-input__textarea"
1032
+ rows="1"
1033
+ [id]="inputId"
1034
+ [value]="value()"
1035
+ [placeholder]="placeholder() ?? t().chatPlaceholder"
1036
+ [disabled]="disabled()"
1037
+ [attr.maxlength]="maxLength() || null"
1038
+ [attr.aria-label]="ariaLabel() ?? t().chatMessage"
1039
+ [attr.aria-describedby]="maxLength() ? inputId + '-count' : null"
1040
+ (input)="onInput($event)"
1041
+ (keydown)="onKeydown($event)"
1042
+ (paste)="onPaste($event)"
1043
+ ></textarea>
1044
+ @if (streaming()) {
1045
+ <button type="button" class="cb-chat-input__send cb-chat-input__send--stop" [attr.aria-label]="t().chatStop" (click)="stop.emit()">
1046
+ <svg viewBox="0 0 24 24" width="14" height="14" aria-hidden="true"><rect x="5" y="5" width="14" height="14" rx="2" fill="currentColor" /></svg>
1047
+ </button>
1048
+ } @else {
1049
+ <button type="submit" class="cb-chat-input__send" [attr.aria-label]="t().chatSend" [disabled]="!canSend()">
1050
+ <cb-icon name="arrow-up" [size]="18" aria-hidden="true" />
1051
+ </button>
1052
+ }
1053
+ </div>
1054
+ </form>
1055
+ @if (maxLength()) {
1056
+ <div class="cb-chat-input__count" [id]="inputId + '-count'">{{ value().length }} / {{ maxLength() }}</div>
1057
+ }
1058
+ `, styles: [":host{display:flex;flex-direction:column;gap:.5rem;color:var(--cb-color-text);font-family:var(--cb-font-sans)}.cb-chat-input__suggestions{display:flex;flex-wrap:wrap;gap:.5rem}.cb-chat-input__suggestion{padding:.375rem .75rem;border:1px solid var(--cb-color-border);border-radius:var(--cb-radius-full, 999px);background:var(--cb-color-surface-raised);color:inherit;font:inherit;font-size:var(--cb-text-sm, .875rem);cursor:pointer}.cb-chat-input__suggestion:hover:not(:disabled){background:var(--cb-color-surface-hover)}.cb-chat-input__suggestion:focus-visible,.cb-chat-input__icon-btn:focus-visible,.cb-chat-input__send:focus-visible,.cb-chat-input__file-remove:focus-visible{outline:2px solid var(--cb-color-focus-ring);outline-offset:2px}.cb-chat-input__box{display:flex;flex-direction:column;gap:.5rem;margin:0;padding:.5rem;border:1px solid var(--cb-color-border-strong);border-radius:var(--cb-radius-lg, 12px);background:var(--cb-color-surface-raised);transition:border-color var(--cb-duration-fast, .15s) ease}.cb-chat-input__box:focus-within{border-color:var(--cb-color-primary);box-shadow:0 0 0 3px color-mix(in srgb,var(--cb-color-primary) 18%,transparent)}:host(.cb-chat-input--dragging) .cb-chat-input__box{border-style:dashed;border-color:var(--cb-color-primary);background:color-mix(in srgb,var(--cb-color-primary) 6%,var(--cb-color-surface-raised))}:host(.cb-chat-input--disabled) .cb-chat-input__box{opacity:.6}.cb-chat-input__files{display:flex;flex-wrap:wrap;gap:.375rem;margin:0;padding:0;list-style:none}.cb-chat-input__file{display:inline-flex;align-items:center;gap:.375rem;max-width:16rem;padding:.25rem .25rem .25rem .5rem;border-radius:var(--cb-radius-md, 8px);background:var(--cb-color-surface-subtle);font-size:var(--cb-text-xs, .75rem)}.cb-chat-input__file-thumb{flex:none;width:1.75rem;height:1.75rem;border-radius:var(--cb-radius-sm, 4px);object-fit:cover}.cb-chat-input__file-name{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.cb-chat-input__file-size{flex:none;color:var(--cb-color-text-muted)}.cb-chat-input__file-remove{display:inline-flex;flex:none;padding:.125rem;border:0;border-radius:var(--cb-radius-sm, 4px);background:transparent;color:var(--cb-color-text-muted);cursor:pointer}.cb-chat-input__row{display:flex;align-items:flex-end;gap:.375rem}.cb-chat-input__textarea{flex:1;min-width:0;max-height:none;margin:0;padding:.375rem .25rem;border:0;outline:none;resize:none;overflow-y:hidden;background:transparent;color:inherit;font:inherit;font-size:var(--cb-text-sm, .875rem);line-height:1.5}.cb-chat-input__textarea::placeholder{color:var(--cb-color-text-muted)}.cb-chat-input__icon-btn,.cb-chat-input__send{display:inline-flex;flex:none;align-items:center;justify-content:center;width:2.25rem;height:2.25rem;border:0;border-radius:50%;cursor:pointer}.cb-chat-input__icon-btn{background:transparent;color:var(--cb-color-text-muted)}.cb-chat-input__icon-btn:hover:not(:disabled){background:var(--cb-color-surface-hover);color:var(--cb-color-text)}.cb-chat-input__send{background:var(--cb-color-primary);color:var(--cb-color-primary-contrast)}.cb-chat-input__send:disabled{background:var(--cb-color-surface-hover);color:var(--cb-color-text-muted);cursor:not-allowed}.cb-chat-input__send--stop{background:var(--cb-color-text-strong);color:var(--cb-color-surface-raised)}.cb-chat-input__count{align-self:flex-end;color:var(--cb-color-text-muted);font-size:var(--cb-text-xs, .75rem)}\n"] }]
1059
+ }], ctorParameters: () => [], propDecorators: { textarea: [{ type: i0.ViewChild, args: ['textarea', { isSignal: true }] }], value: [{ type: i0.Input, args: [{ isSignal: true, alias: "value", required: false }] }, { type: i0.Output, args: ["valueChange"] }], files: [{ type: i0.Input, args: [{ isSignal: true, alias: "files", required: false }] }, { type: i0.Output, args: ["filesChange"] }], placeholder: [{ type: i0.Input, args: [{ isSignal: true, alias: "placeholder", required: false }] }], ariaLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "ariaLabel", required: false }] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], streaming: [{ type: i0.Input, args: [{ isSignal: true, alias: "streaming", required: false }] }], attachments: [{ type: i0.Input, args: [{ isSignal: true, alias: "attachments", required: false }] }], accept: [{ type: i0.Input, args: [{ isSignal: true, alias: "accept", required: false }] }], multiple: [{ type: i0.Input, args: [{ isSignal: true, alias: "multiple", required: false }] }], maxLength: [{ type: i0.Input, args: [{ isSignal: true, alias: "maxLength", required: false }] }], maxRows: [{ type: i0.Input, args: [{ isSignal: true, alias: "maxRows", required: false }] }], submitOnEnter: [{ type: i0.Input, args: [{ isSignal: true, alias: "submitOnEnter", required: false }] }], clearOnSend: [{ type: i0.Input, args: [{ isSignal: true, alias: "clearOnSend", required: false }] }], suggestions: [{ type: i0.Input, args: [{ isSignal: true, alias: "suggestions", required: false }] }], send: [{ type: i0.Output, args: ["send"] }], stop: [{ type: i0.Output, args: ["stop"] }] } });
1060
+
1061
+ let nextUid$1 = 0;
1062
+ /**
1063
+ * Razonamiento del modelo ("Pensando…"), plegable. Mientras llega se abre y
1064
+ * muestra un brillo; al terminar se pliega solo (`autoCollapse`) y resume
1065
+ * "Razonó durante N s". El texto admite markdown seguro.
1066
+ *
1067
+ * @example
1068
+ * ```html
1069
+ * <cb-chat-reasoning [text]="m.reasoning" [streaming]="m.pensando" [duration]="m.segundos" />
1070
+ * ```
1071
+ */
1072
+ class CbChatReasoning {
1073
+ constructor() {
1074
+ /** Textos de interfaz en el idioma activo (`provideCaribeNG({ locale })`). */
1075
+ this.t = injectCbTranslation();
1076
+ this.uid = `cb-chat-reasoning-${++nextUid$1}`;
1077
+ /** Texto del razonamiento (markdown). */
1078
+ this.text = input('', /* @ts-ignore */
1079
+ ...(ngDevMode ? [{ debugName: "text" }] : /* istanbul ignore next */ []));
1080
+ /** El razonamiento sigue llegando. */
1081
+ this.streaming = input(false, { ...(ngDevMode ? { debugName: "streaming" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
1082
+ /** Segundos que duró (se muestra al terminar). */
1083
+ this.duration = input(undefined, /* @ts-ignore */
1084
+ ...(ngDevMode ? [{ debugName: "duration" }] : /* istanbul ignore next */ []));
1085
+ /** Desplegado. */
1086
+ this.open = model(false, /* @ts-ignore */
1087
+ ...(ngDevMode ? [{ debugName: "open" }] : /* istanbul ignore next */ []));
1088
+ /** Se abre mientras llega y se pliega al terminar. */
1089
+ this.autoCollapse = input(true, { ...(ngDevMode ? { debugName: "autoCollapse" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
1090
+ this.html = computed(() => cbMarkdownToHtml(this.text()), /* @ts-ignore */
1091
+ ...(ngDevMode ? [{ debugName: "html" }] : /* istanbul ignore next */ []));
1092
+ this.summary = computed(() => {
1093
+ if (this.streaming())
1094
+ return this.t().chatThinking;
1095
+ const seconds = this.duration();
1096
+ return seconds !== undefined ? cbFormat(this.t().chatThoughtFor, String(Math.max(1, Math.round(seconds)))) : this.t().chatReasoning;
1097
+ }, /* @ts-ignore */
1098
+ ...(ngDevMode ? [{ debugName: "summary" }] : /* istanbul ignore next */ []));
1099
+ effect(() => {
1100
+ const streaming = this.streaming();
1101
+ untracked(() => {
1102
+ if (this.autoCollapse())
1103
+ this.open.set(streaming);
1104
+ });
1105
+ });
1106
+ }
1107
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.5", ngImport: i0, type: CbChatReasoning, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
1108
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.0.5", type: CbChatReasoning, isStandalone: true, selector: "cb-chat-reasoning", inputs: { text: { classPropertyName: "text", publicName: "text", isSignal: true, isRequired: false, transformFunction: null }, streaming: { classPropertyName: "streaming", publicName: "streaming", isSignal: true, isRequired: false, transformFunction: null }, duration: { classPropertyName: "duration", publicName: "duration", isSignal: true, isRequired: false, transformFunction: null }, open: { classPropertyName: "open", publicName: "open", isSignal: true, isRequired: false, transformFunction: null }, autoCollapse: { classPropertyName: "autoCollapse", publicName: "autoCollapse", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { open: "openChange" }, host: { properties: { "class.cb-chat-reasoning--streaming": "streaming()" }, classAttribute: "cb-chat-reasoning" }, ngImport: i0, template: `
1109
+ <button
1110
+ type="button"
1111
+ class="cb-chat-reasoning__toggle"
1112
+ [attr.aria-expanded]="open()"
1113
+ [attr.aria-controls]="uid"
1114
+ (click)="open.set(!open())"
1115
+ >
1116
+ <cb-icon name="sparkles" [size]="14" aria-hidden="true" />
1117
+ <span class="cb-chat-reasoning__label">{{ summary() }}</span>
1118
+ <cb-icon class="cb-chat-reasoning__chevron" name="chevron-down" [size]="14" aria-hidden="true" />
1119
+ </button>
1120
+ @if (open()) {
1121
+ <div class="cb-chat-reasoning__body" [id]="uid" role="region" [attr.aria-label]="t().chatReasoning" [attr.aria-busy]="streaming() ? 'true' : null">
1122
+ @if (text()) {
1123
+ <div class="cb-chat-reasoning__markdown" [innerHTML]="html()"></div>
1124
+ }
1125
+ <ng-content />
1126
+ </div>
1127
+ }
1128
+ `, isInline: true, styles: [":host{display:block;max-width:100%;color:var(--cb-color-text-muted);font-family:var(--cb-font-sans);font-size:var(--cb-text-sm, .875rem)}.cb-chat-reasoning__toggle{display:inline-flex;align-items:center;gap:.375rem;padding:.25rem .5rem .25rem .25rem;border:0;border-radius:var(--cb-radius-sm, 4px);background:transparent;color:inherit;font:inherit;cursor:pointer}.cb-chat-reasoning__toggle:hover{color:var(--cb-color-text)}.cb-chat-reasoning__toggle:focus-visible{outline:2px solid var(--cb-color-focus-ring);outline-offset:2px}.cb-chat-reasoning__chevron{transition:transform var(--cb-duration-fast, .15s) ease}.cb-chat-reasoning__toggle[aria-expanded=true] .cb-chat-reasoning__chevron{transform:rotate(180deg)}:host(.cb-chat-reasoning--streaming) .cb-chat-reasoning__label{background:linear-gradient(90deg,currentColor 25%,var(--cb-color-text-strong) 50%,currentColor 75%);background-size:200% 100%;-webkit-background-clip:text;background-clip:text;color:transparent;animation:cb-chat-shimmer 1.6s linear infinite}@keyframes cb-chat-shimmer{0%{background-position:100% 0}to{background-position:-100% 0}}.cb-chat-reasoning__body{margin:.25rem 0 0 .5rem;padding:.25rem 0 .25rem .875rem;border-left:2px solid var(--cb-color-border);line-height:1.55}.cb-chat-reasoning__markdown ::ng-deep p{margin:0 0 .5rem}.cb-chat-reasoning__markdown ::ng-deep p:last-child{margin-bottom:0}.cb-chat-reasoning__markdown ::ng-deep code{font-family:var(--cb-font-mono);font-size:.9em}@media(prefers-reduced-motion:reduce){:host(.cb-chat-reasoning--streaming) .cb-chat-reasoning__label{animation:none;background:none;color:inherit}}\n"], dependencies: [{ kind: "component", type: CbIcon, selector: "cb-icon", inputs: ["name", "size", "spin", "ariaLabel"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
1129
+ }
1130
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.5", ngImport: i0, type: CbChatReasoning, decorators: [{
1131
+ type: Component,
1132
+ args: [{ selector: 'cb-chat-reasoning', changeDetection: ChangeDetectionStrategy.OnPush, encapsulation: ViewEncapsulation.Emulated, imports: [CbIcon], host: { class: 'cb-chat-reasoning', '[class.cb-chat-reasoning--streaming]': 'streaming()' }, template: `
1133
+ <button
1134
+ type="button"
1135
+ class="cb-chat-reasoning__toggle"
1136
+ [attr.aria-expanded]="open()"
1137
+ [attr.aria-controls]="uid"
1138
+ (click)="open.set(!open())"
1139
+ >
1140
+ <cb-icon name="sparkles" [size]="14" aria-hidden="true" />
1141
+ <span class="cb-chat-reasoning__label">{{ summary() }}</span>
1142
+ <cb-icon class="cb-chat-reasoning__chevron" name="chevron-down" [size]="14" aria-hidden="true" />
1143
+ </button>
1144
+ @if (open()) {
1145
+ <div class="cb-chat-reasoning__body" [id]="uid" role="region" [attr.aria-label]="t().chatReasoning" [attr.aria-busy]="streaming() ? 'true' : null">
1146
+ @if (text()) {
1147
+ <div class="cb-chat-reasoning__markdown" [innerHTML]="html()"></div>
1148
+ }
1149
+ <ng-content />
1150
+ </div>
1151
+ }
1152
+ `, styles: [":host{display:block;max-width:100%;color:var(--cb-color-text-muted);font-family:var(--cb-font-sans);font-size:var(--cb-text-sm, .875rem)}.cb-chat-reasoning__toggle{display:inline-flex;align-items:center;gap:.375rem;padding:.25rem .5rem .25rem .25rem;border:0;border-radius:var(--cb-radius-sm, 4px);background:transparent;color:inherit;font:inherit;cursor:pointer}.cb-chat-reasoning__toggle:hover{color:var(--cb-color-text)}.cb-chat-reasoning__toggle:focus-visible{outline:2px solid var(--cb-color-focus-ring);outline-offset:2px}.cb-chat-reasoning__chevron{transition:transform var(--cb-duration-fast, .15s) ease}.cb-chat-reasoning__toggle[aria-expanded=true] .cb-chat-reasoning__chevron{transform:rotate(180deg)}:host(.cb-chat-reasoning--streaming) .cb-chat-reasoning__label{background:linear-gradient(90deg,currentColor 25%,var(--cb-color-text-strong) 50%,currentColor 75%);background-size:200% 100%;-webkit-background-clip:text;background-clip:text;color:transparent;animation:cb-chat-shimmer 1.6s linear infinite}@keyframes cb-chat-shimmer{0%{background-position:100% 0}to{background-position:-100% 0}}.cb-chat-reasoning__body{margin:.25rem 0 0 .5rem;padding:.25rem 0 .25rem .875rem;border-left:2px solid var(--cb-color-border);line-height:1.55}.cb-chat-reasoning__markdown ::ng-deep p{margin:0 0 .5rem}.cb-chat-reasoning__markdown ::ng-deep p:last-child{margin-bottom:0}.cb-chat-reasoning__markdown ::ng-deep code{font-family:var(--cb-font-mono);font-size:.9em}@media(prefers-reduced-motion:reduce){:host(.cb-chat-reasoning--streaming) .cb-chat-reasoning__label{animation:none;background:none;color:inherit}}\n"] }]
1153
+ }], ctorParameters: () => [], propDecorators: { text: [{ type: i0.Input, args: [{ isSignal: true, alias: "text", required: false }] }], streaming: [{ type: i0.Input, args: [{ isSignal: true, alias: "streaming", required: false }] }], duration: [{ type: i0.Input, args: [{ isSignal: true, alias: "duration", required: false }] }], open: [{ type: i0.Input, args: [{ isSignal: true, alias: "open", required: false }] }, { type: i0.Output, args: ["openChange"] }], autoCollapse: [{ type: i0.Input, args: [{ isSignal: true, alias: "autoCollapse", required: false }] }] } });
1154
+
1155
+ let nextUid = 0;
1156
+ /** Objeto → JSON legible; texto, tal cual. */
1157
+ function show(value) {
1158
+ if (value === undefined || value === null || value === '')
1159
+ return null;
1160
+ if (typeof value === 'string') {
1161
+ try {
1162
+ return { code: JSON.stringify(JSON.parse(value), null, 2), language: 'json' };
1163
+ }
1164
+ catch {
1165
+ return { code: value, language: '' };
1166
+ }
1167
+ }
1168
+ try {
1169
+ return { code: JSON.stringify(value, null, 2), language: 'json' };
1170
+ }
1171
+ catch {
1172
+ return { code: String(value), language: '' };
1173
+ }
1174
+ }
1175
+ /**
1176
+ * Llamada a una herramienta (función) del asistente: nombre, estado
1177
+ * ("Usando buscar_movimientos…", "Usó…", "Falló…") y, al desplegar, la
1178
+ * entrada y el resultado como JSON resaltado.
1179
+ *
1180
+ * @example
1181
+ * ```html
1182
+ * <cb-chat-tool name="buscar_movimientos" status="done" [input]="{ cuenta: '4829' }" [output]="resultado" />
1183
+ * ```
1184
+ */
1185
+ class CbChatTool {
1186
+ constructor() {
1187
+ /** Textos de interfaz en el idioma activo (`provideCaribeNG({ locale })`). */
1188
+ this.t = injectCbTranslation();
1189
+ this.uid = `cb-chat-tool-${++nextUid}`;
1190
+ /** Nombre de la herramienta (`buscar_movimientos`). */
1191
+ this.name = input('', /* @ts-ignore */
1192
+ ...(ngDevMode ? [{ debugName: "name" }] : /* istanbul ignore next */ []));
1193
+ /** Texto que sustituye al nombre en la cabecera ("Buscando movimientos"). */
1194
+ this.title = input('', /* @ts-ignore */
1195
+ ...(ngDevMode ? [{ debugName: "title" }] : /* istanbul ignore next */ []));
1196
+ this.status = input('done', /* @ts-ignore */
1197
+ ...(ngDevMode ? [{ debugName: "status" }] : /* istanbul ignore next */ []));
1198
+ /** Argumentos (objeto o JSON). */
1199
+ this.input = input(undefined, /* @ts-ignore */
1200
+ ...(ngDevMode ? [{ debugName: "input" }] : /* istanbul ignore next */ []));
1201
+ /** Resultado (objeto, JSON o texto). */
1202
+ this.output = input(undefined, /* @ts-ignore */
1203
+ ...(ngDevMode ? [{ debugName: "output" }] : /* istanbul ignore next */ []));
1204
+ /** Desplegado. */
1205
+ this.open = model(false, /* @ts-ignore */
1206
+ ...(ngDevMode ? [{ debugName: "open" }] : /* istanbul ignore next */ []));
1207
+ this.shownInput = computed(() => show(this.input()), /* @ts-ignore */
1208
+ ...(ngDevMode ? [{ debugName: "shownInput" }] : /* istanbul ignore next */ []));
1209
+ this.shownOutput = computed(() => show(this.output()), /* @ts-ignore */
1210
+ ...(ngDevMode ? [{ debugName: "shownOutput" }] : /* istanbul ignore next */ []));
1211
+ this.label = computed(() => {
1212
+ const name = this.title() || this.name();
1213
+ const status = this.status();
1214
+ if (status === 'running')
1215
+ return cbFormat(this.t().chatToolRunning, name);
1216
+ if (status === 'error')
1217
+ return cbFormat(this.t().chatToolError, name);
1218
+ return cbFormat(this.t().chatToolDone, name);
1219
+ }, /* @ts-ignore */
1220
+ ...(ngDevMode ? [{ debugName: "label" }] : /* istanbul ignore next */ []));
1221
+ }
1222
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.5", ngImport: i0, type: CbChatTool, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
1223
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.0.5", type: CbChatTool, isStandalone: true, selector: "cb-chat-tool", inputs: { name: { classPropertyName: "name", publicName: "name", isSignal: true, isRequired: false, transformFunction: null }, title: { classPropertyName: "title", publicName: "title", isSignal: true, isRequired: false, transformFunction: null }, status: { classPropertyName: "status", publicName: "status", isSignal: true, isRequired: false, transformFunction: null }, input: { classPropertyName: "input", publicName: "input", isSignal: true, isRequired: false, transformFunction: null }, output: { classPropertyName: "output", publicName: "output", isSignal: true, isRequired: false, transformFunction: null }, open: { classPropertyName: "open", publicName: "open", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { open: "openChange" }, host: { properties: { "class.cb-chat-tool--running": "status() === 'running'", "class.cb-chat-tool--done": "status() === 'done'", "class.cb-chat-tool--error": "status() === 'error'" }, classAttribute: "cb-chat-tool" }, ngImport: i0, template: `
1224
+ <button type="button" class="cb-chat-tool__header" [attr.aria-expanded]="open()" [attr.aria-controls]="uid" (click)="open.set(!open())">
1225
+ @switch (status()) {
1226
+ @case ('running') {
1227
+ <span class="cb-chat-tool__spinner" aria-hidden="true"></span>
1228
+ }
1229
+ @case ('error') {
1230
+ <cb-icon class="cb-chat-tool__icon" name="circle-alert" [size]="16" aria-hidden="true" />
1231
+ }
1232
+ @default {
1233
+ <cb-icon class="cb-chat-tool__icon" name="check" [size]="16" aria-hidden="true" />
1234
+ }
1235
+ }
1236
+ <span class="cb-chat-tool__label">{{ label() }}</span>
1237
+ <cb-icon class="cb-chat-tool__chevron" name="chevron-down" [size]="14" aria-hidden="true" />
1238
+ </button>
1239
+ <span class="cb-chat-tool__status" role="status">{{ status() === 'running' ? '' : label() }}</span>
1240
+ @if (open()) {
1241
+ <div class="cb-chat-tool__body" [id]="uid">
1242
+ @if (shownInput(); as i) {
1243
+ <div class="cb-chat-tool__section">{{ t().chatToolInput }}</div>
1244
+ <cb-code-block [code]="i.code" [language]="i.language" [header]="false" wrap maxHeight="14rem" [ariaLabel]="t().chatToolInput" />
1245
+ }
1246
+ @if (shownOutput(); as o) {
1247
+ <div class="cb-chat-tool__section">{{ t().chatToolOutput }}</div>
1248
+ <cb-code-block [code]="o.code" [language]="o.language" [header]="false" wrap maxHeight="14rem" [ariaLabel]="t().chatToolOutput" />
1249
+ }
1250
+ <ng-content />
1251
+ </div>
1252
+ }
1253
+ `, isInline: true, styles: [":host{display:block;max-width:100%;overflow:hidden;border:1px solid var(--cb-color-border);border-radius:var(--cb-radius-md, 8px);background:var(--cb-color-surface-raised);color:var(--cb-color-text);font-family:var(--cb-font-sans);font-size:var(--cb-text-sm, .875rem)}.cb-chat-tool__header{display:flex;align-items:center;gap:.5rem;width:100%;padding:.5rem .75rem;border:0;background:transparent;color:inherit;font:inherit;text-align:start;cursor:pointer}.cb-chat-tool__header:hover{background:var(--cb-color-surface-hover)}.cb-chat-tool__header:focus-visible{outline:2px solid var(--cb-color-focus-ring);outline-offset:-2px}.cb-chat-tool__label{flex:1;min-width:0;overflow:hidden;font-family:var(--cb-font-mono);font-size:.8125rem;text-overflow:ellipsis;white-space:nowrap}:host{--cb-chat-tool-ok: var(--cb-color-success-700);--cb-chat-tool-error: var(--cb-color-danger-700)}:host-context([data-theme=\"dark\"]){--cb-chat-tool-ok: var(--cb-color-success-400);--cb-chat-tool-error: var(--cb-color-danger-400)}:host(.cb-chat-tool--done) .cb-chat-tool__icon{color:var(--cb-chat-tool-ok)}:host(.cb-chat-tool--error) .cb-chat-tool__icon{color:var(--cb-chat-tool-error)}.cb-chat-tool__spinner{flex:none;width:14px;height:14px;border:2px solid var(--cb-color-border-strong);border-top-color:var(--cb-color-primary);border-radius:50%;animation:cb-chat-spin .8s linear infinite}@keyframes cb-chat-spin{to{transform:rotate(360deg)}}.cb-chat-tool__chevron{flex:none;color:var(--cb-color-text-muted);transition:transform var(--cb-duration-fast, .15s) ease}.cb-chat-tool__header[aria-expanded=true] .cb-chat-tool__chevron{transform:rotate(180deg)}.cb-chat-tool__status{position:absolute;width:1px;height:1px;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap}.cb-chat-tool__body{display:flex;flex-direction:column;gap:.375rem;padding:0 .75rem .75rem}.cb-chat-tool__section{margin-top:.25rem;color:var(--cb-color-text-muted);font-size:var(--cb-text-xs, .75rem);font-weight:var(--cb-font-weight-semibold, 600)}@media(prefers-reduced-motion:reduce){.cb-chat-tool__spinner{animation-duration:2.4s}}\n"], dependencies: [{ kind: "component", type: CbIcon, selector: "cb-icon", inputs: ["name", "size", "spin", "ariaLabel"] }, { kind: "component", type: CbCodeBlock, selector: "cb-code-block", inputs: ["code", "language", "filename", "lineNumbers", "startLine", "highlightLines", "wrap", "copyable", "header", "maxHeight", "ariaLabel"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
1254
+ }
1255
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.5", ngImport: i0, type: CbChatTool, decorators: [{
1256
+ type: Component,
1257
+ args: [{ selector: 'cb-chat-tool', changeDetection: ChangeDetectionStrategy.OnPush, encapsulation: ViewEncapsulation.Emulated, imports: [CbIcon, CbCodeBlock], host: {
1258
+ class: 'cb-chat-tool',
1259
+ '[class.cb-chat-tool--running]': "status() === 'running'",
1260
+ '[class.cb-chat-tool--done]': "status() === 'done'",
1261
+ '[class.cb-chat-tool--error]': "status() === 'error'",
1262
+ }, template: `
1263
+ <button type="button" class="cb-chat-tool__header" [attr.aria-expanded]="open()" [attr.aria-controls]="uid" (click)="open.set(!open())">
1264
+ @switch (status()) {
1265
+ @case ('running') {
1266
+ <span class="cb-chat-tool__spinner" aria-hidden="true"></span>
1267
+ }
1268
+ @case ('error') {
1269
+ <cb-icon class="cb-chat-tool__icon" name="circle-alert" [size]="16" aria-hidden="true" />
1270
+ }
1271
+ @default {
1272
+ <cb-icon class="cb-chat-tool__icon" name="check" [size]="16" aria-hidden="true" />
1273
+ }
1274
+ }
1275
+ <span class="cb-chat-tool__label">{{ label() }}</span>
1276
+ <cb-icon class="cb-chat-tool__chevron" name="chevron-down" [size]="14" aria-hidden="true" />
1277
+ </button>
1278
+ <span class="cb-chat-tool__status" role="status">{{ status() === 'running' ? '' : label() }}</span>
1279
+ @if (open()) {
1280
+ <div class="cb-chat-tool__body" [id]="uid">
1281
+ @if (shownInput(); as i) {
1282
+ <div class="cb-chat-tool__section">{{ t().chatToolInput }}</div>
1283
+ <cb-code-block [code]="i.code" [language]="i.language" [header]="false" wrap maxHeight="14rem" [ariaLabel]="t().chatToolInput" />
1284
+ }
1285
+ @if (shownOutput(); as o) {
1286
+ <div class="cb-chat-tool__section">{{ t().chatToolOutput }}</div>
1287
+ <cb-code-block [code]="o.code" [language]="o.language" [header]="false" wrap maxHeight="14rem" [ariaLabel]="t().chatToolOutput" />
1288
+ }
1289
+ <ng-content />
1290
+ </div>
1291
+ }
1292
+ `, styles: [":host{display:block;max-width:100%;overflow:hidden;border:1px solid var(--cb-color-border);border-radius:var(--cb-radius-md, 8px);background:var(--cb-color-surface-raised);color:var(--cb-color-text);font-family:var(--cb-font-sans);font-size:var(--cb-text-sm, .875rem)}.cb-chat-tool__header{display:flex;align-items:center;gap:.5rem;width:100%;padding:.5rem .75rem;border:0;background:transparent;color:inherit;font:inherit;text-align:start;cursor:pointer}.cb-chat-tool__header:hover{background:var(--cb-color-surface-hover)}.cb-chat-tool__header:focus-visible{outline:2px solid var(--cb-color-focus-ring);outline-offset:-2px}.cb-chat-tool__label{flex:1;min-width:0;overflow:hidden;font-family:var(--cb-font-mono);font-size:.8125rem;text-overflow:ellipsis;white-space:nowrap}:host{--cb-chat-tool-ok: var(--cb-color-success-700);--cb-chat-tool-error: var(--cb-color-danger-700)}:host-context([data-theme=\"dark\"]){--cb-chat-tool-ok: var(--cb-color-success-400);--cb-chat-tool-error: var(--cb-color-danger-400)}:host(.cb-chat-tool--done) .cb-chat-tool__icon{color:var(--cb-chat-tool-ok)}:host(.cb-chat-tool--error) .cb-chat-tool__icon{color:var(--cb-chat-tool-error)}.cb-chat-tool__spinner{flex:none;width:14px;height:14px;border:2px solid var(--cb-color-border-strong);border-top-color:var(--cb-color-primary);border-radius:50%;animation:cb-chat-spin .8s linear infinite}@keyframes cb-chat-spin{to{transform:rotate(360deg)}}.cb-chat-tool__chevron{flex:none;color:var(--cb-color-text-muted);transition:transform var(--cb-duration-fast, .15s) ease}.cb-chat-tool__header[aria-expanded=true] .cb-chat-tool__chevron{transform:rotate(180deg)}.cb-chat-tool__status{position:absolute;width:1px;height:1px;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap}.cb-chat-tool__body{display:flex;flex-direction:column;gap:.375rem;padding:0 .75rem .75rem}.cb-chat-tool__section{margin-top:.25rem;color:var(--cb-color-text-muted);font-size:var(--cb-text-xs, .75rem);font-weight:var(--cb-font-weight-semibold, 600)}@media(prefers-reduced-motion:reduce){.cb-chat-tool__spinner{animation-duration:2.4s}}\n"] }]
1293
+ }], propDecorators: { name: [{ type: i0.Input, args: [{ isSignal: true, alias: "name", required: false }] }], title: [{ type: i0.Input, args: [{ isSignal: true, alias: "title", required: false }] }], status: [{ type: i0.Input, args: [{ isSignal: true, alias: "status", required: false }] }], input: [{ type: i0.Input, args: [{ isSignal: true, alias: "input", required: false }] }], output: [{ type: i0.Input, args: [{ isSignal: true, alias: "output", required: false }] }], open: [{ type: i0.Input, args: [{ isSignal: true, alias: "open", required: false }] }, { type: i0.Output, args: ["openChange"] }] } });
1294
+
1295
+ /*
1296
+ * Public API Surface de @caribeng/ui/chat
1297
+ */
1298
+
1299
+ /**
1300
+ * Generated bundle index. Do not edit.
1301
+ */
1302
+
1303
+ export { CbChatInput, CbChatMessage, CbChatMessages, CbChatReasoning, CbChatTool, cbMarkdownToBlocks, cbMarkdownToHtml, cbSafeUrl };