@praxisui/settings-panel 1.0.0-beta.4 → 1.0.0-beta.41

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
  }
@@ -260,7 +241,7 @@ class SettingsPanelComponent {
260
241
  }
261
242
  }
262
243
  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 });
244
+ 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 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
246
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.1.4", ngImport: i0, type: SettingsPanelComponent, decorators: [{
266
247
  type: Component,
@@ -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 };
@@ -427,11 +400,22 @@ 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);
410
+ }
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();
435
419
  }
436
420
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.1.4", ngImport: i0, type: GlobalConfigAdminService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
437
421
  static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.1.4", ngImport: i0, type: GlobalConfigAdminService, providedIn: 'root' });
@@ -581,6 +565,105 @@ function buildGlobalConfigFormConfig() {
581
565
  controlType: FieldControlType.TOGGLE,
582
566
  dataAttributes: { globalPath: 'dynamicFields.asyncSelect.useCursor' },
583
567
  },
568
+ // AI
569
+ {
570
+ name: safeName('ai.provider'),
571
+ label: 'Provedor LLM',
572
+ controlType: FieldControlType.SELECT,
573
+ selectOptions: [],
574
+ hint: 'Selecione o provedor para listar modelos e testar a chave.',
575
+ dataAttributes: { globalPath: 'ai.provider' },
576
+ },
577
+ {
578
+ name: safeName('ai.apiKey'),
579
+ label: 'Chave de API',
580
+ controlType: FieldControlType.INPUT,
581
+ hint: 'Informe a chave do provedor selecionado para testar.',
582
+ dataAttributes: { globalPath: 'ai.apiKey' },
583
+ },
584
+ {
585
+ name: safeName('ai.model'),
586
+ label: 'Modelo de IA',
587
+ controlType: FieldControlType.SEARCHABLE_SELECT,
588
+ selectOptions: [],
589
+ resourcePath: '',
590
+ dataAttributes: { globalPath: 'ai.model' },
591
+ },
592
+ {
593
+ name: safeName('ai.temperature'),
594
+ label: 'Temperatura (Criatividade)',
595
+ controlType: FieldControlType.NUMERIC_TEXT_BOX,
596
+ hint: '0.0 (determinístico) a 1.0 (criativo)',
597
+ dataAttributes: { globalPath: 'ai.temperature' },
598
+ },
599
+ {
600
+ name: safeName('ai.maxTokens'),
601
+ label: 'Max tokens',
602
+ controlType: FieldControlType.NUMERIC_TEXT_BOX,
603
+ hint: 'Limite maximo de tokens na resposta.',
604
+ dataAttributes: { globalPath: 'ai.maxTokens' },
605
+ },
606
+ {
607
+ name: safeName('ai.riskPolicy'),
608
+ label: 'Assistente: validação de risco',
609
+ controlType: FieldControlType.SELECT,
610
+ selectOptions: [
611
+ { text: 'Estrito (recomendado)', value: 'strict' },
612
+ { text: 'Padrão', value: 'standard' },
613
+ ],
614
+ hint: 'Estrito exige confirmação em risco médio/alto. Padrão segue a indicação retornada pelo backend.',
615
+ dataAttributes: { globalPath: 'ai.riskPolicy' },
616
+ },
617
+ {
618
+ name: safeName('ai.embedding.useSameAsLlm'),
619
+ label: 'Embeddings: usar mesmo LLM',
620
+ controlType: FieldControlType.TOGGLE,
621
+ hint: 'Replica provedor e chave do LLM para embeddings.',
622
+ dataAttributes: { globalPath: 'ai.embedding.useSameAsLlm' },
623
+ },
624
+ {
625
+ name: safeName('ai.embedding.provider'),
626
+ label: 'Embeddings: provedor',
627
+ controlType: FieldControlType.SELECT,
628
+ selectOptions: [],
629
+ hint: 'Provedor usado para gerar embeddings (RAG).',
630
+ dataAttributes: { globalPath: 'ai.embedding.provider' },
631
+ },
632
+ {
633
+ name: safeName('ai.embedding.apiKey'),
634
+ label: 'Embeddings: chave de API',
635
+ controlType: FieldControlType.INPUT,
636
+ hint: 'Chave do provedor de embeddings.',
637
+ dataAttributes: { globalPath: 'ai.embedding.apiKey' },
638
+ },
639
+ {
640
+ name: safeName('ai.embedding.model'),
641
+ label: 'Embeddings: modelo',
642
+ controlType: FieldControlType.SEARCHABLE_SELECT,
643
+ selectOptions: [
644
+ { text: 'text-embedding-3-large', value: 'text-embedding-3-large' },
645
+ { text: 'text-embedding-3-small', value: 'text-embedding-3-small' },
646
+ { text: 'text-embedding-ada-002 (legacy)', value: 'text-embedding-ada-002' },
647
+ ],
648
+ emptyOptionText: 'Padrão do provedor',
649
+ hint: 'Modelos OpenAI para embeddings.',
650
+ dataAttributes: { globalPath: 'ai.embedding.model' },
651
+ },
652
+ {
653
+ name: safeName('ai.embedding.dimensions'),
654
+ label: 'Embeddings: dimensoes',
655
+ controlType: FieldControlType.NUMERIC_TEXT_BOX,
656
+ hint: 'Dimensoes do vetor de embedding (default 768).',
657
+ dataAttributes: { globalPath: 'ai.embedding.dimensions' },
658
+ },
659
+ // Cache
660
+ {
661
+ name: safeName('cache.disableSchemaCache'),
662
+ label: 'Desabilitar cache de schema (LocalStorage)',
663
+ controlType: FieldControlType.TOGGLE,
664
+ hint: 'Se ativo, sempre baixa o schema do servidor (ignora cache local). Útil para desenvolvimento.',
665
+ dataAttributes: { globalPath: 'cache.disableSchemaCache' },
666
+ },
584
667
  // Table
585
668
  {
586
669
  name: safeName('table.behavior.pagination.enabled'),
@@ -713,12 +796,21 @@ function buildGlobalConfigFormConfig() {
713
796
  ],
714
797
  dataAttributes: { globalPath: 'dialog.defaults.confirm.ariaRole' },
715
798
  },
799
+ { name: safeName('dialog.defaults.confirm.title'), label: 'Dialog Defaults: confirm — título', controlType: FieldControlType.INPUT, dataAttributes: { globalPath: 'dialog.defaults.confirm.title' } },
800
+ { name: safeName('dialog.defaults.confirm.icon'), label: 'Dialog Defaults: confirm — ícone', controlType: FieldControlType.INPUT, dataAttributes: { globalPath: 'dialog.defaults.confirm.icon' } },
801
+ { name: safeName('dialog.defaults.confirm.message'), label: 'Dialog Defaults: confirm — mensagem', controlType: FieldControlType.TEXTAREA, dataAttributes: { globalPath: 'dialog.defaults.confirm.message' } },
802
+ { 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
803
  {
717
804
  name: safeName('dialog.defaults.confirm.closeOnBackdropClick'),
718
805
  label: 'Dialog Defaults: confirm — fechar ao clicar no backdrop',
719
806
  controlType: FieldControlType.TOGGLE,
720
807
  dataAttributes: { globalPath: 'dialog.defaults.confirm.closeOnBackdropClick' },
721
808
  },
809
+ { name: safeName('dialog.defaults.confirm.disableClose'), label: 'Dialog Defaults: confirm — disableClose', controlType: FieldControlType.TOGGLE, dataAttributes: { globalPath: 'dialog.defaults.confirm.disableClose' } },
810
+ { name: safeName('dialog.defaults.confirm.hasBackdrop'), label: 'Dialog Defaults: confirm — hasBackdrop', controlType: FieldControlType.TOGGLE, dataAttributes: { globalPath: 'dialog.defaults.confirm.hasBackdrop' } },
811
+ { name: safeName('dialog.defaults.confirm.panelClass'), label: 'Dialog Defaults: confirm — panelClass', controlType: FieldControlType.INPUT, dataAttributes: { globalPath: 'dialog.defaults.confirm.panelClass' } },
812
+ { 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)"}' },
813
+ { 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
814
  {
723
815
  name: safeName('dialog.defaults.alert.ariaRole'),
724
816
  label: 'Dialog Defaults: alert — ariaRole',
@@ -729,12 +821,21 @@ function buildGlobalConfigFormConfig() {
729
821
  ],
730
822
  dataAttributes: { globalPath: 'dialog.defaults.alert.ariaRole' },
731
823
  },
824
+ { name: safeName('dialog.defaults.alert.title'), label: 'Dialog Defaults: alert — título', controlType: FieldControlType.INPUT, dataAttributes: { globalPath: 'dialog.defaults.alert.title' } },
825
+ { name: safeName('dialog.defaults.alert.icon'), label: 'Dialog Defaults: alert — ícone', controlType: FieldControlType.INPUT, dataAttributes: { globalPath: 'dialog.defaults.alert.icon' } },
826
+ { name: safeName('dialog.defaults.alert.message'), label: 'Dialog Defaults: alert — mensagem', controlType: FieldControlType.TEXTAREA, dataAttributes: { globalPath: 'dialog.defaults.alert.message' } },
827
+ { 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
828
  {
733
829
  name: safeName('dialog.defaults.alert.closeOnBackdropClick'),
734
830
  label: 'Dialog Defaults: alert — fechar ao clicar no backdrop',
735
831
  controlType: FieldControlType.TOGGLE,
736
832
  dataAttributes: { globalPath: 'dialog.defaults.alert.closeOnBackdropClick' },
737
833
  },
834
+ { name: safeName('dialog.defaults.alert.disableClose'), label: 'Dialog Defaults: alert — disableClose', controlType: FieldControlType.TOGGLE, dataAttributes: { globalPath: 'dialog.defaults.alert.disableClose' } },
835
+ { name: safeName('dialog.defaults.alert.hasBackdrop'), label: 'Dialog Defaults: alert — hasBackdrop', controlType: FieldControlType.TOGGLE, dataAttributes: { globalPath: 'dialog.defaults.alert.hasBackdrop' } },
836
+ { name: safeName('dialog.defaults.alert.panelClass'), label: 'Dialog Defaults: alert — panelClass', controlType: FieldControlType.INPUT, dataAttributes: { globalPath: 'dialog.defaults.alert.panelClass' } },
837
+ { 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)"}' },
838
+ { 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
839
  {
739
840
  name: safeName('dialog.defaults.prompt.ariaRole'),
740
841
  label: 'Dialog Defaults: prompt — ariaRole',
@@ -745,12 +846,21 @@ function buildGlobalConfigFormConfig() {
745
846
  ],
746
847
  dataAttributes: { globalPath: 'dialog.defaults.prompt.ariaRole' },
747
848
  },
849
+ { name: safeName('dialog.defaults.prompt.title'), label: 'Dialog Defaults: prompt — título', controlType: FieldControlType.INPUT, dataAttributes: { globalPath: 'dialog.defaults.prompt.title' } },
850
+ { name: safeName('dialog.defaults.prompt.icon'), label: 'Dialog Defaults: prompt — ícone', controlType: FieldControlType.INPUT, dataAttributes: { globalPath: 'dialog.defaults.prompt.icon' } },
851
+ { name: safeName('dialog.defaults.prompt.message'), label: 'Dialog Defaults: prompt — mensagem', controlType: FieldControlType.TEXTAREA, dataAttributes: { globalPath: 'dialog.defaults.prompt.message' } },
852
+ { 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
853
  {
749
854
  name: safeName('dialog.defaults.prompt.closeOnBackdropClick'),
750
855
  label: 'Dialog Defaults: prompt — fechar ao clicar no backdrop',
751
856
  controlType: FieldControlType.TOGGLE,
752
857
  dataAttributes: { globalPath: 'dialog.defaults.prompt.closeOnBackdropClick' },
753
858
  },
859
+ { name: safeName('dialog.defaults.prompt.disableClose'), label: 'Dialog Defaults: prompt — disableClose', controlType: FieldControlType.TOGGLE, dataAttributes: { globalPath: 'dialog.defaults.prompt.disableClose' } },
860
+ { name: safeName('dialog.defaults.prompt.hasBackdrop'), label: 'Dialog Defaults: prompt — hasBackdrop', controlType: FieldControlType.TOGGLE, dataAttributes: { globalPath: 'dialog.defaults.prompt.hasBackdrop' } },
861
+ { name: safeName('dialog.defaults.prompt.panelClass'), label: 'Dialog Defaults: prompt — panelClass', controlType: FieldControlType.INPUT, dataAttributes: { globalPath: 'dialog.defaults.prompt.panelClass' } },
862
+ { 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)"}' },
863
+ { 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
864
  // Dialog — Variants per profile (danger, info, success, question, error)
755
865
  // Each variant exposes commonly customized fields + JSON editors for actions/styles
756
866
  // danger
@@ -761,7 +871,7 @@ function buildGlobalConfigFormConfig() {
761
871
  { name: safeName('dialog.variants.danger.closeOnBackdropClick'), label: 'Variant danger — fechar ao clicar no backdrop', controlType: FieldControlType.TOGGLE, dataAttributes: { globalPath: 'dialog.variants.danger.closeOnBackdropClick' } },
762
872
  { name: safeName('dialog.variants.danger.panelClass'), label: 'Variant danger — panelClass', controlType: FieldControlType.INPUT, dataAttributes: { globalPath: 'dialog.variants.danger.panelClass' } },
763
873
  { 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 } },
874
+ { 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
875
  { name: safeName('dialog.variants.danger.animation'), label: 'Variant danger — animation (JSON/boolean)', controlType: FieldControlType.TEXTAREA, dataAttributes: { globalPath: 'dialog.variants.danger.animation', monospace: true } },
766
876
  // info
767
877
  { name: safeName('dialog.variants.info.title'), label: 'Variant info — título', controlType: FieldControlType.INPUT, dataAttributes: { globalPath: 'dialog.variants.info.title' } },
@@ -771,7 +881,7 @@ function buildGlobalConfigFormConfig() {
771
881
  { name: safeName('dialog.variants.info.closeOnBackdropClick'), label: 'Variant info — fechar ao clicar no backdrop', controlType: FieldControlType.TOGGLE, dataAttributes: { globalPath: 'dialog.variants.info.closeOnBackdropClick' } },
772
882
  { name: safeName('dialog.variants.info.panelClass'), label: 'Variant info — panelClass', controlType: FieldControlType.INPUT, dataAttributes: { globalPath: 'dialog.variants.info.panelClass' } },
773
883
  { 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 } },
884
+ { 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
885
  { name: safeName('dialog.variants.info.animation'), label: 'Variant info — animation (JSON/boolean)', controlType: FieldControlType.TEXTAREA, dataAttributes: { globalPath: 'dialog.variants.info.animation', monospace: true } },
776
886
  // success
777
887
  { name: safeName('dialog.variants.success.title'), label: 'Variant success — título', controlType: FieldControlType.INPUT, dataAttributes: { globalPath: 'dialog.variants.success.title' } },
@@ -781,7 +891,7 @@ function buildGlobalConfigFormConfig() {
781
891
  { name: safeName('dialog.variants.success.closeOnBackdropClick'), label: 'Variant success — fechar ao clicar no backdrop', controlType: FieldControlType.TOGGLE, dataAttributes: { globalPath: 'dialog.variants.success.closeOnBackdropClick' } },
782
892
  { name: safeName('dialog.variants.success.panelClass'), label: 'Variant success — panelClass', controlType: FieldControlType.INPUT, dataAttributes: { globalPath: 'dialog.variants.success.panelClass' } },
783
893
  { 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 } },
894
+ { 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
895
  { name: safeName('dialog.variants.success.animation'), label: 'Variant success — animation (JSON/boolean)', controlType: FieldControlType.TEXTAREA, dataAttributes: { globalPath: 'dialog.variants.success.animation', monospace: true } },
786
896
  // question
787
897
  { name: safeName('dialog.variants.question.title'), label: 'Variant question — título', controlType: FieldControlType.INPUT, dataAttributes: { globalPath: 'dialog.variants.question.title' } },
@@ -791,7 +901,7 @@ function buildGlobalConfigFormConfig() {
791
901
  { name: safeName('dialog.variants.question.closeOnBackdropClick'), label: 'Variant question — fechar ao clicar no backdrop', controlType: FieldControlType.TOGGLE, dataAttributes: { globalPath: 'dialog.variants.question.closeOnBackdropClick' } },
792
902
  { name: safeName('dialog.variants.question.panelClass'), label: 'Variant question — panelClass', controlType: FieldControlType.INPUT, dataAttributes: { globalPath: 'dialog.variants.question.panelClass' } },
793
903
  { 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 } },
904
+ { 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
905
  { name: safeName('dialog.variants.question.animation'), label: 'Variant question — animation (JSON/boolean)', controlType: FieldControlType.TEXTAREA, dataAttributes: { globalPath: 'dialog.variants.question.animation', monospace: true } },
796
906
  // error
797
907
  { name: safeName('dialog.variants.error.title'), label: 'Variant error — título', controlType: FieldControlType.INPUT, dataAttributes: { globalPath: 'dialog.variants.error.title' } },
@@ -801,7 +911,7 @@ function buildGlobalConfigFormConfig() {
801
911
  { name: safeName('dialog.variants.error.closeOnBackdropClick'), label: 'Variant error — fechar ao clicar no backdrop', controlType: FieldControlType.TOGGLE, dataAttributes: { globalPath: 'dialog.variants.error.closeOnBackdropClick' } },
802
912
  { name: safeName('dialog.variants.error.panelClass'), label: 'Variant error — panelClass', controlType: FieldControlType.INPUT, dataAttributes: { globalPath: 'dialog.variants.error.panelClass' } },
803
913
  { 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 } },
914
+ { 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
915
  { name: safeName('dialog.variants.error.animation'), label: 'Variant error — animation (JSON/boolean)', controlType: FieldControlType.TEXTAREA, dataAttributes: { globalPath: 'dialog.variants.error.animation', monospace: true } },
806
916
  ];
807
917
  const cfg = {
@@ -854,6 +964,58 @@ function buildGlobalConfigFormConfig() {
854
964
  },
855
965
  ],
856
966
  },
967
+ {
968
+ id: 'ai-credentials',
969
+ title: 'Credenciais', // Internal title, UI might override
970
+ rows: [
971
+ {
972
+ id: 'ai-cred-row-1',
973
+ columns: [
974
+ { id: 'ai-cred-col-1', fields: [safeName('ai.provider')] },
975
+ { id: 'ai-cred-col-2', fields: [safeName('ai.apiKey')] },
976
+ ],
977
+ },
978
+ ],
979
+ },
980
+ {
981
+ id: 'ai-model',
982
+ title: 'Modelo & Comportamento',
983
+ rows: [
984
+ {
985
+ id: 'ai-mod-row-1',
986
+ columns: [
987
+ { id: 'ai-mod-col-1', fields: [safeName('ai.model'), safeName('ai.temperature'), safeName('ai.maxTokens')] },
988
+ { id: 'ai-mod-col-2', fields: [safeName('ai.riskPolicy')] },
989
+ ],
990
+ },
991
+ ],
992
+ },
993
+ {
994
+ id: 'ai-embedding',
995
+ title: 'Embeddings',
996
+ rows: [
997
+ {
998
+ id: 'ai-embed-row-1',
999
+ columns: [
1000
+ { id: 'ai-embed-col-1', fields: [safeName('ai.embedding.useSameAsLlm')] },
1001
+ { id: 'ai-embed-col-2', fields: [safeName('ai.embedding.provider'), safeName('ai.embedding.apiKey')] },
1002
+ { id: 'ai-embed-col-3', fields: [safeName('ai.embedding.model'), safeName('ai.embedding.dimensions')] },
1003
+ ],
1004
+ },
1005
+ ],
1006
+ },
1007
+ {
1008
+ id: 'cache',
1009
+ title: 'Cache & Persistência',
1010
+ rows: [
1011
+ {
1012
+ id: 'cache-row-1',
1013
+ columns: [
1014
+ { id: 'cache-col-1', fields: [safeName('cache.disableSchemaCache')] },
1015
+ ],
1016
+ },
1017
+ ],
1018
+ },
857
1019
  {
858
1020
  id: 'table',
859
1021
  title: 'Tabela',
@@ -1047,6 +1209,16 @@ function buildGlobalConfigFormConfig() {
1047
1209
  return cfg;
1048
1210
  }
1049
1211
 
1212
+ /**
1213
+ * Optional token used by GlobalConfigEditorComponent to render forms.
1214
+ * Provide the PraxisDynamicForm component (or a compatible dynamic form)
1215
+ * to avoid creating a hard dependency from @praxisui/settings-panel to
1216
+ * @praxisui/dynamic-form.
1217
+ */
1218
+ const GLOBAL_CONFIG_DYNAMIC_FORM_COMPONENT = new InjectionToken('GLOBAL_CONFIG_DYNAMIC_FORM_COMPONENT', {
1219
+ factory: () => null,
1220
+ });
1221
+
1050
1222
  class GlobalConfigEditorComponent {
1051
1223
  admin;
1052
1224
  snack;
@@ -1055,82 +1227,240 @@ class GlobalConfigEditorComponent {
1055
1227
  currentValues = {};
1056
1228
  pathMap = {};
1057
1229
  bootstrappedSections = new Set();
1058
- allSections = ['crud', 'dynamic-fields', 'table', 'dialog'];
1230
+ allSections = ['crud', 'dynamic-fields', 'table', 'dialog', 'ai-credentials', 'ai-model', 'ai-embedding'];
1059
1231
  // SettingsPanel integration signals
1060
1232
  isDirty$ = new BehaviorSubject(false);
1061
1233
  isValid$ = new BehaviorSubject(true);
1062
1234
  isBusy$ = new BehaviorSubject(false);
1063
1235
  hostCrud;
1064
1236
  hostFields;
1237
+ hostCache;
1065
1238
  hostTable;
1066
1239
  hostDialog;
1240
+ hostAiCredentials;
1241
+ hostAiModel;
1242
+ hostAiEmbedding;
1067
1243
  destroyRef = inject(DestroyRef);
1068
1244
  iconPicker = inject(IconPickerService);
1245
+ aiApi = inject(AiBackendApiService);
1246
+ cdr = inject(ChangeDetectorRef);
1247
+ logger = inject(LoggerService);
1248
+ logContext = {
1249
+ lib: 'praxis-settings-panel',
1250
+ component: 'GlobalConfigEditorComponent',
1251
+ };
1252
+ providedDynamicFormCtor = inject(GLOBAL_CONFIG_DYNAMIC_FORM_COMPONENT, { optional: true });
1253
+ dynamicFormCtor = this.providedDynamicFormCtor ?? null;
1254
+ loggedMissingDynamicForm = false;
1069
1255
  // Guardar instância do form da seção 'dialog' para atualizar controles diretamente
1070
1256
  dialogFormInst = null;
1257
+ aiModelFormRef = null;
1258
+ aiEmbeddingFormRef = null;
1259
+ pendingModelOptions = [];
1260
+ embeddingModelOptions = [
1261
+ { text: 'text-embedding-3-large', value: 'text-embedding-3-large' },
1262
+ { text: 'text-embedding-3-small', value: 'text-embedding-3-small' },
1263
+ { text: 'text-embedding-ada-002 (legacy)', value: 'text-embedding-ada-002' },
1264
+ ];
1265
+ componentRefs = new Map();
1071
1266
  dialogVariantKeys = ['danger', 'info', 'success', 'question', 'error'];
1267
+ isTestingAi = false;
1268
+ isRefetchingModels = false;
1269
+ isClearingGlobalConfig = false;
1270
+ aiTestResult = null;
1271
+ availableModels = [];
1272
+ providers = [];
1273
+ selectedProvider = null;
1274
+ apiKeyLast4 = null;
1275
+ hasStoredApiKey = false;
1276
+ hasStoredGlobalConfig = false;
1277
+ configSourceLabel = 'Padrões do servidor (env vars)';
1278
+ // UX: API Key monitoring
1279
+ apiKeyChanged$ = new Subject();
1280
+ hasApiKey = false;
1281
+ selectedModelDetails = '';
1282
+ get hasCurrentApiKey() {
1283
+ const apiKey = this.currentValues?.[this.safeName('ai.apiKey')] || '';
1284
+ return !!apiKey;
1285
+ }
1286
+ get apiKeyStatusLabel() {
1287
+ const apiKey = this.currentValues?.[this.safeName('ai.apiKey')] || '';
1288
+ if (apiKey)
1289
+ return 'Chave informada';
1290
+ if (this.apiKeyLast4)
1291
+ return `Chave salva (****${this.apiKeyLast4})`;
1292
+ if (this.hasStoredApiKey)
1293
+ return 'Chave salva';
1294
+ return 'Sem chave salva';
1295
+ }
1072
1296
  constructor(admin, snack) {
1073
1297
  this.admin = admin;
1074
1298
  this.snack = snack;
1075
1299
  }
1076
- ngOnInit() {
1300
+ async ngOnInit() {
1077
1301
  const cfg = this.admin.getEffectiveConfig();
1078
1302
  const flat = this.flatten(cfg);
1079
1303
  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;
1304
+ this.applyConfigSnapshot(flat, { resetForms: false });
1305
+ await this.loadProviderCatalog();
1306
+ this.applyProviderOptions();
1307
+ this.applyEmbeddingProviderOptions();
1308
+ const providerSafe = this.safeName('ai.provider');
1309
+ const apiKeySafe = this.safeName('ai.apiKey');
1310
+ const selectedProvider = this.currentValues[providerSafe] || this.resolveDefaultProvider();
1311
+ if (selectedProvider && !this.currentValues[providerSafe]) {
1312
+ this.currentValues[providerSafe] = selectedProvider;
1313
+ if (!this.initialValues[providerSafe]) {
1314
+ this.initialValues[providerSafe] = selectedProvider;
1315
+ }
1316
+ this.setFieldDefaultValue(providerSafe, selectedProvider);
1089
1317
  }
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;
1318
+ this.updateSelectedProvider(selectedProvider);
1319
+ this.updateApiKeyState(selectedProvider, this.currentValues[apiKeySafe]);
1320
+ this.syncEmbeddingDefaults(false);
1321
+ this.ensureEmbeddingModelDefaults(false);
1322
+ this.ensureEmbeddingDimensionsDefaults(false);
1323
+ await this.refreshStoredConfigState();
1324
+ // Monitor API Key changes to auto-fetch
1325
+ this.apiKeyChanged$.pipe(debounceTime(1500), // Wait for typing to stop
1326
+ distinctUntilChanged((prev, curr) => prev.apiKey === curr.apiKey && prev.provider === curr.provider), filter(({ apiKey, provider }) => this.canAutoRefreshModels(provider, apiKey))).subscribe(() => {
1327
+ this.refreshModels();
1328
+ });
1329
+ if (this.hasApiKey && this.selectedProvider?.supportsModels !== false) {
1330
+ // Auto-fetch models on load if we have a key (silent)
1331
+ this.refreshModels(false, true);
1097
1332
  }
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);
1333
+ this.bootstrapDynamicForms();
1334
+ }
1335
+ ngAfterViewInit() {
1336
+ this.ensureAiModelForm();
1337
+ }
1338
+ onValueChange(sectionId, ev) {
1339
+ this.isValid$.next(!!ev.isValid);
1340
+ // Mesclar valores por seção (cada form emite somente seu grupo)
1341
+ this.currentValues = { ...this.currentValues, ...ev.formData };
1342
+ if (sectionId === 'ai-credentials') {
1343
+ const apiKeySafe = this.safeName('ai.apiKey');
1344
+ const providerSafe = this.safeName('ai.provider');
1345
+ const apiKey = ev.formData[apiKeySafe];
1346
+ const provider = ev.formData[providerSafe] || this.currentValues[providerSafe] || this.resolveDefaultProvider();
1347
+ const providerChanged = provider !== this.selectedProvider?.id;
1348
+ if (providerChanged) {
1349
+ this.updateSelectedProvider(provider);
1350
+ this.aiTestResult = null;
1351
+ this.availableModels = [];
1352
+ this.pendingModelOptions = [];
1353
+ this.selectedModelDetails = '';
1354
+ this.clearModelSelection(this.selectedProvider?.defaultModel);
1355
+ this.applyEmbeddingProviderOptions();
1356
+ }
1357
+ this.updateApiKeyState(provider, apiKey);
1358
+ this.syncEmbeddingDefaults();
1359
+ this.ensureEmbeddingModelDefaults();
1360
+ this.ensureEmbeddingDimensionsDefaults();
1361
+ if (apiKey !== this.initialValues[apiKeySafe] || providerChanged) {
1362
+ this.apiKeyChanged$.next({ apiKey: apiKey || '', provider });
1363
+ // Reset test result when key changes
1364
+ if (this.aiTestResult) {
1365
+ this.aiTestResult = null;
1366
+ }
1367
+ }
1368
+ if (this.hasApiKey) {
1369
+ setTimeout(() => this.ensureAiModelForm(), 0);
1370
+ }
1371
+ else {
1372
+ if (this.aiModelFormRef) {
1373
+ try {
1374
+ this.aiModelFormRef.destroy();
1112
1375
  }
1376
+ catch { }
1377
+ this.componentRefs.delete('ai-model');
1378
+ this.aiModelFormRef = null;
1113
1379
  }
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);
1380
+ this.availableModels = [];
1381
+ this.pendingModelOptions = [];
1382
+ }
1383
+ }
1384
+ if (sectionId === 'ai-model') {
1385
+ const modelSafe = this.safeName('ai.model');
1386
+ const model = ev.formData[modelSafe];
1387
+ this.updateModelDetails(model);
1388
+ }
1389
+ if (sectionId === 'ai-embedding') {
1390
+ this.syncEmbeddingDefaults();
1391
+ this.ensureEmbeddingModelDefaults();
1392
+ this.ensureEmbeddingDimensionsDefaults();
1393
+ }
1394
+ const firstTime = !this.bootstrappedSections.has(sectionId);
1395
+ if (firstTime) {
1396
+ this.initialValues = { ...this.initialValues, ...ev.formData };
1397
+ this.bootstrappedSections.add(sectionId);
1398
+ }
1399
+ this.isDirty$.next(!this.shallowEqual(this.currentValues, this.initialValues));
1400
+ }
1401
+ buildSectionConfig(sectionId) {
1402
+ const sec = (this.formConfig.sections || []).find((s) => s.id === sectionId);
1403
+ if (!sec)
1404
+ return { sections: [], fieldMetadata: [] };
1405
+ const names = new Set();
1406
+ for (const row of sec.rows || []) {
1407
+ for (const col of row.columns || []) {
1408
+ for (const fname of col.fields || [])
1409
+ names.add(fname);
1410
+ }
1411
+ }
1412
+ const fms = (this.formConfig.fieldMetadata || []).filter((f) => names.has(f.name));
1413
+ return { sections: [sec], fieldMetadata: fms };
1414
+ }
1415
+ bootstrapDynamicForms() {
1416
+ if (!this.dynamicFormCtor) {
1417
+ if (!this.loggedMissingDynamicForm) {
1418
+ 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' }));
1419
+ this.loggedMissingDynamicForm = true;
1420
+ }
1421
+ return;
1422
+ }
1423
+ const groups = [
1424
+ { id: 'crud', host: this.hostCrud, cfg: this.buildSectionConfig('crud') },
1425
+ { id: 'dynamic-fields', host: this.hostFields, cfg: this.buildSectionConfig('dynamic-fields') },
1426
+ { id: 'cache', host: this.hostCache, cfg: this.buildSectionConfig('cache') },
1427
+ { id: 'table', host: this.hostTable, cfg: this.buildSectionConfig('table') },
1428
+ { id: 'dialog', host: this.hostDialog, cfg: this.buildSectionConfig('dialog') },
1429
+ { id: 'ai-credentials', host: this.hostAiCredentials, cfg: this.buildSectionConfig('ai-credentials') },
1430
+ { id: 'ai-embedding', host: this.hostAiEmbedding, cfg: this.buildSectionConfig('ai-embedding') },
1431
+ ];
1432
+ for (const g of groups) {
1433
+ try {
1434
+ if (!g.host) {
1435
+ this.logger.warnOnce('[GlobalConfigEditor] Missing host for section', this.buildLogOptions({ sectionId: g.id }, {
1436
+ dedupeKey: `global-config-editor:missing-host:${g.id}`,
1437
+ context: { actionId: `section.${g.id}` },
1438
+ }));
1439
+ continue;
1440
+ }
1441
+ this.logger.debug('[GlobalConfigEditor] Creating form section', this.buildLogOptions({ sectionId: g.id, config: g.cfg }, {
1442
+ context: { actionId: `section.${g.id}` },
1443
+ throttleKey: 'global-config-editor:create-form-section',
1444
+ }));
1445
+ const ref = g.host.createComponent(this.dynamicFormCtor);
1125
1446
  ref.setInput('config', g.cfg);
1126
1447
  ref.setInput('mode', 'edit');
1448
+ // Não habilitar o modo de edição de layout (canvas) — queremos apenas edição de valores
1127
1449
  ref.setInput('editModeEnabled', false);
1128
1450
  const inst = ref.instance;
1451
+ // FORÇAR estado de sucesso para renderizar o formulário sem resourcePath
1452
+ inst.initializationStatus = 'success';
1453
+ inst.isLoading = false;
1129
1454
  try {
1130
1455
  if (typeof inst.buildFormFromConfig === 'function')
1131
1456
  inst.buildFormFromConfig();
1132
1457
  }
1133
- catch { }
1458
+ catch (err) {
1459
+ this.logger.warnOnce('[GlobalConfigEditor] buildFormFromConfig failed', this.buildLogOptions({ sectionId: g.id, error: err }, {
1460
+ context: { actionId: `section.${g.id}.build-form` },
1461
+ dedupeKey: `global-config-editor:build-form-failed:${g.id}`,
1462
+ }));
1463
+ }
1134
1464
  try {
1135
1465
  inst.onSubmit = () => { };
1136
1466
  }
@@ -1142,35 +1472,425 @@ class GlobalConfigEditorComponent {
1142
1472
  if (inst?.valueChange?.subscribe) {
1143
1473
  inst.valueChange.subscribe((ev) => this.onValueChange(g.id, ev));
1144
1474
  }
1475
+ this.componentRefs.set(g.id, ref);
1145
1476
  if (g.id === 'dialog') {
1146
1477
  // Guardar referência para facilitar setValue em campos de ícone
1147
1478
  this.dialogFormInst = inst;
1148
1479
  }
1480
+ if (g.id === 'ai-embedding') {
1481
+ this.aiEmbeddingFormRef = ref;
1482
+ }
1149
1483
  this.destroyRef.onDestroy(() => { try {
1150
1484
  ref.destroy();
1151
1485
  }
1152
1486
  catch { } });
1153
1487
  }
1154
- });
1488
+ catch (err) {
1489
+ this.logger.error('[GlobalConfigEditor] Failed to create form section', this.buildLogOptions({ sectionId: g.id, error: err }, { context: { actionId: `section.${g.id}.create` } }));
1490
+ }
1491
+ }
1492
+ // Criar a seção de modelo somente quando a chave estiver disponível e o host existir
1493
+ this.ensureAiModelForm();
1155
1494
  }
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);
1495
+ ensureAiModelForm() {
1496
+ if (!this.hasApiKey)
1497
+ return;
1498
+ if (!this.aiModelFormRef) {
1499
+ this.createAiModelForm();
1500
+ }
1501
+ }
1502
+ async loadProviderCatalog() {
1503
+ try {
1504
+ const response = await firstValueFrom(this.aiApi.listProviderCatalog());
1505
+ const providers = Array.isArray(response?.providers) ? response.providers : [];
1506
+ this.providers = providers.filter((p) => p && p.id);
1507
+ }
1508
+ catch (err) {
1509
+ this.logger.warnOnce('[GlobalConfigEditor] Failed to load provider catalog, using fallback.', this.buildLogOptions({ error: err }, {
1510
+ context: { actionId: 'providers.load-catalog' },
1511
+ dedupeKey: 'global-config-editor:provider-catalog-fallback',
1512
+ }));
1513
+ this.providers = this.buildFallbackProviders();
1514
+ }
1515
+ }
1516
+ buildFallbackProviders() {
1517
+ return [
1518
+ {
1519
+ id: 'gemini',
1520
+ label: 'Google Gemini',
1521
+ description: 'Modelos rápidos e multimodais do Google.',
1522
+ defaultModel: 'gemini-2.0-flash',
1523
+ requiresApiKey: true,
1524
+ supportsModels: true,
1525
+ supportsEmbeddings: true,
1526
+ iconKey: 'gemini',
1527
+ },
1528
+ {
1529
+ id: 'openai',
1530
+ label: 'OpenAI',
1531
+ description: 'Modelos GPT para texto e chat.',
1532
+ defaultModel: 'gpt-4o-mini',
1533
+ requiresApiKey: true,
1534
+ supportsModels: true,
1535
+ supportsEmbeddings: true,
1536
+ iconKey: 'openai',
1537
+ },
1538
+ {
1539
+ id: 'xai',
1540
+ label: 'xAI (Grok)',
1541
+ description: 'Modelos Grok focados em raciocínio.',
1542
+ defaultModel: 'grok-2-latest',
1543
+ requiresApiKey: true,
1544
+ supportsModels: true,
1545
+ supportsEmbeddings: false,
1546
+ iconKey: 'xai',
1547
+ },
1548
+ {
1549
+ id: 'mock',
1550
+ label: 'Mock (dev)',
1551
+ description: 'Modo local para testes sem chave.',
1552
+ defaultModel: 'mock-default',
1553
+ requiresApiKey: false,
1554
+ supportsModels: true,
1555
+ supportsEmbeddings: true,
1556
+ iconKey: 'mock',
1557
+ },
1558
+ ];
1559
+ }
1560
+ applyProviderOptions() {
1561
+ const options = this.providers.map((provider) => ({
1562
+ text: provider.label || provider.id,
1563
+ value: provider.id,
1564
+ }));
1565
+ this.setProviderFieldOptions(options);
1566
+ }
1567
+ applyEmbeddingProviderOptions() {
1568
+ const options = this.providers
1569
+ .filter((provider) => provider?.supportsEmbeddings !== false || provider?.id === 'openai')
1570
+ .map((provider) => ({
1571
+ text: provider.label || provider.id,
1572
+ value: provider.id,
1573
+ }));
1574
+ this.setEmbeddingProviderFieldOptions(options);
1575
+ }
1576
+ setProviderFieldOptions(providerOptions) {
1577
+ const safeProviderName = this.safeName('ai.provider');
1578
+ const providerField = (this.formConfig.fieldMetadata || []).find((f) => f.name === safeProviderName);
1579
+ if (providerField) {
1580
+ providerField.selectOptions = providerOptions;
1581
+ }
1582
+ }
1583
+ setEmbeddingProviderFieldOptions(providerOptions) {
1584
+ const safeProviderName = this.safeName('ai.embedding.provider');
1585
+ const providerField = (this.formConfig.fieldMetadata || []).find((f) => f.name === safeProviderName);
1586
+ if (providerField) {
1587
+ providerField.selectOptions = providerOptions;
1588
+ }
1589
+ }
1590
+ resolveDefaultProvider() {
1591
+ const preferred = this.providers.find((provider) => provider.id === 'gemini') || this.providers[0];
1592
+ return preferred?.id || 'gemini';
1593
+ }
1594
+ setFieldDefaultValue(safeName, value) {
1595
+ const field = (this.formConfig.fieldMetadata || []).find((f) => f.name === safeName);
1596
+ if (field) {
1597
+ field.defaultValue = value;
1598
+ }
1599
+ }
1600
+ updateSelectedProvider(providerId) {
1601
+ this.selectedProvider = this.findProvider(providerId) || null;
1602
+ }
1603
+ updateApiKeyState(providerId, apiKey) {
1604
+ const meta = this.findProvider(providerId);
1605
+ const requiresKey = meta?.requiresApiKey !== false;
1606
+ this.hasApiKey = !requiresKey || !!apiKey || this.hasStoredApiKey;
1607
+ }
1608
+ get embeddingUseSameAsLlm() {
1609
+ return !!this.currentValues?.[this.safeName('ai.embedding.useSameAsLlm')];
1610
+ }
1611
+ get embeddingDimensionMismatch() {
1612
+ const raw = this.currentValues?.[this.safeName('ai.embedding.dimensions')];
1613
+ if (raw === undefined || raw === null || raw === '')
1614
+ return false;
1615
+ const parsed = Number(raw);
1616
+ if (!Number.isFinite(parsed))
1617
+ return false;
1618
+ return parsed !== 768;
1619
+ }
1620
+ get canUseLlmForEmbeddings() {
1621
+ return !!this.selectedProvider?.supportsEmbeddings;
1622
+ }
1623
+ canAutoRefreshModels(providerId, apiKey) {
1624
+ const meta = this.findProvider(providerId);
1625
+ if (meta?.supportsModels === false)
1626
+ return false;
1627
+ if (meta?.requiresApiKey === false)
1628
+ return true;
1629
+ if (apiKey && apiKey.length > 10)
1630
+ return true;
1631
+ return this.hasStoredApiKey;
1632
+ }
1633
+ ensureDefaultModelSelection() {
1634
+ const modelSafe = this.safeName('ai.model');
1635
+ const current = this.currentValues[modelSafe];
1636
+ if (current)
1637
+ return;
1638
+ const fallback = this.selectedProvider?.defaultModel;
1639
+ if (!fallback)
1640
+ return;
1641
+ this.setFieldDefaultValue(modelSafe, fallback);
1642
+ this.currentValues[modelSafe] = fallback;
1643
+ this.updateModelDetails(fallback);
1644
+ }
1645
+ clearModelSelection(defaultModel) {
1646
+ const modelSafe = this.safeName('ai.model');
1647
+ const nextValue = defaultModel || '';
1648
+ this.currentValues[modelSafe] = nextValue;
1649
+ this.setFieldDefaultValue(modelSafe, nextValue);
1650
+ this.updateModelDetails(nextValue);
1651
+ const inst = this.aiModelFormRef?.instance;
1652
+ const ctrl = inst?.form?.get?.(modelSafe);
1653
+ if (ctrl) {
1654
+ ctrl.setValue(nextValue);
1655
+ }
1656
+ }
1657
+ findProvider(providerId) {
1658
+ return this.providers.find((provider) => provider.id === providerId);
1659
+ }
1660
+ setModelFieldOptions(modelOptions) {
1661
+ const safeModelName = this.safeName('ai.model');
1662
+ const aiModelField = (this.formConfig.fieldMetadata || []).find((f) => f.name === safeModelName);
1663
+ if (aiModelField) {
1664
+ aiModelField.selectOptions = modelOptions;
1665
+ }
1666
+ }
1667
+ applyConfigSnapshot(flat, opts) {
1668
+ this.pathMap = {};
1669
+ for (const fm of this.formConfig.fieldMetadata || []) {
1670
+ const da = fm.dataAttributes || {};
1671
+ const path = da.globalPath || fm.name;
1672
+ this.pathMap[fm.name] = path;
1673
+ const v = flat[path];
1674
+ if (v !== undefined)
1675
+ fm.defaultValue = v;
1676
+ }
1677
+ this.initialValues = {};
1678
+ for (const safe of Object.keys(this.pathMap)) {
1679
+ const path = this.pathMap[safe];
1680
+ const v = flat[path];
1681
+ if (v !== undefined)
1682
+ this.initialValues[safe] = v;
1683
+ }
1684
+ this.currentValues = { ...this.initialValues };
1685
+ this.apiKeyLast4 = flat['ai.apiKeyLast4'] || null;
1686
+ this.hasStoredApiKey = !!flat['ai.hasApiKey'] || !!this.apiKeyLast4;
1687
+ if (opts.resetForms) {
1688
+ const resetValues = {};
1689
+ for (const safe of Object.keys(this.pathMap)) {
1690
+ if (safe in this.initialValues) {
1691
+ resetValues[safe] = this.initialValues[safe];
1692
+ }
1693
+ else {
1694
+ resetValues[safe] = null;
1695
+ }
1696
+ }
1697
+ this.patchFormsWithValues(resetValues);
1698
+ this.bootstrappedSections = new Set(this.allSections);
1699
+ this.isDirty$.next(false);
1700
+ }
1701
+ }
1702
+ patchFormsWithValues(values) {
1703
+ for (const ref of this.componentRefs.values()) {
1704
+ const inst = ref?.instance;
1705
+ const form = inst?.form;
1706
+ if (form?.patchValue) {
1707
+ try {
1708
+ form.patchValue(values, { emitEvent: false });
1709
+ }
1710
+ catch { }
1711
+ }
1712
+ }
1713
+ }
1714
+ refreshAiStateAfterConfig() {
1715
+ const providerSafe = this.safeName('ai.provider');
1716
+ const apiKeySafe = this.safeName('ai.apiKey');
1717
+ const selectedProvider = this.currentValues[providerSafe] || this.resolveDefaultProvider();
1718
+ if (selectedProvider && !this.currentValues[providerSafe]) {
1719
+ this.currentValues[providerSafe] = selectedProvider;
1720
+ if (!this.initialValues[providerSafe]) {
1721
+ this.initialValues[providerSafe] = selectedProvider;
1722
+ }
1723
+ this.setFieldDefaultValue(providerSafe, selectedProvider);
1724
+ }
1725
+ this.updateSelectedProvider(selectedProvider);
1726
+ this.updateApiKeyState(selectedProvider, this.currentValues[apiKeySafe]);
1727
+ this.syncEmbeddingDefaults(false);
1728
+ this.ensureEmbeddingModelDefaults(false);
1729
+ this.ensureEmbeddingDimensionsDefaults(false);
1730
+ this.updateModelDetails(this.currentValues[this.safeName('ai.model')] || '');
1731
+ }
1732
+ async refreshStoredConfigState() {
1733
+ try {
1734
+ this.hasStoredGlobalConfig = await this.admin.hasStoredConfig();
1735
+ }
1736
+ catch {
1737
+ this.hasStoredGlobalConfig = false;
1738
+ }
1739
+ this.configSourceLabel = this.hasStoredGlobalConfig
1740
+ ? 'Configuração salva (UI) — sobrescreve os defaults do servidor'
1741
+ : 'Padrões do servidor (env vars)';
1742
+ }
1743
+ async clearStoredConfig() {
1744
+ if (this.isClearingGlobalConfig)
1745
+ return;
1746
+ this.isClearingGlobalConfig = true;
1747
+ try {
1748
+ await this.admin.clearStoredConfig();
1749
+ const cfg = this.admin.getEffectiveConfig();
1750
+ const flat = this.flatten(cfg);
1751
+ this.applyConfigSnapshot(flat, { resetForms: true });
1752
+ this.aiTestResult = null;
1753
+ this.refreshAiStateAfterConfig();
1754
+ await this.refreshStoredConfigState();
1755
+ try {
1756
+ this.snack.open('Configuração global limpa. Usando defaults do servidor.', undefined, { duration: 2500 });
1167
1757
  }
1758
+ catch { }
1759
+ }
1760
+ catch (err) {
1761
+ try {
1762
+ this.snack.open('Falha ao limpar a configuração global.', undefined, { duration: 4000 });
1763
+ }
1764
+ catch { }
1765
+ }
1766
+ finally {
1767
+ this.isClearingGlobalConfig = false;
1768
+ this.cdr.detectChanges();
1769
+ }
1770
+ }
1771
+ syncEmbeddingDefaults(markDirty = true) {
1772
+ const useSameSafe = this.safeName('ai.embedding.useSameAsLlm');
1773
+ const embeddingProviderSafe = this.safeName('ai.embedding.provider');
1774
+ const embeddingApiKeySafe = this.safeName('ai.embedding.apiKey');
1775
+ const useSame = !!this.currentValues[useSameSafe];
1776
+ if (!useSame)
1777
+ return;
1778
+ if (!this.canUseLlmForEmbeddings) {
1779
+ this.setEmbeddingValue(useSameSafe, false, markDirty);
1168
1780
  return;
1169
1781
  }
1170
- this.isDirty$.next(!this.shallowEqual(this.currentValues, this.initialValues));
1782
+ const llmProvider = this.currentValues[this.safeName('ai.provider')] || this.resolveDefaultProvider();
1783
+ const llmApiKey = this.currentValues[this.safeName('ai.apiKey')] || '';
1784
+ this.setEmbeddingValue(embeddingProviderSafe, llmProvider, markDirty);
1785
+ this.setEmbeddingValue(embeddingApiKeySafe, llmApiKey, markDirty);
1786
+ }
1787
+ ensureEmbeddingModelDefaults(markDirty = true) {
1788
+ const providerSafe = this.safeName('ai.embedding.provider');
1789
+ const modelSafe = this.safeName('ai.embedding.model');
1790
+ const provider = this.currentValues[providerSafe];
1791
+ const model = this.currentValues[modelSafe];
1792
+ if (provider === 'openai' && !model) {
1793
+ this.setEmbeddingValue(modelSafe, this.embeddingModelOptions[0]?.value || 'text-embedding-3-large', markDirty);
1794
+ }
1795
+ }
1796
+ ensureEmbeddingDimensionsDefaults(markDirty = true) {
1797
+ const providerSafe = this.safeName('ai.embedding.provider');
1798
+ const dimensionsSafe = this.safeName('ai.embedding.dimensions');
1799
+ const provider = this.currentValues[providerSafe];
1800
+ const dimensions = this.currentValues[dimensionsSafe];
1801
+ if (provider === 'openai' && (dimensions === undefined || dimensions === null || dimensions === '')) {
1802
+ this.setEmbeddingValue(dimensionsSafe, 768, markDirty);
1803
+ }
1804
+ }
1805
+ useLlmForEmbeddings() {
1806
+ if (!this.canUseLlmForEmbeddings)
1807
+ return;
1808
+ const useSameSafe = this.safeName('ai.embedding.useSameAsLlm');
1809
+ this.setEmbeddingValue(useSameSafe, true);
1810
+ this.syncEmbeddingDefaults();
1811
+ }
1812
+ createAiModelForm() {
1813
+ if (!this.dynamicFormCtor || !this.hostAiModel)
1814
+ return;
1815
+ const cfg = this.buildSectionConfig('ai-model');
1816
+ if (!cfg.sections?.length)
1817
+ return;
1818
+ try {
1819
+ // Apply pending options to metadata before creating
1820
+ if (this.pendingModelOptions.length) {
1821
+ this.setModelFieldOptions(this.pendingModelOptions);
1822
+ }
1823
+ const ref = this.hostAiModel.createComponent(this.dynamicFormCtor);
1824
+ ref.setInput('config', cfg);
1825
+ ref.setInput('mode', 'edit');
1826
+ ref.setInput('editModeEnabled', false);
1827
+ const inst = ref.instance;
1828
+ inst.initializationStatus = 'success';
1829
+ inst.isLoading = false;
1830
+ try {
1831
+ if (typeof inst.buildFormFromConfig === 'function')
1832
+ inst.buildFormFromConfig();
1833
+ }
1834
+ catch (err) {
1835
+ this.logger.warnOnce('[GlobalConfigEditor] buildFormFromConfig failed', this.buildLogOptions({ sectionId: 'ai-model', error: err }, {
1836
+ context: { actionId: 'section.ai-model.build-form' },
1837
+ dedupeKey: 'global-config-editor:build-form-failed:ai-model',
1838
+ }));
1839
+ }
1840
+ try {
1841
+ inst.onSubmit = () => { };
1842
+ }
1843
+ catch { }
1844
+ try {
1845
+ ref.changeDetectorRef.detectChanges();
1846
+ }
1847
+ catch { }
1848
+ if (inst?.valueChange?.subscribe) {
1849
+ inst.valueChange.subscribe((ev) => this.onValueChange('ai-model', ev));
1850
+ }
1851
+ this.componentRefs.set('ai-model', ref);
1852
+ this.aiModelFormRef = ref;
1853
+ this.updateModelDetails(this.currentValues[this.safeName('ai.model')] || '');
1854
+ this.destroyRef.onDestroy(() => { try {
1855
+ ref.destroy();
1856
+ }
1857
+ catch { } });
1858
+ }
1859
+ catch (err) {
1860
+ this.logger.error('[GlobalConfigEditor] Failed to create AI model form', this.buildLogOptions({ error: err }, { context: { actionId: 'section.ai-model.create' } }));
1861
+ }
1171
1862
  }
1172
- hasBootstrappedAll() {
1173
- return this.allSections.every((s) => this.bootstrappedSections.has(s));
1863
+ setEmbeddingValue(safeName, value, markDirty = true) {
1864
+ this.currentValues[safeName] = value;
1865
+ const inst = this.aiEmbeddingFormRef?.instance;
1866
+ const ctrl = inst?.form?.get?.(safeName);
1867
+ if (ctrl) {
1868
+ ctrl.setValue(value);
1869
+ }
1870
+ if (markDirty) {
1871
+ this.isDirty$.next(!this.shallowEqual(this.currentValues, this.initialValues));
1872
+ }
1873
+ }
1874
+ recreateAiModelForm() {
1875
+ if (this.aiModelFormRef) {
1876
+ try {
1877
+ this.aiModelFormRef.destroy();
1878
+ }
1879
+ catch { }
1880
+ this.componentRefs.delete('ai-model');
1881
+ this.aiModelFormRef = null;
1882
+ }
1883
+ this.createAiModelForm();
1884
+ }
1885
+ updateModelDetails(modelName) {
1886
+ const model = this.availableModels.find(m => m.name.endsWith(modelName));
1887
+ if (model) {
1888
+ const formatLimit = (n) => n ? (n >= 1000 ? (n / 1000).toFixed(0) + 'k' : n) : 'N/A';
1889
+ this.selectedModelDetails = `Modelo: ${model.displayName || modelName} | Tokens (in/out): ${formatLimit(model.inputTokenLimit)}/${formatLimit(model.outputTokenLimit)} | Métodos: ${model.supportedGenerationMethods?.join(', ') || 'N/A'}`;
1890
+ }
1891
+ else {
1892
+ this.selectedModelDetails = 'Detalhes do modelo não disponíveis.';
1893
+ }
1174
1894
  }
1175
1895
  // SettingsPanel expects these methods
1176
1896
  reset() {
@@ -1185,30 +1905,217 @@ class GlobalConfigEditorComponent {
1185
1905
  this.isDirty$.next(false);
1186
1906
  }
1187
1907
  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
- }
1908
+ const flat = this.buildChangedValues();
1194
1909
  return this.toNested(flat);
1195
1910
  }
1196
- onSave() {
1911
+ async onSave() {
1197
1912
  const partial = this.getSettingsValue();
1198
- this.admin.save(partial);
1199
- // Update baseline after save
1200
- this.initialValues = { ...this.currentValues };
1201
- this.isDirty$.next(false);
1913
+ this.isBusy$.next(true);
1202
1914
  try {
1203
- this.snack.open('Configurações salvas', undefined, { duration: 2000 });
1915
+ await this.admin.save(partial);
1916
+ // Update baseline after save
1917
+ this.initialValues = { ...this.currentValues };
1918
+ this.isDirty$.next(false);
1919
+ try {
1920
+ this.snack.open('Configurações salvas', undefined, { duration: 2000 });
1921
+ }
1922
+ catch { }
1923
+ return partial;
1204
1924
  }
1205
- catch { }
1206
- return partial;
1925
+ catch (err) {
1926
+ const message = this.resolveSaveErrorMessage(err);
1927
+ try {
1928
+ this.snack.open(message, undefined, { duration: 4000 });
1929
+ }
1930
+ catch { }
1931
+ return undefined;
1932
+ }
1933
+ finally {
1934
+ this.isBusy$.next(false);
1935
+ }
1936
+ }
1937
+ async refreshModels(force = false, silent = false) {
1938
+ const apiKey = this.currentValues[this.safeName('ai.apiKey')] || '';
1939
+ const provider = this.currentValues[this.safeName('ai.provider')] || this.resolveDefaultProvider();
1940
+ const meta = this.findProvider(provider);
1941
+ if (meta?.supportsModels === false) {
1942
+ if (!silent) {
1943
+ this.aiTestResult = { success: false, message: 'Este provedor não oferece catálogo de modelos.' };
1944
+ this.cdr.detectChanges();
1945
+ }
1946
+ return;
1947
+ }
1948
+ const requiresKey = meta?.requiresApiKey !== false;
1949
+ const hasKey = !!apiKey || this.hasStoredApiKey || !requiresKey;
1950
+ if (requiresKey && !hasKey) {
1951
+ this.aiTestResult = { success: false, message: 'Insira uma chave de API para buscar modelos.' };
1952
+ this.cdr.detectChanges();
1953
+ return;
1954
+ }
1955
+ if (!silent) {
1956
+ this.isRefetchingModels = true;
1957
+ this.cdr.detectChanges();
1958
+ }
1959
+ let modelOptions = [];
1960
+ try {
1961
+ const request = { provider };
1962
+ if (apiKey)
1963
+ request.apiKey = apiKey;
1964
+ const response = await firstValueFrom(this.aiApi.listModels(request));
1965
+ if (!response?.success) {
1966
+ const msg = response?.message || 'Erro ao buscar modelos.';
1967
+ throw new Error(msg);
1968
+ }
1969
+ const models = response.models || [];
1970
+ this.availableModels = models; // Store available models
1971
+ const formatLimit = (n) => n ? (n >= 1000 ? (n / 1000).toFixed(0) + 'k' : n) : '?';
1972
+ modelOptions = (models || [])
1973
+ .filter((m) => m.name)
1974
+ .map((m) => {
1975
+ const shortName = m.name.split('/').pop() || m.name;
1976
+ const details = `${formatLimit(m.inputTokenLimit)} in / ${formatLimit(m.outputTokenLimit)} out`;
1977
+ return {
1978
+ text: `${m.displayName || shortName} (${details})`,
1979
+ value: shortName
1980
+ };
1981
+ });
1982
+ if (modelOptions.length || force) {
1983
+ this.applyModelOptions(modelOptions, silent);
1984
+ }
1985
+ }
1986
+ catch (error) {
1987
+ this.logger.error('[GlobalConfigEditor] Error fetching models', this.buildLogOptions({ error, provider }, { context: { actionId: 'models.fetch' } }));
1988
+ this.aiTestResult = { success: false, message: error.message || 'Erro ao buscar modelos.' };
1989
+ if (!silent) {
1990
+ this.snack.open('Erro ao buscar modelos. Verifique a chave de API.', 'Fechar', { duration: 5000 });
1991
+ }
1992
+ }
1993
+ finally {
1994
+ this.logger.debug('[GlobalConfigEditor] refreshModels complete.', this.buildLogOptions({ modelOptionsCount: modelOptions?.length ?? 0, hasAiModelFormRef: !!this.aiModelFormRef }, {
1995
+ context: { actionId: 'models.refresh' },
1996
+ throttleKey: 'global-config-editor:refresh-models-complete',
1997
+ }));
1998
+ this.isRefetchingModels = false;
1999
+ this.cdr.detectChanges();
2000
+ }
2001
+ }
2002
+ // New method to apply model options to the form
2003
+ applyModelOptions(modelOptions, silent = false) {
2004
+ // Store if form ref not ready yet
2005
+ if (!this.aiModelFormRef) {
2006
+ this.pendingModelOptions = modelOptions; // Store for later application
2007
+ this.logger.warnOnce('[GlobalConfigEditor] aiModelFormRef not ready. Storing model options.', this.buildLogOptions({ modelOptionsCount: modelOptions.length }, {
2008
+ context: { actionId: 'models.apply-options' },
2009
+ dedupeKey: 'global-config-editor:ai-model-form-ref-not-ready',
2010
+ }));
2011
+ this.ensureAiModelForm();
2012
+ return;
2013
+ }
2014
+ this.pendingModelOptions = modelOptions;
2015
+ this.setModelFieldOptions(modelOptions);
2016
+ this.ensureDefaultModelSelection();
2017
+ this.recreateAiModelForm();
2018
+ if (!silent) {
2019
+ try {
2020
+ this.snack.open(`Lista de modelos atualizada (${modelOptions.length} modelos)`, undefined, { duration: 3000 });
2021
+ }
2022
+ catch { }
2023
+ }
2024
+ this.pendingModelOptions = [];
2025
+ }
2026
+ async testAiConnection() {
2027
+ this.isTestingAi = true;
2028
+ this.aiTestResult = null;
2029
+ this.cdr.detectChanges(); // Force update to show spinner
2030
+ // Get current value from form (even if not saved)
2031
+ const apiKey = this.currentValues[this.safeName('ai.apiKey')] || '';
2032
+ const provider = this.currentValues[this.safeName('ai.provider')] || this.resolveDefaultProvider();
2033
+ const meta = this.findProvider(provider);
2034
+ const selectedModel = this.currentValues[this.safeName('ai.model')] || meta?.defaultModel || 'gemini-2.0-flash';
2035
+ const requiresKey = meta?.requiresApiKey !== false;
2036
+ const hasKey = !!apiKey || this.hasStoredApiKey || !requiresKey;
2037
+ if (requiresKey && !hasKey) {
2038
+ this.aiTestResult = { success: false, message: 'Insira uma chave de API para testar.' };
2039
+ this.isTestingAi = false;
2040
+ this.cdr.detectChanges();
2041
+ return;
2042
+ }
2043
+ try {
2044
+ const request = { provider, model: selectedModel };
2045
+ if (apiKey)
2046
+ request.apiKey = apiKey;
2047
+ const response = await firstValueFrom(this.aiApi.testProvider(request));
2048
+ if (!response?.success) {
2049
+ throw new Error(response?.message || 'Falha ao testar conexao.');
2050
+ }
2051
+ this.aiTestResult = { success: true, message: response.message || 'Conexao estabelecida com sucesso!' };
2052
+ this.hasApiKey = true; // Garantir desbloqueio da seção de modelo após sucesso
2053
+ // Após validar a chave, já buscar a lista de modelos para preencher o select
2054
+ await this.refreshModels(true);
2055
+ }
2056
+ catch (err) {
2057
+ this.logger.error('[GlobalConfigEditor] AI Connection Test Error', this.buildLogOptions({ error: err, provider, model: selectedModel }, { context: { actionId: 'provider.test-connection' } }));
2058
+ const msg = err?.message || 'Erro desconhecido';
2059
+ this.aiTestResult = { success: false, message: msg };
2060
+ }
2061
+ finally {
2062
+ this.isTestingAi = false;
2063
+ this.cdr.detectChanges(); // Force update to hide spinner
2064
+ }
2065
+ }
2066
+ buildLogOptions(data, options = {}) {
2067
+ return {
2068
+ ...options,
2069
+ context: {
2070
+ ...this.logContext,
2071
+ ...(options.context ?? {}),
2072
+ },
2073
+ data,
2074
+ };
1207
2075
  }
1208
2076
  // ===== Helpers de UX para ícones de Dialog (variants) =====
1209
2077
  safeName(path) {
1210
2078
  return path.replace(/[^a-zA-Z0-9_]/g, '_');
1211
2079
  }
2080
+ buildChangedValues() {
2081
+ const out = {};
2082
+ for (const [safe, value] of Object.entries(this.currentValues)) {
2083
+ const initial = this.initialValues[safe];
2084
+ if (!this.shouldIncludeField(initial, value))
2085
+ continue;
2086
+ const path = this.pathMap[safe] || safe;
2087
+ out[path] = value;
2088
+ }
2089
+ return out;
2090
+ }
2091
+ shouldIncludeField(initial, current) {
2092
+ if (initial === undefined) {
2093
+ if (current === null || current === undefined)
2094
+ return false;
2095
+ if (typeof current === 'string' && current.trim() === '')
2096
+ return false;
2097
+ if (Array.isArray(current) && current.length === 0)
2098
+ return false;
2099
+ }
2100
+ return initial !== current;
2101
+ }
2102
+ resolveSaveErrorMessage(err) {
2103
+ const apiMessage = typeof err?.error?.message === 'string' ? err.error.message : null;
2104
+ const details = err?.error?.details;
2105
+ if (apiMessage && apiMessage.trim()) {
2106
+ return apiMessage;
2107
+ }
2108
+ if (details && typeof details === 'object') {
2109
+ const first = Object.values(details).find((v) => typeof v === 'string' && v.trim());
2110
+ if (typeof first === 'string') {
2111
+ return first;
2112
+ }
2113
+ }
2114
+ if (typeof err?.message === 'string' && err.message.trim()) {
2115
+ return err.message;
2116
+ }
2117
+ return 'Falha ao salvar configurações.';
2118
+ }
1212
2119
  getVariantIcon(key) {
1213
2120
  const safe = this.safeName(`dialog.variants.${key}.icon`);
1214
2121
  return this.currentValues[safe];
@@ -1308,32 +2215,240 @@ class GlobalConfigEditorComponent {
1308
2215
  return true;
1309
2216
  }
1310
2217
  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: `
2218
+ 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: "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
2219
  <mat-accordion multi>
1313
- <mat-expansion-panel [expanded]="true">
2220
+ <mat-expansion-panel>
1314
2221
  <mat-expansion-panel-header>
1315
- <mat-panel-title>CRUD</mat-panel-title>
2222
+ <mat-panel-title>
2223
+ <mat-icon class="panel-icon">construction</mat-icon>
2224
+ CRUD
2225
+ </mat-panel-title>
1316
2226
  <mat-panel-description>Políticas globais de abertura, back e header</mat-panel-description>
1317
2227
  </mat-expansion-panel-header>
1318
2228
  <ng-template #hostCrud></ng-template>
1319
2229
  </mat-expansion-panel>
1320
2230
  <mat-expansion-panel>
1321
2231
  <mat-expansion-panel-header>
1322
- <mat-panel-title>Dynamic Fields</mat-panel-title>
2232
+ <mat-panel-title>
2233
+ <mat-icon class="panel-icon">dynamic_form</mat-icon>
2234
+ Dynamic Fields
2235
+ </mat-panel-title>
1323
2236
  <mat-panel-description>Async Select, cascata e paginação</mat-panel-description>
1324
2237
  </mat-expansion-panel-header>
1325
2238
  <ng-template #hostFields></ng-template>
1326
2239
  </mat-expansion-panel>
1327
2240
  <mat-expansion-panel>
1328
2241
  <mat-expansion-panel-header>
1329
- <mat-panel-title>Tabela</mat-panel-title>
2242
+ <mat-panel-title>
2243
+ <mat-icon class="panel-icon">cached</mat-icon>
2244
+ Cache & Persistência
2245
+ </mat-panel-title>
2246
+ <mat-panel-description>Estratégia de cache de schema (local vs server)</mat-panel-description>
2247
+ </mat-expansion-panel-header>
2248
+ <ng-template #hostCache></ng-template>
2249
+ </mat-expansion-panel>
2250
+ <mat-expansion-panel>
2251
+ <mat-expansion-panel-header>
2252
+ <mat-panel-title>
2253
+ <mat-icon class="panel-icon">psychology</mat-icon>
2254
+ Inteligência Artificial
2255
+ </mat-panel-title>
2256
+ <mat-panel-description>Integração com LLM</mat-panel-description>
2257
+ </mat-expansion-panel-header>
2258
+
2259
+ <div class="ai-config-container">
2260
+ <div class="ai-config-source">
2261
+ <div class="ai-config-source__meta">
2262
+ <mat-icon>settings_suggest</mat-icon>
2263
+ <span>{{ configSourceLabel }}</span>
2264
+ </div>
2265
+ <button
2266
+ mat-stroked-button
2267
+ type="button"
2268
+ class="ai-action-btn ai-action-btn--clear"
2269
+ *ngIf="hasStoredGlobalConfig"
2270
+ [attr.aria-busy]="isClearingGlobalConfig ? 'true' : null"
2271
+ [disabled]="isClearingGlobalConfig"
2272
+ (click)="clearStoredConfig()"
2273
+ matTooltip="Apaga o config salvo e volta aos defaults do servidor"
2274
+ >
2275
+ <ng-container *ngIf="isClearingGlobalConfig; else clearContent">
2276
+ <mat-spinner diameter="16" class="btn-spinner"></mat-spinner>
2277
+ <span class="ai-action-label">Limpando...</span>
2278
+ </ng-container>
2279
+ <ng-template #clearContent>
2280
+ <mat-icon>delete_sweep</mat-icon>
2281
+ <span class="ai-action-label">Limpar config salvo</span>
2282
+ </ng-template>
2283
+ </button>
2284
+ </div>
2285
+ <!-- Group 1: Credentials -->
2286
+ <div class="ai-group">
2287
+ <div class="ai-group-header">
2288
+ <div class="ai-group-title">
2289
+ <mat-icon>vpn_key</mat-icon> Credenciais
2290
+ </div>
2291
+ <button mat-stroked-button
2292
+ type="button"
2293
+ class="ai-action-btn"
2294
+ [class.is-success]="aiTestResult?.success"
2295
+ [attr.aria-busy]="isTestingAi ? 'true' : null"
2296
+ (click)="testAiConnection()"
2297
+ [disabled]="isTestingAi || !hasApiKey"
2298
+ matTooltip="Testar conexão com a chave informada">
2299
+ <ng-container *ngIf="isTestingAi; else btnContent">
2300
+ <mat-spinner diameter="16" class="btn-spinner"></mat-spinner>
2301
+ <span class="ai-action-label">Conectando...</span>
2302
+ </ng-container>
2303
+ <ng-template #btnContent>
2304
+ <mat-icon>{{ aiTestResult?.success ? 'check' : 'bolt' }}</mat-icon>
2305
+ <span class="ai-action-label">
2306
+ {{ aiTestResult?.success ? 'Conectado' : 'Testar conexão' }}
2307
+ </span>
2308
+ </ng-template>
2309
+ </button>
2310
+ </div>
2311
+ <div class="ai-provider-summary" *ngIf="selectedProvider">
2312
+ <span class="ai-provider-icon" aria-hidden="true">
2313
+ <ng-container [ngSwitch]="selectedProvider.iconKey">
2314
+ <svg *ngSwitchCase="'gemini'" viewBox="0 0 24 24" class="provider-svg" fill="none" stroke="currentColor" stroke-width="1.6">
2315
+ <path d="M12 3l2.6 5.4L20 11l-5.4 2.6L12 19l-2.6-5.4L4 11l5.4-2.6L12 3z" />
2316
+ </svg>
2317
+ <svg *ngSwitchCase="'openai'" viewBox="0 0 24 24" class="provider-svg" fill="none" stroke="currentColor" stroke-width="1.6">
2318
+ <polygon points="12,2 20,7 20,17 12,22 4,17 4,7" />
2319
+ </svg>
2320
+ <svg *ngSwitchCase="'xai'" viewBox="0 0 24 24" class="provider-svg" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round">
2321
+ <line x1="5" y1="5" x2="19" y2="19" />
2322
+ <line x1="19" y1="5" x2="5" y2="19" />
2323
+ </svg>
2324
+ <svg *ngSwitchCase="'mock'" viewBox="0 0 24 24" class="provider-svg" fill="none" stroke="currentColor" stroke-width="1.6">
2325
+ <rect x="4.5" y="4.5" width="15" height="15" rx="2" ry="2" stroke-dasharray="2 2" />
2326
+ </svg>
2327
+ <svg *ngSwitchDefault viewBox="0 0 24 24" class="provider-svg" fill="none" stroke="currentColor" stroke-width="1.6">
2328
+ <circle cx="12" cy="12" r="7.5" />
2329
+ </svg>
2330
+ </ng-container>
2331
+ </span>
2332
+ <div class="ai-provider-meta">
2333
+ <div class="ai-provider-name">{{ selectedProvider.label }}</div>
2334
+ <div class="ai-provider-desc">{{ selectedProvider.description }}</div>
2335
+ </div>
2336
+ <div
2337
+ class="ai-provider-key"
2338
+ *ngIf="selectedProvider?.requiresApiKey !== false"
2339
+ [class.is-present]="hasCurrentApiKey"
2340
+ [class.is-saved]="!hasCurrentApiKey && (apiKeyLast4 || hasStoredApiKey)"
2341
+ [class.is-missing]="!hasCurrentApiKey && !apiKeyLast4 && !hasStoredApiKey"
2342
+ >
2343
+ <mat-icon>vpn_key</mat-icon>
2344
+ <span>{{ apiKeyStatusLabel }}</span>
2345
+ </div>
2346
+ <div class="ai-provider-key is-unlocked" *ngIf="selectedProvider?.requiresApiKey === false">
2347
+ <mat-icon>check_circle</mat-icon>
2348
+ <span>Sem chave necessária</span>
2349
+ </div>
2350
+ </div>
2351
+ <div class="ai-credentials-row">
2352
+ <div class="ai-form-inline">
2353
+ <ng-template #hostAiCredentials></ng-template>
2354
+ </div>
2355
+ </div>
2356
+
2357
+ <!-- Feedback / Error Message -->
2358
+ <div class="ai-feedback" *ngIf="aiTestResult && !aiTestResult.success">
2359
+ <mat-icon color="warn">error</mat-icon>
2360
+ <span class="error-text">{{ aiTestResult.message }}</span>
2361
+ </div>
2362
+ </div>
2363
+
2364
+ <!-- Group 2: Model & Behavior (Only visible if Key is present) -->
2365
+ <div class="ai-group" [class.disabled-group]="!hasApiKey">
2366
+ <div class="ai-group-header">
2367
+ <div class="ai-group-title">
2368
+ <mat-icon>smart_toy</mat-icon> Modelo & Comportamento
2369
+ </div>
2370
+ <div class="ai-header-actions">
2371
+ <span class="ai-subtext" *ngIf="hasApiKey">Escolha o modelo após validar a chave.</span>
2372
+ <mat-chip-option *ngIf="!hasApiKey" disabled>Requer chave API validada</mat-chip-option>
2373
+ <button mat-icon-button (click)="refreshModels(true)" [disabled]="isTestingAi || !hasApiKey" matTooltip="Atualizar lista de modelos">
2374
+ <mat-icon [class.spin]="isRefetchingModels">sync</mat-icon>
2375
+ </button>
2376
+ </div>
2377
+ </div>
2378
+
2379
+ <div class="ai-model-content" *ngIf="hasApiKey">
2380
+ <div class="ai-model-controls">
2381
+ <ng-template #hostAiModel></ng-template>
2382
+ </div>
2383
+
2384
+ <!-- Model Details (Placeholder for future metadata) -->
2385
+ <div class="ai-model-details" *ngIf="selectedModelDetails">
2386
+ <mat-icon inline>info</mat-icon> {{ selectedModelDetails }}
2387
+ </div>
2388
+ </div>
2389
+
2390
+ <div class="ai-placeholder" *ngIf="!hasApiKey">
2391
+ <mat-icon>lock</mat-icon>
2392
+ <span>Configure e teste sua chave de API para desbloquear a seleção de modelos.</span>
2393
+ </div>
2394
+ </div>
2395
+
2396
+ <!-- Group 3: Embeddings (RAG) -->
2397
+ <div class="ai-group">
2398
+ <div class="ai-group-header">
2399
+ <div class="ai-group-title">
2400
+ <mat-icon>scatter_plot</mat-icon> Embeddings (RAG)
2401
+ </div>
2402
+ <div class="ai-header-actions">
2403
+ <button
2404
+ mat-stroked-button
2405
+ type="button"
2406
+ class="ai-action-btn"
2407
+ (click)="useLlmForEmbeddings()"
2408
+ [disabled]="!canUseLlmForEmbeddings"
2409
+ matTooltip="Aplicar provedor e chave do LLM aos embeddings"
2410
+ >
2411
+ <mat-icon>merge_type</mat-icon>
2412
+ <span class="ai-action-label">Usar mesmo LLM</span>
2413
+ </button>
2414
+ <mat-chip-option *ngIf="!canUseLlmForEmbeddings" disabled>Provedor sem embeddings</mat-chip-option>
2415
+ </div>
2416
+ </div>
2417
+ <div class="ai-subtext">
2418
+ Configure o provedor de embeddings para buscas vetoriais (templates e schemas).
2419
+ </div>
2420
+ <div class="ai-subtext" *ngIf="embeddingUseSameAsLlm">
2421
+ Sincronizado com o LLM. Os campos abaixo acompanham a credencial principal.
2422
+ </div>
2423
+ <div class="ai-embedding-row">
2424
+ <ng-template #hostAiEmbedding></ng-template>
2425
+ </div>
2426
+ <div class="ai-feedback ai-feedback--warn" *ngIf="embeddingDimensionMismatch">
2427
+ <mat-icon>warning</mat-icon>
2428
+ <span class="error-text">
2429
+ Dimensão de embeddings diferente do banco (768). Ajuste para 768 ou refaça a migração.
2430
+ </span>
2431
+ </div>
2432
+ </div>
2433
+ </div>
2434
+
2435
+ </mat-expansion-panel>
2436
+ <mat-expansion-panel>
2437
+ <mat-expansion-panel-header>
2438
+ <mat-panel-title>
2439
+ <mat-icon class="panel-icon">table_chart</mat-icon>
2440
+ Tabela
2441
+ </mat-panel-title>
1330
2442
  <mat-panel-description>Toolbar, aparência e filtro avançado</mat-panel-description>
1331
2443
  </mat-expansion-panel-header>
1332
2444
  <ng-template #hostTable></ng-template>
1333
2445
  </mat-expansion-panel>
1334
2446
  <mat-expansion-panel>
1335
2447
  <mat-expansion-panel-header>
1336
- <mat-panel-title>Dialog</mat-panel-title>
2448
+ <mat-panel-title>
2449
+ <mat-icon class="panel-icon">forum</mat-icon>
2450
+ Dialog
2451
+ </mat-panel-title>
1337
2452
  <mat-panel-description>Defaults e variants (danger, info, success, question, error)</mat-panel-description>
1338
2453
  </mat-expansion-panel-header>
1339
2454
  <ng-template #hostDialog></ng-template>
@@ -1356,36 +2471,244 @@ class GlobalConfigEditorComponent {
1356
2471
  </div>
1357
2472
  </mat-expansion-panel>
1358
2473
  </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"] }] });
2474
+ `, 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
2475
  }
1361
2476
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.1.4", ngImport: i0, type: GlobalConfigEditorComponent, decorators: [{
1362
2477
  type: Component,
1363
- args: [{ selector: 'praxis-global-config-editor', standalone: true, imports: [CommonModule, MatSnackBarModule, MatExpansionModule, MatIconModule, MatButtonModule, MatTooltipModule, PraxisIconDirective], template: `
2478
+ args: [{ selector: 'praxis-global-config-editor', standalone: true, imports: [CommonModule, MatSnackBarModule, MatExpansionModule, MatIconModule, MatButtonModule, MatTooltipModule, MatProgressSpinnerModule, MatChipsModule, PraxisIconDirective], template: `
1364
2479
  <mat-accordion multi>
1365
- <mat-expansion-panel [expanded]="true">
2480
+ <mat-expansion-panel>
1366
2481
  <mat-expansion-panel-header>
1367
- <mat-panel-title>CRUD</mat-panel-title>
2482
+ <mat-panel-title>
2483
+ <mat-icon class="panel-icon">construction</mat-icon>
2484
+ CRUD
2485
+ </mat-panel-title>
1368
2486
  <mat-panel-description>Políticas globais de abertura, back e header</mat-panel-description>
1369
2487
  </mat-expansion-panel-header>
1370
2488
  <ng-template #hostCrud></ng-template>
1371
2489
  </mat-expansion-panel>
1372
2490
  <mat-expansion-panel>
1373
2491
  <mat-expansion-panel-header>
1374
- <mat-panel-title>Dynamic Fields</mat-panel-title>
2492
+ <mat-panel-title>
2493
+ <mat-icon class="panel-icon">dynamic_form</mat-icon>
2494
+ Dynamic Fields
2495
+ </mat-panel-title>
1375
2496
  <mat-panel-description>Async Select, cascata e paginação</mat-panel-description>
1376
2497
  </mat-expansion-panel-header>
1377
2498
  <ng-template #hostFields></ng-template>
1378
2499
  </mat-expansion-panel>
1379
2500
  <mat-expansion-panel>
1380
2501
  <mat-expansion-panel-header>
1381
- <mat-panel-title>Tabela</mat-panel-title>
2502
+ <mat-panel-title>
2503
+ <mat-icon class="panel-icon">cached</mat-icon>
2504
+ Cache & Persistência
2505
+ </mat-panel-title>
2506
+ <mat-panel-description>Estratégia de cache de schema (local vs server)</mat-panel-description>
2507
+ </mat-expansion-panel-header>
2508
+ <ng-template #hostCache></ng-template>
2509
+ </mat-expansion-panel>
2510
+ <mat-expansion-panel>
2511
+ <mat-expansion-panel-header>
2512
+ <mat-panel-title>
2513
+ <mat-icon class="panel-icon">psychology</mat-icon>
2514
+ Inteligência Artificial
2515
+ </mat-panel-title>
2516
+ <mat-panel-description>Integração com LLM</mat-panel-description>
2517
+ </mat-expansion-panel-header>
2518
+
2519
+ <div class="ai-config-container">
2520
+ <div class="ai-config-source">
2521
+ <div class="ai-config-source__meta">
2522
+ <mat-icon>settings_suggest</mat-icon>
2523
+ <span>{{ configSourceLabel }}</span>
2524
+ </div>
2525
+ <button
2526
+ mat-stroked-button
2527
+ type="button"
2528
+ class="ai-action-btn ai-action-btn--clear"
2529
+ *ngIf="hasStoredGlobalConfig"
2530
+ [attr.aria-busy]="isClearingGlobalConfig ? 'true' : null"
2531
+ [disabled]="isClearingGlobalConfig"
2532
+ (click)="clearStoredConfig()"
2533
+ matTooltip="Apaga o config salvo e volta aos defaults do servidor"
2534
+ >
2535
+ <ng-container *ngIf="isClearingGlobalConfig; else clearContent">
2536
+ <mat-spinner diameter="16" class="btn-spinner"></mat-spinner>
2537
+ <span class="ai-action-label">Limpando...</span>
2538
+ </ng-container>
2539
+ <ng-template #clearContent>
2540
+ <mat-icon>delete_sweep</mat-icon>
2541
+ <span class="ai-action-label">Limpar config salvo</span>
2542
+ </ng-template>
2543
+ </button>
2544
+ </div>
2545
+ <!-- Group 1: Credentials -->
2546
+ <div class="ai-group">
2547
+ <div class="ai-group-header">
2548
+ <div class="ai-group-title">
2549
+ <mat-icon>vpn_key</mat-icon> Credenciais
2550
+ </div>
2551
+ <button mat-stroked-button
2552
+ type="button"
2553
+ class="ai-action-btn"
2554
+ [class.is-success]="aiTestResult?.success"
2555
+ [attr.aria-busy]="isTestingAi ? 'true' : null"
2556
+ (click)="testAiConnection()"
2557
+ [disabled]="isTestingAi || !hasApiKey"
2558
+ matTooltip="Testar conexão com a chave informada">
2559
+ <ng-container *ngIf="isTestingAi; else btnContent">
2560
+ <mat-spinner diameter="16" class="btn-spinner"></mat-spinner>
2561
+ <span class="ai-action-label">Conectando...</span>
2562
+ </ng-container>
2563
+ <ng-template #btnContent>
2564
+ <mat-icon>{{ aiTestResult?.success ? 'check' : 'bolt' }}</mat-icon>
2565
+ <span class="ai-action-label">
2566
+ {{ aiTestResult?.success ? 'Conectado' : 'Testar conexão' }}
2567
+ </span>
2568
+ </ng-template>
2569
+ </button>
2570
+ </div>
2571
+ <div class="ai-provider-summary" *ngIf="selectedProvider">
2572
+ <span class="ai-provider-icon" aria-hidden="true">
2573
+ <ng-container [ngSwitch]="selectedProvider.iconKey">
2574
+ <svg *ngSwitchCase="'gemini'" viewBox="0 0 24 24" class="provider-svg" fill="none" stroke="currentColor" stroke-width="1.6">
2575
+ <path d="M12 3l2.6 5.4L20 11l-5.4 2.6L12 19l-2.6-5.4L4 11l5.4-2.6L12 3z" />
2576
+ </svg>
2577
+ <svg *ngSwitchCase="'openai'" viewBox="0 0 24 24" class="provider-svg" fill="none" stroke="currentColor" stroke-width="1.6">
2578
+ <polygon points="12,2 20,7 20,17 12,22 4,17 4,7" />
2579
+ </svg>
2580
+ <svg *ngSwitchCase="'xai'" viewBox="0 0 24 24" class="provider-svg" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round">
2581
+ <line x1="5" y1="5" x2="19" y2="19" />
2582
+ <line x1="19" y1="5" x2="5" y2="19" />
2583
+ </svg>
2584
+ <svg *ngSwitchCase="'mock'" viewBox="0 0 24 24" class="provider-svg" fill="none" stroke="currentColor" stroke-width="1.6">
2585
+ <rect x="4.5" y="4.5" width="15" height="15" rx="2" ry="2" stroke-dasharray="2 2" />
2586
+ </svg>
2587
+ <svg *ngSwitchDefault viewBox="0 0 24 24" class="provider-svg" fill="none" stroke="currentColor" stroke-width="1.6">
2588
+ <circle cx="12" cy="12" r="7.5" />
2589
+ </svg>
2590
+ </ng-container>
2591
+ </span>
2592
+ <div class="ai-provider-meta">
2593
+ <div class="ai-provider-name">{{ selectedProvider.label }}</div>
2594
+ <div class="ai-provider-desc">{{ selectedProvider.description }}</div>
2595
+ </div>
2596
+ <div
2597
+ class="ai-provider-key"
2598
+ *ngIf="selectedProvider?.requiresApiKey !== false"
2599
+ [class.is-present]="hasCurrentApiKey"
2600
+ [class.is-saved]="!hasCurrentApiKey && (apiKeyLast4 || hasStoredApiKey)"
2601
+ [class.is-missing]="!hasCurrentApiKey && !apiKeyLast4 && !hasStoredApiKey"
2602
+ >
2603
+ <mat-icon>vpn_key</mat-icon>
2604
+ <span>{{ apiKeyStatusLabel }}</span>
2605
+ </div>
2606
+ <div class="ai-provider-key is-unlocked" *ngIf="selectedProvider?.requiresApiKey === false">
2607
+ <mat-icon>check_circle</mat-icon>
2608
+ <span>Sem chave necessária</span>
2609
+ </div>
2610
+ </div>
2611
+ <div class="ai-credentials-row">
2612
+ <div class="ai-form-inline">
2613
+ <ng-template #hostAiCredentials></ng-template>
2614
+ </div>
2615
+ </div>
2616
+
2617
+ <!-- Feedback / Error Message -->
2618
+ <div class="ai-feedback" *ngIf="aiTestResult && !aiTestResult.success">
2619
+ <mat-icon color="warn">error</mat-icon>
2620
+ <span class="error-text">{{ aiTestResult.message }}</span>
2621
+ </div>
2622
+ </div>
2623
+
2624
+ <!-- Group 2: Model & Behavior (Only visible if Key is present) -->
2625
+ <div class="ai-group" [class.disabled-group]="!hasApiKey">
2626
+ <div class="ai-group-header">
2627
+ <div class="ai-group-title">
2628
+ <mat-icon>smart_toy</mat-icon> Modelo & Comportamento
2629
+ </div>
2630
+ <div class="ai-header-actions">
2631
+ <span class="ai-subtext" *ngIf="hasApiKey">Escolha o modelo após validar a chave.</span>
2632
+ <mat-chip-option *ngIf="!hasApiKey" disabled>Requer chave API validada</mat-chip-option>
2633
+ <button mat-icon-button (click)="refreshModels(true)" [disabled]="isTestingAi || !hasApiKey" matTooltip="Atualizar lista de modelos">
2634
+ <mat-icon [class.spin]="isRefetchingModels">sync</mat-icon>
2635
+ </button>
2636
+ </div>
2637
+ </div>
2638
+
2639
+ <div class="ai-model-content" *ngIf="hasApiKey">
2640
+ <div class="ai-model-controls">
2641
+ <ng-template #hostAiModel></ng-template>
2642
+ </div>
2643
+
2644
+ <!-- Model Details (Placeholder for future metadata) -->
2645
+ <div class="ai-model-details" *ngIf="selectedModelDetails">
2646
+ <mat-icon inline>info</mat-icon> {{ selectedModelDetails }}
2647
+ </div>
2648
+ </div>
2649
+
2650
+ <div class="ai-placeholder" *ngIf="!hasApiKey">
2651
+ <mat-icon>lock</mat-icon>
2652
+ <span>Configure e teste sua chave de API para desbloquear a seleção de modelos.</span>
2653
+ </div>
2654
+ </div>
2655
+
2656
+ <!-- Group 3: Embeddings (RAG) -->
2657
+ <div class="ai-group">
2658
+ <div class="ai-group-header">
2659
+ <div class="ai-group-title">
2660
+ <mat-icon>scatter_plot</mat-icon> Embeddings (RAG)
2661
+ </div>
2662
+ <div class="ai-header-actions">
2663
+ <button
2664
+ mat-stroked-button
2665
+ type="button"
2666
+ class="ai-action-btn"
2667
+ (click)="useLlmForEmbeddings()"
2668
+ [disabled]="!canUseLlmForEmbeddings"
2669
+ matTooltip="Aplicar provedor e chave do LLM aos embeddings"
2670
+ >
2671
+ <mat-icon>merge_type</mat-icon>
2672
+ <span class="ai-action-label">Usar mesmo LLM</span>
2673
+ </button>
2674
+ <mat-chip-option *ngIf="!canUseLlmForEmbeddings" disabled>Provedor sem embeddings</mat-chip-option>
2675
+ </div>
2676
+ </div>
2677
+ <div class="ai-subtext">
2678
+ Configure o provedor de embeddings para buscas vetoriais (templates e schemas).
2679
+ </div>
2680
+ <div class="ai-subtext" *ngIf="embeddingUseSameAsLlm">
2681
+ Sincronizado com o LLM. Os campos abaixo acompanham a credencial principal.
2682
+ </div>
2683
+ <div class="ai-embedding-row">
2684
+ <ng-template #hostAiEmbedding></ng-template>
2685
+ </div>
2686
+ <div class="ai-feedback ai-feedback--warn" *ngIf="embeddingDimensionMismatch">
2687
+ <mat-icon>warning</mat-icon>
2688
+ <span class="error-text">
2689
+ Dimensão de embeddings diferente do banco (768). Ajuste para 768 ou refaça a migração.
2690
+ </span>
2691
+ </div>
2692
+ </div>
2693
+ </div>
2694
+
2695
+ </mat-expansion-panel>
2696
+ <mat-expansion-panel>
2697
+ <mat-expansion-panel-header>
2698
+ <mat-panel-title>
2699
+ <mat-icon class="panel-icon">table_chart</mat-icon>
2700
+ Tabela
2701
+ </mat-panel-title>
1382
2702
  <mat-panel-description>Toolbar, aparência e filtro avançado</mat-panel-description>
1383
2703
  </mat-expansion-panel-header>
1384
2704
  <ng-template #hostTable></ng-template>
1385
2705
  </mat-expansion-panel>
1386
2706
  <mat-expansion-panel>
1387
2707
  <mat-expansion-panel-header>
1388
- <mat-panel-title>Dialog</mat-panel-title>
2708
+ <mat-panel-title>
2709
+ <mat-icon class="panel-icon">forum</mat-icon>
2710
+ Dialog
2711
+ </mat-panel-title>
1389
2712
  <mat-panel-description>Defaults e variants (danger, info, success, question, error)</mat-panel-description>
1390
2713
  </mat-expansion-panel-header>
1391
2714
  <ng-template #hostDialog></ng-template>
@@ -1408,19 +2731,31 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.1.4", ngImpor
1408
2731
  </div>
1409
2732
  </mat-expansion-panel>
1410
2733
  </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"] }]
2734
+ `, 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
2735
  }], ctorParameters: () => [{ type: GlobalConfigAdminService }, { type: i2.MatSnackBar }], propDecorators: { hostCrud: [{
1413
2736
  type: ViewChild,
1414
2737
  args: ['hostCrud', { read: ViewContainerRef, static: true }]
1415
2738
  }], hostFields: [{
1416
2739
  type: ViewChild,
1417
2740
  args: ['hostFields', { read: ViewContainerRef, static: true }]
2741
+ }], hostCache: [{
2742
+ type: ViewChild,
2743
+ args: ['hostCache', { read: ViewContainerRef, static: true }]
1418
2744
  }], hostTable: [{
1419
2745
  type: ViewChild,
1420
2746
  args: ['hostTable', { read: ViewContainerRef, static: true }]
1421
2747
  }], hostDialog: [{
1422
2748
  type: ViewChild,
1423
2749
  args: ['hostDialog', { read: ViewContainerRef, static: true }]
2750
+ }], hostAiCredentials: [{
2751
+ type: ViewChild,
2752
+ args: ['hostAiCredentials', { read: ViewContainerRef, static: true }]
2753
+ }], hostAiModel: [{
2754
+ type: ViewChild,
2755
+ args: ['hostAiModel', { read: ViewContainerRef, static: false }]
2756
+ }], hostAiEmbedding: [{
2757
+ type: ViewChild,
2758
+ args: ['hostAiEmbedding', { read: ViewContainerRef, static: true }]
1424
2759
  }] } });
1425
2760
 
1426
2761
  /**
@@ -1437,9 +2772,43 @@ function openGlobalConfigEditor(settings, opts) {
1437
2772
  });
1438
2773
  }
1439
2774
 
2775
+ /**
2776
+ * Capabilities catalog for SettingsPanelConfig.
2777
+ */
2778
+ const SETTINGS_PANEL_AI_CAPABILITIES = {
2779
+ version: 'v1.0',
2780
+ enums: {},
2781
+ targets: ['praxis-global-config-editor', 'praxis-settings-panel'],
2782
+ notes: [
2783
+ 'SettingsPanelConfig descreve o painel e o componente embutido (content.component).',
2784
+ 'content.component deve ser uma referencia de classe Angular fornecida pelo host.',
2785
+ ],
2786
+ capabilities: [
2787
+ { path: 'id', category: 'identity', valueKind: 'string', description: 'ID do painel.' },
2788
+ { path: 'title', category: 'identity', valueKind: 'string', description: 'Titulo do painel.' },
2789
+ { path: 'titleIcon', category: 'identity', valueKind: 'string', description: 'Icone do titulo (Material icon name).' },
2790
+ { path: 'expanded', category: 'behavior', valueKind: 'boolean', description: 'Estado inicial expandido.' },
2791
+ { path: 'content', category: 'content', valueKind: 'object', description: 'Conteudo do painel.' },
2792
+ {
2793
+ path: 'content.component',
2794
+ category: 'content',
2795
+ valueKind: 'object',
2796
+ description: 'Referencia do componente Angular a ser renderizado.',
2797
+ critical: true,
2798
+ safetyNotes: 'Nao gerar dinamicamente; o host deve fornecer a referencia.',
2799
+ },
2800
+ {
2801
+ path: 'content.inputs',
2802
+ category: 'content',
2803
+ valueKind: 'object',
2804
+ description: 'Inputs passados para o componente de conteudo.',
2805
+ },
2806
+ ],
2807
+ };
2808
+
1440
2809
  /**
1441
2810
  * Generated bundle index. Do not edit.
1442
2811
  */
1443
2812
 
1444
- export { GlobalConfigAdminService, GlobalConfigEditorComponent, SETTINGS_PANEL_DATA, SETTINGS_PANEL_REF, SettingsPanelComponent, SettingsPanelRef, SettingsPanelService, buildGlobalConfigFormConfig, openGlobalConfigEditor };
2813
+ export { GLOBAL_CONFIG_DYNAMIC_FORM_COMPONENT, GlobalConfigAdminService, GlobalConfigEditorComponent, SETTINGS_PANEL_AI_CAPABILITIES, SETTINGS_PANEL_DATA, SETTINGS_PANEL_REF, SettingsPanelComponent, SettingsPanelRef, SettingsPanelService, buildGlobalConfigFormConfig, openGlobalConfigEditor };
1445
2814
  //# sourceMappingURL=praxisui-settings-panel.mjs.map