@praxisui/table 8.0.0-beta.4 → 8.0.0-beta.40

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