@praxisui/expansion 8.0.0-beta.3 → 8.0.0-beta.30

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,7 +1,7 @@
1
1
  import * as i1 from '@angular/common';
2
2
  import { CommonModule } from '@angular/common';
3
3
  import * as i0 from '@angular/core';
4
- import { EventEmitter, inject, ChangeDetectorRef, DestroyRef, ViewChildren, ViewChild, Output, Input, ChangeDetectionStrategy, Component, Inject, ENVIRONMENT_INITIALIZER } from '@angular/core';
4
+ import { EventEmitter, inject, ChangeDetectorRef, DestroyRef, effect, ViewChildren, ViewChild, Output, Input, ChangeDetectionStrategy, Component, Inject, ENVIRONMENT_INITIALIZER } from '@angular/core';
5
5
  import { ActivatedRoute } from '@angular/router';
6
6
  import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
7
7
  import * as i2 from '@angular/material/expansion';
@@ -21,9 +21,9 @@ import { MatTooltipModule } from '@angular/material/tooltip';
21
21
  import { DynamicFieldLoaderDirective } from '@praxisui/dynamic-fields';
22
22
  import { deepMerge, ASYNC_CONFIG_STORAGE, DynamicFormService, ComponentKeyService, DynamicWidgetLoaderDirective, PraxisIconDirective, ComponentMetadataRegistry } from '@praxisui/core';
23
23
  import { SettingsPanelService, SETTINGS_PANEL_DATA } from '@praxisui/settings-panel';
24
- import { BehaviorSubject } from 'rxjs';
24
+ import { firstValueFrom, BehaviorSubject, Subscription } from 'rxjs';
25
25
  import { take } from 'rxjs/operators';
26
- import { BaseAiAdapter, PraxisAiAssistantComponent } from '@praxisui/ai';
26
+ import { BaseAiAdapter, shouldRoutePromptToGovernedDecision, AiBackendApiService, PraxisAssistantSessionRegistryService, PraxisAssistantTurnOrchestratorService, createPraxisAssistantViewportLayout, PraxisAiAssistantShellComponent } from '@praxisui/ai';
27
27
 
28
28
  /**
29
29
  * Capabilities catalog for Praxis Expansion (ExpansionMetadata).
@@ -62,6 +62,7 @@ const CAPS = [
62
62
  { path: 'panels[].id', category: 'panels', valueKind: 'string', description: 'Panel id.' },
63
63
  { path: 'panels[].title', category: 'panels', valueKind: 'string', description: 'Panel title.' },
64
64
  { path: 'panels[].description', category: 'panels', valueKind: 'string', description: 'Panel description.' },
65
+ { path: 'panels[].icon', category: 'panels', valueKind: 'string', description: 'Panel header icon rendered through PraxisIconDirective.' },
65
66
  { path: 'panels[].disabled', category: 'panels', valueKind: 'boolean', description: 'Disable panel.' },
66
67
  { path: 'panels[].expanded', category: 'panels', valueKind: 'boolean', description: 'Expanded state.' },
67
68
  { path: 'panels[].hideToggle', category: 'panels', valueKind: 'boolean', description: 'Hide toggle icon for panel.' },
@@ -104,6 +105,8 @@ const EXPANSION_AI_CAPABILITIES = {
104
105
 
105
106
  class ExpansionAiAdapter extends BaseAiAdapter {
106
107
  expansion;
108
+ componentId = 'praxis-expansion';
109
+ componentType = 'expansion';
107
110
  componentName = 'Praxis Expansion';
108
111
  constructor(expansion) {
109
112
  super();
@@ -124,6 +127,43 @@ class ExpansionAiAdapter extends BaseAiAdapter {
124
127
  multi: this.expansion.config?.accordion?.multi ?? false,
125
128
  };
126
129
  }
130
+ getDataProfile() {
131
+ const panels = this.getCurrentConfig().panels || [];
132
+ return {
133
+ expansionId: this.expansion.expansionId,
134
+ panelCount: panels.length,
135
+ contentPanelCount: panels.filter((panel) => Array.isArray(panel.content) && panel.content.length > 0).length,
136
+ widgetPanelCount: panels.filter((panel) => Array.isArray(panel.widgets) && panel.widgets.length > 0).length,
137
+ actionPanelCount: panels.filter((panel) => Array.isArray(panel.actionButtons) && panel.actionButtons.length > 0).length,
138
+ multi: this.expansion.config?.accordion?.multi ?? false,
139
+ displayMode: this.expansion.config?.accordion?.displayMode || 'default',
140
+ };
141
+ }
142
+ getSchemaFields() {
143
+ return (this.getCurrentConfig().panels || []).map((panel, index) => ({
144
+ name: panel.id || panel.title || `panel-${index + 1}`,
145
+ label: panel.title || panel.id || `Painel ${index + 1}`,
146
+ disabled: !!panel.disabled,
147
+ expanded: !!panel.expanded,
148
+ hasContent: Array.isArray(panel.content) && panel.content.length > 0,
149
+ hasWidgets: Array.isArray(panel.widgets) && panel.widgets.length > 0,
150
+ hasActions: Array.isArray(panel.actionButtons) && panel.actionButtons.length > 0,
151
+ }));
152
+ }
153
+ getAuthoringContext() {
154
+ return {
155
+ authoringManifestRef: 'PRAXIS_EXPANSION_AUTHORING_MANIFEST',
156
+ runtimeAuthoringPolicy: {
157
+ mode: 'agentic-authoring',
158
+ enableCustomization: !!this.expansion.enableCustomization,
159
+ canApplyLocalPatch: false,
160
+ reason: 'praxis-expansion exige componentEditPlan validado pelo manifesto antes de aplicar mudancas runtime.',
161
+ },
162
+ domainCatalog: {
163
+ recommendedAuthoringFlow: 'component_authoring',
164
+ },
165
+ };
166
+ }
127
167
  createSnapshot() {
128
168
  return this.getCurrentConfig();
129
169
  }
@@ -184,6 +224,259 @@ class ExpansionAiAdapter extends BaseAiAdapter {
184
224
  }
185
225
  }
186
226
 
227
+ class ExpansionAgenticAuthoringTurnFlow {
228
+ adapter;
229
+ aiApi;
230
+ mode = 'agentic-authoring';
231
+ constructor(adapter, aiApi) {
232
+ this.adapter = adapter;
233
+ this.aiApi = aiApi;
234
+ }
235
+ async submit(request) {
236
+ const prompt = (request.prompt ?? '').trim();
237
+ if (!prompt)
238
+ return { state: 'listening', phase: 'capture', statusText: '' };
239
+ const componentId = this.adapter.componentId || request.componentId || 'praxis-expansion';
240
+ const componentType = this.adapter.componentType || request.componentType || 'expansion';
241
+ const currentState = this.toAiJsonObject(this.adapter.getCurrentConfig());
242
+ const dataProfile = this.optionalJsonObject(this.adapter.getDataProfile?.());
243
+ const runtimeState = this.optionalJsonObject(this.adapter.getRuntimeState?.());
244
+ const schemaFields = this.adapter.getSchemaFields?.()
245
+ ?.map((field) => this.toAiJsonObject(field))
246
+ .filter((field) => Object.keys(field).length > 0);
247
+ const contextHints = this.optionalJsonObject(this.adapter.getAuthoringContext?.());
248
+ if (this.shouldRouteToGovernedDecision(prompt, contextHints)) {
249
+ return this.toGovernedDecisionHandoff(prompt, request);
250
+ }
251
+ const response = await firstValueFrom(this.aiApi.getPatch({
252
+ componentId,
253
+ componentType,
254
+ userPrompt: prompt,
255
+ sessionId: request.sessionId,
256
+ clientTurnId: request.clientTurnId,
257
+ messages: this.toChatMessages(request.messages, prompt),
258
+ currentState,
259
+ currentStateDigest: this.buildCurrentStateDigest(dataProfile),
260
+ uiContextRef: { componentId, componentType },
261
+ ...(dataProfile ? { dataProfile } : {}),
262
+ ...(runtimeState ? { runtimeState } : {}),
263
+ ...(schemaFields?.length ? { schemaFields } : {}),
264
+ ...(contextHints ? { contextHints } : {}),
265
+ }));
266
+ return this.toTurnResult(this.compileAdapterResponse(response), request);
267
+ }
268
+ async apply(_request) {
269
+ return {
270
+ state: 'error',
271
+ phase: 'apply',
272
+ assistantMessage: 'O expansion ainda exige componentEditPlan validado pelo manifesto antes de aplicar mudancas locais.',
273
+ errorText: 'Aplicacao local bloqueada ate existir compilacao manifest-backed para praxis-expansion.',
274
+ canApply: false,
275
+ pendingPatch: null,
276
+ };
277
+ }
278
+ cancel() {
279
+ return Promise.resolve({
280
+ state: 'listening',
281
+ phase: 'capture',
282
+ assistantMessage: 'Solicitacao cancelada.',
283
+ statusText: '',
284
+ canApply: false,
285
+ pendingPatch: null,
286
+ pendingClarification: null,
287
+ });
288
+ }
289
+ retry(request) {
290
+ const lastPrompt = [...(request.messages ?? [])].reverse()
291
+ .find((message) => message.role === 'user')?.text;
292
+ return this.submit({ ...request, prompt: lastPrompt ?? request.prompt, action: { kind: 'retry' } });
293
+ }
294
+ toTurnResult(response, request) {
295
+ if (!response) {
296
+ return { state: 'error', phase: 'capture', assistantMessage: 'Resposta vazia da IA.', errorText: 'Resposta vazia da IA.' };
297
+ }
298
+ if (response.type === 'clarification') {
299
+ return {
300
+ state: 'clarification',
301
+ phase: 'clarify',
302
+ sessionId: response.sessionId ?? request.sessionId,
303
+ assistantMessage: response.message || 'Preciso de mais detalhes para continuar.',
304
+ clarificationQuestions: this.toClarificationQuestions(response),
305
+ quickReplies: this.toQuickReplies(response),
306
+ canApply: false,
307
+ };
308
+ }
309
+ if (response.type === 'info') {
310
+ const message = response.message || response.explanation || 'Informacao gerada.';
311
+ return { state: 'success', phase: 'summarize', sessionId: response.sessionId ?? request.sessionId, assistantMessage: message, statusText: message, canApply: false };
312
+ }
313
+ if (response.type === 'error') {
314
+ const message = response.message || 'Falha ao gerar alteracao de expansion.';
315
+ return {
316
+ state: 'error',
317
+ phase: 'capture',
318
+ sessionId: response.sessionId ?? request.sessionId,
319
+ assistantMessage: message,
320
+ errorText: message,
321
+ diagnostics: response.warnings?.length ? { warnings: response.warnings } : undefined,
322
+ };
323
+ }
324
+ if (response.patch && Object.keys(response.patch).length > 0) {
325
+ return {
326
+ state: 'error',
327
+ phase: 'review',
328
+ sessionId: response.sessionId ?? request.sessionId,
329
+ assistantMessage: 'O expansion rejeitou patch livre. Gere um componentEditPlan validado pelo PRAXIS_EXPANSION_AUTHORING_MANIFEST antes de propor alteracao local.',
330
+ errorText: 'Patch livre de expansion rejeitado.',
331
+ canApply: false,
332
+ pendingPatch: null,
333
+ diagnostics: {
334
+ warnings: [
335
+ 'free-expansion-patch-rejected',
336
+ 'Use componentEditPlan validado contra PRAXIS_EXPANSION_AUTHORING_MANIFEST.',
337
+ ],
338
+ },
339
+ };
340
+ }
341
+ return {
342
+ state: 'success',
343
+ phase: 'summarize',
344
+ sessionId: response.sessionId ?? request.sessionId,
345
+ assistantMessage: response.message || response.explanation || 'Nenhuma alteracao necessaria.',
346
+ statusText: response.message || response.explanation || 'Nenhuma alteracao necessaria.',
347
+ canApply: false,
348
+ };
349
+ }
350
+ compileAdapterResponse(response) {
351
+ const compiled = this.adapter.compileAiResponse?.(response);
352
+ if (!compiled)
353
+ return response;
354
+ if (compiled.type === 'error') {
355
+ return {
356
+ type: 'error',
357
+ message: compiled.message || 'O componentEditPlan do expansion nao passou na validacao de capacidades.',
358
+ warnings: compiled.warnings,
359
+ };
360
+ }
361
+ const warnings = [...(response.warnings ?? []), ...(compiled.warnings ?? [])];
362
+ return { ...response, ...compiled, patch: compiled.patch, warnings: warnings.length ? warnings : undefined };
363
+ }
364
+ toChatMessages(messages, prompt) {
365
+ const supported = (messages ?? [])
366
+ .filter((message) => message.role === 'user' || message.role === 'assistant' || message.role === 'system')
367
+ .map((message) => ({ role: message.role, content: message.text }))
368
+ .filter((message) => message.content.trim().length > 0);
369
+ return supported.length ? supported : [{ role: 'user', content: prompt }];
370
+ }
371
+ toClarificationQuestions(response) {
372
+ const labels = response.questions?.length
373
+ ? response.questions
374
+ : response.message ? [response.message] : ['Qual ajuste voce quer aplicar nos paineis?'];
375
+ const options = this.toQuickReplies(response).map((reply) => ({ id: reply.id, label: reply.label, value: reply.prompt }));
376
+ return labels.map((label, index) => ({
377
+ id: `expansion-clarification-${index + 1}`,
378
+ type: options.length ? 'single-choice' : 'text',
379
+ label,
380
+ allowCustom: true,
381
+ options,
382
+ }));
383
+ }
384
+ toQuickReplies(response) {
385
+ const payloads = response.optionPayloads ?? [];
386
+ if (payloads.length) {
387
+ return payloads.map((option, index) => {
388
+ const label = option.label?.trim() || option.value?.trim() || `Opcao ${index + 1}`;
389
+ const prompt = option.example?.trim() || option.value?.trim() || label;
390
+ return { id: `option-${index + 1}`, label, prompt, kind: 'clarification-option' };
391
+ });
392
+ }
393
+ return (response.options ?? [])
394
+ .filter((option) => !!option?.trim())
395
+ .map((option, index) => ({ id: `option-${index + 1}`, label: option.trim(), prompt: option.trim(), kind: 'clarification-option' }));
396
+ }
397
+ buildCurrentStateDigest(dataProfile) {
398
+ const panelCount = typeof dataProfile?.['panelCount'] === 'number' ? dataProfile['panelCount'] : undefined;
399
+ return panelCount !== undefined ? { rowCount: panelCount } : {};
400
+ }
401
+ shouldRouteToGovernedDecision(prompt, contextHints) {
402
+ return shouldRoutePromptToGovernedDecision(prompt, contextHints);
403
+ }
404
+ toGovernedDecisionHandoff(prompt, request) {
405
+ const message = 'Esse pedido parece alterar uma decisao de negocio compartilhada. O expansion pode localizar o painel e a experiencia afetada, mas a regra deve seguir pelo fluxo governado de domain-rules antes de qualquer materializacao runtime.';
406
+ return {
407
+ state: 'clarification',
408
+ phase: 'clarify',
409
+ sessionId: request.sessionId,
410
+ assistantMessage: message,
411
+ statusText: 'Handoff governado necessario.',
412
+ canApply: false,
413
+ quickReplies: [
414
+ {
415
+ id: 'shared-rule-handoff',
416
+ label: 'Continuar como regra governada',
417
+ prompt,
418
+ kind: 'shared-rule-handoff',
419
+ description: 'Criar intake de domain-rules em vez de aplicar patch local no expansion.',
420
+ icon: 'rule',
421
+ tone: 'warning',
422
+ contextHints: {
423
+ flowId: 'shared_rule_authoring',
424
+ source: 'praxis-expansion',
425
+ recommendedAction: 'domain-rules/intake',
426
+ },
427
+ },
428
+ ],
429
+ clarificationQuestions: [
430
+ {
431
+ id: 'expansion-governed-rule-confirmation',
432
+ type: 'confirm',
433
+ label: 'Deseja continuar pelo fluxo governado de regras compartilhadas?',
434
+ description: 'Esse caminho permite intake, simulacao, aprovacao/publicacao, materializacao e validacao de enforcement.',
435
+ required: true,
436
+ options: [
437
+ {
438
+ id: 'shared-rule-handoff',
439
+ label: 'Sim, continuar governado',
440
+ value: prompt,
441
+ description: 'Nao aplicar como patch local do expansion.',
442
+ contextHints: { flowId: 'shared_rule_authoring', source: 'praxis-expansion' },
443
+ },
444
+ ],
445
+ },
446
+ ],
447
+ diagnostics: {
448
+ governedDecisionHandoff: {
449
+ flowId: 'shared_rule_authoring',
450
+ sourcePrompt: prompt,
451
+ sourceComponent: 'praxis-expansion',
452
+ },
453
+ },
454
+ };
455
+ }
456
+ optionalJsonObject(value) {
457
+ if (value === undefined || value === null)
458
+ return undefined;
459
+ const object = this.toAiJsonObject(value);
460
+ return Object.keys(object).length ? object : undefined;
461
+ }
462
+ toAiJsonObject(value) {
463
+ const record = this.toRecord(value);
464
+ if (!record)
465
+ return {};
466
+ try {
467
+ return JSON.parse(JSON.stringify(record));
468
+ }
469
+ catch {
470
+ return {};
471
+ }
472
+ }
473
+ toRecord(value) {
474
+ return value && typeof value === 'object' && !Array.isArray(value)
475
+ ? value
476
+ : null;
477
+ }
478
+ }
479
+
187
480
  class PraxisExpansion {
188
481
  config;
189
482
  expansionId;
@@ -211,9 +504,35 @@ class PraxisExpansion {
211
504
  catch {
212
505
  return undefined;
213
506
  } })();
507
+ aiApi = inject(AiBackendApiService);
508
+ assistantSessions = inject(PraxisAssistantSessionRegistryService);
509
+ aiTurnOrchestrator = inject(PraxisAssistantTurnOrchestratorService);
510
+ aiAssistantSessionEffect = effect(() => {
511
+ const session = this.assistantSessions.activeSession();
512
+ if (!session || session.id !== this.resolveAiAssistantSessionId())
513
+ return;
514
+ if (!this.aiAssistantOpen) {
515
+ this.openAiAssistantFromSession(session);
516
+ }
517
+ }, ...(ngDevMode ? [{ debugName: "aiAssistantSessionEffect" }] : []));
214
518
  warnedMissingId = false;
215
519
  panelForms = new Map();
216
520
  aiAdapter = new ExpansionAiAdapter(this);
521
+ aiAssistantOpen = false;
522
+ aiAssistantPrompt = '';
523
+ aiAssistantViewState = null;
524
+ aiAssistantLayout = createPraxisAssistantViewportLayout();
525
+ aiAssistantLabels = {
526
+ title: 'Copiloto semantico Praxis',
527
+ subtitle: 'Converse, revise e governe ajustes dos paineis.',
528
+ prompt: 'Mensagem',
529
+ promptPlaceholder: 'Descreva o ajuste que voce precisa nos paineis.',
530
+ emptyConversation: 'Diga o que voce quer alterar no expansion.',
531
+ submit: 'Interpretar pedido',
532
+ apply: 'Aplicar ajuste',
533
+ };
534
+ aiAssistantController = null;
535
+ aiAssistantStateSubscription = null;
217
536
  accordionRef;
218
537
  panels;
219
538
  injectedDefaults = inject(MAT_EXPANSION_PANEL_DEFAULT_OPTIONS, { optional: true });
@@ -237,6 +556,10 @@ class PraxisExpansion {
237
556
  this.persistConfig(this.config);
238
557
  }
239
558
  }
559
+ ngOnDestroy() {
560
+ this.assistantSessions.removeContextSession(this.buildAiAssistantContextSnapshot().identity);
561
+ this.aiAssistantStateSubscription?.unsubscribe();
562
+ }
240
563
  styleCss() {
241
564
  const t = this.config?.appearance?.tokens;
242
565
  const appearance = this.config?.appearance;
@@ -476,6 +799,306 @@ class PraxisExpansion {
476
799
  }
477
800
  this.cdr.markForCheck();
478
801
  }
802
+ openAiAssistant() {
803
+ this.initializeAiAssistantController();
804
+ this.aiAssistantOpen = true;
805
+ this.aiAssistantController?.setContextItems(this.buildAiAssistantContextItems());
806
+ this.syncAiAssistantSession('active');
807
+ this.cdr.markForCheck();
808
+ }
809
+ openAiAssistantFromSession(session) {
810
+ if (session.id !== this.resolveAiAssistantSessionId())
811
+ return;
812
+ this.initializeAiAssistantController();
813
+ this.aiAssistantOpen = true;
814
+ this.aiAssistantController?.setContextItems(this.buildAiAssistantContextItems());
815
+ this.syncAiAssistantSession('active');
816
+ this.cdr.markForCheck();
817
+ }
818
+ closeAiAssistant() {
819
+ this.aiAssistantOpen = false;
820
+ this.syncAiAssistantSession('minimized');
821
+ this.cdr.markForCheck();
822
+ }
823
+ onAiAssistantPromptChange(prompt) {
824
+ this.aiAssistantPrompt = prompt;
825
+ this.syncAiAssistantSession();
826
+ }
827
+ onAiAssistantSubmit(prompt) {
828
+ this.aiAssistantController?.submitPrompt(prompt).subscribe((state) => {
829
+ this.aiAssistantPrompt = '';
830
+ this.aiAssistantViewState = state;
831
+ this.syncAiAssistantSession();
832
+ this.cdr.markForCheck();
833
+ });
834
+ }
835
+ onAiAssistantApply() {
836
+ this.aiAssistantController?.apply().subscribe((state) => {
837
+ this.aiAssistantViewState = state;
838
+ this.syncAiAssistantSession();
839
+ this.cdr.markForCheck();
840
+ });
841
+ }
842
+ onAiAssistantRetry() {
843
+ this.aiAssistantController?.retry().subscribe((state) => {
844
+ this.aiAssistantViewState = state;
845
+ this.syncAiAssistantSession();
846
+ this.cdr.markForCheck();
847
+ });
848
+ }
849
+ onAiAssistantCancel() {
850
+ this.aiAssistantController?.cancel().subscribe((state) => {
851
+ this.aiAssistantPrompt = '';
852
+ this.aiAssistantViewState = state;
853
+ this.syncAiAssistantSession();
854
+ this.cdr.markForCheck();
855
+ });
856
+ }
857
+ onAiAssistantQuickReply(reply) {
858
+ const controller = this.aiAssistantController;
859
+ if (!controller)
860
+ return;
861
+ const state = controller.snapshot();
862
+ const next$ = state.state === 'clarification'
863
+ ? controller.answerClarification(reply.prompt)
864
+ : controller.submitPrompt(reply.prompt, {
865
+ kind: reply.kind || 'quick-reply',
866
+ id: reply.id,
867
+ value: reply.prompt,
868
+ });
869
+ next$.subscribe((nextState) => {
870
+ this.aiAssistantPrompt = '';
871
+ this.aiAssistantViewState = nextState;
872
+ this.syncAiAssistantSession();
873
+ this.cdr.markForCheck();
874
+ });
875
+ }
876
+ onAiAssistantEditMessage(message) {
877
+ this.aiAssistantPrompt = message.text;
878
+ this.cdr.markForCheck();
879
+ }
880
+ onAiAssistantResendMessage(message) {
881
+ this.aiAssistantController?.resendMessage(message.id).subscribe((state) => {
882
+ this.aiAssistantPrompt = '';
883
+ this.aiAssistantViewState = state;
884
+ this.syncAiAssistantSession();
885
+ this.cdr.markForCheck();
886
+ });
887
+ }
888
+ onAiAssistantLayoutChange(layout) {
889
+ this.aiAssistantLayout = layout;
890
+ }
891
+ buildAiAssistantContextItems() {
892
+ const panels = this.config?.panels ?? [];
893
+ const items = [
894
+ {
895
+ id: 'component',
896
+ label: 'Componente',
897
+ value: 'Expansion',
898
+ kind: 'component',
899
+ icon: 'unfold_more',
900
+ },
901
+ {
902
+ id: 'expansion-id',
903
+ label: 'Expansion',
904
+ value: this.safeAiAssistantExpansionId(),
905
+ kind: 'custom',
906
+ icon: 'tag',
907
+ },
908
+ {
909
+ id: 'panels',
910
+ label: 'Paineis',
911
+ value: String(panels.length),
912
+ kind: 'custom',
913
+ icon: 'view_agenda',
914
+ },
915
+ ];
916
+ const expanded = panels.filter((panel) => panel.expanded).length;
917
+ if (expanded > 0) {
918
+ items.push({
919
+ id: 'expanded-panels',
920
+ label: 'Expandidos',
921
+ value: String(expanded),
922
+ kind: 'custom',
923
+ icon: 'expand_less',
924
+ });
925
+ }
926
+ return items;
927
+ }
928
+ initializeAiAssistantController() {
929
+ if (this.aiAssistantController)
930
+ return;
931
+ const flow = new ExpansionAgenticAuthoringTurnFlow(this.aiAdapter, this.aiApi);
932
+ const controller = this.aiTurnOrchestrator.createController(flow, {
933
+ componentId: this.aiAdapter.componentId || 'praxis-expansion',
934
+ componentType: this.aiAdapter.componentType || 'expansion',
935
+ contextItems: this.buildAiAssistantContextItems(),
936
+ });
937
+ this.aiAssistantController = controller;
938
+ this.aiAssistantViewState = controller.snapshot();
939
+ this.aiAssistantStateSubscription?.unsubscribe();
940
+ this.aiAssistantStateSubscription = controller.state$.subscribe((state) => {
941
+ this.aiAssistantViewState = state;
942
+ this.syncAiAssistantSession();
943
+ this.cdr.markForCheck();
944
+ });
945
+ this.cdr.markForCheck();
946
+ }
947
+ buildAiAssistantContextSnapshot() {
948
+ const counts = this.collectAiAssistantCounts();
949
+ const panelNames = this.collectAiAssistantPanelNames();
950
+ return {
951
+ identity: {
952
+ sessionId: this.resolveAiAssistantSessionId(),
953
+ ownerId: this.resolveAiAssistantOwnerId(),
954
+ ownerType: 'expansion',
955
+ componentId: 'praxis-expansion',
956
+ componentType: 'expansion',
957
+ routeKey: this.resolveAiAssistantRouteKey(),
958
+ },
959
+ target: {
960
+ kind: 'component',
961
+ id: this.resolveAiAssistantOwnerId(),
962
+ label: this.safeAiAssistantExpansionId(),
963
+ metadata: {
964
+ expansionId: this.safeAiAssistantExpansionId(),
965
+ hasCustomization: !!this.enableCustomization,
966
+ },
967
+ },
968
+ contextItems: this.buildAiAssistantContextItems().map((item) => ({
969
+ id: item.id,
970
+ label: item.label,
971
+ value: item.value || '',
972
+ kind: item.kind,
973
+ })),
974
+ mode: 'agentic-authoring',
975
+ authoringManifestRef: {
976
+ componentId: 'praxis-expansion',
977
+ source: 'PRAXIS_EXPANSION_AUTHORING_MANIFEST',
978
+ },
979
+ schemaFields: panelNames.length ? panelNames : undefined,
980
+ dataProfileDigest: {
981
+ summary: `${counts.panelCount} painel(is), ${counts.contentPanelCount} com campo(s), ${counts.widgetPanelCount} com widget(s)`,
982
+ counts,
983
+ },
984
+ runtimeStateDigest: {
985
+ summary: `Expansion ${this.config?.accordion?.multi ? 'multi' : 'single'}, ${counts.expandedCount} painel(is) expandido(s)`,
986
+ fields: [
987
+ 'panels',
988
+ 'accordion',
989
+ 'content',
990
+ 'widgets',
991
+ ],
992
+ },
993
+ capabilityRefs: [
994
+ {
995
+ id: 'expansion.component-edit-plan',
996
+ label: 'Plano de edicao de paineis',
997
+ source: 'PRAXIS_EXPANSION_AUTHORING_MANIFEST',
998
+ risk: 'medium',
999
+ },
1000
+ ],
1001
+ governanceHints: [
1002
+ {
1003
+ kind: 'business-rule-boundary',
1004
+ label: 'Regras compartilhadas exigem governanca',
1005
+ reason: 'Politicas de acesso, validacoes reutilizaveis e compliance nao devem ser aplicadas como patch local do expansion.',
1006
+ risk: 'high',
1007
+ },
1008
+ ],
1009
+ };
1010
+ }
1011
+ syncAiAssistantSession(visibility = null) {
1012
+ if (!this.enableCustomization)
1013
+ return;
1014
+ if (!this.aiAssistantOpen && !this.hasAiAssistantSessionState() && visibility !== 'minimized')
1015
+ return;
1016
+ const state = this.aiAssistantViewState;
1017
+ this.assistantSessions.upsertContextSession(this.buildAiAssistantContextSnapshot(), {
1018
+ title: 'Copiloto semantico Praxis',
1019
+ summary: this.resolveAiAssistantSummary(),
1020
+ mode: state?.mode || 'agentic-authoring',
1021
+ state: state?.state || 'idle',
1022
+ visibility: visibility ?? (this.aiAssistantOpen ? 'active' : 'minimized'),
1023
+ badge: this.resolveAiAssistantBadge(),
1024
+ icon: this.resolveAiAssistantIcon(),
1025
+ });
1026
+ }
1027
+ hasAiAssistantSessionState() {
1028
+ return !!this.aiAssistantPrompt.trim()
1029
+ || !!this.aiAssistantViewState?.messages?.length
1030
+ || !!this.aiAssistantViewState?.quickReplies?.length
1031
+ || !!this.aiAssistantViewState?.pendingPatch
1032
+ || !!this.aiAssistantViewState?.statusText?.trim()
1033
+ || !!this.aiAssistantViewState?.errorText?.trim();
1034
+ }
1035
+ resolveAiAssistantSessionId() {
1036
+ return `expansion:${this.resolveAiAssistantRouteKey()}:${this.resolveAiAssistantOwnerId()}`;
1037
+ }
1038
+ resolveAiAssistantOwnerId() {
1039
+ return (this.componentInstanceId || this.safeAiAssistantExpansionId() || 'expansion').trim() || 'expansion';
1040
+ }
1041
+ safeAiAssistantExpansionId() {
1042
+ return String(this.expansionId || '').trim();
1043
+ }
1044
+ resolveAiAssistantRouteKey() {
1045
+ const routePath = this.route?.snapshot?.routeConfig?.path?.trim();
1046
+ return routePath || 'local';
1047
+ }
1048
+ resolveAiAssistantSummary() {
1049
+ const status = this.aiAssistantViewState?.statusText?.trim();
1050
+ if (status)
1051
+ return status;
1052
+ const error = this.aiAssistantViewState?.errorText?.trim();
1053
+ if (error)
1054
+ return error;
1055
+ const prompt = this.aiAssistantPrompt.trim();
1056
+ if (prompt)
1057
+ return prompt.length > 96 ? `${prompt.slice(0, 93)}...` : prompt;
1058
+ const lastMessage = [...(this.aiAssistantViewState?.messages ?? [])].reverse()
1059
+ .find((message) => message.role === 'assistant' || message.role === 'user');
1060
+ if (lastMessage?.text) {
1061
+ return lastMessage.text.length > 96 ? `${lastMessage.text.slice(0, 93)}...` : lastMessage.text;
1062
+ }
1063
+ return 'Assistente contextual dos paineis.';
1064
+ }
1065
+ resolveAiAssistantBadge() {
1066
+ const state = this.aiAssistantViewState?.state;
1067
+ if (state === 'error')
1068
+ return 'erro';
1069
+ if (state === 'clarification')
1070
+ return 'revisar';
1071
+ if (state === 'review')
1072
+ return 'preview';
1073
+ if (state === 'success')
1074
+ return 'ok';
1075
+ return undefined;
1076
+ }
1077
+ resolveAiAssistantIcon() {
1078
+ const state = this.aiAssistantViewState?.state;
1079
+ if (state === 'error')
1080
+ return 'error';
1081
+ if (state === 'clarification')
1082
+ return 'rule';
1083
+ if (state === 'review')
1084
+ return 'rate_review';
1085
+ return 'auto_awesome';
1086
+ }
1087
+ collectAiAssistantPanelNames() {
1088
+ return Array.from(new Set((this.config?.panels ?? [])
1089
+ .map((panel, index) => panel.id || panel.title || `panel-${index + 1}`)
1090
+ .filter((name) => typeof name === 'string' && !!name.trim())));
1091
+ }
1092
+ collectAiAssistantCounts() {
1093
+ const panels = this.config?.panels ?? [];
1094
+ return {
1095
+ panelCount: panels.length,
1096
+ contentPanelCount: panels.filter((panel) => Array.isArray(panel.content) && panel.content.length > 0).length,
1097
+ widgetPanelCount: panels.filter((panel) => Array.isArray(panel.widgets) && panel.widgets.length > 0).length,
1098
+ actionPanelCount: panels.filter((panel) => Array.isArray(panel.actionButtons) && panel.actionButtons.length > 0).length,
1099
+ expandedCount: panels.filter((panel) => !!panel.expanded).length,
1100
+ };
1101
+ }
479
1102
  openEditor() {
480
1103
  const key = this.storageKey() || this.expansionId || 'default';
481
1104
  const ref = this.settings.open({
@@ -573,10 +1196,48 @@ class PraxisExpansion {
573
1196
 
574
1197
  @if (enableCustomization) {
575
1198
  <div class="expansion-ai-assistant">
576
- <praxis-ai-assistant [adapter]="aiAdapter"></praxis-ai-assistant>
1199
+ <button
1200
+ mat-mini-fab
1201
+ color="primary"
1202
+ type="button"
1203
+ class="expansion-ai-assistant-trigger"
1204
+ aria-label="Abrir copiloto semantico Praxis dos paineis"
1205
+ data-testid="praxis-expansion-ai-assistant-trigger"
1206
+ (click)="openAiAssistant()"
1207
+ >
1208
+ <mat-icon [praxisIcon]="'auto_awesome'"></mat-icon>
1209
+ </button>
577
1210
  </div>
578
1211
  }
579
1212
 
1213
+ <praxis-ai-assistant-shell
1214
+ *ngIf="aiAssistantOpen && aiAssistantViewState"
1215
+ [labels]="aiAssistantLabels"
1216
+ [mode]="aiAssistantViewState.mode"
1217
+ [state]="aiAssistantViewState.state"
1218
+ [contextItems]="aiAssistantViewState.contextItems"
1219
+ [attachments]="aiAssistantViewState.attachments"
1220
+ [messages]="aiAssistantViewState.messages"
1221
+ [quickReplies]="aiAssistantViewState.quickReplies"
1222
+ [prompt]="aiAssistantPrompt"
1223
+ [statusText]="aiAssistantViewState.statusText"
1224
+ [errorText]="aiAssistantViewState.errorText"
1225
+ [busy]="aiAssistantViewState.state === 'processing' || aiAssistantViewState.state === 'applying'"
1226
+ [canApply]="aiAssistantViewState.canApply"
1227
+ [layout]="aiAssistantLayout"
1228
+ testIdPrefix="praxis-expansion-ai-assistant"
1229
+ (promptChange)="onAiAssistantPromptChange($event)"
1230
+ (submitPrompt)="onAiAssistantSubmit($event)"
1231
+ (apply)="onAiAssistantApply()"
1232
+ (retryTurn)="onAiAssistantRetry()"
1233
+ (cancelTurn)="onAiAssistantCancel()"
1234
+ (quickReply)="onAiAssistantQuickReply($event)"
1235
+ (editMessage)="onAiAssistantEditMessage($event)"
1236
+ (resendMessage)="onAiAssistantResendMessage($event)"
1237
+ (layoutChange)="onAiAssistantLayoutChange($event)"
1238
+ (close)="closeAiAssistant()"
1239
+ ></praxis-ai-assistant-shell>
1240
+
580
1241
  @if (hasMultiple()) {
581
1242
  <mat-accordion
582
1243
  #accordion
@@ -603,8 +1264,9 @@ class PraxisExpansion {
603
1264
  >
604
1265
  <mat-expansion-panel-header
605
1266
  [collapsedHeight]="p.collapsedHeight || (defaultOptions?.collapsedHeight || injectedDefaults?.collapsedHeight) || ''"
606
- [expandedHeight]="p.expandedHeight || (defaultOptions?.expandedHeight || injectedDefaults?.expandedHeight) || ''"
607
- >
1267
+ [expandedHeight]="p.expandedHeight || (defaultOptions?.expandedHeight || injectedDefaults?.expandedHeight) || ''"
1268
+ >
1269
+ <mat-icon *ngIf="p.icon" [praxisIcon]="p.icon"></mat-icon>
608
1270
  <mat-panel-title>{{ p.title }}</mat-panel-title>
609
1271
  <mat-panel-description *ngIf="p.description">{{ p.description }}</mat-panel-description>
610
1272
  </mat-expansion-panel-header>
@@ -660,8 +1322,9 @@ class PraxisExpansion {
660
1322
  >
661
1323
  <mat-expansion-panel-header
662
1324
  [collapsedHeight]="p.collapsedHeight || (defaultOptions?.collapsedHeight || injectedDefaults?.collapsedHeight) || ''"
663
- [expandedHeight]="p.expandedHeight || (defaultOptions?.expandedHeight || injectedDefaults?.expandedHeight) || ''"
664
- >
1325
+ [expandedHeight]="p.expandedHeight || (defaultOptions?.expandedHeight || injectedDefaults?.expandedHeight) || ''"
1326
+ >
1327
+ <mat-icon *ngIf="p.icon" [praxisIcon]="p.icon"></mat-icon>
665
1328
  <mat-panel-title>{{ p.title }}</mat-panel-title>
666
1329
  <mat-panel-description *ngIf="p.description">{{ p.description }}</mat-panel-description>
667
1330
  </mat-expansion-panel-header>
@@ -718,7 +1381,7 @@ class PraxisExpansion {
718
1381
  >
719
1382
  <mat-icon fontIcon="edit"></mat-icon>
720
1383
  </button>
721
- `, isInline: true, styles: [":host{display:block;position:relative;color:var(--md-sys-color-on-surface)}.praxis-expansion-root{display:block;--p-exp-surface: var(--md-sys-color-surface);--p-exp-surface-container: var(--md-sys-color-surface-container);--p-exp-border: var(--md-sys-color-outline-variant);--p-exp-text: var(--md-sys-color-on-surface);--p-exp-text-muted: var(--md-sys-color-on-surface-variant);--p-exp-focus: var(--md-sys-color-primary);--p-exp-radius: 12px}.mat-expansion-panel{background:var(--p-exp-surface);border:1px solid var(--p-exp-border);border-radius:var(--p-exp-radius);overflow:hidden}.mat-expansion-panel:not(:last-child){margin-bottom:var(--p-exp-panel-gap, 12px)}.mat-expansion-panel-header{background:var(--p-exp-surface-container);color:var(--p-exp-text)}.mat-expansion-panel-header:focus-visible{outline:2px solid var(--p-exp-focus);outline-offset:-2px}.mat-expansion-panel-header-title{font-weight:600}.mat-expansion-panel-header-description{color:var(--p-exp-text-muted)}.mat-expansion-panel-body{padding:12px 16px 16px}.mat-action-row{border-top:1px solid var(--p-exp-border)}.density-compact .mat-expansion-panel-body{padding:8px 12px 12px}.density-compact .mat-expansion-panel-header{min-height:40px;padding:0 12px}.density-comfortable .mat-expansion-panel-body{padding:12px 16px 16px}.density-comfortable .mat-expansion-panel-header{min-height:48px;padding:0 16px}.density-spacious .mat-expansion-panel-body{padding:16px 20px 20px}.density-spacious .mat-expansion-panel-header{min-height:56px;padding:0 20px}.praxis-expansion-empty{display:flex;gap:8px;align-items:center;padding:8px 12px;color:var(--p-exp-text-muted)}.edit-fab{position:absolute;right:12px;bottom:12px;z-index:2}.expansion-ai-assistant{position:absolute;right:12px;bottom:72px;z-index:3}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "directive", type: i1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "ngmodule", type: MatExpansionModule }, { kind: "directive", type: i2.MatAccordion, selector: "mat-accordion", inputs: ["hideToggle", "displayMode", "togglePosition"], exportAs: ["matAccordion"] }, { kind: "component", type: i2.MatExpansionPanel, selector: "mat-expansion-panel", inputs: ["hideToggle", "togglePosition"], outputs: ["afterExpand", "afterCollapse"], exportAs: ["matExpansionPanel"] }, { kind: "directive", type: i2.MatExpansionPanelActionRow, selector: "mat-action-row" }, { kind: "component", type: i2.MatExpansionPanelHeader, selector: "mat-expansion-panel-header", inputs: ["expandedHeight", "collapsedHeight", "tabIndex"] }, { kind: "directive", type: i2.MatExpansionPanelTitle, selector: "mat-panel-title" }, { kind: "directive", type: i2.MatExpansionPanelDescription, selector: "mat-panel-description" }, { kind: "directive", type: i2.MatExpansionPanelContent, selector: "ng-template[matExpansionPanelContent]" }, { kind: "ngmodule", type: MatIconModule }, { kind: "component", type: i3.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "ngmodule", type: MatButtonModule }, { kind: "component", type: i4.MatButton, selector: " button[matButton], a[matButton], button[mat-button], button[mat-raised-button], button[mat-flat-button], button[mat-stroked-button], a[mat-button], a[mat-raised-button], a[mat-flat-button], a[mat-stroked-button] ", inputs: ["matButton"], exportAs: ["matButton", "matAnchor"] }, { kind: "component", type: i4.MatFabButton, selector: "button[mat-fab], a[mat-fab], button[matFab], a[matFab]", inputs: ["extended"], exportAs: ["matButton", "matAnchor"] }, { kind: "directive", type: DynamicFieldLoaderDirective, selector: "[dynamicFieldLoader]", inputs: ["fields", "formGroup", "enableExternalControlBinding", "itemTemplate", "debugTrace", "debugTraceLabel", "readonlyMode", "disabledMode", "presentationMode", "visible", "canvasMode"], outputs: ["componentsCreated", "fieldCreated", "fieldDestroyed", "renderError", "canvasMouseEnter", "canvasMouseLeave", "canvasClick"] }, { kind: "directive", type: DynamicWidgetLoaderDirective, selector: "[dynamicWidgetLoader]", inputs: ["dynamicWidgetLoader", "ownerWidgetKey", "context", "strictValidation", "autoWireOutputs"], outputs: ["widgetEvent", "widgetDiagnostic"], exportAs: ["dynamicWidgetLoader"] }, { kind: "directive", type: PraxisIconDirective, selector: "mat-icon[praxisIcon]", inputs: ["praxisIcon"] }, { kind: "component", type: PraxisAiAssistantComponent, selector: "praxis-ai-assistant", inputs: ["adapter", "riskPolicy", "allowManualPatchEdit"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
1384
+ `, isInline: true, styles: [":host{display:block;position:relative;color:var(--md-sys-color-on-surface)}.praxis-expansion-root{display:block;--p-exp-surface: var(--md-sys-color-surface);--p-exp-surface-container: var(--md-sys-color-surface-container);--p-exp-border: var(--md-sys-color-outline-variant);--p-exp-text: var(--md-sys-color-on-surface);--p-exp-text-muted: var(--md-sys-color-on-surface-variant);--p-exp-focus: var(--md-sys-color-primary);--p-exp-radius: 12px}.mat-expansion-panel{background:var(--p-exp-surface);border:1px solid var(--p-exp-border);border-radius:var(--p-exp-radius);overflow:hidden}.mat-expansion-panel:not(:last-child){margin-bottom:var(--p-exp-panel-gap, 12px)}.mat-expansion-panel-header{background:var(--p-exp-surface-container);color:var(--p-exp-text)}.mat-expansion-panel-header:focus-visible{outline:2px solid var(--p-exp-focus);outline-offset:-2px}.mat-expansion-panel-header-title{font-weight:600}.mat-expansion-panel-header-description{color:var(--p-exp-text-muted)}.mat-expansion-panel-body{padding:12px 16px 16px}.mat-action-row{border-top:1px solid var(--p-exp-border)}.density-compact .mat-expansion-panel-body{padding:8px 12px 12px}.density-compact .mat-expansion-panel-header{min-height:40px;padding:0 12px}.density-comfortable .mat-expansion-panel-body{padding:12px 16px 16px}.density-comfortable .mat-expansion-panel-header{min-height:48px;padding:0 16px}.density-spacious .mat-expansion-panel-body{padding:16px 20px 20px}.density-spacious .mat-expansion-panel-header{min-height:56px;padding:0 20px}.praxis-expansion-empty{display:flex;gap:8px;align-items:center;padding:8px 12px;color:var(--p-exp-text-muted)}.edit-fab{position:absolute;right:12px;bottom:12px;z-index:2}.expansion-ai-assistant{position:absolute;right:12px;bottom:72px;z-index:3}.expansion-ai-assistant-trigger{box-shadow:var(--md-sys-elevation-level2, 0 4px 12px rgba(0, 0, 0, .18))}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "directive", type: i1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "ngmodule", type: MatExpansionModule }, { kind: "directive", type: i2.MatAccordion, selector: "mat-accordion", inputs: ["hideToggle", "displayMode", "togglePosition"], exportAs: ["matAccordion"] }, { kind: "component", type: i2.MatExpansionPanel, selector: "mat-expansion-panel", inputs: ["hideToggle", "togglePosition"], outputs: ["afterExpand", "afterCollapse"], exportAs: ["matExpansionPanel"] }, { kind: "directive", type: i2.MatExpansionPanelActionRow, selector: "mat-action-row" }, { kind: "component", type: i2.MatExpansionPanelHeader, selector: "mat-expansion-panel-header", inputs: ["expandedHeight", "collapsedHeight", "tabIndex"] }, { kind: "directive", type: i2.MatExpansionPanelTitle, selector: "mat-panel-title" }, { kind: "directive", type: i2.MatExpansionPanelDescription, selector: "mat-panel-description" }, { kind: "directive", type: i2.MatExpansionPanelContent, selector: "ng-template[matExpansionPanelContent]" }, { kind: "ngmodule", type: MatIconModule }, { kind: "component", type: i3.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "ngmodule", type: MatButtonModule }, { kind: "component", type: i4.MatButton, selector: " button[matButton], a[matButton], button[mat-button], button[mat-raised-button], button[mat-flat-button], button[mat-stroked-button], a[mat-button], a[mat-raised-button], a[mat-flat-button], a[mat-stroked-button] ", inputs: ["matButton"], exportAs: ["matButton", "matAnchor"] }, { kind: "component", type: i4.MatMiniFabButton, selector: "button[mat-mini-fab], a[mat-mini-fab], button[matMiniFab], a[matMiniFab]", exportAs: ["matButton", "matAnchor"] }, { kind: "component", type: i4.MatFabButton, selector: "button[mat-fab], a[mat-fab], button[matFab], a[matFab]", inputs: ["extended"], exportAs: ["matButton", "matAnchor"] }, { kind: "directive", type: DynamicFieldLoaderDirective, selector: "[dynamicFieldLoader]", inputs: ["fields", "formGroup", "enableExternalControlBinding", "itemTemplate", "debugTrace", "debugTraceLabel", "readonlyMode", "disabledMode", "presentationMode", "visible", "canvasMode"], outputs: ["componentsCreated", "fieldCreated", "fieldDestroyed", "renderError", "canvasMouseEnter", "canvasMouseLeave", "canvasClick", "canvasFocus"] }, { kind: "directive", type: DynamicWidgetLoaderDirective, selector: "[dynamicWidgetLoader]", inputs: ["dynamicWidgetLoader", "ownerWidgetKey", "context", "strictValidation", "autoWireOutputs"], outputs: ["widgetEvent", "widgetDiagnostic"], exportAs: ["dynamicWidgetLoader"] }, { kind: "directive", type: PraxisIconDirective, selector: "mat-icon[praxisIcon]", inputs: ["praxisIcon"] }, { kind: "component", type: PraxisAiAssistantShellComponent, selector: "praxis-ai-assistant-shell", inputs: ["labels", "mode", "state", "contextItems", "attachments", "messages", "quickReplies", "prompt", "statusText", "errorText", "testIdPrefix", "panelTestId", "submitTestId", "applyTestId", "primaryAction", "secondaryActions", "governanceActions", "busy", "canSubmit", "canApply", "submitOnEnter", "showAttachAction", "enablePastedAttachments", "enableFileAttachments", "attachmentAccept", "attachmentMultiple", "draggable", "resizable", "minWidth", "minHeight", "margin", "layout"], outputs: ["promptChange", "submitPrompt", "apply", "retryTurn", "cancelTurn", "shellAction", "close", "attach", "attachmentsPasted", "attachmentsSelected", "removeAttachment", "messageAction", "editMessage", "resendMessage", "quickReply", "layoutChange"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
722
1385
  }
723
1386
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImport: i0, type: PraxisExpansion, decorators: [{
724
1387
  type: Component,
@@ -730,7 +1393,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImpo
730
1393
  DynamicFieldLoaderDirective,
731
1394
  DynamicWidgetLoaderDirective,
732
1395
  PraxisIconDirective,
733
- PraxisAiAssistantComponent,
1396
+ PraxisAiAssistantShellComponent,
734
1397
  ], template: `
735
1398
  <div
736
1399
  class="praxis-expansion-root"
@@ -745,10 +1408,48 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImpo
745
1408
 
746
1409
  @if (enableCustomization) {
747
1410
  <div class="expansion-ai-assistant">
748
- <praxis-ai-assistant [adapter]="aiAdapter"></praxis-ai-assistant>
1411
+ <button
1412
+ mat-mini-fab
1413
+ color="primary"
1414
+ type="button"
1415
+ class="expansion-ai-assistant-trigger"
1416
+ aria-label="Abrir copiloto semantico Praxis dos paineis"
1417
+ data-testid="praxis-expansion-ai-assistant-trigger"
1418
+ (click)="openAiAssistant()"
1419
+ >
1420
+ <mat-icon [praxisIcon]="'auto_awesome'"></mat-icon>
1421
+ </button>
749
1422
  </div>
750
1423
  }
751
1424
 
1425
+ <praxis-ai-assistant-shell
1426
+ *ngIf="aiAssistantOpen && aiAssistantViewState"
1427
+ [labels]="aiAssistantLabels"
1428
+ [mode]="aiAssistantViewState.mode"
1429
+ [state]="aiAssistantViewState.state"
1430
+ [contextItems]="aiAssistantViewState.contextItems"
1431
+ [attachments]="aiAssistantViewState.attachments"
1432
+ [messages]="aiAssistantViewState.messages"
1433
+ [quickReplies]="aiAssistantViewState.quickReplies"
1434
+ [prompt]="aiAssistantPrompt"
1435
+ [statusText]="aiAssistantViewState.statusText"
1436
+ [errorText]="aiAssistantViewState.errorText"
1437
+ [busy]="aiAssistantViewState.state === 'processing' || aiAssistantViewState.state === 'applying'"
1438
+ [canApply]="aiAssistantViewState.canApply"
1439
+ [layout]="aiAssistantLayout"
1440
+ testIdPrefix="praxis-expansion-ai-assistant"
1441
+ (promptChange)="onAiAssistantPromptChange($event)"
1442
+ (submitPrompt)="onAiAssistantSubmit($event)"
1443
+ (apply)="onAiAssistantApply()"
1444
+ (retryTurn)="onAiAssistantRetry()"
1445
+ (cancelTurn)="onAiAssistantCancel()"
1446
+ (quickReply)="onAiAssistantQuickReply($event)"
1447
+ (editMessage)="onAiAssistantEditMessage($event)"
1448
+ (resendMessage)="onAiAssistantResendMessage($event)"
1449
+ (layoutChange)="onAiAssistantLayoutChange($event)"
1450
+ (close)="closeAiAssistant()"
1451
+ ></praxis-ai-assistant-shell>
1452
+
752
1453
  @if (hasMultiple()) {
753
1454
  <mat-accordion
754
1455
  #accordion
@@ -775,8 +1476,9 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImpo
775
1476
  >
776
1477
  <mat-expansion-panel-header
777
1478
  [collapsedHeight]="p.collapsedHeight || (defaultOptions?.collapsedHeight || injectedDefaults?.collapsedHeight) || ''"
778
- [expandedHeight]="p.expandedHeight || (defaultOptions?.expandedHeight || injectedDefaults?.expandedHeight) || ''"
779
- >
1479
+ [expandedHeight]="p.expandedHeight || (defaultOptions?.expandedHeight || injectedDefaults?.expandedHeight) || ''"
1480
+ >
1481
+ <mat-icon *ngIf="p.icon" [praxisIcon]="p.icon"></mat-icon>
780
1482
  <mat-panel-title>{{ p.title }}</mat-panel-title>
781
1483
  <mat-panel-description *ngIf="p.description">{{ p.description }}</mat-panel-description>
782
1484
  </mat-expansion-panel-header>
@@ -832,8 +1534,9 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImpo
832
1534
  >
833
1535
  <mat-expansion-panel-header
834
1536
  [collapsedHeight]="p.collapsedHeight || (defaultOptions?.collapsedHeight || injectedDefaults?.collapsedHeight) || ''"
835
- [expandedHeight]="p.expandedHeight || (defaultOptions?.expandedHeight || injectedDefaults?.expandedHeight) || ''"
836
- >
1537
+ [expandedHeight]="p.expandedHeight || (defaultOptions?.expandedHeight || injectedDefaults?.expandedHeight) || ''"
1538
+ >
1539
+ <mat-icon *ngIf="p.icon" [praxisIcon]="p.icon"></mat-icon>
837
1540
  <mat-panel-title>{{ p.title }}</mat-panel-title>
838
1541
  <mat-panel-description *ngIf="p.description">{{ p.description }}</mat-panel-description>
839
1542
  </mat-expansion-panel-header>
@@ -890,7 +1593,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImpo
890
1593
  >
891
1594
  <mat-icon fontIcon="edit"></mat-icon>
892
1595
  </button>
893
- `, changeDetection: ChangeDetectionStrategy.OnPush, styles: [":host{display:block;position:relative;color:var(--md-sys-color-on-surface)}.praxis-expansion-root{display:block;--p-exp-surface: var(--md-sys-color-surface);--p-exp-surface-container: var(--md-sys-color-surface-container);--p-exp-border: var(--md-sys-color-outline-variant);--p-exp-text: var(--md-sys-color-on-surface);--p-exp-text-muted: var(--md-sys-color-on-surface-variant);--p-exp-focus: var(--md-sys-color-primary);--p-exp-radius: 12px}.mat-expansion-panel{background:var(--p-exp-surface);border:1px solid var(--p-exp-border);border-radius:var(--p-exp-radius);overflow:hidden}.mat-expansion-panel:not(:last-child){margin-bottom:var(--p-exp-panel-gap, 12px)}.mat-expansion-panel-header{background:var(--p-exp-surface-container);color:var(--p-exp-text)}.mat-expansion-panel-header:focus-visible{outline:2px solid var(--p-exp-focus);outline-offset:-2px}.mat-expansion-panel-header-title{font-weight:600}.mat-expansion-panel-header-description{color:var(--p-exp-text-muted)}.mat-expansion-panel-body{padding:12px 16px 16px}.mat-action-row{border-top:1px solid var(--p-exp-border)}.density-compact .mat-expansion-panel-body{padding:8px 12px 12px}.density-compact .mat-expansion-panel-header{min-height:40px;padding:0 12px}.density-comfortable .mat-expansion-panel-body{padding:12px 16px 16px}.density-comfortable .mat-expansion-panel-header{min-height:48px;padding:0 16px}.density-spacious .mat-expansion-panel-body{padding:16px 20px 20px}.density-spacious .mat-expansion-panel-header{min-height:56px;padding:0 20px}.praxis-expansion-empty{display:flex;gap:8px;align-items:center;padding:8px 12px;color:var(--p-exp-text-muted)}.edit-fab{position:absolute;right:12px;bottom:12px;z-index:2}.expansion-ai-assistant{position:absolute;right:12px;bottom:72px;z-index:3}\n"] }]
1596
+ `, changeDetection: ChangeDetectionStrategy.OnPush, styles: [":host{display:block;position:relative;color:var(--md-sys-color-on-surface)}.praxis-expansion-root{display:block;--p-exp-surface: var(--md-sys-color-surface);--p-exp-surface-container: var(--md-sys-color-surface-container);--p-exp-border: var(--md-sys-color-outline-variant);--p-exp-text: var(--md-sys-color-on-surface);--p-exp-text-muted: var(--md-sys-color-on-surface-variant);--p-exp-focus: var(--md-sys-color-primary);--p-exp-radius: 12px}.mat-expansion-panel{background:var(--p-exp-surface);border:1px solid var(--p-exp-border);border-radius:var(--p-exp-radius);overflow:hidden}.mat-expansion-panel:not(:last-child){margin-bottom:var(--p-exp-panel-gap, 12px)}.mat-expansion-panel-header{background:var(--p-exp-surface-container);color:var(--p-exp-text)}.mat-expansion-panel-header:focus-visible{outline:2px solid var(--p-exp-focus);outline-offset:-2px}.mat-expansion-panel-header-title{font-weight:600}.mat-expansion-panel-header-description{color:var(--p-exp-text-muted)}.mat-expansion-panel-body{padding:12px 16px 16px}.mat-action-row{border-top:1px solid var(--p-exp-border)}.density-compact .mat-expansion-panel-body{padding:8px 12px 12px}.density-compact .mat-expansion-panel-header{min-height:40px;padding:0 12px}.density-comfortable .mat-expansion-panel-body{padding:12px 16px 16px}.density-comfortable .mat-expansion-panel-header{min-height:48px;padding:0 16px}.density-spacious .mat-expansion-panel-body{padding:16px 20px 20px}.density-spacious .mat-expansion-panel-header{min-height:56px;padding:0 20px}.praxis-expansion-empty{display:flex;gap:8px;align-items:center;padding:8px 12px;color:var(--p-exp-text-muted)}.edit-fab{position:absolute;right:12px;bottom:12px;z-index:2}.expansion-ai-assistant{position:absolute;right:12px;bottom:72px;z-index:3}.expansion-ai-assistant-trigger{box-shadow:var(--md-sys-elevation-level2, 0 4px 12px rgba(0, 0, 0, .18))}\n"] }]
894
1597
  }], propDecorators: { config: [{
895
1598
  type: Input
896
1599
  }], expansionId: [{
@@ -1099,7 +1802,7 @@ class PraxisExpansionConfigEditor {
1099
1802
  }
1100
1803
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.17", ngImport: i0, type: PraxisExpansionConfigEditor, deps: [{ token: SETTINGS_PANEL_DATA }], target: i0.ɵɵFactoryTarget.Component });
1101
1804
  static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.17", type: PraxisExpansionConfigEditor, isStandalone: true, selector: "praxis-expansion-config-editor", inputs: { config: "config", expansionId: "expansionId" }, usesOnChanges: true, ngImport: i0, template: `
1102
- <div class="pdx-expansion-editor">
1805
+ <div class="pdx-expansion-editor" data-testid="expansion-config-editor">
1103
1806
  <section class="sec">
1104
1807
  <h4>Aparência</h4>
1105
1808
  <div class="grid two">
@@ -1321,6 +2024,9 @@ class PraxisExpansionConfigEditor {
1321
2024
  <mat-form-field appearance="outline"><mat-label>Descrição</mat-label>
1322
2025
  <input matInput [value]="p.description || ''" (input)="setPanel(i, 'description', $any($event.target).value)" />
1323
2026
  </mat-form-field>
2027
+ <mat-form-field appearance="outline"><mat-label>Ícone</mat-label>
2028
+ <input matInput [value]="p.icon || ''" (input)="setPanel(i, 'icon', $any($event.target).value)" placeholder="info" />
2029
+ </mat-form-field>
1324
2030
  <mat-form-field appearance="outline"><mat-label>Altura recolhida</mat-label>
1325
2031
  <input matInput placeholder="48px" [value]="p.collapsedHeight || ''" (input)="setPanel(i, 'collapsedHeight', $any($event.target).value)" />
1326
2032
  </mat-form-field>
@@ -1349,7 +2055,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImpo
1349
2055
  MatTooltipModule,
1350
2056
  PraxisIconDirective,
1351
2057
  ], changeDetection: ChangeDetectionStrategy.OnPush, template: `
1352
- <div class="pdx-expansion-editor">
2058
+ <div class="pdx-expansion-editor" data-testid="expansion-config-editor">
1353
2059
  <section class="sec">
1354
2060
  <h4>Aparência</h4>
1355
2061
  <div class="grid two">
@@ -1571,6 +2277,9 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImpo
1571
2277
  <mat-form-field appearance="outline"><mat-label>Descrição</mat-label>
1572
2278
  <input matInput [value]="p.description || ''" (input)="setPanel(i, 'description', $any($event.target).value)" />
1573
2279
  </mat-form-field>
2280
+ <mat-form-field appearance="outline"><mat-label>Ícone</mat-label>
2281
+ <input matInput [value]="p.icon || ''" (input)="setPanel(i, 'icon', $any($event.target).value)" placeholder="info" />
2282
+ </mat-form-field>
1574
2283
  <mat-form-field appearance="outline"><mat-label>Altura recolhida</mat-label>
1575
2284
  <input matInput placeholder="48px" [value]="p.collapsedHeight || ''" (input)="setPanel(i, 'collapsedHeight', $any($event.target).value)" />
1576
2285
  </mat-form-field>
@@ -1595,6 +2304,116 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImpo
1595
2304
  type: Input
1596
2305
  }] } });
1597
2306
 
2307
+ class PraxisExpansionWidgetConfigEditor {
2308
+ inputs = null;
2309
+ widgetKey;
2310
+ expansionEditor;
2311
+ isDirty$ = new BehaviorSubject(false);
2312
+ isValid$ = new BehaviorSubject(true);
2313
+ isBusy$ = new BehaviorSubject(false);
2314
+ subscription = new Subscription();
2315
+ emptyConfig = {};
2316
+ get config() {
2317
+ return this.inputs?.config ?? this.emptyConfig;
2318
+ }
2319
+ get expansionId() {
2320
+ return this.inputs?.expansionId ?? this.widgetKey;
2321
+ }
2322
+ ngAfterViewInit() {
2323
+ if (!this.expansionEditor) {
2324
+ return;
2325
+ }
2326
+ this.subscription.add(this.expansionEditor.isDirty$.subscribe((value) => this.isDirty$.next(value)));
2327
+ this.subscription.add(this.expansionEditor.isValid$.subscribe((value) => this.isValid$.next(value)));
2328
+ this.subscription.add(this.expansionEditor.isBusy$.subscribe((value) => this.isBusy$.next(value)));
2329
+ }
2330
+ ngOnDestroy() {
2331
+ this.subscription.unsubscribe();
2332
+ }
2333
+ getSettingsValue() {
2334
+ return this.buildValue(this.expansionEditor?.getSettingsValue());
2335
+ }
2336
+ onSave() {
2337
+ return this.buildValue(this.expansionEditor?.getSettingsValue());
2338
+ }
2339
+ reset() {
2340
+ this.expansionEditor?.reset();
2341
+ }
2342
+ buildValue(config) {
2343
+ return {
2344
+ inputs: {
2345
+ ...(this.inputs ?? {}),
2346
+ config: config ?? this.config,
2347
+ ...(this.expansionId ? { expansionId: this.expansionId } : {}),
2348
+ },
2349
+ };
2350
+ }
2351
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.17", ngImport: i0, type: PraxisExpansionWidgetConfigEditor, deps: [], target: i0.ɵɵFactoryTarget.Component });
2352
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.17", type: PraxisExpansionWidgetConfigEditor, isStandalone: true, selector: "praxis-expansion-widget-config-editor", inputs: { inputs: "inputs", widgetKey: "widgetKey" }, viewQueries: [{ propertyName: "expansionEditor", first: true, predicate: ["expansionEditor"], descendants: true }], ngImport: i0, template: `
2353
+ <section data-testid="expansion-widget-config-editor">
2354
+ <praxis-expansion-config-editor
2355
+ #expansionEditor
2356
+ [config]="config"
2357
+ [expansionId]="expansionId"
2358
+ />
2359
+ </section>
2360
+ `, isInline: true, dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "component", type: PraxisExpansionConfigEditor, selector: "praxis-expansion-config-editor", inputs: ["config", "expansionId"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
2361
+ }
2362
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImport: i0, type: PraxisExpansionWidgetConfigEditor, decorators: [{
2363
+ type: Component,
2364
+ args: [{
2365
+ selector: 'praxis-expansion-widget-config-editor',
2366
+ standalone: true,
2367
+ imports: [CommonModule, PraxisExpansionConfigEditor],
2368
+ template: `
2369
+ <section data-testid="expansion-widget-config-editor">
2370
+ <praxis-expansion-config-editor
2371
+ #expansionEditor
2372
+ [config]="config"
2373
+ [expansionId]="expansionId"
2374
+ />
2375
+ </section>
2376
+ `,
2377
+ changeDetection: ChangeDetectionStrategy.OnPush,
2378
+ }]
2379
+ }], propDecorators: { inputs: [{
2380
+ type: Input
2381
+ }], widgetKey: [{
2382
+ type: Input
2383
+ }], expansionEditor: [{
2384
+ type: ViewChild,
2385
+ args: ['expansionEditor']
2386
+ }] } });
2387
+
2388
+ const PRAXIS_EXPANSION_PORTS = [
2389
+ {
2390
+ id: 'config',
2391
+ label: 'Configuracao',
2392
+ direction: 'input',
2393
+ semanticKind: 'config-fragment',
2394
+ schema: {
2395
+ id: 'ExpansionMetadata',
2396
+ kind: 'ts-type',
2397
+ ref: 'ExpansionMetadata',
2398
+ },
2399
+ description: 'Fragmento canonico de configuracao dos paineis e dos widgets internos.',
2400
+ exposure: { public: true, group: 'config' },
2401
+ },
2402
+ {
2403
+ id: 'widgetEvent',
2404
+ label: 'Evento interno de widget',
2405
+ direction: 'output',
2406
+ semanticKind: 'event',
2407
+ schema: {
2408
+ id: 'WidgetEventEnvelope',
2409
+ kind: 'ts-type',
2410
+ ref: 'WidgetEventEnvelope',
2411
+ },
2412
+ cardinality: 'stream',
2413
+ description: 'Bridge composta para transporte de eventos internos. As portas canonicas dos filhos sao resolvidas por component-port + nestedPath.',
2414
+ exposure: { public: true, advanced: true, group: 'composite' },
2415
+ },
2416
+ ];
1598
2417
  const PRAXIS_EXPANSION_COMPONENT_METADATA = {
1599
2418
  id: 'praxis-expansion',
1600
2419
  selector: 'praxis-expansion',
@@ -1602,6 +2421,14 @@ const PRAXIS_EXPANSION_COMPONENT_METADATA = {
1602
2421
  friendlyName: 'Praxis Expansion Panel',
1603
2422
  description: 'Acordeão/Painéis de expansão configuráveis por metadata, com aparência e tokens M3.',
1604
2423
  icon: 'unfold_more',
2424
+ authoringManifestRef: {
2425
+ componentId: 'praxis-expansion',
2426
+ source: 'PRAXIS_EXPANSION_AUTHORING_MANIFEST',
2427
+ },
2428
+ configEditor: {
2429
+ component: PraxisExpansionWidgetConfigEditor,
2430
+ title: 'Configure expansion',
2431
+ },
1605
2432
  inputs: [
1606
2433
  { name: 'config', type: 'ExpansionMetadata', label: 'Configuração', description: 'Configuração JSON (acordeão, aparência e painéis)' },
1607
2434
  { name: 'expansionId', type: 'string', label: 'ID', description: 'Identificador para persistência (obrigatório)' },
@@ -1676,6 +2503,7 @@ const PRAXIS_EXPANSION_COMPONENT_METADATA = {
1676
2503
  ],
1677
2504
  tags: ['widget', 'expansion', 'accordion', 'configurable'],
1678
2505
  lib: '@praxisui/expansion',
2506
+ ports: PRAXIS_EXPANSION_PORTS,
1679
2507
  };
1680
2508
  function providePraxisExpansionMetadata() {
1681
2509
  return {
@@ -1691,6 +2519,315 @@ function providePraxisExpansionDefaults(opts) {
1691
2519
  return { provide: MAT_EXPANSION_PANEL_DEFAULT_OPTIONS, useValue: opts };
1692
2520
  }
1693
2521
 
2522
+ const panelItemSchema = {
2523
+ type: 'object',
2524
+ required: ['id', 'title'],
2525
+ properties: {
2526
+ id: { type: 'string' },
2527
+ title: { type: 'string' },
2528
+ description: { type: 'string' },
2529
+ icon: { type: 'string' },
2530
+ disabled: { type: 'boolean' },
2531
+ expanded: { type: 'boolean' },
2532
+ hideToggle: { type: 'boolean' },
2533
+ collapsedHeight: { type: 'string' },
2534
+ expandedHeight: { type: 'string' },
2535
+ content: { type: 'array', items: { type: 'object' } },
2536
+ widgets: { type: 'array', items: { type: 'object' } },
2537
+ actionButtons: { type: 'array', items: { type: 'object' } },
2538
+ },
2539
+ };
2540
+ const panelPatchSchema = {
2541
+ type: 'object',
2542
+ minProperties: 1,
2543
+ properties: {
2544
+ id: { type: 'string' },
2545
+ title: { type: 'string' },
2546
+ description: { type: 'string' },
2547
+ icon: { type: 'string' },
2548
+ disabled: { type: 'boolean' },
2549
+ expanded: { type: 'boolean' },
2550
+ hideToggle: { type: 'boolean' },
2551
+ collapsedHeight: { type: 'string' },
2552
+ expandedHeight: { type: 'string' },
2553
+ content: { type: 'array', items: { type: 'object' } },
2554
+ widgets: { type: 'array', items: { type: 'object' } },
2555
+ actionButtons: { type: 'array', items: { type: 'object' } },
2556
+ },
2557
+ };
2558
+ const PRAXIS_EXPANSION_AUTHORING_MANIFEST = {
2559
+ schemaVersion: '1.0.0',
2560
+ componentId: 'praxis-expansion',
2561
+ ownerPackage: '@praxisui/expansion',
2562
+ configSchemaId: 'ExpansionMetadata',
2563
+ manifestVersion: '1.0.0',
2564
+ runtimeInputs: [
2565
+ { name: 'config', type: 'ExpansionMetadata', description: 'Canonical accordion and panel configuration.' },
2566
+ { name: 'expansionId', type: 'string', description: 'Stable id used to derive expansion config persistence scope.' },
2567
+ { name: 'componentInstanceId', type: 'string', description: 'Optional instance discriminator for persistence scope.' },
2568
+ { name: 'context', type: 'Record<string, any>', description: 'Context passed to nested widgets.' },
2569
+ { name: 'strictValidation', type: 'boolean', description: 'Controls nested widget validation strictness.' },
2570
+ { name: 'defaultOptions', type: 'MatExpansionPanelDefaultOptions', description: 'Instance-level Material expansion defaults.' },
2571
+ { name: 'enableCustomization', type: 'boolean', description: 'Enables Settings Panel authoring surfaces.' },
2572
+ ],
2573
+ editableTargets: [
2574
+ { kind: 'panel', resolver: 'panel-by-id-or-title', description: 'A panel in config.panels[].' },
2575
+ { kind: 'panelHeader', resolver: 'panel-by-id-or-title', description: 'Header title, description, icon and heights for a panel.' },
2576
+ { kind: 'panelContent', resolver: 'panel-content-by-id', description: 'Lazy panel content hosted through fields, widgets or action buttons.' },
2577
+ { kind: 'expandedState', resolver: 'panel-by-id-or-title', description: 'Panel expanded state and default expanded selection.' },
2578
+ { kind: 'disabledState', resolver: 'panel-by-id-or-title', description: 'Panel disabled state.' },
2579
+ { kind: 'layout', resolver: 'expansion-layout-config', description: 'Accordion display mode, toggle position, density and visual layout.' },
2580
+ { kind: 'behavior', resolver: 'expansion-behavior-config', description: 'Accordion multi-expand and hide-toggle behavior.' },
2581
+ ],
2582
+ operations: [
2583
+ {
2584
+ operationId: 'panel.add',
2585
+ title: 'Add panel',
2586
+ scope: 'global',
2587
+ targetKind: 'panel',
2588
+ target: { kind: 'panel', resolver: 'panels-array', ambiguityPolicy: 'fail', required: false },
2589
+ inputSchema: panelItemSchema,
2590
+ effects: [{ kind: 'append-unique', path: 'panels[]', key: 'id' }],
2591
+ destructive: false,
2592
+ requiresConfirmation: false,
2593
+ validators: ['panel-id-unique', 'panel-order-deterministic', 'panel-content-valid'],
2594
+ affectedPaths: ['panels[]'],
2595
+ submissionImpact: 'config-only',
2596
+ preconditions: ['config-initialized'],
2597
+ },
2598
+ {
2599
+ operationId: 'panel.remove',
2600
+ title: 'Remove panel',
2601
+ scope: 'layout',
2602
+ targetKind: 'panel',
2603
+ target: { kind: 'panel', resolver: 'panel-by-id-or-title', ambiguityPolicy: 'fail', required: true },
2604
+ inputSchema: {
2605
+ type: 'object',
2606
+ properties: {
2607
+ replacementExpandedPanelId: { type: 'string' },
2608
+ },
2609
+ },
2610
+ effects: [{
2611
+ kind: 'compile-domain-patch',
2612
+ handler: 'expansion-panel-remove',
2613
+ handlerContract: {
2614
+ reads: ['accordion.multi', 'panels[]', 'panels[].id', 'panels[].expanded'],
2615
+ writes: ['panels[]', 'panels[].expanded'],
2616
+ identityKeys: ['panels[].id'],
2617
+ inputSchema: {
2618
+ type: 'object',
2619
+ properties: {
2620
+ replacementExpandedPanelId: { type: 'string' },
2621
+ },
2622
+ },
2623
+ failureModes: ['panel-not-found', 'replacement-panel-not-found', 'content-removal-not-confirmed', 'single-expand-conflict'],
2624
+ description: 'Removes the target panel by stable id and applies replacement expanded state when the removed panel was expanded.',
2625
+ },
2626
+ }],
2627
+ destructive: true,
2628
+ requiresConfirmation: true,
2629
+ validators: ['panel-exists', 'default-expanded-removal-safe', 'panel-content-removal-confirmed'],
2630
+ affectedPaths: ['panels[]', 'panels[].expanded'],
2631
+ submissionImpact: 'config-only',
2632
+ preconditions: ['config-initialized', 'target-panel-exists', 'confirmation-collected'],
2633
+ },
2634
+ {
2635
+ operationId: 'panel.title.set',
2636
+ title: 'Set panel title',
2637
+ scope: 'layout',
2638
+ targetKind: 'panelHeader',
2639
+ target: { kind: 'panelHeader', resolver: 'panel-by-id-or-title', ambiguityPolicy: 'fail', required: true },
2640
+ inputSchema: { type: 'object', required: ['title'], properties: { title: { type: 'string' } } },
2641
+ effects: [{ kind: 'merge-by-key', path: 'panels[]', key: 'id' }],
2642
+ destructive: false,
2643
+ requiresConfirmation: false,
2644
+ validators: ['panel-exists', 'panel-title-valid'],
2645
+ affectedPaths: ['panels[].title'],
2646
+ submissionImpact: 'config-only',
2647
+ preconditions: ['config-initialized', 'target-panel-exists'],
2648
+ },
2649
+ {
2650
+ operationId: 'panel.description.set',
2651
+ title: 'Set panel description',
2652
+ scope: 'layout',
2653
+ targetKind: 'panelHeader',
2654
+ target: { kind: 'panelHeader', resolver: 'panel-by-id-or-title', ambiguityPolicy: 'fail', required: true },
2655
+ inputSchema: { type: 'object', required: ['description'], properties: { description: { type: 'string' } } },
2656
+ effects: [{ kind: 'merge-by-key', path: 'panels[]', key: 'id' }],
2657
+ destructive: false,
2658
+ requiresConfirmation: false,
2659
+ validators: ['panel-exists', 'panel-description-valid'],
2660
+ affectedPaths: ['panels[].description'],
2661
+ submissionImpact: 'config-only',
2662
+ preconditions: ['config-initialized', 'target-panel-exists'],
2663
+ },
2664
+ {
2665
+ operationId: 'panel.icon.set',
2666
+ title: 'Set panel icon',
2667
+ scope: 'layout',
2668
+ targetKind: 'panelHeader',
2669
+ target: { kind: 'panelHeader', resolver: 'panel-by-id-or-title', ambiguityPolicy: 'fail', required: true },
2670
+ inputSchema: { type: 'object', required: ['icon'], properties: { icon: { type: 'string' } } },
2671
+ effects: [{ kind: 'merge-by-key', path: 'panels[]', key: 'id' }],
2672
+ destructive: false,
2673
+ requiresConfirmation: false,
2674
+ validators: ['panel-exists', 'panel-icon-valid', 'editor-runtime-round-trip'],
2675
+ affectedPaths: ['panels[].icon'],
2676
+ submissionImpact: 'config-only',
2677
+ preconditions: ['config-initialized', 'target-panel-exists'],
2678
+ },
2679
+ {
2680
+ operationId: 'panel.order.set',
2681
+ title: 'Reorder panels',
2682
+ scope: 'layout',
2683
+ targetKind: 'panel',
2684
+ target: { kind: 'panel', resolver: 'panel-by-id-or-title', ambiguityPolicy: 'fail', required: true },
2685
+ inputSchema: { type: 'object', required: ['beforePanelId'], properties: { beforePanelId: { type: 'string' } } },
2686
+ effects: [{ kind: 'reorder-by-key', path: 'panels[]', key: 'id' }],
2687
+ destructive: false,
2688
+ requiresConfirmation: false,
2689
+ validators: ['panel-exists', 'panel-order-deterministic'],
2690
+ affectedPaths: ['panels[]'],
2691
+ submissionImpact: 'config-only',
2692
+ preconditions: ['config-initialized', 'target-panel-exists'],
2693
+ },
2694
+ {
2695
+ operationId: 'panel.disabled.set',
2696
+ title: 'Set disabled state',
2697
+ scope: 'interaction',
2698
+ targetKind: 'disabledState',
2699
+ target: { kind: 'disabledState', resolver: 'panel-by-id-or-title', ambiguityPolicy: 'fail', required: true },
2700
+ inputSchema: { type: 'object', required: ['disabled'], properties: { disabled: { type: 'boolean' } } },
2701
+ effects: [{ kind: 'merge-by-key', path: 'panels[]', key: 'id' }],
2702
+ destructive: false,
2703
+ requiresConfirmation: false,
2704
+ validators: ['panel-exists', 'disabled-expanded-compatible'],
2705
+ affectedPaths: ['panels[].disabled', 'panels[].expanded'],
2706
+ submissionImpact: 'config-only',
2707
+ preconditions: ['config-initialized', 'target-panel-exists'],
2708
+ },
2709
+ {
2710
+ operationId: 'behavior.multiExpand.set',
2711
+ title: 'Set multi-expand behavior',
2712
+ scope: 'interaction',
2713
+ targetKind: 'behavior',
2714
+ target: { kind: 'behavior', resolver: 'expansion-behavior-config', ambiguityPolicy: 'fail', required: true },
2715
+ inputSchema: { type: 'object', required: ['multi'], properties: { multi: { type: 'boolean' } } },
2716
+ effects: [{
2717
+ kind: 'compile-domain-patch',
2718
+ handler: 'expansion-multi-expand-set',
2719
+ handlerContract: {
2720
+ reads: ['accordion.multi', 'panels[]', 'panels[].id', 'panels[].expanded'],
2721
+ writes: ['accordion.multi', 'panels[].expanded'],
2722
+ identityKeys: ['panels[].id'],
2723
+ inputSchema: { type: 'object', required: ['multi'], properties: { multi: { type: 'boolean' } } },
2724
+ failureModes: ['multiple-expanded-panels-conflict', 'panel-id-missing'],
2725
+ description: 'Sets accordion.multi and collapses competing expanded panels when switching to single-expand behavior.',
2726
+ },
2727
+ }],
2728
+ destructive: false,
2729
+ requiresConfirmation: false,
2730
+ validators: ['multi-expand-default-compatible', 'accordion-values-valid', 'editor-runtime-round-trip'],
2731
+ affectedPaths: ['accordion.multi', 'panels[].expanded'],
2732
+ submissionImpact: 'config-only',
2733
+ preconditions: ['config-initialized'],
2734
+ },
2735
+ {
2736
+ operationId: 'behavior.defaultExpanded.set',
2737
+ title: 'Set default expanded panel',
2738
+ scope: 'interaction',
2739
+ targetKind: 'expandedState',
2740
+ target: { kind: 'expandedState', resolver: 'panel-by-id-or-title', ambiguityPolicy: 'fail', required: true },
2741
+ inputSchema: {
2742
+ type: 'object',
2743
+ required: ['expanded'],
2744
+ properties: {
2745
+ expanded: { type: 'boolean' },
2746
+ collapseOthers: { type: 'boolean' },
2747
+ },
2748
+ },
2749
+ effects: [{
2750
+ kind: 'compile-domain-patch',
2751
+ handler: 'expansion-default-expanded-upsert',
2752
+ handlerContract: {
2753
+ reads: ['accordion.multi', 'panels[]', 'panels[].id', 'panels[].expanded', 'panels[].disabled'],
2754
+ writes: ['panels[].expanded'],
2755
+ identityKeys: ['panels[].id'],
2756
+ inputSchema: {
2757
+ type: 'object',
2758
+ required: ['panelId', 'expanded'],
2759
+ properties: {
2760
+ panelId: { type: 'string' },
2761
+ expanded: { type: 'boolean' },
2762
+ collapseOthers: { type: 'boolean' },
2763
+ },
2764
+ },
2765
+ failureModes: ['panel-not-found', 'panel-disabled', 'single-expand-conflict'],
2766
+ description: 'Sets a panel expanded state and collapses competing panels when accordion.multi is false.',
2767
+ },
2768
+ }],
2769
+ validators: ['panel-exists', 'default-expanded-panel-exists', 'multi-expand-default-compatible', 'disabled-expanded-compatible'],
2770
+ destructive: false,
2771
+ requiresConfirmation: false,
2772
+ affectedPaths: ['panels[].expanded', 'accordion.multi'],
2773
+ submissionImpact: 'config-only',
2774
+ preconditions: ['config-initialized', 'target-panel-exists'],
2775
+ },
2776
+ {
2777
+ operationId: 'panel.content.set',
2778
+ title: 'Set panel content',
2779
+ scope: 'layout',
2780
+ targetKind: 'panelContent',
2781
+ target: { kind: 'panelContent', resolver: 'panel-content-by-id', ambiguityPolicy: 'fail', required: true },
2782
+ inputSchema: panelPatchSchema,
2783
+ effects: [{ kind: 'merge-by-key', path: 'panels[]', key: 'id' }],
2784
+ destructive: false,
2785
+ requiresConfirmation: false,
2786
+ validators: ['panel-exists', 'panel-content-valid', 'lazy-content-compatible', 'nested-widget-contract-delegated'],
2787
+ affectedPaths: ['panels[].content', 'panels[].widgets', 'panels[].actionButtons'],
2788
+ submissionImpact: 'affects-schema-backed-data',
2789
+ preconditions: ['config-initialized', 'target-panel-exists'],
2790
+ },
2791
+ ],
2792
+ validators: [
2793
+ { validatorId: 'panel-id-unique', level: 'error', code: 'PEXP001', description: 'Panel ids must be unique and stable within config.panels[].' },
2794
+ { validatorId: 'panel-exists', level: 'error', code: 'PEXP002', description: 'Target panel must exist before applying the operation.' },
2795
+ { validatorId: 'panel-order-deterministic', level: 'error', code: 'PEXP003', description: 'Panel ordering must use stable ids, not transient array index as identity.' },
2796
+ { validatorId: 'panel-title-valid', level: 'error', code: 'PEXP004', description: 'Panel title must be a non-empty text value after localization/domain projection.' },
2797
+ { validatorId: 'panel-description-valid', level: 'warning', code: 'PEXP005', description: 'Panel description should remain plain header-support text and not replace panel content.' },
2798
+ { validatorId: 'panel-icon-valid', level: 'warning', code: 'PEXP006', description: 'Panel icon metadata must remain compatible with PraxisIconDirective and editor round-trip.' },
2799
+ { validatorId: 'panel-content-valid', level: 'error', code: 'PEXP007', description: 'Panel content must remain valid FieldMetadata[], WidgetDefinition[] or action button metadata.' },
2800
+ { validatorId: 'panel-content-removal-confirmed', level: 'error', code: 'PEXP008', description: 'Removing a panel with fields, widgets or action buttons is destructive and requires confirmation.' },
2801
+ { validatorId: 'default-expanded-panel-exists', level: 'error', code: 'PEXP009', description: 'Default expanded state must reference an existing panel id.' },
2802
+ { validatorId: 'default-expanded-removal-safe', level: 'error', code: 'PEXP010', description: 'Removing an expanded panel requires deterministic replacement state or explicit confirmation.' },
2803
+ { validatorId: 'multi-expand-default-compatible', level: 'error', code: 'PEXP011', description: 'When accordion.multi is false, at most one panel may be marked expanded by default.' },
2804
+ { validatorId: 'disabled-expanded-compatible', level: 'warning', code: 'PEXP012', description: 'A disabled panel should not be the only expanded/default focus target without explicit intent.' },
2805
+ { validatorId: 'accordion-values-valid', level: 'error', code: 'PEXP013', description: 'Accordion behavior values must match ExpansionMetadata and Angular Material expansion bindings.' },
2806
+ { validatorId: 'lazy-content-compatible', level: 'info', code: 'PEXP015', description: 'Panel content remains lazy through matExpansionPanelContent and should not require eager child runtime state.' },
2807
+ { validatorId: 'nested-widget-contract-delegated', level: 'info', code: 'PEXP016', description: 'Nested widget content remains governed by child component contracts and component-port nestedPath semantics.' },
2808
+ { validatorId: 'editor-runtime-round-trip', level: 'error', code: 'PEXP017', description: 'Settings Panel editor, runtime persistence and registry projection must preserve panel ids, order, icons and expanded state.' },
2809
+ ],
2810
+ roundTripRequirements: [
2811
+ 'Operations must preserve stable panel ids; array index may be used only as a resolver fallback, never as canonical identity.',
2812
+ 'Settings Panel editor, runtime persistence and registry projection must round-trip ExpansionMetadata without losing panel ids, order, icons or expanded state.',
2813
+ 'When accordion.multi is false, authoring must collapse competing panels or fail validation instead of producing multiple default-expanded panels.',
2814
+ 'Lazy panel content remains represented by panels[].content, panels[].widgets and panels[].actionButtons; authoring cannot require eager child component instances.',
2815
+ 'Nested widgets remain delegated through WidgetDefinition and component-port nestedPath semantics instead of being redefined by the expansion contract.',
2816
+ ],
2817
+ examples: [
2818
+ { id: 'add-summary-panel', request: 'Add a summary panel before the audit panel.', operationId: 'panel.add', params: { id: 'summary', title: 'Summary' }, isPositive: true },
2819
+ { id: 'rename-details-panel', request: 'Rename details to Account details.', operationId: 'panel.title.set', target: 'details', params: { title: 'Account details' }, isPositive: true },
2820
+ { id: 'describe-audit-panel', request: 'Set audit panel description to Recent changes.', operationId: 'panel.description.set', target: 'audit', params: { description: 'Recent changes' }, isPositive: true },
2821
+ { id: 'set-panel-icon', request: 'Use the info icon on the summary panel.', operationId: 'panel.icon.set', target: 'summary', params: { icon: 'info' }, isPositive: true },
2822
+ { id: 'set-default-expanded-panel', request: 'Open the summary panel by default.', operationId: 'behavior.defaultExpanded.set', target: 'summary', params: { expanded: true, collapseOthers: true }, isPositive: true },
2823
+ { id: 'enable-multi-expand', request: 'Allow multiple panels to stay open.', operationId: 'behavior.multiExpand.set', params: { multi: true }, isPositive: true },
2824
+ { id: 'disable-archive-panel', request: 'Disable the archive panel.', operationId: 'panel.disabled.set', target: 'archive', params: { disabled: true }, isPositive: true },
2825
+ { id: 'reject-missing-default-expanded', request: 'Open the missing panel by default.', operationId: 'behavior.defaultExpanded.set', target: 'missing', params: { expanded: true }, isPositive: false },
2826
+ { id: 'reject-duplicate-panel-id', request: 'Add another panel with id summary.', operationId: 'panel.add', params: { id: 'summary', title: 'Duplicate summary' }, isPositive: false },
2827
+ { id: 'confirm-remove-content-panel', request: 'Remove the details panel that contains widgets.', operationId: 'panel.remove', target: 'details', params: { replacementExpandedPanelId: 'summary' }, isPositive: true },
2828
+ ],
2829
+ };
2830
+
1694
2831
  /*
1695
2832
  * Public API Surface of praxis-expansion
1696
2833
  */
@@ -1699,4 +2836,4 @@ function providePraxisExpansionDefaults(opts) {
1699
2836
  * Generated bundle index. Do not edit.
1700
2837
  */
1701
2838
 
1702
- export { EXPANSION_AI_CAPABILITIES, PRAXIS_EXPANSION_COMPONENT_METADATA, PraxisExpansion, PraxisExpansionConfigEditor, providePraxisExpansionDefaults, providePraxisExpansionMetadata };
2839
+ export { EXPANSION_AI_CAPABILITIES, PRAXIS_EXPANSION_AUTHORING_MANIFEST, PRAXIS_EXPANSION_COMPONENT_METADATA, PraxisExpansion, PraxisExpansionConfigEditor, PraxisExpansionWidgetConfigEditor, providePraxisExpansionDefaults, providePraxisExpansionMetadata };