@praxisui/table 8.0.0-beta.0 → 8.0.0-beta.1

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.
package/README.md CHANGED
@@ -1,4 +1,4 @@
1
- ---
1
+ ---
2
2
  title: "Table"
3
3
  slug: "table-overview"
4
4
  description: "Visao geral do @praxisui/table com TableConfig unificada, filtros, renderers, performance e integracao enterprise."
@@ -256,6 +256,40 @@ O novo editor é "column-first" e usa uma tipagem compartilhada com o Editor d
256
256
  - Preview: execute “Testar” para ver quantas linhas seriam afetadas pelas regras ativas.
257
257
  - Import/Export: exporta JSON sem o campo `enabled`; importa com validação estrutural de JSON Logic e sanitização de estilos (allowlist).
258
258
 
259
+ #### Estados de linha vs estilos de célula
260
+
261
+ Use `rowConditionalStyles` quando a condição descreve o estado semântico da linha inteira, como prioridade, SLA vencido, item sem responsável, bloqueio ou estagnação. Para temas e faixas visuais, prefira `cssClass` em `rowConditionalStyles` e resolva a aparência no CSS do host. Isso preserva o ownership de tema do host e evita que gradientes ou backgrounds reiniciem em cada célula.
262
+
263
+ Use `columns[].conditionalStyles` quando o destaque pertence a uma coluna específica, como texto negativo em vermelho, status em badge, valor monetário fora da faixa ou ícone contextual de uma célula.
264
+
265
+ Padrão recomendado:
266
+
267
+ ```ts
268
+ rowConditionalStyles: [
269
+ {
270
+ condition: { contains: [{ var: 'prioridadeNome' }, 'Alta'] },
271
+ cssClass: 'app-row--priority-high',
272
+ description: 'Prioridade alta recebe âncora visual de linha.',
273
+ },
274
+ ]
275
+ ```
276
+
277
+ ```scss
278
+ .app-table .mat-mdc-row.app-row--priority-high {
279
+ background: linear-gradient(
280
+ 90deg,
281
+ color-mix(in srgb, var(--md-sys-color-error-container) 42%, transparent),
282
+ transparent
283
+ );
284
+ }
285
+
286
+ .app-table .mat-mdc-row.app-row--priority-high .mat-mdc-cell {
287
+ background: transparent;
288
+ }
289
+ ```
290
+
291
+ Evite aplicar gradientes de estado de linha diretamente em `.mat-mdc-cell`. Como cada célula tem largura, sticky behavior e renderer próprios, o gradiente pode parecer quebrado por coluna, especialmente em tema dark.
292
+
259
293
  Exemplos de JSON Logic:
260
294
 
261
295
  ```json
@@ -428,14 +462,22 @@ Um botão de engrenagem será exibido no canto superior direito. Ao clicar n
428
462
 
429
463
  As alterações podem ser aplicadas temporariamente com **Aplicar** ou salvas de forma persistente com **Salvar & Fechar**.
430
464
 
465
+ ### Assistente de IA da Tabela
466
+
467
+ Quando `enableCustomization` esta ativo, a tabela expoe o assistente de IA pelo shell canonico de `@praxisui/ai`.
468
+ O chrome conversacional, `sessionId`, `clientTurnId`, respostas rapidas e ciclo de revisao/aplicacao sao orquestrados
469
+ por `PraxisAssistantTurnOrchestratorService`; a semantica especifica da tabela continua em `TableAiAdapter` e no
470
+ `TableAgenticAuthoringTurnFlow`.
471
+
431
472
  ## 🚀 Instalação
432
473
 
433
474
  ```bash
434
- npm install @praxisui/core @praxisui/table
475
+ npm install @praxisui/ai @praxisui/core @praxisui/table
435
476
  ```
436
477
 
437
478
  Peers necessários (instale no app host):
438
479
  - `@angular/core` `^20.0.0`, `@angular/common` `^20.0.0`
480
+ - `@praxisui/ai` `^8.0.0-beta.0`
439
481
  - `@praxisui/core` `^0.0.1`
440
482
  - `@praxisui/dynamic-fields` `^0.0.1` (quando usar editores/inputs dinâmicos)
441
483
  - `@praxisui/dynamic-form` `^0.0.1` (quando integrar com formulários dinâmicos)
@@ -1716,4 +1758,3 @@ Apache-2.0 — consulte `LICENSE` na raiz do workspace para detalhes.
1716
1758
  **Parte do Praxis UI Workspace**
1717
1759
  **Versão**: 2.0.0 (Unified Architecture)
1718
1760
  **Compatibilidade**: Angular 18+
1719
-
@@ -115,7 +115,7 @@ class FilterFormDialogHostComponent {
115
115
  {{ data.i18n?.apply || 'Aplicar' }}
116
116
  </button>
117
117
  </mat-dialog-actions>
118
- `, isInline: true, styles: [".pfx-dialog-title{display:flex;align-items:center;justify-content:space-between;gap:12px;padding-right:8px}.pfx-dialog-title-text{display:inline-flex;align-items:center;gap:8px;font-weight:600;color:var(--md-sys-color-on-surface)}.pfx-dialog-close{margin-left:auto}.pfx-filter-dialog-content{display:flex;flex-direction:column;gap:12px;padding-top:8px}.pfx-empty-state{margin:8px 0 0;color:var(--md-sys-color-on-surface-variant)}.pfx-dialog-actions{padding:var(--pdx-dialog-actions-padding, 12px 24px 16px);border-top:1px solid var(--md-sys-color-outline-variant);background:transparent;display:flex;align-items:center;gap:8px}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i2.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "ngmodule", type: MatDialogModule }, { kind: "directive", type: i1.MatDialogTitle, selector: "[mat-dialog-title], [matDialogTitle]", inputs: ["id"], exportAs: ["matDialogTitle"] }, { kind: "directive", type: i1.MatDialogActions, selector: "[mat-dialog-actions], mat-dialog-actions, [matDialogActions]", inputs: ["align"] }, { kind: "directive", type: i1.MatDialogContent, selector: "[mat-dialog-content], mat-dialog-content, [matDialogContent]" }, { kind: "ngmodule", type: MatButtonModule }, { kind: "component", type: i3.MatButton, selector: " button[matButton], a[matButton], button[mat-button], button[mat-raised-button], button[mat-flat-button], button[mat-stroked-button], a[mat-button], a[mat-raised-button], a[mat-flat-button], a[mat-stroked-button] ", inputs: ["matButton"], exportAs: ["matButton", "matAnchor"] }, { kind: "component", type: i3.MatIconButton, selector: "button[mat-icon-button], a[mat-icon-button], button[matIconButton], a[matIconButton]", exportAs: ["matButton", "matAnchor"] }, { kind: "ngmodule", type: MatProgressBarModule }, { kind: "component", type: i15.MatProgressBar, selector: "mat-progress-bar", inputs: ["color", "value", "bufferValue", "mode"], outputs: ["animationEnd"], exportAs: ["matProgressBar"] }, { kind: "ngmodule", type: MatIconModule }, { kind: "component", type: i5.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "component", type: PraxisFilterForm, selector: "praxis-filter-form", inputs: ["config", "formId", "resourcePath", "mode"], outputs: ["formReady", "valueChange", "submit", "requestSearch", "validityChange"] }] });
118
+ `, isInline: true, styles: [".pfx-dialog-title{display:flex;align-items:center;justify-content:space-between;gap:12px;padding-right:8px}.pfx-dialog-title-text{display:inline-flex;align-items:center;gap:8px;font-weight:600;color:var(--mdc-dialog-subhead-color, var(--md-sys-color-on-surface))}.pfx-dialog-close{margin-left:auto}.pfx-filter-dialog-content{display:flex;flex-direction:column;gap:12px;padding-top:8px}.pfx-empty-state{margin:8px 0 0;color:var(--mdc-dialog-supporting-text-color, var(--md-sys-color-on-surface-variant))}.pfx-dialog-actions{padding:var(--pdx-dialog-actions-padding, 12px 24px 16px);border-top:1px solid var(--md-sys-color-outline-variant);background:transparent;display:flex;align-items:center;gap:8px}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i2.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "ngmodule", type: MatDialogModule }, { kind: "directive", type: i1.MatDialogTitle, selector: "[mat-dialog-title], [matDialogTitle]", inputs: ["id"], exportAs: ["matDialogTitle"] }, { kind: "directive", type: i1.MatDialogActions, selector: "[mat-dialog-actions], mat-dialog-actions, [matDialogActions]", inputs: ["align"] }, { kind: "directive", type: i1.MatDialogContent, selector: "[mat-dialog-content], mat-dialog-content, [matDialogContent]" }, { kind: "ngmodule", type: MatButtonModule }, { kind: "component", type: i3.MatButton, selector: " button[matButton], a[matButton], button[mat-button], button[mat-raised-button], button[mat-flat-button], button[mat-stroked-button], a[mat-button], a[mat-raised-button], a[mat-flat-button], a[mat-stroked-button] ", inputs: ["matButton"], exportAs: ["matButton", "matAnchor"] }, { kind: "component", type: i3.MatIconButton, selector: "button[mat-icon-button], a[mat-icon-button], button[matIconButton], a[matIconButton]", exportAs: ["matButton", "matAnchor"] }, { kind: "ngmodule", type: MatProgressBarModule }, { kind: "component", type: i15.MatProgressBar, selector: "mat-progress-bar", inputs: ["color", "value", "bufferValue", "mode"], outputs: ["animationEnd"], exportAs: ["matProgressBar"] }, { kind: "ngmodule", type: MatIconModule }, { kind: "component", type: i5.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "component", type: PraxisFilterForm, selector: "praxis-filter-form", inputs: ["config", "formId", "resourcePath", "mode"], outputs: ["formReady", "valueChange", "submit", "requestSearch", "validityChange"] }] });
119
119
  }
120
120
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImport: i0, type: FilterFormDialogHostComponent, decorators: [{
121
121
  type: Component,
@@ -156,7 +156,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImpo
156
156
  {{ data.i18n?.apply || 'Aplicar' }}
157
157
  </button>
158
158
  </mat-dialog-actions>
159
- `, styles: [".pfx-dialog-title{display:flex;align-items:center;justify-content:space-between;gap:12px;padding-right:8px}.pfx-dialog-title-text{display:inline-flex;align-items:center;gap:8px;font-weight:600;color:var(--md-sys-color-on-surface)}.pfx-dialog-close{margin-left:auto}.pfx-filter-dialog-content{display:flex;flex-direction:column;gap:12px;padding-top:8px}.pfx-empty-state{margin:8px 0 0;color:var(--md-sys-color-on-surface-variant)}.pfx-dialog-actions{padding:var(--pdx-dialog-actions-padding, 12px 24px 16px);border-top:1px solid var(--md-sys-color-outline-variant);background:transparent;display:flex;align-items:center;gap:8px}\n"] }]
159
+ `, styles: [".pfx-dialog-title{display:flex;align-items:center;justify-content:space-between;gap:12px;padding-right:8px}.pfx-dialog-title-text{display:inline-flex;align-items:center;gap:8px;font-weight:600;color:var(--mdc-dialog-subhead-color, var(--md-sys-color-on-surface))}.pfx-dialog-close{margin-left:auto}.pfx-filter-dialog-content{display:flex;flex-direction:column;gap:12px;padding-top:8px}.pfx-empty-state{margin:8px 0 0;color:var(--mdc-dialog-supporting-text-color, var(--md-sys-color-on-surface-variant))}.pfx-dialog-actions{padding:var(--pdx-dialog-actions-padding, 12px 24px 16px);border-top:1px solid var(--md-sys-color-outline-variant);background:transparent;display:flex;align-items:center;gap:8px}\n"] }]
160
160
  }], ctorParameters: () => [{ type: undefined, decorators: [{
161
161
  type: Inject,
162
162
  args: [MAT_DIALOG_DATA]
@@ -0,0 +1,280 @@
1
+ import { firstValueFrom } from 'rxjs';
2
+
3
+ class TableAgenticAuthoringTurnFlow {
4
+ adapter;
5
+ aiApi;
6
+ mode = 'config';
7
+ constructor(adapter, aiApi) {
8
+ this.adapter = adapter;
9
+ this.aiApi = aiApi;
10
+ }
11
+ async submit(request) {
12
+ const prompt = (request.prompt ?? '').trim();
13
+ if (!prompt) {
14
+ return {
15
+ state: 'listening',
16
+ phase: 'capture',
17
+ statusText: '',
18
+ };
19
+ }
20
+ const componentId = this.adapter.componentId || request.componentId || 'praxis-table';
21
+ const componentType = this.adapter.componentType || request.componentType || 'table';
22
+ const currentState = this.toAiJsonObject(this.adapter.getCurrentConfig());
23
+ const dataProfile = this.optionalJsonObject(this.adapter.getDataProfile?.());
24
+ const runtimeState = this.optionalJsonObject(this.adapter.getRuntimeState?.());
25
+ const schemaFields = this.adapter.getSchemaFields?.()
26
+ ?.map((field) => this.toAiJsonObject(field))
27
+ .filter((field) => Object.keys(field).length > 0);
28
+ const contextHints = this.optionalJsonObject(this.adapter.getAuthoringContext?.());
29
+ const response = await firstValueFrom(this.aiApi.getPatch({
30
+ componentId,
31
+ componentType,
32
+ userPrompt: prompt,
33
+ sessionId: request.sessionId,
34
+ clientTurnId: request.clientTurnId,
35
+ messages: this.toChatMessages(request.messages, prompt),
36
+ currentState,
37
+ currentStateDigest: this.buildCurrentStateDigest(currentState, dataProfile),
38
+ uiContextRef: {
39
+ componentId,
40
+ componentType,
41
+ },
42
+ ...(dataProfile ? { dataProfile } : {}),
43
+ ...(runtimeState ? { runtimeState } : {}),
44
+ ...(schemaFields?.length ? { schemaFields } : {}),
45
+ ...(contextHints ? { contextHints } : {}),
46
+ }));
47
+ return this.toTurnResult(this.compileAdapterResponse(response), request);
48
+ }
49
+ async apply(request) {
50
+ const patch = this.toRecord(request.pendingPatch);
51
+ if (!patch) {
52
+ return {
53
+ state: 'error',
54
+ phase: 'apply',
55
+ assistantMessage: 'Nao ha alteracao de tabela pronta para aplicar.',
56
+ errorText: 'Nao ha alteracao de tabela pronta para aplicar.',
57
+ canApply: false,
58
+ };
59
+ }
60
+ const result = await this.adapter.applyPatch(patch, request.prompt);
61
+ if (!result.success) {
62
+ return {
63
+ state: 'error',
64
+ phase: 'apply',
65
+ assistantMessage: result.error || 'Nao foi possivel aplicar as alteracoes na tabela.',
66
+ errorText: result.error || 'Nao foi possivel aplicar as alteracoes na tabela.',
67
+ canApply: true,
68
+ pendingPatch: patch,
69
+ };
70
+ }
71
+ return {
72
+ state: 'success',
73
+ phase: 'summarize',
74
+ assistantMessage: 'Alteracoes aplicadas na tabela.',
75
+ statusText: 'Alteracoes aplicadas na tabela.',
76
+ canApply: false,
77
+ pendingPatch: null,
78
+ diagnostics: result.warnings?.length ? { warnings: result.warnings } : undefined,
79
+ };
80
+ }
81
+ cancel() {
82
+ return Promise.resolve({
83
+ state: 'listening',
84
+ phase: 'capture',
85
+ assistantMessage: 'Solicitacao cancelada.',
86
+ statusText: '',
87
+ canApply: false,
88
+ pendingPatch: null,
89
+ pendingClarification: null,
90
+ });
91
+ }
92
+ retry(request) {
93
+ const lastPrompt = [...(request.messages ?? [])].reverse()
94
+ .find((message) => message.role === 'user')?.text;
95
+ return this.submit({
96
+ ...request,
97
+ prompt: lastPrompt ?? request.prompt,
98
+ action: { kind: 'retry' },
99
+ });
100
+ }
101
+ toTurnResult(response, request) {
102
+ if (!response) {
103
+ return {
104
+ state: 'error',
105
+ phase: 'capture',
106
+ assistantMessage: 'Resposta vazia da IA.',
107
+ errorText: 'Resposta vazia da IA.',
108
+ };
109
+ }
110
+ if (response.sessionId && response.sessionId !== request.sessionId) {
111
+ request = { ...request, sessionId: response.sessionId };
112
+ }
113
+ if (response.type === 'clarification') {
114
+ const questions = this.toClarificationQuestions(response);
115
+ return {
116
+ state: 'clarification',
117
+ phase: 'clarify',
118
+ sessionId: response.sessionId ?? request.sessionId,
119
+ assistantMessage: response.message || 'Preciso de mais detalhes para continuar.',
120
+ clarificationQuestions: questions,
121
+ quickReplies: this.toQuickReplies(response),
122
+ canApply: false,
123
+ };
124
+ }
125
+ if (response.type === 'info') {
126
+ const message = response.message || response.explanation || 'Informacao gerada.';
127
+ return {
128
+ state: 'success',
129
+ phase: 'summarize',
130
+ sessionId: response.sessionId ?? request.sessionId,
131
+ assistantMessage: message,
132
+ statusText: message,
133
+ canApply: false,
134
+ };
135
+ }
136
+ if (response.type === 'error') {
137
+ const message = response.message || 'Falha ao gerar alteracao de tabela.';
138
+ return {
139
+ state: 'error',
140
+ phase: 'capture',
141
+ sessionId: response.sessionId ?? request.sessionId,
142
+ assistantMessage: message,
143
+ errorText: message,
144
+ diagnostics: response.warnings?.length ? { warnings: response.warnings } : undefined,
145
+ };
146
+ }
147
+ if (response.patch && Object.keys(response.patch).length > 0) {
148
+ const warnings = response.warnings?.filter(Boolean) ?? [];
149
+ const suffix = warnings.length ? ` Avisos: ${warnings.join('; ')}` : '';
150
+ return {
151
+ state: 'review',
152
+ phase: 'review',
153
+ sessionId: response.sessionId ?? request.sessionId,
154
+ assistantMessage: `${response.explanation || 'Proposta de alteracao pronta para revisar.'}${suffix}`,
155
+ statusText: 'Revise a proposta antes de aplicar.',
156
+ canApply: true,
157
+ pendingPatch: response.patch,
158
+ preview: {
159
+ kind: 'table-config-patch',
160
+ diff: response.diff ?? [],
161
+ },
162
+ diagnostics: warnings.length ? { warnings } : undefined,
163
+ };
164
+ }
165
+ return {
166
+ state: 'success',
167
+ phase: 'summarize',
168
+ sessionId: response.sessionId ?? request.sessionId,
169
+ assistantMessage: response.message || response.explanation || 'Nenhuma alteracao necessaria.',
170
+ statusText: response.message || response.explanation || 'Nenhuma alteracao necessaria.',
171
+ canApply: false,
172
+ };
173
+ }
174
+ compileAdapterResponse(response) {
175
+ const compiled = this.adapter.compileAiResponse?.(response);
176
+ if (!compiled) {
177
+ return response;
178
+ }
179
+ const warnings = [
180
+ ...(response.warnings ?? []),
181
+ ...(compiled.warnings ?? []),
182
+ ];
183
+ return {
184
+ ...response,
185
+ ...compiled,
186
+ warnings: warnings.length ? warnings : undefined,
187
+ };
188
+ }
189
+ toChatMessages(messages, prompt) {
190
+ const supported = (messages ?? [])
191
+ .filter((message) => message.role === 'user' || message.role === 'assistant' || message.role === 'system')
192
+ .map((message) => ({
193
+ role: message.role,
194
+ content: message.text,
195
+ }))
196
+ .filter((message) => message.content.trim().length > 0);
197
+ return supported.length ? supported : [{ role: 'user', content: prompt }];
198
+ }
199
+ toClarificationQuestions(response) {
200
+ const labels = response.questions?.length
201
+ ? response.questions
202
+ : response.message
203
+ ? [response.message]
204
+ : ['Qual ajuste voce quer aplicar na tabela?'];
205
+ const options = this.toQuickReplies(response).map((reply) => ({
206
+ id: reply.id,
207
+ label: reply.label,
208
+ value: reply.prompt,
209
+ }));
210
+ return labels.map((label, index) => ({
211
+ id: `table-clarification-${index + 1}`,
212
+ type: options.length ? 'single-choice' : 'text',
213
+ label,
214
+ allowCustom: true,
215
+ options,
216
+ }));
217
+ }
218
+ toQuickReplies(response) {
219
+ const payloads = response.optionPayloads ?? [];
220
+ if (payloads.length) {
221
+ return payloads
222
+ .map((option, index) => {
223
+ const label = option.label?.trim() || option.value?.trim() || `Opcao ${index + 1}`;
224
+ const prompt = option.example?.trim() || option.value?.trim() || label;
225
+ return {
226
+ id: `option-${index + 1}`,
227
+ label,
228
+ prompt,
229
+ kind: 'clarification-option',
230
+ };
231
+ });
232
+ }
233
+ return (response.options ?? [])
234
+ .filter((option) => !!option?.trim())
235
+ .map((option, index) => ({
236
+ id: `option-${index + 1}`,
237
+ label: option.trim(),
238
+ prompt: option.trim(),
239
+ kind: 'clarification-option',
240
+ }));
241
+ }
242
+ buildCurrentStateDigest(currentState, dataProfile) {
243
+ const columns = Array.isArray(currentState['columns'])
244
+ ? currentState['columns']
245
+ .map((column) => this.toRecord(column)?.['field'])
246
+ .filter((field) => typeof field === 'string' && field.length > 0)
247
+ : undefined;
248
+ const rowCount = typeof dataProfile?.['rowCount'] === 'number' ? dataProfile['rowCount'] : undefined;
249
+ return {
250
+ ...(columns?.length ? { columns } : {}),
251
+ ...(rowCount !== undefined ? { rowCount } : {}),
252
+ };
253
+ }
254
+ optionalJsonObject(value) {
255
+ if (value === undefined || value === null) {
256
+ return undefined;
257
+ }
258
+ const object = this.toAiJsonObject(value);
259
+ return Object.keys(object).length ? object : undefined;
260
+ }
261
+ toAiJsonObject(value) {
262
+ const record = this.toRecord(value);
263
+ if (!record) {
264
+ return {};
265
+ }
266
+ try {
267
+ return JSON.parse(JSON.stringify(record));
268
+ }
269
+ catch {
270
+ return {};
271
+ }
272
+ }
273
+ toRecord(value) {
274
+ return value && typeof value === 'object' && !Array.isArray(value)
275
+ ? value
276
+ : null;
277
+ }
278
+ }
279
+
280
+ export { TableAgenticAuthoringTurnFlow };
@@ -1,7 +1,7 @@
1
1
  import { firstValueFrom } from 'rxjs';
2
2
  import { BaseAiAdapter } from '@praxisui/ai';
3
3
  import { deepMerge } from '@praxisui/core';
4
- import { TABLE_AI_CAPABILITIES, TASK_PRESETS } from './praxisui-table.mjs';
4
+ import { coerceTableComponentEditPlans, compileTableComponentEditPlans, TABLE_AI_CAPABILITIES, TASK_PRESETS, getTableComponentEditPlanCapabilities, TABLE_COMPONENT_EDIT_PLAN_EXPECTED_PATHS, TABLE_COMPONENT_EDIT_PLAN_ALLOWED_CHANGE_KINDS, TABLE_COMPONENT_EDIT_PLAN_JSON_SCHEMA, TABLE_COMPONENT_EDIT_PLAN_VERSION, TABLE_COMPONENT_EDIT_PLAN_BATCH_KIND, TABLE_COMPONENT_EDIT_PLAN_KIND } from './praxisui-table.mjs';
5
5
 
6
6
  /**
7
7
  * Analisa uma amostra de dados da tabela para gerar estatísticas
@@ -123,6 +123,25 @@ class TableAiAdapter extends BaseAiAdapter {
123
123
  this.aiService = aiService;
124
124
  }
125
125
  // -------- Core contract --------
126
+ compileAiResponse(response) {
127
+ const componentEditPlans = coerceTableComponentEditPlans(response);
128
+ if (!componentEditPlans) {
129
+ return null;
130
+ }
131
+ const compiled = compileTableComponentEditPlans(componentEditPlans, this.getCurrentConfig());
132
+ if (compiled.patch) {
133
+ return {
134
+ patch: this.normalizePatch(compiled.patch),
135
+ explanation: typeof response['explanation'] === 'string' ? response['explanation'] : compiled.explanation,
136
+ warnings: compiled.warnings,
137
+ };
138
+ }
139
+ return {
140
+ type: 'error',
141
+ message: 'O plano de edicao da tabela nao passou na validacao de capacidades.',
142
+ warnings: [...compiled.warnings, ...compiled.failureCodes],
143
+ };
144
+ }
126
145
  getCurrentConfig() {
127
146
  try {
128
147
  return structuredClone(this.table.config);
@@ -148,6 +167,31 @@ class TableAiAdapter extends BaseAiAdapter {
148
167
  isLoading: false // TODO: expose real loading flag
149
168
  };
150
169
  }
170
+ getAuthoringContext() {
171
+ return {
172
+ authoringContract: {
173
+ kind: 'praxis.component-authoring-context',
174
+ componentId: this.componentId,
175
+ componentType: this.componentType,
176
+ preferredResponse: 'componentEditPlan',
177
+ componentEditPlan: {
178
+ kind: TABLE_COMPONENT_EDIT_PLAN_KIND,
179
+ batchKind: TABLE_COMPONENT_EDIT_PLAN_BATCH_KIND,
180
+ version: TABLE_COMPONENT_EDIT_PLAN_VERSION,
181
+ schemaId: TABLE_COMPONENT_EDIT_PLAN_JSON_SCHEMA.$id,
182
+ allowedChangeKinds: [...TABLE_COMPONENT_EDIT_PLAN_ALLOWED_CHANGE_KINDS],
183
+ expectedPaths: { ...TABLE_COMPONENT_EDIT_PLAN_EXPECTED_PATHS },
184
+ capabilities: getTableComponentEditPlanCapabilities(),
185
+ rules: [
186
+ 'Use componentEditPlan instead of free patch when the request fits an allowed table edit changeKind.',
187
+ 'Use batch kind with operations for multiple table edits in one request.',
188
+ 'Use Json Logic objects for computed expressions and conditional rules.',
189
+ 'Do not invent fields, changeKinds, capabilityPaths, formats, badge variants, or colors outside the provided contract.',
190
+ ],
191
+ },
192
+ },
193
+ };
194
+ }
151
195
  getSuggestionContext() {
152
196
  const config = this.getCurrentConfig();
153
197
  const behavior = config.behavior || {};
@@ -500,8 +544,9 @@ Columns Analysis:
500
544
  // -------- Two-step flow helpers --------
501
545
  getFilteredCapabilities(category) {
502
546
  const all = TABLE_AI_CAPABILITIES.capabilities;
547
+ const editPlanCaps = getTableComponentEditPlanCapabilities();
503
548
  if (!category || category === 'unknown')
504
- return all;
549
+ return [...all, ...editPlanCaps];
505
550
  const categoryMap = {
506
551
  columns: ['columns', 'format', 'mapping', 'renderer', 'conditional'],
507
552
  appearance: ['appearance', 'conditional'],
@@ -510,7 +555,10 @@ Columns Analysis:
510
555
  actions: ['actions', 'toolbar', 'export']
511
556
  };
512
557
  const targets = categoryMap[category] || [category];
513
- return all.filter((c) => targets.includes(c.category));
558
+ return [
559
+ ...all.filter((c) => targets.includes(c.category)),
560
+ ...editPlanCaps.filter((c) => targets.includes(c.category)),
561
+ ];
514
562
  }
515
563
  suggestionsKey() {
516
564
  const id = this.table.tableId || 'default';
@@ -568,6 +616,22 @@ Columns Analysis:
568
616
  const context = this.extractContextForIntent(classification);
569
617
  const caps = this.getFilteredCapabilities(classification?.category);
570
618
  const result = await firstValueFrom(aiService.executeEnrichedPrompt(userInput, context.desc, context.config, caps));
619
+ const componentEditPlans = coerceTableComponentEditPlans(result);
620
+ if (componentEditPlans) {
621
+ const compiled = compileTableComponentEditPlans(componentEditPlans, this.getCurrentConfig());
622
+ if (compiled.patch) {
623
+ return {
624
+ patch: this.normalizePatch(compiled.patch),
625
+ explanation: result.explanation || compiled.explanation,
626
+ warnings: compiled.warnings,
627
+ };
628
+ }
629
+ return {
630
+ type: 'error',
631
+ message: 'O plano de edicao da tabela nao passou na validacao de capacidades.',
632
+ warnings: [...compiled.warnings, ...compiled.failureCodes],
633
+ };
634
+ }
571
635
  if (!result || !result.patch) {
572
636
  return { type: 'error', message: 'Nenhum patch gerado.' };
573
637
  }