@praxisui/settings-panel 1.0.0-beta.8 → 2.0.0-beta.0

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,5 +1,5 @@
1
1
  import * as i0 from '@angular/core';
2
- import { inject, DestroyRef, ViewContainerRef, HostListener, ViewChild, ChangeDetectionStrategy, Component, InjectionToken, Injector, Injectable } from '@angular/core';
2
+ import { inject, DestroyRef, ViewContainerRef, HostListener, ViewChild, ChangeDetectionStrategy, Component, InjectionToken, Injector, Injectable, ChangeDetectorRef } from '@angular/core';
3
3
  import { ComponentPortal } from '@angular/cdk/portal';
4
4
  import * as i3 from '@angular/common';
5
5
  import { CommonModule } from '@angular/common';
@@ -7,7 +7,7 @@ import * as i3$1 from '@angular/material/button';
7
7
  import { MatButtonModule } from '@angular/material/button';
8
8
  import * as i4 from '@angular/material/icon';
9
9
  import { MatIconModule } from '@angular/material/icon';
10
- import { PraxisIconDirective, GlobalConfigService, FieldControlType, IconPickerService } from '@praxisui/core';
10
+ import { PraxisIconDirective, GlobalConfigService, FieldControlType, IconPickerService, LoggerService } from '@praxisui/core';
11
11
  import * as i5 from '@angular/material/tooltip';
12
12
  import { MatTooltipModule } from '@angular/material/tooltip';
13
13
  import { CdkTrapFocus } from '@angular/cdk/a11y';
@@ -17,13 +17,16 @@ import * as i1 from '@angular/material/dialog';
17
17
  import { MatDialogModule } from '@angular/material/dialog';
18
18
  import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
19
19
  import { isObservable, firstValueFrom, of, Subject, BehaviorSubject } from 'rxjs';
20
- import { filter, switchMap } from 'rxjs/operators';
20
+ import { filter, switchMap, debounceTime, distinctUntilChanged } from 'rxjs/operators';
21
21
  import { ConfirmDialogComponent } from '@praxisui/dynamic-fields';
22
22
  import * as i1$1 from '@angular/cdk/overlay';
23
23
  import * as i2 from '@angular/material/snack-bar';
24
24
  import { MatSnackBarModule } from '@angular/material/snack-bar';
25
25
  import * as i4$1 from '@angular/material/expansion';
26
26
  import { MatExpansionModule } from '@angular/material/expansion';
27
+ import * as i9 from '@angular/material/chips';
28
+ import { MatChipsModule } from '@angular/material/chips';
29
+ import { AiBackendApiService } from '@praxisui/ai';
27
30
 
28
31
  class SettingsPanelComponent {
29
32
  cdr;
@@ -38,6 +41,7 @@ class SettingsPanelComponent {
38
41
  isDirty = false;
39
42
  isValid = true;
40
43
  isBusy = false;
44
+ lastSavedAt = null;
41
45
  get canApply() {
42
46
  return this.isDirty && this.isValid && !this.isBusy;
43
47
  }
@@ -56,6 +60,27 @@ class SettingsPanelComponent {
56
60
  }
57
61
  return '';
58
62
  }
63
+ get statusTone() {
64
+ if (this.isBusy)
65
+ return 'busy';
66
+ if (this.isDirty)
67
+ return 'dirty';
68
+ if (this.lastSavedAt)
69
+ return 'saved';
70
+ return 'idle';
71
+ }
72
+ get statusMessage() {
73
+ if (this.statusTone === 'busy') {
74
+ return 'Operação em andamento...';
75
+ }
76
+ if (this.statusTone === 'dirty') {
77
+ return 'Alterações não salvas';
78
+ }
79
+ if (this.statusTone === 'saved' && this.lastSavedAt) {
80
+ return `Salvo às ${this.formatClock(this.lastSavedAt)}`;
81
+ }
82
+ return 'Sem alterações';
83
+ }
59
84
  destroyRef = inject(DestroyRef);
60
85
  contentHost;
61
86
  constructor(cdr, dialog) {
@@ -73,10 +98,6 @@ class SettingsPanelComponent {
73
98
  .pipe(takeUntilDestroyed(this.destroyRef))
74
99
  .subscribe((dirty) => {
75
100
  this.isDirty = dirty;
76
- try {
77
- (console.log || console.debug)('[SettingsPanel] isDirty$ ->', dirty);
78
- }
79
- catch { }
80
101
  this.cdr.markForCheck();
81
102
  });
82
103
  }
@@ -104,10 +125,6 @@ class SettingsPanelComponent {
104
125
  ? selected$.pipe(takeUntilDestroyed(this.destroyRef))
105
126
  : selected$;
106
127
  obs.subscribe((value) => {
107
- try {
108
- (console.log || console.debug)('[SettingsPanel] content.selected → apply()');
109
- }
110
- catch { }
111
128
  // Apply emits value to the opener without closing the panel.
112
129
  this.ref.apply(value);
113
130
  });
@@ -125,11 +142,12 @@ class SettingsPanelComponent {
125
142
  icon: 'restart_alt',
126
143
  };
127
144
  this.dialog
128
- .open(ConfirmDialogComponent, { data: dialogData })
145
+ .open(ConfirmDialogComponent, { data: dialogData, autoFocus: false })
129
146
  .afterClosed()
130
147
  .pipe(filter((confirmed) => confirmed))
131
148
  .subscribe(() => {
132
149
  this.contentRef?.instance?.reset?.();
150
+ this.lastSavedAt = null;
133
151
  this.ref.reset();
134
152
  });
135
153
  }
@@ -137,74 +155,49 @@ class SettingsPanelComponent {
137
155
  if (!this.canApply)
138
156
  return;
139
157
  const value = this.contentRef?.instance?.getSettingsValue?.();
140
- try {
141
- (console.log || console.debug)('[SettingsPanel] onApply()', { canApply: this.canApply });
142
- }
143
- catch { }
144
158
  this.ref.apply(value);
145
159
  }
146
160
  onSave() {
147
161
  if (!this.canSave)
148
162
  return;
149
163
  const instance = this.contentRef?.instance;
150
- try {
151
- const valuePreview = instance?.getSettingsValue?.();
152
- const keys = valuePreview && typeof valuePreview === 'object' ? Object.keys(valuePreview) : undefined;
153
- const keyTypes = valuePreview && typeof valuePreview === 'object'
154
- ? Object.fromEntries(Object.entries(valuePreview).map(([k, v]) => [k, Array.isArray(v) ? `array(${v.length})` : (v === null ? 'null' : typeof v)]))
155
- : undefined;
156
- (console.log || console.debug)('[SettingsPanel] onSave()', { canSave: this.canSave, preview: { keysCount: Array.isArray(keys) ? keys.length : undefined, keyTypes } });
157
- }
158
- catch { }
159
164
  const result = instance?.onSave?.();
160
165
  if (isObservable(result)) {
161
- firstValueFrom(result).then((value) => {
162
- try {
163
- const keys = value && typeof value === 'object' ? Object.keys(value) : undefined;
164
- const keyTypes = value && typeof value === 'object'
165
- ? Object.fromEntries(Object.entries(value).map(([k, v]) => [k, Array.isArray(v) ? `array(${v.length})` : (v === null ? 'null' : typeof v)]))
166
- : undefined;
167
- (console.log || console.debug)('[SettingsPanel] onSave(result:observable)', { keysCount: Array.isArray(keys) ? keys.length : undefined, keyTypes });
168
- }
169
- catch { }
170
- this.ref.save(value);
171
- });
166
+ firstValueFrom(result)
167
+ .then((value) => this.emitSave(value))
168
+ .catch(() => { });
172
169
  }
173
170
  else if (result instanceof Promise) {
174
- result.then((value) => {
175
- try {
176
- const keys = value && typeof value === 'object' ? Object.keys(value) : undefined;
177
- const keyTypes = value && typeof value === 'object'
178
- ? Object.fromEntries(Object.entries(value).map(([k, v]) => [k, Array.isArray(v) ? `array(${v.length})` : (v === null ? 'null' : typeof v)]))
179
- : undefined;
180
- (console.log || console.debug)('[SettingsPanel] onSave(result:promise)', { keysCount: Array.isArray(keys) ? keys.length : undefined, keyTypes });
181
- }
182
- catch { }
183
- this.ref.save(value);
184
- });
171
+ result
172
+ .then((value) => this.emitSave(value))
173
+ .catch(() => { });
185
174
  }
186
175
  else if (result !== undefined) {
187
- try {
188
- const keys = result && typeof result === 'object' ? Object.keys(result) : undefined;
189
- const keyTypes = result && typeof result === 'object'
190
- ? Object.fromEntries(Object.entries(result).map(([k, v]) => [k, Array.isArray(v) ? `array(${v.length})` : (v === null ? 'null' : typeof v)]))
191
- : undefined;
192
- (console.log || console.debug)('[SettingsPanel] onSave(result:value)', { keysCount: Array.isArray(keys) ? keys.length : undefined, keyTypes });
193
- }
194
- catch { }
195
- this.ref.save(result);
176
+ this.emitSave(result);
196
177
  }
197
178
  else {
198
179
  const value = instance?.getSettingsValue?.();
199
- try {
200
- const keys = value && typeof value === 'object' ? Object.keys(value) : undefined;
201
- const keyTypes = value && typeof value === 'object'
202
- ? Object.fromEntries(Object.entries(value).map(([k, v]) => [k, Array.isArray(v) ? `array(${v.length})` : (v === null ? 'null' : typeof v)]))
203
- : undefined;
204
- (console.log || console.debug)('[SettingsPanel] onSave(getSettingsValue)', { keysCount: Array.isArray(keys) ? keys.length : undefined, keyTypes });
205
- }
206
- catch { }
207
- this.ref.save(value);
180
+ this.emitSave(value);
181
+ }
182
+ }
183
+ emitSave(value) {
184
+ if (value === undefined)
185
+ return;
186
+ this.lastSavedAt = Date.now();
187
+ this.isDirty = false;
188
+ this.ref.save(value);
189
+ this.cdr.markForCheck();
190
+ }
191
+ formatClock(timestamp) {
192
+ try {
193
+ return new Intl.DateTimeFormat('pt-BR', {
194
+ hour: '2-digit',
195
+ minute: '2-digit',
196
+ second: '2-digit',
197
+ }).format(timestamp);
198
+ }
199
+ catch {
200
+ return '';
208
201
  }
209
202
  }
210
203
  toggleExpand() {
@@ -215,16 +208,8 @@ class SettingsPanelComponent {
215
208
  of(this.isDirty)
216
209
  .pipe(switchMap((dirty) => {
217
210
  if (!dirty) {
218
- try {
219
- (console.log || console.debug)('[SettingsPanel] onCancel(): not dirty → close');
220
- }
221
- catch { }
222
211
  return of(true);
223
212
  }
224
- try {
225
- (console.log || console.debug)('[SettingsPanel] onCancel(): dirty → confirm dialog');
226
- }
227
- catch { }
228
213
  const dialogData = {
229
214
  title: 'Descartar Alterações',
230
215
  message: 'Você tem alterações não salvas. Tem certeza de que deseja descartá-las?',
@@ -234,14 +219,10 @@ class SettingsPanelComponent {
234
219
  icon: 'warning',
235
220
  };
236
221
  return this.dialog
237
- .open(ConfirmDialogComponent, { data: dialogData })
222
+ .open(ConfirmDialogComponent, { data: dialogData, autoFocus: false })
238
223
  .afterClosed();
239
224
  }), filter((confirmed) => confirmed))
240
225
  .subscribe(() => {
241
- try {
242
- (console.log || console.debug)('[SettingsPanel] close(confirm)');
243
- }
244
- catch { }
245
226
  this.ref.close('cancel');
246
227
  });
247
228
  }
@@ -259,10 +240,10 @@ class SettingsPanelComponent {
259
240
  this.onSave();
260
241
  }
261
242
  }
262
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.1.4", ngImport: i0, type: SettingsPanelComponent, deps: [{ token: i0.ChangeDetectorRef }, { token: i1.MatDialog }], target: i0.ɵɵFactoryTarget.Component });
263
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.1.4", type: SettingsPanelComponent, isStandalone: true, selector: "praxis-settings-panel", host: { listeners: { "document:keydown": "handleKeydown($event)" } }, viewQueries: [{ propertyName: "contentHost", first: true, predicate: ["contentHost"], descendants: true, read: ViewContainerRef, static: true }], ngImport: i0, template: "<div\n class=\"settings-panel\"\n [class.expanded]=\"expanded\"\n role=\"dialog\"\n aria-modal=\"true\"\n [attr.aria-labelledby]=\"titleId\"\n cdkTrapFocus\n cdkTrapFocusAutoCapture\n>\n <header class=\"settings-panel-header\">\n <h2 class=\"settings-panel-title\" [id]=\"titleId\">\n <mat-icon *ngIf=\"titleIcon\" class=\"title-icon\" [praxisIcon]=\"titleIcon\"></mat-icon>\n <span>{{ title }}</span>\n </h2>\n <span class=\"spacer\"></span>\n <button\n mat-icon-button\n type=\"button\"\n [attr.aria-label]=\"expanded ? 'Reduzir painel' : 'Expandir painel'\"\n [attr.aria-expanded]=\"expanded\"\n [matTooltip]=\"expanded ? 'Reduzir painel' : 'Expandir painel'\"\n (click)=\"toggleExpand()\"\n >\n <mat-icon [praxisIcon]=\"expanded ? 'close_fullscreen' : 'open_in_full'\"></mat-icon>\n </button>\n <button\n mat-icon-button\n type=\"button\"\n aria-label=\"Fechar\"\n matTooltip=\"Fechar\"\n (click)=\"onCancel()\"\n >\n <mat-icon [praxisIcon]=\"'close'\"></mat-icon>\n </button>\n </header>\n <div class=\"settings-panel-body\">\n <ng-template #contentHost></ng-template>\n </div>\n <footer class=\"settings-panel-footer\">\n <button mat-button type=\"button\" (click)=\"onReset()\" [disabled]=\"isBusy\">\n <mat-icon aria-hidden=\"true\" [praxisIcon]=\"'restart_alt'\"></mat-icon>\n <span>Redefinir</span>\n </button>\n <span class=\"spacer\"></span>\n <button\n mat-button\n type=\"button\"\n (click)=\"onCancel()\"\n [disabled]=\"isBusy\"\n >\n <mat-icon aria-hidden=\"true\" [praxisIcon]=\"'close'\"></mat-icon>\n <span>Cancelar</span>\n </button>\n <button\n mat-stroked-button\n type=\"button\"\n (click)=\"onApply()\"\n [disabled]=\"!canApply\"\n [matTooltip]=\"disabledReason\"\n [matTooltipDisabled]=\"canApply\"\n aria-busy=\"isBusy\"\n >\n <mat-progress-spinner\n *ngIf=\"isBusy\"\n mode=\"indeterminate\"\n diameter=\"20\"\n ></mat-progress-spinner>\n <ng-container *ngIf=\"!isBusy\">\n <mat-icon aria-hidden=\"true\" [praxisIcon]=\"'done'\"></mat-icon>\n <span>Aplicar</span>\n </ng-container>\n </button>\n <button\n mat-flat-button\n color=\"primary\"\n type=\"button\"\n (click)=\"onSave()\"\n [disabled]=\"!canSave\"\n [matTooltip]=\"disabledReason\"\n [matTooltipDisabled]=\"canSave\"\n aria-busy=\"isBusy\"\n >\n <mat-progress-spinner\n *ngIf=\"isBusy\"\n mode=\"indeterminate\"\n diameter=\"20\"\n ></mat-progress-spinner>\n <ng-container *ngIf=\"!isBusy\">\n <mat-icon aria-hidden=\"true\" [praxisIcon]=\"'save'\"></mat-icon>\n <span>Salvar &amp; Fechar</span>\n </ng-container>\n </button>\n </footer>\n</div>\n", styles: ["@charset \"UTF-8\";:host{display:block;height:100%}.settings-panel{display:grid;grid-template-rows:auto 1fr auto;grid-template-areas:\"header\" \"body\" \"footer\";height:100%;background:var(--md-sys-color-surface-container);color:var(--md-sys-color-on-surface);border-left:1px solid var(--md-sys-color-outline-variant);width:720px;transition:width .3s ease;overflow:hidden}.settings-panel.expanded{width:min(95vw,1200px)}.settings-panel-header{grid-area:header;display:flex;align-items:center;gap:8px;padding:0 16px;height:64px;border-bottom:1px solid var(--md-sys-color-outline-variant);background:var(--md-sys-color-surface-container-high);box-shadow:0 2px 6px var(--sicoob-shadow-low, rgba(0, 0, 0, .08));flex-shrink:0}.settings-panel-header .spacer{flex:1}.settings-panel-body{grid-area:body;overflow-y:auto;min-height:0;padding:8px 8px 24px;background:var(--md-sys-color-surface)}.settings-panel-content{display:block}.settings-panel-footer{grid-area:footer;border-top:1px solid var(--md-sys-color-outline-variant);background:var(--md-sys-color-surface-container-high);box-shadow:0 -2px 6px var(--sicoob-shadow-low, rgba(0, 0, 0, .08));display:flex;align-items:center;padding:12px 16px;column-gap:12px;flex-shrink:0}.spacer{flex:1}.settings-panel-title{display:inline-flex;align-items:center;gap:8px;font-weight:700;letter-spacing:.2px;margin:0}.settings-panel-title mat-icon{color:var(--md-sys-color-primary)}.settings-panel-title .title-icon{width:20px;height:20px;font-size:20px}.settings-panel-footer button+button{margin-left:12px}.settings-panel-footer button{display:inline-flex;align-items:center}.settings-panel-footer button .mat-icon{font-size:20px;width:20px;height:20px;line-height:20px}.settings-panel-footer .mat-button-wrapper{display:inline-flex;align-items:center;gap:8px}.settings-panel-footer .mat-progress-spinner{margin-right:8px}.settings-panel-footer .mat-flat-button[color=primary]{font-weight:600}:host ::ng-deep .praxis-settings-panel-backdrop{background:var(--pfx-backdrop, rgba(0, 0, 0, .45));backdrop-filter:blur(var(--pfx-backdrop-blur, 6px)) saturate(110%);-webkit-backdrop-filter:blur(var(--pfx-backdrop-blur, 6px)) saturate(110%)}.settings-panel .mat-divider{background-color:var(--md-sys-color-outline-variant)!important}.settings-panel .mat-expansion-panel{background:var(--md-sys-color-surface-container);border:1px solid var(--md-sys-color-outline-variant);border-radius:12px;box-shadow:0 2px 8px var(--sicoob-shadow-low, rgba(0, 0, 0, .08))}.settings-panel .mat-expansion-panel-header{background:var(--md-sys-color-surface-container);color:var(--md-sys-color-on-surface)}.settings-panel .mat-expansion-panel-header mat-icon,.settings-panel .mat-expansion-panel-header .mat-icon{color:var(--md-sys-color-on-surface)!important}.settings-panel .mat-expansion-panel-header .mat-expansion-panel-header-title,.settings-panel .mat-expansion-panel-header .mat-expansion-panel-header-description{color:var(--md-sys-color-on-surface)}.settings-panel .mat-expansion-panel-header .mat-expansion-indicator,.settings-panel .mat-expansion-panel-header .mat-expansion-indicator:after{color:var(--md-sys-color-on-surface-variant);border-color:var(--md-sys-color-on-surface-variant)}.settings-panel .mat-mdc-tab-group .mat-mdc-tab-header{border-bottom:1px solid var(--md-sys-color-outline-variant)}.settings-panel .mat-mdc-tab-group .mdc-tab .mdc-tab__text-label{color:color-mix(in srgb,var(--md-sys-color-on-surface) 72%,transparent)}.settings-panel .mat-mdc-tab-group .mdc-tab.mdc-tab--active .mdc-tab__text-label{color:var(--md-sys-color-on-surface)}.settings-panel .mat-mdc-tab-group .mdc-tab-indicator__content--underline{border-color:var(--mat-sys-primary, var(--md-sys-color-primary))}.settings-panel .mat-mdc-form-field{--mdc-outlined-text-field-outline-color: var(--md-sys-color-outline-variant);--mdc-outlined-text-field-hover-outline-color: var(--md-sys-color-secondary, var(--md-sys-color-primary));--mdc-outlined-text-field-focus-outline-color: var(--md-sys-color-primary)}:host ::ng-deep .praxis-settings-panel-pane{position:fixed!important;top:0!important;right:0!important;height:100vh!important;z-index:1000}:host ::ng-deep .praxis-settings-panel-pane .settings-panel{pointer-events:auto}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i3.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "ngmodule", type: MatButtonModule }, { kind: "component", type: i3$1.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: i3$1.MatIconButton, selector: "button[mat-icon-button], a[mat-icon-button], button[matIconButton], a[matIconButton]", exportAs: ["matButton", "matAnchor"] }, { kind: "ngmodule", type: MatIconModule }, { kind: "component", type: i4.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "directive", type: PraxisIconDirective, selector: "mat-icon[praxisIcon]", inputs: ["praxisIcon"] }, { kind: "ngmodule", type: MatTooltipModule }, { kind: "directive", type: i5.MatTooltip, selector: "[matTooltip]", inputs: ["matTooltipPosition", "matTooltipPositionAtOrigin", "matTooltipDisabled", "matTooltipShowDelay", "matTooltipHideDelay", "matTooltipTouchGestures", "matTooltip", "matTooltipClass"], exportAs: ["matTooltip"] }, { kind: "directive", type: CdkTrapFocus, selector: "[cdkTrapFocus]", inputs: ["cdkTrapFocus", "cdkTrapFocusAutoCapture"], exportAs: ["cdkTrapFocus"] }, { kind: "ngmodule", type: MatProgressSpinnerModule }, { kind: "component", type: i6.MatProgressSpinner, selector: "mat-progress-spinner, mat-spinner", inputs: ["color", "mode", "value", "diameter", "strokeWidth"], exportAs: ["matProgressSpinner"] }, { kind: "ngmodule", type: MatDialogModule }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
243
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.17", ngImport: i0, type: SettingsPanelComponent, deps: [{ token: i0.ChangeDetectorRef }, { token: i1.MatDialog }], target: i0.ɵɵFactoryTarget.Component });
244
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.17", type: SettingsPanelComponent, isStandalone: true, selector: "praxis-settings-panel", host: { listeners: { "document:keydown": "handleKeydown($event)" } }, viewQueries: [{ propertyName: "contentHost", first: true, predicate: ["contentHost"], descendants: true, read: ViewContainerRef, static: true }], ngImport: i0, template: "<div\n class=\"settings-panel\"\n data-testid=\"settings-panel-root\"\n [class.expanded]=\"expanded\"\n role=\"dialog\"\n aria-modal=\"true\"\n [attr.aria-labelledby]=\"titleId\"\n cdkTrapFocus\n cdkTrapFocusAutoCapture\n>\n <header class=\"settings-panel-header\">\n <h2 class=\"settings-panel-title\" [id]=\"titleId\">\n <mat-icon *ngIf=\"titleIcon\" class=\"title-icon\" [praxisIcon]=\"titleIcon\"></mat-icon>\n <span>{{ title }}</span>\n </h2>\n <span class=\"spacer\"></span>\n <button\n mat-icon-button\n type=\"button\"\n data-testid=\"settings-panel-toggle-expand\"\n [attr.aria-label]=\"expanded ? 'Reduzir painel' : 'Expandir painel'\"\n [attr.aria-expanded]=\"expanded\"\n [matTooltip]=\"expanded ? 'Reduzir painel' : 'Expandir painel'\"\n (click)=\"toggleExpand()\"\n >\n <mat-icon [praxisIcon]=\"expanded ? 'close_fullscreen' : 'open_in_full'\"></mat-icon>\n </button>\n <button\n mat-icon-button\n type=\"button\"\n data-testid=\"settings-panel-close-icon\"\n aria-label=\"Fechar\"\n matTooltip=\"Fechar\"\n (click)=\"onCancel()\"\n >\n <mat-icon [praxisIcon]=\"'close'\"></mat-icon>\n </button>\n </header>\n <div\n class=\"settings-panel-status\"\n [attr.data-status]=\"statusTone\"\n role=\"status\"\n aria-live=\"polite\"\n >\n <mat-icon\n aria-hidden=\"true\"\n [praxisIcon]=\"\n statusTone === 'dirty'\n ? 'warning'\n : statusTone === 'saved'\n ? 'check_circle'\n : statusTone === 'busy'\n ? 'autorenew'\n : 'info'\n \"\n ></mat-icon>\n <span>{{ statusMessage }}</span>\n </div>\n <div class=\"settings-panel-body\">\n <ng-template #contentHost></ng-template>\n </div>\n <footer class=\"settings-panel-footer\">\n <button mat-button type=\"button\" data-testid=\"settings-panel-reset\" (click)=\"onReset()\" [disabled]=\"isBusy\">\n <mat-icon aria-hidden=\"true\" [praxisIcon]=\"'restart_alt'\"></mat-icon>\n <span>Redefinir</span>\n </button>\n <span class=\"spacer\"></span>\n <button\n mat-button\n type=\"button\"\n data-testid=\"settings-panel-cancel\"\n (click)=\"onCancel()\"\n [disabled]=\"isBusy\"\n >\n <mat-icon aria-hidden=\"true\" [praxisIcon]=\"'close'\"></mat-icon>\n <span>Cancelar</span>\n </button>\n <button\n mat-stroked-button\n type=\"button\"\n data-testid=\"settings-panel-apply\"\n (click)=\"onApply()\"\n [disabled]=\"!canApply\"\n [matTooltip]=\"disabledReason\"\n [matTooltipDisabled]=\"canApply\"\n [attr.aria-busy]=\"isBusy\"\n >\n <mat-progress-spinner\n *ngIf=\"isBusy\"\n mode=\"indeterminate\"\n diameter=\"20\"\n ></mat-progress-spinner>\n <ng-container *ngIf=\"!isBusy\">\n <mat-icon aria-hidden=\"true\" [praxisIcon]=\"'done'\"></mat-icon>\n <span>Aplicar</span>\n </ng-container>\n </button>\n <button\n mat-flat-button\n color=\"primary\"\n type=\"button\"\n data-testid=\"settings-panel-save\"\n (click)=\"onSave()\"\n [disabled]=\"!canSave\"\n [matTooltip]=\"disabledReason\"\n [matTooltipDisabled]=\"canSave\"\n [attr.aria-busy]=\"isBusy\"\n >\n <mat-progress-spinner\n *ngIf=\"isBusy\"\n mode=\"indeterminate\"\n diameter=\"20\"\n ></mat-progress-spinner>\n <ng-container *ngIf=\"!isBusy\">\n <mat-icon aria-hidden=\"true\" [praxisIcon]=\"'save'\"></mat-icon>\n <span>Salvar &amp; Fechar</span>\n </ng-container>\n </button>\n </footer>\n</div>\n", styles: ["@charset \"UTF-8\";:host{display:block;height:100%}.settings-panel{display:grid;grid-template-rows:auto auto 1fr auto;grid-template-areas:\"header\" \"status\" \"body\" \"footer\";height:100%;background:var(--md-sys-color-surface-container);color:var(--md-sys-color-on-surface);border-left:1px solid var(--md-sys-color-outline-variant);width:var(--pfx-settings-panel-width, 720px);transition:width .3s ease;overflow:hidden}.settings-panel.expanded{width:min(var(--pfx-settings-panel-width-expanded, 95vw),var(--pfx-settings-panel-max-width, 2400px))}.settings-panel-header{grid-area:header;display:flex;align-items:center;gap:var(--pfx-settings-panel-header-gap, 8px);padding:0 var(--pfx-settings-panel-header-padding-x, 16px);height:var(--pfx-settings-panel-header-height, 64px);border-bottom:1px solid var(--md-sys-color-outline-variant);background:var(--md-sys-color-surface-container-high);box-shadow:var(--pfx-settings-panel-header-shadow, var(--md-sys-elevation-level1, 0 2px 6px rgba(0, 0, 0, .08)));flex-shrink:0}.settings-panel-header .spacer{flex:1}.settings-panel-status{grid-area:status;display:flex;align-items:center;gap:8px;min-height:36px;padding:6px 16px;border-bottom:1px solid var(--md-sys-color-outline-variant);background:var(--md-sys-color-surface-container-high);color:var(--md-sys-color-on-surface);font-size:.85rem;font-weight:500}.settings-panel-status mat-icon{width:18px;height:18px;font-size:18px}.settings-panel-status[data-status=dirty]{color:var(--md-sys-color-error);background:color-mix(in srgb,var(--md-sys-color-error-container) 30%,var(--md-sys-color-surface-container-high))}.settings-panel-status[data-status=saved]{color:var(--md-sys-color-primary);background:color-mix(in srgb,var(--md-sys-color-primary-container) 28%,var(--md-sys-color-surface-container-high))}.settings-panel-body{grid-area:body;overflow-y:auto;min-height:0;padding:var(--pfx-settings-panel-body-padding, 8px 8px 24px 8px);background:var(--md-sys-color-surface);display:flex;flex-direction:column}.settings-panel-content{display:block}.settings-panel-footer{grid-area:footer;border-top:1px solid var(--md-sys-color-outline-variant);background:var(--md-sys-color-surface-container-high);box-shadow:var(--pfx-settings-panel-footer-shadow, var(--md-sys-elevation-level1, 0 -2px 6px rgba(0, 0, 0, .08)));display:flex;align-items:center;padding:var(--pfx-settings-panel-footer-padding, 12px 16px);column-gap:var(--pfx-settings-panel-footer-gap, 12px);flex-shrink:0}.spacer{flex:1}.settings-panel-title{display:inline-flex;align-items:center;gap:var(--pfx-settings-panel-title-gap, 8px);font-weight:700;letter-spacing:.2px;margin:0}.settings-panel-title mat-icon{color:var(--md-sys-color-primary)}.settings-panel-title .title-icon{width:20px;height:20px;font-size:20px}.settings-panel-footer button+button{margin-left:var(--pfx-settings-panel-footer-gap, 12px)}.settings-panel-footer button{display:inline-flex;align-items:center}.settings-panel-footer button .mat-icon{font-size:20px;width:20px;height:20px;line-height:1;display:inline-flex;align-items:center;justify-content:center}.settings-panel-footer .mat-button-wrapper{display:inline-flex;align-items:center;gap:var(--pfx-settings-panel-button-gap, 8px)}.settings-panel-footer .mat-progress-spinner{margin-right:8px}.settings-panel-footer .mat-flat-button[color=primary]{font-weight:600}:host ::ng-deep .settings-panel .mdc-button__label{display:inline-flex;align-items:center;gap:var(--pfx-settings-panel-button-gap, 8px);line-height:1}:host ::ng-deep .settings-panel .mdc-button__label>span{display:inline-flex;align-items:center;line-height:1}:host ::ng-deep .settings-panel .mdc-button__label .mat-icon{display:inline-flex;align-items:center;justify-content:center;line-height:1}:host ::ng-deep .praxis-settings-panel-backdrop{background:var(--pfx-backdrop, var(--md-sys-color-scrim, rgba(0, 0, 0, .45)));backdrop-filter:blur(var(--pfx-backdrop-blur, 6px)) saturate(110%);-webkit-backdrop-filter:blur(var(--pfx-backdrop-blur, 6px)) saturate(110%)}.settings-panel .mat-divider{background-color:var(--md-sys-color-outline-variant)!important}.settings-panel .mat-expansion-panel{background:var(--md-sys-color-surface-container);border:1px solid var(--md-sys-color-outline-variant);border-radius:var(--md-sys-shape-corner-medium, 12px);box-shadow:var(--md-sys-elevation-level1, 0 2px 8px rgba(0, 0, 0, .08))}.settings-panel .mat-expansion-panel-header{background:var(--md-sys-color-surface-container);color:var(--md-sys-color-on-surface)}.settings-panel .mat-expansion-panel-header mat-icon,.settings-panel .mat-expansion-panel-header .mat-icon{color:var(--md-sys-color-on-surface)!important}.settings-panel .mat-expansion-panel-header .mat-expansion-panel-header-title,.settings-panel .mat-expansion-panel-header .mat-expansion-panel-header-description{color:var(--md-sys-color-on-surface)}.settings-panel .mat-expansion-panel-header .mat-expansion-indicator,.settings-panel .mat-expansion-panel-header .mat-expansion-indicator:after{color:var(--md-sys-color-on-surface-variant);border-color:var(--md-sys-color-on-surface-variant)}:host ::ng-deep .settings-panel .mat-expansion-panel .mat-expansion-panel-body{padding:8px}.settings-panel .mat-mdc-tab-group .mat-mdc-tab-header{border-bottom:1px solid var(--md-sys-color-outline-variant)}.settings-panel .mat-mdc-tab-group .mdc-tab .mdc-tab__text-label{color:var(--md-sys-color-on-surface-variant)}.settings-panel .mat-mdc-tab-group .mdc-tab.mdc-tab--active .mdc-tab__text-label{color:var(--md-sys-color-on-surface)}.settings-panel .mat-mdc-tab-group .mdc-tab-indicator__content--underline{border-color:var(--md-sys-color-primary)}.settings-panel .mat-mdc-form-field{--mdc-outlined-text-field-outline-color: var(--md-sys-color-outline-variant);--mdc-outlined-text-field-hover-outline-color: var(--md-sys-color-secondary, var(--md-sys-color-primary));--mdc-outlined-text-field-focus-outline-color: var(--md-sys-color-primary)}:host ::ng-deep .praxis-settings-panel-pane{position:fixed!important;top:0!important;right:0!important;height:100vh!important;z-index:1000}:host ::ng-deep .praxis-settings-panel-pane .settings-panel{pointer-events:auto}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i3.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "ngmodule", type: MatButtonModule }, { kind: "component", type: i3$1.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: i3$1.MatIconButton, selector: "button[mat-icon-button], a[mat-icon-button], button[matIconButton], a[matIconButton]", exportAs: ["matButton", "matAnchor"] }, { kind: "ngmodule", type: MatIconModule }, { kind: "component", type: i4.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "directive", type: PraxisIconDirective, selector: "mat-icon[praxisIcon]", inputs: ["praxisIcon"] }, { kind: "ngmodule", type: MatTooltipModule }, { kind: "directive", type: i5.MatTooltip, selector: "[matTooltip]", inputs: ["matTooltipPosition", "matTooltipPositionAtOrigin", "matTooltipDisabled", "matTooltipShowDelay", "matTooltipHideDelay", "matTooltipTouchGestures", "matTooltip", "matTooltipClass"], exportAs: ["matTooltip"] }, { kind: "directive", type: CdkTrapFocus, selector: "[cdkTrapFocus]", inputs: ["cdkTrapFocus", "cdkTrapFocusAutoCapture"], exportAs: ["cdkTrapFocus"] }, { kind: "ngmodule", type: MatProgressSpinnerModule }, { kind: "component", type: i6.MatProgressSpinner, selector: "mat-progress-spinner, mat-spinner", inputs: ["color", "mode", "value", "diameter", "strokeWidth"], exportAs: ["matProgressSpinner"] }, { kind: "ngmodule", type: MatDialogModule }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
264
245
  }
265
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.1.4", ngImport: i0, type: SettingsPanelComponent, decorators: [{
246
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImport: i0, type: SettingsPanelComponent, decorators: [{
266
247
  type: Component,
267
248
  args: [{ selector: 'praxis-settings-panel', standalone: true, imports: [
268
249
  CommonModule,
@@ -273,7 +254,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.1.4", ngImpor
273
254
  CdkTrapFocus,
274
255
  MatProgressSpinnerModule,
275
256
  MatDialogModule,
276
- ], changeDetection: ChangeDetectionStrategy.OnPush, template: "<div\n class=\"settings-panel\"\n [class.expanded]=\"expanded\"\n role=\"dialog\"\n aria-modal=\"true\"\n [attr.aria-labelledby]=\"titleId\"\n cdkTrapFocus\n cdkTrapFocusAutoCapture\n>\n <header class=\"settings-panel-header\">\n <h2 class=\"settings-panel-title\" [id]=\"titleId\">\n <mat-icon *ngIf=\"titleIcon\" class=\"title-icon\" [praxisIcon]=\"titleIcon\"></mat-icon>\n <span>{{ title }}</span>\n </h2>\n <span class=\"spacer\"></span>\n <button\n mat-icon-button\n type=\"button\"\n [attr.aria-label]=\"expanded ? 'Reduzir painel' : 'Expandir painel'\"\n [attr.aria-expanded]=\"expanded\"\n [matTooltip]=\"expanded ? 'Reduzir painel' : 'Expandir painel'\"\n (click)=\"toggleExpand()\"\n >\n <mat-icon [praxisIcon]=\"expanded ? 'close_fullscreen' : 'open_in_full'\"></mat-icon>\n </button>\n <button\n mat-icon-button\n type=\"button\"\n aria-label=\"Fechar\"\n matTooltip=\"Fechar\"\n (click)=\"onCancel()\"\n >\n <mat-icon [praxisIcon]=\"'close'\"></mat-icon>\n </button>\n </header>\n <div class=\"settings-panel-body\">\n <ng-template #contentHost></ng-template>\n </div>\n <footer class=\"settings-panel-footer\">\n <button mat-button type=\"button\" (click)=\"onReset()\" [disabled]=\"isBusy\">\n <mat-icon aria-hidden=\"true\" [praxisIcon]=\"'restart_alt'\"></mat-icon>\n <span>Redefinir</span>\n </button>\n <span class=\"spacer\"></span>\n <button\n mat-button\n type=\"button\"\n (click)=\"onCancel()\"\n [disabled]=\"isBusy\"\n >\n <mat-icon aria-hidden=\"true\" [praxisIcon]=\"'close'\"></mat-icon>\n <span>Cancelar</span>\n </button>\n <button\n mat-stroked-button\n type=\"button\"\n (click)=\"onApply()\"\n [disabled]=\"!canApply\"\n [matTooltip]=\"disabledReason\"\n [matTooltipDisabled]=\"canApply\"\n aria-busy=\"isBusy\"\n >\n <mat-progress-spinner\n *ngIf=\"isBusy\"\n mode=\"indeterminate\"\n diameter=\"20\"\n ></mat-progress-spinner>\n <ng-container *ngIf=\"!isBusy\">\n <mat-icon aria-hidden=\"true\" [praxisIcon]=\"'done'\"></mat-icon>\n <span>Aplicar</span>\n </ng-container>\n </button>\n <button\n mat-flat-button\n color=\"primary\"\n type=\"button\"\n (click)=\"onSave()\"\n [disabled]=\"!canSave\"\n [matTooltip]=\"disabledReason\"\n [matTooltipDisabled]=\"canSave\"\n aria-busy=\"isBusy\"\n >\n <mat-progress-spinner\n *ngIf=\"isBusy\"\n mode=\"indeterminate\"\n diameter=\"20\"\n ></mat-progress-spinner>\n <ng-container *ngIf=\"!isBusy\">\n <mat-icon aria-hidden=\"true\" [praxisIcon]=\"'save'\"></mat-icon>\n <span>Salvar &amp; Fechar</span>\n </ng-container>\n </button>\n </footer>\n</div>\n", styles: ["@charset \"UTF-8\";:host{display:block;height:100%}.settings-panel{display:grid;grid-template-rows:auto 1fr auto;grid-template-areas:\"header\" \"body\" \"footer\";height:100%;background:var(--md-sys-color-surface-container);color:var(--md-sys-color-on-surface);border-left:1px solid var(--md-sys-color-outline-variant);width:720px;transition:width .3s ease;overflow:hidden}.settings-panel.expanded{width:min(95vw,1200px)}.settings-panel-header{grid-area:header;display:flex;align-items:center;gap:8px;padding:0 16px;height:64px;border-bottom:1px solid var(--md-sys-color-outline-variant);background:var(--md-sys-color-surface-container-high);box-shadow:0 2px 6px var(--sicoob-shadow-low, rgba(0, 0, 0, .08));flex-shrink:0}.settings-panel-header .spacer{flex:1}.settings-panel-body{grid-area:body;overflow-y:auto;min-height:0;padding:8px 8px 24px;background:var(--md-sys-color-surface)}.settings-panel-content{display:block}.settings-panel-footer{grid-area:footer;border-top:1px solid var(--md-sys-color-outline-variant);background:var(--md-sys-color-surface-container-high);box-shadow:0 -2px 6px var(--sicoob-shadow-low, rgba(0, 0, 0, .08));display:flex;align-items:center;padding:12px 16px;column-gap:12px;flex-shrink:0}.spacer{flex:1}.settings-panel-title{display:inline-flex;align-items:center;gap:8px;font-weight:700;letter-spacing:.2px;margin:0}.settings-panel-title mat-icon{color:var(--md-sys-color-primary)}.settings-panel-title .title-icon{width:20px;height:20px;font-size:20px}.settings-panel-footer button+button{margin-left:12px}.settings-panel-footer button{display:inline-flex;align-items:center}.settings-panel-footer button .mat-icon{font-size:20px;width:20px;height:20px;line-height:20px}.settings-panel-footer .mat-button-wrapper{display:inline-flex;align-items:center;gap:8px}.settings-panel-footer .mat-progress-spinner{margin-right:8px}.settings-panel-footer .mat-flat-button[color=primary]{font-weight:600}:host ::ng-deep .praxis-settings-panel-backdrop{background:var(--pfx-backdrop, rgba(0, 0, 0, .45));backdrop-filter:blur(var(--pfx-backdrop-blur, 6px)) saturate(110%);-webkit-backdrop-filter:blur(var(--pfx-backdrop-blur, 6px)) saturate(110%)}.settings-panel .mat-divider{background-color:var(--md-sys-color-outline-variant)!important}.settings-panel .mat-expansion-panel{background:var(--md-sys-color-surface-container);border:1px solid var(--md-sys-color-outline-variant);border-radius:12px;box-shadow:0 2px 8px var(--sicoob-shadow-low, rgba(0, 0, 0, .08))}.settings-panel .mat-expansion-panel-header{background:var(--md-sys-color-surface-container);color:var(--md-sys-color-on-surface)}.settings-panel .mat-expansion-panel-header mat-icon,.settings-panel .mat-expansion-panel-header .mat-icon{color:var(--md-sys-color-on-surface)!important}.settings-panel .mat-expansion-panel-header .mat-expansion-panel-header-title,.settings-panel .mat-expansion-panel-header .mat-expansion-panel-header-description{color:var(--md-sys-color-on-surface)}.settings-panel .mat-expansion-panel-header .mat-expansion-indicator,.settings-panel .mat-expansion-panel-header .mat-expansion-indicator:after{color:var(--md-sys-color-on-surface-variant);border-color:var(--md-sys-color-on-surface-variant)}.settings-panel .mat-mdc-tab-group .mat-mdc-tab-header{border-bottom:1px solid var(--md-sys-color-outline-variant)}.settings-panel .mat-mdc-tab-group .mdc-tab .mdc-tab__text-label{color:color-mix(in srgb,var(--md-sys-color-on-surface) 72%,transparent)}.settings-panel .mat-mdc-tab-group .mdc-tab.mdc-tab--active .mdc-tab__text-label{color:var(--md-sys-color-on-surface)}.settings-panel .mat-mdc-tab-group .mdc-tab-indicator__content--underline{border-color:var(--mat-sys-primary, var(--md-sys-color-primary))}.settings-panel .mat-mdc-form-field{--mdc-outlined-text-field-outline-color: var(--md-sys-color-outline-variant);--mdc-outlined-text-field-hover-outline-color: var(--md-sys-color-secondary, var(--md-sys-color-primary));--mdc-outlined-text-field-focus-outline-color: var(--md-sys-color-primary)}:host ::ng-deep .praxis-settings-panel-pane{position:fixed!important;top:0!important;right:0!important;height:100vh!important;z-index:1000}:host ::ng-deep .praxis-settings-panel-pane .settings-panel{pointer-events:auto}\n"] }]
257
+ ], changeDetection: ChangeDetectionStrategy.OnPush, template: "<div\n class=\"settings-panel\"\n data-testid=\"settings-panel-root\"\n [class.expanded]=\"expanded\"\n role=\"dialog\"\n aria-modal=\"true\"\n [attr.aria-labelledby]=\"titleId\"\n cdkTrapFocus\n cdkTrapFocusAutoCapture\n>\n <header class=\"settings-panel-header\">\n <h2 class=\"settings-panel-title\" [id]=\"titleId\">\n <mat-icon *ngIf=\"titleIcon\" class=\"title-icon\" [praxisIcon]=\"titleIcon\"></mat-icon>\n <span>{{ title }}</span>\n </h2>\n <span class=\"spacer\"></span>\n <button\n mat-icon-button\n type=\"button\"\n data-testid=\"settings-panel-toggle-expand\"\n [attr.aria-label]=\"expanded ? 'Reduzir painel' : 'Expandir painel'\"\n [attr.aria-expanded]=\"expanded\"\n [matTooltip]=\"expanded ? 'Reduzir painel' : 'Expandir painel'\"\n (click)=\"toggleExpand()\"\n >\n <mat-icon [praxisIcon]=\"expanded ? 'close_fullscreen' : 'open_in_full'\"></mat-icon>\n </button>\n <button\n mat-icon-button\n type=\"button\"\n data-testid=\"settings-panel-close-icon\"\n aria-label=\"Fechar\"\n matTooltip=\"Fechar\"\n (click)=\"onCancel()\"\n >\n <mat-icon [praxisIcon]=\"'close'\"></mat-icon>\n </button>\n </header>\n <div\n class=\"settings-panel-status\"\n [attr.data-status]=\"statusTone\"\n role=\"status\"\n aria-live=\"polite\"\n >\n <mat-icon\n aria-hidden=\"true\"\n [praxisIcon]=\"\n statusTone === 'dirty'\n ? 'warning'\n : statusTone === 'saved'\n ? 'check_circle'\n : statusTone === 'busy'\n ? 'autorenew'\n : 'info'\n \"\n ></mat-icon>\n <span>{{ statusMessage }}</span>\n </div>\n <div class=\"settings-panel-body\">\n <ng-template #contentHost></ng-template>\n </div>\n <footer class=\"settings-panel-footer\">\n <button mat-button type=\"button\" data-testid=\"settings-panel-reset\" (click)=\"onReset()\" [disabled]=\"isBusy\">\n <mat-icon aria-hidden=\"true\" [praxisIcon]=\"'restart_alt'\"></mat-icon>\n <span>Redefinir</span>\n </button>\n <span class=\"spacer\"></span>\n <button\n mat-button\n type=\"button\"\n data-testid=\"settings-panel-cancel\"\n (click)=\"onCancel()\"\n [disabled]=\"isBusy\"\n >\n <mat-icon aria-hidden=\"true\" [praxisIcon]=\"'close'\"></mat-icon>\n <span>Cancelar</span>\n </button>\n <button\n mat-stroked-button\n type=\"button\"\n data-testid=\"settings-panel-apply\"\n (click)=\"onApply()\"\n [disabled]=\"!canApply\"\n [matTooltip]=\"disabledReason\"\n [matTooltipDisabled]=\"canApply\"\n [attr.aria-busy]=\"isBusy\"\n >\n <mat-progress-spinner\n *ngIf=\"isBusy\"\n mode=\"indeterminate\"\n diameter=\"20\"\n ></mat-progress-spinner>\n <ng-container *ngIf=\"!isBusy\">\n <mat-icon aria-hidden=\"true\" [praxisIcon]=\"'done'\"></mat-icon>\n <span>Aplicar</span>\n </ng-container>\n </button>\n <button\n mat-flat-button\n color=\"primary\"\n type=\"button\"\n data-testid=\"settings-panel-save\"\n (click)=\"onSave()\"\n [disabled]=\"!canSave\"\n [matTooltip]=\"disabledReason\"\n [matTooltipDisabled]=\"canSave\"\n [attr.aria-busy]=\"isBusy\"\n >\n <mat-progress-spinner\n *ngIf=\"isBusy\"\n mode=\"indeterminate\"\n diameter=\"20\"\n ></mat-progress-spinner>\n <ng-container *ngIf=\"!isBusy\">\n <mat-icon aria-hidden=\"true\" [praxisIcon]=\"'save'\"></mat-icon>\n <span>Salvar &amp; Fechar</span>\n </ng-container>\n </button>\n </footer>\n</div>\n", styles: ["@charset \"UTF-8\";:host{display:block;height:100%}.settings-panel{display:grid;grid-template-rows:auto auto 1fr auto;grid-template-areas:\"header\" \"status\" \"body\" \"footer\";height:100%;background:var(--md-sys-color-surface-container);color:var(--md-sys-color-on-surface);border-left:1px solid var(--md-sys-color-outline-variant);width:var(--pfx-settings-panel-width, 720px);transition:width .3s ease;overflow:hidden}.settings-panel.expanded{width:min(var(--pfx-settings-panel-width-expanded, 95vw),var(--pfx-settings-panel-max-width, 2400px))}.settings-panel-header{grid-area:header;display:flex;align-items:center;gap:var(--pfx-settings-panel-header-gap, 8px);padding:0 var(--pfx-settings-panel-header-padding-x, 16px);height:var(--pfx-settings-panel-header-height, 64px);border-bottom:1px solid var(--md-sys-color-outline-variant);background:var(--md-sys-color-surface-container-high);box-shadow:var(--pfx-settings-panel-header-shadow, var(--md-sys-elevation-level1, 0 2px 6px rgba(0, 0, 0, .08)));flex-shrink:0}.settings-panel-header .spacer{flex:1}.settings-panel-status{grid-area:status;display:flex;align-items:center;gap:8px;min-height:36px;padding:6px 16px;border-bottom:1px solid var(--md-sys-color-outline-variant);background:var(--md-sys-color-surface-container-high);color:var(--md-sys-color-on-surface);font-size:.85rem;font-weight:500}.settings-panel-status mat-icon{width:18px;height:18px;font-size:18px}.settings-panel-status[data-status=dirty]{color:var(--md-sys-color-error);background:color-mix(in srgb,var(--md-sys-color-error-container) 30%,var(--md-sys-color-surface-container-high))}.settings-panel-status[data-status=saved]{color:var(--md-sys-color-primary);background:color-mix(in srgb,var(--md-sys-color-primary-container) 28%,var(--md-sys-color-surface-container-high))}.settings-panel-body{grid-area:body;overflow-y:auto;min-height:0;padding:var(--pfx-settings-panel-body-padding, 8px 8px 24px 8px);background:var(--md-sys-color-surface);display:flex;flex-direction:column}.settings-panel-content{display:block}.settings-panel-footer{grid-area:footer;border-top:1px solid var(--md-sys-color-outline-variant);background:var(--md-sys-color-surface-container-high);box-shadow:var(--pfx-settings-panel-footer-shadow, var(--md-sys-elevation-level1, 0 -2px 6px rgba(0, 0, 0, .08)));display:flex;align-items:center;padding:var(--pfx-settings-panel-footer-padding, 12px 16px);column-gap:var(--pfx-settings-panel-footer-gap, 12px);flex-shrink:0}.spacer{flex:1}.settings-panel-title{display:inline-flex;align-items:center;gap:var(--pfx-settings-panel-title-gap, 8px);font-weight:700;letter-spacing:.2px;margin:0}.settings-panel-title mat-icon{color:var(--md-sys-color-primary)}.settings-panel-title .title-icon{width:20px;height:20px;font-size:20px}.settings-panel-footer button+button{margin-left:var(--pfx-settings-panel-footer-gap, 12px)}.settings-panel-footer button{display:inline-flex;align-items:center}.settings-panel-footer button .mat-icon{font-size:20px;width:20px;height:20px;line-height:1;display:inline-flex;align-items:center;justify-content:center}.settings-panel-footer .mat-button-wrapper{display:inline-flex;align-items:center;gap:var(--pfx-settings-panel-button-gap, 8px)}.settings-panel-footer .mat-progress-spinner{margin-right:8px}.settings-panel-footer .mat-flat-button[color=primary]{font-weight:600}:host ::ng-deep .settings-panel .mdc-button__label{display:inline-flex;align-items:center;gap:var(--pfx-settings-panel-button-gap, 8px);line-height:1}:host ::ng-deep .settings-panel .mdc-button__label>span{display:inline-flex;align-items:center;line-height:1}:host ::ng-deep .settings-panel .mdc-button__label .mat-icon{display:inline-flex;align-items:center;justify-content:center;line-height:1}:host ::ng-deep .praxis-settings-panel-backdrop{background:var(--pfx-backdrop, var(--md-sys-color-scrim, rgba(0, 0, 0, .45)));backdrop-filter:blur(var(--pfx-backdrop-blur, 6px)) saturate(110%);-webkit-backdrop-filter:blur(var(--pfx-backdrop-blur, 6px)) saturate(110%)}.settings-panel .mat-divider{background-color:var(--md-sys-color-outline-variant)!important}.settings-panel .mat-expansion-panel{background:var(--md-sys-color-surface-container);border:1px solid var(--md-sys-color-outline-variant);border-radius:var(--md-sys-shape-corner-medium, 12px);box-shadow:var(--md-sys-elevation-level1, 0 2px 8px rgba(0, 0, 0, .08))}.settings-panel .mat-expansion-panel-header{background:var(--md-sys-color-surface-container);color:var(--md-sys-color-on-surface)}.settings-panel .mat-expansion-panel-header mat-icon,.settings-panel .mat-expansion-panel-header .mat-icon{color:var(--md-sys-color-on-surface)!important}.settings-panel .mat-expansion-panel-header .mat-expansion-panel-header-title,.settings-panel .mat-expansion-panel-header .mat-expansion-panel-header-description{color:var(--md-sys-color-on-surface)}.settings-panel .mat-expansion-panel-header .mat-expansion-indicator,.settings-panel .mat-expansion-panel-header .mat-expansion-indicator:after{color:var(--md-sys-color-on-surface-variant);border-color:var(--md-sys-color-on-surface-variant)}:host ::ng-deep .settings-panel .mat-expansion-panel .mat-expansion-panel-body{padding:8px}.settings-panel .mat-mdc-tab-group .mat-mdc-tab-header{border-bottom:1px solid var(--md-sys-color-outline-variant)}.settings-panel .mat-mdc-tab-group .mdc-tab .mdc-tab__text-label{color:var(--md-sys-color-on-surface-variant)}.settings-panel .mat-mdc-tab-group .mdc-tab.mdc-tab--active .mdc-tab__text-label{color:var(--md-sys-color-on-surface)}.settings-panel .mat-mdc-tab-group .mdc-tab-indicator__content--underline{border-color:var(--md-sys-color-primary)}.settings-panel .mat-mdc-form-field{--mdc-outlined-text-field-outline-color: var(--md-sys-color-outline-variant);--mdc-outlined-text-field-hover-outline-color: var(--md-sys-color-secondary, var(--md-sys-color-primary));--mdc-outlined-text-field-focus-outline-color: var(--md-sys-color-primary)}:host ::ng-deep .praxis-settings-panel-pane{position:fixed!important;top:0!important;right:0!important;height:100vh!important;z-index:1000}:host ::ng-deep .praxis-settings-panel-pane .settings-panel{pointer-events:auto}\n"] }]
277
258
  }], ctorParameters: () => [{ type: i0.ChangeDetectorRef }, { type: i1.MatDialog }], propDecorators: { contentHost: [{
278
259
  type: ViewChild,
279
260
  args: ['contentHost', { read: ViewContainerRef, static: true }]
@@ -305,15 +286,6 @@ class SettingsPanelRef {
305
286
  * editor's `onSave()` method so that consumers can persist the new settings.
306
287
  */
307
288
  save(value) {
308
- try {
309
- const type = value == null ? 'nullish' : Array.isArray(value) ? 'array' : typeof value;
310
- const keys = value && typeof value === 'object' ? Object.keys(value) : undefined;
311
- const keyTypes = value && typeof value === 'object'
312
- ? Object.fromEntries(Object.entries(value).map(([k, v]) => [k, Array.isArray(v) ? `array(${v.length})` : (v === null ? 'null' : typeof v)]))
313
- : undefined;
314
- (console.log || console.debug)('[SettingsPanelRef] save()', { type, keysCount: Array.isArray(keys) ? keys.length : undefined, keyTypes });
315
- }
316
- catch { }
317
289
  this.savedSubject.next(value);
318
290
  this.close('save');
319
291
  }
@@ -373,6 +345,7 @@ class SettingsPanelService {
373
345
  const panelRef = overlayRef.attach(panelPortal);
374
346
  panelRef.instance.title = config.title;
375
347
  panelRef.instance.titleIcon = config.titleIcon;
348
+ panelRef.instance.expanded = config.expanded || false;
376
349
  const inputs = config.content.inputs;
377
350
  try {
378
351
  const dbg = inputs && inputs.page ? { hasPage: true, conns: Array.isArray(inputs.page.connections) ? inputs.page.connections.length : undefined } : { hasPage: false };
@@ -405,10 +378,10 @@ class SettingsPanelService {
405
378
  this.currentRef?.close(reason);
406
379
  this.currentRef = undefined;
407
380
  }
408
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.1.4", ngImport: i0, type: SettingsPanelService, deps: [{ token: i1$1.Overlay }, { token: i0.Injector }], target: i0.ɵɵFactoryTarget.Injectable });
409
- static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.1.4", ngImport: i0, type: SettingsPanelService, providedIn: 'root' });
381
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.17", ngImport: i0, type: SettingsPanelService, deps: [{ token: i1$1.Overlay }, { token: i0.Injector }], target: i0.ɵɵFactoryTarget.Injectable });
382
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.17", ngImport: i0, type: SettingsPanelService, providedIn: 'root' });
410
383
  }
411
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.1.4", ngImport: i0, type: SettingsPanelService, decorators: [{
384
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImport: i0, type: SettingsPanelService, decorators: [{
412
385
  type: Injectable,
413
386
  args: [{ providedIn: 'root' }]
414
387
  }], ctorParameters: () => [{ type: i1$1.Overlay }, { type: i0.Injector }] });
@@ -427,20 +400,34 @@ class GlobalConfigAdminService {
427
400
  dynamicFields: this.global.getDynamicFields(),
428
401
  table: this.global.getTable(),
429
402
  dialog: this.global.getDialog(),
403
+ ai: this.global.get('ai'),
404
+ cache: this.global.get('cache'),
430
405
  };
431
406
  }
432
407
  /** Persist a partial update and notify listeners. */
433
408
  save(partial) {
434
- this.global.saveGlobalConfig(partial);
409
+ return this.global.saveGlobalConfig(partial);
435
410
  }
436
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.1.4", ngImport: i0, type: GlobalConfigAdminService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
437
- static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.1.4", ngImport: i0, type: GlobalConfigAdminService, providedIn: 'root' });
411
+ /** True when a tenant/global config exists in storage (overrides env defaults). */
412
+ async hasStoredConfig() {
413
+ await this.global.ready();
414
+ return this.global.hasStoredConfig();
415
+ }
416
+ /** Clear stored config for the active tenant and refresh the cache. */
417
+ clearStoredConfig() {
418
+ return this.global.clearGlobalConfig();
419
+ }
420
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.17", ngImport: i0, type: GlobalConfigAdminService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
421
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.17", ngImport: i0, type: GlobalConfigAdminService, providedIn: 'root' });
438
422
  }
439
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.1.4", ngImport: i0, type: GlobalConfigAdminService, decorators: [{
423
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImport: i0, type: GlobalConfigAdminService, decorators: [{
440
424
  type: Injectable,
441
425
  args: [{ providedIn: 'root' }]
442
426
  }] });
443
427
 
428
+ const TABLE_COMPACT_LENGTH_TOKEN$1 = '(?:0|(?:\\d+|\\d*\\.\\d+)(?:px|rem|em|%|vh|vw|svh|svw|lvh|lvw|dvh|dvw|ch|ex))';
429
+ const TABLE_COMPACT_SPACING_PATTERN = `^(?:var\\(.+\\)|calc\\(.+\\)|${TABLE_COMPACT_LENGTH_TOKEN$1}(?:\\s+${TABLE_COMPACT_LENGTH_TOKEN$1}){0,3})$`;
430
+ const TABLE_COMPACT_FONT_SIZE_PATTERN = `^(?:var\\(.+\\)|calc\\(.+\\)|${TABLE_COMPACT_LENGTH_TOKEN$1})$`;
444
431
  function safeName(path) {
445
432
  return path.replace(/[^a-zA-Z0-9_]/g, '_');
446
433
  }
@@ -581,6 +568,105 @@ function buildGlobalConfigFormConfig() {
581
568
  controlType: FieldControlType.TOGGLE,
582
569
  dataAttributes: { globalPath: 'dynamicFields.asyncSelect.useCursor' },
583
570
  },
571
+ // AI
572
+ {
573
+ name: safeName('ai.provider'),
574
+ label: 'Provedor LLM',
575
+ controlType: FieldControlType.SELECT,
576
+ selectOptions: [],
577
+ hint: 'Selecione o provedor para listar modelos e testar a chave.',
578
+ dataAttributes: { globalPath: 'ai.provider' },
579
+ },
580
+ {
581
+ name: safeName('ai.apiKey'),
582
+ label: 'Chave de API',
583
+ controlType: FieldControlType.INPUT,
584
+ hint: 'Informe a chave do provedor selecionado para testar.',
585
+ dataAttributes: { globalPath: 'ai.apiKey' },
586
+ },
587
+ {
588
+ name: safeName('ai.model'),
589
+ label: 'Modelo de IA',
590
+ controlType: FieldControlType.SEARCHABLE_SELECT,
591
+ selectOptions: [],
592
+ resourcePath: '',
593
+ dataAttributes: { globalPath: 'ai.model' },
594
+ },
595
+ {
596
+ name: safeName('ai.temperature'),
597
+ label: 'Temperatura (Criatividade)',
598
+ controlType: FieldControlType.NUMERIC_TEXT_BOX,
599
+ hint: '0.0 (determinístico) a 1.0 (criativo)',
600
+ dataAttributes: { globalPath: 'ai.temperature' },
601
+ },
602
+ {
603
+ name: safeName('ai.maxTokens'),
604
+ label: 'Max tokens',
605
+ controlType: FieldControlType.NUMERIC_TEXT_BOX,
606
+ hint: 'Limite maximo de tokens na resposta.',
607
+ dataAttributes: { globalPath: 'ai.maxTokens' },
608
+ },
609
+ {
610
+ name: safeName('ai.riskPolicy'),
611
+ label: 'Assistente: validação de risco',
612
+ controlType: FieldControlType.SELECT,
613
+ selectOptions: [
614
+ { text: 'Estrito (recomendado)', value: 'strict' },
615
+ { text: 'Padrão', value: 'standard' },
616
+ ],
617
+ hint: 'Estrito exige confirmação em risco médio/alto. Padrão segue a indicação retornada pelo backend.',
618
+ dataAttributes: { globalPath: 'ai.riskPolicy' },
619
+ },
620
+ {
621
+ name: safeName('ai.embedding.useSameAsLlm'),
622
+ label: 'Embeddings: usar mesmo LLM',
623
+ controlType: FieldControlType.TOGGLE,
624
+ hint: 'Replica provedor e chave do LLM para embeddings.',
625
+ dataAttributes: { globalPath: 'ai.embedding.useSameAsLlm' },
626
+ },
627
+ {
628
+ name: safeName('ai.embedding.provider'),
629
+ label: 'Embeddings: provedor',
630
+ controlType: FieldControlType.SELECT,
631
+ selectOptions: [],
632
+ hint: 'Provedor usado para gerar embeddings (RAG).',
633
+ dataAttributes: { globalPath: 'ai.embedding.provider' },
634
+ },
635
+ {
636
+ name: safeName('ai.embedding.apiKey'),
637
+ label: 'Embeddings: chave de API',
638
+ controlType: FieldControlType.INPUT,
639
+ hint: 'Chave do provedor de embeddings.',
640
+ dataAttributes: { globalPath: 'ai.embedding.apiKey' },
641
+ },
642
+ {
643
+ name: safeName('ai.embedding.model'),
644
+ label: 'Embeddings: modelo',
645
+ controlType: FieldControlType.SEARCHABLE_SELECT,
646
+ selectOptions: [
647
+ { text: 'text-embedding-3-large', value: 'text-embedding-3-large' },
648
+ { text: 'text-embedding-3-small', value: 'text-embedding-3-small' },
649
+ { text: 'text-embedding-ada-002 (legacy)', value: 'text-embedding-ada-002' },
650
+ ],
651
+ emptyOptionText: 'Padrão do provedor',
652
+ hint: 'Modelos OpenAI para embeddings.',
653
+ dataAttributes: { globalPath: 'ai.embedding.model' },
654
+ },
655
+ {
656
+ name: safeName('ai.embedding.dimensions'),
657
+ label: 'Embeddings: dimensoes',
658
+ controlType: FieldControlType.NUMERIC_TEXT_BOX,
659
+ hint: 'Dimensoes do vetor de embedding (default 768).',
660
+ dataAttributes: { globalPath: 'ai.embedding.dimensions' },
661
+ },
662
+ // Cache
663
+ {
664
+ name: safeName('cache.disableSchemaCache'),
665
+ label: 'Desabilitar cache de schema (LocalStorage)',
666
+ controlType: FieldControlType.TOGGLE,
667
+ hint: 'Se ativo, sempre baixa o schema do servidor (ignora cache local). Útil para desenvolvimento.',
668
+ dataAttributes: { globalPath: 'cache.disableSchemaCache' },
669
+ },
584
670
  // Table
585
671
  {
586
672
  name: safeName('table.behavior.pagination.enabled'),
@@ -649,12 +735,48 @@ function buildGlobalConfigFormConfig() {
649
735
  label: 'Aparência: densidade',
650
736
  controlType: FieldControlType.SELECT,
651
737
  selectOptions: [
652
- { text: 'comfortable', value: 'comfortable' },
653
- { text: 'cozy', value: 'cozy' },
654
738
  { text: 'compact', value: 'compact' },
739
+ { text: 'comfortable', value: 'comfortable' },
740
+ { text: 'spacious', value: 'spacious' },
655
741
  ],
656
742
  dataAttributes: { globalPath: 'table.appearance.density' },
657
743
  },
744
+ {
745
+ name: safeName('table.appearance.spacing.cellPadding'),
746
+ label: 'Aparência: padding das células',
747
+ controlType: FieldControlType.INPUT,
748
+ hint: 'Aceita 1 a 4 medidas CSS com unidade, var(...) ou calc(...). Ex.: 6px 12px',
749
+ pattern: TABLE_COMPACT_SPACING_PATTERN,
750
+ patternMessage: 'Use 1 a 4 medidas CSS válidas, como 6px 12px.',
751
+ dataAttributes: { globalPath: 'table.appearance.spacing.cellPadding' },
752
+ },
753
+ {
754
+ name: safeName('table.appearance.spacing.headerPadding'),
755
+ label: 'Aparência: padding do cabeçalho',
756
+ controlType: FieldControlType.INPUT,
757
+ hint: 'Aceita 1 a 4 medidas CSS com unidade, var(...) ou calc(...). Ex.: 8px 12px',
758
+ pattern: TABLE_COMPACT_SPACING_PATTERN,
759
+ patternMessage: 'Use 1 a 4 medidas CSS válidas, como 8px 12px.',
760
+ dataAttributes: { globalPath: 'table.appearance.spacing.headerPadding' },
761
+ },
762
+ {
763
+ name: safeName('table.appearance.typography.fontSize'),
764
+ label: 'Aparência: fonte das células',
765
+ controlType: FieldControlType.INPUT,
766
+ hint: 'Aceita uma medida CSS com unidade, var(...) ou calc(...). Ex.: 13px',
767
+ pattern: TABLE_COMPACT_FONT_SIZE_PATTERN,
768
+ patternMessage: 'Use uma medida CSS válida, como 13px.',
769
+ dataAttributes: { globalPath: 'table.appearance.typography.fontSize' },
770
+ },
771
+ {
772
+ name: safeName('table.appearance.typography.headerFontSize'),
773
+ label: 'Aparência: fonte do cabeçalho',
774
+ controlType: FieldControlType.INPUT,
775
+ hint: 'Aceita uma medida CSS com unidade, var(...) ou calc(...). Ex.: 13px',
776
+ pattern: TABLE_COMPACT_FONT_SIZE_PATTERN,
777
+ patternMessage: 'Use uma medida CSS válida, como 13px.',
778
+ dataAttributes: { globalPath: 'table.appearance.typography.headerFontSize' },
779
+ },
658
780
  {
659
781
  name: safeName('table.filteringUi.advancedOpenMode'),
660
782
  label: 'Filtro: modo de abertura',
@@ -713,12 +835,21 @@ function buildGlobalConfigFormConfig() {
713
835
  ],
714
836
  dataAttributes: { globalPath: 'dialog.defaults.confirm.ariaRole' },
715
837
  },
838
+ { name: safeName('dialog.defaults.confirm.title'), label: 'Dialog Defaults: confirm — título', controlType: FieldControlType.INPUT, dataAttributes: { globalPath: 'dialog.defaults.confirm.title' } },
839
+ { name: safeName('dialog.defaults.confirm.icon'), label: 'Dialog Defaults: confirm — ícone', controlType: FieldControlType.INPUT, dataAttributes: { globalPath: 'dialog.defaults.confirm.icon' } },
840
+ { name: safeName('dialog.defaults.confirm.message'), label: 'Dialog Defaults: confirm — mensagem', controlType: FieldControlType.TEXTAREA, dataAttributes: { globalPath: 'dialog.defaults.confirm.message' } },
841
+ { name: safeName('dialog.defaults.confirm.themeColor'), label: 'Dialog Defaults: confirm — themeColor', controlType: FieldControlType.SELECT, selectOptions: [{ text: 'light', value: 'light' }, { text: 'primary', value: 'primary' }, { text: 'dark', value: 'dark' }], dataAttributes: { globalPath: 'dialog.defaults.confirm.themeColor' } },
716
842
  {
717
843
  name: safeName('dialog.defaults.confirm.closeOnBackdropClick'),
718
844
  label: 'Dialog Defaults: confirm — fechar ao clicar no backdrop',
719
845
  controlType: FieldControlType.TOGGLE,
720
846
  dataAttributes: { globalPath: 'dialog.defaults.confirm.closeOnBackdropClick' },
721
847
  },
848
+ { name: safeName('dialog.defaults.confirm.disableClose'), label: 'Dialog Defaults: confirm — disableClose', controlType: FieldControlType.TOGGLE, dataAttributes: { globalPath: 'dialog.defaults.confirm.disableClose' } },
849
+ { name: safeName('dialog.defaults.confirm.hasBackdrop'), label: 'Dialog Defaults: confirm — hasBackdrop', controlType: FieldControlType.TOGGLE, dataAttributes: { globalPath: 'dialog.defaults.confirm.hasBackdrop' } },
850
+ { name: safeName('dialog.defaults.confirm.panelClass'), label: 'Dialog Defaults: confirm — panelClass', controlType: FieldControlType.INPUT, dataAttributes: { globalPath: 'dialog.defaults.confirm.panelClass' } },
851
+ { name: safeName('dialog.defaults.confirm.styles'), label: 'Dialog Defaults: confirm — styles (JSON)', controlType: FieldControlType.TEXTAREA, dataAttributes: { globalPath: 'dialog.defaults.confirm.styles', monospace: true }, hint: 'Ex.: {"containerColor":"var(--md-sys-color-surface)","containerBorderColor":"var(--md-sys-color-outline-variant)","backdropColor":"var(--md-sys-color-scrim)"}' },
852
+ { name: safeName('dialog.defaults.confirm.animation'), label: 'Dialog Defaults: confirm — animation (JSON/boolean)', controlType: FieldControlType.TEXTAREA, dataAttributes: { globalPath: 'dialog.defaults.confirm.animation', monospace: true } },
722
853
  {
723
854
  name: safeName('dialog.defaults.alert.ariaRole'),
724
855
  label: 'Dialog Defaults: alert — ariaRole',
@@ -729,12 +860,21 @@ function buildGlobalConfigFormConfig() {
729
860
  ],
730
861
  dataAttributes: { globalPath: 'dialog.defaults.alert.ariaRole' },
731
862
  },
863
+ { name: safeName('dialog.defaults.alert.title'), label: 'Dialog Defaults: alert — título', controlType: FieldControlType.INPUT, dataAttributes: { globalPath: 'dialog.defaults.alert.title' } },
864
+ { name: safeName('dialog.defaults.alert.icon'), label: 'Dialog Defaults: alert — ícone', controlType: FieldControlType.INPUT, dataAttributes: { globalPath: 'dialog.defaults.alert.icon' } },
865
+ { name: safeName('dialog.defaults.alert.message'), label: 'Dialog Defaults: alert — mensagem', controlType: FieldControlType.TEXTAREA, dataAttributes: { globalPath: 'dialog.defaults.alert.message' } },
866
+ { name: safeName('dialog.defaults.alert.themeColor'), label: 'Dialog Defaults: alert — themeColor', controlType: FieldControlType.SELECT, selectOptions: [{ text: 'light', value: 'light' }, { text: 'primary', value: 'primary' }, { text: 'dark', value: 'dark' }], dataAttributes: { globalPath: 'dialog.defaults.alert.themeColor' } },
732
867
  {
733
868
  name: safeName('dialog.defaults.alert.closeOnBackdropClick'),
734
869
  label: 'Dialog Defaults: alert — fechar ao clicar no backdrop',
735
870
  controlType: FieldControlType.TOGGLE,
736
871
  dataAttributes: { globalPath: 'dialog.defaults.alert.closeOnBackdropClick' },
737
872
  },
873
+ { name: safeName('dialog.defaults.alert.disableClose'), label: 'Dialog Defaults: alert — disableClose', controlType: FieldControlType.TOGGLE, dataAttributes: { globalPath: 'dialog.defaults.alert.disableClose' } },
874
+ { name: safeName('dialog.defaults.alert.hasBackdrop'), label: 'Dialog Defaults: alert — hasBackdrop', controlType: FieldControlType.TOGGLE, dataAttributes: { globalPath: 'dialog.defaults.alert.hasBackdrop' } },
875
+ { name: safeName('dialog.defaults.alert.panelClass'), label: 'Dialog Defaults: alert — panelClass', controlType: FieldControlType.INPUT, dataAttributes: { globalPath: 'dialog.defaults.alert.panelClass' } },
876
+ { name: safeName('dialog.defaults.alert.styles'), label: 'Dialog Defaults: alert — styles (JSON)', controlType: FieldControlType.TEXTAREA, dataAttributes: { globalPath: 'dialog.defaults.alert.styles', monospace: true }, hint: 'Ex.: {"containerColor":"var(--md-sys-color-surface)","containerBorderColor":"var(--md-sys-color-outline-variant)","backdropColor":"var(--md-sys-color-scrim)"}' },
877
+ { name: safeName('dialog.defaults.alert.animation'), label: 'Dialog Defaults: alert — animation (JSON/boolean)', controlType: FieldControlType.TEXTAREA, dataAttributes: { globalPath: 'dialog.defaults.alert.animation', monospace: true } },
738
878
  {
739
879
  name: safeName('dialog.defaults.prompt.ariaRole'),
740
880
  label: 'Dialog Defaults: prompt — ariaRole',
@@ -745,12 +885,21 @@ function buildGlobalConfigFormConfig() {
745
885
  ],
746
886
  dataAttributes: { globalPath: 'dialog.defaults.prompt.ariaRole' },
747
887
  },
888
+ { name: safeName('dialog.defaults.prompt.title'), label: 'Dialog Defaults: prompt — título', controlType: FieldControlType.INPUT, dataAttributes: { globalPath: 'dialog.defaults.prompt.title' } },
889
+ { name: safeName('dialog.defaults.prompt.icon'), label: 'Dialog Defaults: prompt — ícone', controlType: FieldControlType.INPUT, dataAttributes: { globalPath: 'dialog.defaults.prompt.icon' } },
890
+ { name: safeName('dialog.defaults.prompt.message'), label: 'Dialog Defaults: prompt — mensagem', controlType: FieldControlType.TEXTAREA, dataAttributes: { globalPath: 'dialog.defaults.prompt.message' } },
891
+ { name: safeName('dialog.defaults.prompt.themeColor'), label: 'Dialog Defaults: prompt — themeColor', controlType: FieldControlType.SELECT, selectOptions: [{ text: 'light', value: 'light' }, { text: 'primary', value: 'primary' }, { text: 'dark', value: 'dark' }], dataAttributes: { globalPath: 'dialog.defaults.prompt.themeColor' } },
748
892
  {
749
893
  name: safeName('dialog.defaults.prompt.closeOnBackdropClick'),
750
894
  label: 'Dialog Defaults: prompt — fechar ao clicar no backdrop',
751
895
  controlType: FieldControlType.TOGGLE,
752
896
  dataAttributes: { globalPath: 'dialog.defaults.prompt.closeOnBackdropClick' },
753
897
  },
898
+ { name: safeName('dialog.defaults.prompt.disableClose'), label: 'Dialog Defaults: prompt — disableClose', controlType: FieldControlType.TOGGLE, dataAttributes: { globalPath: 'dialog.defaults.prompt.disableClose' } },
899
+ { name: safeName('dialog.defaults.prompt.hasBackdrop'), label: 'Dialog Defaults: prompt — hasBackdrop', controlType: FieldControlType.TOGGLE, dataAttributes: { globalPath: 'dialog.defaults.prompt.hasBackdrop' } },
900
+ { name: safeName('dialog.defaults.prompt.panelClass'), label: 'Dialog Defaults: prompt — panelClass', controlType: FieldControlType.INPUT, dataAttributes: { globalPath: 'dialog.defaults.prompt.panelClass' } },
901
+ { name: safeName('dialog.defaults.prompt.styles'), label: 'Dialog Defaults: prompt — styles (JSON)', controlType: FieldControlType.TEXTAREA, dataAttributes: { globalPath: 'dialog.defaults.prompt.styles', monospace: true }, hint: 'Ex.: {"containerColor":"var(--md-sys-color-surface)","containerBorderColor":"var(--md-sys-color-outline-variant)","backdropColor":"var(--md-sys-color-scrim)"}' },
902
+ { name: safeName('dialog.defaults.prompt.animation'), label: 'Dialog Defaults: prompt — animation (JSON/boolean)', controlType: FieldControlType.TEXTAREA, dataAttributes: { globalPath: 'dialog.defaults.prompt.animation', monospace: true } },
754
903
  // Dialog — Variants per profile (danger, info, success, question, error)
755
904
  // Each variant exposes commonly customized fields + JSON editors for actions/styles
756
905
  // danger
@@ -761,7 +910,7 @@ function buildGlobalConfigFormConfig() {
761
910
  { name: safeName('dialog.variants.danger.closeOnBackdropClick'), label: 'Variant danger — fechar ao clicar no backdrop', controlType: FieldControlType.TOGGLE, dataAttributes: { globalPath: 'dialog.variants.danger.closeOnBackdropClick' } },
762
911
  { name: safeName('dialog.variants.danger.panelClass'), label: 'Variant danger — panelClass', controlType: FieldControlType.INPUT, dataAttributes: { globalPath: 'dialog.variants.danger.panelClass' } },
763
912
  { name: safeName('dialog.variants.danger.actions'), label: 'Variant danger — actions (JSON array)', controlType: FieldControlType.TEXTAREA, dataAttributes: { globalPath: 'dialog.variants.danger.actions', monospace: true }, hint: 'Ex.: [{"id":"cancel","text":"Cancelar","role":"secondary","close":true,"cssClass":"btn"},{"id":"confirm","text":"Excluir","role":"primary","close":true,"cssClass":"btn btn-danger","icon":"delete"}]' },
764
- { name: safeName('dialog.variants.danger.styles'), label: 'Variant danger — styles (JSON)', controlType: FieldControlType.TEXTAREA, dataAttributes: { globalPath: 'dialog.variants.danger.styles', monospace: true } },
913
+ { name: safeName('dialog.variants.danger.styles'), label: 'Variant danger — styles (JSON)', controlType: FieldControlType.TEXTAREA, dataAttributes: { globalPath: 'dialog.variants.danger.styles', monospace: true }, hint: 'Ex.: {"containerColor":"var(--md-sys-color-surface)","containerBorderColor":"var(--md-sys-color-outline-variant)","backdropColor":"var(--md-sys-color-scrim)","actionButtonRadius":"8px"}' },
765
914
  { name: safeName('dialog.variants.danger.animation'), label: 'Variant danger — animation (JSON/boolean)', controlType: FieldControlType.TEXTAREA, dataAttributes: { globalPath: 'dialog.variants.danger.animation', monospace: true } },
766
915
  // info
767
916
  { name: safeName('dialog.variants.info.title'), label: 'Variant info — título', controlType: FieldControlType.INPUT, dataAttributes: { globalPath: 'dialog.variants.info.title' } },
@@ -771,7 +920,7 @@ function buildGlobalConfigFormConfig() {
771
920
  { name: safeName('dialog.variants.info.closeOnBackdropClick'), label: 'Variant info — fechar ao clicar no backdrop', controlType: FieldControlType.TOGGLE, dataAttributes: { globalPath: 'dialog.variants.info.closeOnBackdropClick' } },
772
921
  { name: safeName('dialog.variants.info.panelClass'), label: 'Variant info — panelClass', controlType: FieldControlType.INPUT, dataAttributes: { globalPath: 'dialog.variants.info.panelClass' } },
773
922
  { name: safeName('dialog.variants.info.actions'), label: 'Variant info — actions (JSON array)', controlType: FieldControlType.TEXTAREA, dataAttributes: { globalPath: 'dialog.variants.info.actions', monospace: true } },
774
- { name: safeName('dialog.variants.info.styles'), label: 'Variant info — styles (JSON)', controlType: FieldControlType.TEXTAREA, dataAttributes: { globalPath: 'dialog.variants.info.styles', monospace: true } },
923
+ { name: safeName('dialog.variants.info.styles'), label: 'Variant info — styles (JSON)', controlType: FieldControlType.TEXTAREA, dataAttributes: { globalPath: 'dialog.variants.info.styles', monospace: true }, hint: 'Ex.: {"containerColor":"var(--md-sys-color-surface)","containerBorderColor":"var(--md-sys-color-outline-variant)","backdropColor":"var(--md-sys-color-scrim)","actionButtonRadius":"8px"}' },
775
924
  { name: safeName('dialog.variants.info.animation'), label: 'Variant info — animation (JSON/boolean)', controlType: FieldControlType.TEXTAREA, dataAttributes: { globalPath: 'dialog.variants.info.animation', monospace: true } },
776
925
  // success
777
926
  { name: safeName('dialog.variants.success.title'), label: 'Variant success — título', controlType: FieldControlType.INPUT, dataAttributes: { globalPath: 'dialog.variants.success.title' } },
@@ -781,7 +930,7 @@ function buildGlobalConfigFormConfig() {
781
930
  { name: safeName('dialog.variants.success.closeOnBackdropClick'), label: 'Variant success — fechar ao clicar no backdrop', controlType: FieldControlType.TOGGLE, dataAttributes: { globalPath: 'dialog.variants.success.closeOnBackdropClick' } },
782
931
  { name: safeName('dialog.variants.success.panelClass'), label: 'Variant success — panelClass', controlType: FieldControlType.INPUT, dataAttributes: { globalPath: 'dialog.variants.success.panelClass' } },
783
932
  { name: safeName('dialog.variants.success.actions'), label: 'Variant success — actions (JSON array)', controlType: FieldControlType.TEXTAREA, dataAttributes: { globalPath: 'dialog.variants.success.actions', monospace: true } },
784
- { name: safeName('dialog.variants.success.styles'), label: 'Variant success — styles (JSON)', controlType: FieldControlType.TEXTAREA, dataAttributes: { globalPath: 'dialog.variants.success.styles', monospace: true } },
933
+ { name: safeName('dialog.variants.success.styles'), label: 'Variant success — styles (JSON)', controlType: FieldControlType.TEXTAREA, dataAttributes: { globalPath: 'dialog.variants.success.styles', monospace: true }, hint: 'Ex.: {"containerColor":"var(--md-sys-color-surface)","containerBorderColor":"var(--md-sys-color-outline-variant)","backdropColor":"var(--md-sys-color-scrim)","actionButtonRadius":"8px"}' },
785
934
  { name: safeName('dialog.variants.success.animation'), label: 'Variant success — animation (JSON/boolean)', controlType: FieldControlType.TEXTAREA, dataAttributes: { globalPath: 'dialog.variants.success.animation', monospace: true } },
786
935
  // question
787
936
  { name: safeName('dialog.variants.question.title'), label: 'Variant question — título', controlType: FieldControlType.INPUT, dataAttributes: { globalPath: 'dialog.variants.question.title' } },
@@ -791,7 +940,7 @@ function buildGlobalConfigFormConfig() {
791
940
  { name: safeName('dialog.variants.question.closeOnBackdropClick'), label: 'Variant question — fechar ao clicar no backdrop', controlType: FieldControlType.TOGGLE, dataAttributes: { globalPath: 'dialog.variants.question.closeOnBackdropClick' } },
792
941
  { name: safeName('dialog.variants.question.panelClass'), label: 'Variant question — panelClass', controlType: FieldControlType.INPUT, dataAttributes: { globalPath: 'dialog.variants.question.panelClass' } },
793
942
  { name: safeName('dialog.variants.question.actions'), label: 'Variant question — actions (JSON array)', controlType: FieldControlType.TEXTAREA, dataAttributes: { globalPath: 'dialog.variants.question.actions', monospace: true } },
794
- { name: safeName('dialog.variants.question.styles'), label: 'Variant question — styles (JSON)', controlType: FieldControlType.TEXTAREA, dataAttributes: { globalPath: 'dialog.variants.question.styles', monospace: true } },
943
+ { name: safeName('dialog.variants.question.styles'), label: 'Variant question — styles (JSON)', controlType: FieldControlType.TEXTAREA, dataAttributes: { globalPath: 'dialog.variants.question.styles', monospace: true }, hint: 'Ex.: {"containerColor":"var(--md-sys-color-surface)","containerBorderColor":"var(--md-sys-color-outline-variant)","backdropColor":"var(--md-sys-color-scrim)","actionButtonRadius":"8px"}' },
795
944
  { name: safeName('dialog.variants.question.animation'), label: 'Variant question — animation (JSON/boolean)', controlType: FieldControlType.TEXTAREA, dataAttributes: { globalPath: 'dialog.variants.question.animation', monospace: true } },
796
945
  // error
797
946
  { name: safeName('dialog.variants.error.title'), label: 'Variant error — título', controlType: FieldControlType.INPUT, dataAttributes: { globalPath: 'dialog.variants.error.title' } },
@@ -801,7 +950,7 @@ function buildGlobalConfigFormConfig() {
801
950
  { name: safeName('dialog.variants.error.closeOnBackdropClick'), label: 'Variant error — fechar ao clicar no backdrop', controlType: FieldControlType.TOGGLE, dataAttributes: { globalPath: 'dialog.variants.error.closeOnBackdropClick' } },
802
951
  { name: safeName('dialog.variants.error.panelClass'), label: 'Variant error — panelClass', controlType: FieldControlType.INPUT, dataAttributes: { globalPath: 'dialog.variants.error.panelClass' } },
803
952
  { name: safeName('dialog.variants.error.actions'), label: 'Variant error — actions (JSON array)', controlType: FieldControlType.TEXTAREA, dataAttributes: { globalPath: 'dialog.variants.error.actions', monospace: true } },
804
- { name: safeName('dialog.variants.error.styles'), label: 'Variant error — styles (JSON)', controlType: FieldControlType.TEXTAREA, dataAttributes: { globalPath: 'dialog.variants.error.styles', monospace: true } },
953
+ { name: safeName('dialog.variants.error.styles'), label: 'Variant error — styles (JSON)', controlType: FieldControlType.TEXTAREA, dataAttributes: { globalPath: 'dialog.variants.error.styles', monospace: true }, hint: 'Ex.: {"containerColor":"var(--md-sys-color-surface)","containerBorderColor":"var(--md-sys-color-outline-variant)","backdropColor":"var(--md-sys-color-scrim)","actionButtonRadius":"8px"}' },
805
954
  { name: safeName('dialog.variants.error.animation'), label: 'Variant error — animation (JSON/boolean)', controlType: FieldControlType.TEXTAREA, dataAttributes: { globalPath: 'dialog.variants.error.animation', monospace: true } },
806
955
  ];
807
956
  const cfg = {
@@ -854,6 +1003,58 @@ function buildGlobalConfigFormConfig() {
854
1003
  },
855
1004
  ],
856
1005
  },
1006
+ {
1007
+ id: 'ai-credentials',
1008
+ title: 'Credenciais', // Internal title, UI might override
1009
+ rows: [
1010
+ {
1011
+ id: 'ai-cred-row-1',
1012
+ columns: [
1013
+ { id: 'ai-cred-col-1', fields: [safeName('ai.provider')] },
1014
+ { id: 'ai-cred-col-2', fields: [safeName('ai.apiKey')] },
1015
+ ],
1016
+ },
1017
+ ],
1018
+ },
1019
+ {
1020
+ id: 'ai-model',
1021
+ title: 'Modelo & Comportamento',
1022
+ rows: [
1023
+ {
1024
+ id: 'ai-mod-row-1',
1025
+ columns: [
1026
+ { id: 'ai-mod-col-1', fields: [safeName('ai.model'), safeName('ai.temperature'), safeName('ai.maxTokens')] },
1027
+ { id: 'ai-mod-col-2', fields: [safeName('ai.riskPolicy')] },
1028
+ ],
1029
+ },
1030
+ ],
1031
+ },
1032
+ {
1033
+ id: 'ai-embedding',
1034
+ title: 'Embeddings',
1035
+ rows: [
1036
+ {
1037
+ id: 'ai-embed-row-1',
1038
+ columns: [
1039
+ { id: 'ai-embed-col-1', fields: [safeName('ai.embedding.useSameAsLlm')] },
1040
+ { id: 'ai-embed-col-2', fields: [safeName('ai.embedding.provider'), safeName('ai.embedding.apiKey')] },
1041
+ { id: 'ai-embed-col-3', fields: [safeName('ai.embedding.model'), safeName('ai.embedding.dimensions')] },
1042
+ ],
1043
+ },
1044
+ ],
1045
+ },
1046
+ {
1047
+ id: 'cache',
1048
+ title: 'Cache & Persistência',
1049
+ rows: [
1050
+ {
1051
+ id: 'cache-row-1',
1052
+ columns: [
1053
+ { id: 'cache-col-1', fields: [safeName('cache.disableSchemaCache')] },
1054
+ ],
1055
+ },
1056
+ ],
1057
+ },
857
1058
  {
858
1059
  id: 'table',
859
1060
  title: 'Tabela',
@@ -864,6 +1065,10 @@ function buildGlobalConfigFormConfig() {
864
1065
  { id: 'tbl-col-1', fields: [
865
1066
  safeName('table.toolbar.visible'),
866
1067
  safeName('table.appearance.density'),
1068
+ safeName('table.appearance.spacing.cellPadding'),
1069
+ safeName('table.appearance.spacing.headerPadding'),
1070
+ safeName('table.appearance.typography.fontSize'),
1071
+ safeName('table.appearance.typography.headerFontSize'),
867
1072
  safeName('table.filteringUi.advancedOpenMode'),
868
1073
  safeName('table.filteringUi.overlayVariant'),
869
1074
  safeName('table.filteringUi.overlayBackdrop'),
@@ -1047,6 +1252,33 @@ function buildGlobalConfigFormConfig() {
1047
1252
  return cfg;
1048
1253
  }
1049
1254
 
1255
+ /**
1256
+ * Optional token used by GlobalConfigEditorComponent to render forms.
1257
+ * Provide the PraxisDynamicForm component (or a compatible dynamic form)
1258
+ * to avoid creating a hard dependency from @praxisui/settings-panel to
1259
+ * @praxisui/dynamic-form.
1260
+ */
1261
+ const GLOBAL_CONFIG_DYNAMIC_FORM_COMPONENT = new InjectionToken('GLOBAL_CONFIG_DYNAMIC_FORM_COMPONENT', {
1262
+ factory: () => null,
1263
+ });
1264
+
1265
+ const TABLE_COMPACT_SPACING_PATHS = new Set([
1266
+ 'table.appearance.spacing.cellPadding',
1267
+ 'table.appearance.spacing.headerPadding',
1268
+ ]);
1269
+ const TABLE_COMPACT_FONT_SIZE_PATHS = new Set([
1270
+ 'table.appearance.typography.fontSize',
1271
+ 'table.appearance.typography.headerFontSize',
1272
+ ]);
1273
+ const TABLE_COMPACT_FIELD_LABELS = {
1274
+ 'table.appearance.spacing.cellPadding': 'padding das células',
1275
+ 'table.appearance.spacing.headerPadding': 'padding do cabeçalho',
1276
+ 'table.appearance.typography.fontSize': 'fonte das células',
1277
+ 'table.appearance.typography.headerFontSize': 'fonte do cabeçalho',
1278
+ };
1279
+ const TABLE_COMPACT_LENGTH_TOKEN = '(?:0|(?:\\d+|\\d*\\.\\d+)(?:px|rem|em|%|vh|vw|svh|svw|lvh|lvw|dvh|dvw|ch|ex))';
1280
+ const TABLE_COMPACT_SPACING_REGEX = new RegExp(`^(?:var\\(.+\\)|calc\\(.+\\)|${TABLE_COMPACT_LENGTH_TOKEN}(?:\\s+${TABLE_COMPACT_LENGTH_TOKEN}){0,3})$`, 'i');
1281
+ const TABLE_COMPACT_FONT_SIZE_REGEX = new RegExp(`^(?:var\\(.+\\)|calc\\(.+\\)|${TABLE_COMPACT_LENGTH_TOKEN})$`, 'i');
1050
1282
  class GlobalConfigEditorComponent {
1051
1283
  admin;
1052
1284
  snack;
@@ -1055,82 +1287,240 @@ class GlobalConfigEditorComponent {
1055
1287
  currentValues = {};
1056
1288
  pathMap = {};
1057
1289
  bootstrappedSections = new Set();
1058
- allSections = ['crud', 'dynamic-fields', 'table', 'dialog'];
1290
+ allSections = ['crud', 'dynamic-fields', 'cache', 'table', 'dialog', 'ai-credentials', 'ai-model', 'ai-embedding'];
1059
1291
  // SettingsPanel integration signals
1060
1292
  isDirty$ = new BehaviorSubject(false);
1061
1293
  isValid$ = new BehaviorSubject(true);
1062
1294
  isBusy$ = new BehaviorSubject(false);
1063
1295
  hostCrud;
1064
1296
  hostFields;
1297
+ hostCache;
1065
1298
  hostTable;
1066
1299
  hostDialog;
1300
+ hostAiCredentials;
1301
+ hostAiModel;
1302
+ hostAiEmbedding;
1067
1303
  destroyRef = inject(DestroyRef);
1068
1304
  iconPicker = inject(IconPickerService);
1305
+ aiApi = inject(AiBackendApiService);
1306
+ cdr = inject(ChangeDetectorRef);
1307
+ logger = inject(LoggerService);
1308
+ logContext = {
1309
+ lib: 'praxis-settings-panel',
1310
+ component: 'GlobalConfigEditorComponent',
1311
+ };
1312
+ providedDynamicFormCtor = inject(GLOBAL_CONFIG_DYNAMIC_FORM_COMPONENT, { optional: true });
1313
+ dynamicFormCtor = this.providedDynamicFormCtor ?? null;
1314
+ loggedMissingDynamicForm = false;
1069
1315
  // Guardar instância do form da seção 'dialog' para atualizar controles diretamente
1070
1316
  dialogFormInst = null;
1317
+ aiModelFormRef = null;
1318
+ aiEmbeddingFormRef = null;
1319
+ pendingModelOptions = [];
1320
+ embeddingModelOptions = [
1321
+ { text: 'text-embedding-3-large', value: 'text-embedding-3-large' },
1322
+ { text: 'text-embedding-3-small', value: 'text-embedding-3-small' },
1323
+ { text: 'text-embedding-ada-002 (legacy)', value: 'text-embedding-ada-002' },
1324
+ ];
1325
+ componentRefs = new Map();
1071
1326
  dialogVariantKeys = ['danger', 'info', 'success', 'question', 'error'];
1327
+ isTestingAi = false;
1328
+ isRefetchingModels = false;
1329
+ isClearingGlobalConfig = false;
1330
+ aiTestResult = null;
1331
+ availableModels = [];
1332
+ providers = [];
1333
+ selectedProvider = null;
1334
+ apiKeyLast4 = null;
1335
+ hasStoredApiKey = false;
1336
+ hasStoredGlobalConfig = false;
1337
+ configSourceLabel = 'Padrões do servidor (env vars)';
1338
+ // UX: API Key monitoring
1339
+ apiKeyChanged$ = new Subject();
1340
+ hasApiKey = false;
1341
+ selectedModelDetails = '';
1342
+ get hasCurrentApiKey() {
1343
+ const apiKey = this.currentValues?.[this.safeName('ai.apiKey')] || '';
1344
+ return !!apiKey;
1345
+ }
1346
+ get apiKeyStatusLabel() {
1347
+ const apiKey = this.currentValues?.[this.safeName('ai.apiKey')] || '';
1348
+ if (apiKey)
1349
+ return 'Chave informada';
1350
+ if (this.apiKeyLast4)
1351
+ return `Chave salva (****${this.apiKeyLast4})`;
1352
+ if (this.hasStoredApiKey)
1353
+ return 'Chave salva';
1354
+ return 'Sem chave salva';
1355
+ }
1072
1356
  constructor(admin, snack) {
1073
1357
  this.admin = admin;
1074
1358
  this.snack = snack;
1075
1359
  }
1076
- ngOnInit() {
1360
+ async ngOnInit() {
1077
1361
  const cfg = this.admin.getEffectiveConfig();
1078
1362
  const flat = this.flatten(cfg);
1079
1363
  this.formConfig = buildGlobalConfigFormConfig();
1080
- // Build mapping from safeName -> dot path and set defaultValue on fieldMetadata
1081
- this.pathMap = {};
1082
- for (const fm of this.formConfig.fieldMetadata || []) {
1083
- const da = fm.dataAttributes || {};
1084
- const path = da.globalPath || fm.name;
1085
- this.pathMap[fm.name] = path;
1086
- const v = flat[path];
1087
- if (v !== undefined)
1088
- fm.defaultValue = v;
1364
+ this.applyConfigSnapshot(flat, { resetForms: false });
1365
+ await this.loadProviderCatalog();
1366
+ this.applyProviderOptions();
1367
+ this.applyEmbeddingProviderOptions();
1368
+ const providerSafe = this.safeName('ai.provider');
1369
+ const apiKeySafe = this.safeName('ai.apiKey');
1370
+ const selectedProvider = this.currentValues[providerSafe] || this.resolveDefaultProvider();
1371
+ if (selectedProvider && !this.currentValues[providerSafe]) {
1372
+ this.currentValues[providerSafe] = selectedProvider;
1373
+ if (!this.initialValues[providerSafe]) {
1374
+ this.initialValues[providerSafe] = selectedProvider;
1375
+ }
1376
+ this.setFieldDefaultValue(providerSafe, selectedProvider);
1089
1377
  }
1090
- // Initial/current values in SAFE namespace (control names)
1091
- this.initialValues = {};
1092
- for (const safe of Object.keys(this.pathMap)) {
1093
- const path = this.pathMap[safe];
1094
- const v = flat[path];
1095
- if (v !== undefined)
1096
- this.initialValues[safe] = v;
1378
+ this.updateSelectedProvider(selectedProvider);
1379
+ this.updateApiKeyState(selectedProvider, this.currentValues[apiKeySafe]);
1380
+ this.syncEmbeddingDefaults(false);
1381
+ this.ensureEmbeddingModelDefaults(false);
1382
+ this.ensureEmbeddingDimensionsDefaults(false);
1383
+ await this.refreshStoredConfigState();
1384
+ // Monitor API Key changes to auto-fetch
1385
+ this.apiKeyChanged$.pipe(debounceTime(1500), // Wait for typing to stop
1386
+ distinctUntilChanged((prev, curr) => prev.apiKey === curr.apiKey && prev.provider === curr.provider), filter(({ apiKey, provider }) => this.canAutoRefreshModels(provider, apiKey))).subscribe(() => {
1387
+ this.refreshModels();
1388
+ });
1389
+ if (this.hasApiKey && this.selectedProvider?.supportsModels !== false) {
1390
+ // Auto-fetch models on load if we have a key (silent)
1391
+ this.refreshModels(false, true);
1097
1392
  }
1098
- this.currentValues = { ...this.initialValues };
1099
- // Lazy-load PraxisDynamicForm to avoid circular dependency with settings-panel
1100
- import('@praxisui/dynamic-form').then((m) => {
1101
- const Comp = m.PraxisDynamicForm;
1102
- const makeGroupConfig = (sectionId) => {
1103
- const sec = (this.formConfig.sections || []).find((s) => s.id === sectionId);
1104
- if (!sec)
1105
- return { sections: [], fieldMetadata: [] };
1106
- // Collect field names in this section
1107
- const names = new Set();
1108
- for (const row of sec.rows || []) {
1109
- for (const col of row.columns || []) {
1110
- for (const fname of col.fields || [])
1111
- names.add(fname);
1393
+ this.bootstrapDynamicForms();
1394
+ }
1395
+ ngAfterViewInit() {
1396
+ this.ensureAiModelForm();
1397
+ }
1398
+ onValueChange(sectionId, ev) {
1399
+ this.isValid$.next(!!ev.isValid);
1400
+ // Mesclar valores por seção (cada form emite somente seu grupo)
1401
+ this.currentValues = { ...this.currentValues, ...ev.formData };
1402
+ if (sectionId === 'ai-credentials') {
1403
+ const apiKeySafe = this.safeName('ai.apiKey');
1404
+ const providerSafe = this.safeName('ai.provider');
1405
+ const apiKey = ev.formData[apiKeySafe];
1406
+ const provider = ev.formData[providerSafe] || this.currentValues[providerSafe] || this.resolveDefaultProvider();
1407
+ const providerChanged = provider !== this.selectedProvider?.id;
1408
+ if (providerChanged) {
1409
+ this.updateSelectedProvider(provider);
1410
+ this.aiTestResult = null;
1411
+ this.availableModels = [];
1412
+ this.pendingModelOptions = [];
1413
+ this.selectedModelDetails = '';
1414
+ this.clearModelSelection(this.selectedProvider?.defaultModel);
1415
+ this.applyEmbeddingProviderOptions();
1416
+ }
1417
+ this.updateApiKeyState(provider, apiKey);
1418
+ this.syncEmbeddingDefaults();
1419
+ this.ensureEmbeddingModelDefaults();
1420
+ this.ensureEmbeddingDimensionsDefaults();
1421
+ if (apiKey !== this.initialValues[apiKeySafe] || providerChanged) {
1422
+ this.apiKeyChanged$.next({ apiKey: apiKey || '', provider });
1423
+ // Reset test result when key changes
1424
+ if (this.aiTestResult) {
1425
+ this.aiTestResult = null;
1426
+ }
1427
+ }
1428
+ if (this.hasApiKey) {
1429
+ setTimeout(() => this.ensureAiModelForm(), 0);
1430
+ }
1431
+ else {
1432
+ if (this.aiModelFormRef) {
1433
+ try {
1434
+ this.aiModelFormRef.destroy();
1112
1435
  }
1436
+ catch { }
1437
+ this.componentRefs.delete('ai-model');
1438
+ this.aiModelFormRef = null;
1113
1439
  }
1114
- const fms = (this.formConfig.fieldMetadata || []).filter((f) => names.has(f.name));
1115
- return { sections: [sec], fieldMetadata: fms };
1116
- };
1117
- const groups = [
1118
- { id: 'crud', host: this.hostCrud, cfg: makeGroupConfig('crud') },
1119
- { id: 'dynamic-fields', host: this.hostFields, cfg: makeGroupConfig('dynamic-fields') },
1120
- { id: 'table', host: this.hostTable, cfg: makeGroupConfig('table') },
1121
- { id: 'dialog', host: this.hostDialog, cfg: makeGroupConfig('dialog') },
1122
- ];
1123
- for (const g of groups) {
1124
- const ref = g.host.createComponent(Comp);
1440
+ this.availableModels = [];
1441
+ this.pendingModelOptions = [];
1442
+ }
1443
+ }
1444
+ if (sectionId === 'ai-model') {
1445
+ const modelSafe = this.safeName('ai.model');
1446
+ const model = ev.formData[modelSafe];
1447
+ this.updateModelDetails(model);
1448
+ }
1449
+ if (sectionId === 'ai-embedding') {
1450
+ this.syncEmbeddingDefaults();
1451
+ this.ensureEmbeddingModelDefaults();
1452
+ this.ensureEmbeddingDimensionsDefaults();
1453
+ }
1454
+ const firstTime = !this.bootstrappedSections.has(sectionId);
1455
+ if (firstTime) {
1456
+ this.initialValues = { ...this.initialValues, ...ev.formData };
1457
+ this.bootstrappedSections.add(sectionId);
1458
+ }
1459
+ this.isDirty$.next(!this.shallowEqual(this.currentValues, this.initialValues));
1460
+ }
1461
+ buildSectionConfig(sectionId) {
1462
+ const sec = (this.formConfig.sections || []).find((s) => s.id === sectionId);
1463
+ if (!sec)
1464
+ return { sections: [], fieldMetadata: [] };
1465
+ const names = new Set();
1466
+ for (const row of sec.rows || []) {
1467
+ for (const col of row.columns || []) {
1468
+ for (const fname of col.fields || [])
1469
+ names.add(fname);
1470
+ }
1471
+ }
1472
+ const fms = (this.formConfig.fieldMetadata || []).filter((f) => names.has(f.name));
1473
+ return { sections: [sec], fieldMetadata: fms };
1474
+ }
1475
+ bootstrapDynamicForms() {
1476
+ if (!this.dynamicFormCtor) {
1477
+ if (!this.loggedMissingDynamicForm) {
1478
+ this.logger.warnOnce('[GlobalConfigEditor] Dynamic form component was not provided. Set GLOBAL_CONFIG_DYNAMIC_FORM_COMPONENT to enable form rendering.', this.buildLogOptions({ token: 'GLOBAL_CONFIG_DYNAMIC_FORM_COMPONENT' }, { dedupeKey: 'global-config-editor:missing-dynamic-form' }));
1479
+ this.loggedMissingDynamicForm = true;
1480
+ }
1481
+ return;
1482
+ }
1483
+ const groups = [
1484
+ { id: 'crud', host: this.hostCrud, cfg: this.buildSectionConfig('crud') },
1485
+ { id: 'dynamic-fields', host: this.hostFields, cfg: this.buildSectionConfig('dynamic-fields') },
1486
+ { id: 'cache', host: this.hostCache, cfg: this.buildSectionConfig('cache') },
1487
+ { id: 'table', host: this.hostTable, cfg: this.buildSectionConfig('table') },
1488
+ { id: 'dialog', host: this.hostDialog, cfg: this.buildSectionConfig('dialog') },
1489
+ { id: 'ai-credentials', host: this.hostAiCredentials, cfg: this.buildSectionConfig('ai-credentials') },
1490
+ { id: 'ai-embedding', host: this.hostAiEmbedding, cfg: this.buildSectionConfig('ai-embedding') },
1491
+ ];
1492
+ for (const g of groups) {
1493
+ try {
1494
+ if (!g.host) {
1495
+ this.logger.warnOnce('[GlobalConfigEditor] Missing host for section', this.buildLogOptions({ sectionId: g.id }, {
1496
+ dedupeKey: `global-config-editor:missing-host:${g.id}`,
1497
+ context: { actionId: `section.${g.id}` },
1498
+ }));
1499
+ continue;
1500
+ }
1501
+ this.logger.debug('[GlobalConfigEditor] Creating form section', this.buildLogOptions({ sectionId: g.id, config: g.cfg }, {
1502
+ context: { actionId: `section.${g.id}` },
1503
+ throttleKey: 'global-config-editor:create-form-section',
1504
+ }));
1505
+ const ref = g.host.createComponent(this.dynamicFormCtor);
1125
1506
  ref.setInput('config', g.cfg);
1126
1507
  ref.setInput('mode', 'edit');
1127
- ref.setInput('editModeEnabled', false);
1508
+ // Não habilitar o modo de edição de layout (canvas) — queremos apenas edição de valores
1509
+ ref.setInput('enableCustomization', false);
1128
1510
  const inst = ref.instance;
1511
+ // FORÇAR estado de sucesso para renderizar o formulário sem resourcePath
1512
+ inst.initializationStatus = 'success';
1513
+ inst.isLoading = false;
1129
1514
  try {
1130
1515
  if (typeof inst.buildFormFromConfig === 'function')
1131
1516
  inst.buildFormFromConfig();
1132
1517
  }
1133
- catch { }
1518
+ catch (err) {
1519
+ this.logger.warnOnce('[GlobalConfigEditor] buildFormFromConfig failed', this.buildLogOptions({ sectionId: g.id, error: err }, {
1520
+ context: { actionId: `section.${g.id}.build-form` },
1521
+ dedupeKey: `global-config-editor:build-form-failed:${g.id}`,
1522
+ }));
1523
+ }
1134
1524
  try {
1135
1525
  inst.onSubmit = () => { };
1136
1526
  }
@@ -1142,35 +1532,425 @@ class GlobalConfigEditorComponent {
1142
1532
  if (inst?.valueChange?.subscribe) {
1143
1533
  inst.valueChange.subscribe((ev) => this.onValueChange(g.id, ev));
1144
1534
  }
1535
+ this.componentRefs.set(g.id, ref);
1145
1536
  if (g.id === 'dialog') {
1146
1537
  // Guardar referência para facilitar setValue em campos de ícone
1147
1538
  this.dialogFormInst = inst;
1148
1539
  }
1540
+ if (g.id === 'ai-embedding') {
1541
+ this.aiEmbeddingFormRef = ref;
1542
+ }
1149
1543
  this.destroyRef.onDestroy(() => { try {
1150
1544
  ref.destroy();
1151
1545
  }
1152
1546
  catch { } });
1153
1547
  }
1154
- });
1548
+ catch (err) {
1549
+ this.logger.error('[GlobalConfigEditor] Failed to create form section', this.buildLogOptions({ sectionId: g.id, error: err }, { context: { actionId: `section.${g.id}.create` } }));
1550
+ }
1551
+ }
1552
+ // Criar a seção de modelo somente quando a chave estiver disponível e o host existir
1553
+ this.ensureAiModelForm();
1155
1554
  }
1156
- onValueChange(sectionId, ev) {
1157
- this.isValid$.next(!!ev.isValid);
1158
- // Mesclar valores por seção (cada form emite somente seu grupo)
1159
- this.currentValues = { ...this.currentValues, ...ev.formData };
1160
- // Durante o bootstrap, coletar baseline de todas as seções
1161
- if (!this.hasBootstrappedAll()) {
1162
- this.initialValues = { ...this.initialValues, ...ev.formData };
1163
- this.bootstrappedSections.add(sectionId);
1164
- if (this.hasBootstrappedAll()) {
1165
- // Após todas as seções emitirem, marcar como limpo
1166
- this.isDirty$.next(false);
1555
+ ensureAiModelForm() {
1556
+ if (!this.hasApiKey)
1557
+ return;
1558
+ if (!this.aiModelFormRef) {
1559
+ this.createAiModelForm();
1560
+ }
1561
+ }
1562
+ async loadProviderCatalog() {
1563
+ try {
1564
+ const response = await firstValueFrom(this.aiApi.listProviderCatalog());
1565
+ const providers = Array.isArray(response?.providers) ? response.providers : [];
1566
+ this.providers = providers.filter((p) => p && p.id);
1567
+ }
1568
+ catch (err) {
1569
+ this.logger.warnOnce('[GlobalConfigEditor] Failed to load provider catalog, using fallback.', this.buildLogOptions({ error: err }, {
1570
+ context: { actionId: 'providers.load-catalog' },
1571
+ dedupeKey: 'global-config-editor:provider-catalog-fallback',
1572
+ }));
1573
+ this.providers = this.buildFallbackProviders();
1574
+ }
1575
+ }
1576
+ buildFallbackProviders() {
1577
+ return [
1578
+ {
1579
+ id: 'gemini',
1580
+ label: 'Google Gemini',
1581
+ description: 'Modelos rápidos e multimodais do Google.',
1582
+ defaultModel: 'gemini-2.0-flash',
1583
+ requiresApiKey: true,
1584
+ supportsModels: true,
1585
+ supportsEmbeddings: true,
1586
+ iconKey: 'gemini',
1587
+ },
1588
+ {
1589
+ id: 'openai',
1590
+ label: 'OpenAI',
1591
+ description: 'Modelos GPT para texto e chat.',
1592
+ defaultModel: 'gpt-4o-mini',
1593
+ requiresApiKey: true,
1594
+ supportsModels: true,
1595
+ supportsEmbeddings: true,
1596
+ iconKey: 'openai',
1597
+ },
1598
+ {
1599
+ id: 'xai',
1600
+ label: 'xAI (Grok)',
1601
+ description: 'Modelos Grok focados em raciocínio.',
1602
+ defaultModel: 'grok-2-latest',
1603
+ requiresApiKey: true,
1604
+ supportsModels: true,
1605
+ supportsEmbeddings: false,
1606
+ iconKey: 'xai',
1607
+ },
1608
+ {
1609
+ id: 'mock',
1610
+ label: 'Mock (dev)',
1611
+ description: 'Modo local para testes sem chave.',
1612
+ defaultModel: 'mock-default',
1613
+ requiresApiKey: false,
1614
+ supportsModels: true,
1615
+ supportsEmbeddings: true,
1616
+ iconKey: 'mock',
1617
+ },
1618
+ ];
1619
+ }
1620
+ applyProviderOptions() {
1621
+ const options = this.providers.map((provider) => ({
1622
+ text: provider.label || provider.id,
1623
+ value: provider.id,
1624
+ }));
1625
+ this.setProviderFieldOptions(options);
1626
+ }
1627
+ applyEmbeddingProviderOptions() {
1628
+ const options = this.providers
1629
+ .filter((provider) => provider?.supportsEmbeddings !== false || provider?.id === 'openai')
1630
+ .map((provider) => ({
1631
+ text: provider.label || provider.id,
1632
+ value: provider.id,
1633
+ }));
1634
+ this.setEmbeddingProviderFieldOptions(options);
1635
+ }
1636
+ setProviderFieldOptions(providerOptions) {
1637
+ const safeProviderName = this.safeName('ai.provider');
1638
+ const providerField = (this.formConfig.fieldMetadata || []).find((f) => f.name === safeProviderName);
1639
+ if (providerField) {
1640
+ providerField.selectOptions = providerOptions;
1641
+ }
1642
+ }
1643
+ setEmbeddingProviderFieldOptions(providerOptions) {
1644
+ const safeProviderName = this.safeName('ai.embedding.provider');
1645
+ const providerField = (this.formConfig.fieldMetadata || []).find((f) => f.name === safeProviderName);
1646
+ if (providerField) {
1647
+ providerField.selectOptions = providerOptions;
1648
+ }
1649
+ }
1650
+ resolveDefaultProvider() {
1651
+ const preferred = this.providers.find((provider) => provider.id === 'gemini') || this.providers[0];
1652
+ return preferred?.id || 'gemini';
1653
+ }
1654
+ setFieldDefaultValue(safeName, value) {
1655
+ const field = (this.formConfig.fieldMetadata || []).find((f) => f.name === safeName);
1656
+ if (field) {
1657
+ field.defaultValue = value;
1658
+ }
1659
+ }
1660
+ updateSelectedProvider(providerId) {
1661
+ this.selectedProvider = this.findProvider(providerId) || null;
1662
+ }
1663
+ updateApiKeyState(providerId, apiKey) {
1664
+ const meta = this.findProvider(providerId);
1665
+ const requiresKey = meta?.requiresApiKey !== false;
1666
+ this.hasApiKey = !requiresKey || !!apiKey || this.hasStoredApiKey;
1667
+ }
1668
+ get embeddingUseSameAsLlm() {
1669
+ return !!this.currentValues?.[this.safeName('ai.embedding.useSameAsLlm')];
1670
+ }
1671
+ get embeddingDimensionMismatch() {
1672
+ const raw = this.currentValues?.[this.safeName('ai.embedding.dimensions')];
1673
+ if (raw === undefined || raw === null || raw === '')
1674
+ return false;
1675
+ const parsed = Number(raw);
1676
+ if (!Number.isFinite(parsed))
1677
+ return false;
1678
+ return parsed !== 768;
1679
+ }
1680
+ get canUseLlmForEmbeddings() {
1681
+ return !!this.selectedProvider?.supportsEmbeddings;
1682
+ }
1683
+ canAutoRefreshModels(providerId, apiKey) {
1684
+ const meta = this.findProvider(providerId);
1685
+ if (meta?.supportsModels === false)
1686
+ return false;
1687
+ if (meta?.requiresApiKey === false)
1688
+ return true;
1689
+ if (apiKey && apiKey.length > 10)
1690
+ return true;
1691
+ return this.hasStoredApiKey;
1692
+ }
1693
+ ensureDefaultModelSelection() {
1694
+ const modelSafe = this.safeName('ai.model');
1695
+ const current = this.currentValues[modelSafe];
1696
+ if (current)
1697
+ return;
1698
+ const fallback = this.selectedProvider?.defaultModel;
1699
+ if (!fallback)
1700
+ return;
1701
+ this.setFieldDefaultValue(modelSafe, fallback);
1702
+ this.currentValues[modelSafe] = fallback;
1703
+ this.updateModelDetails(fallback);
1704
+ }
1705
+ clearModelSelection(defaultModel) {
1706
+ const modelSafe = this.safeName('ai.model');
1707
+ const nextValue = defaultModel || '';
1708
+ this.currentValues[modelSafe] = nextValue;
1709
+ this.setFieldDefaultValue(modelSafe, nextValue);
1710
+ this.updateModelDetails(nextValue);
1711
+ const inst = this.aiModelFormRef?.instance;
1712
+ const ctrl = inst?.form?.get?.(modelSafe);
1713
+ if (ctrl) {
1714
+ ctrl.setValue(nextValue);
1715
+ }
1716
+ }
1717
+ findProvider(providerId) {
1718
+ return this.providers.find((provider) => provider.id === providerId);
1719
+ }
1720
+ setModelFieldOptions(modelOptions) {
1721
+ const safeModelName = this.safeName('ai.model');
1722
+ const aiModelField = (this.formConfig.fieldMetadata || []).find((f) => f.name === safeModelName);
1723
+ if (aiModelField) {
1724
+ aiModelField.selectOptions = modelOptions;
1725
+ }
1726
+ }
1727
+ applyConfigSnapshot(flat, opts) {
1728
+ this.pathMap = {};
1729
+ for (const fm of this.formConfig.fieldMetadata || []) {
1730
+ const da = fm.dataAttributes || {};
1731
+ const path = da.globalPath || fm.name;
1732
+ this.pathMap[fm.name] = path;
1733
+ const v = this.normalizeFieldValue(path, flat[path]);
1734
+ if (v !== undefined)
1735
+ fm.defaultValue = v;
1736
+ }
1737
+ this.initialValues = {};
1738
+ for (const safe of Object.keys(this.pathMap)) {
1739
+ const path = this.pathMap[safe];
1740
+ const v = this.normalizeFieldValue(path, flat[path]);
1741
+ if (v !== undefined)
1742
+ this.initialValues[safe] = v;
1743
+ }
1744
+ this.currentValues = { ...this.initialValues };
1745
+ this.apiKeyLast4 = flat['ai.apiKeyLast4'] || null;
1746
+ this.hasStoredApiKey = !!flat['ai.hasApiKey'] || !!this.apiKeyLast4;
1747
+ if (opts.resetForms) {
1748
+ const resetValues = {};
1749
+ for (const safe of Object.keys(this.pathMap)) {
1750
+ if (safe in this.initialValues) {
1751
+ resetValues[safe] = this.initialValues[safe];
1752
+ }
1753
+ else {
1754
+ resetValues[safe] = null;
1755
+ }
1756
+ }
1757
+ this.patchFormsWithValues(resetValues);
1758
+ this.bootstrappedSections = new Set(this.allSections);
1759
+ this.isDirty$.next(false);
1760
+ }
1761
+ }
1762
+ patchFormsWithValues(values) {
1763
+ for (const ref of this.componentRefs.values()) {
1764
+ const inst = ref?.instance;
1765
+ const form = inst?.form;
1766
+ if (form?.patchValue) {
1767
+ try {
1768
+ form.patchValue(values, { emitEvent: false });
1769
+ }
1770
+ catch { }
1167
1771
  }
1772
+ }
1773
+ }
1774
+ refreshAiStateAfterConfig() {
1775
+ const providerSafe = this.safeName('ai.provider');
1776
+ const apiKeySafe = this.safeName('ai.apiKey');
1777
+ const selectedProvider = this.currentValues[providerSafe] || this.resolveDefaultProvider();
1778
+ if (selectedProvider && !this.currentValues[providerSafe]) {
1779
+ this.currentValues[providerSafe] = selectedProvider;
1780
+ if (!this.initialValues[providerSafe]) {
1781
+ this.initialValues[providerSafe] = selectedProvider;
1782
+ }
1783
+ this.setFieldDefaultValue(providerSafe, selectedProvider);
1784
+ }
1785
+ this.updateSelectedProvider(selectedProvider);
1786
+ this.updateApiKeyState(selectedProvider, this.currentValues[apiKeySafe]);
1787
+ this.syncEmbeddingDefaults(false);
1788
+ this.ensureEmbeddingModelDefaults(false);
1789
+ this.ensureEmbeddingDimensionsDefaults(false);
1790
+ this.updateModelDetails(this.currentValues[this.safeName('ai.model')] || '');
1791
+ }
1792
+ async refreshStoredConfigState() {
1793
+ try {
1794
+ this.hasStoredGlobalConfig = await this.admin.hasStoredConfig();
1795
+ }
1796
+ catch {
1797
+ this.hasStoredGlobalConfig = false;
1798
+ }
1799
+ this.configSourceLabel = this.hasStoredGlobalConfig
1800
+ ? 'Configuração salva (UI) — sobrescreve os defaults do servidor'
1801
+ : 'Padrões do servidor (env vars)';
1802
+ }
1803
+ async clearStoredConfig() {
1804
+ if (this.isClearingGlobalConfig)
1168
1805
  return;
1806
+ this.isClearingGlobalConfig = true;
1807
+ try {
1808
+ await this.admin.clearStoredConfig();
1809
+ const cfg = this.admin.getEffectiveConfig();
1810
+ const flat = this.flatten(cfg);
1811
+ this.applyConfigSnapshot(flat, { resetForms: true });
1812
+ this.aiTestResult = null;
1813
+ this.refreshAiStateAfterConfig();
1814
+ await this.refreshStoredConfigState();
1815
+ try {
1816
+ this.snack.open('Configuração global limpa. Usando defaults do servidor.', undefined, { duration: 2500 });
1817
+ }
1818
+ catch { }
1819
+ }
1820
+ catch (err) {
1821
+ try {
1822
+ this.snack.open('Falha ao limpar a configuração global.', undefined, { duration: 4000 });
1823
+ }
1824
+ catch { }
1825
+ }
1826
+ finally {
1827
+ this.isClearingGlobalConfig = false;
1828
+ this.cdr.detectChanges();
1169
1829
  }
1170
- this.isDirty$.next(!this.shallowEqual(this.currentValues, this.initialValues));
1171
1830
  }
1172
- hasBootstrappedAll() {
1173
- return this.allSections.every((s) => this.bootstrappedSections.has(s));
1831
+ syncEmbeddingDefaults(markDirty = true) {
1832
+ const useSameSafe = this.safeName('ai.embedding.useSameAsLlm');
1833
+ const embeddingProviderSafe = this.safeName('ai.embedding.provider');
1834
+ const embeddingApiKeySafe = this.safeName('ai.embedding.apiKey');
1835
+ const useSame = !!this.currentValues[useSameSafe];
1836
+ if (!useSame)
1837
+ return;
1838
+ if (!this.canUseLlmForEmbeddings) {
1839
+ this.setEmbeddingValue(useSameSafe, false, markDirty);
1840
+ return;
1841
+ }
1842
+ const llmProvider = this.currentValues[this.safeName('ai.provider')] || this.resolveDefaultProvider();
1843
+ const llmApiKey = this.currentValues[this.safeName('ai.apiKey')] || '';
1844
+ this.setEmbeddingValue(embeddingProviderSafe, llmProvider, markDirty);
1845
+ this.setEmbeddingValue(embeddingApiKeySafe, llmApiKey, markDirty);
1846
+ }
1847
+ ensureEmbeddingModelDefaults(markDirty = true) {
1848
+ const providerSafe = this.safeName('ai.embedding.provider');
1849
+ const modelSafe = this.safeName('ai.embedding.model');
1850
+ const provider = this.currentValues[providerSafe];
1851
+ const model = this.currentValues[modelSafe];
1852
+ if (provider === 'openai' && !model) {
1853
+ this.setEmbeddingValue(modelSafe, this.embeddingModelOptions[0]?.value || 'text-embedding-3-large', markDirty);
1854
+ }
1855
+ }
1856
+ ensureEmbeddingDimensionsDefaults(markDirty = true) {
1857
+ const providerSafe = this.safeName('ai.embedding.provider');
1858
+ const dimensionsSafe = this.safeName('ai.embedding.dimensions');
1859
+ const provider = this.currentValues[providerSafe];
1860
+ const dimensions = this.currentValues[dimensionsSafe];
1861
+ if (provider === 'openai' && (dimensions === undefined || dimensions === null || dimensions === '')) {
1862
+ this.setEmbeddingValue(dimensionsSafe, 768, markDirty);
1863
+ }
1864
+ }
1865
+ useLlmForEmbeddings() {
1866
+ if (!this.canUseLlmForEmbeddings)
1867
+ return;
1868
+ const useSameSafe = this.safeName('ai.embedding.useSameAsLlm');
1869
+ this.setEmbeddingValue(useSameSafe, true);
1870
+ this.syncEmbeddingDefaults();
1871
+ }
1872
+ createAiModelForm() {
1873
+ if (!this.dynamicFormCtor || !this.hostAiModel)
1874
+ return;
1875
+ const cfg = this.buildSectionConfig('ai-model');
1876
+ if (!cfg.sections?.length)
1877
+ return;
1878
+ try {
1879
+ // Apply pending options to metadata before creating
1880
+ if (this.pendingModelOptions.length) {
1881
+ this.setModelFieldOptions(this.pendingModelOptions);
1882
+ }
1883
+ const ref = this.hostAiModel.createComponent(this.dynamicFormCtor);
1884
+ ref.setInput('config', cfg);
1885
+ ref.setInput('mode', 'edit');
1886
+ ref.setInput('enableCustomization', false);
1887
+ const inst = ref.instance;
1888
+ inst.initializationStatus = 'success';
1889
+ inst.isLoading = false;
1890
+ try {
1891
+ if (typeof inst.buildFormFromConfig === 'function')
1892
+ inst.buildFormFromConfig();
1893
+ }
1894
+ catch (err) {
1895
+ this.logger.warnOnce('[GlobalConfigEditor] buildFormFromConfig failed', this.buildLogOptions({ sectionId: 'ai-model', error: err }, {
1896
+ context: { actionId: 'section.ai-model.build-form' },
1897
+ dedupeKey: 'global-config-editor:build-form-failed:ai-model',
1898
+ }));
1899
+ }
1900
+ try {
1901
+ inst.onSubmit = () => { };
1902
+ }
1903
+ catch { }
1904
+ try {
1905
+ ref.changeDetectorRef.detectChanges();
1906
+ }
1907
+ catch { }
1908
+ if (inst?.valueChange?.subscribe) {
1909
+ inst.valueChange.subscribe((ev) => this.onValueChange('ai-model', ev));
1910
+ }
1911
+ this.componentRefs.set('ai-model', ref);
1912
+ this.aiModelFormRef = ref;
1913
+ this.updateModelDetails(this.currentValues[this.safeName('ai.model')] || '');
1914
+ this.destroyRef.onDestroy(() => { try {
1915
+ ref.destroy();
1916
+ }
1917
+ catch { } });
1918
+ }
1919
+ catch (err) {
1920
+ this.logger.error('[GlobalConfigEditor] Failed to create AI model form', this.buildLogOptions({ error: err }, { context: { actionId: 'section.ai-model.create' } }));
1921
+ }
1922
+ }
1923
+ setEmbeddingValue(safeName, value, markDirty = true) {
1924
+ this.currentValues[safeName] = value;
1925
+ const inst = this.aiEmbeddingFormRef?.instance;
1926
+ const ctrl = inst?.form?.get?.(safeName);
1927
+ if (ctrl) {
1928
+ ctrl.setValue(value);
1929
+ }
1930
+ if (markDirty) {
1931
+ this.isDirty$.next(!this.shallowEqual(this.currentValues, this.initialValues));
1932
+ }
1933
+ }
1934
+ recreateAiModelForm() {
1935
+ if (this.aiModelFormRef) {
1936
+ try {
1937
+ this.aiModelFormRef.destroy();
1938
+ }
1939
+ catch { }
1940
+ this.componentRefs.delete('ai-model');
1941
+ this.aiModelFormRef = null;
1942
+ }
1943
+ this.createAiModelForm();
1944
+ }
1945
+ updateModelDetails(modelName) {
1946
+ const model = this.availableModels.find(m => m.name.endsWith(modelName));
1947
+ if (model) {
1948
+ const formatLimit = (n) => n ? (n >= 1000 ? (n / 1000).toFixed(0) + 'k' : n) : 'N/A';
1949
+ this.selectedModelDetails = `Modelo: ${model.displayName || modelName} | Tokens (in/out): ${formatLimit(model.inputTokenLimit)}/${formatLimit(model.outputTokenLimit)} | Métodos: ${model.supportedGenerationMethods?.join(', ') || 'N/A'}`;
1950
+ }
1951
+ else {
1952
+ this.selectedModelDetails = 'Detalhes do modelo não disponíveis.';
1953
+ }
1174
1954
  }
1175
1955
  // SettingsPanel expects these methods
1176
1956
  reset() {
@@ -1185,30 +1965,276 @@ class GlobalConfigEditorComponent {
1185
1965
  this.isDirty$.next(false);
1186
1966
  }
1187
1967
  getSettingsValue() {
1188
- // Map SAFE keys back to dot-paths using pathMap
1189
- const flat = {};
1190
- for (const [safe, value] of Object.entries(this.currentValues)) {
1191
- const path = this.pathMap[safe] || safe;
1192
- flat[path] = value;
1193
- }
1968
+ const flat = this.buildChangedValues();
1194
1969
  return this.toNested(flat);
1195
1970
  }
1196
- onSave() {
1971
+ async onSave() {
1197
1972
  const partial = this.getSettingsValue();
1198
- this.admin.save(partial);
1199
- // Update baseline after save
1200
- this.initialValues = { ...this.currentValues };
1201
- this.isDirty$.next(false);
1973
+ const validationMessage = this.validateCompactTableAppearancePayload(partial);
1974
+ if (validationMessage) {
1975
+ try {
1976
+ this.snack.open(validationMessage, undefined, { duration: 5000 });
1977
+ }
1978
+ catch { }
1979
+ return undefined;
1980
+ }
1981
+ this.isBusy$.next(true);
1202
1982
  try {
1203
- this.snack.open('Configurações salvas', undefined, { duration: 2000 });
1983
+ await this.admin.save(partial);
1984
+ // Update baseline after save
1985
+ this.initialValues = { ...this.currentValues };
1986
+ this.isDirty$.next(false);
1987
+ try {
1988
+ this.snack.open('Configurações salvas', undefined, { duration: 2000 });
1989
+ }
1990
+ catch { }
1991
+ return partial;
1992
+ }
1993
+ catch (err) {
1994
+ const message = this.resolveSaveErrorMessage(err);
1995
+ try {
1996
+ this.snack.open(message, undefined, { duration: 4000 });
1997
+ }
1998
+ catch { }
1999
+ return undefined;
2000
+ }
2001
+ finally {
2002
+ this.isBusy$.next(false);
1204
2003
  }
1205
- catch { }
1206
- return partial;
2004
+ }
2005
+ async refreshModels(force = false, silent = false) {
2006
+ const apiKey = this.currentValues[this.safeName('ai.apiKey')] || '';
2007
+ const provider = this.currentValues[this.safeName('ai.provider')] || this.resolveDefaultProvider();
2008
+ const meta = this.findProvider(provider);
2009
+ if (meta?.supportsModels === false) {
2010
+ if (!silent) {
2011
+ this.aiTestResult = { success: false, message: 'Este provedor não oferece catálogo de modelos.' };
2012
+ this.cdr.detectChanges();
2013
+ }
2014
+ return;
2015
+ }
2016
+ const requiresKey = meta?.requiresApiKey !== false;
2017
+ const hasKey = !!apiKey || this.hasStoredApiKey || !requiresKey;
2018
+ if (requiresKey && !hasKey) {
2019
+ this.aiTestResult = { success: false, message: 'Insira uma chave de API para buscar modelos.' };
2020
+ this.cdr.detectChanges();
2021
+ return;
2022
+ }
2023
+ if (!silent) {
2024
+ this.isRefetchingModels = true;
2025
+ this.cdr.detectChanges();
2026
+ }
2027
+ let modelOptions = [];
2028
+ try {
2029
+ const request = { provider };
2030
+ if (apiKey)
2031
+ request.apiKey = apiKey;
2032
+ const response = await firstValueFrom(this.aiApi.listModels(request));
2033
+ if (!response?.success) {
2034
+ const msg = response?.message || 'Erro ao buscar modelos.';
2035
+ throw new Error(msg);
2036
+ }
2037
+ const models = response.models || [];
2038
+ this.availableModels = models; // Store available models
2039
+ const formatLimit = (n) => n ? (n >= 1000 ? (n / 1000).toFixed(0) + 'k' : n) : '?';
2040
+ modelOptions = (models || [])
2041
+ .filter((m) => m.name)
2042
+ .map((m) => {
2043
+ const shortName = m.name.split('/').pop() || m.name;
2044
+ const details = `${formatLimit(m.inputTokenLimit)} in / ${formatLimit(m.outputTokenLimit)} out`;
2045
+ return {
2046
+ text: `${m.displayName || shortName} (${details})`,
2047
+ value: shortName
2048
+ };
2049
+ });
2050
+ if (modelOptions.length || force) {
2051
+ this.applyModelOptions(modelOptions, silent);
2052
+ }
2053
+ }
2054
+ catch (error) {
2055
+ this.logger.error('[GlobalConfigEditor] Error fetching models', this.buildLogOptions({ error, provider }, { context: { actionId: 'models.fetch' } }));
2056
+ this.aiTestResult = { success: false, message: error.message || 'Erro ao buscar modelos.' };
2057
+ if (!silent) {
2058
+ this.snack.open('Erro ao buscar modelos. Verifique a chave de API.', 'Fechar', { duration: 5000 });
2059
+ }
2060
+ }
2061
+ finally {
2062
+ this.logger.debug('[GlobalConfigEditor] refreshModels complete.', this.buildLogOptions({ modelOptionsCount: modelOptions?.length ?? 0, hasAiModelFormRef: !!this.aiModelFormRef }, {
2063
+ context: { actionId: 'models.refresh' },
2064
+ throttleKey: 'global-config-editor:refresh-models-complete',
2065
+ }));
2066
+ this.isRefetchingModels = false;
2067
+ this.cdr.detectChanges();
2068
+ }
2069
+ }
2070
+ // New method to apply model options to the form
2071
+ applyModelOptions(modelOptions, silent = false) {
2072
+ // Store if form ref not ready yet
2073
+ if (!this.aiModelFormRef) {
2074
+ this.pendingModelOptions = modelOptions; // Store for later application
2075
+ this.logger.warnOnce('[GlobalConfigEditor] aiModelFormRef not ready. Storing model options.', this.buildLogOptions({ modelOptionsCount: modelOptions.length }, {
2076
+ context: { actionId: 'models.apply-options' },
2077
+ dedupeKey: 'global-config-editor:ai-model-form-ref-not-ready',
2078
+ }));
2079
+ this.ensureAiModelForm();
2080
+ return;
2081
+ }
2082
+ this.pendingModelOptions = modelOptions;
2083
+ this.setModelFieldOptions(modelOptions);
2084
+ this.ensureDefaultModelSelection();
2085
+ this.recreateAiModelForm();
2086
+ if (!silent) {
2087
+ try {
2088
+ this.snack.open(`Lista de modelos atualizada (${modelOptions.length} modelos)`, undefined, { duration: 3000 });
2089
+ }
2090
+ catch { }
2091
+ }
2092
+ this.pendingModelOptions = [];
2093
+ }
2094
+ async testAiConnection() {
2095
+ this.isTestingAi = true;
2096
+ this.aiTestResult = null;
2097
+ this.cdr.detectChanges(); // Force update to show spinner
2098
+ // Get current value from form (even if not saved)
2099
+ const apiKey = this.currentValues[this.safeName('ai.apiKey')] || '';
2100
+ const provider = this.currentValues[this.safeName('ai.provider')] || this.resolveDefaultProvider();
2101
+ const meta = this.findProvider(provider);
2102
+ const selectedModel = this.currentValues[this.safeName('ai.model')] || meta?.defaultModel || 'gemini-2.0-flash';
2103
+ const requiresKey = meta?.requiresApiKey !== false;
2104
+ const hasKey = !!apiKey || this.hasStoredApiKey || !requiresKey;
2105
+ if (requiresKey && !hasKey) {
2106
+ this.aiTestResult = { success: false, message: 'Insira uma chave de API para testar.' };
2107
+ this.isTestingAi = false;
2108
+ this.cdr.detectChanges();
2109
+ return;
2110
+ }
2111
+ try {
2112
+ const request = { provider, model: selectedModel };
2113
+ if (apiKey)
2114
+ request.apiKey = apiKey;
2115
+ const response = await firstValueFrom(this.aiApi.testProvider(request));
2116
+ if (!response?.success) {
2117
+ throw new Error(response?.message || 'Falha ao testar conexao.');
2118
+ }
2119
+ this.aiTestResult = { success: true, message: response.message || 'Conexao estabelecida com sucesso!' };
2120
+ this.hasApiKey = true; // Garantir desbloqueio da seção de modelo após sucesso
2121
+ // Após validar a chave, já buscar a lista de modelos para preencher o select
2122
+ await this.refreshModels(true);
2123
+ }
2124
+ catch (err) {
2125
+ this.logger.error('[GlobalConfigEditor] AI Connection Test Error', this.buildLogOptions({ error: err, provider, model: selectedModel }, { context: { actionId: 'provider.test-connection' } }));
2126
+ const msg = err?.message || 'Erro desconhecido';
2127
+ this.aiTestResult = { success: false, message: msg };
2128
+ }
2129
+ finally {
2130
+ this.isTestingAi = false;
2131
+ this.cdr.detectChanges(); // Force update to hide spinner
2132
+ }
2133
+ }
2134
+ buildLogOptions(data, options = {}) {
2135
+ return {
2136
+ ...options,
2137
+ context: {
2138
+ ...this.logContext,
2139
+ ...(options.context ?? {}),
2140
+ },
2141
+ data,
2142
+ };
1207
2143
  }
1208
2144
  // ===== Helpers de UX para ícones de Dialog (variants) =====
1209
2145
  safeName(path) {
1210
2146
  return path.replace(/[^a-zA-Z0-9_]/g, '_');
1211
2147
  }
2148
+ buildChangedValues() {
2149
+ const out = {};
2150
+ for (const [safe, value] of Object.entries(this.currentValues)) {
2151
+ const path = this.pathMap[safe] || safe;
2152
+ const normalizedValue = this.normalizeFieldValue(path, value);
2153
+ const normalizedInitial = this.normalizeFieldValue(path, this.initialValues[safe]);
2154
+ if (!this.shouldIncludeField(normalizedInitial, normalizedValue))
2155
+ continue;
2156
+ out[path] = normalizedValue;
2157
+ }
2158
+ return out;
2159
+ }
2160
+ shouldIncludeField(initial, current) {
2161
+ if (initial === undefined) {
2162
+ if (current === null || current === undefined)
2163
+ return false;
2164
+ if (typeof current === 'string' && current.trim() === '')
2165
+ return false;
2166
+ if (Array.isArray(current) && current.length === 0)
2167
+ return false;
2168
+ }
2169
+ return initial !== current;
2170
+ }
2171
+ resolveSaveErrorMessage(err) {
2172
+ const apiMessage = typeof err?.error?.message === 'string' ? err.error.message : null;
2173
+ const details = err?.error?.details;
2174
+ if (apiMessage && apiMessage.trim()) {
2175
+ return apiMessage;
2176
+ }
2177
+ if (details && typeof details === 'object') {
2178
+ const first = Object.values(details).find((v) => typeof v === 'string' && v.trim());
2179
+ if (typeof first === 'string') {
2180
+ return first;
2181
+ }
2182
+ }
2183
+ if (typeof err?.message === 'string' && err.message.trim()) {
2184
+ return err.message;
2185
+ }
2186
+ return 'Falha ao salvar configurações.';
2187
+ }
2188
+ normalizeFieldValue(path, value) {
2189
+ if (typeof value !== 'string')
2190
+ return value;
2191
+ if (!TABLE_COMPACT_SPACING_PATHS.has(path) && !TABLE_COMPACT_FONT_SIZE_PATHS.has(path)) {
2192
+ return value;
2193
+ }
2194
+ return value.trim().replace(/\s+/g, ' ');
2195
+ }
2196
+ validateCompactTableAppearancePayload(partial) {
2197
+ const appearance = partial?.table?.appearance;
2198
+ if (!appearance || typeof appearance !== 'object')
2199
+ return null;
2200
+ const checks = [
2201
+ {
2202
+ path: 'table.appearance.spacing.cellPadding',
2203
+ value: appearance?.spacing?.cellPadding,
2204
+ regex: TABLE_COMPACT_SPACING_REGEX,
2205
+ example: '6px 12px',
2206
+ },
2207
+ {
2208
+ path: 'table.appearance.spacing.headerPadding',
2209
+ value: appearance?.spacing?.headerPadding,
2210
+ regex: TABLE_COMPACT_SPACING_REGEX,
2211
+ example: '8px 12px',
2212
+ },
2213
+ {
2214
+ path: 'table.appearance.typography.fontSize',
2215
+ value: appearance?.typography?.fontSize,
2216
+ regex: TABLE_COMPACT_FONT_SIZE_REGEX,
2217
+ example: '13px',
2218
+ },
2219
+ {
2220
+ path: 'table.appearance.typography.headerFontSize',
2221
+ value: appearance?.typography?.headerFontSize,
2222
+ regex: TABLE_COMPACT_FONT_SIZE_REGEX,
2223
+ example: '13px',
2224
+ },
2225
+ ];
2226
+ for (const check of checks) {
2227
+ if (typeof check.value !== 'string')
2228
+ continue;
2229
+ const normalized = this.normalizeFieldValue(check.path, check.value);
2230
+ if (!normalized)
2231
+ continue;
2232
+ if (!check.regex.test(normalized)) {
2233
+ return `Valor inválido em ${TABLE_COMPACT_FIELD_LABELS[check.path]}. Use uma string CSS simples, como ${check.example}.`;
2234
+ }
2235
+ }
2236
+ return null;
2237
+ }
1212
2238
  getVariantIcon(key) {
1213
2239
  const safe = this.safeName(`dialog.variants.${key}.icon`);
1214
2240
  return this.currentValues[safe];
@@ -1307,33 +2333,241 @@ class GlobalConfigEditorComponent {
1307
2333
  }
1308
2334
  return true;
1309
2335
  }
1310
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.1.4", ngImport: i0, type: GlobalConfigEditorComponent, deps: [{ token: GlobalConfigAdminService }, { token: i2.MatSnackBar }], target: i0.ɵɵFactoryTarget.Component });
1311
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.1.4", type: GlobalConfigEditorComponent, isStandalone: true, selector: "praxis-global-config-editor", viewQueries: [{ propertyName: "hostCrud", first: true, predicate: ["hostCrud"], descendants: true, read: ViewContainerRef, static: true }, { propertyName: "hostFields", first: true, predicate: ["hostFields"], descendants: true, read: ViewContainerRef, static: true }, { propertyName: "hostTable", first: true, predicate: ["hostTable"], descendants: true, read: ViewContainerRef, static: true }, { propertyName: "hostDialog", first: true, predicate: ["hostDialog"], descendants: true, read: ViewContainerRef, static: true }], ngImport: i0, template: `
2336
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.17", ngImport: i0, type: GlobalConfigEditorComponent, deps: [{ token: GlobalConfigAdminService }, { token: i2.MatSnackBar }], target: i0.ɵɵFactoryTarget.Component });
2337
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.17", type: GlobalConfigEditorComponent, isStandalone: true, selector: "praxis-global-config-editor", viewQueries: [{ propertyName: "hostCrud", first: true, predicate: ["hostCrud"], descendants: true, read: ViewContainerRef, static: true }, { propertyName: "hostFields", first: true, predicate: ["hostFields"], descendants: true, read: ViewContainerRef, static: true }, { propertyName: "hostCache", first: true, predicate: ["hostCache"], descendants: true, read: ViewContainerRef, static: true }, { propertyName: "hostTable", first: true, predicate: ["hostTable"], descendants: true, read: ViewContainerRef, static: true }, { propertyName: "hostDialog", first: true, predicate: ["hostDialog"], descendants: true, read: ViewContainerRef, static: true }, { propertyName: "hostAiCredentials", first: true, predicate: ["hostAiCredentials"], descendants: true, read: ViewContainerRef, static: true }, { propertyName: "hostAiModel", first: true, predicate: ["hostAiModel"], descendants: true, read: ViewContainerRef }, { propertyName: "hostAiEmbedding", first: true, predicate: ["hostAiEmbedding"], descendants: true, read: ViewContainerRef, static: true }], ngImport: i0, template: `
1312
2338
  <mat-accordion multi>
1313
- <mat-expansion-panel [expanded]="true">
2339
+ <mat-expansion-panel>
1314
2340
  <mat-expansion-panel-header>
1315
- <mat-panel-title>CRUD</mat-panel-title>
2341
+ <mat-panel-title>
2342
+ <mat-icon class="panel-icon">construction</mat-icon>
2343
+ CRUD
2344
+ </mat-panel-title>
1316
2345
  <mat-panel-description>Políticas globais de abertura, back e header</mat-panel-description>
1317
2346
  </mat-expansion-panel-header>
1318
2347
  <ng-template #hostCrud></ng-template>
1319
2348
  </mat-expansion-panel>
1320
2349
  <mat-expansion-panel>
1321
2350
  <mat-expansion-panel-header>
1322
- <mat-panel-title>Dynamic Fields</mat-panel-title>
2351
+ <mat-panel-title>
2352
+ <mat-icon class="panel-icon">dynamic_form</mat-icon>
2353
+ Dynamic Fields
2354
+ </mat-panel-title>
1323
2355
  <mat-panel-description>Async Select, cascata e paginação</mat-panel-description>
1324
2356
  </mat-expansion-panel-header>
1325
2357
  <ng-template #hostFields></ng-template>
1326
2358
  </mat-expansion-panel>
1327
2359
  <mat-expansion-panel>
1328
2360
  <mat-expansion-panel-header>
1329
- <mat-panel-title>Tabela</mat-panel-title>
2361
+ <mat-panel-title>
2362
+ <mat-icon class="panel-icon">cached</mat-icon>
2363
+ Cache & Persistência
2364
+ </mat-panel-title>
2365
+ <mat-panel-description>Estratégia de cache de schema (local vs server)</mat-panel-description>
2366
+ </mat-expansion-panel-header>
2367
+ <ng-template #hostCache></ng-template>
2368
+ </mat-expansion-panel>
2369
+ <mat-expansion-panel>
2370
+ <mat-expansion-panel-header>
2371
+ <mat-panel-title>
2372
+ <mat-icon class="panel-icon">psychology</mat-icon>
2373
+ Inteligência Artificial
2374
+ </mat-panel-title>
2375
+ <mat-panel-description>Integração com LLM</mat-panel-description>
2376
+ </mat-expansion-panel-header>
2377
+
2378
+ <div class="ai-config-container">
2379
+ <div class="ai-config-source">
2380
+ <div class="ai-config-source__meta">
2381
+ <mat-icon>settings_suggest</mat-icon>
2382
+ <span>{{ configSourceLabel }}</span>
2383
+ </div>
2384
+ <button
2385
+ mat-stroked-button
2386
+ type="button"
2387
+ class="ai-action-btn ai-action-btn--clear"
2388
+ *ngIf="hasStoredGlobalConfig"
2389
+ [attr.aria-busy]="isClearingGlobalConfig ? 'true' : null"
2390
+ [disabled]="isClearingGlobalConfig"
2391
+ (click)="clearStoredConfig()"
2392
+ matTooltip="Apaga o config salvo e volta aos defaults do servidor"
2393
+ >
2394
+ <ng-container *ngIf="isClearingGlobalConfig; else clearContent">
2395
+ <mat-spinner diameter="16" class="btn-spinner"></mat-spinner>
2396
+ <span class="ai-action-label">Limpando...</span>
2397
+ </ng-container>
2398
+ <ng-template #clearContent>
2399
+ <mat-icon>delete_sweep</mat-icon>
2400
+ <span class="ai-action-label">Limpar config salvo</span>
2401
+ </ng-template>
2402
+ </button>
2403
+ </div>
2404
+ <!-- Group 1: Credentials -->
2405
+ <div class="ai-group">
2406
+ <div class="ai-group-header">
2407
+ <div class="ai-group-title">
2408
+ <mat-icon>vpn_key</mat-icon> Credenciais
2409
+ </div>
2410
+ <button mat-stroked-button
2411
+ type="button"
2412
+ class="ai-action-btn"
2413
+ [class.is-success]="aiTestResult?.success"
2414
+ [attr.aria-busy]="isTestingAi ? 'true' : null"
2415
+ (click)="testAiConnection()"
2416
+ [disabled]="isTestingAi || !hasApiKey"
2417
+ matTooltip="Testar conexão com a chave informada">
2418
+ <ng-container *ngIf="isTestingAi; else btnContent">
2419
+ <mat-spinner diameter="16" class="btn-spinner"></mat-spinner>
2420
+ <span class="ai-action-label">Conectando...</span>
2421
+ </ng-container>
2422
+ <ng-template #btnContent>
2423
+ <mat-icon>{{ aiTestResult?.success ? 'check' : 'bolt' }}</mat-icon>
2424
+ <span class="ai-action-label">
2425
+ {{ aiTestResult?.success ? 'Conectado' : 'Testar conexão' }}
2426
+ </span>
2427
+ </ng-template>
2428
+ </button>
2429
+ </div>
2430
+ <div class="ai-provider-summary" *ngIf="selectedProvider">
2431
+ <span class="ai-provider-icon" aria-hidden="true">
2432
+ <ng-container [ngSwitch]="selectedProvider.iconKey">
2433
+ <svg *ngSwitchCase="'gemini'" viewBox="0 0 24 24" class="provider-svg" fill="none" stroke="currentColor" stroke-width="1.6">
2434
+ <path d="M12 3l2.6 5.4L20 11l-5.4 2.6L12 19l-2.6-5.4L4 11l5.4-2.6L12 3z" />
2435
+ </svg>
2436
+ <svg *ngSwitchCase="'openai'" viewBox="0 0 24 24" class="provider-svg" fill="none" stroke="currentColor" stroke-width="1.6">
2437
+ <polygon points="12,2 20,7 20,17 12,22 4,17 4,7" />
2438
+ </svg>
2439
+ <svg *ngSwitchCase="'xai'" viewBox="0 0 24 24" class="provider-svg" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round">
2440
+ <line x1="5" y1="5" x2="19" y2="19" />
2441
+ <line x1="19" y1="5" x2="5" y2="19" />
2442
+ </svg>
2443
+ <svg *ngSwitchCase="'mock'" viewBox="0 0 24 24" class="provider-svg" fill="none" stroke="currentColor" stroke-width="1.6">
2444
+ <rect x="4.5" y="4.5" width="15" height="15" rx="2" ry="2" stroke-dasharray="2 2" />
2445
+ </svg>
2446
+ <svg *ngSwitchDefault viewBox="0 0 24 24" class="provider-svg" fill="none" stroke="currentColor" stroke-width="1.6">
2447
+ <circle cx="12" cy="12" r="7.5" />
2448
+ </svg>
2449
+ </ng-container>
2450
+ </span>
2451
+ <div class="ai-provider-meta">
2452
+ <div class="ai-provider-name">{{ selectedProvider.label }}</div>
2453
+ <div class="ai-provider-desc">{{ selectedProvider.description }}</div>
2454
+ </div>
2455
+ <div
2456
+ class="ai-provider-key"
2457
+ *ngIf="selectedProvider?.requiresApiKey !== false"
2458
+ [class.is-present]="hasCurrentApiKey"
2459
+ [class.is-saved]="!hasCurrentApiKey && (apiKeyLast4 || hasStoredApiKey)"
2460
+ [class.is-missing]="!hasCurrentApiKey && !apiKeyLast4 && !hasStoredApiKey"
2461
+ >
2462
+ <mat-icon>vpn_key</mat-icon>
2463
+ <span>{{ apiKeyStatusLabel }}</span>
2464
+ </div>
2465
+ <div class="ai-provider-key is-unlocked" *ngIf="selectedProvider?.requiresApiKey === false">
2466
+ <mat-icon>check_circle</mat-icon>
2467
+ <span>Sem chave necessária</span>
2468
+ </div>
2469
+ </div>
2470
+ <div class="ai-credentials-row">
2471
+ <div class="ai-form-inline">
2472
+ <ng-template #hostAiCredentials></ng-template>
2473
+ </div>
2474
+ </div>
2475
+
2476
+ <!-- Feedback / Error Message -->
2477
+ <div class="ai-feedback" *ngIf="aiTestResult && !aiTestResult.success">
2478
+ <mat-icon color="warn">error</mat-icon>
2479
+ <span class="error-text">{{ aiTestResult.message }}</span>
2480
+ </div>
2481
+ </div>
2482
+
2483
+ <!-- Group 2: Model & Behavior (Only visible if Key is present) -->
2484
+ <div class="ai-group" [class.disabled-group]="!hasApiKey">
2485
+ <div class="ai-group-header">
2486
+ <div class="ai-group-title">
2487
+ <mat-icon>smart_toy</mat-icon> Modelo & Comportamento
2488
+ </div>
2489
+ <div class="ai-header-actions">
2490
+ <span class="ai-subtext" *ngIf="hasApiKey">Escolha o modelo após validar a chave.</span>
2491
+ <mat-chip-option *ngIf="!hasApiKey" disabled>Requer chave API validada</mat-chip-option>
2492
+ <button mat-icon-button (click)="refreshModels(true)" [disabled]="isTestingAi || !hasApiKey" matTooltip="Atualizar lista de modelos" aria-label="Atualizar lista de modelos">
2493
+ <mat-icon [class.spin]="isRefetchingModels">sync</mat-icon>
2494
+ </button>
2495
+ </div>
2496
+ </div>
2497
+
2498
+ <div class="ai-model-content" *ngIf="hasApiKey">
2499
+ <div class="ai-model-controls">
2500
+ <ng-template #hostAiModel></ng-template>
2501
+ </div>
2502
+
2503
+ <!-- Model Details (Placeholder for future metadata) -->
2504
+ <div class="ai-model-details" *ngIf="selectedModelDetails">
2505
+ <mat-icon inline>info</mat-icon> {{ selectedModelDetails }}
2506
+ </div>
2507
+ </div>
2508
+
2509
+ <div class="ai-placeholder" *ngIf="!hasApiKey">
2510
+ <mat-icon>lock</mat-icon>
2511
+ <span>Configure e teste sua chave de API para desbloquear a seleção de modelos.</span>
2512
+ </div>
2513
+ </div>
2514
+
2515
+ <!-- Group 3: Embeddings (RAG) -->
2516
+ <div class="ai-group">
2517
+ <div class="ai-group-header">
2518
+ <div class="ai-group-title">
2519
+ <mat-icon>scatter_plot</mat-icon> Embeddings (RAG)
2520
+ </div>
2521
+ <div class="ai-header-actions">
2522
+ <button
2523
+ mat-stroked-button
2524
+ type="button"
2525
+ class="ai-action-btn"
2526
+ (click)="useLlmForEmbeddings()"
2527
+ [disabled]="!canUseLlmForEmbeddings"
2528
+ matTooltip="Aplicar provedor e chave do LLM aos embeddings"
2529
+ >
2530
+ <mat-icon>merge_type</mat-icon>
2531
+ <span class="ai-action-label">Usar mesmo LLM</span>
2532
+ </button>
2533
+ <mat-chip-option *ngIf="!canUseLlmForEmbeddings" disabled>Provedor sem embeddings</mat-chip-option>
2534
+ </div>
2535
+ </div>
2536
+ <div class="ai-subtext">
2537
+ Configure o provedor de embeddings para buscas vetoriais (templates e schemas).
2538
+ </div>
2539
+ <div class="ai-subtext" *ngIf="embeddingUseSameAsLlm">
2540
+ Sincronizado com o LLM. Os campos abaixo acompanham a credencial principal.
2541
+ </div>
2542
+ <div class="ai-embedding-row">
2543
+ <ng-template #hostAiEmbedding></ng-template>
2544
+ </div>
2545
+ <div class="ai-feedback ai-feedback--warn" *ngIf="embeddingDimensionMismatch">
2546
+ <mat-icon>warning</mat-icon>
2547
+ <span class="error-text">
2548
+ Dimensão de embeddings diferente do banco (768). Ajuste para 768 ou refaça a migração.
2549
+ </span>
2550
+ </div>
2551
+ </div>
2552
+ </div>
2553
+
2554
+ </mat-expansion-panel>
2555
+ <mat-expansion-panel>
2556
+ <mat-expansion-panel-header>
2557
+ <mat-panel-title>
2558
+ <mat-icon class="panel-icon">table_chart</mat-icon>
2559
+ Tabela
2560
+ </mat-panel-title>
1330
2561
  <mat-panel-description>Toolbar, aparência e filtro avançado</mat-panel-description>
1331
2562
  </mat-expansion-panel-header>
1332
2563
  <ng-template #hostTable></ng-template>
1333
2564
  </mat-expansion-panel>
1334
2565
  <mat-expansion-panel>
1335
2566
  <mat-expansion-panel-header>
1336
- <mat-panel-title>Dialog</mat-panel-title>
2567
+ <mat-panel-title>
2568
+ <mat-icon class="panel-icon">forum</mat-icon>
2569
+ Dialog
2570
+ </mat-panel-title>
1337
2571
  <mat-panel-description>Defaults e variants (danger, info, success, question, error)</mat-panel-description>
1338
2572
  </mat-expansion-panel-header>
1339
2573
  <ng-template #hostDialog></ng-template>
@@ -1356,36 +2590,244 @@ class GlobalConfigEditorComponent {
1356
2590
  </div>
1357
2591
  </mat-expansion-panel>
1358
2592
  </mat-accordion>
1359
- `, isInline: true, styles: [".dlg-icon-helpers{margin:12px 16px 4px;padding:12px;border:1px dashed rgba(0,0,0,.1);border-radius:8px}.dlg-icon-helpers__head{font-weight:600;margin-bottom:8px;opacity:.8}.dlg-icon-helpers__row{display:flex;align-items:center;gap:12px;padding:6px 0}.dlg-icon-helpers__label{text-transform:capitalize;width:96px;opacity:.8}.dlg-icon-helpers__preview{width:24px;height:24px}.dlg-icon-helpers__value{font-family:monospace;font-size:12px;opacity:.8}.dlg-icon-helpers__hint{margin-top:8px;font-size:12px;opacity:.7}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i3.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i3.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "ngmodule", type: MatSnackBarModule }, { kind: "ngmodule", type: MatExpansionModule }, { kind: "directive", type: i4$1.MatAccordion, selector: "mat-accordion", inputs: ["hideToggle", "displayMode", "togglePosition"], exportAs: ["matAccordion"] }, { kind: "component", type: i4$1.MatExpansionPanel, selector: "mat-expansion-panel", inputs: ["hideToggle", "togglePosition"], outputs: ["afterExpand", "afterCollapse"], exportAs: ["matExpansionPanel"] }, { kind: "component", type: i4$1.MatExpansionPanelHeader, selector: "mat-expansion-panel-header", inputs: ["expandedHeight", "collapsedHeight", "tabIndex"] }, { kind: "directive", type: i4$1.MatExpansionPanelTitle, selector: "mat-panel-title" }, { kind: "directive", type: i4$1.MatExpansionPanelDescription, selector: "mat-panel-description" }, { kind: "ngmodule", type: MatIconModule }, { kind: "component", type: i4.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "ngmodule", type: MatButtonModule }, { kind: "component", type: i3$1.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: i3$1.MatIconButton, selector: "button[mat-icon-button], a[mat-icon-button], button[matIconButton], a[matIconButton]", exportAs: ["matButton", "matAnchor"] }, { kind: "ngmodule", type: MatTooltipModule }, { kind: "directive", type: i5.MatTooltip, selector: "[matTooltip]", inputs: ["matTooltipPosition", "matTooltipPositionAtOrigin", "matTooltipDisabled", "matTooltipShowDelay", "matTooltipHideDelay", "matTooltipTouchGestures", "matTooltip", "matTooltipClass"], exportAs: ["matTooltip"] }, { kind: "directive", type: PraxisIconDirective, selector: "mat-icon[praxisIcon]", inputs: ["praxisIcon"] }] });
2593
+ `, isInline: true, styles: [".dlg-icon-helpers{margin:12px 16px 4px;padding:12px;border:1px dashed var(--md-sys-color-outline-variant);border-radius:var(--md-sys-shape-corner-small, 8px)}.dlg-icon-helpers__head{font-weight:600;margin-bottom:8px;opacity:.8}.dlg-icon-helpers__row{display:flex;align-items:center;gap:12px;padding:6px 0}.dlg-icon-helpers__label{text-transform:capitalize;width:96px;opacity:.8}.dlg-icon-helpers__preview{width:24px;height:24px}.dlg-icon-helpers__value{font-family:monospace;font-size:12px;opacity:.8}.dlg-icon-helpers__hint{margin-top:8px;font-size:12px;opacity:.7}.panel-icon{margin-right:12px;color:var(--md-sys-color-primary)}.ai-config-container{padding:16px;display:flex;flex-direction:column;gap:24px}.ai-config-source{display:flex;align-items:center;justify-content:space-between;gap:12px;padding:10px 12px;border-radius:var(--md-sys-shape-corner-medium, 10px);border:1px dashed var(--md-sys-color-outline-variant);background:var(--md-sys-color-surface-container-low);font-size:12px;color:var(--md-sys-color-on-surface-variant);flex-wrap:wrap}.ai-config-source__meta{display:inline-flex;align-items:center;gap:8px}.ai-action-btn--clear{background:var(--md-sys-color-error-container);color:var(--md-sys-color-on-error-container);--mdc-outlined-button-container-color: var(--md-sys-color-error-container);--mdc-outlined-button-label-text-color: var(--md-sys-color-on-error-container);--mdc-outlined-button-outline-color: var(--md-sys-color-error);--mat-mdc-button-persistent-ripple-color: var(--md-sys-color-on-error-container);--mat-mdc-button-ripple-color: var(--md-sys-color-on-error-container)}.ai-action-btn--clear:hover{background:var(--md-sys-color-error-container)}.ai-group-title{display:flex;align-items:center;gap:8px;font-weight:500;font-size:14px;color:var(--md-sys-color-primary);margin-bottom:12px;border-bottom:1px solid var(--md-sys-color-outline-variant);padding-bottom:4px}.ai-group-title mat-icon{font-size:20px;width:20px;height:20px}.ai-group ::ng-deep .form-actions,.ai-group ::ng-deep button[type=submit]{display:none!important}.ai-credentials-row{display:flex;align-items:flex-start;gap:16px;flex-wrap:wrap}.ai-group-header{display:flex;align-items:center;justify-content:space-between;gap:12px;flex-wrap:wrap}.ai-provider-summary{display:flex;align-items:center;gap:12px;padding:8px 12px;border-radius:var(--md-sys-shape-corner-medium, 10px);background:var(--md-sys-color-surface-variant);border:1px solid var(--md-sys-color-outline-variant);margin:8px 0 4px;flex-wrap:wrap}.ai-provider-icon{width:32px;height:32px;border-radius:var(--md-sys-shape-corner-small, 8px);border:1px solid var(--md-sys-color-outline-variant);display:flex;align-items:center;justify-content:center;background:var(--md-sys-color-surface);color:var(--md-sys-color-primary);flex:0 0 auto}.provider-svg{width:18px;height:18px}.ai-provider-meta{display:flex;flex-direction:column;gap:2px;min-width:200px;flex:1 1 auto}.ai-provider-name{font-weight:600;font-size:13px}.ai-provider-desc{font-size:12px;opacity:.75}.ai-provider-key{display:inline-flex;align-items:center;gap:6px;padding:4px 10px;border-radius:999px;border:1px solid var(--md-sys-color-outline-variant);background:var(--md-sys-color-surface-container-lowest);color:var(--md-sys-color-on-surface-variant);font-size:11px;font-weight:600;letter-spacing:.2px;box-shadow:var(--md-sys-elevation-level1, 0 1px 2px rgba(0,0,0,.2))}.ai-provider-key mat-icon{width:16px;height:16px;font-size:16px}.ai-provider-key.is-present{background:var(--md-sys-color-primary-container);color:var(--md-sys-color-on-primary-container);border-color:var(--md-sys-color-primary)}.ai-provider-key.is-saved{background:var(--md-sys-color-secondary-container);color:var(--md-sys-color-on-secondary-container);border-color:var(--md-sys-color-secondary)}.ai-provider-key.is-missing{background:var(--md-sys-color-surface-container-low);color:var(--md-sys-color-on-surface-variant);border-style:dashed;opacity:.9}.ai-provider-key.is-unlocked{background:var(--md-sys-color-tertiary-container);color:var(--md-sys-color-on-tertiary-container);border-color:var(--md-sys-color-tertiary)}.ai-action-btn{display:inline-flex;align-items:center;gap:8px;height:40px;padding:0 14px;min-width:168px;border-radius:var(--md-sys-shape-corner-medium, 12px);white-space:nowrap;background:var(--md-sys-color-secondary-container);color:var(--md-sys-color-on-secondary-container);border-color:var(--md-sys-color-outline-variant);box-shadow:var(--md-sys-elevation-level1, 0 1px 2px rgba(0,0,0,.2));transition:background-color .15s ease,box-shadow .15s ease,transform .06s ease;--mdc-outlined-button-container-color: var(--md-sys-color-secondary-container);--mdc-outlined-button-label-text-color: var(--md-sys-color-on-secondary-container);--mdc-outlined-button-outline-color: var(--md-sys-color-outline-variant);--mat-mdc-button-persistent-ripple-color: var(--md-sys-color-on-secondary-container);--mat-mdc-button-ripple-color: var(--md-sys-color-on-secondary-container)}.ai-action-btn .mat-button-wrapper,.ai-action-btn .mdc-button__label{display:inline-flex;align-items:center;gap:8px;line-height:1;height:100%}.ai-action-btn .mat-icon{width:18px;height:18px;font-size:18px;line-height:18px}.ai-action-btn .ai-action-label{display:inline-flex;align-items:center;line-height:1}.ai-action-btn .mat-progress-spinner,.ai-action-btn .btn-spinner{display:inline-flex;align-items:center;justify-content:center;vertical-align:middle}.ai-action-btn:hover{background:var(--md-sys-color-secondary-container);box-shadow:var(--md-sys-elevation-level2, 0 2px 6px rgba(0,0,0,.25))}.ai-action-btn:active{transform:translateY(1px)}.ai-action-btn:focus-visible{outline:2px solid var(--md-sys-color-primary);outline-offset:2px}.ai-action-btn.is-success{background:var(--md-sys-color-tertiary-container);color:var(--md-sys-color-on-tertiary-container);border-color:var(--md-sys-color-tertiary);--mdc-outlined-button-container-color: var(--md-sys-color-tertiary-container);--mdc-outlined-button-label-text-color: var(--md-sys-color-on-tertiary-container);--mdc-outlined-button-outline-color: var(--md-sys-color-tertiary);--mat-mdc-button-persistent-ripple-color: var(--md-sys-color-on-tertiary-container);--mat-mdc-button-ripple-color: var(--md-sys-color-on-tertiary-container)}.ai-action-btn.is-success:hover{background:var(--md-sys-color-tertiary-container)}.ai-action-btn:disabled{background:var(--md-sys-color-surface-container-low);color:var(--md-sys-color-on-surface-variant);border-color:var(--md-sys-color-outline-variant);box-shadow:none;opacity:.7}.ai-form-inline{flex:1;min-width:300px}.ai-credentials-row button{margin-top:4px;height:40px}.btn-spinner{margin-right:8px}.ai-feedback{margin-top:8px;display:flex;align-items:center;gap:8px;padding:8px 12px;background:var(--md-sys-color-error-container);color:var(--md-sys-color-on-error-container);border-radius:var(--md-sys-shape-corner-extra-small, 4px);font-size:13px}.ai-feedback--warn{background:var(--md-sys-color-tertiary-container);color:var(--md-sys-color-on-tertiary-container)}.disabled-group{opacity:.6;pointer-events:none}.ai-model-content{display:flex;flex-direction:column;gap:8px}.ai-model-controls{display:flex;align-items:flex-start;gap:8px}.ai-model-controls ::ng-deep praxis-dynamic-form{flex:1}.ai-model-details{font-size:12px;color:var(--md-sys-color-on-surface-variant);margin-left:4px}.ai-subtext{font-size:12px;color:var(--md-sys-color-on-surface-variant)}.ai-header-actions{display:inline-flex;align-items:center;gap:8px;flex-wrap:wrap}.ai-placeholder{display:flex;flex-direction:column;align-items:center;justify-content:center;padding:24px;background:var(--md-sys-color-surface-container-low);border-radius:var(--md-sys-shape-corner-small, 8px);color:var(--md-sys-color-outline);gap:8px;text-align:center;font-size:13px}.spin{animation:spin 1s linear infinite}@keyframes spin{to{-webkit-transform:rotate(360deg);transform:rotate(360deg)}}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i3.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i3.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "directive", type: i3.NgSwitch, selector: "[ngSwitch]", inputs: ["ngSwitch"] }, { kind: "directive", type: i3.NgSwitchCase, selector: "[ngSwitchCase]", inputs: ["ngSwitchCase"] }, { kind: "directive", type: i3.NgSwitchDefault, selector: "[ngSwitchDefault]" }, { kind: "ngmodule", type: MatSnackBarModule }, { kind: "ngmodule", type: MatExpansionModule }, { kind: "directive", type: i4$1.MatAccordion, selector: "mat-accordion", inputs: ["hideToggle", "displayMode", "togglePosition"], exportAs: ["matAccordion"] }, { kind: "component", type: i4$1.MatExpansionPanel, selector: "mat-expansion-panel", inputs: ["hideToggle", "togglePosition"], outputs: ["afterExpand", "afterCollapse"], exportAs: ["matExpansionPanel"] }, { kind: "component", type: i4$1.MatExpansionPanelHeader, selector: "mat-expansion-panel-header", inputs: ["expandedHeight", "collapsedHeight", "tabIndex"] }, { kind: "directive", type: i4$1.MatExpansionPanelTitle, selector: "mat-panel-title" }, { kind: "directive", type: i4$1.MatExpansionPanelDescription, selector: "mat-panel-description" }, { kind: "ngmodule", type: MatIconModule }, { kind: "component", type: i4.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "ngmodule", type: MatButtonModule }, { kind: "component", type: i3$1.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: i3$1.MatIconButton, selector: "button[mat-icon-button], a[mat-icon-button], button[matIconButton], a[matIconButton]", exportAs: ["matButton", "matAnchor"] }, { kind: "ngmodule", type: MatTooltipModule }, { kind: "directive", type: i5.MatTooltip, selector: "[matTooltip]", inputs: ["matTooltipPosition", "matTooltipPositionAtOrigin", "matTooltipDisabled", "matTooltipShowDelay", "matTooltipHideDelay", "matTooltipTouchGestures", "matTooltip", "matTooltipClass"], exportAs: ["matTooltip"] }, { kind: "ngmodule", type: MatProgressSpinnerModule }, { kind: "component", type: i6.MatProgressSpinner, selector: "mat-progress-spinner, mat-spinner", inputs: ["color", "mode", "value", "diameter", "strokeWidth"], exportAs: ["matProgressSpinner"] }, { kind: "ngmodule", type: MatChipsModule }, { kind: "component", type: i9.MatChipOption, selector: "mat-basic-chip-option, [mat-basic-chip-option], mat-chip-option, [mat-chip-option]", inputs: ["selectable", "selected"], outputs: ["selectionChange"] }, { kind: "directive", type: PraxisIconDirective, selector: "mat-icon[praxisIcon]", inputs: ["praxisIcon"] }] });
1360
2594
  }
1361
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.1.4", ngImport: i0, type: GlobalConfigEditorComponent, decorators: [{
2595
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImport: i0, type: GlobalConfigEditorComponent, decorators: [{
1362
2596
  type: Component,
1363
- args: [{ selector: 'praxis-global-config-editor', standalone: true, imports: [CommonModule, MatSnackBarModule, MatExpansionModule, MatIconModule, MatButtonModule, MatTooltipModule, PraxisIconDirective], template: `
2597
+ args: [{ selector: 'praxis-global-config-editor', standalone: true, imports: [CommonModule, MatSnackBarModule, MatExpansionModule, MatIconModule, MatButtonModule, MatTooltipModule, MatProgressSpinnerModule, MatChipsModule, PraxisIconDirective], template: `
1364
2598
  <mat-accordion multi>
1365
- <mat-expansion-panel [expanded]="true">
2599
+ <mat-expansion-panel>
1366
2600
  <mat-expansion-panel-header>
1367
- <mat-panel-title>CRUD</mat-panel-title>
2601
+ <mat-panel-title>
2602
+ <mat-icon class="panel-icon">construction</mat-icon>
2603
+ CRUD
2604
+ </mat-panel-title>
1368
2605
  <mat-panel-description>Políticas globais de abertura, back e header</mat-panel-description>
1369
2606
  </mat-expansion-panel-header>
1370
2607
  <ng-template #hostCrud></ng-template>
1371
2608
  </mat-expansion-panel>
1372
2609
  <mat-expansion-panel>
1373
2610
  <mat-expansion-panel-header>
1374
- <mat-panel-title>Dynamic Fields</mat-panel-title>
2611
+ <mat-panel-title>
2612
+ <mat-icon class="panel-icon">dynamic_form</mat-icon>
2613
+ Dynamic Fields
2614
+ </mat-panel-title>
1375
2615
  <mat-panel-description>Async Select, cascata e paginação</mat-panel-description>
1376
2616
  </mat-expansion-panel-header>
1377
2617
  <ng-template #hostFields></ng-template>
1378
2618
  </mat-expansion-panel>
1379
2619
  <mat-expansion-panel>
1380
2620
  <mat-expansion-panel-header>
1381
- <mat-panel-title>Tabela</mat-panel-title>
2621
+ <mat-panel-title>
2622
+ <mat-icon class="panel-icon">cached</mat-icon>
2623
+ Cache & Persistência
2624
+ </mat-panel-title>
2625
+ <mat-panel-description>Estratégia de cache de schema (local vs server)</mat-panel-description>
2626
+ </mat-expansion-panel-header>
2627
+ <ng-template #hostCache></ng-template>
2628
+ </mat-expansion-panel>
2629
+ <mat-expansion-panel>
2630
+ <mat-expansion-panel-header>
2631
+ <mat-panel-title>
2632
+ <mat-icon class="panel-icon">psychology</mat-icon>
2633
+ Inteligência Artificial
2634
+ </mat-panel-title>
2635
+ <mat-panel-description>Integração com LLM</mat-panel-description>
2636
+ </mat-expansion-panel-header>
2637
+
2638
+ <div class="ai-config-container">
2639
+ <div class="ai-config-source">
2640
+ <div class="ai-config-source__meta">
2641
+ <mat-icon>settings_suggest</mat-icon>
2642
+ <span>{{ configSourceLabel }}</span>
2643
+ </div>
2644
+ <button
2645
+ mat-stroked-button
2646
+ type="button"
2647
+ class="ai-action-btn ai-action-btn--clear"
2648
+ *ngIf="hasStoredGlobalConfig"
2649
+ [attr.aria-busy]="isClearingGlobalConfig ? 'true' : null"
2650
+ [disabled]="isClearingGlobalConfig"
2651
+ (click)="clearStoredConfig()"
2652
+ matTooltip="Apaga o config salvo e volta aos defaults do servidor"
2653
+ >
2654
+ <ng-container *ngIf="isClearingGlobalConfig; else clearContent">
2655
+ <mat-spinner diameter="16" class="btn-spinner"></mat-spinner>
2656
+ <span class="ai-action-label">Limpando...</span>
2657
+ </ng-container>
2658
+ <ng-template #clearContent>
2659
+ <mat-icon>delete_sweep</mat-icon>
2660
+ <span class="ai-action-label">Limpar config salvo</span>
2661
+ </ng-template>
2662
+ </button>
2663
+ </div>
2664
+ <!-- Group 1: Credentials -->
2665
+ <div class="ai-group">
2666
+ <div class="ai-group-header">
2667
+ <div class="ai-group-title">
2668
+ <mat-icon>vpn_key</mat-icon> Credenciais
2669
+ </div>
2670
+ <button mat-stroked-button
2671
+ type="button"
2672
+ class="ai-action-btn"
2673
+ [class.is-success]="aiTestResult?.success"
2674
+ [attr.aria-busy]="isTestingAi ? 'true' : null"
2675
+ (click)="testAiConnection()"
2676
+ [disabled]="isTestingAi || !hasApiKey"
2677
+ matTooltip="Testar conexão com a chave informada">
2678
+ <ng-container *ngIf="isTestingAi; else btnContent">
2679
+ <mat-spinner diameter="16" class="btn-spinner"></mat-spinner>
2680
+ <span class="ai-action-label">Conectando...</span>
2681
+ </ng-container>
2682
+ <ng-template #btnContent>
2683
+ <mat-icon>{{ aiTestResult?.success ? 'check' : 'bolt' }}</mat-icon>
2684
+ <span class="ai-action-label">
2685
+ {{ aiTestResult?.success ? 'Conectado' : 'Testar conexão' }}
2686
+ </span>
2687
+ </ng-template>
2688
+ </button>
2689
+ </div>
2690
+ <div class="ai-provider-summary" *ngIf="selectedProvider">
2691
+ <span class="ai-provider-icon" aria-hidden="true">
2692
+ <ng-container [ngSwitch]="selectedProvider.iconKey">
2693
+ <svg *ngSwitchCase="'gemini'" viewBox="0 0 24 24" class="provider-svg" fill="none" stroke="currentColor" stroke-width="1.6">
2694
+ <path d="M12 3l2.6 5.4L20 11l-5.4 2.6L12 19l-2.6-5.4L4 11l5.4-2.6L12 3z" />
2695
+ </svg>
2696
+ <svg *ngSwitchCase="'openai'" viewBox="0 0 24 24" class="provider-svg" fill="none" stroke="currentColor" stroke-width="1.6">
2697
+ <polygon points="12,2 20,7 20,17 12,22 4,17 4,7" />
2698
+ </svg>
2699
+ <svg *ngSwitchCase="'xai'" viewBox="0 0 24 24" class="provider-svg" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round">
2700
+ <line x1="5" y1="5" x2="19" y2="19" />
2701
+ <line x1="19" y1="5" x2="5" y2="19" />
2702
+ </svg>
2703
+ <svg *ngSwitchCase="'mock'" viewBox="0 0 24 24" class="provider-svg" fill="none" stroke="currentColor" stroke-width="1.6">
2704
+ <rect x="4.5" y="4.5" width="15" height="15" rx="2" ry="2" stroke-dasharray="2 2" />
2705
+ </svg>
2706
+ <svg *ngSwitchDefault viewBox="0 0 24 24" class="provider-svg" fill="none" stroke="currentColor" stroke-width="1.6">
2707
+ <circle cx="12" cy="12" r="7.5" />
2708
+ </svg>
2709
+ </ng-container>
2710
+ </span>
2711
+ <div class="ai-provider-meta">
2712
+ <div class="ai-provider-name">{{ selectedProvider.label }}</div>
2713
+ <div class="ai-provider-desc">{{ selectedProvider.description }}</div>
2714
+ </div>
2715
+ <div
2716
+ class="ai-provider-key"
2717
+ *ngIf="selectedProvider?.requiresApiKey !== false"
2718
+ [class.is-present]="hasCurrentApiKey"
2719
+ [class.is-saved]="!hasCurrentApiKey && (apiKeyLast4 || hasStoredApiKey)"
2720
+ [class.is-missing]="!hasCurrentApiKey && !apiKeyLast4 && !hasStoredApiKey"
2721
+ >
2722
+ <mat-icon>vpn_key</mat-icon>
2723
+ <span>{{ apiKeyStatusLabel }}</span>
2724
+ </div>
2725
+ <div class="ai-provider-key is-unlocked" *ngIf="selectedProvider?.requiresApiKey === false">
2726
+ <mat-icon>check_circle</mat-icon>
2727
+ <span>Sem chave necessária</span>
2728
+ </div>
2729
+ </div>
2730
+ <div class="ai-credentials-row">
2731
+ <div class="ai-form-inline">
2732
+ <ng-template #hostAiCredentials></ng-template>
2733
+ </div>
2734
+ </div>
2735
+
2736
+ <!-- Feedback / Error Message -->
2737
+ <div class="ai-feedback" *ngIf="aiTestResult && !aiTestResult.success">
2738
+ <mat-icon color="warn">error</mat-icon>
2739
+ <span class="error-text">{{ aiTestResult.message }}</span>
2740
+ </div>
2741
+ </div>
2742
+
2743
+ <!-- Group 2: Model & Behavior (Only visible if Key is present) -->
2744
+ <div class="ai-group" [class.disabled-group]="!hasApiKey">
2745
+ <div class="ai-group-header">
2746
+ <div class="ai-group-title">
2747
+ <mat-icon>smart_toy</mat-icon> Modelo & Comportamento
2748
+ </div>
2749
+ <div class="ai-header-actions">
2750
+ <span class="ai-subtext" *ngIf="hasApiKey">Escolha o modelo após validar a chave.</span>
2751
+ <mat-chip-option *ngIf="!hasApiKey" disabled>Requer chave API validada</mat-chip-option>
2752
+ <button mat-icon-button (click)="refreshModels(true)" [disabled]="isTestingAi || !hasApiKey" matTooltip="Atualizar lista de modelos" aria-label="Atualizar lista de modelos">
2753
+ <mat-icon [class.spin]="isRefetchingModels">sync</mat-icon>
2754
+ </button>
2755
+ </div>
2756
+ </div>
2757
+
2758
+ <div class="ai-model-content" *ngIf="hasApiKey">
2759
+ <div class="ai-model-controls">
2760
+ <ng-template #hostAiModel></ng-template>
2761
+ </div>
2762
+
2763
+ <!-- Model Details (Placeholder for future metadata) -->
2764
+ <div class="ai-model-details" *ngIf="selectedModelDetails">
2765
+ <mat-icon inline>info</mat-icon> {{ selectedModelDetails }}
2766
+ </div>
2767
+ </div>
2768
+
2769
+ <div class="ai-placeholder" *ngIf="!hasApiKey">
2770
+ <mat-icon>lock</mat-icon>
2771
+ <span>Configure e teste sua chave de API para desbloquear a seleção de modelos.</span>
2772
+ </div>
2773
+ </div>
2774
+
2775
+ <!-- Group 3: Embeddings (RAG) -->
2776
+ <div class="ai-group">
2777
+ <div class="ai-group-header">
2778
+ <div class="ai-group-title">
2779
+ <mat-icon>scatter_plot</mat-icon> Embeddings (RAG)
2780
+ </div>
2781
+ <div class="ai-header-actions">
2782
+ <button
2783
+ mat-stroked-button
2784
+ type="button"
2785
+ class="ai-action-btn"
2786
+ (click)="useLlmForEmbeddings()"
2787
+ [disabled]="!canUseLlmForEmbeddings"
2788
+ matTooltip="Aplicar provedor e chave do LLM aos embeddings"
2789
+ >
2790
+ <mat-icon>merge_type</mat-icon>
2791
+ <span class="ai-action-label">Usar mesmo LLM</span>
2792
+ </button>
2793
+ <mat-chip-option *ngIf="!canUseLlmForEmbeddings" disabled>Provedor sem embeddings</mat-chip-option>
2794
+ </div>
2795
+ </div>
2796
+ <div class="ai-subtext">
2797
+ Configure o provedor de embeddings para buscas vetoriais (templates e schemas).
2798
+ </div>
2799
+ <div class="ai-subtext" *ngIf="embeddingUseSameAsLlm">
2800
+ Sincronizado com o LLM. Os campos abaixo acompanham a credencial principal.
2801
+ </div>
2802
+ <div class="ai-embedding-row">
2803
+ <ng-template #hostAiEmbedding></ng-template>
2804
+ </div>
2805
+ <div class="ai-feedback ai-feedback--warn" *ngIf="embeddingDimensionMismatch">
2806
+ <mat-icon>warning</mat-icon>
2807
+ <span class="error-text">
2808
+ Dimensão de embeddings diferente do banco (768). Ajuste para 768 ou refaça a migração.
2809
+ </span>
2810
+ </div>
2811
+ </div>
2812
+ </div>
2813
+
2814
+ </mat-expansion-panel>
2815
+ <mat-expansion-panel>
2816
+ <mat-expansion-panel-header>
2817
+ <mat-panel-title>
2818
+ <mat-icon class="panel-icon">table_chart</mat-icon>
2819
+ Tabela
2820
+ </mat-panel-title>
1382
2821
  <mat-panel-description>Toolbar, aparência e filtro avançado</mat-panel-description>
1383
2822
  </mat-expansion-panel-header>
1384
2823
  <ng-template #hostTable></ng-template>
1385
2824
  </mat-expansion-panel>
1386
2825
  <mat-expansion-panel>
1387
2826
  <mat-expansion-panel-header>
1388
- <mat-panel-title>Dialog</mat-panel-title>
2827
+ <mat-panel-title>
2828
+ <mat-icon class="panel-icon">forum</mat-icon>
2829
+ Dialog
2830
+ </mat-panel-title>
1389
2831
  <mat-panel-description>Defaults e variants (danger, info, success, question, error)</mat-panel-description>
1390
2832
  </mat-expansion-panel-header>
1391
2833
  <ng-template #hostDialog></ng-template>
@@ -1408,19 +2850,31 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.1.4", ngImpor
1408
2850
  </div>
1409
2851
  </mat-expansion-panel>
1410
2852
  </mat-accordion>
1411
- `, styles: [".dlg-icon-helpers{margin:12px 16px 4px;padding:12px;border:1px dashed rgba(0,0,0,.1);border-radius:8px}.dlg-icon-helpers__head{font-weight:600;margin-bottom:8px;opacity:.8}.dlg-icon-helpers__row{display:flex;align-items:center;gap:12px;padding:6px 0}.dlg-icon-helpers__label{text-transform:capitalize;width:96px;opacity:.8}.dlg-icon-helpers__preview{width:24px;height:24px}.dlg-icon-helpers__value{font-family:monospace;font-size:12px;opacity:.8}.dlg-icon-helpers__hint{margin-top:8px;font-size:12px;opacity:.7}\n"] }]
2853
+ `, styles: [".dlg-icon-helpers{margin:12px 16px 4px;padding:12px;border:1px dashed var(--md-sys-color-outline-variant);border-radius:var(--md-sys-shape-corner-small, 8px)}.dlg-icon-helpers__head{font-weight:600;margin-bottom:8px;opacity:.8}.dlg-icon-helpers__row{display:flex;align-items:center;gap:12px;padding:6px 0}.dlg-icon-helpers__label{text-transform:capitalize;width:96px;opacity:.8}.dlg-icon-helpers__preview{width:24px;height:24px}.dlg-icon-helpers__value{font-family:monospace;font-size:12px;opacity:.8}.dlg-icon-helpers__hint{margin-top:8px;font-size:12px;opacity:.7}.panel-icon{margin-right:12px;color:var(--md-sys-color-primary)}.ai-config-container{padding:16px;display:flex;flex-direction:column;gap:24px}.ai-config-source{display:flex;align-items:center;justify-content:space-between;gap:12px;padding:10px 12px;border-radius:var(--md-sys-shape-corner-medium, 10px);border:1px dashed var(--md-sys-color-outline-variant);background:var(--md-sys-color-surface-container-low);font-size:12px;color:var(--md-sys-color-on-surface-variant);flex-wrap:wrap}.ai-config-source__meta{display:inline-flex;align-items:center;gap:8px}.ai-action-btn--clear{background:var(--md-sys-color-error-container);color:var(--md-sys-color-on-error-container);--mdc-outlined-button-container-color: var(--md-sys-color-error-container);--mdc-outlined-button-label-text-color: var(--md-sys-color-on-error-container);--mdc-outlined-button-outline-color: var(--md-sys-color-error);--mat-mdc-button-persistent-ripple-color: var(--md-sys-color-on-error-container);--mat-mdc-button-ripple-color: var(--md-sys-color-on-error-container)}.ai-action-btn--clear:hover{background:var(--md-sys-color-error-container)}.ai-group-title{display:flex;align-items:center;gap:8px;font-weight:500;font-size:14px;color:var(--md-sys-color-primary);margin-bottom:12px;border-bottom:1px solid var(--md-sys-color-outline-variant);padding-bottom:4px}.ai-group-title mat-icon{font-size:20px;width:20px;height:20px}.ai-group ::ng-deep .form-actions,.ai-group ::ng-deep button[type=submit]{display:none!important}.ai-credentials-row{display:flex;align-items:flex-start;gap:16px;flex-wrap:wrap}.ai-group-header{display:flex;align-items:center;justify-content:space-between;gap:12px;flex-wrap:wrap}.ai-provider-summary{display:flex;align-items:center;gap:12px;padding:8px 12px;border-radius:var(--md-sys-shape-corner-medium, 10px);background:var(--md-sys-color-surface-variant);border:1px solid var(--md-sys-color-outline-variant);margin:8px 0 4px;flex-wrap:wrap}.ai-provider-icon{width:32px;height:32px;border-radius:var(--md-sys-shape-corner-small, 8px);border:1px solid var(--md-sys-color-outline-variant);display:flex;align-items:center;justify-content:center;background:var(--md-sys-color-surface);color:var(--md-sys-color-primary);flex:0 0 auto}.provider-svg{width:18px;height:18px}.ai-provider-meta{display:flex;flex-direction:column;gap:2px;min-width:200px;flex:1 1 auto}.ai-provider-name{font-weight:600;font-size:13px}.ai-provider-desc{font-size:12px;opacity:.75}.ai-provider-key{display:inline-flex;align-items:center;gap:6px;padding:4px 10px;border-radius:999px;border:1px solid var(--md-sys-color-outline-variant);background:var(--md-sys-color-surface-container-lowest);color:var(--md-sys-color-on-surface-variant);font-size:11px;font-weight:600;letter-spacing:.2px;box-shadow:var(--md-sys-elevation-level1, 0 1px 2px rgba(0,0,0,.2))}.ai-provider-key mat-icon{width:16px;height:16px;font-size:16px}.ai-provider-key.is-present{background:var(--md-sys-color-primary-container);color:var(--md-sys-color-on-primary-container);border-color:var(--md-sys-color-primary)}.ai-provider-key.is-saved{background:var(--md-sys-color-secondary-container);color:var(--md-sys-color-on-secondary-container);border-color:var(--md-sys-color-secondary)}.ai-provider-key.is-missing{background:var(--md-sys-color-surface-container-low);color:var(--md-sys-color-on-surface-variant);border-style:dashed;opacity:.9}.ai-provider-key.is-unlocked{background:var(--md-sys-color-tertiary-container);color:var(--md-sys-color-on-tertiary-container);border-color:var(--md-sys-color-tertiary)}.ai-action-btn{display:inline-flex;align-items:center;gap:8px;height:40px;padding:0 14px;min-width:168px;border-radius:var(--md-sys-shape-corner-medium, 12px);white-space:nowrap;background:var(--md-sys-color-secondary-container);color:var(--md-sys-color-on-secondary-container);border-color:var(--md-sys-color-outline-variant);box-shadow:var(--md-sys-elevation-level1, 0 1px 2px rgba(0,0,0,.2));transition:background-color .15s ease,box-shadow .15s ease,transform .06s ease;--mdc-outlined-button-container-color: var(--md-sys-color-secondary-container);--mdc-outlined-button-label-text-color: var(--md-sys-color-on-secondary-container);--mdc-outlined-button-outline-color: var(--md-sys-color-outline-variant);--mat-mdc-button-persistent-ripple-color: var(--md-sys-color-on-secondary-container);--mat-mdc-button-ripple-color: var(--md-sys-color-on-secondary-container)}.ai-action-btn .mat-button-wrapper,.ai-action-btn .mdc-button__label{display:inline-flex;align-items:center;gap:8px;line-height:1;height:100%}.ai-action-btn .mat-icon{width:18px;height:18px;font-size:18px;line-height:18px}.ai-action-btn .ai-action-label{display:inline-flex;align-items:center;line-height:1}.ai-action-btn .mat-progress-spinner,.ai-action-btn .btn-spinner{display:inline-flex;align-items:center;justify-content:center;vertical-align:middle}.ai-action-btn:hover{background:var(--md-sys-color-secondary-container);box-shadow:var(--md-sys-elevation-level2, 0 2px 6px rgba(0,0,0,.25))}.ai-action-btn:active{transform:translateY(1px)}.ai-action-btn:focus-visible{outline:2px solid var(--md-sys-color-primary);outline-offset:2px}.ai-action-btn.is-success{background:var(--md-sys-color-tertiary-container);color:var(--md-sys-color-on-tertiary-container);border-color:var(--md-sys-color-tertiary);--mdc-outlined-button-container-color: var(--md-sys-color-tertiary-container);--mdc-outlined-button-label-text-color: var(--md-sys-color-on-tertiary-container);--mdc-outlined-button-outline-color: var(--md-sys-color-tertiary);--mat-mdc-button-persistent-ripple-color: var(--md-sys-color-on-tertiary-container);--mat-mdc-button-ripple-color: var(--md-sys-color-on-tertiary-container)}.ai-action-btn.is-success:hover{background:var(--md-sys-color-tertiary-container)}.ai-action-btn:disabled{background:var(--md-sys-color-surface-container-low);color:var(--md-sys-color-on-surface-variant);border-color:var(--md-sys-color-outline-variant);box-shadow:none;opacity:.7}.ai-form-inline{flex:1;min-width:300px}.ai-credentials-row button{margin-top:4px;height:40px}.btn-spinner{margin-right:8px}.ai-feedback{margin-top:8px;display:flex;align-items:center;gap:8px;padding:8px 12px;background:var(--md-sys-color-error-container);color:var(--md-sys-color-on-error-container);border-radius:var(--md-sys-shape-corner-extra-small, 4px);font-size:13px}.ai-feedback--warn{background:var(--md-sys-color-tertiary-container);color:var(--md-sys-color-on-tertiary-container)}.disabled-group{opacity:.6;pointer-events:none}.ai-model-content{display:flex;flex-direction:column;gap:8px}.ai-model-controls{display:flex;align-items:flex-start;gap:8px}.ai-model-controls ::ng-deep praxis-dynamic-form{flex:1}.ai-model-details{font-size:12px;color:var(--md-sys-color-on-surface-variant);margin-left:4px}.ai-subtext{font-size:12px;color:var(--md-sys-color-on-surface-variant)}.ai-header-actions{display:inline-flex;align-items:center;gap:8px;flex-wrap:wrap}.ai-placeholder{display:flex;flex-direction:column;align-items:center;justify-content:center;padding:24px;background:var(--md-sys-color-surface-container-low);border-radius:var(--md-sys-shape-corner-small, 8px);color:var(--md-sys-color-outline);gap:8px;text-align:center;font-size:13px}.spin{animation:spin 1s linear infinite}@keyframes spin{to{-webkit-transform:rotate(360deg);transform:rotate(360deg)}}\n"] }]
1412
2854
  }], ctorParameters: () => [{ type: GlobalConfigAdminService }, { type: i2.MatSnackBar }], propDecorators: { hostCrud: [{
1413
2855
  type: ViewChild,
1414
2856
  args: ['hostCrud', { read: ViewContainerRef, static: true }]
1415
2857
  }], hostFields: [{
1416
2858
  type: ViewChild,
1417
2859
  args: ['hostFields', { read: ViewContainerRef, static: true }]
2860
+ }], hostCache: [{
2861
+ type: ViewChild,
2862
+ args: ['hostCache', { read: ViewContainerRef, static: true }]
1418
2863
  }], hostTable: [{
1419
2864
  type: ViewChild,
1420
2865
  args: ['hostTable', { read: ViewContainerRef, static: true }]
1421
2866
  }], hostDialog: [{
1422
2867
  type: ViewChild,
1423
2868
  args: ['hostDialog', { read: ViewContainerRef, static: true }]
2869
+ }], hostAiCredentials: [{
2870
+ type: ViewChild,
2871
+ args: ['hostAiCredentials', { read: ViewContainerRef, static: true }]
2872
+ }], hostAiModel: [{
2873
+ type: ViewChild,
2874
+ args: ['hostAiModel', { read: ViewContainerRef, static: false }]
2875
+ }], hostAiEmbedding: [{
2876
+ type: ViewChild,
2877
+ args: ['hostAiEmbedding', { read: ViewContainerRef, static: true }]
1424
2878
  }] } });
1425
2879
 
1426
2880
  /**
@@ -1437,9 +2891,43 @@ function openGlobalConfigEditor(settings, opts) {
1437
2891
  });
1438
2892
  }
1439
2893
 
2894
+ /**
2895
+ * Capabilities catalog for SettingsPanelConfig.
2896
+ */
2897
+ const SETTINGS_PANEL_AI_CAPABILITIES = {
2898
+ version: 'v1.0',
2899
+ enums: {},
2900
+ targets: ['praxis-global-config-editor', 'praxis-settings-panel'],
2901
+ notes: [
2902
+ 'SettingsPanelConfig descreve o painel e o componente embutido (content.component).',
2903
+ 'content.component deve ser uma referencia de classe Angular fornecida pelo host.',
2904
+ ],
2905
+ capabilities: [
2906
+ { path: 'id', category: 'identity', valueKind: 'string', description: 'ID do painel.' },
2907
+ { path: 'title', category: 'identity', valueKind: 'string', description: 'Titulo do painel.' },
2908
+ { path: 'titleIcon', category: 'identity', valueKind: 'string', description: 'Icone do titulo (Material icon name).' },
2909
+ { path: 'expanded', category: 'behavior', valueKind: 'boolean', description: 'Estado inicial expandido.' },
2910
+ { path: 'content', category: 'content', valueKind: 'object', description: 'Conteudo do painel.' },
2911
+ {
2912
+ path: 'content.component',
2913
+ category: 'content',
2914
+ valueKind: 'object',
2915
+ description: 'Referencia do componente Angular a ser renderizado.',
2916
+ critical: true,
2917
+ safetyNotes: 'Nao gerar dinamicamente; o host deve fornecer a referencia.',
2918
+ },
2919
+ {
2920
+ path: 'content.inputs',
2921
+ category: 'content',
2922
+ valueKind: 'object',
2923
+ description: 'Inputs passados para o componente de conteudo.',
2924
+ },
2925
+ ],
2926
+ };
2927
+
1440
2928
  /**
1441
2929
  * Generated bundle index. Do not edit.
1442
2930
  */
1443
2931
 
1444
- export { GlobalConfigAdminService, GlobalConfigEditorComponent, SETTINGS_PANEL_DATA, SETTINGS_PANEL_REF, SettingsPanelComponent, SettingsPanelRef, SettingsPanelService, buildGlobalConfigFormConfig, openGlobalConfigEditor };
2932
+ export { GLOBAL_CONFIG_DYNAMIC_FORM_COMPONENT, GlobalConfigAdminService, GlobalConfigEditorComponent, SETTINGS_PANEL_AI_CAPABILITIES, SETTINGS_PANEL_DATA, SETTINGS_PANEL_REF, SettingsPanelComponent, SettingsPanelRef, SettingsPanelService, buildGlobalConfigFormConfig, openGlobalConfigEditor };
1445
2933
  //# sourceMappingURL=praxisui-settings-panel.mjs.map