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

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,34 @@ 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
+
472
+ ## Agentic Authoring & Manifest
473
+
474
+ O `@praxisui/table` suporta authoring agentic através de um `ComponentAuthoringManifest` canônico completo. Este manifesto define o contrato executável para que agentes de IA descubram alvos e realizem operações na configuração da tabela.
475
+
476
+ - **Component ID:** `praxis-table`
477
+ - **Config Schema:** `TableConfig`
478
+ - **Alvos Editáveis (30):** `column`, `computedColumn`, `renderer`, `conditionalRenderer`, `rowAction`, `toolbarAction`, `toolbar`, `filter`, `grouping`, `selection`, `export`, `appearance`, `expansion`, `rule`, `meta`, `bulkAction`, `contextAction`, `pagination`, `sorting`, `interaction`, `loading`, `emptyState`, `resizing`, `dragging`, `editing`, `messages`, `localization`, `performance`, `data`, `accessibility`.
479
+ - **Famílias de Operações (58):** Implementação semântica rigorosa, com target resolvers próprios por operação, alinhada com o `TableConfigV2` canônico. Inclui as 22 famílias obrigatórias do gate e cobre 100% dos paths publicados em `TABLE_AI_CAPABILITIES`: metadados, colunas, renderizadores, regras condicionais, paginação, ordenação, filtros, seleção, interação, loading, empty state, resizing, dragging, edição, toolbar, ações, exportação, aparência, expansão, mensagens, localização, performance, dados e acessibilidade.
480
+ - **Validação:** Inclui 16 validadores determinísticos cobrindo unicidade, integridade de caminhos, suporte a renderizadores, presets de formatação, segurança de estilos, segurança de resource binding e round-trip do editor. A spec focal compara o manifesto contra `TABLE_AI_CAPABILITIES` e falha se qualquer path publicado ficar sem operação de authoring.
481
+
482
+ O manifesto (v2.0.0) é exportado como `PRAXIS_TABLE_AUTHORING_MANIFEST` e está integrado no `ai_registry` para backend tools.
483
+
431
484
  ## 🚀 Instalação
432
485
 
433
486
  ```bash
434
- npm install @praxisui/core @praxisui/table
487
+ npm install @praxisui/ai @praxisui/core @praxisui/table
435
488
  ```
436
489
 
437
490
  Peers necessários (instale no app host):
438
491
  - `@angular/core` `^20.0.0`, `@angular/common` `^20.0.0`
492
+ - `@praxisui/ai` `^8.0.0-beta.0`
439
493
  - `@praxisui/core` `^0.0.1`
440
494
  - `@praxisui/dynamic-fields` `^0.0.1` (quando usar editores/inputs dinâmicos)
441
495
  - `@praxisui/dynamic-form` `^0.0.1` (quando integrar com formulários dinâmicos)
@@ -896,7 +950,7 @@ onSchemaStatus(ev: { outdated: boolean; serverHash?: string; lastVerifiedAt?: st
896
950
 
897
951
  - O backend anota o schema com `x-ui.resource.idField` (e `idFieldValid`) via `/schemas/filtered`.
898
952
  - A tabela adota o campo identificador automaticamente com a seguinte precedência:
899
- - `@Input() idField` → `crudContext.idField` → `config.meta.idField` (persistido) → `GenericCrudService.getResourceIdField()` (derivado do schema) → `'id'`.
953
+ - `config.meta.idField` (persistido) → contexto runtime/serviço derivado do schema → `'id'`.
900
954
  - Se `config.meta.idField` divergir do servidor, o componente alerta o usuário e mantém o valor do TableConfig até reconciliação.
901
955
  - A resolução ocorre no `loadSchema()` e também é considerada em tempo de execução para evitar corridas.
902
956
  - Para recursos cuja PK não é `id`, defina `getIdFieldName()` no controller backend correspondente.
@@ -919,7 +973,7 @@ sequenceDiagram
919
973
  Docs-->>GS: 200/304 schema + x-ui.resource.idField
920
974
  GS->>GS: cache + lastResourceMeta.idField
921
975
  GS-->>PT: FieldDefinition[] (normalizado)
922
- Note over PT: idField = input || context || GS.getResourceIdField() || 'id'
976
+ Note over PT: idField = config.meta.idField || runtime/schema || 'id'
923
977
  PT->>PT: construir colunas e renderizar
924
978
  ```
925
979
 
@@ -944,7 +998,7 @@ sequenceDiagram
944
998
  ### Troubleshooting rápido (idField)
945
999
 
946
1000
  - A ação delete falhou por ID ausente: verifique se o schema contém `x-ui.resource.idField` e se a coluna correspondente existe no dataset.
947
- - O ID está em outra propriedade: defina `@Input() idField` ou `crudContext.idField` temporariamente; ajuste o backend com `getIdFieldName()` para persistir o comportamento.
1001
+ - O ID está em outra propriedade: defina `config.meta.idField` no TableConfig; ajuste o backend com `getIdFieldName()` para persistir o comportamento derivado do schema.
948
1002
  - Cache 304 sem idField aplicado: confirme que o serviço recebeu o body pelo menos uma vez (200) e que `GenericCrudService.getResourceIdField()` retorna o valor esperado.
949
1003
 
950
1004
  ### Uso com Dados Locais (Client-Side)
@@ -1716,4 +1770,3 @@ Apache-2.0 — consulte `LICENSE` na raiz do workspace para detalhes.
1716
1770
  **Parte do Praxis UI Workspace**
1717
1771
  **Versão**: 2.0.0 (Unified Architecture)
1718
1772
  **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,17 +167,43 @@ 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 || {};
154
198
  const actions = config.actions || {};
155
199
  const availableFeatures = [];
156
200
  const missingCapabilities = [];
201
+ const effectiveIdField = this.normalizeString(this.table.getIdField?.());
157
202
  if (!this.table.resourcePath) {
158
203
  availableFeatures.push('data-connection');
159
204
  }
160
- if (!this.table.idField) {
161
- availableFeatures.push('bindings.idField');
205
+ if (!effectiveIdField) {
206
+ availableFeatures.push('meta.idField');
162
207
  }
163
208
  if (!behavior.filtering?.enabled) {
164
209
  availableFeatures.push('behavior.filtering');
@@ -220,14 +265,15 @@ class TableAiAdapter extends BaseAiAdapter {
220
265
  authoringContract: {
221
266
  kind: 'praxis.table.editor',
222
267
  usesBindings: true,
223
- bindingsPaths: ['bindings.resourcePath', 'bindings.idField', 'bindings.horizontalScroll'],
268
+ bindingsPaths: ['bindings.resourcePath', 'bindings.horizontalScroll'],
269
+ configPaths: ['meta.idField'],
224
270
  runtimeConfigProjection: 'TableConfig',
225
271
  },
226
272
  availableFeatures,
227
273
  missingCapabilities: Array.from(new Set(missingCapabilities)),
228
274
  inputs: {
229
275
  resourcePath: this.table.resourcePath || null,
230
- idField: this.table.getIdField?.() || null,
276
+ idField: effectiveIdField || null,
231
277
  horizontalScroll: this.table.horizontalScroll || null,
232
278
  },
233
279
  };
@@ -259,6 +305,13 @@ class TableAiAdapter extends BaseAiAdapter {
259
305
  this.applyConfig(nextConfig);
260
306
  return { success: true };
261
307
  }
308
+ normalizeString(value) {
309
+ if (typeof value !== 'string') {
310
+ return null;
311
+ }
312
+ const normalized = value.trim();
313
+ return normalized ? normalized : null;
314
+ }
262
315
  // -------- Context & suggestions --------
263
316
  /**
264
317
  * Human-friendly summary for prompt/context (columns + feature flags + data stats).
@@ -500,8 +553,9 @@ Columns Analysis:
500
553
  // -------- Two-step flow helpers --------
501
554
  getFilteredCapabilities(category) {
502
555
  const all = TABLE_AI_CAPABILITIES.capabilities;
556
+ const editPlanCaps = getTableComponentEditPlanCapabilities();
503
557
  if (!category || category === 'unknown')
504
- return all;
558
+ return [...all, ...editPlanCaps];
505
559
  const categoryMap = {
506
560
  columns: ['columns', 'format', 'mapping', 'renderer', 'conditional'],
507
561
  appearance: ['appearance', 'conditional'],
@@ -510,13 +564,16 @@ Columns Analysis:
510
564
  actions: ['actions', 'toolbar', 'export']
511
565
  };
512
566
  const targets = categoryMap[category] || [category];
513
- return all.filter((c) => targets.includes(c.category));
567
+ return [
568
+ ...all.filter((c) => targets.includes(c.category)),
569
+ ...editPlanCaps.filter((c) => targets.includes(c.category)),
570
+ ];
514
571
  }
515
572
  suggestionsKey() {
516
573
  const id = this.table.tableId || 'default';
517
574
  const rp = this.table.resourcePath || 'default';
518
575
  // bump version to invalidate old cached suggestions
519
- return `ai-suggestions:v3:${id}:${rp}`;
576
+ return `ai-suggestions:v4:${id}:${rp}`;
520
577
  }
521
578
  getColumnNames() {
522
579
  return (this.table.config?.columns || [])
@@ -568,6 +625,22 @@ Columns Analysis:
568
625
  const context = this.extractContextForIntent(classification);
569
626
  const caps = this.getFilteredCapabilities(classification?.category);
570
627
  const result = await firstValueFrom(aiService.executeEnrichedPrompt(userInput, context.desc, context.config, caps));
628
+ const componentEditPlans = coerceTableComponentEditPlans(result);
629
+ if (componentEditPlans) {
630
+ const compiled = compileTableComponentEditPlans(componentEditPlans, this.getCurrentConfig());
631
+ if (compiled.patch) {
632
+ return {
633
+ patch: this.normalizePatch(compiled.patch),
634
+ explanation: result.explanation || compiled.explanation,
635
+ warnings: compiled.warnings,
636
+ };
637
+ }
638
+ return {
639
+ type: 'error',
640
+ message: 'O plano de edicao da tabela nao passou na validacao de capacidades.',
641
+ warnings: [...compiled.warnings, ...compiled.failureCodes],
642
+ };
643
+ }
571
644
  if (!result || !result.patch) {
572
645
  return { type: 'error', message: 'Nenhum patch gerado.' };
573
646
  }