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