@praxisui/table 8.0.0-beta.7 → 8.0.0-beta.70
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 +191 -11
- package/docs/DSL-Extensions-Guide.md +23 -0
- package/docs/adr/2026-03-dynamic-filter-cross-lib-coupling.md +107 -0
- package/docs/adr/2026-03-filter-drawer-adapter-light-entrypoint.md +105 -0
- package/docs/adr/2026-03-table-editor-idfield-decision.md +85 -0
- package/docs/column-resize-reorder-implementation-plan.md +338 -0
- package/docs/column-resize-reorder-review-prompt.md +34 -0
- package/docs/dynamic-filter-architecture-overview.md +207 -0
- package/docs/dynamic-filter-backend-contract-cheatsheet.md +167 -0
- package/docs/dynamic-filter-editor-settings-guide.md +229 -0
- package/docs/dynamic-filter-host-integration-guide.md +266 -0
- package/docs/dynamic-filter-payload-contract.md +332 -0
- package/docs/dynamic-filter-range-filters-guide.md +296 -0
- package/docs/dynamic-filter-troubleshooting-guide.md +257 -0
- package/docs/dynamic-inline-filter-catalog.md +147 -0
- package/docs/e2e-column-drag-playwright.md +62 -0
- package/docs/expandable-rows-enterprise-big-leagues-plan.md +1080 -0
- package/docs/json-logic-operators-and-helpers.md +57 -0
- package/docs/local-data-mode-precedence.md +12 -0
- package/docs/local-data-pre-implementation-baseline.md +22 -0
- package/docs/local-data-preimplementation-go-no-go.md +39 -0
- package/docs/local-data-support-implementation-plan.md +524 -0
- package/docs/local-data-support-pr-package.md +66 -0
- package/docs/localization-persistence-merge.md +22 -0
- package/docs/performance-hardening-v2-implementation-plan.md +479 -0
- package/docs/playground-scenario-curation-plan.md +482 -0
- package/docs/playground-scenario-second-opinion-prompt.md +121 -0
- package/docs/playground-scenario-second-opinion-review.md +234 -0
- package/docs/release-notes-p1-hardening.md +76 -0
- package/docs/table-authoring-document-completeness-checklist.md +120 -0
- package/docs/table-editor-capability-review-prompt.md +349 -0
- package/docs/visual-rules-editor-transition.md +29 -0
- package/fesm2022/praxisui-table-filter-form-dialog-host.component-DbwGIMjF.mjs +232 -0
- package/fesm2022/praxisui-table-praxisui-table-CRPaOsiX.mjs +59795 -0
- package/fesm2022/praxisui-table-table-agentic-authoring-turn-flow-CmfIj9ao.mjs +2845 -0
- package/fesm2022/praxisui-table-table-ai.adapter-DMlNQpdn.mjs +3558 -0
- package/fesm2022/praxisui-table.mjs +1 -51444
- package/filter-drawer-adapter/package.json +2 -1
- package/package.json +22 -15
- package/src/lib/praxis-table.json-api.md +1357 -0
- package/{index.d.ts → types/praxisui-table.d.ts} +545 -120
- package/fesm2022/praxisui-table-filter-form-dialog-host.component-Dm2f0muy.mjs +0 -165
- package/fesm2022/praxisui-table-table-agentic-authoring-turn-flow-tu7jtTwV.mjs +0 -280
- package/fesm2022/praxisui-table-table-ai.adapter-DxjDaQqy.mjs +0 -895
- /package/{filter-drawer-adapter/index.d.ts → types/praxisui-table-filter-drawer-adapter.d.ts} +0 -0
|
@@ -0,0 +1,2845 @@
|
|
|
1
|
+
import { firstValueFrom, Observable } from 'rxjs';
|
|
2
|
+
|
|
3
|
+
class TableAgenticAuthoringTurnFlow {
|
|
4
|
+
adapter;
|
|
5
|
+
aiApi;
|
|
6
|
+
mode = 'agentic-authoring';
|
|
7
|
+
filterFieldLabels = new Map();
|
|
8
|
+
filterFieldCatalogEntries = [];
|
|
9
|
+
selectionDerivedFilterCandidateFields = new Set();
|
|
10
|
+
selectedRecordFieldsForTurn = new Set();
|
|
11
|
+
selectedRecordsCountForTurn = 0;
|
|
12
|
+
filterExpressionSupportedForTurn = null;
|
|
13
|
+
contextHintsForTurn = null;
|
|
14
|
+
streamTerminalWatchdogMs = 75_000;
|
|
15
|
+
streamTerminalReconnectLimit = 2;
|
|
16
|
+
selectedRecordStreamTerminalWatchdogMs = 45_000;
|
|
17
|
+
selectedRecordSurfaceStreamTerminalWatchdogMs = 18_000;
|
|
18
|
+
constructor(adapter, aiApi) {
|
|
19
|
+
this.adapter = adapter;
|
|
20
|
+
this.aiApi = aiApi;
|
|
21
|
+
}
|
|
22
|
+
submit(request) {
|
|
23
|
+
const prompt = (request.prompt ?? '').trim();
|
|
24
|
+
if (!prompt) {
|
|
25
|
+
return Promise.resolve({
|
|
26
|
+
state: 'listening',
|
|
27
|
+
phase: 'capture',
|
|
28
|
+
statusText: '',
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
const componentId = this.adapter.componentId || request.componentId || 'praxis-table';
|
|
32
|
+
const componentType = this.adapter.componentType || request.componentType || 'table';
|
|
33
|
+
const currentState = this.toAiJsonObject(this.adapter.getCurrentConfig());
|
|
34
|
+
const dataProfile = this.optionalJsonObject(this.adapter.getDataProfile?.());
|
|
35
|
+
const pendingCompletion = this.completePendingComponentEditClarification(request);
|
|
36
|
+
if (pendingCompletion) {
|
|
37
|
+
return Promise.resolve(this.toTurnResult(this.compileAdapterResponse(pendingCompletion, request), request));
|
|
38
|
+
}
|
|
39
|
+
const run = async () => {
|
|
40
|
+
await this.prepareAuthoringContext();
|
|
41
|
+
const runtimeState = this.optionalJsonObject(this.adapter.getRuntimeState?.());
|
|
42
|
+
const schemaFields = this.adapter.getSchemaFields?.()
|
|
43
|
+
?.map((field) => this.toAiJsonObject(field))
|
|
44
|
+
.filter((field) => Object.keys(field).length > 0);
|
|
45
|
+
const contextHints = this.mergeJsonObjects(this.mergeJsonObjects(this.optionalJsonObject(this.adapter.getAuthoringContext?.()), this.tableConversationMemoryHints(request)), this.optionalJsonObject(request.action?.contextHints)) ?? null;
|
|
46
|
+
this.filterFieldCatalogEntries = this.extractFilterFieldCatalogEntries(contextHints);
|
|
47
|
+
this.filterFieldLabels = this.extractFilterFieldLabels(this.filterFieldCatalogEntries);
|
|
48
|
+
this.selectionDerivedFilterCandidateFields = this.extractSelectionDerivedFilterCandidateFields(contextHints);
|
|
49
|
+
this.selectedRecordFieldsForTurn = this.extractSelectedRecordFields(contextHints);
|
|
50
|
+
this.selectedRecordsCountForTurn = this.extractSelectedRecordsCount(contextHints);
|
|
51
|
+
this.filterExpressionSupportedForTurn = this.resolveFilterExpressionSupported(contextHints);
|
|
52
|
+
this.contextHintsForTurn = contextHints;
|
|
53
|
+
const messages = this.withCapabilitySystemMessages(this.toChatMessages(request.messages, prompt), contextHints);
|
|
54
|
+
return {
|
|
55
|
+
contextHints,
|
|
56
|
+
patchRequest: {
|
|
57
|
+
componentId,
|
|
58
|
+
componentType,
|
|
59
|
+
userPrompt: prompt,
|
|
60
|
+
sessionId: request.sessionId,
|
|
61
|
+
clientTurnId: request.clientTurnId,
|
|
62
|
+
messages,
|
|
63
|
+
currentState,
|
|
64
|
+
currentStateDigest: this.buildCurrentStateDigest(currentState, dataProfile),
|
|
65
|
+
uiContextRef: {
|
|
66
|
+
componentId,
|
|
67
|
+
componentType,
|
|
68
|
+
},
|
|
69
|
+
...(dataProfile ? { dataProfile } : {}),
|
|
70
|
+
...(runtimeState ? { runtimeState } : {}),
|
|
71
|
+
...(schemaFields?.length ? { schemaFields } : {}),
|
|
72
|
+
...(contextHints ? { contextHints } : {}),
|
|
73
|
+
},
|
|
74
|
+
};
|
|
75
|
+
};
|
|
76
|
+
const progressPrompt = this.progressPromptFor(request, prompt);
|
|
77
|
+
if (this.canUsePatchStream()) {
|
|
78
|
+
return this.submitViaStream(request, progressPrompt, currentState, componentId, run);
|
|
79
|
+
}
|
|
80
|
+
return this.submitViaSnapshot(request, currentState, run);
|
|
81
|
+
}
|
|
82
|
+
async submitViaSnapshot(request, currentState, buildRequest) {
|
|
83
|
+
const { patchRequest } = await buildRequest();
|
|
84
|
+
const response = await firstValueFrom(this.aiApi.getPatch(patchRequest));
|
|
85
|
+
return this.toTurnResult(this.compileAdapterResponse(this.groundRelativeColumnOrder(response, request, currentState), request), request);
|
|
86
|
+
}
|
|
87
|
+
submitViaStream(request, prompt, currentState, componentId, buildRequest) {
|
|
88
|
+
return new Observable((subscriber) => {
|
|
89
|
+
let closed = false;
|
|
90
|
+
let connection = null;
|
|
91
|
+
let subscription = null;
|
|
92
|
+
let terminalWatchdog = null;
|
|
93
|
+
let streamId = null;
|
|
94
|
+
let streamAccessToken;
|
|
95
|
+
let lastEventId;
|
|
96
|
+
let reconnectAttempts = 0;
|
|
97
|
+
let streamContextHints = null;
|
|
98
|
+
const clearTerminalWatchdog = () => {
|
|
99
|
+
if (terminalWatchdog) {
|
|
100
|
+
clearTimeout(terminalWatchdog);
|
|
101
|
+
terminalWatchdog = null;
|
|
102
|
+
}
|
|
103
|
+
};
|
|
104
|
+
const closeConnection = () => {
|
|
105
|
+
clearTerminalWatchdog();
|
|
106
|
+
subscription?.unsubscribe();
|
|
107
|
+
subscription = null;
|
|
108
|
+
if (connection) {
|
|
109
|
+
connection.close();
|
|
110
|
+
connection = null;
|
|
111
|
+
}
|
|
112
|
+
};
|
|
113
|
+
const emitProgress = (statusText) => {
|
|
114
|
+
if (closed || !statusText.trim())
|
|
115
|
+
return;
|
|
116
|
+
subscriber.next({
|
|
117
|
+
state: 'processing',
|
|
118
|
+
phase: 'contextualize',
|
|
119
|
+
statusText,
|
|
120
|
+
canApply: false,
|
|
121
|
+
});
|
|
122
|
+
};
|
|
123
|
+
const completeWithSnapshotFallback = async () => {
|
|
124
|
+
if (closed)
|
|
125
|
+
return;
|
|
126
|
+
let shouldClose = true;
|
|
127
|
+
try {
|
|
128
|
+
emitProgress(this.buildSnapshotFallbackMessage(prompt, componentId));
|
|
129
|
+
const result = await this.submitViaSnapshot(request, currentState, buildRequest);
|
|
130
|
+
if (this.isProcessingTurnResult(result)
|
|
131
|
+
&& streamId
|
|
132
|
+
&& reconnectAttempts < this.streamTerminalReconnectLimitFor(streamContextHints)) {
|
|
133
|
+
subscriber.next(result);
|
|
134
|
+
reconnectAttempts += 1;
|
|
135
|
+
shouldClose = false;
|
|
136
|
+
emitProgress(this.buildStreamReplayMessage(prompt, componentId));
|
|
137
|
+
openConnection();
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
if (!closed) {
|
|
141
|
+
subscriber.next(result);
|
|
142
|
+
subscriber.complete();
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
catch (error) {
|
|
146
|
+
if (!closed)
|
|
147
|
+
subscriber.error(error);
|
|
148
|
+
}
|
|
149
|
+
finally {
|
|
150
|
+
if (shouldClose) {
|
|
151
|
+
closed = true;
|
|
152
|
+
closeConnection();
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
};
|
|
156
|
+
const armTerminalWatchdog = () => {
|
|
157
|
+
clearTerminalWatchdog();
|
|
158
|
+
const watchdogMs = this.streamTerminalWatchdogDelay(streamContextHints);
|
|
159
|
+
if (watchdogMs <= 0)
|
|
160
|
+
return;
|
|
161
|
+
terminalWatchdog = setTimeout(() => {
|
|
162
|
+
if (closed || !streamId)
|
|
163
|
+
return;
|
|
164
|
+
if (reconnectAttempts < this.streamTerminalReconnectLimitFor(streamContextHints)) {
|
|
165
|
+
reconnectAttempts += 1;
|
|
166
|
+
emitProgress(this.buildStreamReplayMessage(prompt, componentId));
|
|
167
|
+
openConnection();
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
void completeWithSnapshotFallback();
|
|
171
|
+
}, watchdogMs);
|
|
172
|
+
};
|
|
173
|
+
const openConnection = () => {
|
|
174
|
+
if (!streamId || closed)
|
|
175
|
+
return;
|
|
176
|
+
closeConnection();
|
|
177
|
+
connection = this.aiApi.connectPatchStream(streamId, lastEventId, streamAccessToken);
|
|
178
|
+
subscription = connection.events$.subscribe({
|
|
179
|
+
next: (event) => {
|
|
180
|
+
if (closed)
|
|
181
|
+
return;
|
|
182
|
+
if (event?.eventId) {
|
|
183
|
+
lastEventId = event.eventId;
|
|
184
|
+
}
|
|
185
|
+
const terminal = this.toTerminalStreamResult(event, request, currentState);
|
|
186
|
+
if (terminal) {
|
|
187
|
+
subscriber.next(terminal);
|
|
188
|
+
subscriber.complete();
|
|
189
|
+
closed = true;
|
|
190
|
+
closeConnection();
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
if (event.type !== 'heartbeat') {
|
|
194
|
+
armTerminalWatchdog();
|
|
195
|
+
}
|
|
196
|
+
const progress = this.buildStreamProgressMessage(event.type, this.toRecord(event.payload) ?? {}, prompt, componentId);
|
|
197
|
+
emitProgress(progress);
|
|
198
|
+
},
|
|
199
|
+
error: async () => {
|
|
200
|
+
if (closed)
|
|
201
|
+
return;
|
|
202
|
+
if (streamId && reconnectAttempts < this.streamTerminalReconnectLimitFor(streamContextHints)) {
|
|
203
|
+
reconnectAttempts += 1;
|
|
204
|
+
emitProgress(this.buildStreamReplayMessage(prompt, componentId));
|
|
205
|
+
openConnection();
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
await completeWithSnapshotFallback();
|
|
209
|
+
},
|
|
210
|
+
});
|
|
211
|
+
armTerminalWatchdog();
|
|
212
|
+
};
|
|
213
|
+
void (async () => {
|
|
214
|
+
try {
|
|
215
|
+
emitProgress(this.buildInitialProgressMessage(prompt, componentId));
|
|
216
|
+
const { patchRequest, contextHints } = await buildRequest();
|
|
217
|
+
streamContextHints = contextHints ?? null;
|
|
218
|
+
if (this.shouldUseSelectedRecordSurfaceSnapshotFallback(streamContextHints)) {
|
|
219
|
+
const result = await this.submitViaSnapshot(request, currentState, buildRequest);
|
|
220
|
+
if (!closed) {
|
|
221
|
+
subscriber.next(result);
|
|
222
|
+
subscriber.complete();
|
|
223
|
+
}
|
|
224
|
+
closed = true;
|
|
225
|
+
closeConnection();
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
const start = await this.awaitPatchStreamStart(patchRequest, streamContextHints);
|
|
229
|
+
streamId = start.streamId;
|
|
230
|
+
streamAccessToken = start.streamAccessToken ?? undefined;
|
|
231
|
+
if (start.observationId) {
|
|
232
|
+
emitProgress(this.buildStreamProgressMessage('status', { message: 'Stream iniciado.', phase: 'request' }, prompt, componentId));
|
|
233
|
+
}
|
|
234
|
+
openConnection();
|
|
235
|
+
}
|
|
236
|
+
catch (error) {
|
|
237
|
+
if (closed)
|
|
238
|
+
return;
|
|
239
|
+
await completeWithSnapshotFallback();
|
|
240
|
+
}
|
|
241
|
+
})();
|
|
242
|
+
return () => {
|
|
243
|
+
closed = true;
|
|
244
|
+
closeConnection();
|
|
245
|
+
};
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
streamTerminalWatchdogDelay(contextHints) {
|
|
249
|
+
if (this.shouldUseSelectedRecordSurfaceSnapshotFallback(contextHints)) {
|
|
250
|
+
return Math.min(this.streamTerminalWatchdogMs, this.selectedRecordSurfaceStreamTerminalWatchdogMs);
|
|
251
|
+
}
|
|
252
|
+
if (this.shouldUseSelectedRecordSnapshotFallback(contextHints)) {
|
|
253
|
+
return Math.min(this.streamTerminalWatchdogMs, this.selectedRecordStreamTerminalWatchdogMs);
|
|
254
|
+
}
|
|
255
|
+
return this.streamTerminalWatchdogMs;
|
|
256
|
+
}
|
|
257
|
+
streamTerminalReconnectLimitFor(contextHints) {
|
|
258
|
+
if (this.shouldUseSelectedRecordSurfaceSnapshotFallback(contextHints)) {
|
|
259
|
+
return 0;
|
|
260
|
+
}
|
|
261
|
+
return this.streamTerminalReconnectLimit;
|
|
262
|
+
}
|
|
263
|
+
shouldUseSelectedRecordSnapshotFallback(contextHints) {
|
|
264
|
+
return this.selectedRecordsCountForTurn > 0
|
|
265
|
+
&& (this.selectedRecordFilterCandidates(contextHints).length > 0
|
|
266
|
+
|| this.shouldUseSelectedRecordSurfaceSnapshotFallback(contextHints));
|
|
267
|
+
}
|
|
268
|
+
shouldUseSelectedRecordSurfaceSnapshotFallback(contextHints) {
|
|
269
|
+
return this.selectedRecordsCountForTurn > 0
|
|
270
|
+
&& this.selectedRecordSurfaces(contextHints).length > 0;
|
|
271
|
+
}
|
|
272
|
+
selectedRecordSurfaces(contextHints) {
|
|
273
|
+
const authoringContract = this.toRecord(contextHints?.['authoringContract']);
|
|
274
|
+
const consultativeContext = this.toRecord(authoringContract?.['consultativeContext']);
|
|
275
|
+
const recordSurfaces = this.toRecord(consultativeContext?.['recordSurfaces']);
|
|
276
|
+
const surfaces = Array.isArray(recordSurfaces?.['surfaces']) ? recordSurfaces['surfaces'] : [];
|
|
277
|
+
return surfaces
|
|
278
|
+
.map((surface) => this.toRecord(surface))
|
|
279
|
+
.filter((surface) => !!surface);
|
|
280
|
+
}
|
|
281
|
+
isProcessingTurnResult(result) {
|
|
282
|
+
return result?.state === 'processing';
|
|
283
|
+
}
|
|
284
|
+
awaitPatchStreamStart(patchRequest, contextHints) {
|
|
285
|
+
const start = firstValueFrom(this.aiApi.startPatchStream(patchRequest));
|
|
286
|
+
if (!this.shouldUseSelectedRecordSnapshotFallback(contextHints)) {
|
|
287
|
+
return start;
|
|
288
|
+
}
|
|
289
|
+
return this.withTimeout(start, this.streamTerminalWatchdogDelay(contextHints), 'selected-record-stream-start-timeout');
|
|
290
|
+
}
|
|
291
|
+
withTimeout(promise, timeoutMs, message) {
|
|
292
|
+
if (timeoutMs <= 0)
|
|
293
|
+
return promise;
|
|
294
|
+
return new Promise((resolve, reject) => {
|
|
295
|
+
const timeout = setTimeout(() => reject(new Error(message)), timeoutMs);
|
|
296
|
+
promise.then((value) => {
|
|
297
|
+
clearTimeout(timeout);
|
|
298
|
+
resolve(value);
|
|
299
|
+
}, (error) => {
|
|
300
|
+
clearTimeout(timeout);
|
|
301
|
+
reject(error);
|
|
302
|
+
});
|
|
303
|
+
});
|
|
304
|
+
}
|
|
305
|
+
canUsePatchStream() {
|
|
306
|
+
return typeof this.aiApi.startPatchStream === 'function'
|
|
307
|
+
&& typeof this.aiApi.connectPatchStream === 'function';
|
|
308
|
+
}
|
|
309
|
+
toTerminalStreamResult(event, request, currentState) {
|
|
310
|
+
if (!event)
|
|
311
|
+
return null;
|
|
312
|
+
const payload = this.toRecord(event.payload) ?? {};
|
|
313
|
+
if (event.type === 'result') {
|
|
314
|
+
const response = (this.toRecord(payload['response']) ?? payload);
|
|
315
|
+
return this.toTurnResult(this.compileAdapterResponse(this.groundRelativeColumnOrder(response, request, currentState), request), request);
|
|
316
|
+
}
|
|
317
|
+
if (event.type === 'error' || event.type === 'cancelled') {
|
|
318
|
+
const message = this.stringValue(payload['assistantMessage'])
|
|
319
|
+
|| this.stringValue(payload['message'])
|
|
320
|
+
|| 'Nao foi possivel concluir o pedido da tabela.';
|
|
321
|
+
return {
|
|
322
|
+
state: 'error',
|
|
323
|
+
phase: 'capture',
|
|
324
|
+
assistantMessage: message,
|
|
325
|
+
errorText: message,
|
|
326
|
+
canApply: false,
|
|
327
|
+
};
|
|
328
|
+
}
|
|
329
|
+
return null;
|
|
330
|
+
}
|
|
331
|
+
buildInitialProgressMessage(prompt, componentId) {
|
|
332
|
+
return `Vou acompanhar este pedido na ${this.componentProgressLabel(componentId)}: "${this.promptPreview(prompt)}".`;
|
|
333
|
+
}
|
|
334
|
+
buildSnapshotFallbackMessage(prompt, componentId) {
|
|
335
|
+
return `Vou continuar pela resposta direta para este pedido na ${this.componentProgressLabel(componentId)}: "${this.promptPreview(prompt)}".`;
|
|
336
|
+
}
|
|
337
|
+
buildStreamReplayMessage(prompt, componentId) {
|
|
338
|
+
return `Ainda estou aguardando a conclusão na ${this.componentProgressLabel(componentId)}; vou sincronizar novamente este pedido: "${this.promptPreview(prompt)}".`;
|
|
339
|
+
}
|
|
340
|
+
buildStreamProgressMessage(eventType, payload, prompt, componentId) {
|
|
341
|
+
const phase = this.stringValue(payload['phase']);
|
|
342
|
+
const title = this.stringValue(payload['title']);
|
|
343
|
+
const detail = this.stringValue(payload['detail']);
|
|
344
|
+
const message = this.stringValue(payload['message']);
|
|
345
|
+
const summary = this.stringValue(payload['summary']);
|
|
346
|
+
const base = detail || summary || message || title || this.progressFallbackForPhase(phase, eventType);
|
|
347
|
+
const target = this.componentProgressLabel(componentId);
|
|
348
|
+
const preview = this.promptPreview(prompt);
|
|
349
|
+
if (phase === 'request' || eventType === 'status' && payload['state'] === 'started') {
|
|
350
|
+
return `Pedido recebido na ${target}: "${preview}".`;
|
|
351
|
+
}
|
|
352
|
+
if (phase === 'proposal') {
|
|
353
|
+
return `${base} Pedido em foco: "${preview}".`;
|
|
354
|
+
}
|
|
355
|
+
if (phase === 'analysis') {
|
|
356
|
+
return `Analisando contexto e capacidades da ${target} para: "${preview}".`;
|
|
357
|
+
}
|
|
358
|
+
return `${base} Pedido: "${preview}".`;
|
|
359
|
+
}
|
|
360
|
+
progressFallbackForPhase(phase, eventType) {
|
|
361
|
+
if (phase === 'analysis')
|
|
362
|
+
return 'Analisando contexto e capacidades disponiveis.';
|
|
363
|
+
if (phase === 'proposal')
|
|
364
|
+
return 'Preparando uma resposta aplicavel.';
|
|
365
|
+
if (eventType === 'heartbeat')
|
|
366
|
+
return 'Ainda processando o pedido.';
|
|
367
|
+
return 'Processando o pedido.';
|
|
368
|
+
}
|
|
369
|
+
componentProgressLabel(componentId) {
|
|
370
|
+
return componentId === 'praxis-table' ? 'tabela dinamica' : 'superficie selecionada';
|
|
371
|
+
}
|
|
372
|
+
promptPreview(prompt) {
|
|
373
|
+
const normalized = prompt.replace(/\s+/g, ' ').trim();
|
|
374
|
+
return normalized.length > 90 ? `${normalized.slice(0, 87)}...` : normalized;
|
|
375
|
+
}
|
|
376
|
+
progressPromptFor(request, fallback) {
|
|
377
|
+
const displayPrompt = typeof request.action?.displayPrompt === 'string'
|
|
378
|
+
? request.action.displayPrompt.trim()
|
|
379
|
+
: '';
|
|
380
|
+
return displayPrompt || fallback;
|
|
381
|
+
}
|
|
382
|
+
normalizePrompt(prompt) {
|
|
383
|
+
return prompt
|
|
384
|
+
.normalize('NFD')
|
|
385
|
+
.replace(/[\u0300-\u036f]/g, '')
|
|
386
|
+
.toLowerCase()
|
|
387
|
+
.trim();
|
|
388
|
+
}
|
|
389
|
+
extractResourcePath(source) {
|
|
390
|
+
if (!source)
|
|
391
|
+
return null;
|
|
392
|
+
const direct = this.nonEmptyStringValue(source['resourcePath']);
|
|
393
|
+
if (direct)
|
|
394
|
+
return direct;
|
|
395
|
+
const consultativeContext = this.toRecord(source['consultativeContext']);
|
|
396
|
+
const fromConsultative = this.nonEmptyStringValue(consultativeContext?.['resourcePath']);
|
|
397
|
+
if (fromConsultative)
|
|
398
|
+
return fromConsultative;
|
|
399
|
+
const authoringContract = this.toRecord(source['authoringContract']);
|
|
400
|
+
if (authoringContract && authoringContract !== source) {
|
|
401
|
+
return this.extractResourcePath(authoringContract);
|
|
402
|
+
}
|
|
403
|
+
return null;
|
|
404
|
+
}
|
|
405
|
+
nonEmptyStringValue(value) {
|
|
406
|
+
return typeof value === 'string' && value.trim() ? value.trim() : null;
|
|
407
|
+
}
|
|
408
|
+
async apply(request) {
|
|
409
|
+
const patch = this.toRecord(request.pendingPatch);
|
|
410
|
+
if (!patch) {
|
|
411
|
+
return {
|
|
412
|
+
state: 'error',
|
|
413
|
+
phase: 'apply',
|
|
414
|
+
assistantMessage: 'Não há alteração de tabela pronta para aplicar.',
|
|
415
|
+
errorText: 'Não há alteração de tabela pronta para aplicar.',
|
|
416
|
+
canApply: false,
|
|
417
|
+
};
|
|
418
|
+
}
|
|
419
|
+
const result = await this.adapter.applyPatch(patch, request.prompt);
|
|
420
|
+
if (!result.success) {
|
|
421
|
+
return {
|
|
422
|
+
state: 'error',
|
|
423
|
+
phase: 'apply',
|
|
424
|
+
assistantMessage: result.error || 'Não foi possível aplicar as alterações na tabela.',
|
|
425
|
+
errorText: result.error || 'Não foi possível aplicar as alterações na tabela.',
|
|
426
|
+
canApply: true,
|
|
427
|
+
pendingPatch: patch,
|
|
428
|
+
};
|
|
429
|
+
}
|
|
430
|
+
return {
|
|
431
|
+
state: 'success',
|
|
432
|
+
phase: 'summarize',
|
|
433
|
+
assistantMessage: 'Alterações aplicadas na tabela.',
|
|
434
|
+
statusText: 'Alterações aplicadas na tabela.',
|
|
435
|
+
canApply: false,
|
|
436
|
+
pendingPatch: null,
|
|
437
|
+
diagnostics: result.warnings?.length ? { warnings: result.warnings } : undefined,
|
|
438
|
+
};
|
|
439
|
+
}
|
|
440
|
+
cancel() {
|
|
441
|
+
return Promise.resolve({
|
|
442
|
+
state: 'listening',
|
|
443
|
+
phase: 'capture',
|
|
444
|
+
assistantMessage: 'Solicitação cancelada.',
|
|
445
|
+
statusText: '',
|
|
446
|
+
canApply: false,
|
|
447
|
+
pendingPatch: null,
|
|
448
|
+
pendingClarification: null,
|
|
449
|
+
});
|
|
450
|
+
}
|
|
451
|
+
retry(request) {
|
|
452
|
+
const lastPrompt = [...(request.messages ?? [])].reverse()
|
|
453
|
+
.find((message) => message.role === 'user')?.text;
|
|
454
|
+
return this.submit({
|
|
455
|
+
...request,
|
|
456
|
+
prompt: lastPrompt ?? request.prompt,
|
|
457
|
+
action: { kind: 'retry' },
|
|
458
|
+
});
|
|
459
|
+
}
|
|
460
|
+
toTurnResult(response, request) {
|
|
461
|
+
if (!response) {
|
|
462
|
+
return {
|
|
463
|
+
state: 'error',
|
|
464
|
+
phase: 'capture',
|
|
465
|
+
assistantMessage: 'Resposta vazia da IA.',
|
|
466
|
+
errorText: 'Resposta vazia da IA.',
|
|
467
|
+
};
|
|
468
|
+
}
|
|
469
|
+
if (response.sessionId && response.sessionId !== request.sessionId) {
|
|
470
|
+
request = { ...request, sessionId: response.sessionId };
|
|
471
|
+
}
|
|
472
|
+
if (response.type === 'clarification') {
|
|
473
|
+
const questions = this.toClarificationQuestions(response, request);
|
|
474
|
+
const diagnostics = this.buildClarificationDiagnostics(response);
|
|
475
|
+
return {
|
|
476
|
+
state: 'clarification',
|
|
477
|
+
phase: 'clarify',
|
|
478
|
+
sessionId: response.sessionId ?? request.sessionId,
|
|
479
|
+
observationId: response.observationId ?? request.observationId ?? null,
|
|
480
|
+
assistantMessage: response.message || 'Preciso de mais detalhes para continuar.',
|
|
481
|
+
clarificationQuestions: questions,
|
|
482
|
+
quickReplies: this.toQuickReplies(response, request),
|
|
483
|
+
canApply: false,
|
|
484
|
+
diagnostics,
|
|
485
|
+
};
|
|
486
|
+
}
|
|
487
|
+
if (response.type === 'info') {
|
|
488
|
+
if (this.isTurnInProgressResponse(response)) {
|
|
489
|
+
const message = response.message || response.explanation || 'Ainda estou processando este pedido.';
|
|
490
|
+
return {
|
|
491
|
+
state: 'processing',
|
|
492
|
+
phase: 'contextualize',
|
|
493
|
+
sessionId: response.sessionId ?? request.sessionId,
|
|
494
|
+
observationId: response.observationId ?? request.observationId ?? null,
|
|
495
|
+
statusText: message,
|
|
496
|
+
canApply: false,
|
|
497
|
+
};
|
|
498
|
+
}
|
|
499
|
+
const message = response.message || response.explanation || 'Informação gerada.';
|
|
500
|
+
return {
|
|
501
|
+
state: 'success',
|
|
502
|
+
phase: 'summarize',
|
|
503
|
+
sessionId: response.sessionId ?? request.sessionId,
|
|
504
|
+
observationId: response.observationId ?? request.observationId ?? null,
|
|
505
|
+
assistantMessage: message,
|
|
506
|
+
statusText: message,
|
|
507
|
+
quickReplies: this.toQuickReplies(response, request),
|
|
508
|
+
canApply: false,
|
|
509
|
+
};
|
|
510
|
+
}
|
|
511
|
+
if (response.type === 'error') {
|
|
512
|
+
const message = response.message || 'Falha ao gerar alteração de tabela.';
|
|
513
|
+
return {
|
|
514
|
+
state: 'error',
|
|
515
|
+
phase: 'capture',
|
|
516
|
+
sessionId: response.sessionId ?? request.sessionId,
|
|
517
|
+
observationId: response.observationId ?? request.observationId ?? null,
|
|
518
|
+
assistantMessage: message,
|
|
519
|
+
errorText: message,
|
|
520
|
+
diagnostics: response.warnings?.length ? { warnings: response.warnings } : undefined,
|
|
521
|
+
};
|
|
522
|
+
}
|
|
523
|
+
if (response.patch && Object.keys(response.patch).length > 0) {
|
|
524
|
+
const warnings = response.warnings?.filter(Boolean) ?? [];
|
|
525
|
+
const quickReplies = this.toQuickReplies(response, request);
|
|
526
|
+
const diagnostics = this.buildReviewDiagnostics(response, warnings);
|
|
527
|
+
return {
|
|
528
|
+
state: 'review',
|
|
529
|
+
phase: 'review',
|
|
530
|
+
sessionId: response.sessionId ?? request.sessionId,
|
|
531
|
+
observationId: response.observationId ?? request.observationId ?? null,
|
|
532
|
+
assistantMessage: this.toReviewMessage(response),
|
|
533
|
+
statusText: 'Revise a proposta antes de aplicar.',
|
|
534
|
+
quickReplies,
|
|
535
|
+
canApply: true,
|
|
536
|
+
pendingPatch: response.patch,
|
|
537
|
+
preview: {
|
|
538
|
+
kind: 'table-config-patch',
|
|
539
|
+
diff: response.diff ?? [],
|
|
540
|
+
},
|
|
541
|
+
diagnostics,
|
|
542
|
+
};
|
|
543
|
+
}
|
|
544
|
+
return {
|
|
545
|
+
state: 'success',
|
|
546
|
+
phase: 'summarize',
|
|
547
|
+
sessionId: response.sessionId ?? request.sessionId,
|
|
548
|
+
observationId: response.observationId ?? request.observationId ?? null,
|
|
549
|
+
assistantMessage: response.message || response.explanation || 'Nenhuma alteração necessária.',
|
|
550
|
+
statusText: response.message || response.explanation || 'Nenhuma alteração necessária.',
|
|
551
|
+
canApply: false,
|
|
552
|
+
};
|
|
553
|
+
}
|
|
554
|
+
isTurnInProgressResponse(response) {
|
|
555
|
+
return this.normalizeLabel(response.code ?? '') === 'turn in progress';
|
|
556
|
+
}
|
|
557
|
+
compileAdapterResponse(response, request) {
|
|
558
|
+
if (response.type === 'clarification' || response.type === 'info' || response.type === 'error') {
|
|
559
|
+
const compiledExecutable = !this.responseCarriesClarificationChoices(response)
|
|
560
|
+
&& this.responseMayContainExecutableEnvelope(response)
|
|
561
|
+
? this.adapter.compileAiResponse?.(response)
|
|
562
|
+
: null;
|
|
563
|
+
if (compiledExecutable?.patch && Object.keys(compiledExecutable.patch).length > 0) {
|
|
564
|
+
const warnings = [
|
|
565
|
+
...(response.warnings ?? []),
|
|
566
|
+
...(compiledExecutable.warnings ?? []),
|
|
567
|
+
];
|
|
568
|
+
const executableResponse = {
|
|
569
|
+
...response,
|
|
570
|
+
patch: compiledExecutable.patch,
|
|
571
|
+
warnings: warnings.length ? warnings : undefined,
|
|
572
|
+
};
|
|
573
|
+
delete executableResponse.type;
|
|
574
|
+
return executableResponse;
|
|
575
|
+
}
|
|
576
|
+
if (compiledExecutable?.type === 'error') {
|
|
577
|
+
return {
|
|
578
|
+
type: 'error',
|
|
579
|
+
message: compiledExecutable.message || 'O componentEditPlan da tabela nao passou na validacao de capacidades.',
|
|
580
|
+
warnings: compiledExecutable.warnings,
|
|
581
|
+
};
|
|
582
|
+
}
|
|
583
|
+
const continuedSurfaceRowAction = this.selectedRecordSurfaceRowActionPlanForClarification(response, request);
|
|
584
|
+
if (continuedSurfaceRowAction) {
|
|
585
|
+
return this.compileAdapterResponse(continuedSurfaceRowAction, request);
|
|
586
|
+
}
|
|
587
|
+
return response;
|
|
588
|
+
}
|
|
589
|
+
const compiled = this.adapter.compileAiResponse?.(response);
|
|
590
|
+
if (!compiled && response.patch && Object.keys(response.patch).length > 0) {
|
|
591
|
+
return {
|
|
592
|
+
type: 'error',
|
|
593
|
+
message: 'A tabela exige componentEditPlan validado pelo manifesto antes de gerar patch local.',
|
|
594
|
+
warnings: [
|
|
595
|
+
'free-table-patch-rejected',
|
|
596
|
+
'Use componentEditPlan validado contra PRAXIS_TABLE_AUTHORING_MANIFEST.',
|
|
597
|
+
],
|
|
598
|
+
};
|
|
599
|
+
}
|
|
600
|
+
if (!compiled) {
|
|
601
|
+
return response;
|
|
602
|
+
}
|
|
603
|
+
if (compiled.type === 'error') {
|
|
604
|
+
return {
|
|
605
|
+
type: 'error',
|
|
606
|
+
message: compiled.message || 'O componentEditPlan da tabela nao passou na validacao de capacidades.',
|
|
607
|
+
warnings: compiled.warnings,
|
|
608
|
+
};
|
|
609
|
+
}
|
|
610
|
+
const governedSelectedRecordFilter = this.isGovernedSelectedRecordFilterResponse(response);
|
|
611
|
+
if (!governedSelectedRecordFilter && this.shouldBlockUnsupportedFilterExpressionRuntimePatch(compiled.patch, request)) {
|
|
612
|
+
const questions = this.unsupportedFilterExpressionClarificationQuestions(compiled.patch);
|
|
613
|
+
return {
|
|
614
|
+
type: 'clarification',
|
|
615
|
+
message: [
|
|
616
|
+
'Esse pedido combina alternativas entre campos diferentes, mas o contrato atual da tabela so materializa filtros simples.',
|
|
617
|
+
'Posso aplicar uma unica escolha de filtro simples por vez, derivada dos campos suportados desta tabela, ou manter a tabela sem alteracao enquanto a intencao e refinada.',
|
|
618
|
+
].join(' '),
|
|
619
|
+
questions,
|
|
620
|
+
warnings: [
|
|
621
|
+
'unsupported-filter-expression-materialization-blocked',
|
|
622
|
+
'Residual textual boolean-operator guard acted only after LLM proposed a runtime table.filter.apply operation while filterExpression is unsupported.',
|
|
623
|
+
],
|
|
624
|
+
};
|
|
625
|
+
}
|
|
626
|
+
const implicitSelectedFilterClarification = governedSelectedRecordFilter ? null : this.implicitSelectedRecordFilterClarification(compiled.patch, request);
|
|
627
|
+
if (implicitSelectedFilterClarification) {
|
|
628
|
+
return implicitSelectedFilterClarification;
|
|
629
|
+
}
|
|
630
|
+
const warnings = [
|
|
631
|
+
...(response.warnings ?? []),
|
|
632
|
+
...(compiled.warnings ?? []),
|
|
633
|
+
];
|
|
634
|
+
return {
|
|
635
|
+
...response,
|
|
636
|
+
...compiled,
|
|
637
|
+
patch: compiled.patch,
|
|
638
|
+
warnings: warnings.length ? warnings : undefined,
|
|
639
|
+
};
|
|
640
|
+
}
|
|
641
|
+
responseCarriesClarificationChoices(response) {
|
|
642
|
+
if (response.type !== 'clarification')
|
|
643
|
+
return false;
|
|
644
|
+
const record = response;
|
|
645
|
+
const questions = Array.isArray(response.questions) ? response.questions : [];
|
|
646
|
+
const optionPayloads = Array.isArray(record['optionPayloads']) ? record['optionPayloads'] : [];
|
|
647
|
+
return questions.length > 0 || optionPayloads.length > 0;
|
|
648
|
+
}
|
|
649
|
+
responseMayContainExecutableEnvelope(response) {
|
|
650
|
+
return this.responseMayContainRuntimeOperationEnvelope(response)
|
|
651
|
+
|| this.responseMayContainComponentEditPlanEnvelope(response);
|
|
652
|
+
}
|
|
653
|
+
responseMayContainRuntimeOperationEnvelope(response) {
|
|
654
|
+
const record = response;
|
|
655
|
+
if (record['tableRuntimeOperations'])
|
|
656
|
+
return true;
|
|
657
|
+
const patch = this.toRecord(record['patch']);
|
|
658
|
+
if (patch?.['tableRuntimeOperations'])
|
|
659
|
+
return true;
|
|
660
|
+
return ['message', 'assistantMessage', 'content', 'explanation'].some((key) => {
|
|
661
|
+
const value = this.stringValue(record[key]);
|
|
662
|
+
return value.includes('tableRuntimeOperations') && value.includes('operationId');
|
|
663
|
+
});
|
|
664
|
+
}
|
|
665
|
+
responseMayContainComponentEditPlanEnvelope(response) {
|
|
666
|
+
const record = response;
|
|
667
|
+
if (record['componentEditPlan'] || record['tableEditPlan'] || record['editPlan'])
|
|
668
|
+
return true;
|
|
669
|
+
return ['message', 'assistantMessage', 'content', 'explanation'].some((key) => {
|
|
670
|
+
const value = this.stringValue(record[key]);
|
|
671
|
+
return value.includes('componentEditPlan') && value.includes('operationId');
|
|
672
|
+
});
|
|
673
|
+
}
|
|
674
|
+
isGovernedSelectedRecordFilterResponse(response) {
|
|
675
|
+
return (response.warnings ?? []).some((warning) => [
|
|
676
|
+
'selected-record-filter-request-materialized',
|
|
677
|
+
'selected-record-filter-clarification-materialized',
|
|
678
|
+
].includes(warning));
|
|
679
|
+
}
|
|
680
|
+
shouldBlockUnsupportedFilterExpressionRuntimePatch(patch, request) {
|
|
681
|
+
if (this.filterExpressionSupportedForTurn === true)
|
|
682
|
+
return false;
|
|
683
|
+
if (!this.patchHasTableFilterApply(patch))
|
|
684
|
+
return false;
|
|
685
|
+
const prompt = this.normalizeLabel(request?.prompt ?? '');
|
|
686
|
+
// Safety guard only after an LLM-authored executable operation exists. It does
|
|
687
|
+
// not route primary intent; it blocks degraded materialization of explicit
|
|
688
|
+
// boolean alternatives when the canonical contract lacks filterExpression.
|
|
689
|
+
return /\b(ou|or|anyof|oneof|allof)\b/u.test(prompt);
|
|
690
|
+
}
|
|
691
|
+
unsupportedFilterExpressionClarificationQuestions(patch) {
|
|
692
|
+
const labels = this.attemptedRuntimeFilterFieldLabels(patch);
|
|
693
|
+
const fieldOptions = labels
|
|
694
|
+
.slice(0, 2)
|
|
695
|
+
.map((label) => `Aplicar apenas o filtro por ${label}`);
|
|
696
|
+
return [
|
|
697
|
+
...fieldOptions,
|
|
698
|
+
'Nao aplicar filtro e revisar a intencao',
|
|
699
|
+
].slice(0, 3);
|
|
700
|
+
}
|
|
701
|
+
attemptedRuntimeFilterFieldLabels(patch) {
|
|
702
|
+
const envelope = this.toRecord(this.toRecord(patch)?.['tableRuntimeOperations']);
|
|
703
|
+
const operations = Array.isArray(envelope?.['operations']) ? envelope['operations'] : [];
|
|
704
|
+
const labels = new Set();
|
|
705
|
+
for (const operation of operations) {
|
|
706
|
+
const record = this.toRecord(operation);
|
|
707
|
+
if (this.stringValue(record?.['operationId']) !== 'table.filter.apply')
|
|
708
|
+
continue;
|
|
709
|
+
const input = this.toRecord(record?.['input']) ?? {};
|
|
710
|
+
const criteria = this.toRecord(input['criteria']) ?? {};
|
|
711
|
+
for (const field of Object.keys(criteria)) {
|
|
712
|
+
labels.add(this.humanizeFilterField(field));
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
return [...labels];
|
|
716
|
+
}
|
|
717
|
+
patchHasTableFilterApply(patch) {
|
|
718
|
+
const envelope = this.toRecord(this.toRecord(patch)?.['tableRuntimeOperations']);
|
|
719
|
+
const operations = Array.isArray(envelope?.['operations']) ? envelope['operations'] : [];
|
|
720
|
+
return operations
|
|
721
|
+
.map((operation) => this.toRecord(operation))
|
|
722
|
+
.some((operation) => this.stringValue(operation?.['operationId']) === 'table.filter.apply');
|
|
723
|
+
}
|
|
724
|
+
selectedRecordFilterCandidate(contextHints, field) {
|
|
725
|
+
const normalizedField = field.trim();
|
|
726
|
+
if (!normalizedField)
|
|
727
|
+
return null;
|
|
728
|
+
return this.selectedRecordFilterCandidates(contextHints)
|
|
729
|
+
.find((candidate) => this.stringValue(candidate['field']) === normalizedField) ?? null;
|
|
730
|
+
}
|
|
731
|
+
selectedRecordFilterCandidates(contextHints) {
|
|
732
|
+
const authoringContract = this.toRecord(contextHints?.['authoringContract']);
|
|
733
|
+
const consultativeContext = this.toRecord(authoringContract?.['consultativeContext']);
|
|
734
|
+
const selectedRecordsContext = this.toRecord(consultativeContext?.['selectedRecordsContext'])
|
|
735
|
+
?? this.toRecord(contextHints?.['selectedRecordsContext']);
|
|
736
|
+
const candidates = Array.isArray(selectedRecordsContext?.['filterCandidates'])
|
|
737
|
+
? selectedRecordsContext['filterCandidates']
|
|
738
|
+
: [];
|
|
739
|
+
return candidates
|
|
740
|
+
.map((candidate) => this.toRecord(candidate))
|
|
741
|
+
.filter((candidate) => !!candidate);
|
|
742
|
+
}
|
|
743
|
+
selectedRecordSampleRows(contextHints) {
|
|
744
|
+
const authoringContract = this.toRecord(contextHints?.['authoringContract']);
|
|
745
|
+
const consultativeContext = this.toRecord(authoringContract?.['consultativeContext']);
|
|
746
|
+
const selectedRecordsContext = this.toRecord(consultativeContext?.['selectedRecordsContext'])
|
|
747
|
+
?? this.toRecord(contextHints?.['selectedRecordsContext']);
|
|
748
|
+
const sampleRows = Array.isArray(selectedRecordsContext?.['sampleRows'])
|
|
749
|
+
? selectedRecordsContext['sampleRows']
|
|
750
|
+
: [];
|
|
751
|
+
return sampleRows
|
|
752
|
+
.map((row) => this.toRecord(row))
|
|
753
|
+
.filter((row) => !!row);
|
|
754
|
+
}
|
|
755
|
+
selectedRecordFilterClarificationOptionPayload(entry, contextHints) {
|
|
756
|
+
const description = this.selectedRecordFilterCandidateDescription(entry.name, contextHints);
|
|
757
|
+
return {
|
|
758
|
+
value: entry.name,
|
|
759
|
+
label: `Filtrar por ${entry.label}`,
|
|
760
|
+
contextHints: {
|
|
761
|
+
selectedRecordsFilter: {
|
|
762
|
+
field: entry.name,
|
|
763
|
+
label: entry.label,
|
|
764
|
+
source: 'selected-record-clarification',
|
|
765
|
+
},
|
|
766
|
+
...(description
|
|
767
|
+
? {
|
|
768
|
+
presentation: {
|
|
769
|
+
kind: 'guided-option',
|
|
770
|
+
icon: 'check',
|
|
771
|
+
description,
|
|
772
|
+
ctaLabel: 'Usar esta opção',
|
|
773
|
+
},
|
|
774
|
+
}
|
|
775
|
+
: {}),
|
|
776
|
+
},
|
|
777
|
+
};
|
|
778
|
+
}
|
|
779
|
+
selectedRecordFilterCandidateDescription(field, contextHints) {
|
|
780
|
+
const candidate = this.selectedRecordFilterCandidate(contextHints, field);
|
|
781
|
+
const displayValues = Array.isArray(candidate?.['displayValues'])
|
|
782
|
+
? candidate['displayValues']
|
|
783
|
+
.map((entry) => this.formatSelectedRecordFilterValue(entry))
|
|
784
|
+
.filter((entry) => !!entry)
|
|
785
|
+
: [];
|
|
786
|
+
if (displayValues.length) {
|
|
787
|
+
const preview = displayValues.slice(0, 3).join(', ');
|
|
788
|
+
return `Valores dos selecionados: ${preview}${displayValues.length > 3 ? ', ...' : ''}`;
|
|
789
|
+
}
|
|
790
|
+
const criteria = this.toRecord(candidate?.['criteria']);
|
|
791
|
+
const value = criteria?.[field];
|
|
792
|
+
if (value === undefined || value === null)
|
|
793
|
+
return null;
|
|
794
|
+
const record = this.toRecord(value);
|
|
795
|
+
if (record) {
|
|
796
|
+
const start = record['start'] ?? record['startDate'];
|
|
797
|
+
const end = record['end'] ?? record['endDate'];
|
|
798
|
+
if (start !== undefined && end !== undefined) {
|
|
799
|
+
return `Valores dos selecionados: ${this.formatSelectedRecordFilterValue(start)} até ${this.formatSelectedRecordFilterValue(end)}`;
|
|
800
|
+
}
|
|
801
|
+
}
|
|
802
|
+
if (Array.isArray(value) && value.length) {
|
|
803
|
+
const preview = value.slice(0, 3).map((entry) => this.formatSelectedRecordFilterValue(entry)).join(', ');
|
|
804
|
+
return `Valores dos selecionados: ${preview}${value.length > 3 ? ', ...' : ''}`;
|
|
805
|
+
}
|
|
806
|
+
return null;
|
|
807
|
+
}
|
|
808
|
+
formatSelectedRecordFilterValue(value) {
|
|
809
|
+
if (typeof value === 'number' && Number.isFinite(value)) {
|
|
810
|
+
return new Intl.NumberFormat('pt-BR', { maximumFractionDigits: 2 }).format(value);
|
|
811
|
+
}
|
|
812
|
+
if (typeof value === 'string') {
|
|
813
|
+
const trimmed = value.trim();
|
|
814
|
+
const dateMatch = trimmed.match(/^(\d{4})-(\d{2})-(\d{2})$/u);
|
|
815
|
+
if (dateMatch)
|
|
816
|
+
return `${dateMatch[3]}/${dateMatch[2]}/${dateMatch[1]}`;
|
|
817
|
+
return trimmed;
|
|
818
|
+
}
|
|
819
|
+
if (typeof value === 'boolean')
|
|
820
|
+
return value ? 'sim' : 'não';
|
|
821
|
+
return String(value ?? '');
|
|
822
|
+
}
|
|
823
|
+
implicitSelectedRecordFilterClarification(patch, request) {
|
|
824
|
+
if (this.selectedRecordsCountForTurn <= 0)
|
|
825
|
+
return null;
|
|
826
|
+
if (this.filterFieldCatalogEntries.length < 2)
|
|
827
|
+
return null;
|
|
828
|
+
const operations = this.tableFilterApplyOperations(patch);
|
|
829
|
+
if (!operations.length)
|
|
830
|
+
return null;
|
|
831
|
+
const prompt = this.normalizeLabel(request?.prompt ?? '');
|
|
832
|
+
if (!prompt)
|
|
833
|
+
return null;
|
|
834
|
+
const groundedSurfaceOperation = this.selectedRecordSurfaceOperationForMisgroundedFilter(prompt, request);
|
|
835
|
+
if (groundedSurfaceOperation) {
|
|
836
|
+
return groundedSurfaceOperation;
|
|
837
|
+
}
|
|
838
|
+
const implicitFields = operations
|
|
839
|
+
.flatMap((operation) => Object.keys(this.toRecord(this.toRecord(operation['input'])?.['criteria']) ?? {}))
|
|
840
|
+
.filter((field) => field && !this.promptMentionsFilterField(prompt, field));
|
|
841
|
+
if (!implicitFields.length)
|
|
842
|
+
return null;
|
|
843
|
+
const contextHints = this.contextHintsFor(request);
|
|
844
|
+
// This guard runs only after the LLM authored an executable table.filter.apply
|
|
845
|
+
// operation. It does not route primary intent; it blocks ungrounded
|
|
846
|
+
// materialization when selected rows offer multiple plausible filter fields.
|
|
847
|
+
const options = this.selectedRecordFilterClarificationOptionEntries(implicitFields);
|
|
848
|
+
if (options.length < 2)
|
|
849
|
+
return null;
|
|
850
|
+
return {
|
|
851
|
+
type: 'clarification',
|
|
852
|
+
message: [
|
|
853
|
+
`Encontrei ${this.selectedRecordsCountForTurn} registro${this.selectedRecordsCountForTurn === 1 ? '' : 's'} selecionado${this.selectedRecordsCountForTurn === 1 ? '' : 's'}.`,
|
|
854
|
+
'Para buscar registros parecidos, escolha qual propriedade deve guiar o filtro.',
|
|
855
|
+
].join(' '),
|
|
856
|
+
questions: ['Como você quer definir registros parecidos?'],
|
|
857
|
+
optionPayloads: options.map((entry) => this.selectedRecordFilterClarificationOptionPayload(entry, contextHints)),
|
|
858
|
+
warnings: [
|
|
859
|
+
'implicit-selected-record-filter-materialization-blocked',
|
|
860
|
+
'Residual grounding guard acted only after LLM proposed table.filter.apply from selected-record context with multiple plausible filter fields.',
|
|
861
|
+
],
|
|
862
|
+
};
|
|
863
|
+
}
|
|
864
|
+
selectedRecordSurfaceOperationForMisgroundedFilter(normalizedPrompt, request) {
|
|
865
|
+
if (this.promptRequestsSimilarRecords(normalizedPrompt))
|
|
866
|
+
return null;
|
|
867
|
+
const contextHints = this.contextHintsFor(request);
|
|
868
|
+
const surfaces = this.selectedRecordSurfaces(contextHints);
|
|
869
|
+
if (!surfaces.length)
|
|
870
|
+
return null;
|
|
871
|
+
const ranked = surfaces
|
|
872
|
+
.map((surface) => ({
|
|
873
|
+
surface,
|
|
874
|
+
score: this.selectedRecordSurfacePromptScore(normalizedPrompt, surface),
|
|
875
|
+
}))
|
|
876
|
+
.filter((entry) => entry.score >= 2)
|
|
877
|
+
.sort((a, b) => b.score - a.score);
|
|
878
|
+
if (!ranked.length || (ranked.length > 1 && ranked[0].score === ranked[1].score)) {
|
|
879
|
+
return null;
|
|
880
|
+
}
|
|
881
|
+
const surface = ranked[0].surface;
|
|
882
|
+
const surfaceId = this.stringValue(surface['id']);
|
|
883
|
+
if (!surfaceId)
|
|
884
|
+
return null;
|
|
885
|
+
const surfaceLabel = this.stringValue(surface['label'])
|
|
886
|
+
|| this.stringValue(this.toRecord(surface['resourceSurface'])?.['title'])
|
|
887
|
+
|| this.humanizeField(surfaceId);
|
|
888
|
+
// Residual grounding guard: this runs only after the LLM authored a
|
|
889
|
+
// selected-record table.filter.apply operation. It ranks declared canonical
|
|
890
|
+
// recordSurfaces to correct an unsafe filter materialization, not to route
|
|
891
|
+
// primary user intent by command text.
|
|
892
|
+
return {
|
|
893
|
+
type: 'patch',
|
|
894
|
+
patch: {
|
|
895
|
+
tableRuntimeOperations: {
|
|
896
|
+
source: 'selected-record-surface-grounding-guard',
|
|
897
|
+
operations: [{
|
|
898
|
+
operationId: 'dynamicPage.surface.open',
|
|
899
|
+
input: {
|
|
900
|
+
surfaceId,
|
|
901
|
+
surfaceLabel,
|
|
902
|
+
source: 'selected-records',
|
|
903
|
+
},
|
|
904
|
+
}],
|
|
905
|
+
},
|
|
906
|
+
},
|
|
907
|
+
warnings: [
|
|
908
|
+
'selected-record-filter-materialization-corrected-to-record-surface',
|
|
909
|
+
'Residual grounding guard acted only after LLM proposed table.filter.apply while declared recordSurfaces semantically matched the selected-record request.',
|
|
910
|
+
],
|
|
911
|
+
};
|
|
912
|
+
}
|
|
913
|
+
selectedRecordSurfaceRowActionPlanForClarification(response, request) {
|
|
914
|
+
if (!request)
|
|
915
|
+
return null;
|
|
916
|
+
const normalizedPrompt = this.normalizeLabel(request.prompt ?? '');
|
|
917
|
+
const normalizedConversation = this.normalizeLabel([
|
|
918
|
+
...(request.messages ?? []).slice(-8).map((message) => message?.text ?? ''),
|
|
919
|
+
request.prompt ?? '',
|
|
920
|
+
].join(' '));
|
|
921
|
+
if (!this.textMentionsRowActionRequest(normalizedPrompt || normalizedConversation)) {
|
|
922
|
+
return null;
|
|
923
|
+
}
|
|
924
|
+
const clarificationKind = this.selectedRecordSurfaceRowActionClarificationKind(response);
|
|
925
|
+
if (!clarificationKind)
|
|
926
|
+
return null;
|
|
927
|
+
const contextHints = this.contextHintsFor(request);
|
|
928
|
+
const surfaces = this.selectedRecordSurfaces(contextHints);
|
|
929
|
+
if (!surfaces.length)
|
|
930
|
+
return null;
|
|
931
|
+
const ranked = surfaces
|
|
932
|
+
.map((surface) => ({
|
|
933
|
+
surface,
|
|
934
|
+
score: this.selectedRecordSurfacePromptScore(normalizedConversation || normalizedPrompt, surface),
|
|
935
|
+
}))
|
|
936
|
+
.filter((entry) => entry.score >= 2)
|
|
937
|
+
.sort((left, right) => right.score - left.score);
|
|
938
|
+
if (!ranked.length || (ranked.length > 1 && ranked[0].score === ranked[1].score)) {
|
|
939
|
+
return null;
|
|
940
|
+
}
|
|
941
|
+
const surface = ranked[0].surface;
|
|
942
|
+
const resourceSurface = this.toRecord(surface['resourceSurface']);
|
|
943
|
+
if (!this.isResourceSurfaceCatalogDigest(resourceSurface)) {
|
|
944
|
+
return null;
|
|
945
|
+
}
|
|
946
|
+
const surfaceId = this.stringValue(surface['id']) || this.stringValue(resourceSurface['id']);
|
|
947
|
+
if (!surfaceId)
|
|
948
|
+
return null;
|
|
949
|
+
const surfaceLabel = this.stringValue(surface['label'])
|
|
950
|
+
|| this.stringValue(resourceSurface['title'])
|
|
951
|
+
|| this.humanizeField(surfaceId);
|
|
952
|
+
const actionId = `open-${this.slugifyActionId(surfaceId)}`;
|
|
953
|
+
const rowAction = {
|
|
954
|
+
id: actionId,
|
|
955
|
+
label: surfaceLabel,
|
|
956
|
+
action: 'surface.open',
|
|
957
|
+
icon: 'open_in_new',
|
|
958
|
+
recordSurface: this.toAiJsonObject(resourceSurface),
|
|
959
|
+
};
|
|
960
|
+
// Residual continuity guard: this runs only after the LLM returned a
|
|
961
|
+
// clarification/info response for a row-button request. The target surface
|
|
962
|
+
// is ranked from declared canonical recordSurfaces and recent conversation
|
|
963
|
+
// context, so the guard repairs materialization without becoming primary
|
|
964
|
+
// intent routing by local command text.
|
|
965
|
+
return {
|
|
966
|
+
type: 'patch',
|
|
967
|
+
componentEditPlan: {
|
|
968
|
+
kind: 'praxis.table.component-edit-plan',
|
|
969
|
+
version: '1.0',
|
|
970
|
+
componentId: 'praxis-table',
|
|
971
|
+
operationId: 'rowAction.add',
|
|
972
|
+
changeKind: 'add_row_action',
|
|
973
|
+
capabilityPath: 'actions.row.actions[]',
|
|
974
|
+
input: rowAction,
|
|
975
|
+
value: rowAction,
|
|
976
|
+
},
|
|
977
|
+
explanation: `Vou criar um botão em cada linha para abrir ${surfaceLabel}.`,
|
|
978
|
+
warnings: [
|
|
979
|
+
clarificationKind === 'technical'
|
|
980
|
+
? 'selected-record-surface-row-action-continued-from-technical-clarification'
|
|
981
|
+
: 'selected-record-surface-row-action-continued-from-generic-clarification',
|
|
982
|
+
clarificationKind === 'technical'
|
|
983
|
+
? 'Residual continuity guard acted only after LLM asked for technical row-action details while declared recordSurfaces and conversation history already grounded the target surface.'
|
|
984
|
+
: 'Residual continuity guard acted only after LLM asked a generic dataset/listing clarification while declared recordSurfaces and a row-action request already grounded the target surface.',
|
|
985
|
+
],
|
|
986
|
+
};
|
|
987
|
+
}
|
|
988
|
+
selectedRecordSurfaceRowActionClarificationKind(response) {
|
|
989
|
+
const text = this.normalizeLabel([
|
|
990
|
+
response.message ?? '',
|
|
991
|
+
response.explanation ?? '',
|
|
992
|
+
...(response.questions ?? []),
|
|
993
|
+
].join(' '));
|
|
994
|
+
if ([
|
|
995
|
+
'surface to open',
|
|
996
|
+
'button label',
|
|
997
|
+
'placement in row',
|
|
998
|
+
'mais detalhes sobre surface',
|
|
999
|
+
'mais detalhes sobre button',
|
|
1000
|
+
'mais detalhes sobre placement',
|
|
1001
|
+
].some((needle) => text.includes(needle))) {
|
|
1002
|
+
return 'technical';
|
|
1003
|
+
}
|
|
1004
|
+
if ([
|
|
1005
|
+
'qual conjunto de dados',
|
|
1006
|
+
'conjunto de dados',
|
|
1007
|
+
'dados voce quer listar',
|
|
1008
|
+
'dados você quer listar',
|
|
1009
|
+
'quer listar',
|
|
1010
|
+
'listar',
|
|
1011
|
+
'dataset',
|
|
1012
|
+
].some((needle) => text.includes(this.normalizeLabel(needle)))) {
|
|
1013
|
+
return 'generic-dataset';
|
|
1014
|
+
}
|
|
1015
|
+
return null;
|
|
1016
|
+
}
|
|
1017
|
+
textMentionsRowActionRequest(normalizedText) {
|
|
1018
|
+
return [
|
|
1019
|
+
'botao',
|
|
1020
|
+
'acao de linha',
|
|
1021
|
+
'acoes de linha',
|
|
1022
|
+
'nas linhas',
|
|
1023
|
+
'por linha',
|
|
1024
|
+
'em cada linha',
|
|
1025
|
+
].some((needle) => normalizedText.includes(needle));
|
|
1026
|
+
}
|
|
1027
|
+
isResourceSurfaceCatalogDigest(value) {
|
|
1028
|
+
if (!value)
|
|
1029
|
+
return false;
|
|
1030
|
+
return !!this.stringValue(value['id'])
|
|
1031
|
+
&& !!this.stringValue(value['kind'])
|
|
1032
|
+
&& !!this.stringValue(value['scope'])
|
|
1033
|
+
&& !!this.stringValue(value['path'])
|
|
1034
|
+
&& !!this.stringValue(value['method']);
|
|
1035
|
+
}
|
|
1036
|
+
slugifyActionId(value) {
|
|
1037
|
+
return this.normalizeLabel(value)
|
|
1038
|
+
.replace(/[^a-z0-9]+/gu, '-')
|
|
1039
|
+
.replace(/^-+|-+$/gu, '')
|
|
1040
|
+
|| 'surface';
|
|
1041
|
+
}
|
|
1042
|
+
promptRequestsSimilarRecords(normalizedPrompt) {
|
|
1043
|
+
return [
|
|
1044
|
+
'registro parecido',
|
|
1045
|
+
'registros parecidos',
|
|
1046
|
+
'outro registro',
|
|
1047
|
+
'outros registros',
|
|
1048
|
+
'buscar registros',
|
|
1049
|
+
'procurar registros',
|
|
1050
|
+
'filtrar registros',
|
|
1051
|
+
'filtrar por',
|
|
1052
|
+
].some((needle) => this.normalizedTextContainsApproxPhrase(normalizedPrompt, needle));
|
|
1053
|
+
}
|
|
1054
|
+
selectedRecordSurfacePromptScore(normalizedPrompt, surface) {
|
|
1055
|
+
const resourceSurface = this.toRecord(surface['resourceSurface']);
|
|
1056
|
+
const tags = Array.isArray(resourceSurface?.['tags']) ? resourceSurface['tags'] : [];
|
|
1057
|
+
const haystack = this.normalizeLabel([
|
|
1058
|
+
this.stringValue(surface['id']),
|
|
1059
|
+
this.stringValue(surface['label']),
|
|
1060
|
+
this.stringValue(surface['title']),
|
|
1061
|
+
this.stringValue(surface['description']),
|
|
1062
|
+
this.stringValue(surface['semanticDescription']),
|
|
1063
|
+
this.stringValue(surface['semanticIntent']),
|
|
1064
|
+
this.stringValue(surface['kind']),
|
|
1065
|
+
this.stringValue(surface['scope']),
|
|
1066
|
+
this.stringValue(surface['relation']),
|
|
1067
|
+
this.stringValue(resourceSurface?.['id']),
|
|
1068
|
+
this.stringValue(resourceSurface?.['title']),
|
|
1069
|
+
this.stringValue(resourceSurface?.['description']),
|
|
1070
|
+
this.stringValue(resourceSurface?.['semanticIntent']),
|
|
1071
|
+
this.stringValue(resourceSurface?.['kind']),
|
|
1072
|
+
this.stringValue(resourceSurface?.['scope']),
|
|
1073
|
+
...this.compactStringArray(surface['tags']),
|
|
1074
|
+
...tags.map((tag) => this.stringValue(tag)),
|
|
1075
|
+
].join(' '));
|
|
1076
|
+
const promptTokens = normalizedPrompt
|
|
1077
|
+
.split(/\s+/u)
|
|
1078
|
+
.map((token) => token.trim())
|
|
1079
|
+
.filter((token) => token.length >= 4 && !this.isSelectedRecordSurfaceStopToken(token));
|
|
1080
|
+
return [...new Set(promptTokens)]
|
|
1081
|
+
.reduce((score, token) => score + (haystack.includes(token) ? 1 : 0), 0);
|
|
1082
|
+
}
|
|
1083
|
+
isSelectedRecordSurfaceStopToken(token) {
|
|
1084
|
+
return [
|
|
1085
|
+
'quero',
|
|
1086
|
+
'mostra',
|
|
1087
|
+
'mostrar',
|
|
1088
|
+
'mostre',
|
|
1089
|
+
'abrir',
|
|
1090
|
+
'abre',
|
|
1091
|
+
'deste',
|
|
1092
|
+
'desse',
|
|
1093
|
+
'dele',
|
|
1094
|
+
'dela',
|
|
1095
|
+
'registro',
|
|
1096
|
+
'selecionado',
|
|
1097
|
+
'selecionada',
|
|
1098
|
+
].includes(token);
|
|
1099
|
+
}
|
|
1100
|
+
compactStringArray(value) {
|
|
1101
|
+
return Array.isArray(value)
|
|
1102
|
+
? value
|
|
1103
|
+
.map((entry) => this.stringValue(entry))
|
|
1104
|
+
.filter((entry) => !!entry)
|
|
1105
|
+
: [];
|
|
1106
|
+
}
|
|
1107
|
+
contextHintsFor(request) {
|
|
1108
|
+
return this.toRecord(request?.contextHints) ?? this.contextHintsForTurn;
|
|
1109
|
+
}
|
|
1110
|
+
tableFilterApplyOperations(patch) {
|
|
1111
|
+
const envelope = this.toRecord(this.toRecord(patch)?.['tableRuntimeOperations']);
|
|
1112
|
+
const operations = Array.isArray(envelope?.['operations']) ? envelope['operations'] : [];
|
|
1113
|
+
return operations
|
|
1114
|
+
.map((operation) => this.toRecord(operation))
|
|
1115
|
+
.filter((operation) => !!operation && this.stringValue(operation['operationId']) === 'table.filter.apply');
|
|
1116
|
+
}
|
|
1117
|
+
promptMentionsFilterField(prompt, fieldName) {
|
|
1118
|
+
const normalizedPrompt = this.normalizeLabel(prompt);
|
|
1119
|
+
if (!normalizedPrompt)
|
|
1120
|
+
return false;
|
|
1121
|
+
const entry = this.filterFieldCatalogEntries.find((candidate) => candidate.name === fieldName);
|
|
1122
|
+
const rawCandidates = [
|
|
1123
|
+
fieldName,
|
|
1124
|
+
this.humanizeFilterField(fieldName),
|
|
1125
|
+
entry?.label,
|
|
1126
|
+
...(entry?.aliases ?? []),
|
|
1127
|
+
...(entry?.relatedColumnFields ?? []),
|
|
1128
|
+
...(entry?.relatedColumnLabels ?? []),
|
|
1129
|
+
];
|
|
1130
|
+
return rawCandidates
|
|
1131
|
+
.flatMap((candidate) => this.filterFieldMentionVariants(candidate ?? ''))
|
|
1132
|
+
.some((candidate) => candidate && this.normalizedTextContainsApproxPhrase(normalizedPrompt, candidate));
|
|
1133
|
+
}
|
|
1134
|
+
selectedRecordFilterPromptGroundingScore(prompt, candidate) {
|
|
1135
|
+
const fieldName = this.stringValue(candidate['field']);
|
|
1136
|
+
if (!fieldName)
|
|
1137
|
+
return 0;
|
|
1138
|
+
const normalizedPrompt = this.normalizeLabel(prompt);
|
|
1139
|
+
if (!normalizedPrompt)
|
|
1140
|
+
return 0;
|
|
1141
|
+
const entry = this.filterFieldCatalogEntries.find((candidateEntry) => candidateEntry.name === fieldName);
|
|
1142
|
+
const candidateLabel = this.stringValue(candidate['label']);
|
|
1143
|
+
const candidateAliases = this.stringArrayValue(candidate['aliases']);
|
|
1144
|
+
const candidateRelatedColumnFields = this.stringArrayValue(candidate['relatedColumnFields']);
|
|
1145
|
+
const candidateRelatedColumnLabels = this.stringArrayValue(candidate['relatedColumnLabels']);
|
|
1146
|
+
const criterionKind = this.normalizeLabel(this.stringValue(candidate['criterionKind']) || entry?.criterionKind || '');
|
|
1147
|
+
let score = 0;
|
|
1148
|
+
score += this.promptContainsAnyVariant(normalizedPrompt, candidateLabel || entry?.label || this.humanizeFilterField(fieldName)) ? 180 : 0;
|
|
1149
|
+
score += candidateLabel && entry?.label && candidateLabel !== entry.label && this.promptContainsAnyVariant(normalizedPrompt, entry.label) ? 160 : 0;
|
|
1150
|
+
score += this.promptContainsAnyVariant(normalizedPrompt, this.humanizeFilterField(fieldName)) ? 120 : 0;
|
|
1151
|
+
for (const alias of candidateAliases) {
|
|
1152
|
+
score += this.promptContainsAnyVariant(normalizedPrompt, alias) ? 100 : 0;
|
|
1153
|
+
}
|
|
1154
|
+
for (const alias of entry?.aliases ?? []) {
|
|
1155
|
+
score += this.promptContainsAnyVariant(normalizedPrompt, alias) ? 90 : 0;
|
|
1156
|
+
}
|
|
1157
|
+
for (const label of candidateRelatedColumnLabels) {
|
|
1158
|
+
score += this.promptContainsAnyVariant(normalizedPrompt, label) ? 50 : 0;
|
|
1159
|
+
}
|
|
1160
|
+
for (const label of entry?.relatedColumnLabels ?? []) {
|
|
1161
|
+
score += this.promptContainsAnyVariant(normalizedPrompt, label) ? 40 : 0;
|
|
1162
|
+
}
|
|
1163
|
+
for (const field of candidateRelatedColumnFields) {
|
|
1164
|
+
score += this.promptContainsAnyVariant(normalizedPrompt, field) ? 35 : 0;
|
|
1165
|
+
}
|
|
1166
|
+
for (const field of entry?.relatedColumnFields ?? []) {
|
|
1167
|
+
score += this.promptContainsAnyVariant(normalizedPrompt, field) ? 30 : 0;
|
|
1168
|
+
}
|
|
1169
|
+
const hasRangeIntent = ['faixa', 'banda', 'entre'].some((token) => this.normalizedTextContainsApproxToken(normalizedPrompt, token));
|
|
1170
|
+
const hasDateIntent = ['data', 'periodo', 'admissao', 'entrou', 'entrada', 'epoca'].some((token) => this.normalizedTextContainsApproxToken(normalizedPrompt, token));
|
|
1171
|
+
const hasPeriodIntent = ['periodo', 'intervalo', 'entre'].some((token) => this.normalizedTextContainsApproxToken(normalizedPrompt, token));
|
|
1172
|
+
const hasRecentIntent = ['recente', 'recentes', 'ultimo', 'ultimos'].some((token) => this.normalizedTextContainsApproxToken(normalizedPrompt, token));
|
|
1173
|
+
if (criterionKind.includes('date range')) {
|
|
1174
|
+
score += hasDateIntent ? 70 : 0;
|
|
1175
|
+
score += hasPeriodIntent ? 45 : 0;
|
|
1176
|
+
}
|
|
1177
|
+
else if (criterionKind.includes('range')) {
|
|
1178
|
+
score += hasRangeIntent ? 70 : 0;
|
|
1179
|
+
score += hasPeriodIntent ? 25 : 0;
|
|
1180
|
+
}
|
|
1181
|
+
if (fieldName.endsWith('LastDays')) {
|
|
1182
|
+
score += hasRecentIntent ? 35 : 0;
|
|
1183
|
+
score -= hasPeriodIntent ? 25 : 0;
|
|
1184
|
+
}
|
|
1185
|
+
return score;
|
|
1186
|
+
}
|
|
1187
|
+
promptContainsAnyVariant(normalizedPrompt, value) {
|
|
1188
|
+
return this.filterFieldMentionVariants(value)
|
|
1189
|
+
.some((variant) => !!variant && this.normalizedTextContainsApproxPhrase(normalizedPrompt, variant));
|
|
1190
|
+
}
|
|
1191
|
+
filterFieldMentionVariants(value) {
|
|
1192
|
+
const normalized = this.normalizeLabel(value);
|
|
1193
|
+
if (!normalized)
|
|
1194
|
+
return [];
|
|
1195
|
+
const variants = new Set([normalized]);
|
|
1196
|
+
if (normalized.endsWith('s') && normalized.length > 3) {
|
|
1197
|
+
variants.add(normalized.slice(0, -1));
|
|
1198
|
+
}
|
|
1199
|
+
else {
|
|
1200
|
+
variants.add(`${normalized}s`);
|
|
1201
|
+
}
|
|
1202
|
+
for (const token of normalized.split(/\s+/u)) {
|
|
1203
|
+
if (token.length >= 4) {
|
|
1204
|
+
variants.add(token);
|
|
1205
|
+
if (token.endsWith('s') && token.length > 4) {
|
|
1206
|
+
variants.add(token.slice(0, -1));
|
|
1207
|
+
}
|
|
1208
|
+
else {
|
|
1209
|
+
variants.add(`${token}s`);
|
|
1210
|
+
}
|
|
1211
|
+
}
|
|
1212
|
+
}
|
|
1213
|
+
return [...variants];
|
|
1214
|
+
}
|
|
1215
|
+
normalizedTextContainsApproxPhrase(normalizedText, normalizedPhrase) {
|
|
1216
|
+
if (!normalizedText || !normalizedPhrase)
|
|
1217
|
+
return false;
|
|
1218
|
+
if (normalizedText.includes(normalizedPhrase))
|
|
1219
|
+
return true;
|
|
1220
|
+
const phraseTokens = normalizedPhrase
|
|
1221
|
+
.split(/\s+/u)
|
|
1222
|
+
.filter((token) => token.length > 2);
|
|
1223
|
+
if (!phraseTokens.length)
|
|
1224
|
+
return false;
|
|
1225
|
+
return phraseTokens.every((token) => this.normalizedTextContainsApproxToken(normalizedText, token));
|
|
1226
|
+
}
|
|
1227
|
+
normalizedTextContainsApproxToken(normalizedText, rawToken) {
|
|
1228
|
+
const token = this.normalizeLabel(rawToken);
|
|
1229
|
+
if (!normalizedText || !token)
|
|
1230
|
+
return false;
|
|
1231
|
+
if (normalizedText.includes(token))
|
|
1232
|
+
return true;
|
|
1233
|
+
if (token.length < 5)
|
|
1234
|
+
return false;
|
|
1235
|
+
const maxDistance = token.length >= 9 ? 2 : 1;
|
|
1236
|
+
return normalizedText
|
|
1237
|
+
.split(/\s+/u)
|
|
1238
|
+
.some((word) => word.length >= 4 && !this.isApproximateMatchingStopword(word) && ((word[0] === token[0] && this.boundedEditDistance(word, token, maxDistance) <= maxDistance)
|
|
1239
|
+
|| this.normalizedConsonantSignature(word) === this.normalizedConsonantSignature(token)));
|
|
1240
|
+
}
|
|
1241
|
+
isApproximateMatchingStopword(word) {
|
|
1242
|
+
return new Set([
|
|
1243
|
+
'estao',
|
|
1244
|
+
'esta',
|
|
1245
|
+
'sao',
|
|
1246
|
+
'tem',
|
|
1247
|
+
'têm',
|
|
1248
|
+
'esses',
|
|
1249
|
+
'essas',
|
|
1250
|
+
'eles',
|
|
1251
|
+
'elas',
|
|
1252
|
+
'entrou',
|
|
1253
|
+
'entrada',
|
|
1254
|
+
]).has(word);
|
|
1255
|
+
}
|
|
1256
|
+
normalizedConsonantSignature(value) {
|
|
1257
|
+
return this.normalizeLabel(value).replace(/[aeiou]/gu, '');
|
|
1258
|
+
}
|
|
1259
|
+
stringArrayValue(value) {
|
|
1260
|
+
return Array.isArray(value)
|
|
1261
|
+
? value.map((entry) => this.stringValue(entry)).filter((entry) => !!entry)
|
|
1262
|
+
: [];
|
|
1263
|
+
}
|
|
1264
|
+
boundedEditDistance(left, right, maxDistance) {
|
|
1265
|
+
if (Math.abs(left.length - right.length) > maxDistance)
|
|
1266
|
+
return maxDistance + 1;
|
|
1267
|
+
let previous = Array.from({ length: right.length + 1 }, (_, index) => index);
|
|
1268
|
+
for (let leftIndex = 1; leftIndex <= left.length; leftIndex += 1) {
|
|
1269
|
+
const current = [leftIndex];
|
|
1270
|
+
let rowMin = current[0];
|
|
1271
|
+
for (let rightIndex = 1; rightIndex <= right.length; rightIndex += 1) {
|
|
1272
|
+
const cost = left[leftIndex - 1] === right[rightIndex - 1] ? 0 : 1;
|
|
1273
|
+
const value = Math.min(previous[rightIndex] + 1, current[rightIndex - 1] + 1, previous[rightIndex - 1] + cost);
|
|
1274
|
+
current[rightIndex] = value;
|
|
1275
|
+
rowMin = Math.min(rowMin, value);
|
|
1276
|
+
}
|
|
1277
|
+
if (rowMin > maxDistance)
|
|
1278
|
+
return maxDistance + 1;
|
|
1279
|
+
previous = current;
|
|
1280
|
+
}
|
|
1281
|
+
return previous[right.length];
|
|
1282
|
+
}
|
|
1283
|
+
selectedRecordFilterClarificationOptionEntries(preferredFields) {
|
|
1284
|
+
const preferred = new Set(preferredFields);
|
|
1285
|
+
const entries = [...this.filterFieldCatalogEntries]
|
|
1286
|
+
.sort((left, right) => this.selectedRecordFilterOptionScore(right, preferred)
|
|
1287
|
+
- this.selectedRecordFilterOptionScore(left, preferred));
|
|
1288
|
+
const seen = new Set();
|
|
1289
|
+
const options = [];
|
|
1290
|
+
for (const entry of entries) {
|
|
1291
|
+
const key = this.normalizeLabel(entry.label);
|
|
1292
|
+
if (!key || seen.has(key))
|
|
1293
|
+
continue;
|
|
1294
|
+
seen.add(key);
|
|
1295
|
+
options.push(entry);
|
|
1296
|
+
if (options.length >= 4)
|
|
1297
|
+
break;
|
|
1298
|
+
}
|
|
1299
|
+
return options;
|
|
1300
|
+
}
|
|
1301
|
+
selectedRecordFilterOptionScore(entry, preferred) {
|
|
1302
|
+
let score = preferred.has(entry.name) ? 40 : 0;
|
|
1303
|
+
const name = this.normalizeLabel(entry.name);
|
|
1304
|
+
const label = this.normalizeLabel(entry.label);
|
|
1305
|
+
const controlType = this.normalizeLabel(entry.controlType ?? '');
|
|
1306
|
+
const type = this.normalizeLabel(entry.type ?? '');
|
|
1307
|
+
const criterionKind = this.normalizeLabel(entry.criterionKind ?? '');
|
|
1308
|
+
if (this.selectionDerivedFilterCandidateFields.has(entry.name))
|
|
1309
|
+
score += 120;
|
|
1310
|
+
if (/IdsIn$/u.test(entry.name))
|
|
1311
|
+
score += 40;
|
|
1312
|
+
if (criterionKind.includes('range') || controlType.includes('range'))
|
|
1313
|
+
score += 120;
|
|
1314
|
+
if (this.filterEntryHasSelectedRecordSourceField(entry))
|
|
1315
|
+
score += 45;
|
|
1316
|
+
if (criterionKind === 'date range' || type.includes('date') || type.includes('time'))
|
|
1317
|
+
score += 5;
|
|
1318
|
+
if (controlType.includes('select') || controlType.includes('lookup'))
|
|
1319
|
+
score += 25;
|
|
1320
|
+
if (type === 'boolean' || controlType.includes('switch'))
|
|
1321
|
+
score += 15;
|
|
1322
|
+
if (type === 'string' || controlType.includes('text') || controlType.includes('input'))
|
|
1323
|
+
score -= 10;
|
|
1324
|
+
if (name.includes('nome') || label.includes('nome'))
|
|
1325
|
+
score -= 15;
|
|
1326
|
+
return score;
|
|
1327
|
+
}
|
|
1328
|
+
groundRelativeColumnOrder(response, request, currentState) {
|
|
1329
|
+
const prompt = this.normalizeLabel(request.prompt ?? '');
|
|
1330
|
+
const position = this.relativeOrderPosition(prompt);
|
|
1331
|
+
if (!position)
|
|
1332
|
+
return response;
|
|
1333
|
+
const plan = this.toRecord(response.componentEditPlan);
|
|
1334
|
+
if (!plan)
|
|
1335
|
+
return response;
|
|
1336
|
+
const columns = Array.isArray(currentState['columns'])
|
|
1337
|
+
? currentState['columns']
|
|
1338
|
+
.map((column) => this.toRecord(column))
|
|
1339
|
+
.filter((column) => !!column && !!this.stringValue(column['field']))
|
|
1340
|
+
: [];
|
|
1341
|
+
if (columns.length < 2)
|
|
1342
|
+
return response;
|
|
1343
|
+
const operations = this.componentEditOperations(plan);
|
|
1344
|
+
if (!operations.some((operation) => this.isColumnOrderOperation(operation))) {
|
|
1345
|
+
return response;
|
|
1346
|
+
}
|
|
1347
|
+
const groundOperation = (operation) => {
|
|
1348
|
+
if (!this.isColumnOrderOperation(operation))
|
|
1349
|
+
return operation;
|
|
1350
|
+
const targetField = this.resolveOperationTargetField(operation, columns);
|
|
1351
|
+
if (!targetField)
|
|
1352
|
+
return operation;
|
|
1353
|
+
const reference = this.findRelativeOrderReferenceColumn(prompt, columns, targetField);
|
|
1354
|
+
if (!reference)
|
|
1355
|
+
return operation;
|
|
1356
|
+
const reorderedBase = columns.filter((column) => this.stringValue(column['field']) !== targetField);
|
|
1357
|
+
const referenceIndex = reorderedBase.findIndex((column) => this.stringValue(column['field']) === reference);
|
|
1358
|
+
if (referenceIndex < 0)
|
|
1359
|
+
return operation;
|
|
1360
|
+
const order = position === 'after' ? referenceIndex + 1 : referenceIndex;
|
|
1361
|
+
const input = {
|
|
1362
|
+
...(this.toRecord(operation['input']) ?? {}),
|
|
1363
|
+
order,
|
|
1364
|
+
};
|
|
1365
|
+
return { ...operation, input };
|
|
1366
|
+
};
|
|
1367
|
+
if (Array.isArray(plan['operations'])) {
|
|
1368
|
+
return {
|
|
1369
|
+
...response,
|
|
1370
|
+
componentEditPlan: {
|
|
1371
|
+
...plan,
|
|
1372
|
+
operations: operations.map((operation) => groundOperation(operation)),
|
|
1373
|
+
},
|
|
1374
|
+
};
|
|
1375
|
+
}
|
|
1376
|
+
return {
|
|
1377
|
+
...response,
|
|
1378
|
+
componentEditPlan: groundOperation(plan),
|
|
1379
|
+
};
|
|
1380
|
+
}
|
|
1381
|
+
relativeOrderPosition(prompt) {
|
|
1382
|
+
if (prompt.includes('antes'))
|
|
1383
|
+
return 'before';
|
|
1384
|
+
if (prompt.includes('depois') || prompt.includes('apos'))
|
|
1385
|
+
return 'after';
|
|
1386
|
+
return null;
|
|
1387
|
+
}
|
|
1388
|
+
isColumnOrderOperation(operation) {
|
|
1389
|
+
return this.stringValue(operation['operationId']) === 'column.order.set'
|
|
1390
|
+
|| this.stringValue(operation['changeKind']) === 'set_column_order';
|
|
1391
|
+
}
|
|
1392
|
+
resolveOperationTargetField(operation, columns) {
|
|
1393
|
+
const target = this.toRecord(operation['target']);
|
|
1394
|
+
const input = this.toRecord(operation['input']);
|
|
1395
|
+
const rawTarget = this.stringValue(target?.['field'])
|
|
1396
|
+
|| this.stringValue(operation['field'])
|
|
1397
|
+
|| this.stringValue(input?.['field']);
|
|
1398
|
+
if (!rawTarget)
|
|
1399
|
+
return null;
|
|
1400
|
+
const normalizedTarget = this.normalizeLabel(rawTarget);
|
|
1401
|
+
const match = columns.find((column) => this.normalizeLabel(this.stringValue(column['field'])) === normalizedTarget
|
|
1402
|
+
|| this.normalizeLabel(this.stringValue(column['header'])) === normalizedTarget);
|
|
1403
|
+
return this.stringValue(match?.['field']) || rawTarget;
|
|
1404
|
+
}
|
|
1405
|
+
findRelativeOrderReferenceColumn(prompt, columns, targetField) {
|
|
1406
|
+
const candidates = columns
|
|
1407
|
+
.filter((column) => this.stringValue(column['field']) !== targetField)
|
|
1408
|
+
.map((column) => {
|
|
1409
|
+
const field = this.stringValue(column['field']);
|
|
1410
|
+
const header = this.stringValue(column['header']) || this.humanizeField(field);
|
|
1411
|
+
const fieldKey = this.normalizeLabel(field);
|
|
1412
|
+
const headerKey = this.normalizeLabel(header);
|
|
1413
|
+
let score = 0;
|
|
1414
|
+
if (headerKey && prompt.includes(headerKey))
|
|
1415
|
+
score += 4;
|
|
1416
|
+
if (fieldKey && prompt.includes(fieldKey))
|
|
1417
|
+
score += 3;
|
|
1418
|
+
return { field, score };
|
|
1419
|
+
})
|
|
1420
|
+
.filter((candidate) => candidate.field && candidate.score > 0)
|
|
1421
|
+
.sort((left, right) => right.score - left.score);
|
|
1422
|
+
return candidates[0]?.field ?? null;
|
|
1423
|
+
}
|
|
1424
|
+
toReviewMessage(response) {
|
|
1425
|
+
const runtimeSummaries = this.describeTableRuntimeOperations(response.patch);
|
|
1426
|
+
if (runtimeSummaries.length) {
|
|
1427
|
+
return [
|
|
1428
|
+
'Preparei esta operação para revisão:',
|
|
1429
|
+
'',
|
|
1430
|
+
...runtimeSummaries.map((summary) => `- ${summary}`),
|
|
1431
|
+
].join('\n');
|
|
1432
|
+
}
|
|
1433
|
+
const planSummaries = this.describeComponentEditPlan(response.componentEditPlan);
|
|
1434
|
+
if (planSummaries.length) {
|
|
1435
|
+
return [
|
|
1436
|
+
'Preparei este ajuste para revisão:',
|
|
1437
|
+
'',
|
|
1438
|
+
...planSummaries.map((summary) => `- ${summary}`),
|
|
1439
|
+
].join('\n');
|
|
1440
|
+
}
|
|
1441
|
+
return response.explanation || 'Proposta de alteração pronta para revisar.';
|
|
1442
|
+
}
|
|
1443
|
+
describeTableRuntimeOperations(patch) {
|
|
1444
|
+
const envelope = this.toRecord(this.toRecord(patch)?.['tableRuntimeOperations']);
|
|
1445
|
+
const operations = Array.isArray(envelope?.['operations']) ? envelope['operations'] : [];
|
|
1446
|
+
return operations
|
|
1447
|
+
.map((operation) => this.toRecord(operation))
|
|
1448
|
+
.filter((operation) => !!operation)
|
|
1449
|
+
.map((operation) => this.describeTableRuntimeOperation(operation))
|
|
1450
|
+
.filter((summary) => !!summary);
|
|
1451
|
+
}
|
|
1452
|
+
describeTableRuntimeOperation(operation) {
|
|
1453
|
+
const operationId = this.stringValue(operation['operationId']);
|
|
1454
|
+
const input = this.toRecord(operation['input']) ?? {};
|
|
1455
|
+
if (operationId === 'table.filter.apply') {
|
|
1456
|
+
const criteria = this.toRecord(input['criteria']) ?? {};
|
|
1457
|
+
const fields = Object.keys(criteria);
|
|
1458
|
+
if (!fields.length)
|
|
1459
|
+
return 'Vou aplicar filtros na tabela.';
|
|
1460
|
+
return `Vou aplicar filtros por ${fields.map((field) => `**${this.humanizeFilterField(field)}**`).join(', ')}.`;
|
|
1461
|
+
}
|
|
1462
|
+
if (operationId === 'table.export.run') {
|
|
1463
|
+
const format = this.stringValue(input['format']).toUpperCase();
|
|
1464
|
+
const scope = this.describeExportScope(this.stringValue(input['scope']));
|
|
1465
|
+
return `Vou exportar ${scope}${format ? ` em **${format}**` : ''}.`;
|
|
1466
|
+
}
|
|
1467
|
+
if (operationId === 'dynamicPage.surface.open') {
|
|
1468
|
+
const surfaceId = this.stringValue(input['surfaceId'] ?? input['id']);
|
|
1469
|
+
const surfaceLabel = this.stringValue(input['surfaceLabel']);
|
|
1470
|
+
return surfaceId
|
|
1471
|
+
? `Vou abrir a superfície relacionada **${surfaceLabel || this.humanizeField(surfaceId)}**.`
|
|
1472
|
+
: 'Vou abrir a superfície relacionada solicitada.';
|
|
1473
|
+
}
|
|
1474
|
+
return null;
|
|
1475
|
+
}
|
|
1476
|
+
describeExportScope(scope) {
|
|
1477
|
+
switch (scope) {
|
|
1478
|
+
case 'selected':
|
|
1479
|
+
return 'as linhas selecionadas';
|
|
1480
|
+
case 'filtered':
|
|
1481
|
+
return 'o resultado filtrado';
|
|
1482
|
+
case 'currentPage':
|
|
1483
|
+
return 'a pagina atual';
|
|
1484
|
+
case 'all':
|
|
1485
|
+
return 'todos os registros';
|
|
1486
|
+
default:
|
|
1487
|
+
return 'os dados da tabela';
|
|
1488
|
+
}
|
|
1489
|
+
}
|
|
1490
|
+
describeComponentEditPlan(componentEditPlan) {
|
|
1491
|
+
const operations = this.componentEditOperations(componentEditPlan);
|
|
1492
|
+
const booleanStateSummary = this.describeBooleanStateRenderers(operations);
|
|
1493
|
+
if (booleanStateSummary)
|
|
1494
|
+
return [booleanStateSummary];
|
|
1495
|
+
const categoricalRendererSummary = this.describeCategoricalRenderers(operations);
|
|
1496
|
+
if (categoricalRendererSummary)
|
|
1497
|
+
return [categoricalRendererSummary];
|
|
1498
|
+
const seen = new Set();
|
|
1499
|
+
return operations
|
|
1500
|
+
.map((operation) => this.describeComponentEditOperation(operation))
|
|
1501
|
+
.filter((summary) => {
|
|
1502
|
+
if (!summary || seen.has(summary))
|
|
1503
|
+
return false;
|
|
1504
|
+
seen.add(summary);
|
|
1505
|
+
return true;
|
|
1506
|
+
});
|
|
1507
|
+
}
|
|
1508
|
+
componentEditOperations(componentEditPlan) {
|
|
1509
|
+
const plan = this.toRecord(componentEditPlan);
|
|
1510
|
+
if (!plan)
|
|
1511
|
+
return [];
|
|
1512
|
+
const operations = plan['operations'];
|
|
1513
|
+
if (Array.isArray(operations)) {
|
|
1514
|
+
return operations
|
|
1515
|
+
.map((operation) => this.toRecord(operation))
|
|
1516
|
+
.filter((operation) => !!operation);
|
|
1517
|
+
}
|
|
1518
|
+
return plan['operationId'] || plan['changeKind'] || plan['capabilityPath']
|
|
1519
|
+
? [plan]
|
|
1520
|
+
: [];
|
|
1521
|
+
}
|
|
1522
|
+
buildClarificationDiagnostics(response) {
|
|
1523
|
+
const continuation = this.extractPendingComponentEditContinuation(response);
|
|
1524
|
+
return continuation ? { tableComponentEditContinuation: continuation } : undefined;
|
|
1525
|
+
}
|
|
1526
|
+
buildReviewDiagnostics(response, warnings) {
|
|
1527
|
+
const memory = this.extractComponentEditDecisionMemory(response);
|
|
1528
|
+
if (!memory && !warnings.length)
|
|
1529
|
+
return undefined;
|
|
1530
|
+
return {
|
|
1531
|
+
...(warnings.length ? { warnings: [...warnings] } : {}),
|
|
1532
|
+
...(memory ? { tableComponentEditDecision: memory } : {}),
|
|
1533
|
+
};
|
|
1534
|
+
}
|
|
1535
|
+
tableConversationMemoryHints(request) {
|
|
1536
|
+
const diagnostics = this.toRecord(request.diagnostics);
|
|
1537
|
+
const decision = this.toRecord(diagnostics?.['tableComponentEditDecision']);
|
|
1538
|
+
if (!decision)
|
|
1539
|
+
return undefined;
|
|
1540
|
+
return {
|
|
1541
|
+
tableConversationMemory: {
|
|
1542
|
+
lastComponentEditDecision: this.toAiJsonObject(decision),
|
|
1543
|
+
},
|
|
1544
|
+
};
|
|
1545
|
+
}
|
|
1546
|
+
extractComponentEditDecisionMemory(response) {
|
|
1547
|
+
const operations = this.componentEditOperations(response.componentEditPlan);
|
|
1548
|
+
if (!operations.length)
|
|
1549
|
+
return null;
|
|
1550
|
+
const mappedOperations = operations
|
|
1551
|
+
.map((operation) => this.extractComponentEditOperationMemory(operation))
|
|
1552
|
+
.filter((operation) => !!operation);
|
|
1553
|
+
if (!mappedOperations.length)
|
|
1554
|
+
return null;
|
|
1555
|
+
return {
|
|
1556
|
+
kind: 'praxis.table.component-edit-decision-memory',
|
|
1557
|
+
operations: mappedOperations,
|
|
1558
|
+
lastTarget: mappedOperations.at(-1)?.['target'] ?? null,
|
|
1559
|
+
lastOperationId: this.stringValue(mappedOperations.at(-1)?.['operationId']),
|
|
1560
|
+
};
|
|
1561
|
+
}
|
|
1562
|
+
extractComponentEditOperationMemory(operation) {
|
|
1563
|
+
const operationId = this.stringValue(operation['operationId']) || this.stringValue(operation['changeKind']);
|
|
1564
|
+
const target = this.toRecord(operation['target']) ?? {};
|
|
1565
|
+
const input = this.toRecord(operation['input']) ?? this.toRecord(operation['params']) ?? {};
|
|
1566
|
+
const field = this.stringValue(target['field'])
|
|
1567
|
+
|| this.stringValue(operation['field'])
|
|
1568
|
+
|| this.stringValue(input['field']);
|
|
1569
|
+
if (!operationId && !field)
|
|
1570
|
+
return null;
|
|
1571
|
+
return {
|
|
1572
|
+
...(operationId ? { operationId } : {}),
|
|
1573
|
+
...(field ? { target: { ...target, kind: this.stringValue(target['kind']) || 'column', field } } : {}),
|
|
1574
|
+
...(Object.keys(input).length ? { input } : {}),
|
|
1575
|
+
};
|
|
1576
|
+
}
|
|
1577
|
+
extractPendingComponentEditContinuation(response) {
|
|
1578
|
+
const operation = this.componentEditOperations(response.componentEditPlan)[0];
|
|
1579
|
+
if (!operation)
|
|
1580
|
+
return null;
|
|
1581
|
+
const operationId = this.stringValue(operation['operationId']) || this.stringValue(operation['changeKind']);
|
|
1582
|
+
const target = this.toRecord(operation['target']) ?? {};
|
|
1583
|
+
const input = this.toRecord(operation['input']) ?? this.toRecord(operation['params']) ?? {};
|
|
1584
|
+
const field = this.stringValue(target['field'])
|
|
1585
|
+
|| this.stringValue(operation['field'])
|
|
1586
|
+
|| this.stringValue(input['field']);
|
|
1587
|
+
if (!operationId || !field)
|
|
1588
|
+
return null;
|
|
1589
|
+
const missingInputKey = this.pendingComponentEditInputKey(operationId, input);
|
|
1590
|
+
if (!missingInputKey)
|
|
1591
|
+
return null;
|
|
1592
|
+
return {
|
|
1593
|
+
operationId,
|
|
1594
|
+
target: { ...target, field },
|
|
1595
|
+
input: { ...input },
|
|
1596
|
+
missingInputKey,
|
|
1597
|
+
};
|
|
1598
|
+
}
|
|
1599
|
+
pendingComponentEditInputKey(operationId, input) {
|
|
1600
|
+
if (operationId === 'column.width.set' && !this.stringValue(input['width'] ?? input['value'])) {
|
|
1601
|
+
return 'width';
|
|
1602
|
+
}
|
|
1603
|
+
return null;
|
|
1604
|
+
}
|
|
1605
|
+
completePendingComponentEditClarification(request) {
|
|
1606
|
+
if (request.action?.kind !== 'clarify')
|
|
1607
|
+
return null;
|
|
1608
|
+
const diagnostics = this.toRecord(request.pendingClarification?.diagnostics);
|
|
1609
|
+
const continuation = this.toRecord(diagnostics?.['tableComponentEditContinuation']);
|
|
1610
|
+
if (!continuation)
|
|
1611
|
+
return null;
|
|
1612
|
+
const operationId = this.stringValue(continuation['operationId']);
|
|
1613
|
+
const missingInputKey = this.stringValue(continuation['missingInputKey']);
|
|
1614
|
+
const target = this.toRecord(continuation['target']);
|
|
1615
|
+
const input = this.toRecord(continuation['input']) ?? {};
|
|
1616
|
+
const value = (request.prompt ?? '').trim();
|
|
1617
|
+
if (!operationId || !missingInputKey || !target || !value)
|
|
1618
|
+
return null;
|
|
1619
|
+
return {
|
|
1620
|
+
type: 'patch',
|
|
1621
|
+
sessionId: request.sessionId,
|
|
1622
|
+
componentEditPlan: {
|
|
1623
|
+
kind: 'praxis.table.component-edit-plan',
|
|
1624
|
+
version: '1.0',
|
|
1625
|
+
componentId: this.adapter.componentId || request.componentId || 'praxis-table',
|
|
1626
|
+
operationId,
|
|
1627
|
+
target,
|
|
1628
|
+
input: {
|
|
1629
|
+
...input,
|
|
1630
|
+
[missingInputKey]: value,
|
|
1631
|
+
},
|
|
1632
|
+
},
|
|
1633
|
+
explanation: 'Clarificacao aplicada ao ajuste pendente.',
|
|
1634
|
+
};
|
|
1635
|
+
}
|
|
1636
|
+
describeBooleanStateRenderers(operations) {
|
|
1637
|
+
if (operations.length < 2)
|
|
1638
|
+
return null;
|
|
1639
|
+
const rendererOperations = operations
|
|
1640
|
+
.filter((operation) => this.stringValue(operation['operationId']) === 'column.conditionalRenderer.add')
|
|
1641
|
+
.map((operation) => {
|
|
1642
|
+
const target = this.toRecord(operation['target']);
|
|
1643
|
+
const input = this.toRecord(operation['input']) ?? this.toRecord(operation['params']) ?? {};
|
|
1644
|
+
const renderer = this.toRecord(input['renderer']);
|
|
1645
|
+
const rendererType = this.stringValue(renderer?.['type']) || 'badge';
|
|
1646
|
+
const visual = this.toRecord(renderer?.[rendererType]) ?? {};
|
|
1647
|
+
return {
|
|
1648
|
+
field: this.stringValue(target?.['field']) || this.stringValue(operation['field']) || this.stringValue(input['field']),
|
|
1649
|
+
trueLabel: this.booleanConditionValue(input['condition']) === true ? this.stringValue(visual['text']) : '',
|
|
1650
|
+
falseLabel: this.booleanConditionValue(input['condition']) === false ? this.stringValue(visual['text']) : '',
|
|
1651
|
+
rendererType,
|
|
1652
|
+
details: this.booleanRendererBranchDetails(input, visual),
|
|
1653
|
+
};
|
|
1654
|
+
})
|
|
1655
|
+
.filter((item) => item.field && (item.trueLabel || item.falseLabel));
|
|
1656
|
+
if (rendererOperations.length < 2)
|
|
1657
|
+
return null;
|
|
1658
|
+
const field = rendererOperations[0].field;
|
|
1659
|
+
if (!rendererOperations.every((item) => item.field === field))
|
|
1660
|
+
return null;
|
|
1661
|
+
const trueLabel = rendererOperations.find((item) => item.trueLabel)?.trueLabel;
|
|
1662
|
+
const falseLabel = rendererOperations.find((item) => item.falseLabel)?.falseLabel;
|
|
1663
|
+
if (!trueLabel || !falseLabel)
|
|
1664
|
+
return null;
|
|
1665
|
+
const rendererType = rendererOperations.find((item) => item.rendererType)?.rendererType || 'badge';
|
|
1666
|
+
const trueDetails = rendererOperations.find((item) => item.trueLabel)?.details ?? [];
|
|
1667
|
+
const falseDetails = rendererOperations.find((item) => item.falseLabel)?.details ?? [];
|
|
1668
|
+
const trueSuffix = trueDetails.length ? ` (${trueDetails.join(', ')})` : '';
|
|
1669
|
+
const falseSuffix = falseDetails.length ? ` (${falseDetails.join(', ')})` : '';
|
|
1670
|
+
return `Vou mostrar a coluna **${this.humanizeField(field)}** como ${this.rendererLabel(rendererType)}: **${trueLabel}** para verdadeiro${trueSuffix} e **${falseLabel}** para falso${falseSuffix}.`;
|
|
1671
|
+
}
|
|
1672
|
+
booleanRendererBranchDetails(input, visual) {
|
|
1673
|
+
const details = [];
|
|
1674
|
+
const variant = this.stringValue(visual['variant']);
|
|
1675
|
+
const color = this.stringValue(visual['color']);
|
|
1676
|
+
const icon = this.stringValue(visual['icon']);
|
|
1677
|
+
const tooltip = this.tooltipLabel(input['tooltip']);
|
|
1678
|
+
if (variant)
|
|
1679
|
+
details.push(`visual ${this.variantLabel(variant)}`);
|
|
1680
|
+
if (color)
|
|
1681
|
+
details.push(`cor ${this.colorLabel(color)}`);
|
|
1682
|
+
if (icon)
|
|
1683
|
+
details.push(`icone ${icon}`);
|
|
1684
|
+
if (tooltip)
|
|
1685
|
+
details.push(`dica "${tooltip}"`);
|
|
1686
|
+
return details;
|
|
1687
|
+
}
|
|
1688
|
+
describeCategoricalRenderers(operations) {
|
|
1689
|
+
const rendererOperations = operations
|
|
1690
|
+
.filter((operation) => this.stringValue(operation['operationId']) === 'column.conditionalRenderer.add')
|
|
1691
|
+
.map((operation) => {
|
|
1692
|
+
const target = this.toRecord(operation['target']);
|
|
1693
|
+
const input = this.toRecord(operation['input']) ?? this.toRecord(operation['params']) ?? {};
|
|
1694
|
+
const field = this.stringValue(target?.['field'])
|
|
1695
|
+
|| this.stringValue(operation['field'])
|
|
1696
|
+
|| this.stringValue(input['field']);
|
|
1697
|
+
const equality = this.literalEqualityCondition(input['condition']);
|
|
1698
|
+
const renderer = this.toRecord(input['renderer']);
|
|
1699
|
+
const rendererType = this.stringValue(renderer?.['type']) || 'badge';
|
|
1700
|
+
const visual = this.toRecord(renderer?.[rendererType]) ?? {};
|
|
1701
|
+
const label = this.stringValue(visual['text']) || equality?.valueLabel || '';
|
|
1702
|
+
return {
|
|
1703
|
+
field,
|
|
1704
|
+
conditionField: equality?.field,
|
|
1705
|
+
valueLabel: equality?.valueLabel,
|
|
1706
|
+
label,
|
|
1707
|
+
rendererType,
|
|
1708
|
+
details: this.categoricalRendererDetails(input, visual),
|
|
1709
|
+
};
|
|
1710
|
+
})
|
|
1711
|
+
.filter((item) => item.field && item.conditionField && item.valueLabel);
|
|
1712
|
+
if (rendererOperations.length < 2)
|
|
1713
|
+
return null;
|
|
1714
|
+
const field = rendererOperations[0].field;
|
|
1715
|
+
if (!rendererOperations.every((item) => item.field === field && item.conditionField === field))
|
|
1716
|
+
return null;
|
|
1717
|
+
const rendererType = rendererOperations.find((item) => item.rendererType)?.rendererType || 'badge';
|
|
1718
|
+
const uniqueOperations = this.uniqueCategoricalRendererOperations(rendererOperations);
|
|
1719
|
+
const options = uniqueOperations
|
|
1720
|
+
.slice(0, 6)
|
|
1721
|
+
.map((item) => {
|
|
1722
|
+
const details = item.details.length ? ` (${item.details.join(', ')})` : '';
|
|
1723
|
+
return `**${this.enumValueLabel(item.label || item.valueLabel || '')}**${details}`;
|
|
1724
|
+
});
|
|
1725
|
+
const suffix = uniqueOperations.length > options.length
|
|
1726
|
+
? ` e mais ${uniqueOperations.length - options.length} valores`
|
|
1727
|
+
: '';
|
|
1728
|
+
return `Vou mostrar a coluna **${this.humanizeField(field)}** como ${this.rendererPluralLabel(rendererType)} coloridos para ${options.join(', ')}${suffix}.`;
|
|
1729
|
+
}
|
|
1730
|
+
categoricalRendererDetails(input, visual) {
|
|
1731
|
+
const details = this.rendererVisualDetails(visual);
|
|
1732
|
+
const tooltip = this.tooltipLabel(input['tooltip']);
|
|
1733
|
+
if (tooltip)
|
|
1734
|
+
details.push(`dica "${tooltip}"`);
|
|
1735
|
+
return details;
|
|
1736
|
+
}
|
|
1737
|
+
uniqueCategoricalRendererOperations(operations) {
|
|
1738
|
+
const seen = new Set();
|
|
1739
|
+
const unique = [];
|
|
1740
|
+
for (const operation of operations) {
|
|
1741
|
+
const key = this.normalizeComparableLabel(operation.valueLabel || '');
|
|
1742
|
+
if (!key || seen.has(key))
|
|
1743
|
+
continue;
|
|
1744
|
+
seen.add(key);
|
|
1745
|
+
unique.push(operation);
|
|
1746
|
+
}
|
|
1747
|
+
return unique;
|
|
1748
|
+
}
|
|
1749
|
+
describeComponentEditOperation(operation) {
|
|
1750
|
+
const operationId = this.stringValue(operation['operationId']) || this.stringValue(operation['changeKind']);
|
|
1751
|
+
const target = this.toRecord(operation['target']);
|
|
1752
|
+
const input = this.toRecord(operation['input']) ?? this.toRecord(operation['params']) ?? {};
|
|
1753
|
+
const field = this.stringValue(target?.['field'])
|
|
1754
|
+
|| this.stringValue(operation['field'])
|
|
1755
|
+
|| this.stringValue(input['field']);
|
|
1756
|
+
const label = field ? this.humanizeField(field) : 'a tabela';
|
|
1757
|
+
switch (operationId) {
|
|
1758
|
+
case 'column.add':
|
|
1759
|
+
case 'add_column':
|
|
1760
|
+
return `Vou adicionar a coluna **${label}** na tabela.`;
|
|
1761
|
+
case 'column.format.set':
|
|
1762
|
+
case 'set_column_format':
|
|
1763
|
+
return `Vou formatar a coluna **${label}** como **${this.formatLabel(input['format'] ?? operation['value'])}**.`;
|
|
1764
|
+
case 'column.valueMapping.set':
|
|
1765
|
+
case 'set_column_value_mapping':
|
|
1766
|
+
return this.describeValueMapping(label, input['valueMapping'] ?? operation['valueMapping'] ?? operation['value']);
|
|
1767
|
+
case 'column.header.set':
|
|
1768
|
+
case 'set_column_header':
|
|
1769
|
+
return `Vou renomear a coluna **${label}** para **${this.stringValue(input['header'] ?? operation['value']) || label}**.`;
|
|
1770
|
+
case 'column.visibility.set':
|
|
1771
|
+
case 'set_column_visibility':
|
|
1772
|
+
return `${this.booleanInput(input['visible'] ?? operation['value']) === false ? 'Vou ocultar' : 'Vou exibir'} a coluna **${label}**.`;
|
|
1773
|
+
case 'column.sticky.set':
|
|
1774
|
+
return this.describeStickyColumn(label, input['sticky'] ?? input['value'] ?? operation['value']);
|
|
1775
|
+
case 'column.width.set':
|
|
1776
|
+
return `Vou ajustar a largura da coluna **${label}**.`;
|
|
1777
|
+
case 'column.order.set':
|
|
1778
|
+
return `Vou reposicionar a coluna **${label}** na tabela.`;
|
|
1779
|
+
case 'column.align.set':
|
|
1780
|
+
return `Vou alinhar a coluna **${label}** ${this.alignmentLabel(input['align'] ?? input['value'] ?? operation['value'])}.`;
|
|
1781
|
+
case 'column.style.set':
|
|
1782
|
+
return this.describeColumnStyle(label, input['style'] ?? input['value'] ?? operation['value'], 'coluna');
|
|
1783
|
+
case 'column.headerStyle.set':
|
|
1784
|
+
return this.describeColumnStyle(label, input['headerStyle'] ?? input['value'] ?? operation['value'], 'cabecalho');
|
|
1785
|
+
case 'column.remove':
|
|
1786
|
+
return `Vou remover a coluna **${label}** da tabela.`;
|
|
1787
|
+
case 'column.renderer.set':
|
|
1788
|
+
case 'set_column_renderer':
|
|
1789
|
+
return `Vou ajustar a apresentacao visual da coluna **${label}**.`;
|
|
1790
|
+
case 'column.conditionalRenderer.add':
|
|
1791
|
+
case 'set_column_conditional_badge_renderers':
|
|
1792
|
+
return this.describeConditionalRenderer(label, input);
|
|
1793
|
+
case 'column.conditionalStyle.add':
|
|
1794
|
+
case 'set_column_conditional_style':
|
|
1795
|
+
return this.describeConditionalStyle(label, this.toRecord(operation['value']) ?? input);
|
|
1796
|
+
case 'column.computed.add':
|
|
1797
|
+
case 'column.computed.set':
|
|
1798
|
+
case 'add_computed_column':
|
|
1799
|
+
return `Vou criar ou atualizar a coluna calculada **${label}**.`;
|
|
1800
|
+
case 'behavior.filtering.configure':
|
|
1801
|
+
return 'Vou atualizar os filtros da tabela.';
|
|
1802
|
+
case 'filter.advanced.configure':
|
|
1803
|
+
case 'configure_advanced_filters':
|
|
1804
|
+
return this.describeAdvancedFiltersConfigure(input);
|
|
1805
|
+
case 'filter.advanced.fields.add':
|
|
1806
|
+
return this.describeAdvancedFilterFields(input, 'add');
|
|
1807
|
+
case 'filter.advanced.fields.remove':
|
|
1808
|
+
return this.describeAdvancedFilterFields(input, 'remove');
|
|
1809
|
+
case 'behavior.pagination.configure':
|
|
1810
|
+
return 'Vou atualizar a paginacao da tabela.';
|
|
1811
|
+
case 'behavior.selection.configure':
|
|
1812
|
+
return 'Vou atualizar a selecao de linhas.';
|
|
1813
|
+
case 'toolbar.configure':
|
|
1814
|
+
return 'Vou ajustar a barra de acoes da tabela.';
|
|
1815
|
+
case 'toolbar.action.add':
|
|
1816
|
+
case 'add_toolbar_action':
|
|
1817
|
+
return `Vou adicionar a acao **${this.stringValue(input['label']) || this.stringValue(input['id']) || 'solicitada'}** na barra da tabela.`;
|
|
1818
|
+
case 'rowAction.add':
|
|
1819
|
+
case 'add_row_action':
|
|
1820
|
+
return `Vou criar um botao em cada linha para abrir **${this.stringValue(input['label']) || this.stringValue(input['id']) || 'a opcao solicitada'}**.`;
|
|
1821
|
+
case 'export.configure':
|
|
1822
|
+
return this.describeExportConfigure(input);
|
|
1823
|
+
case 'appearance.density.set':
|
|
1824
|
+
return `Vou ajustar a densidade da tabela para **${this.stringValue(input['density'] ?? operation['value']) || 'o valor escolhido'}**.`;
|
|
1825
|
+
default:
|
|
1826
|
+
if (field && (input['format'] != null || operation['format'] != null)) {
|
|
1827
|
+
return `Vou formatar a coluna **${label}** como **${this.formatLabel(input['format'] ?? operation['format'])}**.`;
|
|
1828
|
+
}
|
|
1829
|
+
return field
|
|
1830
|
+
? `Vou atualizar a coluna **${label}**.`
|
|
1831
|
+
: 'Vou atualizar a configuracao da tabela.';
|
|
1832
|
+
}
|
|
1833
|
+
}
|
|
1834
|
+
describeExportConfigure(input) {
|
|
1835
|
+
const scope = this.stringValue(input['scope'] ?? input['exportScope']);
|
|
1836
|
+
const serialized = JSON.stringify(input).toLowerCase();
|
|
1837
|
+
const selectedOnly = scope === 'selected'
|
|
1838
|
+
|| scope === 'selection'
|
|
1839
|
+
|| serialized.includes('selected')
|
|
1840
|
+
|| serialized.includes('selection');
|
|
1841
|
+
if (selectedOnly) {
|
|
1842
|
+
return 'Vou habilitar a exportacao das linhas selecionadas.';
|
|
1843
|
+
}
|
|
1844
|
+
return 'Vou habilitar a exportacao da tabela.';
|
|
1845
|
+
}
|
|
1846
|
+
describeValueMapping(label, mappingValue) {
|
|
1847
|
+
const mapping = this.toRecord(mappingValue);
|
|
1848
|
+
if (!mapping) {
|
|
1849
|
+
return `Vou mapear os valores da coluna **${label}** para rotulos legiveis.`;
|
|
1850
|
+
}
|
|
1851
|
+
const trueLabel = this.stringValue(mapping['true'] ?? mapping['TRUE'] ?? mapping['1']);
|
|
1852
|
+
const falseLabel = this.stringValue(mapping['false'] ?? mapping['FALSE'] ?? mapping['0']);
|
|
1853
|
+
if (trueLabel && falseLabel) {
|
|
1854
|
+
return `Vou mostrar a coluna **${label}** como **${trueLabel}/${falseLabel}**.`;
|
|
1855
|
+
}
|
|
1856
|
+
const entries = Object.entries(mapping)
|
|
1857
|
+
.map(([key, value]) => {
|
|
1858
|
+
const text = this.stringValue(value);
|
|
1859
|
+
return key && text ? `**${key}** como **${text}**` : null;
|
|
1860
|
+
})
|
|
1861
|
+
.filter((item) => !!item)
|
|
1862
|
+
.slice(0, 3);
|
|
1863
|
+
if (entries.length > 0) {
|
|
1864
|
+
return `Vou mapear ${entries.join(', ')} na coluna **${label}**.`;
|
|
1865
|
+
}
|
|
1866
|
+
return `Vou mapear os valores da coluna **${label}** para rotulos legiveis.`;
|
|
1867
|
+
}
|
|
1868
|
+
describeStickyColumn(label, value) {
|
|
1869
|
+
const sticky = this.stringValue(value).toLowerCase();
|
|
1870
|
+
if (sticky === 'false' || sticky === 'none') {
|
|
1871
|
+
return `Vou desafixar a coluna **${label}**.`;
|
|
1872
|
+
}
|
|
1873
|
+
if (sticky === 'end' || sticky === 'right') {
|
|
1874
|
+
return `Vou fixar a coluna **${label}** no fim da tabela.`;
|
|
1875
|
+
}
|
|
1876
|
+
return `Vou fixar a coluna **${label}** no inicio da tabela.`;
|
|
1877
|
+
}
|
|
1878
|
+
alignmentLabel(value) {
|
|
1879
|
+
switch (this.stringValue(value).toLowerCase()) {
|
|
1880
|
+
case 'left':
|
|
1881
|
+
return 'a esquerda';
|
|
1882
|
+
case 'right':
|
|
1883
|
+
return 'a direita';
|
|
1884
|
+
case 'center':
|
|
1885
|
+
return 'ao centro';
|
|
1886
|
+
default:
|
|
1887
|
+
return 'como solicitado';
|
|
1888
|
+
}
|
|
1889
|
+
}
|
|
1890
|
+
describeAdvancedFiltersConfigure(input) {
|
|
1891
|
+
const enabled = this.booleanInput(input['enabled']);
|
|
1892
|
+
if (enabled === false) {
|
|
1893
|
+
return 'Vou desativar os filtros avancados da tabela.';
|
|
1894
|
+
}
|
|
1895
|
+
const settings = this.toRecord(input['settings']) ?? {};
|
|
1896
|
+
const fields = this.arrayOfStrings(settings['alwaysVisibleFields']);
|
|
1897
|
+
if (fields.length) {
|
|
1898
|
+
return `Vou ativar os filtros avancados e deixar **${fields.map((field) => this.humanizeFilterField(field)).join(', ')}** sempre visiveis.`;
|
|
1899
|
+
}
|
|
1900
|
+
return 'Vou ativar os filtros avancados da tabela.';
|
|
1901
|
+
}
|
|
1902
|
+
describeAdvancedFilterFields(input, action) {
|
|
1903
|
+
const fields = this.arrayOfStrings(input['fields']);
|
|
1904
|
+
if (!fields.length) {
|
|
1905
|
+
return action === 'add'
|
|
1906
|
+
? 'Vou incluir novos campos nos filtros avancados.'
|
|
1907
|
+
: 'Vou remover campos dos filtros avancados.';
|
|
1908
|
+
}
|
|
1909
|
+
const labels = fields.map((field) => this.humanizeFilterField(field)).join(', ');
|
|
1910
|
+
if (action === 'remove') {
|
|
1911
|
+
return `Vou remover **${labels}** dos filtros avancados.`;
|
|
1912
|
+
}
|
|
1913
|
+
const alwaysVisible = this.booleanInput(input['alwaysVisible']);
|
|
1914
|
+
if (alwaysVisible === true) {
|
|
1915
|
+
return `Vou incluir **${labels}** nos filtros avancados e deixar disponivel na area principal.`;
|
|
1916
|
+
}
|
|
1917
|
+
return `Vou incluir **${labels}** nos filtros avancados.`;
|
|
1918
|
+
}
|
|
1919
|
+
describeConditionalRenderer(label, input) {
|
|
1920
|
+
const renderer = this.toRecord(input['renderer']);
|
|
1921
|
+
const rendererType = this.stringValue(renderer?.['type']) || 'badge';
|
|
1922
|
+
const visual = this.toRecord(renderer?.[rendererType]) ?? {};
|
|
1923
|
+
const text = this.stringValue(visual['text']);
|
|
1924
|
+
const visualDetails = this.rendererVisualDetails(visual);
|
|
1925
|
+
const visualSuffix = visualDetails.length ? `, ${visualDetails.join(', ')}` : '';
|
|
1926
|
+
const description = this.stringValue(input['description']);
|
|
1927
|
+
const condition = this.conditionPhrase(input['condition'], label);
|
|
1928
|
+
const conditionSuffix = condition ? ` quando **${condition}**` : '';
|
|
1929
|
+
const tooltip = this.tooltipLabel(input['tooltip']) ?? this.semanticDescriptionLabel(input['description']);
|
|
1930
|
+
if (tooltip && !text) {
|
|
1931
|
+
return `Vou adicionar uma dica na coluna **${label}**${conditionSuffix}: **${tooltip}**.`;
|
|
1932
|
+
}
|
|
1933
|
+
if (text) {
|
|
1934
|
+
return `Vou destacar a coluna **${label}** com ${this.rendererLabel(rendererType)} **${text}**${visualSuffix}${conditionSuffix}.`;
|
|
1935
|
+
}
|
|
1936
|
+
if (description) {
|
|
1937
|
+
return `Vou destacar a coluna **${label}**${visualSuffix} quando **${description}**.`;
|
|
1938
|
+
}
|
|
1939
|
+
return `Vou destacar a coluna **${label}**${visualSuffix}${conditionSuffix}.`;
|
|
1940
|
+
}
|
|
1941
|
+
rendererVisualDetails(visual) {
|
|
1942
|
+
const details = [];
|
|
1943
|
+
const color = this.stringValue(visual['color']);
|
|
1944
|
+
const variant = this.stringValue(visual['variant']);
|
|
1945
|
+
const icon = this.stringValue(visual['icon']);
|
|
1946
|
+
if (color)
|
|
1947
|
+
details.push(`cor ${this.colorLabel(color)}`);
|
|
1948
|
+
if (variant)
|
|
1949
|
+
details.push(`estilo ${this.variantLabel(variant)}`);
|
|
1950
|
+
if (icon)
|
|
1951
|
+
details.push(`icone ${icon}`);
|
|
1952
|
+
return details;
|
|
1953
|
+
}
|
|
1954
|
+
describeConditionalStyle(label, input) {
|
|
1955
|
+
const parts = [`Vou destacar a coluna **${label}**`];
|
|
1956
|
+
const condition = this.conditionPhrase(input['condition'], label);
|
|
1957
|
+
const style = this.styleLabel(this.toRecord(input['style']));
|
|
1958
|
+
const tooltip = this.tooltipLabel(input['tooltip']) ?? this.semanticDescriptionLabel(input['description']);
|
|
1959
|
+
if (!condition && !style && tooltip) {
|
|
1960
|
+
return `Vou adicionar uma dica na coluna **${label}**: **${tooltip}**.`;
|
|
1961
|
+
}
|
|
1962
|
+
if (condition && !style && tooltip) {
|
|
1963
|
+
return `Vou adicionar uma dica na coluna **${label}** quando **${condition}**: **${tooltip}**.`;
|
|
1964
|
+
}
|
|
1965
|
+
if (condition) {
|
|
1966
|
+
parts.push(`quando **${condition}**`);
|
|
1967
|
+
}
|
|
1968
|
+
if (style) {
|
|
1969
|
+
parts.push(`usando ${style}`);
|
|
1970
|
+
}
|
|
1971
|
+
if (tooltip) {
|
|
1972
|
+
parts.push(`com dica **${tooltip}**`);
|
|
1973
|
+
}
|
|
1974
|
+
return `${parts.join(' ')}.`;
|
|
1975
|
+
}
|
|
1976
|
+
variantLabel(value) {
|
|
1977
|
+
const normalized = value.toLowerCase();
|
|
1978
|
+
switch (normalized) {
|
|
1979
|
+
case 'filled':
|
|
1980
|
+
case 'solid':
|
|
1981
|
+
return 'preenchido';
|
|
1982
|
+
case 'outlined':
|
|
1983
|
+
case 'outline':
|
|
1984
|
+
return 'com contorno';
|
|
1985
|
+
case 'soft':
|
|
1986
|
+
return 'suave';
|
|
1987
|
+
default:
|
|
1988
|
+
return value;
|
|
1989
|
+
}
|
|
1990
|
+
}
|
|
1991
|
+
conditionPhrase(condition, currentLabel) {
|
|
1992
|
+
const record = this.toRecord(condition);
|
|
1993
|
+
if (!record)
|
|
1994
|
+
return null;
|
|
1995
|
+
if (this.isTautologyCondition(record))
|
|
1996
|
+
return null;
|
|
1997
|
+
for (const operator of ['===', '==']) {
|
|
1998
|
+
const operands = record[operator];
|
|
1999
|
+
if (!Array.isArray(operands) || operands.length < 2)
|
|
2000
|
+
continue;
|
|
2001
|
+
const booleanLabel = this.booleanEqualityConditionLabel(operands[0], operands[1], currentLabel)
|
|
2002
|
+
?? this.booleanEqualityConditionLabel(operands[1], operands[0], currentLabel);
|
|
2003
|
+
if (booleanLabel)
|
|
2004
|
+
return booleanLabel;
|
|
2005
|
+
const literalLabel = this.literalEqualityConditionLabel(operands[0], operands[1], currentLabel)
|
|
2006
|
+
?? this.literalEqualityConditionLabel(operands[1], operands[0], currentLabel);
|
|
2007
|
+
if (literalLabel)
|
|
2008
|
+
return literalLabel;
|
|
2009
|
+
}
|
|
2010
|
+
return this.conditionLabel(condition);
|
|
2011
|
+
}
|
|
2012
|
+
literalEqualityCondition(condition) {
|
|
2013
|
+
const record = this.toRecord(condition);
|
|
2014
|
+
if (!record)
|
|
2015
|
+
return null;
|
|
2016
|
+
for (const operator of ['===', '==']) {
|
|
2017
|
+
const operands = record[operator];
|
|
2018
|
+
if (!Array.isArray(operands) || operands.length < 2)
|
|
2019
|
+
continue;
|
|
2020
|
+
const direct = this.literalEqualityOperands(operands[0], operands[1])
|
|
2021
|
+
?? this.literalEqualityOperands(operands[1], operands[0]);
|
|
2022
|
+
if (direct)
|
|
2023
|
+
return direct;
|
|
2024
|
+
}
|
|
2025
|
+
return null;
|
|
2026
|
+
}
|
|
2027
|
+
literalEqualityOperands(fieldOperand, valueOperand) {
|
|
2028
|
+
const field = this.stringValue(this.toRecord(fieldOperand)?.['var']);
|
|
2029
|
+
if (!field)
|
|
2030
|
+
return null;
|
|
2031
|
+
if (typeof valueOperand !== 'string' && typeof valueOperand !== 'number')
|
|
2032
|
+
return null;
|
|
2033
|
+
return { field, valueLabel: String(valueOperand) };
|
|
2034
|
+
}
|
|
2035
|
+
literalEqualityConditionLabel(fieldOperand, valueOperand, currentLabel) {
|
|
2036
|
+
const equality = this.literalEqualityOperands(fieldOperand, valueOperand);
|
|
2037
|
+
if (!equality)
|
|
2038
|
+
return null;
|
|
2039
|
+
const fieldLabel = this.humanizeField(equality.field);
|
|
2040
|
+
const valueLabel = this.enumValueLabel(equality.valueLabel);
|
|
2041
|
+
const normalizedCurrent = currentLabel ? this.normalizeComparableLabel(currentLabel) : '';
|
|
2042
|
+
const normalizedField = this.normalizeComparableLabel(fieldLabel);
|
|
2043
|
+
if (normalizedCurrent && normalizedCurrent === normalizedField) {
|
|
2044
|
+
return `for ${valueLabel}`;
|
|
2045
|
+
}
|
|
2046
|
+
return `${fieldLabel} for ${valueLabel}`;
|
|
2047
|
+
}
|
|
2048
|
+
isTautologyCondition(condition) {
|
|
2049
|
+
for (const operator of ['===', '==']) {
|
|
2050
|
+
const operands = condition[operator];
|
|
2051
|
+
if (!Array.isArray(operands) || operands.length < 2)
|
|
2052
|
+
continue;
|
|
2053
|
+
if (this.samePrimitiveOperand(operands[0], operands[1]))
|
|
2054
|
+
return true;
|
|
2055
|
+
}
|
|
2056
|
+
return false;
|
|
2057
|
+
}
|
|
2058
|
+
samePrimitiveOperand(left, right) {
|
|
2059
|
+
const leftType = typeof left;
|
|
2060
|
+
return (leftType === 'string' || leftType === 'number' || leftType === 'boolean')
|
|
2061
|
+
&& leftType === typeof right
|
|
2062
|
+
&& left === right;
|
|
2063
|
+
}
|
|
2064
|
+
booleanEqualityConditionLabel(fieldOperand, valueOperand, currentLabel) {
|
|
2065
|
+
if (typeof valueOperand !== 'boolean')
|
|
2066
|
+
return null;
|
|
2067
|
+
const field = this.stringValue(this.toRecord(fieldOperand)?.['var']);
|
|
2068
|
+
if (!field)
|
|
2069
|
+
return null;
|
|
2070
|
+
const fieldLabel = this.humanizeField(field);
|
|
2071
|
+
const normalizedCurrent = currentLabel ? this.normalizeComparableLabel(currentLabel) : '';
|
|
2072
|
+
const normalizedField = this.normalizeComparableLabel(fieldLabel);
|
|
2073
|
+
const state = valueOperand ? 'ativo' : 'inativo';
|
|
2074
|
+
if (normalizedCurrent && normalizedCurrent === normalizedField) {
|
|
2075
|
+
return `estiver ${state}`;
|
|
2076
|
+
}
|
|
2077
|
+
return `${fieldLabel} estiver ${state}`;
|
|
2078
|
+
}
|
|
2079
|
+
normalizeComparableLabel(value) {
|
|
2080
|
+
return value
|
|
2081
|
+
.normalize('NFD')
|
|
2082
|
+
.replace(/[\u0300-\u036f]/g, '')
|
|
2083
|
+
.toLowerCase()
|
|
2084
|
+
.replace(/[^a-z0-9]+/g, '');
|
|
2085
|
+
}
|
|
2086
|
+
conditionLabel(condition) {
|
|
2087
|
+
const record = this.toRecord(condition);
|
|
2088
|
+
if (!record)
|
|
2089
|
+
return null;
|
|
2090
|
+
const startsWith = record['startsWith'];
|
|
2091
|
+
if (Array.isArray(startsWith) && startsWith.length >= 2) {
|
|
2092
|
+
const field = this.jsonLogicOperandLabel(startsWith[0]);
|
|
2093
|
+
const value = this.jsonLogicOperandLabel(startsWith[1]);
|
|
2094
|
+
if (field && value) {
|
|
2095
|
+
return `${field} comeca com ${value}`;
|
|
2096
|
+
}
|
|
2097
|
+
}
|
|
2098
|
+
for (const operator of ['>=', '<=', '>', '<', '===', '==']) {
|
|
2099
|
+
const operands = record[operator];
|
|
2100
|
+
if (!Array.isArray(operands) || operands.length < 2)
|
|
2101
|
+
continue;
|
|
2102
|
+
const field = this.jsonLogicOperandLabel(operands[0]);
|
|
2103
|
+
const value = this.jsonLogicOperandLabel(operands[1]);
|
|
2104
|
+
if (field && value) {
|
|
2105
|
+
return `${field} ${operator} ${value}`;
|
|
2106
|
+
}
|
|
2107
|
+
}
|
|
2108
|
+
return null;
|
|
2109
|
+
}
|
|
2110
|
+
jsonLogicOperandLabel(operand) {
|
|
2111
|
+
const record = this.toRecord(operand);
|
|
2112
|
+
const field = this.stringValue(record?.['var']);
|
|
2113
|
+
if (field)
|
|
2114
|
+
return this.humanizeField(field);
|
|
2115
|
+
if (typeof operand === 'string')
|
|
2116
|
+
return operand;
|
|
2117
|
+
if (typeof operand === 'number' || typeof operand === 'boolean')
|
|
2118
|
+
return String(operand);
|
|
2119
|
+
return null;
|
|
2120
|
+
}
|
|
2121
|
+
styleLabel(style) {
|
|
2122
|
+
if (!style)
|
|
2123
|
+
return null;
|
|
2124
|
+
const labels = [];
|
|
2125
|
+
const background = this.stringValue(style['backgroundColor'] ?? style['background-color']);
|
|
2126
|
+
const color = this.stringValue(style['color']);
|
|
2127
|
+
const fontWeight = this.stringValue(style['fontWeight'] ?? style['font-weight']);
|
|
2128
|
+
const opacity = this.stringValue(style['opacity']);
|
|
2129
|
+
const border = this.stringValue(style['border']
|
|
2130
|
+
?? style['borderLeft']
|
|
2131
|
+
?? style['border-left']
|
|
2132
|
+
?? style['borderRight']
|
|
2133
|
+
?? style['border-right']
|
|
2134
|
+
?? style['borderTop']
|
|
2135
|
+
?? style['border-top']
|
|
2136
|
+
?? style['borderBottom']
|
|
2137
|
+
?? style['border-bottom']);
|
|
2138
|
+
if (background)
|
|
2139
|
+
labels.push(`fundo ${this.colorLabel(background)}`);
|
|
2140
|
+
if (color)
|
|
2141
|
+
labels.push(`texto ${this.colorLabel(color)}`);
|
|
2142
|
+
if (fontWeight)
|
|
2143
|
+
labels.push('texto em destaque');
|
|
2144
|
+
if (opacity)
|
|
2145
|
+
labels.push(`opacidade ${opacity}`);
|
|
2146
|
+
if (border)
|
|
2147
|
+
labels.push('borda discreta');
|
|
2148
|
+
return labels.length ? labels.join(', ') : null;
|
|
2149
|
+
}
|
|
2150
|
+
describeColumnStyle(label, value, target) {
|
|
2151
|
+
const details = this.styleLabel(this.styleRecord(value));
|
|
2152
|
+
const suffix = details ? ` com ${details}` : '';
|
|
2153
|
+
if (target === 'cabecalho') {
|
|
2154
|
+
return `Vou ajustar o estilo do cabecalho da coluna **${label}**${suffix}.`;
|
|
2155
|
+
}
|
|
2156
|
+
return `Vou ajustar o estilo visual da coluna **${label}**${suffix}.`;
|
|
2157
|
+
}
|
|
2158
|
+
styleRecord(value) {
|
|
2159
|
+
const record = this.toRecord(value);
|
|
2160
|
+
if (record)
|
|
2161
|
+
return record;
|
|
2162
|
+
const style = this.stringValue(value);
|
|
2163
|
+
if (!style)
|
|
2164
|
+
return null;
|
|
2165
|
+
const parsed = {};
|
|
2166
|
+
for (const declaration of style.split(';')) {
|
|
2167
|
+
const separatorIndex = declaration.indexOf(':');
|
|
2168
|
+
if (separatorIndex <= 0)
|
|
2169
|
+
continue;
|
|
2170
|
+
const property = declaration.slice(0, separatorIndex).trim();
|
|
2171
|
+
const cssValue = declaration.slice(separatorIndex + 1).trim();
|
|
2172
|
+
if (property && cssValue)
|
|
2173
|
+
parsed[property] = cssValue;
|
|
2174
|
+
}
|
|
2175
|
+
return Object.keys(parsed).length ? parsed : null;
|
|
2176
|
+
}
|
|
2177
|
+
colorLabel(value) {
|
|
2178
|
+
const normalized = value.toLowerCase();
|
|
2179
|
+
if (normalized === 'warn' || normalized === 'warning') {
|
|
2180
|
+
return 'de alerta';
|
|
2181
|
+
}
|
|
2182
|
+
if (normalized === 'accent') {
|
|
2183
|
+
return 'de destaque';
|
|
2184
|
+
}
|
|
2185
|
+
if (normalized === 'info') {
|
|
2186
|
+
return 'informativa';
|
|
2187
|
+
}
|
|
2188
|
+
if (normalized === 'basic' || normalized === 'neutral' || normalized === 'default') {
|
|
2189
|
+
return 'neutra';
|
|
2190
|
+
}
|
|
2191
|
+
if (normalized === 'success' || normalized.includes('46, 125, 50') || normalized.includes('27, 94, 32') || normalized.includes('green')
|
|
2192
|
+
|| normalized.includes('#e8f5e9') || normalized.includes('#1b5e20') || normalized.includes('#a5d6a7')
|
|
2193
|
+
|| normalized.includes('#2e7d32')) {
|
|
2194
|
+
return 'verde suave';
|
|
2195
|
+
}
|
|
2196
|
+
if (normalized.includes('255, 152, 0') || normalized.includes('255, 243, 224') || normalized.includes('138, 75, 0') || normalized.includes('orange')
|
|
2197
|
+
|| normalized.includes('#fff3e0') || normalized.includes('#ff9800') || normalized.includes('#ffa500') || normalized.includes('#8a4b00')) {
|
|
2198
|
+
return 'laranja suave';
|
|
2199
|
+
}
|
|
2200
|
+
if (normalized.includes('244, 67, 54') || normalized.includes('183, 28, 28') || normalized.includes('red')
|
|
2201
|
+
|| normalized.includes('#ffebee') || normalized.includes('#b71c1c')) {
|
|
2202
|
+
return 'vermelho suave';
|
|
2203
|
+
}
|
|
2204
|
+
if (normalized === 'primary' || normalized.includes('blue') || normalized.includes('#1976d2')
|
|
2205
|
+
|| normalized.includes('#2196f3')) {
|
|
2206
|
+
return 'azul';
|
|
2207
|
+
}
|
|
2208
|
+
return value;
|
|
2209
|
+
}
|
|
2210
|
+
enumValueLabel(value) {
|
|
2211
|
+
const trimmed = value.trim();
|
|
2212
|
+
if (!trimmed)
|
|
2213
|
+
return value;
|
|
2214
|
+
return trimmed
|
|
2215
|
+
.toLowerCase()
|
|
2216
|
+
.split(/[_\s-]+/u)
|
|
2217
|
+
.filter((part) => part.length > 0)
|
|
2218
|
+
.map((part) => part.charAt(0).toUpperCase() + part.slice(1))
|
|
2219
|
+
.join(' ');
|
|
2220
|
+
}
|
|
2221
|
+
tooltipLabel(value) {
|
|
2222
|
+
const tooltip = this.toRecord(value);
|
|
2223
|
+
const text = this.stringValue(tooltip?.['text']) || this.stringValue(value);
|
|
2224
|
+
return this.cleanReviewTooltipText(text);
|
|
2225
|
+
}
|
|
2226
|
+
semanticDescriptionLabel(value) {
|
|
2227
|
+
const text = this.cleanReviewTooltipText(this.stringValue(value));
|
|
2228
|
+
if (!text)
|
|
2229
|
+
return null;
|
|
2230
|
+
if (/^(adicionar|ajustar|alterar|destacar|aplicar|criar)\b/i.test(text))
|
|
2231
|
+
return null;
|
|
2232
|
+
return text;
|
|
2233
|
+
}
|
|
2234
|
+
cleanReviewTooltipText(value) {
|
|
2235
|
+
const cleaned = value
|
|
2236
|
+
.trim()
|
|
2237
|
+
.replace(/\bcondicao\b/gi, 'condição')
|
|
2238
|
+
.replace(/^adicionar\s+(?:tooltip|dica)\s*(?:dizendo|com\s+texto)?\s*/i, '')
|
|
2239
|
+
.replace(/^adiciona\s+/i, '')
|
|
2240
|
+
.replace(/^aplica(?:r)?\s+/i, '')
|
|
2241
|
+
.replace(/^com\s+tooltip\s*/i, '')
|
|
2242
|
+
.replace(/^tooltip\s*/i, '')
|
|
2243
|
+
.trim();
|
|
2244
|
+
return cleaned || null;
|
|
2245
|
+
}
|
|
2246
|
+
toChatMessages(messages, prompt) {
|
|
2247
|
+
const supported = (messages ?? [])
|
|
2248
|
+
.filter((message) => message.role === 'user' || message.role === 'assistant' || message.role === 'system')
|
|
2249
|
+
.map((message) => ({
|
|
2250
|
+
role: message.role,
|
|
2251
|
+
content: message.text,
|
|
2252
|
+
}))
|
|
2253
|
+
.filter((message) => message.content.trim().length > 0);
|
|
2254
|
+
return supported.length ? supported : [{ role: 'user', content: prompt }];
|
|
2255
|
+
}
|
|
2256
|
+
withCapabilitySystemMessages(messages, contextHints) {
|
|
2257
|
+
const policy = this.buildFilterExpressionSystemPolicy(contextHints);
|
|
2258
|
+
return policy ? [policy, ...messages] : messages;
|
|
2259
|
+
}
|
|
2260
|
+
buildFilterExpressionSystemPolicy(contextHints) {
|
|
2261
|
+
const authoringContract = this.toRecord(contextHints?.['authoringContract']);
|
|
2262
|
+
const runtimeOperations = this.toRecord(authoringContract?.['runtimeOperations']);
|
|
2263
|
+
const filterExpression = this.toRecord(runtimeOperations?.['filterExpression']);
|
|
2264
|
+
if (filterExpression?.['supported'] === true) {
|
|
2265
|
+
return null;
|
|
2266
|
+
}
|
|
2267
|
+
const resourceCapabilities = this.toRecord(this.toRecord(authoringContract?.['consultativeContext'])?.['resourceCapabilities']);
|
|
2268
|
+
const canonicalOperations = this.toRecord(resourceCapabilities?.['canonicalOperations']);
|
|
2269
|
+
if (canonicalOperations?.['filter'] !== true && filterExpression?.['supported'] !== false) {
|
|
2270
|
+
return null;
|
|
2271
|
+
}
|
|
2272
|
+
return {
|
|
2273
|
+
role: 'system',
|
|
2274
|
+
content: [
|
|
2275
|
+
'Praxis table filter capability policy:',
|
|
2276
|
+
'- The current table runtime can materialize table.filter.apply only as simple conjunction criteria over declared filter fields.',
|
|
2277
|
+
'- Resource capability filter=true means the flat /filter DTO is available; it does not mean cross-field OR or nested boolean filter expressions are supported.',
|
|
2278
|
+
'- runtimeOperations.filterExpression.supported is not true for this request.',
|
|
2279
|
+
'- If the user asks for alternatives such as A OR B, cross-field OR, anyOf, oneOf, allOf, or nested boolean groups, do not say it can be applied and do not offer to apply it as a runtime filter.',
|
|
2280
|
+
'- Instead, explain that this resource/runtime contract cannot materialize that compound filter yet and ask the user to choose one simple field/value filter or another supported action such as export of the current selection.',
|
|
2281
|
+
].join('\n'),
|
|
2282
|
+
};
|
|
2283
|
+
}
|
|
2284
|
+
resolveFilterExpressionSupported(contextHints) {
|
|
2285
|
+
const authoringContract = this.toRecord(contextHints?.['authoringContract']);
|
|
2286
|
+
const runtimeOperations = this.toRecord(authoringContract?.['runtimeOperations']);
|
|
2287
|
+
const filterExpression = this.toRecord(runtimeOperations?.['filterExpression']);
|
|
2288
|
+
if (filterExpression && typeof filterExpression['supported'] === 'boolean') {
|
|
2289
|
+
return filterExpression['supported'];
|
|
2290
|
+
}
|
|
2291
|
+
const resourceCapabilities = this.toRecord(this.toRecord(authoringContract?.['consultativeContext'])?.['resourceCapabilities']);
|
|
2292
|
+
if (resourceCapabilities && typeof resourceCapabilities['filterExpressionSupported'] === 'boolean') {
|
|
2293
|
+
return resourceCapabilities['filterExpressionSupported'];
|
|
2294
|
+
}
|
|
2295
|
+
return null;
|
|
2296
|
+
}
|
|
2297
|
+
toClarificationQuestions(response, request) {
|
|
2298
|
+
const labels = response.questions?.length
|
|
2299
|
+
? response.questions
|
|
2300
|
+
: response.message
|
|
2301
|
+
? [response.message]
|
|
2302
|
+
: ['Qual ajuste você quer aplicar na tabela?'];
|
|
2303
|
+
const options = this.toQuickReplies(response, request).map((reply) => ({
|
|
2304
|
+
id: reply.id,
|
|
2305
|
+
label: reply.label,
|
|
2306
|
+
value: typeof reply.value === 'string' && reply.value.trim() ? reply.value.trim() : reply.prompt,
|
|
2307
|
+
displayPrompt: reply.label,
|
|
2308
|
+
description: reply.description ?? undefined,
|
|
2309
|
+
contextHints: reply.contextHints ? { ...reply.contextHints } : undefined,
|
|
2310
|
+
}));
|
|
2311
|
+
return labels.map((label, index) => ({
|
|
2312
|
+
id: `table-clarification-${index + 1}`,
|
|
2313
|
+
type: options.length ? 'single-choice' : 'text',
|
|
2314
|
+
label,
|
|
2315
|
+
allowCustom: true,
|
|
2316
|
+
options,
|
|
2317
|
+
}));
|
|
2318
|
+
}
|
|
2319
|
+
toQuickReplies(response, request) {
|
|
2320
|
+
const payloads = response.optionPayloads ?? [];
|
|
2321
|
+
if (payloads.length) {
|
|
2322
|
+
return payloads
|
|
2323
|
+
.map((option, index) => {
|
|
2324
|
+
const rawLabel = option.label?.trim() || option.value?.trim() || `Opcao ${index + 1}`;
|
|
2325
|
+
const label = this.humanizeClarificationOptionLabel(rawLabel);
|
|
2326
|
+
const canonicalValue = option.value?.trim() || option.example?.trim() || label;
|
|
2327
|
+
return {
|
|
2328
|
+
id: `option-${index + 1}`,
|
|
2329
|
+
label,
|
|
2330
|
+
prompt: label,
|
|
2331
|
+
value: canonicalValue,
|
|
2332
|
+
kind: 'clarification-option',
|
|
2333
|
+
description: this.optionDescription(option),
|
|
2334
|
+
icon: this.optionIcon(option),
|
|
2335
|
+
tone: this.optionTone(option),
|
|
2336
|
+
presentation: this.optionPresentation(option) ?? this.defaultGuidedOptionPresentation(option),
|
|
2337
|
+
contextHints: this.optionContextHints(option),
|
|
2338
|
+
};
|
|
2339
|
+
});
|
|
2340
|
+
}
|
|
2341
|
+
if (response.type && response.type !== 'clarification') {
|
|
2342
|
+
return [];
|
|
2343
|
+
}
|
|
2344
|
+
return this.enhanceColumnClarificationOptions(response.options ?? [], response, request)
|
|
2345
|
+
.filter((option) => !!option?.trim())
|
|
2346
|
+
.map((option, index) => ({
|
|
2347
|
+
id: `option-${index + 1}`,
|
|
2348
|
+
label: this.humanizeClarificationOptionLabel(option.trim()),
|
|
2349
|
+
prompt: option.trim(),
|
|
2350
|
+
kind: 'clarification-option',
|
|
2351
|
+
presentation: {
|
|
2352
|
+
kind: 'guided-option',
|
|
2353
|
+
icon: 'check',
|
|
2354
|
+
ctaLabel: 'Usar esta opção',
|
|
2355
|
+
},
|
|
2356
|
+
}));
|
|
2357
|
+
}
|
|
2358
|
+
enhanceColumnClarificationOptions(options, response, request) {
|
|
2359
|
+
const normalizedOptions = new Set(options
|
|
2360
|
+
.map((option) => this.normalizeLabel(option))
|
|
2361
|
+
.filter((option) => option.length > 0));
|
|
2362
|
+
const enhanced = [...options];
|
|
2363
|
+
if (normalizedOptions.size && this.optionsRepresentCurrentColumns(normalizedOptions)) {
|
|
2364
|
+
this.appendMissingSchemaFields(enhanced, normalizedOptions);
|
|
2365
|
+
return enhanced;
|
|
2366
|
+
}
|
|
2367
|
+
if (this.isFieldClarification(response)) {
|
|
2368
|
+
this.appendRankedSchemaFieldCandidates(enhanced, normalizedOptions, request, response);
|
|
2369
|
+
}
|
|
2370
|
+
return enhanced;
|
|
2371
|
+
}
|
|
2372
|
+
appendMissingSchemaFields(enhanced, normalizedOptions) {
|
|
2373
|
+
const schemaFields = this.adapter.getSchemaFields?.() ?? [];
|
|
2374
|
+
for (const field of schemaFields) {
|
|
2375
|
+
const record = this.toRecord(field);
|
|
2376
|
+
const name = this.stringValue(record?.['name']);
|
|
2377
|
+
if (!name)
|
|
2378
|
+
continue;
|
|
2379
|
+
const label = this.stringValue(record?.['label']) || this.humanizeField(name);
|
|
2380
|
+
const canonical = this.normalizeLabel(label);
|
|
2381
|
+
const technical = this.normalizeLabel(name);
|
|
2382
|
+
if (normalizedOptions.has(canonical) || normalizedOptions.has(technical))
|
|
2383
|
+
continue;
|
|
2384
|
+
normalizedOptions.add(canonical);
|
|
2385
|
+
normalizedOptions.add(technical);
|
|
2386
|
+
enhanced.push(label);
|
|
2387
|
+
}
|
|
2388
|
+
}
|
|
2389
|
+
appendRankedSchemaFieldCandidates(enhanced, normalizedOptions, request, response) {
|
|
2390
|
+
const text = this.normalizeLabel([
|
|
2391
|
+
request?.prompt,
|
|
2392
|
+
response?.message,
|
|
2393
|
+
...(response?.questions ?? []),
|
|
2394
|
+
...(response?.options ?? []),
|
|
2395
|
+
].filter(Boolean).join(' '));
|
|
2396
|
+
if (!text)
|
|
2397
|
+
return;
|
|
2398
|
+
const candidates = (this.adapter.getSchemaFields?.() ?? [])
|
|
2399
|
+
.map((field) => this.toRecord(field))
|
|
2400
|
+
.filter((field) => !!field)
|
|
2401
|
+
.map((field) => {
|
|
2402
|
+
const name = this.stringValue(field['name']);
|
|
2403
|
+
const label = this.stringValue(field['label']) || this.humanizeField(name);
|
|
2404
|
+
return { name, label, score: this.schemaFieldCandidateScore(name, label, text) };
|
|
2405
|
+
})
|
|
2406
|
+
.filter((field) => field.name && field.score > 0)
|
|
2407
|
+
.sort((left, right) => right.score - left.score || left.label.localeCompare(right.label))
|
|
2408
|
+
.slice(0, 4);
|
|
2409
|
+
for (const candidate of candidates) {
|
|
2410
|
+
const canonical = this.normalizeLabel(candidate.label);
|
|
2411
|
+
const technical = this.normalizeLabel(candidate.name);
|
|
2412
|
+
if (normalizedOptions.has(canonical) || normalizedOptions.has(technical))
|
|
2413
|
+
continue;
|
|
2414
|
+
normalizedOptions.add(canonical);
|
|
2415
|
+
normalizedOptions.add(technical);
|
|
2416
|
+
enhanced.push(candidate.label);
|
|
2417
|
+
}
|
|
2418
|
+
}
|
|
2419
|
+
isFieldClarification(response) {
|
|
2420
|
+
const text = this.normalizeLabel([
|
|
2421
|
+
response?.message,
|
|
2422
|
+
...(response?.questions ?? []),
|
|
2423
|
+
...(response?.options ?? []),
|
|
2424
|
+
].filter(Boolean).join(' '));
|
|
2425
|
+
return text.includes('campo') || text.includes('coluna') || text.includes('field');
|
|
2426
|
+
}
|
|
2427
|
+
schemaFieldCandidateScore(name, label, normalizedText) {
|
|
2428
|
+
let score = 0;
|
|
2429
|
+
const technical = this.normalizeLabel(name);
|
|
2430
|
+
const canonical = this.normalizeLabel(label);
|
|
2431
|
+
if (technical && normalizedText.includes(technical))
|
|
2432
|
+
score += 3;
|
|
2433
|
+
if (canonical && normalizedText.includes(canonical))
|
|
2434
|
+
score += 4;
|
|
2435
|
+
if (technical && canonical && technical !== canonical && canonical.includes(technical))
|
|
2436
|
+
score += 1;
|
|
2437
|
+
return score;
|
|
2438
|
+
}
|
|
2439
|
+
optionsRepresentCurrentColumns(normalizedOptions) {
|
|
2440
|
+
const columns = this.toArray(this.adapter.getCurrentConfig()?.['columns'])
|
|
2441
|
+
.map((column) => this.toRecord(column))
|
|
2442
|
+
.filter((column) => !!column);
|
|
2443
|
+
if (!columns.length)
|
|
2444
|
+
return false;
|
|
2445
|
+
const columnKeys = new Set();
|
|
2446
|
+
for (const column of columns) {
|
|
2447
|
+
const field = this.stringValue(column['field']);
|
|
2448
|
+
const header = this.stringValue(column['header']);
|
|
2449
|
+
if (field)
|
|
2450
|
+
columnKeys.add(this.normalizeLabel(field));
|
|
2451
|
+
if (header)
|
|
2452
|
+
columnKeys.add(this.normalizeLabel(header));
|
|
2453
|
+
}
|
|
2454
|
+
let matched = 0;
|
|
2455
|
+
for (const option of normalizedOptions) {
|
|
2456
|
+
if (columnKeys.has(option))
|
|
2457
|
+
matched += 1;
|
|
2458
|
+
}
|
|
2459
|
+
return matched > 0 && matched === normalizedOptions.size;
|
|
2460
|
+
}
|
|
2461
|
+
humanizeClarificationOptionLabel(label) {
|
|
2462
|
+
return label
|
|
2463
|
+
.replace(/\s*\((?:column|filter|behavior|toolbar|export|appearance)\.[^)]+\)\s*$/u, '')
|
|
2464
|
+
.replace(/\s*—\s*ocultar\s*$/u, ' — ocultar coluna')
|
|
2465
|
+
.replace(/\s*—\s*remover completamente\s*$/u, ' — remover coluna')
|
|
2466
|
+
.replace(/\s*—\s*exibir\s*$/u, ' — exibir coluna')
|
|
2467
|
+
.trim();
|
|
2468
|
+
}
|
|
2469
|
+
defaultGuidedOptionPresentation(option) {
|
|
2470
|
+
const description = this.optionDescription(option);
|
|
2471
|
+
const icon = this.optionIcon(option) || 'check';
|
|
2472
|
+
const tone = this.optionTone(option);
|
|
2473
|
+
return {
|
|
2474
|
+
kind: 'guided-option',
|
|
2475
|
+
icon,
|
|
2476
|
+
tone,
|
|
2477
|
+
description,
|
|
2478
|
+
ctaLabel: 'Usar esta opção',
|
|
2479
|
+
};
|
|
2480
|
+
}
|
|
2481
|
+
optionContextHints(option) {
|
|
2482
|
+
return this.toRecord(option.contextHints);
|
|
2483
|
+
}
|
|
2484
|
+
optionPresentation(option) {
|
|
2485
|
+
const hints = this.optionContextHints(option);
|
|
2486
|
+
return this.toRecord(hints?.['presentation']);
|
|
2487
|
+
}
|
|
2488
|
+
optionDescription(option) {
|
|
2489
|
+
const presentation = this.optionPresentation(option);
|
|
2490
|
+
const value = presentation?.['description'];
|
|
2491
|
+
return typeof value === 'string' && value.trim() ? value.trim() : null;
|
|
2492
|
+
}
|
|
2493
|
+
optionIcon(option) {
|
|
2494
|
+
const presentation = this.optionPresentation(option);
|
|
2495
|
+
const value = presentation?.['icon'];
|
|
2496
|
+
return typeof value === 'string' && value.trim() ? value.trim() : null;
|
|
2497
|
+
}
|
|
2498
|
+
optionTone(option) {
|
|
2499
|
+
const presentation = this.optionPresentation(option);
|
|
2500
|
+
const value = presentation?.['tone'];
|
|
2501
|
+
return typeof value === 'string' && value.trim() ? value.trim() : null;
|
|
2502
|
+
}
|
|
2503
|
+
booleanConditionValue(condition) {
|
|
2504
|
+
const record = this.toRecord(condition);
|
|
2505
|
+
if (!record)
|
|
2506
|
+
return null;
|
|
2507
|
+
for (const operator of ['==', '===']) {
|
|
2508
|
+
const operands = record[operator];
|
|
2509
|
+
if (!Array.isArray(operands))
|
|
2510
|
+
continue;
|
|
2511
|
+
const literal = operands.find((operand) => typeof operand === 'boolean');
|
|
2512
|
+
if (typeof literal === 'boolean')
|
|
2513
|
+
return literal;
|
|
2514
|
+
}
|
|
2515
|
+
return null;
|
|
2516
|
+
}
|
|
2517
|
+
booleanInput(value) {
|
|
2518
|
+
return typeof value === 'boolean' ? value : null;
|
|
2519
|
+
}
|
|
2520
|
+
arrayOfStrings(value) {
|
|
2521
|
+
return Array.isArray(value)
|
|
2522
|
+
? value.filter((item) => typeof item === 'string' && item.trim().length > 0)
|
|
2523
|
+
: [];
|
|
2524
|
+
}
|
|
2525
|
+
toArray(value) {
|
|
2526
|
+
return Array.isArray(value) ? value : [];
|
|
2527
|
+
}
|
|
2528
|
+
formatLabel(value) {
|
|
2529
|
+
const format = this.stringValue(value);
|
|
2530
|
+
if (!format)
|
|
2531
|
+
return 'o formato solicitado';
|
|
2532
|
+
if (format.startsWith('custom|')) {
|
|
2533
|
+
const parts = format.split('|');
|
|
2534
|
+
if (parts.length >= 3 && parts[1] && parts[2])
|
|
2535
|
+
return `${parts[1]}/${parts[2]}`;
|
|
2536
|
+
return 'formato personalizado';
|
|
2537
|
+
}
|
|
2538
|
+
if (format === '000.000.000-00')
|
|
2539
|
+
return 'CPF brasileiro';
|
|
2540
|
+
if (/^BRL\b/u.test(format))
|
|
2541
|
+
return 'moeda brasileira';
|
|
2542
|
+
switch (format) {
|
|
2543
|
+
case 'active-inactive':
|
|
2544
|
+
return 'Ativo/Inativo';
|
|
2545
|
+
case 'yes-no':
|
|
2546
|
+
return 'Sim/Não';
|
|
2547
|
+
case 'true-false':
|
|
2548
|
+
return 'Verdadeiro/Falso';
|
|
2549
|
+
case 'on-off':
|
|
2550
|
+
case 'onoff':
|
|
2551
|
+
return 'Ligado/Desligado';
|
|
2552
|
+
case 'shortDate':
|
|
2553
|
+
return 'data curta';
|
|
2554
|
+
case 'mediumDate':
|
|
2555
|
+
return 'data com mes abreviado';
|
|
2556
|
+
case 'longDate':
|
|
2557
|
+
return 'data por extenso';
|
|
2558
|
+
case 'fullDate':
|
|
2559
|
+
return 'data completa com dia da semana';
|
|
2560
|
+
case 'MMM/yyyy':
|
|
2561
|
+
return 'mes e ano';
|
|
2562
|
+
case 'dd/MM/yyyy':
|
|
2563
|
+
return 'data no padrao brasileiro';
|
|
2564
|
+
case 'yyyy-MM-dd':
|
|
2565
|
+
return 'data ISO';
|
|
2566
|
+
case 'yyyy-MM-dd HH:mm':
|
|
2567
|
+
return 'data e hora';
|
|
2568
|
+
case 'shortTime':
|
|
2569
|
+
return 'hora curta';
|
|
2570
|
+
case 'short':
|
|
2571
|
+
return 'data e hora curtas';
|
|
2572
|
+
}
|
|
2573
|
+
return format;
|
|
2574
|
+
}
|
|
2575
|
+
rendererLabel(value) {
|
|
2576
|
+
switch (value.trim().toLowerCase()) {
|
|
2577
|
+
case 'chip':
|
|
2578
|
+
return 'chip';
|
|
2579
|
+
case 'avatar':
|
|
2580
|
+
return 'avatar';
|
|
2581
|
+
case 'badge':
|
|
2582
|
+
return 'badge';
|
|
2583
|
+
default:
|
|
2584
|
+
return 'indicador visual';
|
|
2585
|
+
}
|
|
2586
|
+
}
|
|
2587
|
+
rendererPluralLabel(value) {
|
|
2588
|
+
switch (value.trim().toLowerCase()) {
|
|
2589
|
+
case 'chip':
|
|
2590
|
+
return 'chips';
|
|
2591
|
+
case 'badge':
|
|
2592
|
+
return 'badges';
|
|
2593
|
+
case 'avatar':
|
|
2594
|
+
return 'avatares';
|
|
2595
|
+
default:
|
|
2596
|
+
return `${this.rendererLabel(value)}s`;
|
|
2597
|
+
}
|
|
2598
|
+
}
|
|
2599
|
+
humanizeField(field) {
|
|
2600
|
+
if (field.trim().toLowerCase() === 'cpf')
|
|
2601
|
+
return 'CPF';
|
|
2602
|
+
return field
|
|
2603
|
+
.replace(/[_-]+/gu, ' ')
|
|
2604
|
+
.replace(/([a-z])([A-Z])/g, '$1 $2')
|
|
2605
|
+
.trim()
|
|
2606
|
+
.replace(/^./u, (char) => char.toLocaleUpperCase('pt-BR'));
|
|
2607
|
+
}
|
|
2608
|
+
humanizeFilterField(field) {
|
|
2609
|
+
const normalized = field.trim();
|
|
2610
|
+
if (!normalized)
|
|
2611
|
+
return 'Campo';
|
|
2612
|
+
const catalogLabel = this.filterFieldLabels.get(normalized);
|
|
2613
|
+
if (catalogLabel)
|
|
2614
|
+
return catalogLabel;
|
|
2615
|
+
if (normalized.toLowerCase() === 'cpf')
|
|
2616
|
+
return 'CPF';
|
|
2617
|
+
if (/between$/iu.test(normalized)) {
|
|
2618
|
+
const base = normalized.replace(/Between$/iu, '');
|
|
2619
|
+
return `Faixa de ${this.humanizeField(base).toLocaleLowerCase('pt-BR')}`;
|
|
2620
|
+
}
|
|
2621
|
+
if (/range$/iu.test(normalized)) {
|
|
2622
|
+
const base = normalized.replace(/Range$/iu, '');
|
|
2623
|
+
return `Periodo de ${this.humanizeFilterBase(base).toLocaleLowerCase('pt-BR')}`;
|
|
2624
|
+
}
|
|
2625
|
+
if (/lastDays$/iu.test(normalized)) {
|
|
2626
|
+
const base = normalized.replace(/LastDays$/iu, '');
|
|
2627
|
+
if (this.normalizeLabel(base) === 'data admissao') {
|
|
2628
|
+
return 'Admissões recentes';
|
|
2629
|
+
}
|
|
2630
|
+
return `${this.humanizeFilterBase(base)} recentes`;
|
|
2631
|
+
}
|
|
2632
|
+
if (/IdsIn$/iu.test(normalized)) {
|
|
2633
|
+
return this.pluralizeFilterLabel(this.humanizeField(normalized.replace(/IdsIn$/iu, '')));
|
|
2634
|
+
}
|
|
2635
|
+
return this.humanizeField(normalized);
|
|
2636
|
+
}
|
|
2637
|
+
pluralizeFilterLabel(label) {
|
|
2638
|
+
const normalized = label.trim();
|
|
2639
|
+
if (!normalized)
|
|
2640
|
+
return label;
|
|
2641
|
+
if (/s$/iu.test(normalized))
|
|
2642
|
+
return normalized;
|
|
2643
|
+
if (/ão$/iu.test(normalized))
|
|
2644
|
+
return normalized.replace(/ão$/iu, 'ões');
|
|
2645
|
+
if (/m$/iu.test(normalized))
|
|
2646
|
+
return `${normalized.slice(0, -1)}ns`;
|
|
2647
|
+
if (/l$/iu.test(normalized))
|
|
2648
|
+
return `${normalized.slice(0, -1)}is`;
|
|
2649
|
+
if (/[aeo]$/iu.test(normalized))
|
|
2650
|
+
return `${normalized}s`;
|
|
2651
|
+
return normalized;
|
|
2652
|
+
}
|
|
2653
|
+
extractFilterFieldLabels(entries) {
|
|
2654
|
+
const labels = new Map();
|
|
2655
|
+
for (const field of entries) {
|
|
2656
|
+
labels.set(field.name, field.label);
|
|
2657
|
+
}
|
|
2658
|
+
return labels;
|
|
2659
|
+
}
|
|
2660
|
+
extractFilterFieldCatalogEntries(contextHints) {
|
|
2661
|
+
return this.filterFieldCatalogFields(contextHints)
|
|
2662
|
+
.map((field) => {
|
|
2663
|
+
const record = this.toRecord(field);
|
|
2664
|
+
const name = this.stringValue(record?.['name']);
|
|
2665
|
+
if (!name)
|
|
2666
|
+
return null;
|
|
2667
|
+
const label = this.stringValue(record?.['label']) || this.humanizeFilterField(name);
|
|
2668
|
+
const aliases = Array.isArray(record?.['aliases'])
|
|
2669
|
+
? record['aliases'].map((alias) => this.stringValue(alias)).filter((alias) => !!alias)
|
|
2670
|
+
: [];
|
|
2671
|
+
const relatedColumnFields = Array.isArray(record?.['relatedColumnFields'])
|
|
2672
|
+
? record['relatedColumnFields'].map((field) => this.stringValue(field)).filter((field) => !!field)
|
|
2673
|
+
: [];
|
|
2674
|
+
const relatedColumnLabels = Array.isArray(record?.['relatedColumnLabels'])
|
|
2675
|
+
? record['relatedColumnLabels'].map((label) => this.stringValue(label)).filter((label) => !!label)
|
|
2676
|
+
: [];
|
|
2677
|
+
const controlType = this.stringValue(record?.['controlType']) || undefined;
|
|
2678
|
+
const type = this.stringValue(record?.['type']) || undefined;
|
|
2679
|
+
const criterionKind = this.stringValue(record?.['criterionKind']) || undefined;
|
|
2680
|
+
const criterionValueShape = this.stringValue(record?.['criterionValueShape']) || undefined;
|
|
2681
|
+
return {
|
|
2682
|
+
name,
|
|
2683
|
+
label,
|
|
2684
|
+
aliases,
|
|
2685
|
+
relatedColumnFields,
|
|
2686
|
+
relatedColumnLabels,
|
|
2687
|
+
...(controlType ? { controlType } : {}),
|
|
2688
|
+
...(type ? { type } : {}),
|
|
2689
|
+
...(criterionKind ? { criterionKind } : {}),
|
|
2690
|
+
...(criterionValueShape ? { criterionValueShape } : {}),
|
|
2691
|
+
};
|
|
2692
|
+
})
|
|
2693
|
+
.filter((entry) => !!entry);
|
|
2694
|
+
}
|
|
2695
|
+
filterFieldCatalogFields(contextHints) {
|
|
2696
|
+
const authoringContract = this.toRecord(contextHints?.['authoringContract']);
|
|
2697
|
+
const componentEditPlan = this.toRecord(authoringContract?.['componentEditPlan']);
|
|
2698
|
+
const nestedCatalog = this.toRecord(componentEditPlan?.['filterFieldCatalog']);
|
|
2699
|
+
const nestedFields = nestedCatalog?.['fields'];
|
|
2700
|
+
if (Array.isArray(nestedFields))
|
|
2701
|
+
return nestedFields;
|
|
2702
|
+
const directCatalog = this.toRecord(authoringContract?.['filterFieldCatalog'])
|
|
2703
|
+
?? this.toRecord(contextHints?.['filterFieldCatalog']);
|
|
2704
|
+
const directFields = directCatalog?.['fields'];
|
|
2705
|
+
return Array.isArray(directFields) ? directFields : [];
|
|
2706
|
+
}
|
|
2707
|
+
extractSelectedRecordsCount(contextHints) {
|
|
2708
|
+
const authoringContract = this.toRecord(contextHints?.['authoringContract']);
|
|
2709
|
+
const consultativeContext = this.toRecord(authoringContract?.['consultativeContext']);
|
|
2710
|
+
const selectedRecordsContext = this.toRecord(consultativeContext?.['selectedRecordsContext'])
|
|
2711
|
+
?? this.toRecord(contextHints?.['selectedRecordsContext']);
|
|
2712
|
+
const selectedCount = selectedRecordsContext?.['selectedCount'];
|
|
2713
|
+
return typeof selectedCount === 'number' && Number.isFinite(selectedCount)
|
|
2714
|
+
? Math.max(0, selectedCount)
|
|
2715
|
+
: 0;
|
|
2716
|
+
}
|
|
2717
|
+
extractSelectionDerivedFilterCandidateFields(contextHints) {
|
|
2718
|
+
const authoringContract = this.toRecord(contextHints?.['authoringContract']);
|
|
2719
|
+
const consultativeContext = this.toRecord(authoringContract?.['consultativeContext']);
|
|
2720
|
+
const selectedRecordsContext = this.toRecord(consultativeContext?.['selectedRecordsContext'])
|
|
2721
|
+
?? this.toRecord(contextHints?.['selectedRecordsContext']);
|
|
2722
|
+
const candidates = Array.isArray(selectedRecordsContext?.['filterCandidates'])
|
|
2723
|
+
? selectedRecordsContext['filterCandidates']
|
|
2724
|
+
: [];
|
|
2725
|
+
return new Set(candidates
|
|
2726
|
+
.map((candidate) => this.stringValue(this.toRecord(candidate)?.['field']))
|
|
2727
|
+
.filter((field) => !!field));
|
|
2728
|
+
}
|
|
2729
|
+
extractSelectedRecordFields(contextHints) {
|
|
2730
|
+
const authoringContract = this.toRecord(contextHints?.['authoringContract']);
|
|
2731
|
+
const consultativeContext = this.toRecord(authoringContract?.['consultativeContext']);
|
|
2732
|
+
const selectedRecordsContext = this.toRecord(consultativeContext?.['selectedRecordsContext'])
|
|
2733
|
+
?? this.toRecord(contextHints?.['selectedRecordsContext']);
|
|
2734
|
+
const fields = Array.isArray(selectedRecordsContext?.['fields'])
|
|
2735
|
+
? selectedRecordsContext['fields'].map((field) => this.stringValue(field)).filter((field) => !!field)
|
|
2736
|
+
: [];
|
|
2737
|
+
const sampleRows = Array.isArray(selectedRecordsContext?.['sampleRows'])
|
|
2738
|
+
? selectedRecordsContext['sampleRows']
|
|
2739
|
+
: [];
|
|
2740
|
+
for (const row of sampleRows) {
|
|
2741
|
+
const record = this.toRecord(row);
|
|
2742
|
+
if (!record)
|
|
2743
|
+
continue;
|
|
2744
|
+
fields.push(...Object.keys(record));
|
|
2745
|
+
}
|
|
2746
|
+
return new Set(fields);
|
|
2747
|
+
}
|
|
2748
|
+
filterEntryHasSelectedRecordSourceField(entry) {
|
|
2749
|
+
const sourceFields = [
|
|
2750
|
+
...entry.relatedColumnFields,
|
|
2751
|
+
...entry.aliases,
|
|
2752
|
+
];
|
|
2753
|
+
return sourceFields.some((field) => this.selectedRecordFieldsForTurn.has(field));
|
|
2754
|
+
}
|
|
2755
|
+
humanizeFilterBase(field) {
|
|
2756
|
+
const label = this.humanizeField(field).replace(/^Data\s+/iu, '');
|
|
2757
|
+
if (this.normalizeLabel(label) === 'admissao')
|
|
2758
|
+
return 'admissão';
|
|
2759
|
+
return label;
|
|
2760
|
+
}
|
|
2761
|
+
normalizeLabel(value) {
|
|
2762
|
+
return value
|
|
2763
|
+
.normalize('NFD')
|
|
2764
|
+
.replace(/\p{Diacritic}/gu, '')
|
|
2765
|
+
.replace(/[_-]+/gu, ' ')
|
|
2766
|
+
.replace(/([a-z])([A-Z])/g, '$1 $2')
|
|
2767
|
+
.trim()
|
|
2768
|
+
.toLocaleLowerCase('pt-BR')
|
|
2769
|
+
.replace(/\s+/gu, ' ');
|
|
2770
|
+
}
|
|
2771
|
+
stringValue(value) {
|
|
2772
|
+
return typeof value === 'string' ? value.trim() : '';
|
|
2773
|
+
}
|
|
2774
|
+
buildCurrentStateDigest(currentState, dataProfile) {
|
|
2775
|
+
const currentColumns = Array.isArray(currentState['columns'])
|
|
2776
|
+
? currentState['columns']
|
|
2777
|
+
.map((column) => this.toRecord(column))
|
|
2778
|
+
.filter((column) => !!column)
|
|
2779
|
+
: [];
|
|
2780
|
+
const columns = currentColumns.length
|
|
2781
|
+
? currentColumns
|
|
2782
|
+
.map((column) => column['field'])
|
|
2783
|
+
.filter((field) => typeof field === 'string' && field.length > 0)
|
|
2784
|
+
: undefined;
|
|
2785
|
+
const columnOrder = currentColumns
|
|
2786
|
+
.map((column, index) => {
|
|
2787
|
+
const field = this.stringValue(column['field']);
|
|
2788
|
+
if (!field)
|
|
2789
|
+
return null;
|
|
2790
|
+
return {
|
|
2791
|
+
field,
|
|
2792
|
+
index,
|
|
2793
|
+
...(this.stringValue(column['header']) ? { header: this.stringValue(column['header']) } : {}),
|
|
2794
|
+
...(typeof column['order'] === 'number' ? { order: column['order'] } : {}),
|
|
2795
|
+
};
|
|
2796
|
+
})
|
|
2797
|
+
.filter((column) => !!column);
|
|
2798
|
+
const rowCount = typeof dataProfile?.['rowCount'] === 'number' ? dataProfile['rowCount'] : undefined;
|
|
2799
|
+
return {
|
|
2800
|
+
...(columns?.length ? { columns } : {}),
|
|
2801
|
+
...(columnOrder.length ? { columnOrder } : {}),
|
|
2802
|
+
...(rowCount !== undefined ? { rowCount } : {}),
|
|
2803
|
+
};
|
|
2804
|
+
}
|
|
2805
|
+
optionalJsonObject(value) {
|
|
2806
|
+
if (value === undefined || value === null) {
|
|
2807
|
+
return undefined;
|
|
2808
|
+
}
|
|
2809
|
+
const object = this.toAiJsonObject(value);
|
|
2810
|
+
return Object.keys(object).length ? object : undefined;
|
|
2811
|
+
}
|
|
2812
|
+
async prepareAuthoringContext() {
|
|
2813
|
+
const adapter = this.adapter;
|
|
2814
|
+
await adapter.prepareAuthoringContext?.();
|
|
2815
|
+
}
|
|
2816
|
+
mergeJsonObjects(base, overlay) {
|
|
2817
|
+
if (!base)
|
|
2818
|
+
return overlay;
|
|
2819
|
+
if (!overlay)
|
|
2820
|
+
return base;
|
|
2821
|
+
return {
|
|
2822
|
+
...base,
|
|
2823
|
+
...overlay,
|
|
2824
|
+
};
|
|
2825
|
+
}
|
|
2826
|
+
toAiJsonObject(value) {
|
|
2827
|
+
const record = this.toRecord(value);
|
|
2828
|
+
if (!record) {
|
|
2829
|
+
return {};
|
|
2830
|
+
}
|
|
2831
|
+
try {
|
|
2832
|
+
return JSON.parse(JSON.stringify(record));
|
|
2833
|
+
}
|
|
2834
|
+
catch {
|
|
2835
|
+
return {};
|
|
2836
|
+
}
|
|
2837
|
+
}
|
|
2838
|
+
toRecord(value) {
|
|
2839
|
+
return value && typeof value === 'object' && !Array.isArray(value)
|
|
2840
|
+
? value
|
|
2841
|
+
: null;
|
|
2842
|
+
}
|
|
2843
|
+
}
|
|
2844
|
+
|
|
2845
|
+
export { TableAgenticAuthoringTurnFlow };
|