@firestitch/report 18.0.10 → 18.0.12

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.
Files changed (23) hide show
  1. package/app/reports/data/report.data.d.ts +4 -0
  2. package/app/reports/dialogs/entity-functions/entity-functions.component.d.ts +22 -0
  3. package/app/reports/dialogs/global-settings/components/general/general.component.d.ts +8 -0
  4. package/app/reports/dialogs/global-settings/components/semantic-database/semantic-database.component.d.ts +21 -21
  5. package/app/reports/dialogs/global-settings/global-settings.component.d.ts +8 -4
  6. package/app/reports/dialogs/instruction-history/instruction-history.component.d.ts +16 -0
  7. package/app/reports/interfaces/report.interface.d.ts +11 -1
  8. package/esm2022/app/reports/data/report.data.mjs +15 -1
  9. package/esm2022/app/reports/dialogs/entity-functions/entity-functions.component.mjs +81 -0
  10. package/esm2022/app/reports/dialogs/global-settings/components/general/general.component.mjs +34 -0
  11. package/esm2022/app/reports/dialogs/global-settings/components/semantic-database/semantic-database.component.mjs +70 -75
  12. package/esm2022/app/reports/dialogs/global-settings/global-settings.component.mjs +58 -24
  13. package/esm2022/app/reports/dialogs/instruction-history/instruction-history.component.mjs +60 -0
  14. package/esm2022/app/reports/interfaces/report.interface.mjs +1 -1
  15. package/fesm2022/{firestitch-report-echarts-Cel1iHUg.mjs → firestitch-report-echarts-1VNsyco5.mjs} +2 -2
  16. package/fesm2022/{firestitch-report-echarts-Cel1iHUg.mjs.map → firestitch-report-echarts-1VNsyco5.mjs.map} +1 -1
  17. package/fesm2022/{firestitch-report-firestitch-report-Y1XG_Gms.mjs → firestitch-report-firestitch-report-BNHGwPj2.mjs} +274 -119
  18. package/fesm2022/firestitch-report-firestitch-report-BNHGwPj2.mjs.map +1 -0
  19. package/fesm2022/firestitch-report.mjs +1 -1
  20. package/package.json +1 -1
  21. package/app/reports/dialogs/global-settings/components/history/history.component.d.ts +0 -8
  22. package/esm2022/app/reports/dialogs/global-settings/components/history/history.component.mjs +0 -32
  23. package/fesm2022/firestitch-report-firestitch-report-Y1XG_Gms.mjs.map +0 -1
@@ -1,9 +1,9 @@
1
1
  import * as i0 from '@angular/core';
2
- import { inject, Injectable, Component, ChangeDetectionStrategy, Input, DestroyRef, ViewChild, EventEmitter, ChangeDetectorRef, NgZone, ElementRef, Output, HostBinding, signal, CUSTOM_ELEMENTS_SCHEMA, model, input, output, computed, viewChild, effect } from '@angular/core';
2
+ import { inject, Injectable, Component, ChangeDetectionStrategy, Input, DestroyRef, ViewChild, EventEmitter, ChangeDetectorRef, NgZone, ElementRef, Output, HostBinding, signal, CUSTOM_ELEMENTS_SCHEMA, model, input, computed, viewChild, effect, output } from '@angular/core';
3
3
  import * as i1$3 from '@angular/forms';
4
4
  import { FormsModule, ControlContainer, NgForm } from '@angular/forms';
5
5
  import { MAT_DIALOG_DATA, MatDialogRef, MatDialogTitle, MatDialogContent, MatDialogActions, MatDialog, MatDialogClose } from '@angular/material/dialog';
6
- import * as i4$1 from '@angular/material/icon';
6
+ import * as i4$2 from '@angular/material/icon';
7
7
  import { MatIcon, MatIconModule } from '@angular/material/icon';
8
8
  import * as i2$2 from '@firestitch/autocomplete-chips';
9
9
  import { FsAutocompleteChipsModule } from '@firestitch/autocomplete-chips';
@@ -53,11 +53,11 @@ import { guid } from '@firestitch/common';
53
53
  import { Router, ActivatedRoute, RouterLink } from '@angular/router';
54
54
  import * as i2$4 from '@firestitch/date';
55
55
  import { FsDateModule } from '@firestitch/date';
56
- import * as i6$1 from '@firestitch/skeleton';
56
+ import * as i4$1 from '@firestitch/skeleton';
57
57
  import { FsSkeletonModule } from '@firestitch/skeleton';
58
- import { FsMarkdownEditorComponent, FsMarkdownRendererComponent } from '@firestitch/markdown-editor';
59
58
  import * as i1$4 from '@firestitch/chip';
60
59
  import { FsChipModule } from '@firestitch/chip';
60
+ import { FsMarkdownRendererComponent, FsMarkdownEditorComponent } from '@firestitch/markdown-editor';
61
61
  import * as i1$5 from '@angular/material/expansion';
62
62
  import { MatExpansionModule } from '@angular/material/expansion';
63
63
 
@@ -245,6 +245,20 @@ class ReportData {
245
245
  ...config,
246
246
  });
247
247
  }
248
+ // The report module's General settings (semantic-build concurrency + its bounds).
249
+ settings(config = {}) {
250
+ return this._api.get(this._path('settings'), {}, {
251
+ key: 'settings',
252
+ ...config,
253
+ });
254
+ }
255
+ // Save the General settings; resolves to the same { settings } shape.
256
+ saveSettings(settings, config = {}) {
257
+ return this._api.put(this._path('settings'), settings, {
258
+ key: 'settings',
259
+ ...config,
260
+ });
261
+ }
248
262
  // Update a filter group — its exposure level ('component' | 'report' | 'both')
249
263
  // and/or its default value (the value seeding the report bar on open). Send
250
264
  // `default: null` (or an empty default) to clear it. Only the keys provided
@@ -2527,7 +2541,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
2527
2541
  // complete synchronously after setOption.
2528
2542
  async function renderChartImage(component, data, width, height, pixelRatio = 3) {
2529
2543
  // The same tree-shaken echarts build + 'report' theme the live canvas uses.
2530
- const echarts = (await import('./firestitch-report-echarts-Cel1iHUg.mjs')).default;
2544
+ const echarts = (await import('./firestitch-report-echarts-1VNsyco5.mjs')).default;
2531
2545
  const host = document.createElement('div');
2532
2546
  host.style.width = `${width}px`;
2533
2547
  host.style.height = `${height}px`;
@@ -3271,7 +3285,7 @@ class ReportComponent {
3271
3285
  ReportPdfService,
3272
3286
  // Tree-shaken ECharts core + the 'report' house theme, loaded lazily with
3273
3287
  // this route's chunk.
3274
- provideEchartsCore({ echarts: () => import('./firestitch-report-echarts-Cel1iHUg.mjs').then((module) => module.default) }),
3288
+ provideEchartsCore({ echarts: () => import('./firestitch-report-echarts-1VNsyco5.mjs').then((module) => module.default) }),
3275
3289
  ], viewQueries: [{ propertyName: "_split", first: true, predicate: ["split"], descendants: true, static: true }, { propertyName: "_chatPanel", first: true, predicate: ["chatPanel"], descendants: true, read: ElementRef, static: true }], ngImport: i0, template: "<div\n #split\n class=\"report fs-row.align-start\">\n <fs-ai-chat\n #chatPanel\n class=\"chat\"\n style=\"min-height: 500px;\"\n basePath=\"reports\"\n [requestData]=\"{ reportId: selected?.id ?? null }\"\n [introMessage]=\"introMessage\"\n (response)=\"onChatResponse($event)\">\n </fs-ai-chat>\n <div\n class=\"resizer\"\n (pointerdown)=\"onResizeStart($event)\">\n </div>\n <div class=\"viewer fs-flex fs-column\">\n <div class=\"fs-row.align-center.gap-sm\">\n <fs-autocomplete-chips\n class=\"fs-flex\"\n [fetch]=\"fetchReports\"\n [(ngModel)]=\"selected\"\n [disabled]=\"loadingReports\"\n [padless]=\"true\"\n [multiple]=\"false\"\n [fetchOnFocus]=\"true\"\n (ngModelChange)=\"reportChange($event)\"\n placeholder=\"Report\"\n name=\"report\">\n <ng-template\n fsAutocompleteChipsTemplate\n let-object=\"object\">\n {{ object.name }}\n </ng-template>\n <ng-template\n fsAutocompleteChipsStatic\n (click)=\"createReport()\">\n Create Report\n </ng-template>\n </fs-autocomplete-chips>\n @if (report) {\n <fs-menu>\n <ng-template\n fs-menu-item\n (click)=\"reportSettings()\">\n <mat-icon>\n tune\n </mat-icon>\n Report settings\n </ng-template>\n <ng-template\n fs-menu-item\n (click)=\"toggleEditMode()\"\n [disabled]=\"editMode\">\n <mat-icon>\n dashboard_2_edit\n </mat-icon>\n {{ editMode ? 'Done editing layout' : 'Edit layout' }}\n </ng-template>\n <ng-template\n fs-menu-item\n (click)=\"exportPowerpoint()\">\n <mat-icon>\n slideshow\n </mat-icon>\n Export PowerPoint\n </ng-template>\n <ng-template\n fs-menu-item\n (click)=\"exportPdf()\">\n <mat-icon>\n picture_as_pdf\n </mat-icon>\n Export PDF\n </ng-template>\n </fs-menu>\n }\n </div>\n @if (report) {\n <!-- Report-level filters only (the report's actions live in the menu\n above). fs-filter reads its config once at init, so it's keyed on the\n filter signature: when the report-level filter set changes the block\n is recreated and re-reads the rebuilt config. -->\n @if (reportHasFilters) {\n @for (key of [reportFilterKey]; track key) {\n <fs-filter [config]=\"reportFilterConfig\"></fs-filter>\n }\n }\n <app-report-canvas\n [report]=\"report\"\n [editMode]=\"editMode\"\n (componentSettings)=\"componentSettings($event)\"\n (reportChanged)=\"onCanvasReportChanged()\"\n (editDone)=\"toggleEditMode()\">\n </app-report-canvas>\n }\n </div>\n</div>", styles: [".report{height:100%;min-height:500px}.report .chat{display:block;flex:0 0 25%;width:100%;height:100%;min-height:500px;min-width:0;border:none}.report .viewer{display:flex;flex-direction:column;min-width:0;height:100%;overflow:hidden}.report .viewer fs-filter{flex:0 0 auto;margin-top:10px;margin-bottom:0}.report .viewer ::ng-deep .mat-mdc-form-field-subscript-wrapper{display:none}.report .resizer{flex:0 0 11px;align-self:stretch;display:flex;justify-content:center;cursor:col-resize;touch-action:none;-webkit-user-select:none;user-select:none}.report .resizer:before{content:\"\";width:0px;background:#0000001f;transition:width .12s ease,background-color .12s ease}.report .resizer:hover:before{width:3px;background:var(--brand-primary-color)}.report.resizing{cursor:col-resize;-webkit-user-select:none;user-select:none}.report.resizing .resizer:before{width:3px;background:var(--brand-primary-color)}.report.resizing .chat,.report.resizing .viewer{pointer-events:none}::ng-deep body.body-report-reports .mat-mdc-card-content{display:flex;flex-direction:column;box-sizing:border-box}::ng-deep body.body-report-reports .mat-mdc-card-content mat-tab-nav-panel{flex:1;min-height:0}::ng-deep body.body-report-reports .mat-mdc-card-content mat-tab-nav-panel router-outlet+ng-component{height:100%}\n"], dependencies: [{ kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1$3.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$3.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "ngmodule", type: FsAutocompleteChipsModule }, { kind: "component", type: i2$2.FsAutocompleteChipsComponent, selector: "fs-autocomplete-chips", inputs: ["fetch", "appearance", "floatLabel", "readonly", "size", "label", "placeholder", "chipImage", "chipBackground", "chipColor", "chipIcon", "chipIconColor", "chipClass", "chipPadding", "shape", "hint", "allowText", "allowObject", "delay", "minPanelWidth", "maxPanelHeight", "validateText", "removable", "allowClear", "color", "background", "orderable", "padless", "initOnClick", "fetchOnFocus", "multiple", "multipleAdd", "confirm", "disabled", "groupBy", "panelWidth", "panelClass", "compareWith"], outputs: ["selected", "removed", "reordered", "clear", "panelOpened", "panelClosed"] }, { kind: "directive", type: i2$2.FsAutocompleteObjectDirective, selector: "[fsAutocompleteObject],[fsAutocompleteChipsTemplate]" }, { kind: "directive", type: i2$2.FsAutocompleteChipsStaticDirective, selector: "[fsAutocompleteChipsStatic]", inputs: ["show", "disable"], outputs: ["click", "selected"] }, { kind: "ngmodule", type: FsFilterModule }, { kind: "component", type: i2.FilterComponent, selector: "fs-filter", inputs: ["config"], outputs: ["closed", "opened", "ready"] }, { kind: "ngmodule", type: FsMenuModule }, { kind: "component", type: i2$3.FsMenuComponent, selector: "fs-menu", inputs: ["class", "buttonClass", "buttonType", "buttonColor"], outputs: ["opened", "closed"] }, { kind: "directive", type: i2$3.FsMenuItemDirective, selector: "fs-menu-group,[fs-menu-item]" }, { kind: "component", type: FsAiChatComponent, selector: "fs-ai-chat", inputs: ["basePath", "requestData", "introMessage"], outputs: ["response"] }, { kind: "component", type: MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "component", type: ReportCanvasComponent, selector: "app-report-canvas", inputs: ["report", "editMode"], outputs: ["componentSettings", "reportChanged", "editDone"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
3276
3290
  }
3277
3291
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: ReportComponent, decorators: [{
@@ -3285,7 +3299,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
3285
3299
  ReportPdfService,
3286
3300
  // Tree-shaken ECharts core + the 'report' house theme, loaded lazily with
3287
3301
  // this route's chunk.
3288
- provideEchartsCore({ echarts: () => import('./firestitch-report-echarts-Cel1iHUg.mjs').then((module) => module.default) }),
3302
+ provideEchartsCore({ echarts: () => import('./firestitch-report-echarts-1VNsyco5.mjs').then((module) => module.default) }),
3289
3303
  ], imports: [
3290
3304
  FormsModule,
3291
3305
  FsAutocompleteChipsModule,
@@ -3303,6 +3317,49 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
3303
3317
  args: ['chatPanel', { static: true, read: ElementRef }]
3304
3318
  }] } });
3305
3319
 
3320
+ // The AI Knowledge document's version history (append-only), each version
3321
+ // previewed and restorable — opened from the Report Settings footer's View
3322
+ // History button. Restores happen here (confirm, then flip the current flag —
3323
+ // no new version is created); the opener refreshes its instructions after this
3324
+ // dialog closes, so a restore made here reseeds the editor there.
3325
+ class InstructionHistoryComponent {
3326
+ _data = inject(MAT_DIALOG_DATA);
3327
+ versions = signal(this._data.versions);
3328
+ _reportData = inject(ReportData);
3329
+ _prompt = inject(FsPrompt);
3330
+ _message = inject(FsMessage);
3331
+ _destroyRef = inject(DestroyRef);
3332
+ // Make an earlier version current — flips the flag, creates no new version.
3333
+ restore(version) {
3334
+ this._prompt.confirm({
3335
+ title: 'Restore version',
3336
+ template: 'Make this version the current AI knowledge? The newer versions stay in the history.',
3337
+ commitLabel: 'Restore',
3338
+ })
3339
+ .pipe(switchMap(() => this._reportData.restoreInstruction(version.id)), takeUntilDestroyed(this._destroyRef))
3340
+ .subscribe((instructions) => {
3341
+ this.versions.set(instructions.versions);
3342
+ this._message.success('Version restored');
3343
+ });
3344
+ }
3345
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: InstructionHistoryComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
3346
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "18.2.14", type: InstructionHistoryComponent, isStandalone: true, selector: "ng-component", ngImport: i0, template: "<fs-dialog>\n <fs-dialog-title>\n AI Knowledge History\n </fs-dialog-title>\n <mat-dialog-content>\n @if (versions().length) {\n <div class=\"versions\">\n @for (version of versions(); track version.id) {\n <div class=\"version\">\n <div class=\"version-header\">\n <fs-date\n [date]=\"version.createDate\"\n format=\"date-time\">\n </fs-date>\n @if (version.accountName) {\n <span class=\"author\">{{ version.accountName }}</span>\n }\n <span class=\"spacer\"></span>\n @if (version.current) {\n <fs-chip\n size=\"tiny\"\n [backgroundColor]=\"'#2e7d32'\"\n color=\"#ffffff\">\n Current\n </fs-chip>\n } @else {\n <button\n type=\"button\"\n mat-stroked-button\n (click)=\"restore(version)\">\n Restore\n </button>\n }\n </div>\n <div class=\"version-preview\">\n <fs-markdown-renderer [markdown]=\"version.content\"></fs-markdown-renderer>\n </div>\n </div>\n }\n </div>\n } @else {\n <fs-message-info>\n No versions yet \u2014 every save of the AI Knowledge document lands here, and\n any version can be restored.\n </fs-message-info>\n }\n </mat-dialog-content>\n <mat-dialog-actions>\n <button\n type=\"button\"\n mat-button\n color=\"primary\"\n [mat-dialog-close]=\"null\">\n Close\n </button>\n </mat-dialog-actions>\n</fs-dialog>\n", styles: [".version{border:1px solid rgba(0,0,0,.12);border-radius:6px;padding:12px}.version+.version{margin-top:12px}.version .version-header{display:flex;align-items:center;gap:8px}.version .version-header .author{color:#0009}.version .version-header .spacer{flex:1}.version .version-preview{margin-top:8px;max-height:200px;overflow-y:auto;border-top:1px solid rgba(0,0,0,.08);padding-top:8px}\n"], dependencies: [{ kind: "component", type: MatButton, selector: " button[mat-button], button[mat-raised-button], button[mat-flat-button], button[mat-stroked-button] ", exportAs: ["matButton"] }, { kind: "directive", type: MatDialogContent, selector: "[mat-dialog-content], mat-dialog-content, [matDialogContent]" }, { kind: "directive", type: MatDialogActions, selector: "[mat-dialog-actions], mat-dialog-actions, [matDialogActions]", inputs: ["align"] }, { kind: "directive", type: MatDialogClose, selector: "[mat-dialog-close], [matDialogClose]", inputs: ["aria-label", "type", "mat-dialog-close", "matDialogClose"], exportAs: ["matDialogClose"] }, { kind: "ngmodule", type: FsChipModule }, { kind: "component", type: i1$4.FsChipComponent, selector: "fs-chip", inputs: ["selectable", "removable", "value", "maxWidth", "width", "backgroundColor", "borderColor", "color", "shape", "outlined", "outlineDash", "icon", "image", "selected", "disabled", "padding", "contrastColor", "size"], outputs: ["selectedToggled", "removed", "click"] }, { kind: "ngmodule", type: FsDateModule }, { kind: "component", type: i2$4.FsDateComponent, selector: "fs-date", inputs: ["date", "format", "timezone"] }, { kind: "ngmodule", type: FsDialogModule }, { kind: "component", type: i3.FsDialogComponent, selector: "fs-dialog", inputs: ["mobileMode", "mobileButtonPlacement", "mobileWidth", "mode", "buttonLayout"] }, { kind: "component", type: i3.FsDialogTitleComponent, selector: "fs-dialog-title", inputs: ["close", "dockToggle", "back"] }, { kind: "component", type: FsMarkdownRendererComponent, selector: "fs-markdown-renderer", inputs: ["config", "markdown"] }, { kind: "ngmodule", type: FsMessageModule }, { kind: "component", type: i4.FsMessageInfoComponent, selector: "fs-message-info" }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
3347
+ }
3348
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: InstructionHistoryComponent, decorators: [{
3349
+ type: Component,
3350
+ args: [{ changeDetection: ChangeDetectionStrategy.OnPush, standalone: true, imports: [
3351
+ MatButton,
3352
+ MatDialogContent,
3353
+ MatDialogActions,
3354
+ MatDialogClose,
3355
+ FsChipModule,
3356
+ FsDateModule,
3357
+ FsDialogModule,
3358
+ FsMarkdownRendererComponent,
3359
+ FsMessageModule,
3360
+ ], template: "<fs-dialog>\n <fs-dialog-title>\n AI Knowledge History\n </fs-dialog-title>\n <mat-dialog-content>\n @if (versions().length) {\n <div class=\"versions\">\n @for (version of versions(); track version.id) {\n <div class=\"version\">\n <div class=\"version-header\">\n <fs-date\n [date]=\"version.createDate\"\n format=\"date-time\">\n </fs-date>\n @if (version.accountName) {\n <span class=\"author\">{{ version.accountName }}</span>\n }\n <span class=\"spacer\"></span>\n @if (version.current) {\n <fs-chip\n size=\"tiny\"\n [backgroundColor]=\"'#2e7d32'\"\n color=\"#ffffff\">\n Current\n </fs-chip>\n } @else {\n <button\n type=\"button\"\n mat-stroked-button\n (click)=\"restore(version)\">\n Restore\n </button>\n }\n </div>\n <div class=\"version-preview\">\n <fs-markdown-renderer [markdown]=\"version.content\"></fs-markdown-renderer>\n </div>\n </div>\n }\n </div>\n } @else {\n <fs-message-info>\n No versions yet \u2014 every save of the AI Knowledge document lands here, and\n any version can be restored.\n </fs-message-info>\n }\n </mat-dialog-content>\n <mat-dialog-actions>\n <button\n type=\"button\"\n mat-button\n color=\"primary\"\n [mat-dialog-close]=\"null\">\n Close\n </button>\n </mat-dialog-actions>\n</fs-dialog>\n", styles: [".version{border:1px solid rgba(0,0,0,.12);border-radius:6px;padding:12px}.version+.version{margin-top:12px}.version .version-header{display:flex;align-items:center;gap:8px}.version .version-header .author{color:#0009}.version .version-header .spacer{flex:1}.version .version-preview{margin-top:8px;max-height:200px;overflow-y:auto;border-top:1px solid rgba(0,0,0,.08);padding-top:8px}\n"] }]
3361
+ }] });
3362
+
3306
3363
  // The AI Knowledge tab: the operator-authored markdown document appended to the
3307
3364
  // report agent's system instructions on every chat turn — the lever for
3308
3365
  // terminology, business rules and special cases no code deploy can cover.
@@ -3327,35 +3384,106 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
3327
3384
  ], template: "<div class=\"fs-column tab-body\">\n <p class=\"hint\">\n This document is given to the report AI on every chat message. Use it for\n terminology, business rules and special cases the AI can't work out from the\n data on its own \u2014 it takes effect immediately, no deploy needed.\n </p>\n <fs-markdown-editor\n [ngModel]=\"content()\"\n (ngModelChange)=\"content.set($event)\"\n [config]=\"config()\"\n name=\"content\">\n </fs-markdown-editor>\n</div>\n", styles: [".tab-body{padding-top:16px}.hint{margin:0 0 12px;color:#0009;font-size:13px;line-height:1.5}fs-markdown-editor{display:block;min-height:320px}\n"] }]
3328
3385
  }] });
3329
3386
 
3330
- // The History tab: every saved version of the AI Knowledge document
3331
- // (append-only), each previewed and restorable. Restoring mutates the current
3332
- // version and reseeds the editor — a parent concern — so this stays
3333
- // presentational: it renders the versions it is given and emits the one to
3334
- // restore.
3335
- class HistoryComponent {
3336
- versions = input.required();
3337
- restore = output();
3338
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: HistoryComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
3339
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "18.2.14", type: HistoryComponent, isStandalone: true, selector: "fs-ai-report-global-settings-history", inputs: { versions: { classPropertyName: "versions", publicName: "versions", isSignal: true, isRequired: true, transformFunction: null } }, outputs: { restore: "restore" }, ngImport: i0, template: "<div class=\"fs-column tab-body\">\n @if (versions().length) {\n @for (version of versions(); track version.id) {\n <div class=\"version\">\n <div class=\"version-header\">\n <fs-date\n [date]=\"version.createDate\"\n format=\"date-time\">\n </fs-date>\n @if (version.accountName) {\n <span class=\"author\">{{ version.accountName }}</span>\n }\n <span class=\"spacer\"></span>\n @if (version.current) {\n <fs-chip\n size=\"tiny\"\n [backgroundColor]=\"'#2e7d32'\"\n color=\"#ffffff\">\n Current\n </fs-chip>\n } @else {\n <button\n type=\"button\"\n mat-stroked-button\n (click)=\"restore.emit(version)\">\n Restore\n </button>\n }\n </div>\n <div class=\"version-preview\">\n <fs-markdown-renderer [markdown]=\"version.content\"></fs-markdown-renderer>\n </div>\n </div>\n }\n } @else {\n <fs-message-info>\n No versions yet \u2014 every save of the AI Knowledge document lands here, and\n any version can be restored.\n </fs-message-info>\n }\n</div>\n", styles: [".tab-body{padding-top:16px}.version{border:1px solid rgba(0,0,0,.12);border-radius:6px;padding:12px}.version+.version{margin-top:12px}.version .version-header{display:flex;align-items:center;gap:8px}.version .version-header .author{color:#0009}.version .version-header .spacer{flex:1}.version .version-preview{margin-top:8px;max-height:200px;overflow-y:auto;border-top:1px solid rgba(0,0,0,.08);padding-top:8px}\n"], dependencies: [{ kind: "component", type: MatButton, selector: " button[mat-button], button[mat-raised-button], button[mat-flat-button], button[mat-stroked-button] ", exportAs: ["matButton"] }, { kind: "ngmodule", type: FsChipModule }, { kind: "component", type: i1$4.FsChipComponent, selector: "fs-chip", inputs: ["selectable", "removable", "value", "maxWidth", "width", "backgroundColor", "borderColor", "color", "shape", "outlined", "outlineDash", "icon", "image", "selected", "disabled", "padding", "contrastColor", "size"], outputs: ["selectedToggled", "removed", "click"] }, { kind: "ngmodule", type: FsDateModule }, { kind: "component", type: i2$4.FsDateComponent, selector: "fs-date", inputs: ["date", "format", "timezone"] }, { kind: "ngmodule", type: FsMessageModule }, { kind: "component", type: i4.FsMessageInfoComponent, selector: "fs-message-info" }, { kind: "component", type: FsMarkdownRendererComponent, selector: "fs-markdown-renderer", inputs: ["config", "markdown"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
3387
+ // The General tab: report-module settings that aren't the AI knowledge document.
3388
+ // Currently just the semantic-build concurrency — how many summary calls the
3389
+ // build runs at once. A field-level child of the settings dialog form (its input
3390
+ // registers with the parent <form fsForm> via the ControlContainer view
3391
+ // provider), so the dialog's Save button submits and validates it. The value
3392
+ // itself is held on the shell (bound through `concurrency`), and the safe band
3393
+ // (`min`/`max`) comes from the backend so the field can't be set out of range.
3394
+ class GeneralComponent {
3395
+ concurrency = model(0);
3396
+ min = input.required();
3397
+ max = input.required();
3398
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: GeneralComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
3399
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "18.2.14", type: GeneralComponent, isStandalone: true, selector: "fs-ai-report-global-settings-general", inputs: { concurrency: { classPropertyName: "concurrency", publicName: "concurrency", isSignal: true, isRequired: false, transformFunction: null }, min: { classPropertyName: "min", publicName: "min", isSignal: true, isRequired: true, transformFunction: null }, max: { classPropertyName: "max", publicName: "max", isSignal: true, isRequired: true, transformFunction: null } }, outputs: { concurrency: "concurrencyChange" }, ngImport: i0, template: "<div class=\"fs-column tab-body\">\n <mat-form-field>\n <mat-label>Semantic build concurrency</mat-label>\n <input\n matInput\n type=\"number\"\n [ngModel]=\"concurrency()\"\n (ngModelChange)=\"concurrency.set($event)\"\n name=\"semanticBuildConcurrency\"\n [fsFormRequired]=\"true\"\n [fsFormInteger]=\"true\"\n [fsFormMin]=\"min()\"\n [fsFormMax]=\"max()\">\n <mat-hint>\n How many AI calls the semantic-database build runs at once ({{ min() }}\u2013{{ max() }}).\n Higher is faster but more likely to hit the AI provider's rate limit. Default is 8.\n </mat-hint>\n </mat-form-field>\n</div>\n", styles: [".tab-body{padding-top:16px}mat-form-field{display:block;max-width:340px}\n"], dependencies: [{ kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1$3.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1$3.NumberValueAccessor, selector: "input[type=number][formControlName],input[type=number][formControl],input[type=number][ngModel]" }, { kind: "directive", type: i1$3.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$3.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "ngmodule", type: FsFormModule }, { kind: "directive", type: i2$1.FsFormRequiredDirective, selector: "[fsFormRequired],[ngModel][required]", inputs: ["fsFormRequired", "required", "fsFormRequiredMessage"] }, { kind: "directive", type: i2$1.FsFormMinDirective, selector: "[fsFormMin]", inputs: ["fsFormMin", "fsFormMinMessage"] }, { kind: "directive", type: i2$1.FsFormMaxDirective, selector: "[fsFormMax]", inputs: ["fsFormMax", "fsFormMaxMessage"] }, { kind: "directive", type: i2$1.FsFormIntegerDirective, selector: "[fsFormInteger]", inputs: ["fsFormInteger", "fsFormIntegerMessage"] }, { kind: "component", type: MatFormField, selector: "mat-form-field", inputs: ["hideRequiredMarker", "color", "floatLabel", "appearance", "subscriptSizing", "hintLabel"], exportAs: ["matFormField"] }, { kind: "directive", type: MatLabel, selector: "mat-label" }, { kind: "directive", type: MatHint, selector: "mat-hint", inputs: ["align", "id"] }, { kind: "directive", type: MatInput, selector: "input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]", inputs: ["disabled", "id", "placeholder", "name", "required", "type", "errorStateMatcher", "aria-describedby", "value", "readonly"], exportAs: ["matInput"] }], viewProviders: [{ provide: ControlContainer, useExisting: NgForm }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
3340
3400
  }
3341
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: HistoryComponent, decorators: [{
3401
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: GeneralComponent, decorators: [{
3342
3402
  type: Component,
3343
- args: [{ selector: 'fs-ai-report-global-settings-history', changeDetection: ChangeDetectionStrategy.OnPush, standalone: true, imports: [
3403
+ args: [{ selector: 'fs-ai-report-global-settings-general', viewProviders: [{ provide: ControlContainer, useExisting: NgForm }], changeDetection: ChangeDetectionStrategy.OnPush, standalone: true, imports: [
3404
+ FormsModule,
3405
+ FsFormModule,
3406
+ MatFormField,
3407
+ MatLabel,
3408
+ MatHint,
3409
+ MatInput,
3410
+ ], template: "<div class=\"fs-column tab-body\">\n <mat-form-field>\n <mat-label>Semantic build concurrency</mat-label>\n <input\n matInput\n type=\"number\"\n [ngModel]=\"concurrency()\"\n (ngModelChange)=\"concurrency.set($event)\"\n name=\"semanticBuildConcurrency\"\n [fsFormRequired]=\"true\"\n [fsFormInteger]=\"true\"\n [fsFormMin]=\"min()\"\n [fsFormMax]=\"max()\">\n <mat-hint>\n How many AI calls the semantic-database build runs at once ({{ min() }}\u2013{{ max() }}).\n Higher is faster but more likely to hit the AI provider's rate limit. Default is 8.\n </mat-hint>\n </mat-form-field>\n</div>\n", styles: [".tab-body{padding-top:16px}mat-form-field{display:block;max-width:340px}\n"] }]
3411
+ }] });
3412
+
3413
+ // One group of an entity's AI-classified cached code functions — the model
3414
+ // class's or the handler class's, per the source it was opened for. Fetched on
3415
+ // open from the shared per-entity endpoint (one payload carries both groups,
3416
+ // each row tagged with its source) and filtered client-side.
3417
+ class EntityFunctionsComponent {
3418
+ // One color per AI classification kind, so a function's nature reads at a glance.
3419
+ static KIND_COLORS = {
3420
+ 'calculation': '#1565c0',
3421
+ 'business-rule': '#6a1b9a',
3422
+ 'query': '#00695c',
3423
+ 'mutation': '#e65100',
3424
+ 'accessor': '#546e7a',
3425
+ 'other': '#757575',
3426
+ };
3427
+ // Interesting logic first: derived values and rules, then data access, then the rest.
3428
+ static KIND_ORDER = ['calculation', 'business-rule', 'query', 'mutation', 'accessor', 'other'];
3429
+ _data = inject(MAT_DIALOG_DATA);
3430
+ entityKey = this._data.entityKey;
3431
+ sourceLabel = this._data.source === 'model' ? 'Model functions' : 'Handler functions';
3432
+ // null until loaded — gates the list behind the skeleton.
3433
+ entityFunctions = signal(null);
3434
+ // The class this group's functions were discovered from.
3435
+ discoveredClass = computed(() => {
3436
+ const loaded = this.entityFunctions();
3437
+ return this._data.source === 'model' ? loaded?.modelClass : loaded?.handlerClass;
3438
+ });
3439
+ // This group's functions, ordered by kind priority then method name.
3440
+ functions = computed(() => this._sortFunctions((this.entityFunctions()?.functions ?? [])
3441
+ .filter((fn) => fn.source === this._data.source)));
3442
+ constructor() {
3443
+ inject(ReportData).semanticEntityFunctions(this._data.entityKey)
3444
+ .pipe(takeUntilDestroyed())
3445
+ .subscribe((entityFunctions) => {
3446
+ this.entityFunctions.set(entityFunctions);
3447
+ });
3448
+ }
3449
+ kindColor(kind) {
3450
+ return EntityFunctionsComponent.KIND_COLORS[kind] ?? EntityFunctionsComponent.KIND_COLORS.other;
3451
+ }
3452
+ _sortFunctions(functions) {
3453
+ return [...functions]
3454
+ .sort((a, b) => {
3455
+ const kindDiff = this._kindRank(a.kind) - this._kindRank(b.kind);
3456
+ return kindDiff !== 0 ? kindDiff : a.method.localeCompare(b.method);
3457
+ });
3458
+ }
3459
+ _kindRank(kind) {
3460
+ const rank = EntityFunctionsComponent.KIND_ORDER.indexOf(kind);
3461
+ return rank === -1 ? EntityFunctionsComponent.KIND_ORDER.length : rank;
3462
+ }
3463
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: EntityFunctionsComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
3464
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "18.2.14", type: EntityFunctionsComponent, isStandalone: true, selector: "ng-component", ngImport: i0, template: "<fs-dialog>\n <fs-dialog-title>\n {{ entityKey }} \u2014 {{ sourceLabel }}\n </fs-dialog-title>\n <mat-dialog-content>\n <div *fsSkeleton=\"entityFunctions()\">\n @if (entityFunctions(); as fns) {\n @if (functions().length) {\n @if (fns.generatedAt) {\n <div class=\"hint functions-generated\">\n Discovered from {{ discoveredClass() }} &middot;\n <fs-date\n [date]=\"fns.generatedAt\"\n format=\"date-time\">\n </fs-date>\n </div>\n }\n <div class=\"functions-list\">\n @for (fn of functions(); track fn.class + '::' + fn.method) {\n <div class=\"function-row\">\n <div class=\"function-head\">\n <fs-chip\n size=\"tiny\"\n [backgroundColor]=\"kindColor(fn.kind)\"\n color=\"#ffffff\">\n {{ fn.kind }}\n </fs-chip>\n <code class=\"function-method\">{{ fn.method }}</code>\n </div>\n <div class=\"function-summary\">{{ fn.summary }}</div>\n @if (fn.produces) {\n <div class=\"function-produces hint\">Produces: {{ fn.produces }}</div>\n }\n </div>\n }\n </div>\n } @else {\n <div class=\"hint\">\n No functions cached for this entity yet \u2014 run a build to discover its code.\n </div>\n }\n }\n </div>\n </mat-dialog-content>\n <mat-dialog-actions>\n <button\n type=\"button\"\n mat-button\n color=\"primary\"\n [mat-dialog-close]=\"undefined\">\n Close\n </button>\n </mat-dialog-actions>\n</fs-dialog>\n", styles: [".hint{margin:0;color:#0009;font-size:13px;line-height:1.5}.functions-generated{margin-bottom:10px}.functions-list{display:flex;flex-direction:column}.functions-list .function-row{display:flex;flex-direction:column;gap:3px;padding:10px 0}.functions-list .function-row+.function-row{border-top:1px solid rgba(0,0,0,.08)}.functions-list .function-row .function-head{display:flex;align-items:center;gap:8px}.functions-list .function-row .function-method{font-family:monospace;font-size:13px;font-weight:600}.functions-list .function-row .function-summary{color:#000000bf}.functions-list .function-row .function-produces{padding-left:10px}\n"], dependencies: [{ kind: "component", type: MatButton, selector: " button[mat-button], button[mat-raised-button], button[mat-flat-button], button[mat-stroked-button] ", exportAs: ["matButton"] }, { kind: "directive", type: MatDialogContent, selector: "[mat-dialog-content], mat-dialog-content, [matDialogContent]" }, { kind: "directive", type: MatDialogActions, selector: "[mat-dialog-actions], mat-dialog-actions, [matDialogActions]", inputs: ["align"] }, { kind: "directive", type: MatDialogClose, selector: "[mat-dialog-close], [matDialogClose]", inputs: ["aria-label", "type", "mat-dialog-close", "matDialogClose"], exportAs: ["matDialogClose"] }, { kind: "ngmodule", type: FsChipModule }, { kind: "component", type: i1$4.FsChipComponent, selector: "fs-chip", inputs: ["selectable", "removable", "value", "maxWidth", "width", "backgroundColor", "borderColor", "color", "shape", "outlined", "outlineDash", "icon", "image", "selected", "disabled", "padding", "contrastColor", "size"], outputs: ["selectedToggled", "removed", "click"] }, { kind: "ngmodule", type: FsDateModule }, { kind: "component", type: i2$4.FsDateComponent, selector: "fs-date", inputs: ["date", "format", "timezone"] }, { kind: "ngmodule", type: FsDialogModule }, { kind: "component", type: i3.FsDialogComponent, selector: "fs-dialog", inputs: ["mobileMode", "mobileButtonPlacement", "mobileWidth", "mode", "buttonLayout"] }, { kind: "component", type: i3.FsDialogTitleComponent, selector: "fs-dialog-title", inputs: ["close", "dockToggle", "back"] }, { kind: "ngmodule", type: FsSkeletonModule }, { kind: "directive", type: i4$1.FsSkeletonContentDirective, selector: "[fsSkeleton]", inputs: ["fsSkeleton", "fsSkeletonPattern"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
3465
+ }
3466
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: EntityFunctionsComponent, decorators: [{
3467
+ type: Component,
3468
+ args: [{ changeDetection: ChangeDetectionStrategy.OnPush, standalone: true, imports: [
3344
3469
  MatButton,
3470
+ MatDialogContent,
3471
+ MatDialogActions,
3472
+ MatDialogClose,
3345
3473
  FsChipModule,
3346
3474
  FsDateModule,
3347
- FsMessageModule,
3348
- FsMarkdownRendererComponent,
3349
- ], template: "<div class=\"fs-column tab-body\">\n @if (versions().length) {\n @for (version of versions(); track version.id) {\n <div class=\"version\">\n <div class=\"version-header\">\n <fs-date\n [date]=\"version.createDate\"\n format=\"date-time\">\n </fs-date>\n @if (version.accountName) {\n <span class=\"author\">{{ version.accountName }}</span>\n }\n <span class=\"spacer\"></span>\n @if (version.current) {\n <fs-chip\n size=\"tiny\"\n [backgroundColor]=\"'#2e7d32'\"\n color=\"#ffffff\">\n Current\n </fs-chip>\n } @else {\n <button\n type=\"button\"\n mat-stroked-button\n (click)=\"restore.emit(version)\">\n Restore\n </button>\n }\n </div>\n <div class=\"version-preview\">\n <fs-markdown-renderer [markdown]=\"version.content\"></fs-markdown-renderer>\n </div>\n </div>\n }\n } @else {\n <fs-message-info>\n No versions yet \u2014 every save of the AI Knowledge document lands here, and\n any version can be restored.\n </fs-message-info>\n }\n</div>\n", styles: [".tab-body{padding-top:16px}.version{border:1px solid rgba(0,0,0,.12);border-radius:6px;padding:12px}.version+.version{margin-top:12px}.version .version-header{display:flex;align-items:center;gap:8px}.version .version-header .author{color:#0009}.version .version-header .spacer{flex:1}.version .version-preview{margin-top:8px;max-height:200px;overflow-y:auto;border-top:1px solid rgba(0,0,0,.08);padding-top:8px}\n"] }]
3350
- }] });
3475
+ FsDialogModule,
3476
+ FsSkeletonModule,
3477
+ ], template: "<fs-dialog>\n <fs-dialog-title>\n {{ entityKey }} \u2014 {{ sourceLabel }}\n </fs-dialog-title>\n <mat-dialog-content>\n <div *fsSkeleton=\"entityFunctions()\">\n @if (entityFunctions(); as fns) {\n @if (functions().length) {\n @if (fns.generatedAt) {\n <div class=\"hint functions-generated\">\n Discovered from {{ discoveredClass() }} &middot;\n <fs-date\n [date]=\"fns.generatedAt\"\n format=\"date-time\">\n </fs-date>\n </div>\n }\n <div class=\"functions-list\">\n @for (fn of functions(); track fn.class + '::' + fn.method) {\n <div class=\"function-row\">\n <div class=\"function-head\">\n <fs-chip\n size=\"tiny\"\n [backgroundColor]=\"kindColor(fn.kind)\"\n color=\"#ffffff\">\n {{ fn.kind }}\n </fs-chip>\n <code class=\"function-method\">{{ fn.method }}</code>\n </div>\n <div class=\"function-summary\">{{ fn.summary }}</div>\n @if (fn.produces) {\n <div class=\"function-produces hint\">Produces: {{ fn.produces }}</div>\n }\n </div>\n }\n </div>\n } @else {\n <div class=\"hint\">\n No functions cached for this entity yet \u2014 run a build to discover its code.\n </div>\n }\n }\n </div>\n </mat-dialog-content>\n <mat-dialog-actions>\n <button\n type=\"button\"\n mat-button\n color=\"primary\"\n [mat-dialog-close]=\"undefined\">\n Close\n </button>\n </mat-dialog-actions>\n</fs-dialog>\n", styles: [".hint{margin:0;color:#0009;font-size:13px;line-height:1.5}.functions-generated{margin-bottom:10px}.functions-list{display:flex;flex-direction:column}.functions-list .function-row{display:flex;flex-direction:column;gap:3px;padding:10px 0}.functions-list .function-row+.function-row{border-top:1px solid rgba(0,0,0,.08)}.functions-list .function-row .function-head{display:flex;align-items:center;gap:8px}.functions-list .function-row .function-method{font-family:monospace;font-size:13px;font-weight:600}.functions-list .function-row .function-summary{color:#000000bf}.functions-list .function-row .function-produces{padding-left:10px}\n"] }]
3478
+ }], ctorParameters: () => [] });
3351
3479
 
3352
- // The Semantic Database tab: the model scoreboard the report AI is built from.
3353
- // Shows the last build's status, runs (or stops) a build — streamed into the
3354
- // FsProcess dock so it survives the dialog closing — and drills into every
3355
- // entity to reveal its shape, inlined enum values, computed fields and the
3356
- // AI-classified code functions cached from its model class. Each entity's
3357
- // functions load lazily on first expand, because the full set across all
3358
- // entities is far too big to ship with the scoreboard.
3480
+ // The Semantic Database tab: the entity scoreboard the report AI is built from
3481
+ // (an entity = a table + a model + a handler). Shows the last build's status,
3482
+ // runs (or stops) a build — streamed into the FsProcess dock so it survives the
3483
+ // dialog closing — and drills into every entity to reveal its shape, inlined
3484
+ // enum values, computed fields and its cached function counts, whose links open
3485
+ // the functions dialog (the full function set across all entities is far too
3486
+ // big to ship with the scoreboard).
3359
3487
  class SemanticDatabaseComponent {
3360
3488
  // The build-in-flight states of the ff_processes row (queued/running/stopping).
3361
3489
  static ACTIVE_STATES = ['Q', 'R', 'X'];
@@ -3372,17 +3500,6 @@ class SemanticDatabaseComponent {
3372
3500
  // failure.
3373
3501
  static DONE_COLOR = '#2e7d32';
3374
3502
  static PENDING_COLOR = '#9e9e9e';
3375
- // One color per AI classification kind, so a function's nature reads at a glance.
3376
- static KIND_COLORS = {
3377
- 'calculation': '#1565c0',
3378
- 'business-rule': '#6a1b9a',
3379
- 'query': '#00695c',
3380
- 'mutation': '#e65100',
3381
- 'accessor': '#546e7a',
3382
- 'other': '#757575',
3383
- };
3384
- // Interesting logic first: derived values and rules, then data access, then the rest.
3385
- static KIND_ORDER = ['calculation', 'business-rule', 'query', 'mutation', 'accessor', 'other'];
3386
3503
  // The scoreboard's per-entity stage chips, left to right.
3387
3504
  stageKeys = ['introspected', 'enumsInlined', 'computed', 'functions'];
3388
3505
  stageLabels = {
@@ -3398,9 +3515,10 @@ class SemanticDatabaseComponent {
3398
3515
  buildActive = computed(() => this.building()
3399
3516
  || SemanticDatabaseComponent.ACTIVE_STATES.includes(this.status()?.build?.state));
3400
3517
  buildStateLabel = computed(() => SemanticDatabaseComponent.STATE_LABELS[this.status()?.build?.state] ?? '');
3401
- // Per-entity function caches, loaded on first expand. undefined = not
3402
- // requested yet, null = request in flight (the skeleton window), object = loaded.
3403
- _entityFunctions = signal({});
3518
+ // Guards the outcome message to fire exactly once per run — whether the stream
3519
+ // completes (build finished, possibly failed) or errors outright.
3520
+ _buildFinalized = false;
3521
+ _dialog = inject(MatDialog);
3404
3522
  _reportData = inject(ReportData);
3405
3523
  _process = inject(FsProcess);
3406
3524
  _prompt = inject(FsPrompt);
@@ -3441,22 +3559,59 @@ class SemanticDatabaseComponent {
3441
3559
  // dismissing the dialog never interrupts a running build.
3442
3560
  _runBuild() {
3443
3561
  this.building.set(true);
3562
+ this._buildFinalized = false;
3444
3563
  const target$ = this._reportData.semanticBuild()
3445
- .pipe(filter((event) => event?.type === StreamEventType.Data
3564
+ .pipe(
3565
+ // A hard stream/transport failure (the build couldn't even start)
3566
+ // finalizes here — completed$ won't fire in that case.
3567
+ tap({ error: (error) => this._finalizeBuild(error) }), filter((event) => event?.type === StreamEventType.Data
3446
3568
  && event.data?.kind === 'log'
3447
3569
  && typeof event.data.line === 'string'), map((event) => event.data.line));
3448
3570
  this._process
3449
3571
  .run('Semantic database build', target$)
3450
3572
  .completed$
3451
3573
  .pipe(take(1), takeUntilDestroyed(this._destroyRef))
3452
- .subscribe(() => {
3453
- this.building.set(false);
3454
- // The build rewrote every entity's function cache — drop the loaded
3455
- // copies so the next expand refetches fresh data.
3456
- this._entityFunctions.set({});
3574
+ .subscribe(() => this._finalizeBuild(null));
3575
+ }
3576
+ /**
3577
+ * Wrap up one build run exactly once. On a hard stream error there is no
3578
+ * status to read, so say so directly; otherwise refresh the status and
3579
+ * announce how the build actually ended — a build that failed (e.g. the AI
3580
+ * couldn't be reached) reports 'Failed' with a reason, and that must surface
3581
+ * instead of the run silently doing nothing.
3582
+ */
3583
+ _finalizeBuild(streamError) {
3584
+ if (this._buildFinalized) {
3585
+ return;
3586
+ }
3587
+ this._buildFinalized = true;
3588
+ this.building.set(false);
3589
+ if (streamError) {
3590
+ this._message.error('The semantic database build could not run — check the AI connection and try again.');
3457
3591
  this._refreshStatus();
3592
+ return;
3593
+ }
3594
+ this._reportData.semanticBuildStatus()
3595
+ .pipe(takeUntilDestroyed(this._destroyRef))
3596
+ .subscribe((status) => {
3597
+ this.status.set(status);
3598
+ this._announceOutcome(status);
3458
3599
  });
3459
3600
  }
3601
+ /**
3602
+ * Tell the operator how the build ended: a failed/stopped build surfaces its
3603
+ * reason (so an AI connection problem is never silent), a completed one
3604
+ * confirms success.
3605
+ */
3606
+ _announceOutcome(status) {
3607
+ const build = status?.build;
3608
+ if (build?.state === 'F' || build?.state === 'K') {
3609
+ this._message.error(build.message || 'The semantic database build did not finish.');
3610
+ }
3611
+ else if (build?.state === 'C') {
3612
+ this._message.success('Semantic database build complete.');
3613
+ }
3614
+ }
3460
3615
  stageColor(stage) {
3461
3616
  return stage === 'done'
3462
3617
  ? SemanticDatabaseComponent.DONE_COLOR
@@ -3465,37 +3620,18 @@ class SemanticDatabaseComponent {
3465
3620
  stageTooltip(stage) {
3466
3621
  return stage === 'done' ? 'Built' : 'Needs building';
3467
3622
  }
3468
- kindColor(kind) {
3469
- return SemanticDatabaseComponent.KIND_COLORS[kind] ?? SemanticDatabaseComponent.KIND_COLORS.other;
3470
- }
3471
- /**
3472
- * The loaded function cache for an entity: undefined until the panel is first
3473
- * expanded, null while the request is in flight (the skeleton window), then
3474
- * the loaded cache.
3475
- */
3476
- entityFunctions(key) {
3477
- return this._entityFunctions()[key];
3478
- }
3479
3623
  /**
3480
- * First expand of an entity: fetch its cached functions through the per-entity
3481
- * endpoint. Subsequent expands reuse the loaded copy (a finished build clears
3482
- * the copies so a re-expand refetches fresh data).
3624
+ * Open the child dialog listing one group of an entity's cached functions —
3625
+ * the model class's or the handler class's. The dialog fetches fresh on open,
3626
+ * so the tab never holds function payloads.
3483
3627
  */
3484
- entityOpened(entity) {
3485
- if (this._entityFunctions()[entity.key] !== undefined) {
3486
- return;
3487
- }
3488
- this._entityFunctions.update((loaded) => ({ ...loaded, [entity.key]: null }));
3489
- this._reportData.semanticEntityFunctions(entity.key)
3490
- .pipe(takeUntilDestroyed(this._destroyRef))
3491
- .subscribe((entityFunctions) => {
3492
- this._entityFunctions.update((loaded) => ({
3493
- ...loaded,
3494
- [entity.key]: {
3495
- ...entityFunctions,
3496
- functions: this._sortFunctions(entityFunctions),
3497
- },
3498
- }));
3628
+ openFunctions(entity, source) {
3629
+ this._dialog.open(EntityFunctionsComponent, {
3630
+ width: '700px',
3631
+ maxWidth: '95vw',
3632
+ maxHeight: '85vh',
3633
+ autoFocus: false,
3634
+ data: { entityKey: entity.key, source },
3499
3635
  });
3500
3636
  }
3501
3637
  _init() {
@@ -3506,23 +3642,8 @@ class SemanticDatabaseComponent {
3506
3642
  .pipe(takeUntilDestroyed(this._destroyRef))
3507
3643
  .subscribe((status) => this.status.set(status));
3508
3644
  }
3509
- /**
3510
- * Order once at load time (kind priority, then method name) so the template
3511
- * renders the list as-is.
3512
- */
3513
- _sortFunctions(entityFunctions) {
3514
- return [...entityFunctions.functions]
3515
- .sort((a, b) => {
3516
- const kindDiff = this._kindRank(a.kind) - this._kindRank(b.kind);
3517
- return kindDiff !== 0 ? kindDiff : a.method.localeCompare(b.method);
3518
- });
3519
- }
3520
- _kindRank(kind) {
3521
- const rank = SemanticDatabaseComponent.KIND_ORDER.indexOf(kind);
3522
- return rank === -1 ? SemanticDatabaseComponent.KIND_ORDER.length : rank;
3523
- }
3524
3645
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: SemanticDatabaseComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
3525
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "18.2.14", type: SemanticDatabaseComponent, isStandalone: true, selector: "fs-ai-report-global-settings-semantic-database", ngImport: i0, template: "<div class=\"fs-column tab-body\">\n <p class=\"hint\">\n The semantic database is what the report AI uses to understand your data \u2014\n tables, relationships and the calculations in code. Rebuild it after\n significant application updates. The build runs in the background; progress\n appears in the dock at the bottom right and this dialog can be closed while\n it runs.\n </p>\n\n <div *fsSkeleton=\"status()\">\n @if (status(); as buildStatus) {\n <div class=\"build-status\">\n @if (buildStatus.detail.generatedAt) {\n <div class=\"stat\">\n <span class=\"label\">Last built</span>\n <fs-date\n [date]=\"buildStatus.detail.generatedAt\"\n format=\"date-time\">\n </fs-date>\n </div>\n <div class=\"stat\">\n <span class=\"label\">Coverage</span>\n <span>\n {{ buildStatus.detail.entityCount }} entities \u00B7\n {{ buildStatus.detail.functionCount }} code functions\n </span>\n </div>\n } @else {\n <fs-message-warning>\n The semantic database has not been built yet \u2014 the report AI can't\n answer data questions until it is. Run the first build.\n </fs-message-warning>\n }\n @if (buildStatus.build) {\n <div class=\"stat\">\n <span class=\"label\">Last build run</span>\n <span>\n {{ buildStateLabel() }}\n @if (buildStatus.build.message) {\n \u2014 {{ buildStatus.build.message }}\n }\n </span>\n </div>\n }\n </div>\n\n <!-- The model scoreboard: every entity the AI knows about, its\n built-vs-needs-building stages, and a drill-down into its shape, enum\n values, computed fields and AI-classified code functions. -->\n @if (buildStatus.detail.entities?.length) {\n <mat-accordion class=\"entities\">\n @for (entity of buildStatus.detail.entities; track entity.key) {\n <mat-expansion-panel (opened)=\"entityOpened(entity)\">\n <mat-expansion-panel-header>\n <mat-panel-title>{{ entity.key }}</mat-panel-title>\n <mat-panel-description>\n <div class=\"stage-chips\">\n @for (stage of stageKeys; track stage) {\n <fs-chip\n size=\"tiny\"\n [backgroundColor]=\"stageColor(entity.stages[stage])\"\n color=\"#ffffff\"\n [matTooltip]=\"stageTooltip(entity.stages[stage])\">\n {{ stageLabels[stage] }}\n </fs-chip>\n }\n </div>\n </mat-panel-description>\n </mat-expansion-panel-header>\n\n <!-- Deferred: the body (and its functions request) only exists\n once expanded. -->\n <ng-template matExpansionPanelContent>\n <div class=\"entity-detail\">\n <div><strong>Table:</strong> {{ entity.table }}</div>\n <div><strong>Columns:</strong> {{ entity.columns }}</div>\n <div><strong>Relationships:</strong> {{ entity.relationships }}</div>\n @if (entity.enums.length) {\n <div class=\"enum-list\">\n <strong>Enum values:</strong>\n @for (enumColumn of entity.enums; track enumColumn.column) {\n <div class=\"enum-row\">\n <span class=\"enum-column\">{{ enumColumn.column }}</span>\n <span class=\"enum-values\">{{ enumColumn.values.join(', ') }}</span>\n </div>\n }\n </div>\n }\n @if (entity.computedFields.length) {\n <div><strong>Computed fields:</strong> {{ entity.computedFields.join(', ') }}</div>\n }\n\n <div class=\"functions-section\">\n <h3 class=\"functions-heading\">\n Code functions\n @if (entity.functionCount) {\n <span class=\"functions-count\">({{ entity.functionCount }})</span>\n }\n </h3>\n\n <div class=\"functions-body\" *fsSkeleton=\"entityFunctions(entity.key)\">\n @if (entityFunctions(entity.key); as fns) {\n @if (fns.functions.length) {\n @if (fns.generatedAt) {\n <div class=\"hint functions-generated\">\n Discovered from {{ fns.class }} &middot;\n <fs-date\n [date]=\"fns.generatedAt\"\n format=\"date-time\">\n </fs-date>\n </div>\n }\n <div class=\"functions-list\">\n @for (fn of fns.functions; track fn.method) {\n <div class=\"function-row\">\n <div class=\"function-head\">\n <fs-chip\n size=\"tiny\"\n [backgroundColor]=\"kindColor(fn.kind)\"\n color=\"#ffffff\">\n {{ fn.kind }}\n </fs-chip>\n <code class=\"function-method\">{{ fn.method }}</code>\n </div>\n <div class=\"function-summary\">{{ fn.summary }}</div>\n @if (fn.produces) {\n <div class=\"function-produces hint\">Produces: {{ fn.produces }}</div>\n }\n </div>\n }\n </div>\n } @else {\n <div class=\"hint\">\n No functions cached for this entity yet \u2014 run a build to discover its code.\n </div>\n }\n }\n </div>\n </div>\n </div>\n </ng-template>\n </mat-expansion-panel>\n }\n </mat-accordion>\n }\n }\n </div>\n</div>\n", styles: [".tab-body{padding-top:16px}.hint{margin:0 0 12px;color:#0009;font-size:13px;line-height:1.5}.build-status{display:flex;flex-direction:column;gap:8px}.build-status .stat{display:flex;gap:8px}.build-status .stat .label{min-width:110px;color:#0009}.entities{display:block;padding:3px}.entities .stage-chips{display:flex;flex-wrap:wrap;gap:4px}.entities .entity-detail{display:flex;flex-direction:column;gap:5px}.entities .entity-detail .enum-list{display:flex;flex-direction:column;gap:3px}.entities .entity-detail .enum-list .enum-row{display:flex;gap:10px;padding-left:10px}.entities .entity-detail .enum-list .enum-row .enum-column{min-width:140px;font-weight:600}.entities .entity-detail .enum-list .enum-row .enum-values{color:#0009}.entities .entity-detail .functions-section{margin-top:10px}.entities .entity-detail .functions-section .functions-heading{margin:0 0 5px;font-size:15px}.entities .entity-detail .functions-section .functions-heading .functions-count{font-weight:400;color:#0009}.entities .entity-detail .functions-section .functions-generated{margin-bottom:10px}.entities .entity-detail .functions-section .functions-list{display:flex;flex-direction:column}.entities .entity-detail .functions-section .functions-list .function-row{display:flex;flex-direction:column;gap:3px;padding:10px 0}.entities .entity-detail .functions-section .functions-list .function-row+.function-row{border-top:1px solid rgba(0,0,0,.08)}.entities .entity-detail .functions-section .functions-list .function-row .function-head{display:flex;align-items:center;gap:8px}.entities .entity-detail .functions-section .functions-list .function-row .function-method{font-family:monospace;font-size:13px;font-weight:600}.entities .entity-detail .functions-section .functions-list .function-row .function-summary{color:#000000bf}.entities .entity-detail .functions-section .functions-list .function-row .function-produces{padding-left:10px}\n"], dependencies: [{ kind: "ngmodule", type: MatExpansionModule }, { kind: "directive", type: i1$5.MatAccordion, selector: "mat-accordion", inputs: ["hideToggle", "displayMode", "togglePosition"], exportAs: ["matAccordion"] }, { kind: "component", type: i1$5.MatExpansionPanel, selector: "mat-expansion-panel", inputs: ["hideToggle", "togglePosition"], outputs: ["afterExpand", "afterCollapse"], exportAs: ["matExpansionPanel"] }, { kind: "component", type: i1$5.MatExpansionPanelHeader, selector: "mat-expansion-panel-header", inputs: ["expandedHeight", "collapsedHeight", "tabIndex"] }, { kind: "directive", type: i1$5.MatExpansionPanelTitle, selector: "mat-panel-title" }, { kind: "directive", type: i1$5.MatExpansionPanelDescription, selector: "mat-panel-description" }, { kind: "directive", type: i1$5.MatExpansionPanelContent, selector: "ng-template[matExpansionPanelContent]" }, { kind: "ngmodule", type: MatTooltipModule }, { kind: "directive", type: i2$5.MatTooltip, selector: "[matTooltip]", inputs: ["matTooltipPosition", "matTooltipPositionAtOrigin", "matTooltipDisabled", "matTooltipShowDelay", "matTooltipHideDelay", "matTooltipTouchGestures", "matTooltip", "matTooltipClass"], exportAs: ["matTooltip"] }, { kind: "ngmodule", type: FsChipModule }, { kind: "component", type: i1$4.FsChipComponent, selector: "fs-chip", inputs: ["selectable", "removable", "value", "maxWidth", "width", "backgroundColor", "borderColor", "color", "shape", "outlined", "outlineDash", "icon", "image", "selected", "disabled", "padding", "contrastColor", "size"], outputs: ["selectedToggled", "removed", "click"] }, { kind: "ngmodule", type: FsDateModule }, { kind: "component", type: i2$4.FsDateComponent, selector: "fs-date", inputs: ["date", "format", "timezone"] }, { kind: "ngmodule", type: FsMessageModule }, { kind: "component", type: i4.FsMessageWarningComponent, selector: "fs-message-warning" }, { kind: "ngmodule", type: FsSkeletonModule }, { kind: "directive", type: i6$1.FsSkeletonContentDirective, selector: "[fsSkeleton]", inputs: ["fsSkeleton", "fsSkeletonPattern"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
3646
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "18.2.14", type: SemanticDatabaseComponent, isStandalone: true, selector: "fs-ai-report-global-settings-semantic-database", ngImport: i0, template: "<div class=\"fs-column tab-body\">\n <p class=\"hint\">\n The semantic database is what the report AI uses to understand your data \u2014\n tables, relationships and the calculations in code. Rebuild it after\n significant application updates. The build runs in the background; progress\n appears in the dock at the bottom right and this dialog can be closed while\n it runs.\n </p>\n\n <div *fsSkeleton=\"status()\">\n @if (status(); as buildStatus) {\n <div class=\"build-status\">\n @if (buildStatus.detail.generatedAt) {\n <div class=\"stat\">\n <span class=\"label\">Last built</span>\n <fs-date\n [date]=\"buildStatus.detail.generatedAt\"\n format=\"date-time\">\n </fs-date>\n </div>\n <div class=\"stat\">\n <span class=\"label\">Coverage</span>\n <span>\n {{ buildStatus.detail.entityCount }} entities \u00B7\n {{ buildStatus.detail.functionCount }} code functions\n </span>\n </div>\n } @else {\n <fs-message-warning>\n The semantic database has not been built yet \u2014 the report AI can't\n answer data questions until it is. Run the first build.\n </fs-message-warning>\n }\n @if (buildStatus.build) {\n <div class=\"stat\">\n <span class=\"label\">Last build run</span>\n <span>\n {{ buildStateLabel() }}\n @if (buildStatus.build.message) {\n \u2014 {{ buildStatus.build.message }}\n }\n </span>\n </div>\n }\n </div>\n\n <!-- The entity scoreboard: every entity the AI knows about (an entity =\n a table + a model + a handler), its built-vs-needs-building stages,\n and a drill-down into its shape, enum values, computed fields and\n cached function counts \u2014 the counts link into the functions dialog. -->\n @if (buildStatus.detail.entities?.length) {\n <h2>Entities</h2>\n <mat-accordion class=\"entities\">\n @for (entity of buildStatus.detail.entities; track entity.key) {\n <mat-expansion-panel>\n <mat-expansion-panel-header>\n <mat-panel-title>{{ entity.key }}</mat-panel-title>\n <mat-panel-description>\n <div class=\"stage-chips\">\n @for (stage of stageKeys; track stage) {\n <fs-chip\n size=\"tiny\"\n [backgroundColor]=\"stageColor(entity.stages[stage])\"\n color=\"#ffffff\"\n [matTooltip]=\"stageTooltip(entity.stages[stage])\">\n {{ stageLabels[stage] }}\n </fs-chip>\n }\n </div>\n </mat-panel-description>\n </mat-expansion-panel-header>\n\n <!-- Deferred: the body only exists once expanded. -->\n <ng-template matExpansionPanelContent>\n <div class=\"entity-detail\">\n <div class=\"stat\">\n <span class=\"label\">Table</span>\n <span>{{ entity.table }}</span>\n </div>\n <div class=\"stat\">\n <span class=\"label\">Columns</span>\n <span>{{ entity.columns }}</span>\n </div>\n <div class=\"stat\">\n <span class=\"label\">Relationships</span>\n <span>{{ entity.relationships }}</span>\n </div>\n @if (entity.enums.length) {\n <div class=\"stat\">\n <span class=\"label\">Enumeration values</span>\n <div class=\"enum-list\">\n @for (enumColumn of entity.enums; track enumColumn.column) {\n <div class=\"enum-row\">\n <span class=\"enum-column\">{{ enumColumn.column }}</span>\n <span>{{ enumColumn.values.join(', ') }}</span>\n </div>\n }\n </div>\n </div>\n }\n @if (entity.computedFields.length) {\n <div class=\"stat\">\n <span class=\"label\">Computed fields</span>\n <span>{{ entity.computedFields.join(', ') }}</span>\n </div>\n }\n <div class=\"stat\">\n <span class=\"label\">Model functions</span>\n @if (entity.modelFunctionCount) {\n <a\n class=\"count-link\"\n (click)=\"openFunctions(entity, 'model')\">\n {{ entity.modelFunctionCount }}\n </a>\n } @else {\n <span>0</span>\n }\n </div>\n <div class=\"stat\">\n <span class=\"label\">Handler functions</span>\n @if (entity.handlerFunctionCount) {\n <a\n class=\"count-link\"\n (click)=\"openFunctions(entity, 'handler')\">\n {{ entity.handlerFunctionCount }}\n </a>\n } @else {\n <span>0</span>\n }\n </div>\n </div>\n </ng-template>\n </mat-expansion-panel>\n }\n </mat-accordion>\n }\n }\n </div>\n</div>\n", styles: [".hint{margin:0 0 12px;color:#0009;font-size:13px;line-height:1.5}.stat{display:flex;align-items:baseline;gap:12px}.stat>.label{flex:0 0 150px;white-space:nowrap;color:#0009}.build-status{display:flex;flex-direction:column;gap:8px}.entities{display:block;padding:3px}.entities mat-panel-description{justify-content:flex-end;margin-right:8px}.entities .stage-chips{display:flex;gap:4px}.entities .entity-detail{display:flex;flex-direction:column;gap:8px}.entities .entity-detail .enum-list{display:flex;flex-direction:column;gap:3px}.entities .entity-detail .enum-list .enum-row{display:flex;gap:12px}.entities .entity-detail .enum-list .enum-row .enum-column{flex:0 0 140px;white-space:nowrap;color:#0009}\n"], dependencies: [{ kind: "ngmodule", type: MatExpansionModule }, { kind: "directive", type: i1$5.MatAccordion, selector: "mat-accordion", inputs: ["hideToggle", "displayMode", "togglePosition"], exportAs: ["matAccordion"] }, { kind: "component", type: i1$5.MatExpansionPanel, selector: "mat-expansion-panel", inputs: ["hideToggle", "togglePosition"], outputs: ["afterExpand", "afterCollapse"], exportAs: ["matExpansionPanel"] }, { kind: "component", type: i1$5.MatExpansionPanelHeader, selector: "mat-expansion-panel-header", inputs: ["expandedHeight", "collapsedHeight", "tabIndex"] }, { kind: "directive", type: i1$5.MatExpansionPanelTitle, selector: "mat-panel-title" }, { kind: "directive", type: i1$5.MatExpansionPanelDescription, selector: "mat-panel-description" }, { kind: "directive", type: i1$5.MatExpansionPanelContent, selector: "ng-template[matExpansionPanelContent]" }, { kind: "ngmodule", type: MatTooltipModule }, { kind: "directive", type: i2$5.MatTooltip, selector: "[matTooltip]", inputs: ["matTooltipPosition", "matTooltipPositionAtOrigin", "matTooltipDisabled", "matTooltipShowDelay", "matTooltipHideDelay", "matTooltipTouchGestures", "matTooltip", "matTooltipClass"], exportAs: ["matTooltip"] }, { kind: "ngmodule", type: FsChipModule }, { kind: "component", type: i1$4.FsChipComponent, selector: "fs-chip", inputs: ["selectable", "removable", "value", "maxWidth", "width", "backgroundColor", "borderColor", "color", "shape", "outlined", "outlineDash", "icon", "image", "selected", "disabled", "padding", "contrastColor", "size"], outputs: ["selectedToggled", "removed", "click"] }, { kind: "ngmodule", type: FsDateModule }, { kind: "component", type: i2$4.FsDateComponent, selector: "fs-date", inputs: ["date", "format", "timezone"] }, { kind: "ngmodule", type: FsMessageModule }, { kind: "component", type: i4.FsMessageWarningComponent, selector: "fs-message-warning" }, { kind: "ngmodule", type: FsSkeletonModule }, { kind: "directive", type: i4$1.FsSkeletonContentDirective, selector: "[fsSkeleton]", inputs: ["fsSkeleton", "fsSkeletonPattern"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
3526
3647
  }
3527
3648
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: SemanticDatabaseComponent, decorators: [{
3528
3649
  type: Component,
@@ -3533,7 +3654,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
3533
3654
  FsDateModule,
3534
3655
  FsMessageModule,
3535
3656
  FsSkeletonModule,
3536
- ], template: "<div class=\"fs-column tab-body\">\n <p class=\"hint\">\n The semantic database is what the report AI uses to understand your data \u2014\n tables, relationships and the calculations in code. Rebuild it after\n significant application updates. The build runs in the background; progress\n appears in the dock at the bottom right and this dialog can be closed while\n it runs.\n </p>\n\n <div *fsSkeleton=\"status()\">\n @if (status(); as buildStatus) {\n <div class=\"build-status\">\n @if (buildStatus.detail.generatedAt) {\n <div class=\"stat\">\n <span class=\"label\">Last built</span>\n <fs-date\n [date]=\"buildStatus.detail.generatedAt\"\n format=\"date-time\">\n </fs-date>\n </div>\n <div class=\"stat\">\n <span class=\"label\">Coverage</span>\n <span>\n {{ buildStatus.detail.entityCount }} entities \u00B7\n {{ buildStatus.detail.functionCount }} code functions\n </span>\n </div>\n } @else {\n <fs-message-warning>\n The semantic database has not been built yet \u2014 the report AI can't\n answer data questions until it is. Run the first build.\n </fs-message-warning>\n }\n @if (buildStatus.build) {\n <div class=\"stat\">\n <span class=\"label\">Last build run</span>\n <span>\n {{ buildStateLabel() }}\n @if (buildStatus.build.message) {\n \u2014 {{ buildStatus.build.message }}\n }\n </span>\n </div>\n }\n </div>\n\n <!-- The model scoreboard: every entity the AI knows about, its\n built-vs-needs-building stages, and a drill-down into its shape, enum\n values, computed fields and AI-classified code functions. -->\n @if (buildStatus.detail.entities?.length) {\n <mat-accordion class=\"entities\">\n @for (entity of buildStatus.detail.entities; track entity.key) {\n <mat-expansion-panel (opened)=\"entityOpened(entity)\">\n <mat-expansion-panel-header>\n <mat-panel-title>{{ entity.key }}</mat-panel-title>\n <mat-panel-description>\n <div class=\"stage-chips\">\n @for (stage of stageKeys; track stage) {\n <fs-chip\n size=\"tiny\"\n [backgroundColor]=\"stageColor(entity.stages[stage])\"\n color=\"#ffffff\"\n [matTooltip]=\"stageTooltip(entity.stages[stage])\">\n {{ stageLabels[stage] }}\n </fs-chip>\n }\n </div>\n </mat-panel-description>\n </mat-expansion-panel-header>\n\n <!-- Deferred: the body (and its functions request) only exists\n once expanded. -->\n <ng-template matExpansionPanelContent>\n <div class=\"entity-detail\">\n <div><strong>Table:</strong> {{ entity.table }}</div>\n <div><strong>Columns:</strong> {{ entity.columns }}</div>\n <div><strong>Relationships:</strong> {{ entity.relationships }}</div>\n @if (entity.enums.length) {\n <div class=\"enum-list\">\n <strong>Enum values:</strong>\n @for (enumColumn of entity.enums; track enumColumn.column) {\n <div class=\"enum-row\">\n <span class=\"enum-column\">{{ enumColumn.column }}</span>\n <span class=\"enum-values\">{{ enumColumn.values.join(', ') }}</span>\n </div>\n }\n </div>\n }\n @if (entity.computedFields.length) {\n <div><strong>Computed fields:</strong> {{ entity.computedFields.join(', ') }}</div>\n }\n\n <div class=\"functions-section\">\n <h3 class=\"functions-heading\">\n Code functions\n @if (entity.functionCount) {\n <span class=\"functions-count\">({{ entity.functionCount }})</span>\n }\n </h3>\n\n <div class=\"functions-body\" *fsSkeleton=\"entityFunctions(entity.key)\">\n @if (entityFunctions(entity.key); as fns) {\n @if (fns.functions.length) {\n @if (fns.generatedAt) {\n <div class=\"hint functions-generated\">\n Discovered from {{ fns.class }} &middot;\n <fs-date\n [date]=\"fns.generatedAt\"\n format=\"date-time\">\n </fs-date>\n </div>\n }\n <div class=\"functions-list\">\n @for (fn of fns.functions; track fn.method) {\n <div class=\"function-row\">\n <div class=\"function-head\">\n <fs-chip\n size=\"tiny\"\n [backgroundColor]=\"kindColor(fn.kind)\"\n color=\"#ffffff\">\n {{ fn.kind }}\n </fs-chip>\n <code class=\"function-method\">{{ fn.method }}</code>\n </div>\n <div class=\"function-summary\">{{ fn.summary }}</div>\n @if (fn.produces) {\n <div class=\"function-produces hint\">Produces: {{ fn.produces }}</div>\n }\n </div>\n }\n </div>\n } @else {\n <div class=\"hint\">\n No functions cached for this entity yet \u2014 run a build to discover its code.\n </div>\n }\n }\n </div>\n </div>\n </div>\n </ng-template>\n </mat-expansion-panel>\n }\n </mat-accordion>\n }\n }\n </div>\n</div>\n", styles: [".tab-body{padding-top:16px}.hint{margin:0 0 12px;color:#0009;font-size:13px;line-height:1.5}.build-status{display:flex;flex-direction:column;gap:8px}.build-status .stat{display:flex;gap:8px}.build-status .stat .label{min-width:110px;color:#0009}.entities{display:block;padding:3px}.entities .stage-chips{display:flex;flex-wrap:wrap;gap:4px}.entities .entity-detail{display:flex;flex-direction:column;gap:5px}.entities .entity-detail .enum-list{display:flex;flex-direction:column;gap:3px}.entities .entity-detail .enum-list .enum-row{display:flex;gap:10px;padding-left:10px}.entities .entity-detail .enum-list .enum-row .enum-column{min-width:140px;font-weight:600}.entities .entity-detail .enum-list .enum-row .enum-values{color:#0009}.entities .entity-detail .functions-section{margin-top:10px}.entities .entity-detail .functions-section .functions-heading{margin:0 0 5px;font-size:15px}.entities .entity-detail .functions-section .functions-heading .functions-count{font-weight:400;color:#0009}.entities .entity-detail .functions-section .functions-generated{margin-bottom:10px}.entities .entity-detail .functions-section .functions-list{display:flex;flex-direction:column}.entities .entity-detail .functions-section .functions-list .function-row{display:flex;flex-direction:column;gap:3px;padding:10px 0}.entities .entity-detail .functions-section .functions-list .function-row+.function-row{border-top:1px solid rgba(0,0,0,.08)}.entities .entity-detail .functions-section .functions-list .function-row .function-head{display:flex;align-items:center;gap:8px}.entities .entity-detail .functions-section .functions-list .function-row .function-method{font-family:monospace;font-size:13px;font-weight:600}.entities .entity-detail .functions-section .functions-list .function-row .function-summary{color:#000000bf}.entities .entity-detail .functions-section .functions-list .function-row .function-produces{padding-left:10px}\n"] }]
3657
+ ], template: "<div class=\"fs-column tab-body\">\n <p class=\"hint\">\n The semantic database is what the report AI uses to understand your data \u2014\n tables, relationships and the calculations in code. Rebuild it after\n significant application updates. The build runs in the background; progress\n appears in the dock at the bottom right and this dialog can be closed while\n it runs.\n </p>\n\n <div *fsSkeleton=\"status()\">\n @if (status(); as buildStatus) {\n <div class=\"build-status\">\n @if (buildStatus.detail.generatedAt) {\n <div class=\"stat\">\n <span class=\"label\">Last built</span>\n <fs-date\n [date]=\"buildStatus.detail.generatedAt\"\n format=\"date-time\">\n </fs-date>\n </div>\n <div class=\"stat\">\n <span class=\"label\">Coverage</span>\n <span>\n {{ buildStatus.detail.entityCount }} entities \u00B7\n {{ buildStatus.detail.functionCount }} code functions\n </span>\n </div>\n } @else {\n <fs-message-warning>\n The semantic database has not been built yet \u2014 the report AI can't\n answer data questions until it is. Run the first build.\n </fs-message-warning>\n }\n @if (buildStatus.build) {\n <div class=\"stat\">\n <span class=\"label\">Last build run</span>\n <span>\n {{ buildStateLabel() }}\n @if (buildStatus.build.message) {\n \u2014 {{ buildStatus.build.message }}\n }\n </span>\n </div>\n }\n </div>\n\n <!-- The entity scoreboard: every entity the AI knows about (an entity =\n a table + a model + a handler), its built-vs-needs-building stages,\n and a drill-down into its shape, enum values, computed fields and\n cached function counts \u2014 the counts link into the functions dialog. -->\n @if (buildStatus.detail.entities?.length) {\n <h2>Entities</h2>\n <mat-accordion class=\"entities\">\n @for (entity of buildStatus.detail.entities; track entity.key) {\n <mat-expansion-panel>\n <mat-expansion-panel-header>\n <mat-panel-title>{{ entity.key }}</mat-panel-title>\n <mat-panel-description>\n <div class=\"stage-chips\">\n @for (stage of stageKeys; track stage) {\n <fs-chip\n size=\"tiny\"\n [backgroundColor]=\"stageColor(entity.stages[stage])\"\n color=\"#ffffff\"\n [matTooltip]=\"stageTooltip(entity.stages[stage])\">\n {{ stageLabels[stage] }}\n </fs-chip>\n }\n </div>\n </mat-panel-description>\n </mat-expansion-panel-header>\n\n <!-- Deferred: the body only exists once expanded. -->\n <ng-template matExpansionPanelContent>\n <div class=\"entity-detail\">\n <div class=\"stat\">\n <span class=\"label\">Table</span>\n <span>{{ entity.table }}</span>\n </div>\n <div class=\"stat\">\n <span class=\"label\">Columns</span>\n <span>{{ entity.columns }}</span>\n </div>\n <div class=\"stat\">\n <span class=\"label\">Relationships</span>\n <span>{{ entity.relationships }}</span>\n </div>\n @if (entity.enums.length) {\n <div class=\"stat\">\n <span class=\"label\">Enumeration values</span>\n <div class=\"enum-list\">\n @for (enumColumn of entity.enums; track enumColumn.column) {\n <div class=\"enum-row\">\n <span class=\"enum-column\">{{ enumColumn.column }}</span>\n <span>{{ enumColumn.values.join(', ') }}</span>\n </div>\n }\n </div>\n </div>\n }\n @if (entity.computedFields.length) {\n <div class=\"stat\">\n <span class=\"label\">Computed fields</span>\n <span>{{ entity.computedFields.join(', ') }}</span>\n </div>\n }\n <div class=\"stat\">\n <span class=\"label\">Model functions</span>\n @if (entity.modelFunctionCount) {\n <a\n class=\"count-link\"\n (click)=\"openFunctions(entity, 'model')\">\n {{ entity.modelFunctionCount }}\n </a>\n } @else {\n <span>0</span>\n }\n </div>\n <div class=\"stat\">\n <span class=\"label\">Handler functions</span>\n @if (entity.handlerFunctionCount) {\n <a\n class=\"count-link\"\n (click)=\"openFunctions(entity, 'handler')\">\n {{ entity.handlerFunctionCount }}\n </a>\n } @else {\n <span>0</span>\n }\n </div>\n </div>\n </ng-template>\n </mat-expansion-panel>\n }\n </mat-accordion>\n }\n }\n </div>\n</div>\n", styles: [".hint{margin:0 0 12px;color:#0009;font-size:13px;line-height:1.5}.stat{display:flex;align-items:baseline;gap:12px}.stat>.label{flex:0 0 150px;white-space:nowrap;color:#0009}.build-status{display:flex;flex-direction:column;gap:8px}.entities{display:block;padding:3px}.entities mat-panel-description{justify-content:flex-end;margin-right:8px}.entities .stage-chips{display:flex;gap:4px}.entities .entity-detail{display:flex;flex-direction:column;gap:8px}.entities .entity-detail .enum-list{display:flex;flex-direction:column;gap:3px}.entities .entity-detail .enum-list .enum-row{display:flex;gap:12px}.entities .entity-detail .enum-list .enum-row .enum-column{flex:0 0 140px;white-space:nowrap;color:#0009}\n"] }]
3537
3658
  }], ctorParameters: () => [] });
3538
3659
 
3539
3660
  // Matches the backend cap (ReportsView::INSTRUCTIONS_MAX_LENGTH) — the document
@@ -3559,12 +3680,14 @@ const SCAFFOLD = [
3559
3680
  // the dialog frame, the tab group, and a per-tab footer. Each tab is its own
3560
3681
  // component —
3561
3682
  // AI Knowledge — the operator-authored markdown document appended to the
3562
- // report agent's system instructions on every chat turn.
3563
- // History — every saved version (append-only), any of which can be restored.
3683
+ // report agent's system instructions on every chat turn. Its footer carries a
3684
+ // View History button opening the version-history dialog (restores happen
3685
+ // there; the shell refreshes on close).
3564
3686
  // Semantic Database — the build scoreboard the AI is built from, with a
3565
3687
  // self-contained Build action.
3566
- // The shell owns only the instructions document shared by the first two tabs
3567
- // (the editor draft, save and restore); everything else lives in the children.
3688
+ // General — report-module settings (the semantic-build concurrency).
3689
+ // The shell owns the shared data (the instructions document, the settings for
3690
+ // General) plus the tab-aware save; everything else lives in the children.
3568
3691
  class GlobalSettingsComponent {
3569
3692
  selectedTab = 'knowledge';
3570
3693
  // The Semantic Database tab component, so the shared footer can drive its
@@ -3577,48 +3700,80 @@ class GlobalSettingsComponent {
3577
3700
  maxLength: MAX_LENGTH,
3578
3701
  placeholder: 'Terminology, business rules and special cases the report AI should know…',
3579
3702
  };
3580
- // null until loaded — gates the form behind the skeleton.
3703
+ // Both null until loaded — the form waits behind the skeleton until each is in.
3581
3704
  instructions = signal(null);
3705
+ settings = signal(null);
3706
+ // The General tab's concurrency draft (bound to its field), seeded from settings.
3707
+ concurrency = signal(0);
3708
+ // Gate the whole form on both loads, so every tab's fields are ready to render.
3709
+ loaded = computed(() => !!this.instructions() && !!this.settings());
3582
3710
  _dialogRef = inject(MatDialogRef);
3711
+ _dialog = inject(MatDialog);
3583
3712
  _reportData = inject(ReportData);
3584
3713
  _message = inject(FsMessage);
3585
- _prompt = inject(FsPrompt);
3586
3714
  _destroyRef = inject(DestroyRef);
3587
3715
  constructor() {
3588
3716
  this._init();
3589
3717
  }
3590
- // Save the draft as a new version (the backend skips an unchanged save).
3718
+ // The footer Save is shared, so it saves whatever the current tab owns: the
3719
+ // General tab persists its settings (and stays open); AI Knowledge saves a new
3720
+ // document version and closes.
3591
3721
  save = () => {
3722
+ if (this.selectedTab === 'general') {
3723
+ return this._reportData.saveSettings({ semanticBuildConcurrency: this.concurrency() })
3724
+ .pipe(tap((settings) => {
3725
+ this._applySettings(settings);
3726
+ this._message.success('Settings saved');
3727
+ }));
3728
+ }
3592
3729
  return this._reportData.saveInstructions(this.content())
3593
3730
  .pipe(tap((instructions) => {
3594
3731
  this._message.success('AI knowledge saved');
3595
3732
  this._dialogRef.close(instructions);
3596
3733
  }));
3597
3734
  };
3598
- // Make an earlier version current — flips the flag, creates no new version.
3599
- restore(version) {
3600
- this._prompt.confirm({
3601
- title: 'Restore version',
3602
- template: 'Make this version the current AI knowledge? The newer versions stay in the history.',
3603
- commitLabel: 'Restore',
3735
+ // Open the version-history dialog. Restores happen inside it, so when it
3736
+ // closes (however it closes) re-fetch the document: a restore changed the
3737
+ // current version and must reseed the editor; otherwise just refresh the
3738
+ // version list without touching an unsaved draft.
3739
+ openHistory() {
3740
+ const currentId = this.instructions()?.instructions?.id;
3741
+ this._dialog.open(InstructionHistoryComponent, {
3742
+ width: '800px',
3743
+ maxWidth: '95vw',
3744
+ maxHeight: '85vh',
3745
+ autoFocus: false,
3746
+ data: { versions: this.instructions()?.versions ?? [] },
3604
3747
  })
3605
- .pipe(switchMap(() => this._reportData.restoreInstruction(version.id)), takeUntilDestroyed(this._destroyRef))
3748
+ .afterClosed()
3749
+ .pipe(switchMap(() => this._reportData.instructions()), takeUntilDestroyed(this._destroyRef))
3606
3750
  .subscribe((instructions) => {
3607
- this._applyInstructions(instructions);
3608
- this._message.success('Version restored');
3751
+ if (instructions.instructions?.id !== currentId) {
3752
+ this._applyInstructions(instructions);
3753
+ }
3754
+ else {
3755
+ this.instructions.set(instructions);
3756
+ }
3609
3757
  });
3610
3758
  }
3611
3759
  _init() {
3612
3760
  this._reportData.instructions()
3613
3761
  .pipe(takeUntilDestroyed(this._destroyRef))
3614
3762
  .subscribe((instructions) => this._applyInstructions(instructions));
3763
+ this._reportData.settings()
3764
+ .pipe(takeUntilDestroyed(this._destroyRef))
3765
+ .subscribe((settings) => this._applySettings(settings));
3615
3766
  }
3616
3767
  _applyInstructions(instructions) {
3617
3768
  this.instructions.set(instructions);
3618
3769
  this.content.set(instructions.instructions?.content ?? SCAFFOLD);
3619
3770
  }
3771
+ _applySettings(settings) {
3772
+ this.settings.set(settings);
3773
+ this.concurrency.set(settings.semanticBuildConcurrency);
3774
+ }
3620
3775
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: GlobalSettingsComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
3621
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "18.2.14", type: GlobalSettingsComponent, isStandalone: true, selector: "ng-component", viewQueries: [{ propertyName: "semanticDatabase", first: true, predicate: SemanticDatabaseComponent, descendants: true, isSignal: true }], ngImport: i0, template: "<form\n *fsSkeletonForm=\"instructions()\"\n fsForm\n [submit]=\"save\">\n <fs-dialog>\n <fs-dialog-title>\n Report Settings\n </fs-dialog-title>\n <mat-dialog-content>\n <mat-tab-group [(selected)]=\"selectedTab\">\n <mat-tab\n label=\"AI Knowledge\"\n name=\"knowledge\">\n <fs-ai-report-global-settings-ai-knowledge\n [content]=\"content()\"\n (contentChange)=\"content.set($event)\"\n [config]=\"editorConfig\">\n </fs-ai-report-global-settings-ai-knowledge>\n </mat-tab>\n <mat-tab\n label=\"History\"\n name=\"history\">\n <fs-ai-report-global-settings-history\n [versions]=\"instructions()?.versions ?? []\"\n (restore)=\"restore($event)\">\n </fs-ai-report-global-settings-history>\n </mat-tab>\n <mat-tab\n label=\"Semantic Database\"\n name=\"build\">\n <fs-ai-report-global-settings-semantic-database>\n </fs-ai-report-global-settings-semantic-database>\n </mat-tab>\n </mat-tab-group>\n </mat-dialog-content>\n <mat-dialog-actions>\n <!-- The footer follows the selected tab. AI Knowledge is the only editable\n tab, so it gets Save/Cancel; History and Semantic Database are\n read/run surfaces, so they get a primary Close. The Semantic Database\n tab also carries its Build/Rebuild/Stop action to the right of Close. -->\n <fs-form-dialog-actions [save]=\"selectedTab === 'knowledge'\">\n @if (selectedTab !== 'knowledge') {\n <button\n type=\"button\"\n mat-button\n color=\"primary\"\n [mat-dialog-close]=\"undefined\">\n Close\n </button>\n }\n @if (selectedTab === 'build' && semanticDatabase(); as db) {\n @if (db.buildActive()) {\n <button\n type=\"button\"\n mat-button\n color=\"warn\"\n (click)=\"db.stopBuild()\">\n Stop Build\n </button>\n } @else {\n @if (db.status(); as buildStatus) {\n <button\n type=\"button\"\n mat-button\n (click)=\"db.build()\">\n {{ buildStatus.detail.generatedAt ? 'Rebuild' : 'Build' }}\n </button>\n }\n }\n }\n </fs-form-dialog-actions>\n </mat-dialog-actions>\n </fs-dialog>\n</form>\n", styles: [":host{display:block}\n"], dependencies: [{ kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1$3.ɵNgNoValidate, selector: "form:not([ngNoForm]):not([ngNativeValidate])" }, { kind: "directive", type: i1$3.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]" }, { kind: "directive", type: i1$3.NgForm, selector: "form:not([ngNoForm]):not([formGroup]),ng-form,[ngForm]", inputs: ["ngFormOptions"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }, { kind: "ngmodule", type: FsDialogModule }, { kind: "component", type: i3.FsDialogComponent, selector: "fs-dialog", inputs: ["mobileMode", "mobileButtonPlacement", "mobileWidth", "mode", "buttonLayout"] }, { kind: "component", type: i3.FsDialogTitleComponent, selector: "fs-dialog-title", inputs: ["close", "dockToggle", "back"] }, { kind: "ngmodule", type: FsFormModule }, { kind: "directive", type: i2$1.FsFormDirective, selector: "[fsForm]", inputs: ["wrapperSelector", "messageSelector", "hintSelector", "labelSelector", "autocomplete", "shortcuts", "confirm", "confirmDialog", "confirmDrawer", "confirmBrowser", "dirtySubmitButton", "submit", "successDelay", "errorDelay", "deactivationGuard"], outputs: ["fsForm", "invalid", "valid", "submitted", "reseted", "cleared"], exportAs: ["fsForm"] }, { kind: "component", type: i2$1.FsFormDialogActionsComponent, selector: "fs-form-dialog-actions", inputs: ["save", "create", "close", "done", "closeData", "name"] }, { kind: "directive", type: i2$1.FsButtonDirective, selector: "[mat-raised-button],[mat-button],[mat-flat-button],[mat-stroked-button]", inputs: ["name", "dirtySubmit"] }, { kind: "ngmodule", type: FsSkeletonModule }, { kind: "directive", type: i6$1.FsSkeletonFormDirective, selector: "[fsSkeletonForm]", inputs: ["fsSkeletonForm", "fsSkeletonFormLines"] }, { kind: "ngmodule", type: FsTabsModule }, { kind: "directive", type: i5.FsTabsHeaderTabGroupDirective, selector: "mat-tab-group, matTabGroup, [matTabGroup]", inputs: ["orientation", "selected", "selectedData"], outputs: ["selectedChange", "selectedDataChange"], exportAs: ["fsTabsHeaderTabGroup"] }, { kind: "directive", type: i5.FsTabsTabDirective, selector: "mat-tab,matTab", inputs: ["name", "data"], exportAs: ["fsTabsTab"] }, { kind: "component", type: MatButton, selector: " button[mat-button], button[mat-raised-button], button[mat-flat-button], button[mat-stroked-button] ", exportAs: ["matButton"] }, { kind: "directive", type: MatDialogActions, selector: "[mat-dialog-actions], mat-dialog-actions, [matDialogActions]", inputs: ["align"] }, { kind: "directive", type: MatDialogClose, selector: "[mat-dialog-close], [matDialogClose]", inputs: ["aria-label", "type", "mat-dialog-close", "matDialogClose"], exportAs: ["matDialogClose"] }, { kind: "directive", type: MatDialogContent, selector: "[mat-dialog-content], mat-dialog-content, [matDialogContent]" }, { kind: "ngmodule", type: MatTabsModule }, { kind: "component", type: i5$1.MatTab, selector: "mat-tab", inputs: ["disabled", "label", "aria-label", "aria-labelledby", "labelClass", "bodyClass"], exportAs: ["matTab"] }, { kind: "component", type: i5$1.MatTabGroup, selector: "mat-tab-group", inputs: ["color", "fitInkBarToContent", "mat-stretch-tabs", "dynamicHeight", "selectedIndex", "headerPosition", "animationDuration", "contentTabIndex", "disablePagination", "disableRipple", "preserveContent", "backgroundColor", "aria-label", "aria-labelledby"], outputs: ["selectedIndexChange", "focusChange", "animationDone", "selectedTabChange"], exportAs: ["matTabGroup"] }, { kind: "component", type: AiKnowledgeComponent, selector: "fs-ai-report-global-settings-ai-knowledge", inputs: ["content", "config"], outputs: ["contentChange"] }, { kind: "component", type: HistoryComponent, selector: "fs-ai-report-global-settings-history", inputs: ["versions"], outputs: ["restore"] }, { kind: "component", type: SemanticDatabaseComponent, selector: "fs-ai-report-global-settings-semantic-database" }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
3776
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "18.2.14", type: GlobalSettingsComponent, isStandalone: true, selector: "ng-component", viewQueries: [{ propertyName: "semanticDatabase", first: true, predicate: SemanticDatabaseComponent, descendants: true, isSignal: true }], ngImport: i0, template: "<form\n *fsSkeletonForm=\"loaded()\"\n fsForm\n [submit]=\"save\">\n <fs-dialog>\n <fs-dialog-title>\n Report Settings\n </fs-dialog-title>\n <mat-dialog-content>\n <mat-tab-group [(selected)]=\"selectedTab\">\n <mat-tab\n label=\"AI Knowledge\"\n name=\"knowledge\">\n <fs-ai-report-global-settings-ai-knowledge\n [content]=\"content()\"\n (contentChange)=\"content.set($event)\"\n [config]=\"editorConfig\">\n </fs-ai-report-global-settings-ai-knowledge>\n </mat-tab>\n <mat-tab\n label=\"Semantic Database\"\n name=\"build\">\n <fs-ai-report-global-settings-semantic-database>\n </fs-ai-report-global-settings-semantic-database>\n </mat-tab>\n <mat-tab\n label=\"General\"\n name=\"general\">\n <fs-ai-report-global-settings-general\n [concurrency]=\"concurrency()\"\n (concurrencyChange)=\"concurrency.set($event)\"\n [min]=\"settings()!.semanticBuildConcurrencyMin\"\n [max]=\"settings()!.semanticBuildConcurrencyMax\">\n </fs-ai-report-global-settings-general>\n </mat-tab>\n </mat-tab-group>\n </mat-dialog-content>\n <mat-dialog-actions>\n <!-- The footer follows the selected tab. AI Knowledge projects its own\n Save | View History | Cancel row (fs-form-dialog-actions renders\n projected content after Save/Cancel, so the default pair can't keep\n View History beside Save); General uses the default Save/Cancel; the\n Semantic Database tab is a read/run surface, so it gets a primary\n Close plus its Build/Rebuild/Stop action. -->\n <fs-form-dialog-actions [save]=\"selectedTab === 'general'\">\n @if (selectedTab === 'knowledge') {\n <button\n type=\"submit\"\n mat-button\n color=\"primary\">\n Save\n </button>\n <button\n type=\"button\"\n mat-button\n (click)=\"openHistory()\">\n View History\n </button>\n <button\n type=\"button\"\n mat-button\n [mat-dialog-close]=\"null\">\n Cancel\n </button>\n }\n @if (selectedTab === 'build') {\n <button\n type=\"button\"\n mat-button\n color=\"primary\"\n [mat-dialog-close]=\"undefined\">\n Close\n </button>\n }\n @if (selectedTab === 'build' && semanticDatabase(); as db) {\n @if (db.buildActive()) {\n <button\n type=\"button\"\n mat-button\n color=\"warn\"\n (click)=\"db.stopBuild()\">\n Stop Build\n </button>\n } @else {\n @if (db.status(); as buildStatus) {\n <button\n type=\"button\"\n mat-button\n (click)=\"db.build()\">\n {{ buildStatus.detail.generatedAt ? 'Rebuild' : 'Build' }}\n </button>\n }\n }\n }\n </fs-form-dialog-actions>\n </mat-dialog-actions>\n </fs-dialog>\n</form>\n", styles: [":host{display:block}\n"], dependencies: [{ kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1$3.ɵNgNoValidate, selector: "form:not([ngNoForm]):not([ngNativeValidate])" }, { kind: "directive", type: i1$3.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]" }, { kind: "directive", type: i1$3.NgForm, selector: "form:not([ngNoForm]):not([formGroup]),ng-form,[ngForm]", inputs: ["ngFormOptions"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }, { kind: "ngmodule", type: FsDialogModule }, { kind: "component", type: i3.FsDialogComponent, selector: "fs-dialog", inputs: ["mobileMode", "mobileButtonPlacement", "mobileWidth", "mode", "buttonLayout"] }, { kind: "component", type: i3.FsDialogTitleComponent, selector: "fs-dialog-title", inputs: ["close", "dockToggle", "back"] }, { kind: "ngmodule", type: FsFormModule }, { kind: "directive", type: i2$1.FsFormDirective, selector: "[fsForm]", inputs: ["wrapperSelector", "messageSelector", "hintSelector", "labelSelector", "autocomplete", "shortcuts", "confirm", "confirmDialog", "confirmDrawer", "confirmBrowser", "dirtySubmitButton", "submit", "successDelay", "errorDelay", "deactivationGuard"], outputs: ["fsForm", "invalid", "valid", "submitted", "reseted", "cleared"], exportAs: ["fsForm"] }, { kind: "component", type: i2$1.FsFormDialogActionsComponent, selector: "fs-form-dialog-actions", inputs: ["save", "create", "close", "done", "closeData", "name"] }, { kind: "directive", type: i2$1.FsButtonDirective, selector: "[mat-raised-button],[mat-button],[mat-flat-button],[mat-stroked-button]", inputs: ["name", "dirtySubmit"] }, { kind: "ngmodule", type: FsSkeletonModule }, { kind: "directive", type: i4$1.FsSkeletonFormDirective, selector: "[fsSkeletonForm]", inputs: ["fsSkeletonForm", "fsSkeletonFormLines"] }, { kind: "ngmodule", type: FsTabsModule }, { kind: "directive", type: i5.FsTabsHeaderTabGroupDirective, selector: "mat-tab-group, matTabGroup, [matTabGroup]", inputs: ["orientation", "selected", "selectedData"], outputs: ["selectedChange", "selectedDataChange"], exportAs: ["fsTabsHeaderTabGroup"] }, { kind: "directive", type: i5.FsTabsTabDirective, selector: "mat-tab,matTab", inputs: ["name", "data"], exportAs: ["fsTabsTab"] }, { kind: "component", type: MatButton, selector: " button[mat-button], button[mat-raised-button], button[mat-flat-button], button[mat-stroked-button] ", exportAs: ["matButton"] }, { kind: "directive", type: MatDialogActions, selector: "[mat-dialog-actions], mat-dialog-actions, [matDialogActions]", inputs: ["align"] }, { kind: "directive", type: MatDialogClose, selector: "[mat-dialog-close], [matDialogClose]", inputs: ["aria-label", "type", "mat-dialog-close", "matDialogClose"], exportAs: ["matDialogClose"] }, { kind: "directive", type: MatDialogContent, selector: "[mat-dialog-content], mat-dialog-content, [matDialogContent]" }, { kind: "ngmodule", type: MatTabsModule }, { kind: "component", type: i5$1.MatTab, selector: "mat-tab", inputs: ["disabled", "label", "aria-label", "aria-labelledby", "labelClass", "bodyClass"], exportAs: ["matTab"] }, { kind: "component", type: i5$1.MatTabGroup, selector: "mat-tab-group", inputs: ["color", "fitInkBarToContent", "mat-stretch-tabs", "dynamicHeight", "selectedIndex", "headerPosition", "animationDuration", "contentTabIndex", "disablePagination", "disableRipple", "preserveContent", "backgroundColor", "aria-label", "aria-labelledby"], outputs: ["selectedIndexChange", "focusChange", "animationDone", "selectedTabChange"], exportAs: ["matTabGroup"] }, { kind: "component", type: AiKnowledgeComponent, selector: "fs-ai-report-global-settings-ai-knowledge", inputs: ["content", "config"], outputs: ["contentChange"] }, { kind: "component", type: GeneralComponent, selector: "fs-ai-report-global-settings-general", inputs: ["concurrency", "min", "max"], outputs: ["concurrencyChange"] }, { kind: "component", type: SemanticDatabaseComponent, selector: "fs-ai-report-global-settings-semantic-database" }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
3622
3777
  }
3623
3778
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: GlobalSettingsComponent, decorators: [{
3624
3779
  type: Component,
@@ -3634,9 +3789,9 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
3634
3789
  MatDialogContent,
3635
3790
  MatTabsModule,
3636
3791
  AiKnowledgeComponent,
3637
- HistoryComponent,
3792
+ GeneralComponent,
3638
3793
  SemanticDatabaseComponent,
3639
- ], template: "<form\n *fsSkeletonForm=\"instructions()\"\n fsForm\n [submit]=\"save\">\n <fs-dialog>\n <fs-dialog-title>\n Report Settings\n </fs-dialog-title>\n <mat-dialog-content>\n <mat-tab-group [(selected)]=\"selectedTab\">\n <mat-tab\n label=\"AI Knowledge\"\n name=\"knowledge\">\n <fs-ai-report-global-settings-ai-knowledge\n [content]=\"content()\"\n (contentChange)=\"content.set($event)\"\n [config]=\"editorConfig\">\n </fs-ai-report-global-settings-ai-knowledge>\n </mat-tab>\n <mat-tab\n label=\"History\"\n name=\"history\">\n <fs-ai-report-global-settings-history\n [versions]=\"instructions()?.versions ?? []\"\n (restore)=\"restore($event)\">\n </fs-ai-report-global-settings-history>\n </mat-tab>\n <mat-tab\n label=\"Semantic Database\"\n name=\"build\">\n <fs-ai-report-global-settings-semantic-database>\n </fs-ai-report-global-settings-semantic-database>\n </mat-tab>\n </mat-tab-group>\n </mat-dialog-content>\n <mat-dialog-actions>\n <!-- The footer follows the selected tab. AI Knowledge is the only editable\n tab, so it gets Save/Cancel; History and Semantic Database are\n read/run surfaces, so they get a primary Close. The Semantic Database\n tab also carries its Build/Rebuild/Stop action to the right of Close. -->\n <fs-form-dialog-actions [save]=\"selectedTab === 'knowledge'\">\n @if (selectedTab !== 'knowledge') {\n <button\n type=\"button\"\n mat-button\n color=\"primary\"\n [mat-dialog-close]=\"undefined\">\n Close\n </button>\n }\n @if (selectedTab === 'build' && semanticDatabase(); as db) {\n @if (db.buildActive()) {\n <button\n type=\"button\"\n mat-button\n color=\"warn\"\n (click)=\"db.stopBuild()\">\n Stop Build\n </button>\n } @else {\n @if (db.status(); as buildStatus) {\n <button\n type=\"button\"\n mat-button\n (click)=\"db.build()\">\n {{ buildStatus.detail.generatedAt ? 'Rebuild' : 'Build' }}\n </button>\n }\n }\n }\n </fs-form-dialog-actions>\n </mat-dialog-actions>\n </fs-dialog>\n</form>\n", styles: [":host{display:block}\n"] }]
3794
+ ], template: "<form\n *fsSkeletonForm=\"loaded()\"\n fsForm\n [submit]=\"save\">\n <fs-dialog>\n <fs-dialog-title>\n Report Settings\n </fs-dialog-title>\n <mat-dialog-content>\n <mat-tab-group [(selected)]=\"selectedTab\">\n <mat-tab\n label=\"AI Knowledge\"\n name=\"knowledge\">\n <fs-ai-report-global-settings-ai-knowledge\n [content]=\"content()\"\n (contentChange)=\"content.set($event)\"\n [config]=\"editorConfig\">\n </fs-ai-report-global-settings-ai-knowledge>\n </mat-tab>\n <mat-tab\n label=\"Semantic Database\"\n name=\"build\">\n <fs-ai-report-global-settings-semantic-database>\n </fs-ai-report-global-settings-semantic-database>\n </mat-tab>\n <mat-tab\n label=\"General\"\n name=\"general\">\n <fs-ai-report-global-settings-general\n [concurrency]=\"concurrency()\"\n (concurrencyChange)=\"concurrency.set($event)\"\n [min]=\"settings()!.semanticBuildConcurrencyMin\"\n [max]=\"settings()!.semanticBuildConcurrencyMax\">\n </fs-ai-report-global-settings-general>\n </mat-tab>\n </mat-tab-group>\n </mat-dialog-content>\n <mat-dialog-actions>\n <!-- The footer follows the selected tab. AI Knowledge projects its own\n Save | View History | Cancel row (fs-form-dialog-actions renders\n projected content after Save/Cancel, so the default pair can't keep\n View History beside Save); General uses the default Save/Cancel; the\n Semantic Database tab is a read/run surface, so it gets a primary\n Close plus its Build/Rebuild/Stop action. -->\n <fs-form-dialog-actions [save]=\"selectedTab === 'general'\">\n @if (selectedTab === 'knowledge') {\n <button\n type=\"submit\"\n mat-button\n color=\"primary\">\n Save\n </button>\n <button\n type=\"button\"\n mat-button\n (click)=\"openHistory()\">\n View History\n </button>\n <button\n type=\"button\"\n mat-button\n [mat-dialog-close]=\"null\">\n Cancel\n </button>\n }\n @if (selectedTab === 'build') {\n <button\n type=\"button\"\n mat-button\n color=\"primary\"\n [mat-dialog-close]=\"undefined\">\n Close\n </button>\n }\n @if (selectedTab === 'build' && semanticDatabase(); as db) {\n @if (db.buildActive()) {\n <button\n type=\"button\"\n mat-button\n color=\"warn\"\n (click)=\"db.stopBuild()\">\n Stop Build\n </button>\n } @else {\n @if (db.status(); as buildStatus) {\n <button\n type=\"button\"\n mat-button\n (click)=\"db.build()\">\n {{ buildStatus.detail.generatedAt ? 'Rebuild' : 'Build' }}\n </button>\n }\n }\n }\n </fs-form-dialog-actions>\n </mat-dialog-actions>\n </fs-dialog>\n</form>\n", styles: [":host{display:block}\n"] }]
3640
3795
  }], ctorParameters: () => [] });
3641
3796
 
3642
3797
  // The reports listing — an fs-list of reports. Rows are links built from a URL
@@ -4002,8 +4157,8 @@ class FsAiReportComponent {
4002
4157
  ReportPptxService,
4003
4158
  ReportPdfService,
4004
4159
  // Tree-shaken ECharts core + the 'report' house theme, loaded lazily.
4005
- provideEchartsCore({ echarts: () => import('./firestitch-report-echarts-Cel1iHUg.mjs').then((module) => module.default) }),
4006
- ], viewQueries: [{ propertyName: "_split", first: true, predicate: ["split"], descendants: true, static: true }, { propertyName: "_chatPanel", first: true, predicate: ["chatPanel"], descendants: true, read: ElementRef, static: true }], ngImport: i0, template: "<div class=\"fs-ai-report fs-column\">\n\n <div class=\"fs-ai-report__header fs-row.align-center\">\n <h2 class=\"fs-ai-report__title fs-flex\">{{ report?.name }}</h2>\n\n @if (report) {\n <!-- Report-level filters, inline with the toolbar actions. fs-filter\n reads its config once at init, so it's keyed on the filter\n signature: when the report-level filter set changes the block is\n recreated and re-reads the rebuilt config. -->\n @if (reportHasFilters) {\n @for (key of [reportFilterKey]; track key) {\n <fs-filter\n class=\"fs-ai-report__filters\"\n [config]=\"reportFilterConfig\">\n </fs-filter>\n }\n }\n\n <button\n mat-icon-button\n type=\"button\"\n [matTooltip]=\"editMode ? 'Done' : 'Edit layout'\"\n (click)=\"toggleEditMode()\">\n <mat-icon>{{ editMode ? 'lock' : 'open_with' }}</mat-icon>\n </button>\n\n <button\n mat-icon-button\n type=\"button\"\n matTooltip=\"Export\"\n [fsMenuTriggerFor]=\"exportMenu\">\n <mat-icon>download</mat-icon>\n </button>\n <fs-menu #exportMenu>\n <ng-template\n fs-menu-item\n (click)=\"exportPowerpoint()\">\n Export PowerPoint\n </ng-template>\n <ng-template\n fs-menu-item\n (click)=\"exportPdf()\">\n Export PDF\n </ng-template>\n </fs-menu>\n\n <button\n mat-icon-button\n type=\"button\"\n matTooltip=\"Report settings\"\n (click)=\"reportSettings()\">\n <mat-icon>settings</mat-icon>\n </button>\n }\n </div>\n\n <div\n #split\n class=\"fs-ai-report__split fs-row.align-start\">\n <fs-ai-chat\n #chatPanel\n class=\"chat\"\n [basePath]=\"basePath()\"\n [requestData]=\"{ reportId: reportId() }\"\n (response)=\"onChatResponse($event)\">\n </fs-ai-chat>\n\n <div\n class=\"resizer\"\n (pointerdown)=\"onResizeStart($event)\">\n </div>\n\n <div class=\"viewer fs-flex fs-column\">\n @if (report) {\n <app-report-canvas\n [report]=\"report\"\n [editMode]=\"editMode\"\n (componentSettings)=\"componentSettings($event)\"\n (reportChanged)=\"onCanvasReportChanged()\"\n (editDone)=\"toggleEditMode()\">\n </app-report-canvas>\n }\n </div>\n </div>\n</div>\n", styles: [".fs-ai-report{height:100%;min-height:500px;display:flex;flex-direction:column}.fs-ai-report__header{flex:0 0 auto;margin-bottom:10px}.fs-ai-report__title{margin:0;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.fs-ai-report__filters{flex:0 0 auto;display:block;margin:0 5px 0 0}.fs-ai-report__filters ::ng-deep .filter-bar-container{align-items:center!important}.fs-ai-report__filters ::ng-deep .mat-mdc-form-field-subscript-wrapper{display:none}.fs-ai-report__split{flex:1 1 auto;min-height:0}.fs-ai-report .chat{display:block;flex:0 0 25%;width:100%;height:100%;min-height:400px;min-width:0;border:none}.fs-ai-report .viewer{display:flex;flex-direction:column;min-width:0;height:100%;overflow:hidden}.fs-ai-report .resizer{flex:0 0 11px;align-self:stretch;display:flex;justify-content:center;cursor:col-resize;touch-action:none;-webkit-user-select:none;user-select:none}.fs-ai-report .resizer:before{content:\"\";width:3px;border-radius:3px;background:transparent;transition:background-color .12s ease}.fs-ai-report .resizer:hover:before{background:var(--brand-primary-color)}.fs-ai-report__split.resizing{cursor:col-resize;-webkit-user-select:none;user-select:none}.fs-ai-report__split.resizing .resizer:before{background:var(--brand-primary-color)}.fs-ai-report__split.resizing .chat,.fs-ai-report__split.resizing .viewer{pointer-events:none}\n"], dependencies: [{ kind: "ngmodule", type: FsFilterModule }, { kind: "component", type: i2.FilterComponent, selector: "fs-filter", inputs: ["config"], outputs: ["closed", "opened", "ready"] }, { kind: "ngmodule", type: FsMenuModule }, { kind: "component", type: i2$3.FsMenuComponent, selector: "fs-menu", inputs: ["class", "buttonClass", "buttonType", "buttonColor"], outputs: ["opened", "closed"] }, { kind: "directive", type: i2$3.FsMenuItemDirective, selector: "fs-menu-group,[fs-menu-item]" }, { kind: "directive", type: i2$3.FsMenuTriggerDirective, selector: "[fsMenuTriggerFor]", inputs: ["fsMenuTriggerFor"] }, { kind: "component", type: FsAiChatComponent, selector: "fs-ai-chat", inputs: ["basePath", "requestData", "introMessage"], outputs: ["response"] }, { kind: "ngmodule", type: MatButtonModule }, { kind: "component", type: i3$1.MatIconButton, selector: "button[mat-icon-button]", exportAs: ["matButton"] }, { kind: "ngmodule", type: MatIconModule }, { kind: "component", type: i4$1.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "ngmodule", type: MatTooltipModule }, { kind: "directive", type: i2$5.MatTooltip, selector: "[matTooltip]", inputs: ["matTooltipPosition", "matTooltipPositionAtOrigin", "matTooltipDisabled", "matTooltipShowDelay", "matTooltipHideDelay", "matTooltipTouchGestures", "matTooltip", "matTooltipClass"], exportAs: ["matTooltip"] }, { kind: "component", type: ReportCanvasComponent, selector: "app-report-canvas", inputs: ["report", "editMode"], outputs: ["componentSettings", "reportChanged", "editDone"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
4160
+ provideEchartsCore({ echarts: () => import('./firestitch-report-echarts-1VNsyco5.mjs').then((module) => module.default) }),
4161
+ ], viewQueries: [{ propertyName: "_split", first: true, predicate: ["split"], descendants: true, static: true }, { propertyName: "_chatPanel", first: true, predicate: ["chatPanel"], descendants: true, read: ElementRef, static: true }], ngImport: i0, template: "<div class=\"fs-ai-report fs-column\">\n\n <div class=\"fs-ai-report__header fs-row.align-center\">\n <h2 class=\"fs-ai-report__title fs-flex\">{{ report?.name }}</h2>\n\n @if (report) {\n <!-- Report-level filters, inline with the toolbar actions. fs-filter\n reads its config once at init, so it's keyed on the filter\n signature: when the report-level filter set changes the block is\n recreated and re-reads the rebuilt config. -->\n @if (reportHasFilters) {\n @for (key of [reportFilterKey]; track key) {\n <fs-filter\n class=\"fs-ai-report__filters\"\n [config]=\"reportFilterConfig\">\n </fs-filter>\n }\n }\n\n <button\n mat-icon-button\n type=\"button\"\n [matTooltip]=\"editMode ? 'Done' : 'Edit layout'\"\n (click)=\"toggleEditMode()\">\n <mat-icon>{{ editMode ? 'lock' : 'open_with' }}</mat-icon>\n </button>\n\n <button\n mat-icon-button\n type=\"button\"\n matTooltip=\"Export\"\n [fsMenuTriggerFor]=\"exportMenu\">\n <mat-icon>download</mat-icon>\n </button>\n <fs-menu #exportMenu>\n <ng-template\n fs-menu-item\n (click)=\"exportPowerpoint()\">\n Export PowerPoint\n </ng-template>\n <ng-template\n fs-menu-item\n (click)=\"exportPdf()\">\n Export PDF\n </ng-template>\n </fs-menu>\n\n <button\n mat-icon-button\n type=\"button\"\n matTooltip=\"Report settings\"\n (click)=\"reportSettings()\">\n <mat-icon>settings</mat-icon>\n </button>\n }\n </div>\n\n <div\n #split\n class=\"fs-ai-report__split fs-row.align-start\">\n <fs-ai-chat\n #chatPanel\n class=\"chat\"\n [basePath]=\"basePath()\"\n [requestData]=\"{ reportId: reportId() }\"\n (response)=\"onChatResponse($event)\">\n </fs-ai-chat>\n\n <div\n class=\"resizer\"\n (pointerdown)=\"onResizeStart($event)\">\n </div>\n\n <div class=\"viewer fs-flex fs-column\">\n @if (report) {\n <app-report-canvas\n [report]=\"report\"\n [editMode]=\"editMode\"\n (componentSettings)=\"componentSettings($event)\"\n (reportChanged)=\"onCanvasReportChanged()\"\n (editDone)=\"toggleEditMode()\">\n </app-report-canvas>\n }\n </div>\n </div>\n</div>\n", styles: [".fs-ai-report{height:100%;min-height:500px;display:flex;flex-direction:column}.fs-ai-report__header{flex:0 0 auto;margin-bottom:10px}.fs-ai-report__title{margin:0;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.fs-ai-report__filters{flex:0 0 auto;display:block;margin:0 5px 0 0}.fs-ai-report__filters ::ng-deep .filter-bar-container{align-items:center!important}.fs-ai-report__filters ::ng-deep .mat-mdc-form-field-subscript-wrapper{display:none}.fs-ai-report__split{flex:1 1 auto;min-height:0}.fs-ai-report .chat{display:block;flex:0 0 25%;width:100%;height:100%;min-height:400px;min-width:0;border:none}.fs-ai-report .viewer{display:flex;flex-direction:column;min-width:0;height:100%;overflow:hidden}.fs-ai-report .resizer{flex:0 0 11px;align-self:stretch;display:flex;justify-content:center;cursor:col-resize;touch-action:none;-webkit-user-select:none;user-select:none}.fs-ai-report .resizer:before{content:\"\";width:3px;border-radius:3px;background:transparent;transition:background-color .12s ease}.fs-ai-report .resizer:hover:before{background:var(--brand-primary-color)}.fs-ai-report__split.resizing{cursor:col-resize;-webkit-user-select:none;user-select:none}.fs-ai-report__split.resizing .resizer:before{background:var(--brand-primary-color)}.fs-ai-report__split.resizing .chat,.fs-ai-report__split.resizing .viewer{pointer-events:none}\n"], dependencies: [{ kind: "ngmodule", type: FsFilterModule }, { kind: "component", type: i2.FilterComponent, selector: "fs-filter", inputs: ["config"], outputs: ["closed", "opened", "ready"] }, { kind: "ngmodule", type: FsMenuModule }, { kind: "component", type: i2$3.FsMenuComponent, selector: "fs-menu", inputs: ["class", "buttonClass", "buttonType", "buttonColor"], outputs: ["opened", "closed"] }, { kind: "directive", type: i2$3.FsMenuItemDirective, selector: "fs-menu-group,[fs-menu-item]" }, { kind: "directive", type: i2$3.FsMenuTriggerDirective, selector: "[fsMenuTriggerFor]", inputs: ["fsMenuTriggerFor"] }, { kind: "component", type: FsAiChatComponent, selector: "fs-ai-chat", inputs: ["basePath", "requestData", "introMessage"], outputs: ["response"] }, { kind: "ngmodule", type: MatButtonModule }, { kind: "component", type: i3$1.MatIconButton, selector: "button[mat-icon-button]", exportAs: ["matButton"] }, { kind: "ngmodule", type: MatIconModule }, { kind: "component", type: i4$2.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "ngmodule", type: MatTooltipModule }, { kind: "directive", type: i2$5.MatTooltip, selector: "[matTooltip]", inputs: ["matTooltipPosition", "matTooltipPositionAtOrigin", "matTooltipDisabled", "matTooltipShowDelay", "matTooltipHideDelay", "matTooltipTouchGestures", "matTooltip", "matTooltipClass"], exportAs: ["matTooltip"] }, { kind: "component", type: ReportCanvasComponent, selector: "app-report-canvas", inputs: ["report", "editMode"], outputs: ["componentSettings", "reportChanged", "editDone"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
4007
4162
  }
4008
4163
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: FsAiReportComponent, decorators: [{
4009
4164
  type: Component,
@@ -4015,7 +4170,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
4015
4170
  ReportPptxService,
4016
4171
  ReportPdfService,
4017
4172
  // Tree-shaken ECharts core + the 'report' house theme, loaded lazily.
4018
- provideEchartsCore({ echarts: () => import('./firestitch-report-echarts-Cel1iHUg.mjs').then((module) => module.default) }),
4173
+ provideEchartsCore({ echarts: () => import('./firestitch-report-echarts-1VNsyco5.mjs').then((module) => module.default) }),
4019
4174
  ], imports: [
4020
4175
  FsFilterModule,
4021
4176
  FsMenuModule,
@@ -4046,4 +4201,4 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
4046
4201
  */
4047
4202
 
4048
4203
  export { FsAiReportsComponent as F, REPORT_CHART_COLORS_CSS as R, ReportComponent as a, FsAiReportComponent as b, ReportData as c, ReportService as d, ReportFilterStateService as e, FREQUENCY_OPTIONS as f };
4049
- //# sourceMappingURL=firestitch-report-firestitch-report-Y1XG_Gms.mjs.map
4204
+ //# sourceMappingURL=firestitch-report-firestitch-report-BNHGwPj2.mjs.map