@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.
- package/README.md +31 -0
- package/fesm2022/praxisui-settings-panel.mjs +1552 -183
- package/fesm2022/praxisui-settings-panel.mjs.map +1 -1
- package/index.d.ts +124 -9
- package/package.json +5 -5
|
@@ -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)
|
|
162
|
-
|
|
163
|
-
|
|
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
|
|
175
|
-
|
|
176
|
-
|
|
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
|
-
|
|
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
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
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 & 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 & 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 & 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,
|
|
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 & 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
|
-
|
|
1081
|
-
this.
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
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
|
-
|
|
1091
|
-
this.
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
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.
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
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
|
-
|
|
1115
|
-
|
|
1116
|
-
}
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
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
|
-
|
|
1157
|
-
this.
|
|
1158
|
-
|
|
1159
|
-
this.
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
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.
|
|
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
|
-
|
|
1173
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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
|
|
2220
|
+
<mat-expansion-panel>
|
|
1314
2221
|
<mat-expansion-panel-header>
|
|
1315
|
-
<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>
|
|
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>
|
|
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>
|
|
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
|
|
2480
|
+
<mat-expansion-panel>
|
|
1366
2481
|
<mat-expansion-panel-header>
|
|
1367
|
-
<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>
|
|
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>
|
|
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>
|
|
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
|
|
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
|