@praxisui/dynamic-form 8.0.0-beta.2 → 8.0.0-beta.21

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.
@@ -0,0 +1,417 @@
1
+ import { firstValueFrom } from 'rxjs';
2
+
3
+ class DynamicFormAgenticAuthoringTurnFlow {
4
+ adapter;
5
+ aiApi;
6
+ mode = 'agentic-authoring';
7
+ constructor(adapter, aiApi) {
8
+ this.adapter = adapter;
9
+ this.aiApi = aiApi;
10
+ }
11
+ async submit(request) {
12
+ const prompt = (request.prompt ?? '').trim();
13
+ if (!prompt) {
14
+ return {
15
+ state: 'listening',
16
+ phase: 'capture',
17
+ statusText: '',
18
+ };
19
+ }
20
+ const componentId = this.adapter.componentId || request.componentId || 'praxis-dynamic-form';
21
+ const componentType = this.adapter.componentType || request.componentType || 'form';
22
+ const currentState = this.toAiJsonObject(this.adapter.getCurrentConfig());
23
+ const runtimeState = this.optionalJsonObject(this.adapter.getRuntimeState?.());
24
+ const contextHints = this.optionalJsonObject(this.adapter.getAuthoringContext?.());
25
+ if (this.shouldRestrictToInlineHelp(contextHints)) {
26
+ return this.toInlineHelpOnlyResult(prompt, request, contextHints);
27
+ }
28
+ if (this.shouldRouteToGovernedDecision(prompt, contextHints)) {
29
+ return this.toGovernedDecisionHandoff(prompt, request);
30
+ }
31
+ const response = await firstValueFrom(this.aiApi.getPatch({
32
+ componentId,
33
+ componentType,
34
+ userPrompt: prompt,
35
+ sessionId: request.sessionId,
36
+ clientTurnId: request.clientTurnId,
37
+ messages: this.toChatMessages(request.messages, prompt),
38
+ currentState,
39
+ currentStateDigest: this.buildCurrentStateDigest(currentState, runtimeState),
40
+ uiContextRef: {
41
+ componentId,
42
+ componentType,
43
+ },
44
+ ...(runtimeState ? { runtimeState } : {}),
45
+ ...(contextHints ? { contextHints } : {}),
46
+ }));
47
+ return this.toTurnResult(this.compileAdapterResponse(response), request);
48
+ }
49
+ async apply(request) {
50
+ const contextHints = this.optionalJsonObject(this.adapter.getAuthoringContext?.());
51
+ if (this.shouldRestrictToInlineHelp(contextHints)) {
52
+ return {
53
+ state: 'error',
54
+ phase: 'apply',
55
+ assistantMessage: 'Este formulario esta em modo de leitura. Posso explicar ou diagnosticar, mas nao aplicar mudancas locais.',
56
+ errorText: 'Aplicacao bloqueada em view/readonly sem customizacao.',
57
+ canApply: false,
58
+ pendingPatch: null,
59
+ };
60
+ }
61
+ const patch = this.toRecord(request.pendingPatch);
62
+ if (!patch) {
63
+ return {
64
+ state: 'error',
65
+ phase: 'apply',
66
+ assistantMessage: 'Nao ha alteracao de formulario pronta para aplicar.',
67
+ errorText: 'Nao ha alteracao de formulario pronta para aplicar.',
68
+ canApply: false,
69
+ };
70
+ }
71
+ const result = await this.adapter.applyPatch(patch, request.prompt);
72
+ if (!result.success) {
73
+ return {
74
+ state: 'error',
75
+ phase: 'apply',
76
+ assistantMessage: result.error || 'Nao foi possivel aplicar as alteracoes no formulario.',
77
+ errorText: result.error || 'Nao foi possivel aplicar as alteracoes no formulario.',
78
+ canApply: true,
79
+ pendingPatch: patch,
80
+ };
81
+ }
82
+ return {
83
+ state: 'success',
84
+ phase: 'summarize',
85
+ assistantMessage: 'Alteracoes aplicadas no formulario.',
86
+ statusText: 'Alteracoes aplicadas no formulario.',
87
+ canApply: false,
88
+ pendingPatch: null,
89
+ diagnostics: result.warnings?.length ? { warnings: result.warnings } : undefined,
90
+ };
91
+ }
92
+ cancel() {
93
+ return Promise.resolve({
94
+ state: 'listening',
95
+ phase: 'capture',
96
+ assistantMessage: 'Solicitacao cancelada.',
97
+ statusText: '',
98
+ canApply: false,
99
+ pendingPatch: null,
100
+ pendingClarification: null,
101
+ });
102
+ }
103
+ retry(request) {
104
+ const lastPrompt = [...(request.messages ?? [])].reverse()
105
+ .find((message) => message.role === 'user')?.text;
106
+ return this.submit({
107
+ ...request,
108
+ prompt: lastPrompt ?? request.prompt,
109
+ action: { kind: 'retry' },
110
+ });
111
+ }
112
+ toTurnResult(response, request) {
113
+ if (!response) {
114
+ return {
115
+ state: 'error',
116
+ phase: 'capture',
117
+ assistantMessage: 'Resposta vazia da IA.',
118
+ errorText: 'Resposta vazia da IA.',
119
+ };
120
+ }
121
+ if (response.type === 'clarification') {
122
+ const questions = this.toClarificationQuestions(response);
123
+ return {
124
+ state: 'clarification',
125
+ phase: 'clarify',
126
+ sessionId: response.sessionId ?? request.sessionId,
127
+ assistantMessage: response.message || 'Preciso de mais detalhes para continuar.',
128
+ clarificationQuestions: questions,
129
+ quickReplies: this.toQuickReplies(response),
130
+ canApply: false,
131
+ };
132
+ }
133
+ if (response.type === 'info') {
134
+ const message = response.message || response.explanation || 'Informacao gerada.';
135
+ return {
136
+ state: 'success',
137
+ phase: 'summarize',
138
+ sessionId: response.sessionId ?? request.sessionId,
139
+ assistantMessage: message,
140
+ statusText: message,
141
+ canApply: false,
142
+ };
143
+ }
144
+ if (response.type === 'error') {
145
+ const message = response.message || 'Falha ao gerar alteracao de formulario.';
146
+ return {
147
+ state: 'error',
148
+ phase: 'capture',
149
+ sessionId: response.sessionId ?? request.sessionId,
150
+ assistantMessage: message,
151
+ errorText: message,
152
+ diagnostics: response.warnings?.length ? { warnings: response.warnings } : undefined,
153
+ };
154
+ }
155
+ if (response.patch && Object.keys(response.patch).length > 0) {
156
+ const warnings = response.warnings?.filter(Boolean) ?? [];
157
+ const suffix = warnings.length ? ` Avisos: ${warnings.join('; ')}` : '';
158
+ return {
159
+ state: 'review',
160
+ phase: 'review',
161
+ sessionId: response.sessionId ?? request.sessionId,
162
+ assistantMessage: `${response.explanation || 'Proposta de alteracao pronta para revisar.'}${suffix}`,
163
+ statusText: 'Revise a proposta antes de aplicar.',
164
+ canApply: true,
165
+ pendingPatch: response.patch,
166
+ preview: {
167
+ kind: 'dynamic-form-config-patch',
168
+ diff: response.diff ?? [],
169
+ },
170
+ diagnostics: warnings.length ? { warnings } : undefined,
171
+ };
172
+ }
173
+ return {
174
+ state: 'success',
175
+ phase: 'summarize',
176
+ sessionId: response.sessionId ?? request.sessionId,
177
+ assistantMessage: response.message || response.explanation || 'Nenhuma alteracao necessaria.',
178
+ statusText: response.message || response.explanation || 'Nenhuma alteracao necessaria.',
179
+ canApply: false,
180
+ };
181
+ }
182
+ compileAdapterResponse(response) {
183
+ const compiled = this.adapter.compileAiResponse?.(response);
184
+ if (!compiled && response.patch && Object.keys(response.patch).length > 0) {
185
+ return {
186
+ type: 'error',
187
+ message: 'O formulario exige componentEditPlan validado pelo manifesto antes de gerar patch local.',
188
+ warnings: [
189
+ 'free-dynamic-form-patch-rejected',
190
+ 'Use componentEditPlan validado contra PRAXIS_DYNAMIC_FORM_AUTHORING_MANIFEST.',
191
+ ],
192
+ };
193
+ }
194
+ if (!compiled) {
195
+ return response;
196
+ }
197
+ if (compiled.type === 'error') {
198
+ return {
199
+ type: 'error',
200
+ message: compiled.message || 'O componentEditPlan do formulario nao passou na validacao de capacidades.',
201
+ warnings: compiled.warnings,
202
+ };
203
+ }
204
+ const warnings = [
205
+ ...(response.warnings ?? []),
206
+ ...(compiled.warnings ?? []),
207
+ ];
208
+ return {
209
+ ...response,
210
+ ...compiled,
211
+ patch: compiled.patch,
212
+ warnings: warnings.length ? warnings : undefined,
213
+ };
214
+ }
215
+ toChatMessages(messages, prompt) {
216
+ const supported = (messages ?? [])
217
+ .filter((message) => message.role === 'user' || message.role === 'assistant' || message.role === 'system')
218
+ .map((message) => ({
219
+ role: message.role,
220
+ content: message.text,
221
+ }))
222
+ .filter((message) => message.content.trim().length > 0);
223
+ return supported.length ? supported : [{ role: 'user', content: prompt }];
224
+ }
225
+ toClarificationQuestions(response) {
226
+ const labels = response.questions?.length
227
+ ? response.questions
228
+ : response.message
229
+ ? [response.message]
230
+ : ['Qual ajuste voce quer aplicar no formulario?'];
231
+ const options = this.toQuickReplies(response).map((reply) => ({
232
+ id: reply.id,
233
+ label: reply.label,
234
+ value: reply.prompt,
235
+ }));
236
+ return labels.map((label, index) => ({
237
+ id: `dynamic-form-clarification-${index + 1}`,
238
+ type: options.length ? 'single-choice' : 'text',
239
+ label,
240
+ allowCustom: true,
241
+ options,
242
+ }));
243
+ }
244
+ toQuickReplies(response) {
245
+ const payloads = response.optionPayloads ?? [];
246
+ if (payloads.length) {
247
+ return payloads
248
+ .map((option, index) => {
249
+ const label = option.label?.trim() || option.value?.trim() || `Opcao ${index + 1}`;
250
+ const prompt = option.example?.trim() || option.value?.trim() || label;
251
+ return {
252
+ id: `option-${index + 1}`,
253
+ label,
254
+ prompt,
255
+ kind: 'clarification-option',
256
+ };
257
+ });
258
+ }
259
+ return (response.options ?? [])
260
+ .filter((option) => !!option?.trim())
261
+ .map((option, index) => ({
262
+ id: `option-${index + 1}`,
263
+ label: option.trim(),
264
+ prompt: option.trim(),
265
+ kind: 'clarification-option',
266
+ }));
267
+ }
268
+ buildCurrentStateDigest(currentState, runtimeState) {
269
+ const fields = Array.isArray(currentState['fieldMetadata'])
270
+ ? currentState['fieldMetadata']
271
+ .map((field) => this.toRecord(field)?.['name'])
272
+ .filter((field) => typeof field === 'string' && field.length > 0)
273
+ : undefined;
274
+ const sections = Array.isArray(currentState['sections']) ? currentState['sections'].length : undefined;
275
+ return {
276
+ ...(fields?.length ? { columns: fields } : {}),
277
+ ...(typeof runtimeState?.['totalFields'] === 'number'
278
+ ? { rowCount: runtimeState['totalFields'] }
279
+ : sections !== undefined
280
+ ? { rowCount: sections }
281
+ : {}),
282
+ };
283
+ }
284
+ shouldRestrictToInlineHelp(contextHints) {
285
+ const policy = this.toRecord(contextHints?.['runtimeAuthoringPolicy']);
286
+ return policy?.['canApplyLocalPatch'] === false;
287
+ }
288
+ toInlineHelpOnlyResult(prompt, request, contextHints) {
289
+ const policy = this.toRecord(contextHints?.['runtimeAuthoringPolicy']);
290
+ const reason = typeof policy?.['reason'] === 'string'
291
+ ? policy['reason']
292
+ : 'Formulario em modo de leitura ou readonly.';
293
+ return {
294
+ state: 'success',
295
+ phase: 'summarize',
296
+ sessionId: request.sessionId,
297
+ assistantMessage: `Posso ajudar explicando este formulario ou diagnosticando o estado atual, mas nao vou aplicar alteracoes locais aqui. Motivo: ${reason}`,
298
+ statusText: 'Modo somente assistencia.',
299
+ canApply: false,
300
+ pendingPatch: null,
301
+ quickReplies: [
302
+ {
303
+ id: 'explain-current-form',
304
+ label: 'Explicar formulario atual',
305
+ prompt: `Explique este formulario considerando: ${prompt}`,
306
+ kind: 'inline-help',
307
+ icon: 'help',
308
+ },
309
+ ],
310
+ };
311
+ }
312
+ shouldRouteToGovernedDecision(prompt, contextHints) {
313
+ const normalized = prompt.normalize('NFD').replace(/[\u0300-\u036f]/g, '').toLowerCase();
314
+ const recommendedFlow = this.toRecord(contextHints?.['domainCatalog'])?.['recommendedAuthoringFlow'];
315
+ if (recommendedFlow === 'shared_rule_authoring')
316
+ return true;
317
+ return [
318
+ 'regra',
319
+ 'politica',
320
+ 'policy',
321
+ 'compliance',
322
+ 'lgpd',
323
+ 'privacidade',
324
+ 'aprovacao',
325
+ 'aprovar',
326
+ 'publicar',
327
+ 'materializar',
328
+ 'enforcement',
329
+ 'validacao de negocio',
330
+ 'validar negocio',
331
+ 'elegibilidade',
332
+ 'permissao',
333
+ 'acesso',
334
+ ].some((term) => normalized.includes(term));
335
+ }
336
+ toGovernedDecisionHandoff(prompt, request) {
337
+ const message = 'Esse pedido parece alterar uma decisao de negocio compartilhada. O formulario pode contextualizar campo/secao, mas a regra deve seguir pelo fluxo governado de domain-rules antes de qualquer materializacao runtime.';
338
+ return {
339
+ state: 'clarification',
340
+ phase: 'clarify',
341
+ sessionId: request.sessionId,
342
+ assistantMessage: message,
343
+ statusText: 'Handoff governado necessario.',
344
+ canApply: false,
345
+ quickReplies: [
346
+ {
347
+ id: 'shared-rule-handoff',
348
+ label: 'Continuar como regra governada',
349
+ prompt,
350
+ kind: 'shared-rule-handoff',
351
+ description: 'Criar intake de domain-rules em vez de aplicar regra local no formulario.',
352
+ icon: 'rule',
353
+ tone: 'warning',
354
+ contextHints: {
355
+ flowId: 'shared_rule_authoring',
356
+ source: 'praxis-dynamic-form',
357
+ recommendedAction: 'domain-rules/intake',
358
+ },
359
+ },
360
+ ],
361
+ clarificationQuestions: [
362
+ {
363
+ id: 'dynamic-form-governed-rule-confirmation',
364
+ type: 'confirm',
365
+ label: 'Deseja continuar pelo fluxo governado de regras compartilhadas?',
366
+ description: 'Esse caminho permite intake, simulacao, aprovacao/publicacao, materializacao e validacao de enforcement.',
367
+ required: true,
368
+ options: [
369
+ {
370
+ id: 'shared-rule-handoff',
371
+ label: 'Sim, continuar governado',
372
+ value: prompt,
373
+ description: 'Nao aplicar como patch local do formulario.',
374
+ contextHints: {
375
+ flowId: 'shared_rule_authoring',
376
+ source: 'praxis-dynamic-form',
377
+ },
378
+ },
379
+ ],
380
+ },
381
+ ],
382
+ diagnostics: {
383
+ governedDecisionHandoff: {
384
+ flowId: 'shared_rule_authoring',
385
+ sourcePrompt: prompt,
386
+ sourceComponent: 'praxis-dynamic-form',
387
+ },
388
+ },
389
+ };
390
+ }
391
+ optionalJsonObject(value) {
392
+ if (value === undefined || value === null) {
393
+ return undefined;
394
+ }
395
+ const object = this.toAiJsonObject(value);
396
+ return Object.keys(object).length ? object : undefined;
397
+ }
398
+ toAiJsonObject(value) {
399
+ const record = this.toRecord(value);
400
+ if (!record) {
401
+ return {};
402
+ }
403
+ try {
404
+ return JSON.parse(JSON.stringify(record));
405
+ }
406
+ catch {
407
+ return {};
408
+ }
409
+ }
410
+ toRecord(value) {
411
+ return value && typeof value === 'object' && !Array.isArray(value)
412
+ ? value
413
+ : null;
414
+ }
415
+ }
416
+
417
+ export { DynamicFormAgenticAuthoringTurnFlow };