@praxisui/table 8.0.0-beta.33 → 8.0.0-beta.35
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 +1 -0
- package/docs/dynamic-filter-payload-contract.md +1 -0
- package/fesm2022/{praxisui-table-praxisui-table-BoDxoDos.mjs → praxisui-table-praxisui-table-CBKUT72z.mjs} +1241 -104
- package/fesm2022/praxisui-table-table-agentic-authoring-turn-flow-PxHFt4yB.mjs +3209 -0
- package/fesm2022/{praxisui-table-table-ai.adapter-DW9a89Gl.mjs → praxisui-table-table-ai.adapter-Whr5L9XO.mjs} +439 -24
- package/fesm2022/praxisui-table.mjs +1 -1
- package/package.json +10 -10
- package/src/lib/praxis-table.json-api.md +31 -1
- package/types/praxisui-table.d.ts +117 -10
- package/fesm2022/praxisui-table-table-agentic-authoring-turn-flow-CretQI6Z.mjs +0 -847
|
@@ -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-
|
|
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 =
|
|
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
|
|
1769
|
-
//
|
|
1770
|
-
const sample =
|
|
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.
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2194
|
+
this.addFilterFieldAlias(aliases, columnField);
|
|
2044
2195
|
const header = this.normalizeAliasValue(column?.header);
|
|
2045
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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(
|
|
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
|
|
2700
|
-
if (this.
|
|
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
|
}
|