@praxisui/table 8.0.0-beta.33 → 8.0.0-beta.34

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.
@@ -1,7 +1,7 @@
1
1
  import { firstValueFrom } from 'rxjs';
2
2
  import { BaseAiAdapter, createComponentAuthoringContext } from '@praxisui/ai';
3
3
  import { deepMerge } from '@praxisui/core';
4
- import { z as TABLE_COMPONENT_EDIT_PLAN_OPERATION_IDS, k as PRAXIS_TABLE_AUTHORING_MANIFEST, T as TABLE_AI_CAPABILITIES, R as coerceTableComponentEditPlans, W as compileTableComponentEditPlans, G as TASK_PRESETS, $ as getTableComponentEditPlanCapabilities, w as TABLE_COMPONENT_EDIT_PLAN_EXPECTED_PATHS, u as TABLE_COMPONENT_EDIT_PLAN_ALLOWED_CHANGE_KINDS, x as TABLE_COMPONENT_EDIT_PLAN_JSON_SCHEMA, E as TABLE_COMPONENT_EDIT_PLAN_VERSION, v as TABLE_COMPONENT_EDIT_PLAN_BATCH_KIND, y as TABLE_COMPONENT_EDIT_PLAN_KIND } from './praxisui-table-praxisui-table-BoDxoDos.mjs';
4
+ import { z as TABLE_COMPONENT_EDIT_PLAN_OPERATION_IDS, k as PRAXIS_TABLE_AUTHORING_MANIFEST, T as TABLE_AI_CAPABILITIES, R as coerceTableComponentEditPlans, W as compileTableComponentEditPlans, G as TASK_PRESETS, $ as getTableComponentEditPlanCapabilities, w as TABLE_COMPONENT_EDIT_PLAN_EXPECTED_PATHS, u as TABLE_COMPONENT_EDIT_PLAN_ALLOWED_CHANGE_KINDS, x as TABLE_COMPONENT_EDIT_PLAN_JSON_SCHEMA, E as TABLE_COMPONENT_EDIT_PLAN_VERSION, v as TABLE_COMPONENT_EDIT_PLAN_BATCH_KIND, y as TABLE_COMPONENT_EDIT_PLAN_KIND } from './praxisui-table-praxisui-table-CBKUT72z.mjs';
5
5
 
6
6
  const TABLE_COMPONENT_CONTEXT_PACK = {
7
7
  version: 'v1',
@@ -396,6 +396,7 @@ const TABLE_COMPONENT_CONTEXT_PACK = {
396
396
  options: [
397
397
  { value: 'filled', label: 'Filled' },
398
398
  { value: 'outlined', label: 'Outlined' },
399
+ { value: 'soft', label: 'Soft' },
399
400
  ],
400
401
  },
401
402
  'columns[].renderer.rating.size': {
@@ -502,6 +503,7 @@ const TABLE_COMPONENT_CONTEXT_PACK = {
502
503
  options: [
503
504
  { value: 'filled', label: 'Filled' },
504
505
  { value: 'outlined', label: 'Outlined' },
506
+ { value: 'soft', label: 'Soft' },
505
507
  ],
506
508
  },
507
509
  'columns[].conditionalRenderers[].renderer.rating.size': {
@@ -714,6 +716,17 @@ const TABLE_COMPONENT_CONTEXT_PACK = {
714
716
  columns: [{ field: '{{target}}', width: '{{value}}' }],
715
717
  },
716
718
  },
719
+ {
720
+ id: 'column.order.set',
721
+ intentExamples: ['ordem visual', 'mover coluna', 'colocar depois de', 'colocar antes de'],
722
+ scope: 'COLUMN',
723
+ valueType: 'NUMBER',
724
+ operation: 'update',
725
+ requiresExistingTarget: true,
726
+ patchTemplate: {
727
+ columns: [{ field: '{{target}}', order: '{{value}}' }],
728
+ },
729
+ },
717
730
  {
718
731
  id: 'column.align.set',
719
732
  intentExamples: ['align', 'alinhamento', 'left', 'center', 'right'],
@@ -1621,6 +1634,8 @@ const TABLE_COMPONENT_CONTEXT_PACK = {
1621
1634
  'Set columns[].type before choosing columns[].format presets to avoid mismatched formatting.',
1622
1635
  'Renderer config overrides format/valueMapping; avoid mixing unless required.',
1623
1636
  'Use ADD_CONDITIONAL_RENDERER with renderer.type + renderer.<type> config for conditional badges/chips.',
1637
+ 'When the table already shows a status/boolean column as valueMapping or chip/badge and the user refines colors, variants, icons, animation or tooltip, preserve the target column and create conditionalRenderers per semantic value instead of repeating the same valueMapping.',
1638
+ 'For boolean chips/badges, model true/false branches explicitly: condition {"==":[{"var":"<field>"},true]} for the positive label and {"==":[{"var":"<field>"},false]} for the negative label; use renderer.type "chip" or "badge" with text, color and variant.',
1624
1639
  'DOC: Widget purpose: data table with sorting, filtering, pagination, and actions.',
1625
1640
  'DOC: Host inputs: tableId (required), resourcePath (optional), config (optional).',
1626
1641
  'DOC: config defaults to createDefaultTableConfig(); columns may start empty.',
@@ -1752,7 +1767,7 @@ function isNonEmptyString(value) {
1752
1767
  * que ajudem a IA a sugerir renderers e formatos adequados.
1753
1768
  */
1754
1769
  class TableDataProfiler {
1755
- static SAMPLE_LIMIT = 3; // Analisa no máximo 3 linhas para performance
1770
+ static SAMPLE_LIMIT = 50; // Mantem custo baixo sem perder categorias comuns da pagina.
1756
1771
  static VALUE_TRUNCATE = 50; // Trunca strings longas no contexto
1757
1772
  static profile(data, config, options) {
1758
1773
  const sampleLimit = options?.sampleLimit ?? this.SAMPLE_LIMIT;
@@ -1765,9 +1780,9 @@ class TableDataProfiler {
1765
1780
  if (!data || data.length === 0 || !config.columns) {
1766
1781
  return profile;
1767
1782
  }
1768
- // 1. Selecionar amostra (início, meio e fim se possível, ou apenas slice)
1769
- // Para simplicidade e estabilidade, pegamos os primeiros N itens.
1770
- const sample = data.slice(0, sampleLimit);
1783
+ // 1. Selecionar amostra representativa. Para tabelas maiores, distribui a
1784
+ // amostra pelo conjunto inteiro em vez de olhar apenas as primeiras linhas.
1785
+ const sample = this.sampleRows(data, sampleLimit);
1771
1786
  // 2. Iterar colunas configuradas
1772
1787
  for (const col of config.columns) {
1773
1788
  if (!col || !col.field)
@@ -1827,6 +1842,23 @@ class TableDataProfiler {
1827
1842
  return undefined;
1828
1843
  return path.split('.').reduce((obj, key) => obj?.[key], row);
1829
1844
  }
1845
+ static sampleRows(data, sampleLimit) {
1846
+ if (!Array.isArray(data) || data.length === 0 || sampleLimit <= 0) {
1847
+ return [];
1848
+ }
1849
+ if (data.length <= sampleLimit) {
1850
+ return data.slice();
1851
+ }
1852
+ const indexes = new Set();
1853
+ const maxIndex = data.length - 1;
1854
+ for (let i = 0; i < sampleLimit; i += 1) {
1855
+ indexes.add(Math.round((i * maxIndex) / Math.max(1, sampleLimit - 1)));
1856
+ }
1857
+ return Array.from(indexes)
1858
+ .sort((a, b) => a - b)
1859
+ .map(index => data[index])
1860
+ .filter(row => row !== undefined && row !== null);
1861
+ }
1830
1862
  static inferType(values) {
1831
1863
  const first = values[0];
1832
1864
  if (typeof first === 'number')
@@ -1851,6 +1883,22 @@ class TableDataProfiler {
1851
1883
  }
1852
1884
  }
1853
1885
 
1886
+ const TABLE_RUNTIME_OPERATION_PATCH_KEY = 'tableRuntimeOperations';
1887
+ const TABLE_RUNTIME_OPERATION_BATCH_KIND = 'praxis.table.runtime-operation.batch';
1888
+ const TABLE_RUNTIME_OPERATION_IDS = ['table.filter.apply', 'table.export.run'];
1889
+ const TABLE_EXPORT_SCOPES = ['auto', 'selected', 'filtered', 'currentPage', 'all'];
1890
+ const TABLE_EXPORT_FORMATS = ['excel', 'pdf', 'csv', 'json', 'print'];
1891
+ const TABLE_UNSUPPORTED_FILTER_COMBINATOR_KEYS = new Set([
1892
+ '$and',
1893
+ '$not',
1894
+ '$or',
1895
+ 'allOf',
1896
+ 'and',
1897
+ 'anyOf',
1898
+ 'not',
1899
+ 'oneOf',
1900
+ 'or',
1901
+ ]);
1854
1902
  /**
1855
1903
  * Adapter that connects the AI Engine to a specific PraxisTable instance.
1856
1904
  * Implements two-step intent flow + heuristics for contextual suggestions.
@@ -1872,14 +1920,26 @@ class TableAiAdapter extends BaseAiAdapter {
1872
1920
  await tableWithFilterSchema.ensureFilterSchemaFieldsSnapshot?.();
1873
1921
  }
1874
1922
  compileAiResponse(response) {
1923
+ const runtimeOperations = this.coerceTableRuntimeOperationBatch(response);
1924
+ if (runtimeOperations) {
1925
+ return {
1926
+ patch: {
1927
+ [TABLE_RUNTIME_OPERATION_PATCH_KEY]: runtimeOperations,
1928
+ },
1929
+ explanation: typeof response['explanation'] === 'string'
1930
+ ? response['explanation']
1931
+ : 'Preparei uma operacao de runtime para a tabela.',
1932
+ };
1933
+ }
1875
1934
  const componentEditPlans = coerceTableComponentEditPlans(response);
1876
1935
  if (!componentEditPlans) {
1877
1936
  return null;
1878
1937
  }
1879
- const compiled = compileTableComponentEditPlans(componentEditPlans, this.getCurrentConfig());
1938
+ const compiled = compileTableComponentEditPlans(componentEditPlans, this.getCompileConfig());
1880
1939
  if (compiled.patch) {
1940
+ const patch = this.hydratePatchColumnsFromSchema(compiled.patch);
1881
1941
  return {
1882
- patch: this.normalizePatch(compiled.patch),
1942
+ patch: this.normalizePatch(patch),
1883
1943
  explanation: typeof response['explanation'] === 'string' ? response['explanation'] : compiled.explanation,
1884
1944
  warnings: compiled.warnings,
1885
1945
  };
@@ -1905,6 +1965,7 @@ class TableAiAdapter extends BaseAiAdapter {
1905
1965
  return TASK_PRESETS;
1906
1966
  }
1907
1967
  getRuntimeState() {
1968
+ const selectedRowsContext = this.table.getAiAssistantSelectedRowsContext?.();
1908
1969
  return {
1909
1970
  resourcePath: this.table.resourcePath || null,
1910
1971
  rowsTotal: this.table.dataSource.data.length,
@@ -1913,11 +1974,15 @@ class TableAiAdapter extends BaseAiAdapter {
1913
1974
  sort: this.table.sort ? { active: this.table.sort.active, direction: this.table.sort.direction } : null,
1914
1975
  filters: this.table.filterCriteria,
1915
1976
  selectionCount: this.table.selection.selected.length,
1977
+ ...(selectedRowsContext ? { selection: selectedRowsContext } : {}),
1916
1978
  isLoading: false // TODO: expose real loading flag
1917
1979
  };
1918
1980
  }
1919
1981
  getAuthoringContext() {
1920
1982
  const filterFieldCatalog = this.buildFilterFieldCatalog();
1983
+ const selectedRowsContext = this.getSelectedRowsAuthoringContext();
1984
+ const resourceCapabilities = this.getResourceCapabilityAuthoringContext();
1985
+ const filterExpressionSupported = resourceCapabilities?.filterExpressionSupported === true;
1921
1986
  const responseModes = [
1922
1987
  {
1923
1988
  kind: 'consult',
@@ -1932,6 +1997,12 @@ class TableAiAdapter extends BaseAiAdapter {
1932
1997
  rules: [
1933
1998
  'Return type "info" with message when answering a consultative question.',
1934
1999
  'Do not ask the user to choose a dataset when the current table already exposes resourcePath.',
2000
+ 'When runtimeState.selection exists, treat it as governed context for the rows selected by the user, using only its sanitized ids and sampleRows.',
2001
+ 'When selectedRecordsContext.selectedCount is greater than zero, do not claim that no record is selected; answer from selectedRecordsContext.sampleRows even when there is only one selected row.',
2002
+ 'When selectedRecordsContext.filterCandidates exists, treat those candidates as the canonical selection-derived bridge to advanced filters; prefer their labels and criteria over re-inferring values from prose.',
2003
+ 'When the user asks how selected records can drive advanced filters or export, bridge the selected records to declared filterFieldCatalog fields and export scopes conceptually; ask for clarification when the shared property or export scope is ambiguous.',
2004
+ 'For ambiguous selected-record bridge requests that can become a real table operation, prefer type "clarification" with concise user-facing options grounded in filterFieldCatalog labels or export scopes; do not answer with a long documentation-style explanation.',
2005
+ 'Keep selected-record bridge responses human-facing: mention selected record count and meaningful labels, but do not expose raw ids, endpoint paths, operationIds, internal field names, schema keys, or payload examples unless the user explicitly asks for technical details.',
1935
2006
  'For how-to or documentation-style turns, explain the governed operation shape and give a safe example instead of collecting missing edit parameters.',
1936
2007
  'Do not return clarification for missing target fields, labels, formats, colors, conditions, or columns unless the selected response mode is edit/componentEditPlan.',
1937
2008
  'Only produce componentEditPlan when the user asks to apply or materialize a table change.',
@@ -1954,10 +2025,15 @@ class TableAiAdapter extends BaseAiAdapter {
1954
2025
  responseModes,
1955
2026
  consultativeContext: {
1956
2027
  resourcePath: this.table.resourcePath || null,
2028
+ ...(resourceCapabilities ? { resourceCapabilities } : {}),
2029
+ ...(selectedRowsContext ? { selectedRecordsContext: selectedRowsContext } : {}),
1957
2030
  answerableQuestionKinds: [
1958
2031
  'current-resource-path',
1959
2032
  'current-endpoint',
1960
2033
  'schema-fields',
2034
+ 'selected-records',
2035
+ 'selection-derived-filter-candidates',
2036
+ 'selection-derived-export-scope',
1961
2037
  'styling-capabilities',
1962
2038
  'conditional-renderers',
1963
2039
  'conditional-styles',
@@ -1979,14 +2055,82 @@ class TableAiAdapter extends BaseAiAdapter {
1979
2055
  'Use componentEditPlan instead of free patch when the request fits an allowed table operationId or changeKind.',
1980
2056
  'Prefer manifest operationId for governed agentic authoring responses.',
1981
2057
  'Use batch kind with operations for multiple table edits in one request.',
2058
+ 'For column.order.set, use a zero-based visual index from currentStateDigest.columnOrder; when the user says after a column, use that column index + 1, and when the user says before a column, use that column index.',
1982
2059
  'Use Json Logic objects for computed expressions and conditional rules.',
2060
+ 'For selected-record bridge requests, configure available table capabilities such as filter.advanced.configure, filter.advanced.fields.add, toolbar.action.add, or export.configure; do not invent a runtime apply-filter or run-export operation unless it is declared in the contract.',
1983
2061
  'Do not invent fields, operationIds, changeKinds, capabilityPaths, formats, badge variants, or colors outside the provided contract.',
1984
2062
  ],
1985
2063
  operationRegistryCoverage: TABLE_AUTHORING_REGISTRY_COVERAGE,
1986
2064
  },
2065
+ runtimeOperations: {
2066
+ kind: TABLE_RUNTIME_OPERATION_BATCH_KIND,
2067
+ allowedOperationIds: [...TABLE_RUNTIME_OPERATION_IDS],
2068
+ filterExpression: {
2069
+ supported: filterExpressionSupported,
2070
+ source: resourceCapabilities?.source ?? 'unavailable',
2071
+ materialization: filterExpressionSupported
2072
+ ? 'Capability advertised by resource metadata; this table runtime still declares only table.filter.apply for simple conjunction criteria.'
2073
+ : 'Compound OR/nested boolean filters are not materializable by the current resource/runtime contract.',
2074
+ },
2075
+ operations: [
2076
+ {
2077
+ operationId: 'table.filter.apply',
2078
+ inputSchema: {
2079
+ criteria: 'object',
2080
+ criteriaSemantics: 'simple-conjunction',
2081
+ source: ['selected-records', 'manual', 'current-context'],
2082
+ },
2083
+ materializes: 'Aplica criterios em conjunto no estado runtime dos filtros avancados.',
2084
+ },
2085
+ {
2086
+ operationId: 'table.export.run',
2087
+ inputSchema: {
2088
+ format: [...TABLE_EXPORT_FORMATS],
2089
+ scope: [...TABLE_EXPORT_SCOPES],
2090
+ },
2091
+ materializes: 'Dispara exportacao da tabela usando o pipeline canonico de Collection Export.',
2092
+ },
2093
+ ],
2094
+ rules: [
2095
+ 'Use table.filter.apply only when the user asks to apply current table filters now; criteria must be grounded in declared filterFieldCatalog fields and selectedRecordsContext values when selection is the source.',
2096
+ 'When selectedRecordsContext.filterCandidates contains a candidate whose label or field matches the requested selected-record filter, emit table.filter.apply with that candidate criteria exactly.',
2097
+ 'For filterFieldCatalog fields with criterionKind "range" and selectedRecordsContext as the source, derive criteria from the minimum and maximum selected values in relatedColumnFields and emit { start, end }; for criterionKind "date-range", emit { startDate, endDate } from the earliest and latest selected dates.',
2098
+ 'When the user asks for similar, matching, related, or comparable records from selected records without naming which property should define similarity, return clarification with options derived from filterFieldCatalog labels instead of info text or a patch.',
2099
+ 'When runtimeOperations.filterExpression.supported is not true, do not materialize OR, alternative groups, or nested boolean filters; ask which single filter field/value should be applied or explain the limitation.',
2100
+ 'table.filter.apply accepts only simple conjunction criteria over declared fields; do not emit OR/AND groups, anyOf/oneOf/allOf, or nested boolean filter expressions. Ask for clarification or explain the limitation when the user needs alternatives.',
2101
+ 'Use table.export.run only when the user asks to run an export now; choose scope selected for selected records, filtered for filtered results, and ask for clarification when the user does not specify enough context.',
2102
+ 'For combined runtime requests, return every requested operation in order inside the same tableRuntimeOperations batch; when the user asks to filter and then export the filtered results, emit table.filter.apply first and table.export.run second with scope filtered.',
2103
+ ],
2104
+ },
1987
2105
  };
1988
2106
  return createComponentAuthoringContext(authoringContract);
1989
2107
  }
2108
+ getSelectedRowsAuthoringContext() {
2109
+ try {
2110
+ const selection = this.table.getAiAssistantSelectedRowsContext?.();
2111
+ return selection && typeof selection === 'object' ? selection : null;
2112
+ }
2113
+ catch {
2114
+ return null;
2115
+ }
2116
+ }
2117
+ getResourceCapabilityAuthoringContext() {
2118
+ const tableWithCapabilities = this.table;
2119
+ try {
2120
+ const digest = tableWithCapabilities.getResourceCapabilityDigest?.();
2121
+ if (!digest || typeof digest !== 'object')
2122
+ return null;
2123
+ return {
2124
+ source: digest['source'] || 'schema-x-ui-resource',
2125
+ resourcePath: digest['resourcePath'] || this.table.resourcePath || null,
2126
+ canonicalOperations: { ...(digest['canonicalOperations'] || {}) },
2127
+ filterExpressionSupported: digest['filterExpressionSupported'] === true,
2128
+ };
2129
+ }
2130
+ catch {
2131
+ return null;
2132
+ }
2133
+ }
1990
2134
  buildFilterFieldCatalog() {
1991
2135
  const fields = this.getFilterSchemaFields();
1992
2136
  if (!fields.length)
@@ -1997,11 +2141,17 @@ class TableAiAdapter extends BaseAiAdapter {
1997
2141
  resourcePath: this.table.resourcePath || null,
1998
2142
  fields: fields.map((field) => {
1999
2143
  const aliases = this.buildFilterFieldAliases(field, columns);
2144
+ const relatedColumnFields = this.toStringArray(field['relatedColumnFields']);
2145
+ const relatedColumnLabels = this.toStringArray(field['relatedColumnLabels']);
2146
+ const criterionSemantics = this.inferFilterCriterionSemantics(field);
2000
2147
  return {
2001
2148
  name: field['name'],
2002
2149
  label: field['label'] ?? field['name'],
2003
2150
  type: field['type'] ?? null,
2004
2151
  controlType: field['controlType'] ?? null,
2152
+ ...(relatedColumnFields.length ? { relatedColumnFields } : {}),
2153
+ ...(relatedColumnLabels.length ? { relatedColumnLabels } : {}),
2154
+ ...(criterionSemantics ? criterionSemantics : {}),
2005
2155
  ...(aliases.length ? { aliases } : {}),
2006
2156
  };
2007
2157
  }),
@@ -2018,20 +2168,21 @@ class TableAiAdapter extends BaseAiAdapter {
2018
2168
  }
2019
2169
  buildFilterFieldAliases(field, columns) {
2020
2170
  const aliases = new Set();
2171
+ const declaredAliases = Array.isArray(field['aliases']) ? field['aliases'] : [];
2172
+ for (const value of declaredAliases) {
2173
+ this.addFilterFieldAlias(aliases, value);
2174
+ }
2021
2175
  for (const key of ['name', 'label']) {
2022
2176
  const value = this.normalizeAliasValue(field[key]);
2023
- if (value)
2024
- aliases.add(value);
2177
+ this.addFilterFieldAlias(aliases, value);
2025
2178
  }
2026
2179
  for (const value of field['relatedColumnFields'] || []) {
2027
2180
  const alias = this.normalizeAliasValue(value);
2028
- if (alias)
2029
- aliases.add(alias);
2181
+ this.addFilterFieldAlias(aliases, alias);
2030
2182
  }
2031
2183
  for (const value of field['relatedColumnLabels'] || []) {
2032
2184
  const alias = this.normalizeAliasValue(value);
2033
- if (alias)
2034
- aliases.add(alias);
2185
+ this.addFilterFieldAlias(aliases, alias);
2035
2186
  }
2036
2187
  for (const column of columns || []) {
2037
2188
  const columnField = this.normalizeAliasValue(column?.field);
@@ -2040,14 +2191,41 @@ class TableAiAdapter extends BaseAiAdapter {
2040
2191
  const fieldName = this.normalizeAliasValue(field['name']);
2041
2192
  const fieldLabel = this.normalizeAliasValue(field['label']);
2042
2193
  if (fieldName.includes(columnField) || fieldLabel.includes(columnField)) {
2043
- aliases.add(columnField);
2194
+ this.addFilterFieldAlias(aliases, columnField);
2044
2195
  const header = this.normalizeAliasValue(column?.header);
2045
- if (header)
2046
- aliases.add(header);
2196
+ this.addFilterFieldAlias(aliases, header);
2047
2197
  }
2048
2198
  }
2049
2199
  return [...aliases];
2050
2200
  }
2201
+ addFilterFieldAlias(aliases, value) {
2202
+ const alias = this.normalizeAliasValue(value);
2203
+ if (!alias)
2204
+ return;
2205
+ aliases.add(alias);
2206
+ for (const variant of this.semanticFilterAliasVariants(alias)) {
2207
+ aliases.add(variant);
2208
+ }
2209
+ }
2210
+ semanticFilterAliasVariants(alias) {
2211
+ const variants = new Set();
2212
+ const tokens = alias.split(/\s+/u).filter((token) => token.length > 0);
2213
+ const addIfTokenPresent = (token, values) => {
2214
+ if (tokens.includes(token)) {
2215
+ for (const value of values)
2216
+ variants.add(value);
2217
+ }
2218
+ };
2219
+ addIfTokenPresent('departamento', ['depto', 'depart', 'department', 'departamento']);
2220
+ addIfTokenPresent('departamentos', ['depto', 'depart', 'department', 'departamento']);
2221
+ addIfTokenPresent('admissao', ['admitido', 'admitidos', 'entrada', 'ingresso']);
2222
+ addIfTokenPresent('admissoes', ['admitido', 'admitidos', 'entrada', 'ingresso']);
2223
+ addIfTokenPresent('salario', ['remuneracao', 'remuneracoes', 'ganho', 'ganhos']);
2224
+ addIfTokenPresent('salarial', ['remuneracao', 'remuneracoes', 'ganho', 'ganhos']);
2225
+ addIfTokenPresent('cargo', ['funcao', 'funcoes', 'papel', 'papeis']);
2226
+ addIfTokenPresent('cargos', ['funcao', 'funcoes', 'papel', 'papeis']);
2227
+ return [...variants];
2228
+ }
2051
2229
  normalizeAliasValue(value) {
2052
2230
  if (typeof value !== 'string')
2053
2231
  return '';
@@ -2059,6 +2237,37 @@ class TableAiAdapter extends BaseAiAdapter {
2059
2237
  .trim()
2060
2238
  .toLowerCase();
2061
2239
  }
2240
+ toStringArray(value) {
2241
+ if (!Array.isArray(value))
2242
+ return [];
2243
+ return value.filter((item) => typeof item === 'string' && item.trim().length > 0);
2244
+ }
2245
+ inferFilterCriterionSemantics(field) {
2246
+ const name = typeof field['name'] === 'string' ? field['name'] : '';
2247
+ const normalizedName = this.normalizeAliasValue(name);
2248
+ const normalizedType = this.normalizeAliasValue(field['type']);
2249
+ const normalizedControlType = this.normalizeAliasValue(field['controlType']);
2250
+ const isRange = normalizedControlType.includes('range')
2251
+ || /(?:Between|Range)$/u.test(name);
2252
+ if (!isRange) {
2253
+ return /IdsIn$/u.test(name)
2254
+ ? {
2255
+ criterionKind: 'set',
2256
+ criterionValueShape: 'array',
2257
+ }
2258
+ : null;
2259
+ }
2260
+ const isDate = normalizedType.includes('date')
2261
+ || normalizedControlType.includes('date')
2262
+ || normalizedType.includes('time')
2263
+ || normalizedControlType.includes('time');
2264
+ return {
2265
+ criterionKind: isDate ? 'date-range' : 'range',
2266
+ criterionValueShape: isDate ? 'startDate-endDate' : 'start-end',
2267
+ selectedRecordsAggregation: 'min-max',
2268
+ ...(normalizedName ? { semanticSourceHint: normalizedName } : {}),
2269
+ };
2270
+ }
2062
2271
  getSuggestionContext() {
2063
2272
  const config = this.getCurrentConfig();
2064
2273
  const behavior = config.behavior || {};
@@ -2150,12 +2359,86 @@ class TableAiAdapter extends BaseAiAdapter {
2150
2359
  return TableDataProfiler.profile(data, this.table.config);
2151
2360
  }
2152
2361
  getSchemaFields() {
2362
+ const fieldsByName = new Map();
2153
2363
  try {
2154
- return this.table.getSchemaFieldsSnapshot();
2364
+ for (const field of this.table.getSchemaFieldsSnapshot()) {
2365
+ const name = String(field?.['name'] || '').trim();
2366
+ if (name)
2367
+ fieldsByName.set(name, field);
2368
+ }
2155
2369
  }
2156
2370
  catch {
2157
- return [];
2371
+ // Optional runtime snapshot; filter schema below can still ground available fields.
2372
+ }
2373
+ for (const field of this.getFilterSchemaFields()) {
2374
+ const name = String(field?.['name'] || '').trim();
2375
+ if (!name || fieldsByName.has(name))
2376
+ continue;
2377
+ fieldsByName.set(name, field);
2378
+ }
2379
+ return Array.from(fieldsByName.values());
2380
+ }
2381
+ getCompileConfig() {
2382
+ const currentConfig = this.getCurrentConfig();
2383
+ const columns = [...(currentConfig.columns || [])];
2384
+ const existingFields = new Set(columns
2385
+ .map((column) => String(column?.field || '').trim())
2386
+ .filter(Boolean));
2387
+ for (const field of this.getSchemaFields()) {
2388
+ const name = typeof field['name'] === 'string' ? field['name'].trim() : '';
2389
+ if (!name || existingFields.has(name))
2390
+ continue;
2391
+ existingFields.add(name);
2392
+ const header = typeof field['label'] === 'string' && field['label'].trim()
2393
+ ? field['label'].trim()
2394
+ : name;
2395
+ const type = typeof field['type'] === 'string' && field['type'].trim()
2396
+ ? field['type'].trim()
2397
+ : undefined;
2398
+ columns.push({
2399
+ field: name,
2400
+ header,
2401
+ ...(type ? { type } : {}),
2402
+ });
2158
2403
  }
2404
+ return {
2405
+ ...currentConfig,
2406
+ columns,
2407
+ };
2408
+ }
2409
+ hydratePatchColumnsFromSchema(patch) {
2410
+ const patchColumns = patch.columns;
2411
+ if (!Array.isArray(patchColumns) || patchColumns.length === 0)
2412
+ return patch;
2413
+ const currentFields = new Set((this.getCurrentConfig().columns || [])
2414
+ .map((column) => String(column?.field || '').trim())
2415
+ .filter(Boolean));
2416
+ const schemaFieldsByName = new Map(this.getSchemaFields()
2417
+ .map((field) => [String(field['name'] || '').trim(), field])
2418
+ .filter(([name]) => !!name));
2419
+ const columns = patchColumns.map((column) => {
2420
+ const field = String(column?.field || '').trim();
2421
+ if (!field || currentFields.has(field))
2422
+ return column;
2423
+ const schemaField = schemaFieldsByName.get(field);
2424
+ if (!schemaField)
2425
+ return column;
2426
+ const header = typeof schemaField['label'] === 'string' && schemaField['label'].trim()
2427
+ ? schemaField['label'].trim()
2428
+ : field;
2429
+ const type = typeof schemaField['type'] === 'string' && schemaField['type'].trim()
2430
+ ? schemaField['type'].trim()
2431
+ : undefined;
2432
+ return {
2433
+ ...column,
2434
+ ...(!column.header ? { header } : {}),
2435
+ ...(type && !column.type ? { type } : {}),
2436
+ };
2437
+ });
2438
+ return {
2439
+ ...patch,
2440
+ columns,
2441
+ };
2159
2442
  }
2160
2443
  createSnapshot() {
2161
2444
  return this.getCurrentConfig();
@@ -2165,13 +2448,141 @@ class TableAiAdapter extends BaseAiAdapter {
2165
2448
  return;
2166
2449
  this.applyConfig(snapshot);
2167
2450
  }
2451
+ coerceTableRuntimeOperationBatch(source) {
2452
+ if (!source)
2453
+ return null;
2454
+ const nestedPatch = this.toRecord(source['patch']);
2455
+ if (nestedPatch) {
2456
+ const nested = this.coerceTableRuntimeOperationBatch(nestedPatch);
2457
+ if (nested)
2458
+ return nested;
2459
+ }
2460
+ const nestedComponentEditPlan = this.toRecord(source['componentEditPlan'])
2461
+ ?? (this.stringValue(source['type']) === 'componentEditPlan' ? source : null);
2462
+ if (nestedComponentEditPlan && nestedComponentEditPlan !== source) {
2463
+ const nested = this.coerceTableRuntimeOperationBatch(nestedComponentEditPlan);
2464
+ if (nested)
2465
+ return nested;
2466
+ }
2467
+ const raw = this.toRecord(source[TABLE_RUNTIME_OPERATION_PATCH_KEY])
2468
+ ?? this.toRecord(source['runtimeOperationBatch'])
2469
+ ?? this.toRecord(source['tableRuntimeOperation'])
2470
+ ?? nestedComponentEditPlan;
2471
+ const directOperation = this.toRecord(source['runtimeOperation']);
2472
+ const directOperations = Array.isArray(source['runtimeOperations']) ? source['runtimeOperations'] : null;
2473
+ const operationCandidates = raw
2474
+ ? raw['operations']
2475
+ : directOperation
2476
+ ? [directOperation]
2477
+ : directOperations;
2478
+ if (!Array.isArray(operationCandidates))
2479
+ return null;
2480
+ const operations = operationCandidates
2481
+ .map((candidate) => this.coerceTableRuntimeOperation(candidate))
2482
+ .filter((operation) => !!operation);
2483
+ if (!operations.length)
2484
+ return null;
2485
+ return {
2486
+ kind: TABLE_RUNTIME_OPERATION_BATCH_KIND,
2487
+ operations,
2488
+ };
2489
+ }
2490
+ coerceTableRuntimeOperation(value) {
2491
+ const record = this.toRecord(value);
2492
+ if (!record)
2493
+ return null;
2494
+ const operationId = this.stringValue(record['operationId'] ?? record['type']);
2495
+ if (!this.isRuntimeOperationId(operationId))
2496
+ return null;
2497
+ const input = this.toRecord(record['input'] ?? record['params']) ?? {};
2498
+ return {
2499
+ operationId,
2500
+ input,
2501
+ };
2502
+ }
2168
2503
  async applyPatch(patch, intent) {
2504
+ const runtimeOperations = this.coerceTableRuntimeOperationBatch(patch);
2505
+ if (runtimeOperations) {
2506
+ return this.executeTableRuntimeOperations(runtimeOperations);
2507
+ }
2169
2508
  const current = this.getCurrentConfig();
2170
2509
  const normalizedPatch = this.normalizePatch(patch);
2171
2510
  const nextConfig = this.smartMergeTableConfig(current, normalizedPatch);
2172
2511
  this.applyConfig(nextConfig);
2173
2512
  return { success: true };
2174
2513
  }
2514
+ async executeTableRuntimeOperations(batch) {
2515
+ const warnings = [];
2516
+ for (const operation of batch.operations) {
2517
+ if (operation.operationId === 'table.filter.apply') {
2518
+ const criteria = this.toRecord(operation.input['criteria']);
2519
+ if (!criteria) {
2520
+ return {
2521
+ success: false,
2522
+ error: 'A operacao table.filter.apply exige criterios de filtro.',
2523
+ };
2524
+ }
2525
+ if (this.hasUnsupportedFilterCombinator(criteria)) {
2526
+ return {
2527
+ success: false,
2528
+ error: 'A operacao table.filter.apply aceita apenas criterios simples em conjunto. Expressoes booleanas compostas ainda nao fazem parte do contrato runtime da tabela.',
2529
+ };
2530
+ }
2531
+ this.table.onAdvancedFilterSubmit(criteria);
2532
+ continue;
2533
+ }
2534
+ if (operation.operationId === 'table.export.run') {
2535
+ const format = this.stringValue(operation.input['format']).toLowerCase();
2536
+ if (!this.isExportFormat(format)) {
2537
+ return {
2538
+ success: false,
2539
+ error: 'A operacao table.export.run exige um formato de exportacao valido.',
2540
+ };
2541
+ }
2542
+ if (!this.table.canRunExportAction(format)) {
2543
+ return {
2544
+ success: false,
2545
+ error: 'A operacao table.export.run exige exportacao habilitada e um formato permitido pela configuracao da tabela.',
2546
+ };
2547
+ }
2548
+ const scope = this.stringValue(operation.input['scope']);
2549
+ await this.table.onExportAction({
2550
+ format,
2551
+ ...(this.isExportScope(scope) ? { scope } : {}),
2552
+ });
2553
+ continue;
2554
+ }
2555
+ warnings.push(`Operacao runtime ignorada: ${operation.operationId}`);
2556
+ }
2557
+ return {
2558
+ success: true,
2559
+ ...(warnings.length ? { warnings } : {}),
2560
+ };
2561
+ }
2562
+ isRuntimeOperationId(value) {
2563
+ return TABLE_RUNTIME_OPERATION_IDS.includes(value);
2564
+ }
2565
+ isExportFormat(value) {
2566
+ return TABLE_EXPORT_FORMATS.includes(value);
2567
+ }
2568
+ isExportScope(value) {
2569
+ return TABLE_EXPORT_SCOPES.includes(value);
2570
+ }
2571
+ toRecord(value) {
2572
+ return value && typeof value === 'object' && !Array.isArray(value)
2573
+ ? value
2574
+ : null;
2575
+ }
2576
+ hasUnsupportedFilterCombinator(criteria) {
2577
+ return Object.entries(criteria).some(([key, value]) => {
2578
+ if (!TABLE_UNSUPPORTED_FILTER_COMBINATOR_KEYS.has(key))
2579
+ return false;
2580
+ return Array.isArray(value) || this.toRecord(value) !== null;
2581
+ });
2582
+ }
2583
+ stringValue(value) {
2584
+ return typeof value === 'string' ? value.trim() : '';
2585
+ }
2175
2586
  normalizeString(value) {
2176
2587
  if (typeof value !== 'string') {
2177
2588
  return null;
@@ -2494,10 +2905,11 @@ Columns Analysis:
2494
2905
  const result = await firstValueFrom(aiService.executeEnrichedPrompt(userInput, context.desc, context.config, caps));
2495
2906
  const componentEditPlans = coerceTableComponentEditPlans(result);
2496
2907
  if (componentEditPlans) {
2497
- const compiled = compileTableComponentEditPlans(componentEditPlans, this.getCurrentConfig());
2908
+ const compiled = compileTableComponentEditPlans(componentEditPlans, this.getCompileConfig());
2498
2909
  if (compiled.patch) {
2910
+ const patch = this.hydratePatchColumnsFromSchema(compiled.patch);
2499
2911
  return {
2500
- patch: this.normalizePatch(compiled.patch),
2912
+ patch: this.normalizePatch(patch),
2501
2913
  explanation: result.explanation || compiled.explanation,
2502
2914
  warnings: compiled.warnings,
2503
2915
  };
@@ -2696,14 +3108,17 @@ Columns Analysis:
2696
3108
  return col;
2697
3109
  const next = { ...col };
2698
3110
  // Normalize formats
2699
- if (next.format && !next.type) {
2700
- if (this.looksLikeCurrencyFormat(next.format)) {
3111
+ if (next.format) {
3112
+ if (this.looksLikeMaskFormat(next.format)) {
3113
+ next.type = 'string';
3114
+ }
3115
+ else if (!next.type && this.looksLikeCurrencyFormat(next.format)) {
2701
3116
  next.type = 'currency';
2702
3117
  }
2703
- else if (this.looksLikeBooleanFormat(next.format)) {
3118
+ else if (!next.type && this.looksLikeBooleanFormat(next.format)) {
2704
3119
  next.type = 'boolean';
2705
3120
  }
2706
- else if (this.looksLikeDateFormat(next.format)) {
3121
+ else if (!next.type && this.looksLikeDateFormat(next.format)) {
2707
3122
  next.type = 'date';
2708
3123
  }
2709
3124
  }