@praxisui/table 1.0.0-beta.8 → 3.0.0-beta.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.
@@ -0,0 +1,822 @@
1
+ import { firstValueFrom } from 'rxjs';
2
+ import { BaseAiAdapter } from '@praxisui/ai';
3
+ import { deepMerge } from '@praxisui/core';
4
+ import { TABLE_AI_CAPABILITIES, TASK_PRESETS } from './praxisui-table.mjs';
5
+
6
+ /**
7
+ * Analisa uma amostra de dados da tabela para gerar estatísticas
8
+ * que ajudem a IA a sugerir renderers e formatos adequados.
9
+ */
10
+ class TableDataProfiler {
11
+ static SAMPLE_LIMIT = 3; // Analisa no máximo 3 linhas para performance
12
+ static VALUE_TRUNCATE = 50; // Trunca strings longas no contexto
13
+ static profile(data, config, options) {
14
+ const sampleLimit = options?.sampleLimit ?? this.SAMPLE_LIMIT;
15
+ const rowCount = options?.rowCountOverride ?? data.length;
16
+ const profile = {
17
+ rowCount,
18
+ columns: {},
19
+ generatedAt: Date.now()
20
+ };
21
+ if (!data || data.length === 0 || !config.columns) {
22
+ return profile;
23
+ }
24
+ // 1. Selecionar amostra (início, meio e fim se possível, ou apenas slice)
25
+ // Para simplicidade e estabilidade, pegamos os primeiros N itens.
26
+ const sample = data.slice(0, sampleLimit);
27
+ // 2. Iterar colunas configuradas
28
+ for (const col of config.columns) {
29
+ if (!col || !col.field)
30
+ continue;
31
+ const values = sample.map(row => this.getValue(row, col.field)).filter(v => v !== null && v !== undefined && v !== '');
32
+ const stats = {
33
+ field: col.field,
34
+ header: col.header,
35
+ inferredType: 'empty',
36
+ cardinality: 0,
37
+ hasNulls: values.length < sample.length,
38
+ topValues: []
39
+ };
40
+ if (values.length > 0) {
41
+ stats.inferredType = this.inferType(values);
42
+ stats.cardinality = new Set(values.map(v => String(v))).size;
43
+ // Coletar Top Values (frequência)
44
+ const freqMap = new Map();
45
+ values.forEach(v => {
46
+ const k = String(v);
47
+ freqMap.set(k, (freqMap.get(k) || 0) + 1);
48
+ });
49
+ stats.topValues = Array.from(freqMap.entries())
50
+ .sort((a, b) => b[1] - a[1]) // Sort by count desc
51
+ .slice(0, 5)
52
+ .map(([val]) => this.sanitizeValue(val, stats.inferredType));
53
+ // Stats específicas por tipo
54
+ if (stats.inferredType === 'number') {
55
+ const nums = values.map(v => Number(v)).filter(n => !isNaN(n));
56
+ if (nums.length) {
57
+ stats.min = Math.min(...nums);
58
+ stats.max = Math.max(...nums);
59
+ }
60
+ }
61
+ else if (stats.inferredType === 'date') {
62
+ const dates = values.map(v => new Date(v).getTime()).filter(t => !isNaN(t));
63
+ if (dates.length) {
64
+ stats.dateRange = {
65
+ start: new Date(Math.min(...dates)).toISOString(),
66
+ end: new Date(Math.max(...dates)).toISOString()
67
+ };
68
+ }
69
+ }
70
+ else if (stats.inferredType === 'string') {
71
+ const strVal = String(values[0]);
72
+ stats.isUrl = strVal.startsWith('http');
73
+ stats.isEmail = strVal.includes('@') && strVal.includes('.');
74
+ stats.isLongText = values.some(v => String(v).length > 50);
75
+ }
76
+ }
77
+ profile.columns[col.field] = stats;
78
+ }
79
+ return profile;
80
+ }
81
+ static getValue(row, path) {
82
+ if (!path || typeof path !== 'string')
83
+ return undefined;
84
+ return path.split('.').reduce((obj, key) => obj?.[key], row);
85
+ }
86
+ static inferType(values) {
87
+ const first = values[0];
88
+ if (typeof first === 'number')
89
+ return 'number';
90
+ if (typeof first === 'boolean')
91
+ return 'boolean';
92
+ if (first instanceof Date)
93
+ return 'date';
94
+ // Check string formats
95
+ const str = String(first);
96
+ if (!isNaN(Number(str)) && str.trim() !== '')
97
+ return 'number';
98
+ if (!isNaN(Date.parse(str)) && str.length > 5)
99
+ return 'date'; // length check avoids short strings being parsed as dates
100
+ return 'string';
101
+ }
102
+ static sanitizeValue(val, type) {
103
+ if (type === 'string' && val.length > this.VALUE_TRUNCATE) {
104
+ return val.substring(0, this.VALUE_TRUNCATE) + '...';
105
+ }
106
+ return val;
107
+ }
108
+ }
109
+
110
+ /**
111
+ * Adapter that connects the AI Engine to a specific PraxisTable instance.
112
+ * Implements two-step intent flow + heuristics for contextual suggestions.
113
+ */
114
+ class TableAiAdapter extends BaseAiAdapter {
115
+ table;
116
+ aiService;
117
+ componentName = 'Data Table';
118
+ componentId = 'praxis-table';
119
+ componentType = 'table';
120
+ constructor(table, aiService) {
121
+ super();
122
+ this.table = table;
123
+ this.aiService = aiService;
124
+ }
125
+ // -------- Core contract --------
126
+ getCurrentConfig() {
127
+ try {
128
+ return structuredClone(this.table.config);
129
+ }
130
+ catch {
131
+ return JSON.parse(JSON.stringify(this.table.config));
132
+ }
133
+ }
134
+ getCapabilities() {
135
+ return TABLE_AI_CAPABILITIES.capabilities;
136
+ }
137
+ getTaskPresets() {
138
+ return TASK_PRESETS;
139
+ }
140
+ getRuntimeState() {
141
+ return {
142
+ rowsTotal: this.table.dataSource.data.length,
143
+ rowsVisible: this.table.paginator ? this.table.paginator.pageSize : this.table.dataSource.data.length,
144
+ pageIndex: this.table.paginator ? this.table.paginator.pageIndex : 0,
145
+ sort: this.table.sort ? { active: this.table.sort.active, direction: this.table.sort.direction } : null,
146
+ filters: this.table.filterCriteria,
147
+ selectionCount: this.table.selection.selected.length,
148
+ isLoading: false // TODO: expose real loading flag
149
+ };
150
+ }
151
+ getSuggestionContext() {
152
+ const config = this.getCurrentConfig();
153
+ const behavior = config.behavior || {};
154
+ const actions = config.actions || {};
155
+ const availableFeatures = [];
156
+ const missingCapabilities = [];
157
+ if (!this.table.resourcePath) {
158
+ availableFeatures.push('data-connection');
159
+ }
160
+ if (!this.table.idField) {
161
+ availableFeatures.push('bindings.idField');
162
+ }
163
+ if (!behavior.filtering?.enabled) {
164
+ availableFeatures.push('behavior.filtering');
165
+ missingCapabilities.push('filtering');
166
+ }
167
+ if (!behavior.sorting?.enabled) {
168
+ availableFeatures.push('behavior.sorting');
169
+ missingCapabilities.push('sorting');
170
+ }
171
+ if (!behavior.pagination?.enabled) {
172
+ availableFeatures.push('behavior.pagination');
173
+ missingCapabilities.push('pagination');
174
+ }
175
+ if (!behavior.selection?.enabled) {
176
+ availableFeatures.push('behavior.selection');
177
+ missingCapabilities.push('selection');
178
+ }
179
+ if (!config.toolbar?.visible) {
180
+ availableFeatures.push('toolbar');
181
+ missingCapabilities.push('toolbar');
182
+ }
183
+ if (!actions.row?.enabled) {
184
+ availableFeatures.push('actions.row');
185
+ missingCapabilities.push('actions');
186
+ }
187
+ if (!actions.bulk?.enabled) {
188
+ availableFeatures.push('actions.bulk');
189
+ missingCapabilities.push('actions');
190
+ }
191
+ if (!actions.context?.enabled) {
192
+ availableFeatures.push('actions.context');
193
+ missingCapabilities.push('actions');
194
+ }
195
+ if (!config.export?.enabled) {
196
+ availableFeatures.push('export');
197
+ missingCapabilities.push('export');
198
+ }
199
+ if (!config.localization) {
200
+ availableFeatures.push('localization');
201
+ missingCapabilities.push('localization');
202
+ }
203
+ if (!config.messages) {
204
+ availableFeatures.push('messages');
205
+ missingCapabilities.push('messages');
206
+ }
207
+ if (!config.performance) {
208
+ availableFeatures.push('performance');
209
+ missingCapabilities.push('performance');
210
+ }
211
+ if (!config.accessibility) {
212
+ availableFeatures.push('accessibility');
213
+ missingCapabilities.push('accessibility');
214
+ }
215
+ if (!config.rowConditionalStyles || config.rowConditionalStyles.length === 0) {
216
+ availableFeatures.push('row-conditional-styles');
217
+ missingCapabilities.push('conditional');
218
+ }
219
+ return {
220
+ authoringContract: {
221
+ kind: 'praxis.table.editor',
222
+ usesBindings: true,
223
+ bindingsPaths: ['bindings.resourcePath', 'bindings.idField', 'bindings.horizontalScroll'],
224
+ runtimeConfigProjection: 'TableConfig',
225
+ },
226
+ availableFeatures,
227
+ missingCapabilities: Array.from(new Set(missingCapabilities)),
228
+ inputs: {
229
+ resourcePath: this.table.resourcePath || null,
230
+ idField: this.table.getIdField?.() || null,
231
+ horizontalScroll: this.table.horizontalScroll || null,
232
+ },
233
+ };
234
+ }
235
+ getDataProfile() {
236
+ const data = this.table.dataSource.data || [];
237
+ return TableDataProfiler.profile(data, this.table.config);
238
+ }
239
+ getSchemaFields() {
240
+ try {
241
+ return this.table.getSchemaFieldsSnapshot();
242
+ }
243
+ catch {
244
+ return [];
245
+ }
246
+ }
247
+ createSnapshot() {
248
+ return this.getCurrentConfig();
249
+ }
250
+ async restoreSnapshot(snapshot) {
251
+ if (!snapshot)
252
+ return;
253
+ this.applyConfig(snapshot);
254
+ }
255
+ async applyPatch(patch, intent) {
256
+ const current = this.getCurrentConfig();
257
+ const normalizedPatch = this.normalizePatch(patch);
258
+ const nextConfig = this.smartMergeTableConfig(current, normalizedPatch);
259
+ this.applyConfig(nextConfig);
260
+ return { success: true };
261
+ }
262
+ // -------- Context & suggestions --------
263
+ /**
264
+ * Human-friendly summary for prompt/context (columns + feature flags + data stats).
265
+ */
266
+ getContextDescription(contextOverride) {
267
+ // 1. Generate Data Profile
268
+ const data = this.table.dataSource.data || [];
269
+ const effectiveConfig = contextOverride ? deepMerge(this.table.config, contextOverride) : this.table.config;
270
+ const profile = TableDataProfiler.profile(data, effectiveConfig);
271
+ // 2. Format Column Stats
272
+ const colDetails = Object.values(profile.columns).map(stats => {
273
+ let desc = `${stats.field} (${stats.inferredType})`;
274
+ if (stats.cardinality > 0 && stats.cardinality < 10) {
275
+ desc += `, values=[${stats.topValues.join(', ')}]`;
276
+ }
277
+ else if (stats.inferredType === 'number' && stats.min !== undefined) {
278
+ desc += `, range=[${stats.min}-${stats.max}]`;
279
+ }
280
+ // Check existing config to see if it has renderer/format
281
+ const colCfg = effectiveConfig.columns?.find(c => c.field === stats.field);
282
+ if (colCfg?.renderer)
283
+ desc += ` [has-renderer: ${colCfg.renderer.type}]`;
284
+ if (colCfg?.format)
285
+ desc += ` [has-format]`;
286
+ return desc;
287
+ });
288
+ // 3. Active Features
289
+ const behaviorFlags = [];
290
+ const b = this.table.config?.behavior;
291
+ if (b?.filtering?.enabled)
292
+ behaviorFlags.push('filtering');
293
+ if (b?.sorting?.enabled)
294
+ behaviorFlags.push('sorting');
295
+ if (b?.pagination?.enabled)
296
+ behaviorFlags.push('pagination');
297
+ if (b?.selection?.enabled)
298
+ behaviorFlags.push('selection');
299
+ return `
300
+ Dataset: ${profile.rowCount} rows.
301
+ Active Features: ${behaviorFlags.join(', ') || 'None'}.
302
+ Columns Analysis:
303
+ - ${colDetails.join('\n- ')}
304
+ `.trim();
305
+ }
306
+ /**
307
+ * Dynamic suggestions based on current config/data.
308
+ * Uses AI Service if available, otherwise falls back to heuristics.
309
+ */
310
+ async getSuggestions(forceReload = false) {
311
+ const storage = this.table?.asyncConfigStorage;
312
+ const key = this.suggestionsKey();
313
+ // 1. Try cache
314
+ if (storage && !forceReload) {
315
+ try {
316
+ const cached = await firstValueFrom(storage.loadConfig(key));
317
+ if (cached && Array.isArray(cached) && cached.length) {
318
+ return cached;
319
+ }
320
+ }
321
+ catch { }
322
+ }
323
+ let suggestions = [];
324
+ // 2. Clear cache explicitly when forcing reload
325
+ if (forceReload && storage && typeof storage.clearConfig === 'function') {
326
+ try {
327
+ await firstValueFrom(storage.clearConfig(key));
328
+ }
329
+ catch { }
330
+ }
331
+ // 3. Try AI Generation (if service available)
332
+ if (this.aiService) {
333
+ try {
334
+ const filtered = this.filterCompletedColumnsFromContext();
335
+ suggestions = await this.generateAiSuggestions(filtered);
336
+ }
337
+ catch (err) {
338
+ console.warn('[TableAiAdapter] AI suggestion failed, falling back to heuristics', err);
339
+ }
340
+ }
341
+ // 4. Fallback to Heuristics if AI failed or not available
342
+ if (!suggestions.length) {
343
+ suggestions = this.getHeuristicSuggestions();
344
+ }
345
+ // 5. Save to cache
346
+ const sorted = suggestions.sort((a, b) => (b.score || 0) - (a.score || 0));
347
+ if (storage && sorted.length > 0) {
348
+ try {
349
+ await firstValueFrom(storage.saveConfig(key, sorted));
350
+ }
351
+ catch { }
352
+ }
353
+ return sorted;
354
+ }
355
+ async generateAiSuggestions(contextOverride) {
356
+ if (!this.aiService)
357
+ return [];
358
+ const prompt = `
359
+ Analyze the following data table context and stats.
360
+ Suggest 3 to 5 specific UX/UI improvements based on the data distribution and active features.
361
+
362
+ Guidelines for Suggestions:
363
+ - Focus on: Data formatting (dates/currency), Visual indicators (badges/icons for low-cardinality or mapped fields), Filter types, and missing critical features (pagination/export).
364
+ - Prefer explicit metadata (column types, value mappings, data stats). Avoid guessing semantics from field names.
365
+ - Intents MUST be actionable user commands, not just labels.
366
+
367
+ Intent Examples:
368
+ [GOOD]: "Format column 'birthDate' as dd/MM/yyyy", "Use badges for column 'state' (3 distinct values)", "Enable pagination with 10 rows".
369
+ [BAD]: "Improve formatting", "Add badges", "Pagination settings".
370
+
371
+ Context:
372
+ ${this.getContextDescription(contextOverride)}
373
+
374
+ Return a JSON array of suggestions with: id, label, description, icon, group, intent (the actionable command), and score (0-1).
375
+ `;
376
+ const result = await firstValueFrom(this.aiService.generateJson(prompt, undefined, {
377
+ type: 'OBJECT',
378
+ properties: {
379
+ suggestions: {
380
+ type: 'ARRAY',
381
+ items: {
382
+ type: 'OBJECT',
383
+ properties: {
384
+ id: { type: 'STRING' },
385
+ label: { type: 'STRING' },
386
+ description: { type: 'STRING' },
387
+ icon: { type: 'STRING' },
388
+ group: { type: 'STRING' },
389
+ intent: { type: 'STRING' },
390
+ score: { type: 'NUMBER' }
391
+ },
392
+ required: ['id', 'label', 'intent']
393
+ }
394
+ }
395
+ }
396
+ }));
397
+ return result?.suggestions || [];
398
+ }
399
+ getHeuristicSuggestions() {
400
+ const suggestions = [];
401
+ const config = this.table.config;
402
+ const data = this.table.dataSource.data || [];
403
+ const columns = config.columns || [];
404
+ const profile = TableDataProfiler.profile(data, config);
405
+ const profileColumns = profile.columns || {};
406
+ const capabilities = this.getCapabilities?.() || [];
407
+ const categorySet = new Set(capabilities
408
+ .map((cap) => String(cap?.category || '').toLowerCase())
409
+ .filter((cat) => cat.length > 0));
410
+ const allowCategory = (categories) => {
411
+ if (!categorySet.size)
412
+ return false;
413
+ return categories.some((cat) => categorySet.has(cat));
414
+ };
415
+ // Pagination if many rows and disabled
416
+ if (allowCategory(['pagination', 'behavior']) && !config.behavior?.pagination?.enabled && data.length > 20) {
417
+ suggestions.push({
418
+ id: 'enable-paging',
419
+ label: 'Habilitar Paginação',
420
+ description: `Tabela tem ${data.length} linhas. Paginação melhora a performance.`,
421
+ icon: 'list_alt',
422
+ group: 'Performance',
423
+ intent: 'Habilitar paginação com 10 itens por página',
424
+ score: 0.9
425
+ });
426
+ }
427
+ // Modo compacto se muitas colunas visíveis
428
+ const visibleCols = columns.filter((c) => c.visible !== false).length;
429
+ if (allowCategory(['appearance']) && visibleCols > 8 && config.appearance?.density !== 'compact') {
430
+ suggestions.push({
431
+ id: 'compact-mode',
432
+ label: 'Modo Compacto',
433
+ description: 'Muitas colunas visíveis. Modo compacto exibe mais dados.',
434
+ icon: 'compress',
435
+ group: 'Visual',
436
+ intent: 'Usar densidade compacta',
437
+ score: 0.8
438
+ });
439
+ }
440
+ // Badge/Icon para valores discretos
441
+ const badgeCardinalityMax = 6;
442
+ const badgeCol = columns.find((col) => {
443
+ if (!col || col.renderer || !col.field)
444
+ return false;
445
+ const stats = profileColumns[col.field];
446
+ const inferredType = stats?.inferredType;
447
+ const cardinality = stats?.cardinality ?? -1;
448
+ const isLongText = stats?.isLongText ?? false;
449
+ const hasValueMapping = !!col.valueMapping && Object.keys(col.valueMapping).length > 0;
450
+ const typeMatch = col.type === 'boolean'
451
+ || col.type === 'string'
452
+ || inferredType === 'boolean'
453
+ || inferredType === 'string';
454
+ const lowCardinality = cardinality > 0 && cardinality <= badgeCardinalityMax;
455
+ return !isLongText && (hasValueMapping || (typeMatch && lowCardinality));
456
+ });
457
+ if (allowCategory(['renderer', 'columns']) && badgeCol) {
458
+ suggestions.push({
459
+ id: 'status-badge',
460
+ label: `Badges para ${badgeCol.header || badgeCol.field}`,
461
+ description: 'Destacar valores com badges ou ícones.',
462
+ icon: 'verified',
463
+ group: 'Visual',
464
+ intent: `Usar renderer badge para a coluna ${badgeCol.field}`,
465
+ score: 0.7
466
+ });
467
+ }
468
+ // Datas sem format
469
+ const dateCol = columns.find((c) => c.type === 'date' && !c.format);
470
+ if (allowCategory(['format', 'columns']) && dateCol) {
471
+ suggestions.push({
472
+ id: 'date-format',
473
+ label: `Formatar ${dateCol.header || dateCol.field}`,
474
+ description: 'Padronizar exibição de data.',
475
+ icon: 'calendar_today',
476
+ group: 'Formatação',
477
+ intent: `Formatar coluna ${dateCol.field} como dd/MM/yyyy`,
478
+ score: 0.6
479
+ });
480
+ }
481
+ return suggestions;
482
+ }
483
+ /**
484
+ * Remove colunas já formatadas/tipadas para evitar sugestões redundantes (ex.: salário já em currency)
485
+ */
486
+ filterCompletedColumnsFromContext() {
487
+ const cfg = this.getCurrentConfig();
488
+ const filteredCols = (cfg.columns || []).filter((col) => {
489
+ // Keep if missing type or format (needs suggestion)
490
+ if (!col.type)
491
+ return true;
492
+ if (col.type === 'currency' && col.format)
493
+ return false;
494
+ if (col.type === 'date' && col.format)
495
+ return false;
496
+ return true;
497
+ });
498
+ return { ...cfg, columns: filteredCols };
499
+ }
500
+ // -------- Two-step flow helpers --------
501
+ getFilteredCapabilities(category) {
502
+ const all = TABLE_AI_CAPABILITIES.capabilities;
503
+ if (!category || category === 'unknown')
504
+ return all;
505
+ const categoryMap = {
506
+ columns: ['columns', 'format', 'mapping', 'renderer', 'conditional'],
507
+ appearance: ['appearance', 'conditional'],
508
+ conditional: ['conditional', 'renderer', 'columns'], // Include 'columns' to allow field context
509
+ behavior: ['behavior', 'pagination', 'sorting', 'filtering', 'selection', 'interaction'],
510
+ actions: ['actions', 'toolbar', 'export']
511
+ };
512
+ const targets = categoryMap[category] || [category];
513
+ return all.filter((c) => targets.includes(c.category));
514
+ }
515
+ suggestionsKey() {
516
+ const id = this.table.tableId || 'default';
517
+ const rp = this.table.resourcePath || 'default';
518
+ // bump version to invalidate old cached suggestions
519
+ return `ai-suggestions:v3:${id}:${rp}`;
520
+ }
521
+ getColumnNames() {
522
+ return (this.table.config?.columns || [])
523
+ .map((c) => c.field)
524
+ .filter((f) => !!f);
525
+ }
526
+ extractContextForIntent(intent) {
527
+ const fullConfig = this.getCurrentConfig();
528
+ if ((intent.category === 'columns' || intent.category === 'conditional') && intent.targetField) {
529
+ const col = fullConfig.columns?.find((c) => c.field === intent.targetField);
530
+ if (col) {
531
+ return { desc: `Contexto focado na coluna '${intent.targetField}'.`, config: { columns: [col] } };
532
+ }
533
+ }
534
+ if (intent.category === 'behavior' && fullConfig.behavior) {
535
+ return { desc: 'Contexto focado em Comportamento (pagination/sorting/filtering/selection).', config: { behavior: fullConfig.behavior } };
536
+ }
537
+ if (intent.category === 'appearance' && fullConfig.appearance) {
538
+ return { desc: 'Contexto focado em Aparência.', config: { appearance: fullConfig.appearance } };
539
+ }
540
+ if (intent.category === 'actions' && (fullConfig.actions || fullConfig.toolbar)) {
541
+ return { desc: 'Contexto focado em Ações e Toolbar.', config: { actions: fullConfig.actions, toolbar: fullConfig.toolbar } };
542
+ }
543
+ return { desc: 'Contexto completo da tabela.', config: fullConfig };
544
+ }
545
+ /**
546
+ * Two-step flow: classify intent, build focused context, call enriched prompt.
547
+ * Returns the patch WITHOUT applying, so the UI can review.
548
+ */
549
+ async processUserIntent(userInput, aiService // Use interface type
550
+ ) {
551
+ const classification = await firstValueFrom(aiService.classifyIntent(userInput, this.getColumnNames()));
552
+ if (classification?.needsClarification) {
553
+ return {
554
+ type: 'clarification',
555
+ message: 'Preciso de mais detalhes para continuar.',
556
+ options: classification.options || []
557
+ };
558
+ }
559
+ // Handle QA intent
560
+ if (classification?.intent === 'ask_about_config') {
561
+ const answer = await firstValueFrom(aiService.answerQuestion(userInput, this.getCurrentConfig()));
562
+ return {
563
+ type: 'info',
564
+ message: answer,
565
+ explanation: answer // Map to explanation for UI compatibility if needed, or UI should handle 'info' type
566
+ };
567
+ }
568
+ const context = this.extractContextForIntent(classification);
569
+ const caps = this.getFilteredCapabilities(classification?.category);
570
+ const result = await firstValueFrom(aiService.executeEnrichedPrompt(userInput, context.desc, context.config, caps));
571
+ if (!result || !result.patch) {
572
+ return { type: 'error', message: 'Nenhum patch gerado.' };
573
+ }
574
+ const { sanitized, warnings } = this.validateAndSanitizePatch(result.patch, caps);
575
+ // Passar warnings para o retorno, para que o UI possa mostrá-los
576
+ // Se o patch estiver vazio após sanitização, informar ao usuário
577
+ // Se sobrou algo válido
578
+ if (Object.keys(sanitized).length > 0) {
579
+ const explanation = result.explanation + (warnings.length ? ` (Ajustado: ${warnings.length} itens inválidos removidos)` : '');
580
+ return { patch: sanitized, explanation, warnings };
581
+ }
582
+ else {
583
+ return { type: 'error', message: 'O patch gerado não continha configurações válidas permitidas.', warnings };
584
+ }
585
+ }
586
+ validateAndSanitizePatch(patch, allowedCaps) {
587
+ const allowedPaths = new Set(allowedCaps.map(c => c.path));
588
+ const warnings = [];
589
+ // Some schema-driven nodes are intentionally open objects and must be preserved as-is.
590
+ const passthroughObjectPaths = new Set([
591
+ 'behavior.expansion.detail.source.inlineSchema',
592
+ 'behavior.expansion.detail.source.resourcePath.paramsMap',
593
+ ]);
594
+ const recurse = (obj, currentPath) => {
595
+ if (typeof obj !== 'object' || obj === null)
596
+ return obj;
597
+ if (Array.isArray(obj)) {
598
+ // Se estamos em um array conhecido (ex: columns), o path vira columns[]
599
+ // Se não, assume que é valor final (ex: options: [1,2])
600
+ const isRootArray = currentPath && (allowedPaths.has(currentPath + '[]') || Array.from(allowedPaths).some(p => p.startsWith(currentPath + '[].')));
601
+ if (!isRootArray)
602
+ return obj; // É um valor array (ex: tags), retorna direto
603
+ return obj.map(item => recurse(item, currentPath + '[]'));
604
+ }
605
+ const cleanObj = {};
606
+ for (const key of Object.keys(obj)) {
607
+ const newPath = currentPath ? `${currentPath}.${key}` : key;
608
+ // Verifica se este path exato é permitido OU se é um prefixo de algo permitido
609
+ // Ex: 'columns' é permitido se 'columns[].width' existe
610
+ // Special case: 'columns[].field' is identity, always allow it if we are inside columns[]
611
+ const isIdentityField = currentPath === 'columns[]' && key === 'field';
612
+ const exactMatch = allowedPaths.has(newPath) || isIdentityField;
613
+ const prefixMatch = Array.from(allowedPaths).some(p => p.startsWith(newPath + '.') || p.startsWith(newPath + '['));
614
+ if (exactMatch || prefixMatch) {
615
+ if (exactMatch
616
+ && passthroughObjectPaths.has(newPath)
617
+ && typeof obj[key] === 'object'
618
+ && obj[key] !== null) {
619
+ cleanObj[key] = obj[key];
620
+ continue;
621
+ }
622
+ const val = recurse(obj[key], newPath);
623
+ if (newPath === 'columns[].computed.expression') {
624
+ if (typeof val !== 'string') {
625
+ warnings.push(`Computed expression inválida: ${newPath} (não é string)`);
626
+ continue;
627
+ }
628
+ const trimmed = val.trim();
629
+ if (!trimmed) {
630
+ warnings.push(`Computed expression vazia: ${newPath}`);
631
+ continue;
632
+ }
633
+ if (trimmed.length > 200) {
634
+ warnings.push(`Computed expression muito longa: ${newPath}`);
635
+ continue;
636
+ }
637
+ cleanObj[key] = trimmed;
638
+ continue;
639
+ }
640
+ // Se for objeto vazio após limpeza, não inclui (salvo se for intenção explicita de limpar config, mas patch geralmente é aditivo)
641
+ if (typeof val === 'object' && val !== null && !Array.isArray(val) && Object.keys(val).length === 0) {
642
+ // ignora
643
+ }
644
+ else {
645
+ cleanObj[key] = val;
646
+ }
647
+ }
648
+ else {
649
+ warnings.push(`Campo ignorado: ${newPath}`);
650
+ }
651
+ }
652
+ return cleanObj;
653
+ };
654
+ const sanitized = recurse(patch, '');
655
+ return { sanitized, warnings };
656
+ }
657
+ // -------- Internal helpers --------
658
+ /**
659
+ * Specialized merge for TableConfig that handles Array reconciliation safely.
660
+ * - 'columns': merges by 'field' ID.
661
+ */
662
+ smartMergeTableConfig(base, patch) {
663
+ const result = deepMerge(base, patch);
664
+ if (patch.columns && Array.isArray(patch.columns)) {
665
+ const originalCols = base.columns || [];
666
+ const patchCols = patch.columns;
667
+ const mergedCols = originalCols.map((origCol) => {
668
+ const match = patchCols.find((p) => p.field === origCol.field);
669
+ return match ? deepMerge(origCol, match) : origCol;
670
+ });
671
+ patchCols.forEach((pCol) => {
672
+ if (!originalCols.find((o) => o.field === pCol.field)) {
673
+ mergedCols.push(pCol);
674
+ }
675
+ });
676
+ result.columns = mergedCols;
677
+ }
678
+ return result;
679
+ }
680
+ applyConfig(config) {
681
+ const prev = this.table.config;
682
+ this.table.config = config;
683
+ this.table.ngOnChanges({
684
+ config: {
685
+ previousValue: prev,
686
+ currentValue: config,
687
+ firstChange: false,
688
+ isFirstChange: () => false
689
+ }
690
+ });
691
+ }
692
+ /**
693
+ * If a patch adds only a currency format, ensure the column type is set to 'currency'
694
+ * so the formatter runs. Avoid overriding an explicit non-currency type.
695
+ */
696
+ normalizePatch(patch) {
697
+ if (!patch.columns || !Array.isArray(patch.columns))
698
+ return patch;
699
+ const cols = patch.columns.map((col) => this.normalizeColumn(col));
700
+ return { ...patch, columns: cols };
701
+ }
702
+ looksLikeCurrencyFormat(format) {
703
+ if (!format)
704
+ return false;
705
+ const fmt = format.trim();
706
+ return /^[A-Z]{3}(\|.*)?$/.test(fmt);
707
+ }
708
+ looksLikeDateFormat(format) {
709
+ if (!format)
710
+ return false;
711
+ const fmt = format.trim();
712
+ // Heuristics: contains date tokens like d/M/y or common date separators
713
+ return /(d|M|y){2,}/i.test(fmt) || fmt.includes('/') || fmt.includes('-');
714
+ }
715
+ isIconRendererHint(renderer) {
716
+ if (!renderer)
717
+ return false;
718
+ if (renderer.type && renderer.type !== 'icon')
719
+ return false;
720
+ const icon = renderer.icon || renderer.name || renderer.iconName;
721
+ return typeof icon === 'string' || typeof renderer.color === 'string' || typeof renderer.size === 'number';
722
+ }
723
+ normalizeColumn(col) {
724
+ if (!col)
725
+ return col;
726
+ const next = { ...col };
727
+ // Normalize formats
728
+ if (next.format && !next.type) {
729
+ if (this.looksLikeCurrencyFormat(next.format)) {
730
+ next.type = 'currency';
731
+ }
732
+ else if (this.looksLikeDateFormat(next.format)) {
733
+ next.type = 'date';
734
+ }
735
+ }
736
+ // Normalize renderer structure (icon shorthand)
737
+ if (next.renderer) {
738
+ next.renderer = this.normalizeRenderer(next.renderer);
739
+ }
740
+ // Normalize conditional renderers
741
+ if (Array.isArray(next.conditionalRenderers)) {
742
+ next.conditionalRenderers = next.conditionalRenderers.map((r) => {
743
+ if (!r)
744
+ return r;
745
+ const out = { ...r };
746
+ if (out.renderer)
747
+ out.renderer = this.normalizeRenderer(out.renderer);
748
+ return out;
749
+ });
750
+ }
751
+ return next;
752
+ }
753
+ normalizeRenderer(renderer) {
754
+ if (!renderer)
755
+ return renderer;
756
+ const out = { ...renderer };
757
+ if (out.type === 'icon') {
758
+ // Move icon shorthand to renderer.icon.*
759
+ const iconObj = { ...(out.icon || {}) };
760
+ if (renderer.icon && typeof renderer.icon === 'string')
761
+ iconObj.name = renderer.icon;
762
+ if (renderer.icon?.name)
763
+ iconObj.name = renderer.icon.name;
764
+ if (renderer.icon?.color)
765
+ iconObj.color = renderer.icon.color;
766
+ if (renderer.icon?.size)
767
+ iconObj.size = renderer.icon.size;
768
+ if (renderer.color && !iconObj.color)
769
+ iconObj.color = renderer.color;
770
+ if (renderer.size && !iconObj.size)
771
+ iconObj.size = renderer.size;
772
+ out.icon = iconObj;
773
+ delete out.color;
774
+ delete out.size;
775
+ }
776
+ // Badge shorthand (allow badge.color/variant/icon/text via renderer.badge or root)
777
+ if (out.type === 'badge') {
778
+ const badge = { ...(out.badge || {}) };
779
+ if (renderer.color && !badge.color)
780
+ badge.color = renderer.color;
781
+ if (renderer.variant && !badge.variant)
782
+ badge.variant = renderer.variant;
783
+ if (renderer.text && !badge.text)
784
+ badge.text = renderer.text;
785
+ if (renderer.icon && !badge.icon && typeof renderer.icon === 'string')
786
+ badge.icon = renderer.icon;
787
+ out.badge = badge;
788
+ delete out.color;
789
+ delete out.variant;
790
+ delete out.text;
791
+ if (typeof out.icon === 'string')
792
+ delete out.icon;
793
+ }
794
+ // Chip shorthand
795
+ if (out.type === 'chip') {
796
+ const chip = { ...(out.chip || {}) };
797
+ if (renderer.color && !chip.color)
798
+ chip.color = renderer.color;
799
+ if (renderer.variant && !chip.variant)
800
+ chip.variant = renderer.variant;
801
+ if (renderer.text && !chip.text)
802
+ chip.text = renderer.text;
803
+ if (renderer.icon && !chip.icon && typeof renderer.icon === 'string')
804
+ chip.icon = renderer.icon;
805
+ out.chip = chip;
806
+ delete out.color;
807
+ delete out.variant;
808
+ delete out.text;
809
+ if (typeof out.icon === 'string')
810
+ delete out.icon;
811
+ }
812
+ // Link shorthand
813
+ if (out.type === 'link' && typeof renderer.href === 'string') {
814
+ out.link = { ...(out.link || {}), href: renderer.href };
815
+ delete out.href;
816
+ }
817
+ return out;
818
+ }
819
+ }
820
+
821
+ export { TableAiAdapter };
822
+ //# sourceMappingURL=praxisui-table-table-ai.adapter-C5rjLb8E.mjs.map