@memberjunction/ng-clustering 5.43.0 → 5.44.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -237,8 +237,8 @@ export declare class ClusterConfigPanelComponent implements OnInit, OnDestroy {
237
237
  IsDocSelected(id: string): boolean;
238
238
  /** Toggle a document in/out of the multi-entity selection. */
239
239
  ToggleDoc(id: string): void;
240
- /** Toggle the multi-source expander. */
241
- ToggleMultiSource(): void;
240
+ /** Handle the multi-source accordion's expand/collapse. */
241
+ OnMultiSourceExpandedChange(expanded: boolean): void;
242
242
  /** Set the projection dimensionality (2 or 3). */
243
243
  SetDimensions(d: 2 | 3): void;
244
244
  /** Set the legend/color mode. */
@@ -1 +1 @@
1
- {"version":3,"file":"cluster-config-panel.component.d.ts","sourceRoot":"","sources":["../../src/lib/cluster-config-panel.component.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"cluster-config-panel.component.d.ts","sourceRoot":"","sources":["../../src/lib/cluster-config-panel.component.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=cluster-config-panel.component.dom.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cluster-config-panel.component.dom.test.d.ts","sourceRoot":"","sources":["../../src/lib/cluster-config-panel.component.dom.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,147 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { CommonModule } from '@angular/common';
3
+ import { FormsModule } from '@angular/forms';
4
+ import { renderComponentFixture, query, queryAll, hasClass, click, capture } from '@memberjunction/ng-test-utils';
5
+ import { ClusterConfigPanelComponent } from './cluster-config-panel.component';
6
+ /**
7
+ * DOM-level spec for <mj-cluster-config-panel>. It's a module-declared (standalone:false)
8
+ * component configured via @Inputs, so we render via renderComponentFixture with
9
+ * declarations + CommonModule/FormsModule (needed for ngModel + @if/@for).
10
+ */
11
+ function renderPanel(inputs = {}) {
12
+ return renderComponentFixture(ClusterConfigPanelComponent, {
13
+ imports: [CommonModule, FormsModule],
14
+ declarations: [ClusterConfigPanelComponent],
15
+ inputs,
16
+ });
17
+ }
18
+ const ENTITY_OPTIONS = [{ Name: 'Companies' }, { Name: 'Contacts' }, { Name: 'Deals' }];
19
+ describe('ClusterConfigPanelComponent (DOM)', () => {
20
+ it('renders the entity options into the entity <select>', () => {
21
+ const fixture = renderPanel({ EntityOptions: ENTITY_OPTIONS });
22
+ // First form-group is the entity picker.
23
+ const options = queryAll(fixture, '.form-group select option');
24
+ const names = options.slice(0, 3).map((o) => o.textContent?.trim());
25
+ expect(names).toEqual(['Companies', 'Contacts', 'Deals']);
26
+ });
27
+ it('filters the entity options by AvailableEntities', () => {
28
+ const fixture = renderPanel({ EntityOptions: ENTITY_OPTIONS, AvailableEntities: ['Contacts'] });
29
+ const firstSelectOptions = queryAll(fixture, '.form-group select option');
30
+ expect(firstSelectOptions.map((o) => o.textContent?.trim())).toContain('Contacts');
31
+ expect(firstSelectOptions.map((o) => o.textContent?.trim())).not.toContain('Companies');
32
+ });
33
+ it('shows the K-Means cluster slider by default and hides DBSCAN MinPoints', () => {
34
+ const fixture = renderPanel({ EntityOptions: ENTITY_OPTIONS });
35
+ // K label is present (K-Means default), Min Points label is not.
36
+ const labels = queryAll(fixture, 'label').map((l) => l.textContent?.trim() ?? '');
37
+ expect(labels.some((l) => l === 'Algorithm')).toBe(true);
38
+ expect(labels.some((l) => l.startsWith('Clusters (K)'))).toBe(true);
39
+ expect(labels.some((l) => l.startsWith('Min Points'))).toBe(false);
40
+ });
41
+ it('switches to DBSCAN sliders when the algorithm is dbscan', () => {
42
+ const fixture = renderPanel({ EntityOptions: ENTITY_OPTIONS, DefaultAlgorithm: 'dbscan' });
43
+ const labels = queryAll(fixture, 'label').map((l) => l.textContent?.trim() ?? '');
44
+ expect(labels.some((l) => l.startsWith('Epsilon'))).toBe(true);
45
+ expect(labels.some((l) => l.startsWith('Min Points'))).toBe(true);
46
+ expect(labels.some((l) => l.startsWith('Clusters (K)'))).toBe(false);
47
+ });
48
+ it('disables the Run button when no entity is selectable', () => {
49
+ const fixture = renderPanel({ EntityOptions: [] });
50
+ const runBtn = query(fixture, 'button.run-btn');
51
+ expect(runBtn.disabled).toBe(true);
52
+ });
53
+ it('enables the Run button once an entity is available (auto-selected)', () => {
54
+ const fixture = renderPanel({ EntityOptions: ENTITY_OPTIONS });
55
+ const runBtn = query(fixture, 'button.run-btn');
56
+ expect(runBtn.disabled).toBe(false);
57
+ });
58
+ it('shows the spinner + "Running..." text and disables Run while IsRunning', () => {
59
+ const fixture = renderPanel({ EntityOptions: ENTITY_OPTIONS, IsRunning: true });
60
+ const runBtn = query(fixture, 'button.run-btn');
61
+ expect(runBtn.disabled).toBe(true);
62
+ expect(runBtn.textContent).toContain('Running...');
63
+ expect(query(fixture, 'button.run-btn .fa-spinner')).not.toBeNull();
64
+ });
65
+ it('emits RunClustering with a config snapshot when Run is clicked', () => {
66
+ const fixture = renderPanel({ EntityOptions: ENTITY_OPTIONS });
67
+ const runs = capture(fixture.componentInstance.RunClustering);
68
+ click(fixture, 'button.run-btn');
69
+ expect(runs.length).toBe(1);
70
+ expect(runs[0].EntityName).toBe('Companies');
71
+ });
72
+ it('fires BeforeRunClustering and suppresses RunClustering when canceled', () => {
73
+ const fixture = renderPanel({ EntityOptions: ENTITY_OPTIONS });
74
+ fixture.componentInstance.BeforeRunClustering.subscribe((e) => (e.Cancel = true));
75
+ const runs = capture(fixture.componentInstance.RunClustering);
76
+ click(fixture, 'button.run-btn');
77
+ expect(runs.length).toBe(0);
78
+ });
79
+ it('collapses the body when the chevron is clicked', () => {
80
+ const fixture = renderPanel({ EntityOptions: ENTITY_OPTIONS });
81
+ expect(query(fixture, 'button.run-btn')).not.toBeNull();
82
+ click(fixture, '.header-actions .fa-solid');
83
+ fixture.detectChanges();
84
+ // Body is gated by @if (IsExpanded) — collapsing removes the run button from the DOM.
85
+ expect(query(fixture, 'button.run-btn')).toBeNull();
86
+ });
87
+ it('renders the metrics section when Metrics has records, with a "good" silhouette class', () => {
88
+ const metrics = {
89
+ SilhouetteScore: 0.72,
90
+ ClusterCount: 4,
91
+ ComputationTimeMs: 1200,
92
+ RecordCount: 350,
93
+ OutlierCount: 0,
94
+ };
95
+ const fixture = renderPanel({ EntityOptions: ENTITY_OPTIONS, Metrics: metrics });
96
+ const section = query(fixture, '.metrics-section');
97
+ expect(section).not.toBeNull();
98
+ expect(section?.textContent).toContain('0.72');
99
+ expect(section?.textContent).toContain('1.2s');
100
+ expect(hasClass(fixture, '.metric-value.good', 'good')).toBe(true);
101
+ });
102
+ it('does not render the metrics section when there are no records', () => {
103
+ const fixture = renderPanel({ EntityOptions: ENTITY_OPTIONS, Metrics: null });
104
+ expect(query(fixture, '.metrics-section')).toBeNull();
105
+ });
106
+ it('shows the Save button only when there are results to save', () => {
107
+ const metrics = {
108
+ SilhouetteScore: 0.4,
109
+ ClusterCount: 3,
110
+ ComputationTimeMs: 400,
111
+ RecordCount: 100,
112
+ OutlierCount: 2,
113
+ };
114
+ const fixture = renderPanel({ EntityOptions: ENTITY_OPTIONS, Metrics: metrics, ShowSaveButton: true });
115
+ expect(query(fixture, 'button.save-btn')).not.toBeNull();
116
+ const saves = capture(fixture.componentInstance.SaveVisualization);
117
+ click(fixture, 'button.save-btn');
118
+ expect(saves.length).toBe(1);
119
+ });
120
+ it('hides the algorithm picker when ShowAlgorithmPicker is false', () => {
121
+ const fixture = renderPanel({ EntityOptions: ENTITY_OPTIONS, ShowAlgorithmPicker: false });
122
+ const labels = queryAll(fixture, 'label').map((l) => l.textContent?.trim() ?? '');
123
+ expect(labels.some((l) => l === 'Algorithm')).toBe(false);
124
+ });
125
+ it('moves the active dimension segmented-button when 3D is clicked (SetDimensions)', () => {
126
+ const fixture = renderPanel({ EntityOptions: ENTITY_OPTIONS });
127
+ const segs = queryAll(fixture, 'button.seg-btn').filter((b) => ['2D', '3D'].includes(b.textContent?.trim() ?? ''));
128
+ const [twoD, threeD] = segs;
129
+ expect(twoD.classList.contains('active')).toBe(true); // default 2D
130
+ expect(threeD.classList.contains('active')).toBe(false);
131
+ threeD.click(); // SetDimensions(3)
132
+ fixture.detectChanges();
133
+ expect(threeD.classList.contains('active')).toBe(true);
134
+ expect(twoD.classList.contains('active')).toBe(false);
135
+ });
136
+ it('moves the active color-by segmented-button when Entity is clicked (SetColorBy)', () => {
137
+ const fixture = renderPanel({ EntityOptions: ENTITY_OPTIONS });
138
+ const segs = queryAll(fixture, 'button.seg-btn').filter((b) => ['Cluster', 'Entity'].includes(b.textContent?.trim() ?? ''));
139
+ const [cluster, entity] = segs;
140
+ expect(cluster.classList.contains('active')).toBe(true); // default cluster
141
+ entity.click(); // SetColorBy('entity')
142
+ fixture.detectChanges();
143
+ expect(entity.classList.contains('active')).toBe(true);
144
+ expect(cluster.classList.contains('active')).toBe(false);
145
+ });
146
+ });
147
+ //# sourceMappingURL=cluster-config-panel.component.dom.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cluster-config-panel.component.dom.test.js","sourceRoot":"","sources":["../../src/lib/cluster-config-panel.component.dom.test.ts"],"names":[],"mappings":"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","sourcesContent":["import { describe, it, expect } from 'vitest';\nimport { CommonModule } from '@angular/common';\nimport { FormsModule } from '@angular/forms';\nimport { ComponentFixture } from '@angular/core/testing';\nimport { renderComponentFixture, query, queryAll, hasClass, click, capture } from '@memberjunction/ng-test-utils';\nimport { ClusterConfigPanelComponent } from './cluster-config-panel.component';\nimport { ClusterConfig, ClusterMetrics, ClusterConfigPanelEntityOption } from './clustering.types';\n\n/**\n * DOM-level spec for <mj-cluster-config-panel>. It's a module-declared (standalone:false)\n * component configured via @Inputs, so we render via renderComponentFixture with\n * declarations + CommonModule/FormsModule (needed for ngModel + @if/@for).\n */\nfunction renderPanel(inputs: Record<string, unknown> = {}): ComponentFixture<ClusterConfigPanelComponent> {\n return renderComponentFixture(ClusterConfigPanelComponent, {\n imports: [CommonModule, FormsModule],\n declarations: [ClusterConfigPanelComponent],\n inputs,\n });\n}\n\nconst ENTITY_OPTIONS: ClusterConfigPanelEntityOption[] = [{ Name: 'Companies' }, { Name: 'Contacts' }, { Name: 'Deals' }];\n\ndescribe('ClusterConfigPanelComponent (DOM)', () => {\n it('renders the entity options into the entity <select>', () => {\n const fixture = renderPanel({ EntityOptions: ENTITY_OPTIONS });\n // First form-group is the entity picker.\n const options = queryAll(fixture, '.form-group select option');\n const names = options.slice(0, 3).map((o) => o.textContent?.trim());\n expect(names).toEqual(['Companies', 'Contacts', 'Deals']);\n });\n\n it('filters the entity options by AvailableEntities', () => {\n const fixture = renderPanel({ EntityOptions: ENTITY_OPTIONS, AvailableEntities: ['Contacts'] });\n const firstSelectOptions = queryAll(fixture, '.form-group select option');\n expect(firstSelectOptions.map((o) => o.textContent?.trim())).toContain('Contacts');\n expect(firstSelectOptions.map((o) => o.textContent?.trim())).not.toContain('Companies');\n });\n\n it('shows the K-Means cluster slider by default and hides DBSCAN MinPoints', () => {\n const fixture = renderPanel({ EntityOptions: ENTITY_OPTIONS });\n // K label is present (K-Means default), Min Points label is not.\n const labels = queryAll(fixture, 'label').map((l) => l.textContent?.trim() ?? '');\n expect(labels.some((l) => l === 'Algorithm')).toBe(true);\n expect(labels.some((l) => l.startsWith('Clusters (K)'))).toBe(true);\n expect(labels.some((l) => l.startsWith('Min Points'))).toBe(false);\n });\n\n it('switches to DBSCAN sliders when the algorithm is dbscan', () => {\n const fixture = renderPanel({ EntityOptions: ENTITY_OPTIONS, DefaultAlgorithm: 'dbscan' });\n const labels = queryAll(fixture, 'label').map((l) => l.textContent?.trim() ?? '');\n expect(labels.some((l) => l.startsWith('Epsilon'))).toBe(true);\n expect(labels.some((l) => l.startsWith('Min Points'))).toBe(true);\n expect(labels.some((l) => l.startsWith('Clusters (K)'))).toBe(false);\n });\n\n it('disables the Run button when no entity is selectable', () => {\n const fixture = renderPanel({ EntityOptions: [] });\n const runBtn = query(fixture, 'button.run-btn') as HTMLButtonElement;\n expect(runBtn.disabled).toBe(true);\n });\n\n it('enables the Run button once an entity is available (auto-selected)', () => {\n const fixture = renderPanel({ EntityOptions: ENTITY_OPTIONS });\n const runBtn = query(fixture, 'button.run-btn') as HTMLButtonElement;\n expect(runBtn.disabled).toBe(false);\n });\n\n it('shows the spinner + \"Running...\" text and disables Run while IsRunning', () => {\n const fixture = renderPanel({ EntityOptions: ENTITY_OPTIONS, IsRunning: true });\n const runBtn = query(fixture, 'button.run-btn') as HTMLButtonElement;\n expect(runBtn.disabled).toBe(true);\n expect(runBtn.textContent).toContain('Running...');\n expect(query(fixture, 'button.run-btn .fa-spinner')).not.toBeNull();\n });\n\n it('emits RunClustering with a config snapshot when Run is clicked', () => {\n const fixture = renderPanel({ EntityOptions: ENTITY_OPTIONS });\n const runs = capture<ClusterConfig>(fixture.componentInstance.RunClustering);\n click(fixture, 'button.run-btn');\n expect(runs.length).toBe(1);\n expect(runs[0].EntityName).toBe('Companies');\n });\n\n it('fires BeforeRunClustering and suppresses RunClustering when canceled', () => {\n const fixture = renderPanel({ EntityOptions: ENTITY_OPTIONS });\n fixture.componentInstance.BeforeRunClustering.subscribe((e) => (e.Cancel = true));\n const runs = capture<ClusterConfig>(fixture.componentInstance.RunClustering);\n click(fixture, 'button.run-btn');\n expect(runs.length).toBe(0);\n });\n\n it('collapses the body when the chevron is clicked', () => {\n const fixture = renderPanel({ EntityOptions: ENTITY_OPTIONS });\n expect(query(fixture, 'button.run-btn')).not.toBeNull();\n click(fixture, '.header-actions .fa-solid');\n fixture.detectChanges();\n // Body is gated by @if (IsExpanded) — collapsing removes the run button from the DOM.\n expect(query(fixture, 'button.run-btn')).toBeNull();\n });\n\n it('renders the metrics section when Metrics has records, with a \"good\" silhouette class', () => {\n const metrics: ClusterMetrics = {\n SilhouetteScore: 0.72,\n ClusterCount: 4,\n ComputationTimeMs: 1200,\n RecordCount: 350,\n OutlierCount: 0,\n };\n const fixture = renderPanel({ EntityOptions: ENTITY_OPTIONS, Metrics: metrics });\n const section = query(fixture, '.metrics-section');\n expect(section).not.toBeNull();\n expect(section?.textContent).toContain('0.72');\n expect(section?.textContent).toContain('1.2s');\n expect(hasClass(fixture, '.metric-value.good', 'good')).toBe(true);\n });\n\n it('does not render the metrics section when there are no records', () => {\n const fixture = renderPanel({ EntityOptions: ENTITY_OPTIONS, Metrics: null });\n expect(query(fixture, '.metrics-section')).toBeNull();\n });\n\n it('shows the Save button only when there are results to save', () => {\n const metrics: ClusterMetrics = {\n SilhouetteScore: 0.4,\n ClusterCount: 3,\n ComputationTimeMs: 400,\n RecordCount: 100,\n OutlierCount: 2,\n };\n const fixture = renderPanel({ EntityOptions: ENTITY_OPTIONS, Metrics: metrics, ShowSaveButton: true });\n expect(query(fixture, 'button.save-btn')).not.toBeNull();\n\n const saves = capture<void>(fixture.componentInstance.SaveVisualization);\n click(fixture, 'button.save-btn');\n expect(saves.length).toBe(1);\n });\n\n it('hides the algorithm picker when ShowAlgorithmPicker is false', () => {\n const fixture = renderPanel({ EntityOptions: ENTITY_OPTIONS, ShowAlgorithmPicker: false });\n const labels = queryAll(fixture, 'label').map((l) => l.textContent?.trim() ?? '');\n expect(labels.some((l) => l === 'Algorithm')).toBe(false);\n });\n\n it('moves the active dimension segmented-button when 3D is clicked (SetDimensions)', () => {\n const fixture = renderPanel({ EntityOptions: ENTITY_OPTIONS });\n const segs = queryAll(fixture, 'button.seg-btn').filter((b) => ['2D', '3D'].includes(b.textContent?.trim() ?? ''));\n const [twoD, threeD] = segs as HTMLButtonElement[];\n expect(twoD.classList.contains('active')).toBe(true); // default 2D\n expect(threeD.classList.contains('active')).toBe(false);\n threeD.click(); // SetDimensions(3)\n fixture.detectChanges();\n expect(threeD.classList.contains('active')).toBe(true);\n expect(twoD.classList.contains('active')).toBe(false);\n });\n\n it('moves the active color-by segmented-button when Entity is clicked (SetColorBy)', () => {\n const fixture = renderPanel({ EntityOptions: ENTITY_OPTIONS });\n const segs = queryAll(fixture, 'button.seg-btn').filter((b) => ['Cluster', 'Entity'].includes(b.textContent?.trim() ?? ''));\n const [cluster, entity] = segs as HTMLButtonElement[];\n expect(cluster.classList.contains('active')).toBe(true); // default cluster\n entity.click(); // SetColorBy('entity')\n fixture.detectChanges();\n expect(entity.classList.contains('active')).toBe(true);\n expect(cluster.classList.contains('active')).toBe(false);\n });\n});\n"]}
@@ -28,6 +28,7 @@ import { Component, Input, Output, EventEmitter, ElementRef, ChangeDetectorRef,
28
28
  import { DefaultClusterConfig, } from './clustering.types';
29
29
  import * as i0 from "@angular/core";
30
30
  import * as i1 from "@angular/forms";
31
+ import * as i2 from "@memberjunction/ng-ui-components";
31
32
  const _c0 = ["panelElement"];
32
33
  const _forTrack0 = ($index, $item) => $item.ID;
33
34
  function ClusterConfigPanelComponent_Conditional_8_For_5_Template(rf, ctx) { if (rf & 1) {
@@ -66,17 +67,25 @@ function ClusterConfigPanelComponent_Conditional_8_Conditional_6_Template(rf, ct
66
67
  i0.ɵɵadvance();
67
68
  i0.ɵɵrepeater(ctx_r2.EntityDocOptions);
68
69
  } }
69
- function ClusterConfigPanelComponent_Conditional_8_Conditional_7_Conditional_4_Template(rf, ctx) { if (rf & 1) {
70
- i0.ɵɵelementStart(0, "span", 23);
70
+ function ClusterConfigPanelComponent_Conditional_8_Conditional_7_ng_template_2_Conditional_1_Template(rf, ctx) { if (rf & 1) {
71
+ i0.ɵɵelementStart(0, "span", 24);
71
72
  i0.ɵɵtext(1);
72
73
  i0.ɵɵelementEnd();
73
74
  } if (rf & 2) {
74
- const ctx_r2 = i0.ɵɵnextContext(3);
75
+ const ctx_r2 = i0.ɵɵnextContext(4);
75
76
  i0.ɵɵadvance();
76
77
  i0.ɵɵtextInterpolate(ctx_r2.SelectedDocIDs.length);
77
78
  } }
78
- function ClusterConfigPanelComponent_Conditional_8_Conditional_7_Conditional_5_For_4_Conditional_4_Template(rf, ctx) { if (rf & 1) {
79
- i0.ɵɵelementStart(0, "span", 29);
79
+ function ClusterConfigPanelComponent_Conditional_8_Conditional_7_ng_template_2_Template(rf, ctx) { if (rf & 1) {
80
+ i0.ɵɵtext(0, " Multiple sources ");
81
+ i0.ɵɵconditionalCreate(1, ClusterConfigPanelComponent_Conditional_8_Conditional_7_ng_template_2_Conditional_1_Template, 2, 1, "span", 24);
82
+ } if (rf & 2) {
83
+ const ctx_r2 = i0.ɵɵnextContext(3);
84
+ i0.ɵɵadvance();
85
+ i0.ɵɵconditional(ctx_r2.IsMultiEntity ? 1 : -1);
86
+ } }
87
+ function ClusterConfigPanelComponent_Conditional_8_Conditional_7_ng_template_3_For_4_Conditional_4_Template(rf, ctx) { if (rf & 1) {
88
+ i0.ɵɵelementStart(0, "span", 30);
80
89
  i0.ɵɵtext(1);
81
90
  i0.ɵɵelementEnd();
82
91
  } if (rf & 2) {
@@ -84,15 +93,15 @@ function ClusterConfigPanelComponent_Conditional_8_Conditional_7_Conditional_5_F
84
93
  i0.ɵɵadvance();
85
94
  i0.ɵɵtextInterpolate(doc_r9.EntityName);
86
95
  } }
87
- function ClusterConfigPanelComponent_Conditional_8_Conditional_7_Conditional_5_For_4_Template(rf, ctx) { if (rf & 1) {
96
+ function ClusterConfigPanelComponent_Conditional_8_Conditional_7_ng_template_3_For_4_Template(rf, ctx) { if (rf & 1) {
88
97
  const _r8 = i0.ɵɵgetCurrentView();
89
- i0.ɵɵelementStart(0, "label", 26)(1, "input", 27);
90
- i0.ɵɵlistener("change", function ClusterConfigPanelComponent_Conditional_8_Conditional_7_Conditional_5_For_4_Template_input_change_1_listener() { const doc_r9 = i0.ɵɵrestoreView(_r8).$implicit; const ctx_r2 = i0.ɵɵnextContext(4); return i0.ɵɵresetView(ctx_r2.ToggleDoc(doc_r9.ID)); });
98
+ i0.ɵɵelementStart(0, "label", 27)(1, "input", 28);
99
+ i0.ɵɵlistener("change", function ClusterConfigPanelComponent_Conditional_8_Conditional_7_ng_template_3_For_4_Template_input_change_1_listener() { const doc_r9 = i0.ɵɵrestoreView(_r8).$implicit; const ctx_r2 = i0.ɵɵnextContext(4); return i0.ɵɵresetView(ctx_r2.ToggleDoc(doc_r9.ID)); });
91
100
  i0.ɵɵelementEnd();
92
- i0.ɵɵelementStart(2, "span", 28);
101
+ i0.ɵɵelementStart(2, "span", 29);
93
102
  i0.ɵɵtext(3);
94
103
  i0.ɵɵelementEnd();
95
- i0.ɵɵconditionalCreate(4, ClusterConfigPanelComponent_Conditional_8_Conditional_7_Conditional_5_For_4_Conditional_4_Template, 2, 1, "span", 29);
104
+ i0.ɵɵconditionalCreate(4, ClusterConfigPanelComponent_Conditional_8_Conditional_7_ng_template_3_For_4_Conditional_4_Template, 2, 1, "span", 30);
96
105
  i0.ɵɵelementEnd();
97
106
  } if (rf & 2) {
98
107
  const doc_r9 = ctx.$implicit;
@@ -104,11 +113,11 @@ function ClusterConfigPanelComponent_Conditional_8_Conditional_7_Conditional_5_F
104
113
  i0.ɵɵadvance();
105
114
  i0.ɵɵconditional(doc_r9.EntityName ? 4 : -1);
106
115
  } }
107
- function ClusterConfigPanelComponent_Conditional_8_Conditional_7_Conditional_5_Template(rf, ctx) { if (rf & 1) {
108
- i0.ɵɵelementStart(0, "div", 24)(1, "div", 25);
116
+ function ClusterConfigPanelComponent_Conditional_8_Conditional_7_ng_template_3_Template(rf, ctx) { if (rf & 1) {
117
+ i0.ɵɵelementStart(0, "div", 25)(1, "div", 26);
109
118
  i0.ɵɵtext(2, "Select 2+ documents to cluster them together. All must use the same embedding model.");
110
119
  i0.ɵɵelementEnd();
111
- i0.ɵɵrepeaterCreate(3, ClusterConfigPanelComponent_Conditional_8_Conditional_7_Conditional_5_For_4_Template, 5, 3, "label", 26, _forTrack0);
120
+ i0.ɵɵrepeaterCreate(3, ClusterConfigPanelComponent_Conditional_8_Conditional_7_ng_template_3_For_4_Template, 5, 3, "label", 27, _forTrack0);
112
121
  i0.ɵɵelementEnd();
113
122
  } if (rf & 2) {
114
123
  const ctx_r2 = i0.ɵɵnextContext(3);
@@ -117,22 +126,14 @@ function ClusterConfigPanelComponent_Conditional_8_Conditional_7_Conditional_5_T
117
126
  } }
118
127
  function ClusterConfigPanelComponent_Conditional_8_Conditional_7_Template(rf, ctx) { if (rf & 1) {
119
128
  const _r7 = i0.ɵɵgetCurrentView();
120
- i0.ɵɵelementStart(0, "div", 10)(1, "button", 21);
121
- i0.ɵɵlistener("click", function ClusterConfigPanelComponent_Conditional_8_Conditional_7_Template_button_click_1_listener() { i0.ɵɵrestoreView(_r7); const ctx_r2 = i0.ɵɵnextContext(2); return i0.ɵɵresetView(ctx_r2.ToggleMultiSource()); });
122
- i0.ɵɵelement(2, "i", 22);
123
- i0.ɵɵtext(3, " Multiple sources ");
124
- i0.ɵɵconditionalCreate(4, ClusterConfigPanelComponent_Conditional_8_Conditional_7_Conditional_4_Template, 2, 1, "span", 23);
125
- i0.ɵɵelementEnd();
126
- i0.ɵɵconditionalCreate(5, ClusterConfigPanelComponent_Conditional_8_Conditional_7_Conditional_5_Template, 5, 0, "div", 24);
127
- i0.ɵɵelementEnd();
129
+ i0.ɵɵelementStart(0, "div", 10)(1, "mj-accordion-panel", 21);
130
+ i0.ɵɵlistener("ExpandedChange", function ClusterConfigPanelComponent_Conditional_8_Conditional_7_Template_mj_accordion_panel_ExpandedChange_1_listener($event) { i0.ɵɵrestoreView(_r7); const ctx_r2 = i0.ɵɵnextContext(2); return i0.ɵɵresetView(ctx_r2.OnMultiSourceExpandedChange($event)); });
131
+ i0.ɵɵtemplate(2, ClusterConfigPanelComponent_Conditional_8_Conditional_7_ng_template_2_Template, 2, 1, "ng-template", 22)(3, ClusterConfigPanelComponent_Conditional_8_Conditional_7_ng_template_3_Template, 5, 0, "ng-template", 23);
132
+ i0.ɵɵelementEnd()();
128
133
  } if (rf & 2) {
129
134
  const ctx_r2 = i0.ɵɵnextContext(2);
130
- i0.ɵɵadvance(2);
131
- i0.ɵɵclassProp("fa-chevron-down", !ctx_r2.ShowMultiSource)("fa-chevron-up", ctx_r2.ShowMultiSource);
132
- i0.ɵɵadvance(2);
133
- i0.ɵɵconditional(ctx_r2.IsMultiEntity ? 4 : -1);
134
135
  i0.ɵɵadvance();
135
- i0.ɵɵconditional(ctx_r2.ShowMultiSource ? 5 : -1);
136
+ i0.ɵɵproperty("FlushBody", true)("Expanded", ctx_r2.ShowMultiSource);
136
137
  } }
137
138
  function ClusterConfigPanelComponent_Conditional_8_Conditional_9_Template(rf, ctx) { if (rf & 1) {
138
139
  const _r10 = i0.ɵɵgetCurrentView();
@@ -185,7 +186,7 @@ function ClusterConfigPanelComponent_Conditional_8_Conditional_24_Template(rf, c
185
186
  i0.ɵɵelementStart(0, "div", 7)(1, "label");
186
187
  i0.ɵɵtext(2);
187
188
  i0.ɵɵelementEnd();
188
- i0.ɵɵelementStart(3, "input", 30);
189
+ i0.ɵɵelementStart(3, "input", 31);
189
190
  i0.ɵɵlistener("ngModelChange", function ClusterConfigPanelComponent_Conditional_8_Conditional_24_Template_input_ngModelChange_3_listener($event) { i0.ɵɵrestoreView(_r13); const ctx_r2 = i0.ɵɵnextContext(2); ctx_r2.Config.K = $event; return i0.ɵɵresetView(ctx_r2.OnFieldChanged()); });
190
191
  i0.ɵɵelementEnd()();
191
192
  } if (rf & 2) {
@@ -200,7 +201,7 @@ function ClusterConfigPanelComponent_Conditional_8_Conditional_25_Template(rf, c
200
201
  i0.ɵɵelementStart(0, "div", 7)(1, "label");
201
202
  i0.ɵɵtext(2);
202
203
  i0.ɵɵelementEnd();
203
- i0.ɵɵelementStart(3, "input", 30);
204
+ i0.ɵɵelementStart(3, "input", 31);
204
205
  i0.ɵɵlistener("ngModelChange", function ClusterConfigPanelComponent_Conditional_8_Conditional_25_Template_input_ngModelChange_3_listener($event) { i0.ɵɵrestoreView(_r14); const ctx_r2 = i0.ɵɵnextContext(2); ctx_r2.Config.Epsilon = $event; return i0.ɵɵresetView(ctx_r2.OnFieldChanged()); });
205
206
  i0.ɵɵelementEnd()();
206
207
  } if (rf & 2) {
@@ -215,7 +216,7 @@ function ClusterConfigPanelComponent_Conditional_8_Conditional_26_Template(rf, c
215
216
  i0.ɵɵelementStart(0, "div", 7)(1, "label");
216
217
  i0.ɵɵtext(2);
217
218
  i0.ɵɵelementEnd();
218
- i0.ɵɵelementStart(3, "input", 30);
219
+ i0.ɵɵelementStart(3, "input", 31);
219
220
  i0.ɵɵlistener("ngModelChange", function ClusterConfigPanelComponent_Conditional_8_Conditional_26_Template_input_ngModelChange_3_listener($event) { i0.ɵɵrestoreView(_r15); const ctx_r2 = i0.ɵɵnextContext(2); ctx_r2.Config.MinPoints = $event; return i0.ɵɵresetView(ctx_r2.OnFieldChanged()); });
220
221
  i0.ɵɵelementEnd()();
221
222
  } if (rf & 2) {
@@ -252,26 +253,26 @@ function ClusterConfigPanelComponent_Conditional_8_Conditional_28_Template(rf, c
252
253
  i0.ɵɵrepeater(ctx_r2.MetricOptions);
253
254
  } }
254
255
  function ClusterConfigPanelComponent_Conditional_8_Conditional_35_Template(rf, ctx) { if (rf & 1) {
255
- i0.ɵɵelement(0, "i", 31);
256
+ i0.ɵɵelement(0, "i", 32);
256
257
  i0.ɵɵtext(1, " Running... ");
257
258
  } }
258
259
  function ClusterConfigPanelComponent_Conditional_8_Conditional_36_Template(rf, ctx) { if (rf & 1) {
259
- i0.ɵɵelement(0, "i", 32);
260
+ i0.ɵɵelement(0, "i", 33);
260
261
  i0.ɵɵtext(1, " Run Clustering ");
261
262
  } }
262
263
  function ClusterConfigPanelComponent_Conditional_8_Conditional_37_Template(rf, ctx) { if (rf & 1) {
263
264
  const _r18 = i0.ɵɵgetCurrentView();
264
- i0.ɵɵelementStart(0, "button", 33);
265
+ i0.ɵɵelementStart(0, "button", 34);
265
266
  i0.ɵɵlistener("click", function ClusterConfigPanelComponent_Conditional_8_Conditional_37_Template_button_click_0_listener() { i0.ɵɵrestoreView(_r18); const ctx_r2 = i0.ɵɵnextContext(2); return i0.ɵɵresetView(ctx_r2.OnSave()); });
266
- i0.ɵɵelement(1, "i", 34);
267
+ i0.ɵɵelement(1, "i", 35);
267
268
  i0.ɵɵtext(2, " Save ");
268
269
  i0.ɵɵelementEnd();
269
270
  } }
270
271
  function ClusterConfigPanelComponent_Conditional_8_Conditional_38_Conditional_21_Template(rf, ctx) { if (rf & 1) {
271
- i0.ɵɵelementStart(0, "div", 35)(1, "span", 36);
272
+ i0.ɵɵelementStart(0, "div", 36)(1, "span", 37);
272
273
  i0.ɵɵtext(2, "Outliers");
273
274
  i0.ɵɵelementEnd();
274
- i0.ɵɵelementStart(3, "span", 37);
275
+ i0.ɵɵelementStart(3, "span", 38);
275
276
  i0.ɵɵtext(4);
276
277
  i0.ɵɵelementEnd()();
277
278
  } if (rf & 2) {
@@ -280,31 +281,31 @@ function ClusterConfigPanelComponent_Conditional_8_Conditional_38_Conditional_21
280
281
  i0.ɵɵtextInterpolate(ctx_r2.Metrics.OutlierCount);
281
282
  } }
282
283
  function ClusterConfigPanelComponent_Conditional_8_Conditional_38_Template(rf, ctx) { if (rf & 1) {
283
- i0.ɵɵelementStart(0, "div", 20)(1, "div", 35)(2, "span", 36);
284
+ i0.ɵɵelementStart(0, "div", 20)(1, "div", 36)(2, "span", 37);
284
285
  i0.ɵɵtext(3, "Silhouette");
285
286
  i0.ɵɵelementEnd();
286
- i0.ɵɵelementStart(4, "span", 37);
287
+ i0.ɵɵelementStart(4, "span", 38);
287
288
  i0.ɵɵtext(5);
288
289
  i0.ɵɵelementEnd()();
289
- i0.ɵɵelementStart(6, "div", 35)(7, "span", 36);
290
+ i0.ɵɵelementStart(6, "div", 36)(7, "span", 37);
290
291
  i0.ɵɵtext(8, "Clusters");
291
292
  i0.ɵɵelementEnd();
292
- i0.ɵɵelementStart(9, "span", 37);
293
+ i0.ɵɵelementStart(9, "span", 38);
293
294
  i0.ɵɵtext(10);
294
295
  i0.ɵɵelementEnd()();
295
- i0.ɵɵelementStart(11, "div", 35)(12, "span", 36);
296
+ i0.ɵɵelementStart(11, "div", 36)(12, "span", 37);
296
297
  i0.ɵɵtext(13, "Records");
297
298
  i0.ɵɵelementEnd();
298
- i0.ɵɵelementStart(14, "span", 37);
299
+ i0.ɵɵelementStart(14, "span", 38);
299
300
  i0.ɵɵtext(15);
300
301
  i0.ɵɵelementEnd()();
301
- i0.ɵɵelementStart(16, "div", 35)(17, "span", 36);
302
+ i0.ɵɵelementStart(16, "div", 36)(17, "span", 37);
302
303
  i0.ɵɵtext(18, "Time");
303
304
  i0.ɵɵelementEnd();
304
- i0.ɵɵelementStart(19, "span", 37);
305
+ i0.ɵɵelementStart(19, "span", 38);
305
306
  i0.ɵɵtext(20);
306
307
  i0.ɵɵelementEnd()();
307
- i0.ɵɵconditionalCreate(21, ClusterConfigPanelComponent_Conditional_8_Conditional_38_Conditional_21_Template, 5, 1, "div", 35);
308
+ i0.ɵɵconditionalCreate(21, ClusterConfigPanelComponent_Conditional_8_Conditional_38_Conditional_21_Template, 5, 1, "div", 36);
308
309
  i0.ɵɵelementEnd();
309
310
  } if (rf & 2) {
310
311
  const ctx_r2 = i0.ɵɵnextContext(2);
@@ -331,7 +332,7 @@ function ClusterConfigPanelComponent_Conditional_8_Template(rf, ctx) { if (rf &
331
332
  i0.ɵɵrepeaterCreate(4, ClusterConfigPanelComponent_Conditional_8_For_5_Template, 2, 2, "option", 9, i0.ɵɵcomponentInstance().TrackEntityBy, true);
332
333
  i0.ɵɵelementEnd()();
333
334
  i0.ɵɵconditionalCreate(6, ClusterConfigPanelComponent_Conditional_8_Conditional_6_Template, 6, 1, "div", 7);
334
- i0.ɵɵconditionalCreate(7, ClusterConfigPanelComponent_Conditional_8_Conditional_7_Template, 6, 6, "div", 10);
335
+ i0.ɵɵconditionalCreate(7, ClusterConfigPanelComponent_Conditional_8_Conditional_7_Template, 4, 2, "div", 10);
335
336
  i0.ɵɵelementStart(8, "div", 11);
336
337
  i0.ɵɵconditionalCreate(9, ClusterConfigPanelComponent_Conditional_8_Conditional_9_Template, 8, 4, "div", 7);
337
338
  i0.ɵɵelementStart(10, "div", 7)(11, "label");
@@ -707,9 +708,9 @@ export class ClusterConfigPanelComponent {
707
708
  this.emitConfigChanged();
708
709
  this.cdr.detectChanges();
709
710
  }
710
- /** Toggle the multi-source expander. */
711
- ToggleMultiSource() {
712
- this.ShowMultiSource = !this.ShowMultiSource;
711
+ /** Handle the multi-source accordion's expand/collapse. */
712
+ OnMultiSourceExpandedChange(expanded) {
713
+ this.ShowMultiSource = expanded;
713
714
  this.cdr.detectChanges();
714
715
  }
715
716
  /** Set the projection dimensionality (2 or 3). */
@@ -859,7 +860,7 @@ export class ClusterConfigPanelComponent {
859
860
  } if (rf & 2) {
860
861
  let _t;
861
862
  i0.ɵɵqueryRefresh(_t = i0.ɵɵloadQuery()) && (ctx.panelRef = _t.first);
862
- } }, inputs: { IsRunning: "IsRunning", Metrics: "Metrics", EntityOptions: "EntityOptions", EntityDocOptions: "EntityDocOptions", AllEntityDocOptions: "AllEntityDocOptions", ShowDimensionToggle: "ShowDimensionToggle", ShowMultiEntity: "ShowMultiEntity", ShowSaveButton: "ShowSaveButton", ShowAlgorithmPicker: "ShowAlgorithmPicker", ShowMetricPicker: "ShowMetricPicker", DefaultAlgorithm: "DefaultAlgorithm", AvailableEntities: "AvailableEntities", Collapsed: "Collapsed" }, outputs: { RunClustering: "RunClustering", SaveVisualization: "SaveVisualization", BeforeRunClustering: "BeforeRunClustering", BeforeSave: "BeforeSave", ConfigChanged: "ConfigChanged", AlgorithmChanged: "AlgorithmChanged", DimensionsChanged: "DimensionsChanged" }, standalone: false, decls: 9, vars: 9, consts: [["panelElement", ""], [1, "config-panel"], [1, "panel-header", 3, "mousedown"], [1, "fa-solid", "fa-sliders"], [1, "header-actions"], ["title", "Toggle panel", 1, "fa-solid", 3, "click"], [1, "fa-solid", "fa-grip-dots-vertical", "drag-handle"], [1, "form-group"], [3, "ngModelChange", "ngModel"], [3, "value"], [1, "form-group", "multi-source"], [1, "form-row", "toggle-row"], ["role", "group", 1, "seg-toggle"], ["type", "button", 1, "seg-btn", 3, "click"], ["type", "text", "placeholder", "e.g. Status = 'Active'", 3, "ngModelChange", "ngModel"], [1, "form-row"], ["type", "number", 3, "ngModelChange", "ngModel", "min", "max"], [1, "action-row"], [1, "run-btn", 3, "click", "disabled"], ["title", "Save this visualization", 1, "save-btn"], [1, "metrics-section"], ["type", "button", 1, "multi-source-toggle", 3, "click"], [1, "fa-solid"], [1, "multi-source-badge"], [1, "multi-source-list"], [1, "multi-source-hint"], [1, "multi-source-item"], ["type", "checkbox", 3, "change", "checked"], [1, "multi-source-name"], [1, "multi-source-entity"], ["type", "range", 3, "ngModelChange", "ngModel", "min", "max", "step"], [1, "fa-solid", "fa-spinner", "fa-spin"], [1, "fa-solid", "fa-play"], ["title", "Save this visualization", 1, "save-btn", 3, "click"], [1, "fa-solid", "fa-bookmark"], [1, "metric-item"], [1, "metric-label"], [1, "metric-value"]], template: function ClusterConfigPanelComponent_Template(rf, ctx) { if (rf & 1) {
863
+ } }, inputs: { IsRunning: "IsRunning", Metrics: "Metrics", EntityOptions: "EntityOptions", EntityDocOptions: "EntityDocOptions", AllEntityDocOptions: "AllEntityDocOptions", ShowDimensionToggle: "ShowDimensionToggle", ShowMultiEntity: "ShowMultiEntity", ShowSaveButton: "ShowSaveButton", ShowAlgorithmPicker: "ShowAlgorithmPicker", ShowMetricPicker: "ShowMetricPicker", DefaultAlgorithm: "DefaultAlgorithm", AvailableEntities: "AvailableEntities", Collapsed: "Collapsed" }, outputs: { RunClustering: "RunClustering", SaveVisualization: "SaveVisualization", BeforeRunClustering: "BeforeRunClustering", BeforeSave: "BeforeSave", ConfigChanged: "ConfigChanged", AlgorithmChanged: "AlgorithmChanged", DimensionsChanged: "DimensionsChanged" }, standalone: false, decls: 9, vars: 9, consts: [["panelElement", ""], [1, "config-panel"], [1, "panel-header", 3, "mousedown"], [1, "fa-solid", "fa-sliders"], [1, "header-actions"], ["title", "Toggle panel", 1, "fa-solid", 3, "click"], [1, "fa-solid", "fa-grip-dots-vertical", "drag-handle"], [1, "form-group"], [3, "ngModelChange", "ngModel"], [3, "value"], [1, "form-group", "multi-source"], [1, "form-row", "toggle-row"], ["role", "group", 1, "seg-toggle"], ["type", "button", 1, "seg-btn", 3, "click"], ["type", "text", "placeholder", "e.g. Status = 'Active'", 3, "ngModelChange", "ngModel"], [1, "form-row"], ["type", "number", 3, "ngModelChange", "ngModel", "min", "max"], [1, "action-row"], [1, "run-btn", 3, "click", "disabled"], ["title", "Save this visualization", 1, "save-btn"], [1, "metrics-section"], ["Size", "sm", 3, "ExpandedChange", "FlushBody", "Expanded"], ["mjAccordionTitle", ""], ["mjAccordionBody", ""], [1, "mj-accordion-badge", "mj-accordion-badge--right"], [1, "multi-source-list"], [1, "multi-source-hint"], [1, "multi-source-item"], ["type", "checkbox", 3, "change", "checked"], [1, "multi-source-name"], [1, "multi-source-entity"], ["type", "range", 3, "ngModelChange", "ngModel", "min", "max", "step"], [1, "fa-solid", "fa-spinner", "fa-spin"], [1, "fa-solid", "fa-play"], ["title", "Save this visualization", 1, "save-btn", 3, "click"], [1, "fa-solid", "fa-bookmark"], [1, "metric-item"], [1, "metric-label"], [1, "metric-value"]], template: function ClusterConfigPanelComponent_Template(rf, ctx) { if (rf & 1) {
863
864
  const _r1 = i0.ɵɵgetCurrentView();
864
865
  i0.ɵɵelementStart(0, "div", 1, 0)(2, "h3", 2);
865
866
  i0.ɵɵlistener("mousedown", function ClusterConfigPanelComponent_Template_h3_mousedown_2_listener($event) { i0.ɵɵrestoreView(_r1); return i0.ɵɵresetView(ctx.OnHeaderMouseDown($event)); });
@@ -878,11 +879,11 @@ export class ClusterConfigPanelComponent {
878
879
  i0.ɵɵclassProp("fa-chevron-up", ctx.IsExpanded)("fa-chevron-down", !ctx.IsExpanded);
879
880
  i0.ɵɵadvance(2);
880
881
  i0.ɵɵconditional(ctx.IsExpanded ? 8 : -1);
881
- } }, dependencies: [i1.NgSelectOption, i1.ɵNgSelectMultipleOption, i1.DefaultValueAccessor, i1.NumberValueAccessor, i1.RangeValueAccessor, i1.SelectControlValueAccessor, i1.NgControlStatus, i1.MinValidator, i1.MaxValidator, i1.NgModel], styles: [".config-panel[_ngcontent-%COMP%] {\n position: absolute;\n width: 300px;\n background: color-mix(in srgb, var(--mj-bg-surface-elevated) 95%, transparent);\n backdrop-filter: blur(20px);\n -webkit-backdrop-filter: blur(20px);\n border: 1px solid var(--mj-border-default);\n border-radius: 14px;\n padding: 20px;\n box-shadow: 0 20px 60px color-mix(in srgb, var(--mj-bg-page) 40%, transparent);\n z-index: 10;\n}\n\n.panel-header[_ngcontent-%COMP%] {\n font-size: 0.8rem;\n font-weight: 600;\n margin: 0 0 16px 0;\n display: flex;\n align-items: center;\n gap: 8px;\n color: var(--mj-text-primary);\n cursor: move;\n user-select: none;\n}\n\n.panel-header[_ngcontent-%COMP%] i[_ngcontent-%COMP%]:first-child {\n color: var(--mj-brand-primary);\n}\n\n.header-actions[_ngcontent-%COMP%] {\n margin-left: auto;\n display: flex;\n align-items: center;\n gap: 8px;\n}\n\n.header-actions[_ngcontent-%COMP%] i[_ngcontent-%COMP%] {\n color: var(--mj-text-muted);\n font-size: 0.7rem;\n cursor: pointer;\n transition: color 0.15s;\n}\n\n.header-actions[_ngcontent-%COMP%] i[_ngcontent-%COMP%]:hover {\n color: var(--mj-text-primary);\n}\n\n.drag-handle[_ngcontent-%COMP%] {\n color: var(--mj-text-muted);\n font-size: 0.7rem;\n}\n\n\n\n.form-group[_ngcontent-%COMP%] {\n margin-bottom: 14px;\n}\n\n.form-group[_ngcontent-%COMP%] label[_ngcontent-%COMP%] {\n display: block;\n font-size: 0.7rem;\n font-weight: 500;\n color: var(--mj-text-secondary);\n margin-bottom: 5px;\n text-transform: uppercase;\n letter-spacing: 0.5px;\n}\n\n.form-group[_ngcontent-%COMP%] select[_ngcontent-%COMP%], \n.form-group[_ngcontent-%COMP%] input[type=\"text\"][_ngcontent-%COMP%], \n.form-group[_ngcontent-%COMP%] input[type=\"number\"][_ngcontent-%COMP%] {\n width: 100%;\n padding: 8px 12px;\n background: var(--mj-bg-surface-sunken);\n border: 1px solid var(--mj-border-default);\n border-radius: 7px;\n color: var(--mj-text-primary);\n font-size: 0.82rem;\n outline: none;\n transition: border 0.15s;\n}\n\n.form-group[_ngcontent-%COMP%] select[_ngcontent-%COMP%]:focus, \n.form-group[_ngcontent-%COMP%] input[type=\"text\"][_ngcontent-%COMP%]:focus, \n.form-group[_ngcontent-%COMP%] input[type=\"number\"][_ngcontent-%COMP%]:focus {\n border-color: var(--mj-brand-primary);\n}\n\n.form-group[_ngcontent-%COMP%] input[type=\"range\"][_ngcontent-%COMP%] {\n width: 100%;\n accent-color: var(--mj-brand-primary);\n}\n\n.form-row[_ngcontent-%COMP%] {\n display: flex;\n gap: 10px;\n}\n\n.form-row[_ngcontent-%COMP%] .form-group[_ngcontent-%COMP%] {\n flex: 1;\n}\n\n\n\n.action-row[_ngcontent-%COMP%] {\n display: flex;\n gap: 8px;\n margin-top: 4px;\n}\n\n.run-btn[_ngcontent-%COMP%] {\n flex: 1;\n padding: 10px;\n background: var(--mj-brand-primary);\n border: none;\n border-radius: 8px;\n color: var(--mj-text-inverse);\n font-size: 0.82rem;\n font-weight: 600;\n cursor: pointer;\n display: flex;\n align-items: center;\n justify-content: center;\n gap: 8px;\n transition: background 0.15s;\n}\n\n.run-btn[_ngcontent-%COMP%]:hover:not(:disabled) {\n background: var(--mj-brand-primary-hover);\n}\n\n.run-btn[_ngcontent-%COMP%]:disabled {\n opacity: 0.6;\n cursor: not-allowed;\n}\n\n.save-btn[_ngcontent-%COMP%] {\n flex: 0 0 auto;\n padding: 10px 14px;\n background: var(--mj-bg-surface);\n border: 1px solid var(--mj-border-default);\n border-radius: 8px;\n color: var(--mj-text-secondary);\n font-size: 0.82rem;\n font-weight: 600;\n cursor: pointer;\n display: flex;\n align-items: center;\n gap: 6px;\n transition: all 0.15s;\n}\n\n.save-btn[_ngcontent-%COMP%]:hover {\n border-color: var(--mj-brand-primary);\n color: var(--mj-brand-primary);\n background: color-mix(in srgb, var(--mj-brand-primary) 8%, var(--mj-bg-surface));\n}\n\n\n\n.toggle-row[_ngcontent-%COMP%] .form-group[_ngcontent-%COMP%] { min-width: 0; }\n\n.seg-toggle[_ngcontent-%COMP%] {\n display: inline-flex;\n gap: 2px;\n padding: 3px;\n background: var(--mj-bg-surface-sunken);\n border: 1px solid var(--mj-border-default);\n border-radius: 8px;\n width: 100%;\n}\n\n.seg-btn[_ngcontent-%COMP%] {\n flex: 1;\n padding: 5px 8px;\n border: none;\n background: transparent;\n color: var(--mj-text-secondary);\n font-size: 0.75rem;\n font-weight: 600;\n border-radius: 6px;\n cursor: pointer;\n transition: background 0.15s, color 0.15s;\n}\n\n.seg-btn[_ngcontent-%COMP%]:hover { color: var(--mj-text-primary); }\n\n.seg-btn.active[_ngcontent-%COMP%] {\n background: var(--mj-bg-surface);\n color: var(--mj-brand-primary);\n box-shadow: 0 1px 2px color-mix(in srgb, var(--mj-text-primary) 12%, transparent);\n}\n\n\n\n.multi-source-toggle[_ngcontent-%COMP%] {\n display: flex;\n align-items: center;\n gap: 8px;\n width: 100%;\n padding: 8px 10px;\n background: var(--mj-bg-surface-sunken);\n border: 1px solid var(--mj-border-default);\n border-radius: 8px;\n color: var(--mj-text-secondary);\n font-size: 0.78rem;\n font-weight: 600;\n cursor: pointer;\n}\n\n.multi-source-toggle[_ngcontent-%COMP%]:hover { color: var(--mj-text-primary); }\n\n.multi-source-badge[_ngcontent-%COMP%] {\n margin-left: auto;\n background: var(--mj-brand-primary);\n color: var(--mj-text-inverse);\n border-radius: 10px;\n padding: 1px 8px;\n font-size: 0.7rem;\n}\n\n.multi-source-list[_ngcontent-%COMP%] {\n margin-top: 6px;\n border: 1px solid var(--mj-border-default);\n border-radius: 8px;\n padding: 8px;\n max-height: 180px;\n overflow-y: auto;\n background: var(--mj-bg-surface);\n}\n\n.multi-source-hint[_ngcontent-%COMP%] {\n font-size: 0.68rem;\n color: var(--mj-text-muted);\n margin-bottom: 6px;\n line-height: 1.3;\n}\n\n.multi-source-item[_ngcontent-%COMP%] {\n display: flex;\n align-items: center;\n gap: 8px;\n padding: 5px 4px;\n cursor: pointer;\n font-size: 0.78rem;\n color: var(--mj-text-primary);\n}\n\n.multi-source-item[_ngcontent-%COMP%]:hover { background: var(--mj-bg-surface-hover); border-radius: 6px; }\n\n.multi-source-entity[_ngcontent-%COMP%] {\n margin-left: auto;\n font-size: 0.68rem;\n color: var(--mj-text-muted);\n}\n\n\n\n.metrics-section[_ngcontent-%COMP%] {\n display: grid;\n grid-template-columns: 1fr 1fr;\n gap: 8px;\n margin-top: 14px;\n padding-top: 14px;\n border-top: 1px solid var(--mj-border-default);\n}\n\n.metric-item[_ngcontent-%COMP%] {\n display: flex;\n flex-direction: column;\n gap: 2px;\n}\n\n.metric-label[_ngcontent-%COMP%] {\n font-size: 0.65rem;\n text-transform: uppercase;\n letter-spacing: 0.5px;\n color: var(--mj-text-muted);\n}\n\n.metric-value[_ngcontent-%COMP%] {\n font-size: 0.85rem;\n font-weight: 600;\n color: var(--mj-text-primary);\n}\n\n.metric-value.good[_ngcontent-%COMP%] {\n color: var(--mj-status-success);\n}"] }); }
882
+ } }, dependencies: [i1.NgSelectOption, i1.ɵNgSelectMultipleOption, i1.DefaultValueAccessor, i1.NumberValueAccessor, i1.RangeValueAccessor, i1.SelectControlValueAccessor, i1.NgControlStatus, i1.MinValidator, i1.MaxValidator, i1.NgModel, i2.MJAccordionPanelComponent, i2.MJAccordionTitleDirective, i2.MJAccordionBodyDirective], styles: [".config-panel[_ngcontent-%COMP%] {\n position: absolute;\n width: 300px;\n background: color-mix(in srgb, var(--mj-bg-surface-elevated) 95%, transparent);\n backdrop-filter: blur(20px);\n -webkit-backdrop-filter: blur(20px);\n border: 1px solid var(--mj-border-default);\n border-radius: 14px;\n padding: 20px;\n box-shadow: 0 20px 60px color-mix(in srgb, var(--mj-bg-page) 40%, transparent);\n z-index: 10;\n}\n\n.panel-header[_ngcontent-%COMP%] {\n font-size: 0.8rem;\n font-weight: 600;\n margin: 0 0 16px 0;\n display: flex;\n align-items: center;\n gap: 8px;\n color: var(--mj-text-primary);\n cursor: move;\n user-select: none;\n}\n\n.panel-header[_ngcontent-%COMP%] i[_ngcontent-%COMP%]:first-child {\n color: var(--mj-brand-primary);\n}\n\n.header-actions[_ngcontent-%COMP%] {\n margin-left: auto;\n display: flex;\n align-items: center;\n gap: 8px;\n}\n\n.header-actions[_ngcontent-%COMP%] i[_ngcontent-%COMP%] {\n color: var(--mj-text-muted);\n font-size: 0.7rem;\n cursor: pointer;\n transition: color 0.15s;\n}\n\n.header-actions[_ngcontent-%COMP%] i[_ngcontent-%COMP%]:hover {\n color: var(--mj-text-primary);\n}\n\n.drag-handle[_ngcontent-%COMP%] {\n color: var(--mj-text-muted);\n font-size: 0.7rem;\n}\n\n\n\n.form-group[_ngcontent-%COMP%] {\n margin-bottom: 14px;\n}\n\n.form-group[_ngcontent-%COMP%] label[_ngcontent-%COMP%] {\n display: block;\n font-size: 0.7rem;\n font-weight: 500;\n color: var(--mj-text-secondary);\n margin-bottom: 5px;\n text-transform: uppercase;\n letter-spacing: 0.5px;\n}\n\n.form-group[_ngcontent-%COMP%] select[_ngcontent-%COMP%], \n.form-group[_ngcontent-%COMP%] input[type=\"text\"][_ngcontent-%COMP%], \n.form-group[_ngcontent-%COMP%] input[type=\"number\"][_ngcontent-%COMP%] {\n width: 100%;\n padding: 8px 12px;\n background: var(--mj-bg-surface-sunken);\n border: 1px solid var(--mj-border-default);\n border-radius: 7px;\n color: var(--mj-text-primary);\n font-size: 0.82rem;\n outline: none;\n transition: border 0.15s;\n}\n\n.form-group[_ngcontent-%COMP%] select[_ngcontent-%COMP%]:focus, \n.form-group[_ngcontent-%COMP%] input[type=\"text\"][_ngcontent-%COMP%]:focus, \n.form-group[_ngcontent-%COMP%] input[type=\"number\"][_ngcontent-%COMP%]:focus {\n border-color: var(--mj-brand-primary);\n}\n\n.form-group[_ngcontent-%COMP%] input[type=\"range\"][_ngcontent-%COMP%] {\n width: 100%;\n accent-color: var(--mj-brand-primary);\n}\n\n.form-row[_ngcontent-%COMP%] {\n display: flex;\n gap: 10px;\n}\n\n.form-row[_ngcontent-%COMP%] .form-group[_ngcontent-%COMP%] {\n flex: 1;\n}\n\n\n\n.action-row[_ngcontent-%COMP%] {\n display: flex;\n gap: 8px;\n margin-top: 4px;\n}\n\n.run-btn[_ngcontent-%COMP%] {\n flex: 1;\n padding: 10px;\n background: var(--mj-brand-primary);\n border: none;\n border-radius: 8px;\n color: var(--mj-text-inverse);\n font-size: 0.82rem;\n font-weight: 600;\n cursor: pointer;\n display: flex;\n align-items: center;\n justify-content: center;\n gap: 8px;\n transition: background 0.15s;\n}\n\n.run-btn[_ngcontent-%COMP%]:hover:not(:disabled) {\n background: var(--mj-brand-primary-hover);\n}\n\n.run-btn[_ngcontent-%COMP%]:disabled {\n opacity: 0.6;\n cursor: not-allowed;\n}\n\n.save-btn[_ngcontent-%COMP%] {\n flex: 0 0 auto;\n padding: 10px 14px;\n background: var(--mj-bg-surface);\n border: 1px solid var(--mj-border-default);\n border-radius: 8px;\n color: var(--mj-text-secondary);\n font-size: 0.82rem;\n font-weight: 600;\n cursor: pointer;\n display: flex;\n align-items: center;\n gap: 6px;\n transition: all 0.15s;\n}\n\n.save-btn[_ngcontent-%COMP%]:hover {\n border-color: var(--mj-brand-primary);\n color: var(--mj-brand-primary);\n background: color-mix(in srgb, var(--mj-brand-primary) 8%, var(--mj-bg-surface));\n}\n\n\n\n.toggle-row[_ngcontent-%COMP%] .form-group[_ngcontent-%COMP%] { min-width: 0; }\n\n.seg-toggle[_ngcontent-%COMP%] {\n display: inline-flex;\n gap: 2px;\n padding: 3px;\n background: var(--mj-bg-surface-sunken);\n border: 1px solid var(--mj-border-default);\n border-radius: 8px;\n width: 100%;\n}\n\n.seg-btn[_ngcontent-%COMP%] {\n flex: 1;\n padding: 5px 8px;\n border: none;\n background: transparent;\n color: var(--mj-text-secondary);\n font-size: 0.75rem;\n font-weight: 600;\n border-radius: 6px;\n cursor: pointer;\n transition: background 0.15s, color 0.15s;\n}\n\n.seg-btn[_ngcontent-%COMP%]:hover { color: var(--mj-text-primary); }\n\n.seg-btn.active[_ngcontent-%COMP%] {\n background: var(--mj-bg-surface);\n color: var(--mj-brand-primary);\n box-shadow: 0 1px 2px color-mix(in srgb, var(--mj-text-primary) 12%, transparent);\n}\n\n\n\n\n\n\n\n\n.multi-source-list[_ngcontent-%COMP%] {\n border: 1px solid var(--mj-border-default);\n border-radius: 8px;\n padding: 8px;\n max-height: 180px;\n overflow-y: auto;\n background: var(--mj-bg-surface);\n}\n\n.multi-source-hint[_ngcontent-%COMP%] {\n font-size: 0.68rem;\n color: var(--mj-text-muted);\n margin-bottom: 6px;\n line-height: 1.3;\n}\n\n.multi-source-item[_ngcontent-%COMP%] {\n display: flex;\n align-items: center;\n gap: 8px;\n padding: 5px 4px;\n cursor: pointer;\n font-size: 0.78rem;\n color: var(--mj-text-primary);\n}\n\n.multi-source-item[_ngcontent-%COMP%]:hover { background: var(--mj-bg-surface-hover); border-radius: 6px; }\n\n.multi-source-entity[_ngcontent-%COMP%] {\n margin-left: auto;\n font-size: 0.68rem;\n color: var(--mj-text-muted);\n}\n\n\n\n.metrics-section[_ngcontent-%COMP%] {\n display: grid;\n grid-template-columns: 1fr 1fr;\n gap: 8px;\n margin-top: 14px;\n padding-top: 14px;\n border-top: 1px solid var(--mj-border-default);\n}\n\n.metric-item[_ngcontent-%COMP%] {\n display: flex;\n flex-direction: column;\n gap: 2px;\n}\n\n.metric-label[_ngcontent-%COMP%] {\n font-size: 0.65rem;\n text-transform: uppercase;\n letter-spacing: 0.5px;\n color: var(--mj-text-muted);\n}\n\n.metric-value[_ngcontent-%COMP%] {\n font-size: 0.85rem;\n font-weight: 600;\n color: var(--mj-text-primary);\n}\n\n.metric-value.good[_ngcontent-%COMP%] {\n color: var(--mj-status-success);\n}"] }); }
882
883
  }
883
884
  (() => { (typeof ngDevMode === "undefined" || ngDevMode) && i0.ɵsetClassMetadata(ClusterConfigPanelComponent, [{
884
885
  type: Component,
885
- args: [{ standalone: false, selector: 'mj-cluster-config-panel', template: "<div #panelElement\n class=\"config-panel\"\n [style.top.px]=\"PanelTop\"\n [style.left.px]=\"PanelLeft\">\n\n <!-- Draggable header -->\n <h3 class=\"panel-header\"\n (mousedown)=\"OnHeaderMouseDown($event)\">\n <i class=\"fa-solid fa-sliders\"></i>\n Configuration\n <span class=\"header-actions\">\n <i class=\"fa-solid\"\n [class.fa-chevron-up]=\"IsExpanded\"\n [class.fa-chevron-down]=\"!IsExpanded\"\n (click)=\"ToggleCollapse(); $event.stopPropagation()\"\n title=\"Toggle panel\"></i>\n <i class=\"fa-solid fa-grip-dots-vertical drag-handle\"></i>\n </span>\n </h3>\n\n @if (IsExpanded) {\n <!-- Entity picker -->\n <div class=\"form-group\">\n <label>Entity</label>\n <select [ngModel]=\"Config.EntityName\"\n (ngModelChange)=\"OnEntityChanged($event)\">\n @for (entity of FilteredEntityOptions; track TrackEntityBy($index, entity)) {\n <option [value]=\"entity.Name\">{{ entity.Name }}</option>\n }\n </select>\n </div>\n\n <!-- Entity Document selector (only shown when 2+ docs for selected entity) -->\n @if (EntityDocOptions.length > 1) {\n <div class=\"form-group\">\n <label>Document Template</label>\n <select [ngModel]=\"Config.EntityDocumentID\"\n (ngModelChange)=\"Config.EntityDocumentID = $event; OnFieldChanged()\">\n @for (doc of EntityDocOptions; track doc.ID) {\n <option [value]=\"doc.ID\">{{ doc.Name }}</option>\n }\n </select>\n </div>\n }\n\n <!-- Multi-entity source selector -->\n @if (HasMultiSourceOptions) {\n <div class=\"form-group multi-source\">\n <button type=\"button\" class=\"multi-source-toggle\" (click)=\"ToggleMultiSource()\">\n <i class=\"fa-solid\" [class.fa-chevron-down]=\"!ShowMultiSource\" [class.fa-chevron-up]=\"ShowMultiSource\"></i>\n Multiple sources\n @if (IsMultiEntity) {\n <span class=\"multi-source-badge\">{{ SelectedDocIDs.length }}</span>\n }\n </button>\n @if (ShowMultiSource) {\n <div class=\"multi-source-list\">\n <div class=\"multi-source-hint\">Select 2+ documents to cluster them together. All must use the same embedding model.</div>\n @for (doc of AllEntityDocOptions; track doc.ID) {\n <label class=\"multi-source-item\">\n <input type=\"checkbox\" [checked]=\"IsDocSelected(doc.ID)\" (change)=\"ToggleDoc(doc.ID)\">\n <span class=\"multi-source-name\">{{ doc.Name }}</span>\n @if (doc.EntityName) { <span class=\"multi-source-entity\">{{ doc.EntityName }}</span> }\n </label>\n }\n </div>\n }\n </div>\n }\n\n <!-- Projection (2D / 3D) + Color-by toggles -->\n <div class=\"form-row toggle-row\">\n @if (ShowDimensionToggle) {\n <div class=\"form-group\">\n <label>Projection</label>\n <div class=\"seg-toggle\" role=\"group\">\n <button type=\"button\" class=\"seg-btn\" [class.active]=\"(Config.Dimensions ?? 2) === 2\" (click)=\"SetDimensions(2)\">2D</button>\n <button type=\"button\" class=\"seg-btn\" [class.active]=\"Config.Dimensions === 3\" (click)=\"SetDimensions(3)\">3D</button>\n </div>\n </div>\n }\n <div class=\"form-group\">\n <label>Color by</label>\n <div class=\"seg-toggle\" role=\"group\">\n <button type=\"button\" class=\"seg-btn\" [class.active]=\"(Config.ColorBy ?? 'cluster') === 'cluster'\" (click)=\"SetColorBy('cluster')\">Cluster</button>\n <button type=\"button\" class=\"seg-btn\" [class.active]=\"Config.ColorBy === 'entity'\" (click)=\"SetColorBy('entity')\">Entity</button>\n </div>\n </div>\n </div>\n\n <!-- Filter -->\n <div class=\"form-group\">\n <label>Filter (optional)</label>\n <input type=\"text\"\n [(ngModel)]=\"Config.Filter\"\n (ngModelChange)=\"OnFieldChanged()\"\n placeholder=\"e.g. Status = 'Active'\">\n </div>\n\n <!-- Algorithm + K/Epsilon row -->\n <div class=\"form-row\">\n @if (ShowAlgorithmPicker) {\n <div class=\"form-group\">\n <label>Algorithm</label>\n <select [ngModel]=\"Config.Algorithm\"\n (ngModelChange)=\"OnAlgorithmChanged($event)\">\n @for (alg of AlgorithmOptions; track TrackAlgorithmBy($index, alg)) {\n <option [value]=\"alg.Value\">{{ alg.Label }}</option>\n }\n </select>\n </div>\n }\n\n @if (IsKMeans) {\n <div class=\"form-group\">\n <label>Clusters (K): {{ Config.K }}</label>\n <input type=\"range\"\n [ngModel]=\"Config.K\"\n (ngModelChange)=\"Config.K = $event; OnFieldChanged()\"\n [min]=\"2\"\n [max]=\"20\"\n [step]=\"1\">\n </div>\n } @else {\n <div class=\"form-group\">\n <label>Epsilon: {{ Config.Epsilon }}</label>\n <input type=\"range\"\n [ngModel]=\"Config.Epsilon\"\n (ngModelChange)=\"Config.Epsilon = $event; OnFieldChanged()\"\n [min]=\"0.05\"\n [max]=\"0.95\"\n [step]=\"0.05\">\n </div>\n }\n </div>\n\n <!-- DBSCAN MinPoints row (only visible for DBSCAN) -->\n @if (!IsKMeans) {\n <div class=\"form-group\">\n <label>Min Points: {{ Config.MinPoints }}</label>\n <input type=\"range\"\n [ngModel]=\"Config.MinPoints\"\n (ngModelChange)=\"Config.MinPoints = $event; OnFieldChanged()\"\n [min]=\"2\"\n [max]=\"20\"\n [step]=\"1\">\n </div>\n }\n\n <!-- Distance metric + Max records row -->\n <div class=\"form-row\">\n @if (ShowMetricPicker) {\n <div class=\"form-group\">\n <label>Distance Metric</label>\n <select [ngModel]=\"Config.DistanceMetric\"\n (ngModelChange)=\"OnMetricChanged($event)\">\n @for (metric of MetricOptions; track TrackMetricBy($index, metric)) {\n <option [value]=\"metric.Value\">{{ metric.Label }}</option>\n }\n </select>\n </div>\n }\n <div class=\"form-group\">\n <label>Max Records</label>\n <input type=\"number\"\n [(ngModel)]=\"Config.MaxRecords\"\n (ngModelChange)=\"OnFieldChanged()\"\n [min]=\"10\"\n [max]=\"5000\">\n </div>\n </div>\n\n <!-- Run + Save action row -->\n <div class=\"action-row\">\n <button class=\"run-btn\"\n [disabled]=\"!CanRun\"\n (click)=\"OnRun()\">\n @if (IsRunning) {\n <i class=\"fa-solid fa-spinner fa-spin\"></i> Running...\n } @else {\n <i class=\"fa-solid fa-play\"></i> Run Clustering\n }\n </button>\n @if (CanSave) {\n <button class=\"save-btn\" (click)=\"OnSave()\" title=\"Save this visualization\">\n <i class=\"fa-solid fa-bookmark\"></i> Save\n </button>\n }\n </div>\n\n <!-- Metrics display -->\n @if (Metrics && Metrics.RecordCount > 0) {\n <div class=\"metrics-section\">\n <div class=\"metric-item\">\n <span class=\"metric-label\">Silhouette</span>\n <span class=\"metric-value\"\n [class.good]=\"IsSilhouetteGood(Metrics.SilhouetteScore)\">\n {{ FormatSilhouette(Metrics.SilhouetteScore) }}\n </span>\n </div>\n <div class=\"metric-item\">\n <span class=\"metric-label\">Clusters</span>\n <span class=\"metric-value\">{{ Metrics.ClusterCount }}</span>\n </div>\n <div class=\"metric-item\">\n <span class=\"metric-label\">Records</span>\n <span class=\"metric-value\">{{ Metrics.RecordCount }}</span>\n </div>\n <div class=\"metric-item\">\n <span class=\"metric-label\">Time</span>\n <span class=\"metric-value\">{{ FormatTime(Metrics.ComputationTimeMs) }}</span>\n </div>\n @if (Metrics.OutlierCount > 0) {\n <div class=\"metric-item\">\n <span class=\"metric-label\">Outliers</span>\n <span class=\"metric-value\">{{ Metrics.OutlierCount }}</span>\n </div>\n }\n </div>\n }\n\n }\n</div>\n", styles: [".config-panel {\n position: absolute;\n width: 300px;\n background: color-mix(in srgb, var(--mj-bg-surface-elevated) 95%, transparent);\n backdrop-filter: blur(20px);\n -webkit-backdrop-filter: blur(20px);\n border: 1px solid var(--mj-border-default);\n border-radius: 14px;\n padding: 20px;\n box-shadow: 0 20px 60px color-mix(in srgb, var(--mj-bg-page) 40%, transparent);\n z-index: 10;\n}\n\n.panel-header {\n font-size: 0.8rem;\n font-weight: 600;\n margin: 0 0 16px 0;\n display: flex;\n align-items: center;\n gap: 8px;\n color: var(--mj-text-primary);\n cursor: move;\n user-select: none;\n}\n\n.panel-header i:first-child {\n color: var(--mj-brand-primary);\n}\n\n.header-actions {\n margin-left: auto;\n display: flex;\n align-items: center;\n gap: 8px;\n}\n\n.header-actions i {\n color: var(--mj-text-muted);\n font-size: 0.7rem;\n cursor: pointer;\n transition: color 0.15s;\n}\n\n.header-actions i:hover {\n color: var(--mj-text-primary);\n}\n\n.drag-handle {\n color: var(--mj-text-muted);\n font-size: 0.7rem;\n}\n\n/* Form elements */\n.form-group {\n margin-bottom: 14px;\n}\n\n.form-group label {\n display: block;\n font-size: 0.7rem;\n font-weight: 500;\n color: var(--mj-text-secondary);\n margin-bottom: 5px;\n text-transform: uppercase;\n letter-spacing: 0.5px;\n}\n\n.form-group select,\n.form-group input[type=\"text\"],\n.form-group input[type=\"number\"] {\n width: 100%;\n padding: 8px 12px;\n background: var(--mj-bg-surface-sunken);\n border: 1px solid var(--mj-border-default);\n border-radius: 7px;\n color: var(--mj-text-primary);\n font-size: 0.82rem;\n outline: none;\n transition: border 0.15s;\n}\n\n.form-group select:focus,\n.form-group input[type=\"text\"]:focus,\n.form-group input[type=\"number\"]:focus {\n border-color: var(--mj-brand-primary);\n}\n\n.form-group input[type=\"range\"] {\n width: 100%;\n accent-color: var(--mj-brand-primary);\n}\n\n.form-row {\n display: flex;\n gap: 10px;\n}\n\n.form-row .form-group {\n flex: 1;\n}\n\n/* Run button */\n.action-row {\n display: flex;\n gap: 8px;\n margin-top: 4px;\n}\n\n.run-btn {\n flex: 1;\n padding: 10px;\n background: var(--mj-brand-primary);\n border: none;\n border-radius: 8px;\n color: var(--mj-text-inverse);\n font-size: 0.82rem;\n font-weight: 600;\n cursor: pointer;\n display: flex;\n align-items: center;\n justify-content: center;\n gap: 8px;\n transition: background 0.15s;\n}\n\n.run-btn:hover:not(:disabled) {\n background: var(--mj-brand-primary-hover);\n}\n\n.run-btn:disabled {\n opacity: 0.6;\n cursor: not-allowed;\n}\n\n.save-btn {\n flex: 0 0 auto;\n padding: 10px 14px;\n background: var(--mj-bg-surface);\n border: 1px solid var(--mj-border-default);\n border-radius: 8px;\n color: var(--mj-text-secondary);\n font-size: 0.82rem;\n font-weight: 600;\n cursor: pointer;\n display: flex;\n align-items: center;\n gap: 6px;\n transition: all 0.15s;\n}\n\n.save-btn:hover {\n border-color: var(--mj-brand-primary);\n color: var(--mj-brand-primary);\n background: color-mix(in srgb, var(--mj-brand-primary) 8%, var(--mj-bg-surface));\n}\n\n/* Segmented toggles (Projection 2D/3D, Color-by) */\n.toggle-row .form-group { min-width: 0; }\n\n.seg-toggle {\n display: inline-flex;\n gap: 2px;\n padding: 3px;\n background: var(--mj-bg-surface-sunken);\n border: 1px solid var(--mj-border-default);\n border-radius: 8px;\n width: 100%;\n}\n\n.seg-btn {\n flex: 1;\n padding: 5px 8px;\n border: none;\n background: transparent;\n color: var(--mj-text-secondary);\n font-size: 0.75rem;\n font-weight: 600;\n border-radius: 6px;\n cursor: pointer;\n transition: background 0.15s, color 0.15s;\n}\n\n.seg-btn:hover { color: var(--mj-text-primary); }\n\n.seg-btn.active {\n background: var(--mj-bg-surface);\n color: var(--mj-brand-primary);\n box-shadow: 0 1px 2px color-mix(in srgb, var(--mj-text-primary) 12%, transparent);\n}\n\n/* Multi-entity source selector */\n.multi-source-toggle {\n display: flex;\n align-items: center;\n gap: 8px;\n width: 100%;\n padding: 8px 10px;\n background: var(--mj-bg-surface-sunken);\n border: 1px solid var(--mj-border-default);\n border-radius: 8px;\n color: var(--mj-text-secondary);\n font-size: 0.78rem;\n font-weight: 600;\n cursor: pointer;\n}\n\n.multi-source-toggle:hover { color: var(--mj-text-primary); }\n\n.multi-source-badge {\n margin-left: auto;\n background: var(--mj-brand-primary);\n color: var(--mj-text-inverse);\n border-radius: 10px;\n padding: 1px 8px;\n font-size: 0.7rem;\n}\n\n.multi-source-list {\n margin-top: 6px;\n border: 1px solid var(--mj-border-default);\n border-radius: 8px;\n padding: 8px;\n max-height: 180px;\n overflow-y: auto;\n background: var(--mj-bg-surface);\n}\n\n.multi-source-hint {\n font-size: 0.68rem;\n color: var(--mj-text-muted);\n margin-bottom: 6px;\n line-height: 1.3;\n}\n\n.multi-source-item {\n display: flex;\n align-items: center;\n gap: 8px;\n padding: 5px 4px;\n cursor: pointer;\n font-size: 0.78rem;\n color: var(--mj-text-primary);\n}\n\n.multi-source-item:hover { background: var(--mj-bg-surface-hover); border-radius: 6px; }\n\n.multi-source-entity {\n margin-left: auto;\n font-size: 0.68rem;\n color: var(--mj-text-muted);\n}\n\n/* Metrics */\n.metrics-section {\n display: grid;\n grid-template-columns: 1fr 1fr;\n gap: 8px;\n margin-top: 14px;\n padding-top: 14px;\n border-top: 1px solid var(--mj-border-default);\n}\n\n.metric-item {\n display: flex;\n flex-direction: column;\n gap: 2px;\n}\n\n.metric-label {\n font-size: 0.65rem;\n text-transform: uppercase;\n letter-spacing: 0.5px;\n color: var(--mj-text-muted);\n}\n\n.metric-value {\n font-size: 0.85rem;\n font-weight: 600;\n color: var(--mj-text-primary);\n}\n\n.metric-value.good {\n color: var(--mj-status-success);\n}\n\n\n"] }]
886
+ args: [{ standalone: false, selector: 'mj-cluster-config-panel', template: "<div #panelElement\n class=\"config-panel\"\n [style.top.px]=\"PanelTop\"\n [style.left.px]=\"PanelLeft\">\n\n <!-- Draggable header -->\n <h3 class=\"panel-header\"\n (mousedown)=\"OnHeaderMouseDown($event)\">\n <i class=\"fa-solid fa-sliders\"></i>\n Configuration\n <span class=\"header-actions\">\n <i class=\"fa-solid\"\n [class.fa-chevron-up]=\"IsExpanded\"\n [class.fa-chevron-down]=\"!IsExpanded\"\n (click)=\"ToggleCollapse(); $event.stopPropagation()\"\n title=\"Toggle panel\"></i>\n <i class=\"fa-solid fa-grip-dots-vertical drag-handle\"></i>\n </span>\n </h3>\n\n @if (IsExpanded) {\n <!-- Entity picker -->\n <div class=\"form-group\">\n <label>Entity</label>\n <select [ngModel]=\"Config.EntityName\"\n (ngModelChange)=\"OnEntityChanged($event)\">\n @for (entity of FilteredEntityOptions; track TrackEntityBy($index, entity)) {\n <option [value]=\"entity.Name\">{{ entity.Name }}</option>\n }\n </select>\n </div>\n\n <!-- Entity Document selector (only shown when 2+ docs for selected entity) -->\n @if (EntityDocOptions.length > 1) {\n <div class=\"form-group\">\n <label>Document Template</label>\n <select [ngModel]=\"Config.EntityDocumentID\"\n (ngModelChange)=\"Config.EntityDocumentID = $event; OnFieldChanged()\">\n @for (doc of EntityDocOptions; track doc.ID) {\n <option [value]=\"doc.ID\">{{ doc.Name }}</option>\n }\n </select>\n </div>\n }\n\n <!-- Multi-entity source selector -->\n @if (HasMultiSourceOptions) {\n <div class=\"form-group multi-source\">\n <mj-accordion-panel\n Size=\"sm\"\n [FlushBody]=\"true\"\n [Expanded]=\"ShowMultiSource\"\n (ExpandedChange)=\"OnMultiSourceExpandedChange($event)\">\n <ng-template mjAccordionTitle>\n Multiple sources\n @if (IsMultiEntity) {\n <span class=\"mj-accordion-badge mj-accordion-badge--right\">{{ SelectedDocIDs.length }}</span>\n }\n </ng-template>\n <ng-template mjAccordionBody>\n <div class=\"multi-source-list\">\n <div class=\"multi-source-hint\">Select 2+ documents to cluster them together. All must use the same embedding model.</div>\n @for (doc of AllEntityDocOptions; track doc.ID) {\n <label class=\"multi-source-item\">\n <input type=\"checkbox\" [checked]=\"IsDocSelected(doc.ID)\" (change)=\"ToggleDoc(doc.ID)\">\n <span class=\"multi-source-name\">{{ doc.Name }}</span>\n @if (doc.EntityName) { <span class=\"multi-source-entity\">{{ doc.EntityName }}</span> }\n </label>\n }\n </div>\n </ng-template>\n </mj-accordion-panel>\n </div>\n }\n\n <!-- Projection (2D / 3D) + Color-by toggles -->\n <div class=\"form-row toggle-row\">\n @if (ShowDimensionToggle) {\n <div class=\"form-group\">\n <label>Projection</label>\n <div class=\"seg-toggle\" role=\"group\">\n <button type=\"button\" class=\"seg-btn\" [class.active]=\"(Config.Dimensions ?? 2) === 2\" (click)=\"SetDimensions(2)\">2D</button>\n <button type=\"button\" class=\"seg-btn\" [class.active]=\"Config.Dimensions === 3\" (click)=\"SetDimensions(3)\">3D</button>\n </div>\n </div>\n }\n <div class=\"form-group\">\n <label>Color by</label>\n <div class=\"seg-toggle\" role=\"group\">\n <button type=\"button\" class=\"seg-btn\" [class.active]=\"(Config.ColorBy ?? 'cluster') === 'cluster'\" (click)=\"SetColorBy('cluster')\">Cluster</button>\n <button type=\"button\" class=\"seg-btn\" [class.active]=\"Config.ColorBy === 'entity'\" (click)=\"SetColorBy('entity')\">Entity</button>\n </div>\n </div>\n </div>\n\n <!-- Filter -->\n <div class=\"form-group\">\n <label>Filter (optional)</label>\n <input type=\"text\"\n [(ngModel)]=\"Config.Filter\"\n (ngModelChange)=\"OnFieldChanged()\"\n placeholder=\"e.g. Status = 'Active'\">\n </div>\n\n <!-- Algorithm + K/Epsilon row -->\n <div class=\"form-row\">\n @if (ShowAlgorithmPicker) {\n <div class=\"form-group\">\n <label>Algorithm</label>\n <select [ngModel]=\"Config.Algorithm\"\n (ngModelChange)=\"OnAlgorithmChanged($event)\">\n @for (alg of AlgorithmOptions; track TrackAlgorithmBy($index, alg)) {\n <option [value]=\"alg.Value\">{{ alg.Label }}</option>\n }\n </select>\n </div>\n }\n\n @if (IsKMeans) {\n <div class=\"form-group\">\n <label>Clusters (K): {{ Config.K }}</label>\n <input type=\"range\"\n [ngModel]=\"Config.K\"\n (ngModelChange)=\"Config.K = $event; OnFieldChanged()\"\n [min]=\"2\"\n [max]=\"20\"\n [step]=\"1\">\n </div>\n } @else {\n <div class=\"form-group\">\n <label>Epsilon: {{ Config.Epsilon }}</label>\n <input type=\"range\"\n [ngModel]=\"Config.Epsilon\"\n (ngModelChange)=\"Config.Epsilon = $event; OnFieldChanged()\"\n [min]=\"0.05\"\n [max]=\"0.95\"\n [step]=\"0.05\">\n </div>\n }\n </div>\n\n <!-- DBSCAN MinPoints row (only visible for DBSCAN) -->\n @if (!IsKMeans) {\n <div class=\"form-group\">\n <label>Min Points: {{ Config.MinPoints }}</label>\n <input type=\"range\"\n [ngModel]=\"Config.MinPoints\"\n (ngModelChange)=\"Config.MinPoints = $event; OnFieldChanged()\"\n [min]=\"2\"\n [max]=\"20\"\n [step]=\"1\">\n </div>\n }\n\n <!-- Distance metric + Max records row -->\n <div class=\"form-row\">\n @if (ShowMetricPicker) {\n <div class=\"form-group\">\n <label>Distance Metric</label>\n <select [ngModel]=\"Config.DistanceMetric\"\n (ngModelChange)=\"OnMetricChanged($event)\">\n @for (metric of MetricOptions; track TrackMetricBy($index, metric)) {\n <option [value]=\"metric.Value\">{{ metric.Label }}</option>\n }\n </select>\n </div>\n }\n <div class=\"form-group\">\n <label>Max Records</label>\n <input type=\"number\"\n [(ngModel)]=\"Config.MaxRecords\"\n (ngModelChange)=\"OnFieldChanged()\"\n [min]=\"10\"\n [max]=\"5000\">\n </div>\n </div>\n\n <!-- Run + Save action row -->\n <div class=\"action-row\">\n <button class=\"run-btn\"\n [disabled]=\"!CanRun\"\n (click)=\"OnRun()\">\n @if (IsRunning) {\n <i class=\"fa-solid fa-spinner fa-spin\"></i> Running...\n } @else {\n <i class=\"fa-solid fa-play\"></i> Run Clustering\n }\n </button>\n @if (CanSave) {\n <button class=\"save-btn\" (click)=\"OnSave()\" title=\"Save this visualization\">\n <i class=\"fa-solid fa-bookmark\"></i> Save\n </button>\n }\n </div>\n\n <!-- Metrics display -->\n @if (Metrics && Metrics.RecordCount > 0) {\n <div class=\"metrics-section\">\n <div class=\"metric-item\">\n <span class=\"metric-label\">Silhouette</span>\n <span class=\"metric-value\"\n [class.good]=\"IsSilhouetteGood(Metrics.SilhouetteScore)\">\n {{ FormatSilhouette(Metrics.SilhouetteScore) }}\n </span>\n </div>\n <div class=\"metric-item\">\n <span class=\"metric-label\">Clusters</span>\n <span class=\"metric-value\">{{ Metrics.ClusterCount }}</span>\n </div>\n <div class=\"metric-item\">\n <span class=\"metric-label\">Records</span>\n <span class=\"metric-value\">{{ Metrics.RecordCount }}</span>\n </div>\n <div class=\"metric-item\">\n <span class=\"metric-label\">Time</span>\n <span class=\"metric-value\">{{ FormatTime(Metrics.ComputationTimeMs) }}</span>\n </div>\n @if (Metrics.OutlierCount > 0) {\n <div class=\"metric-item\">\n <span class=\"metric-label\">Outliers</span>\n <span class=\"metric-value\">{{ Metrics.OutlierCount }}</span>\n </div>\n }\n </div>\n }\n\n }\n</div>\n", styles: [".config-panel {\n position: absolute;\n width: 300px;\n background: color-mix(in srgb, var(--mj-bg-surface-elevated) 95%, transparent);\n backdrop-filter: blur(20px);\n -webkit-backdrop-filter: blur(20px);\n border: 1px solid var(--mj-border-default);\n border-radius: 14px;\n padding: 20px;\n box-shadow: 0 20px 60px color-mix(in srgb, var(--mj-bg-page) 40%, transparent);\n z-index: 10;\n}\n\n.panel-header {\n font-size: 0.8rem;\n font-weight: 600;\n margin: 0 0 16px 0;\n display: flex;\n align-items: center;\n gap: 8px;\n color: var(--mj-text-primary);\n cursor: move;\n user-select: none;\n}\n\n.panel-header i:first-child {\n color: var(--mj-brand-primary);\n}\n\n.header-actions {\n margin-left: auto;\n display: flex;\n align-items: center;\n gap: 8px;\n}\n\n.header-actions i {\n color: var(--mj-text-muted);\n font-size: 0.7rem;\n cursor: pointer;\n transition: color 0.15s;\n}\n\n.header-actions i:hover {\n color: var(--mj-text-primary);\n}\n\n.drag-handle {\n color: var(--mj-text-muted);\n font-size: 0.7rem;\n}\n\n/* Form elements */\n.form-group {\n margin-bottom: 14px;\n}\n\n.form-group label {\n display: block;\n font-size: 0.7rem;\n font-weight: 500;\n color: var(--mj-text-secondary);\n margin-bottom: 5px;\n text-transform: uppercase;\n letter-spacing: 0.5px;\n}\n\n.form-group select,\n.form-group input[type=\"text\"],\n.form-group input[type=\"number\"] {\n width: 100%;\n padding: 8px 12px;\n background: var(--mj-bg-surface-sunken);\n border: 1px solid var(--mj-border-default);\n border-radius: 7px;\n color: var(--mj-text-primary);\n font-size: 0.82rem;\n outline: none;\n transition: border 0.15s;\n}\n\n.form-group select:focus,\n.form-group input[type=\"text\"]:focus,\n.form-group input[type=\"number\"]:focus {\n border-color: var(--mj-brand-primary);\n}\n\n.form-group input[type=\"range\"] {\n width: 100%;\n accent-color: var(--mj-brand-primary);\n}\n\n.form-row {\n display: flex;\n gap: 10px;\n}\n\n.form-row .form-group {\n flex: 1;\n}\n\n/* Run button */\n.action-row {\n display: flex;\n gap: 8px;\n margin-top: 4px;\n}\n\n.run-btn {\n flex: 1;\n padding: 10px;\n background: var(--mj-brand-primary);\n border: none;\n border-radius: 8px;\n color: var(--mj-text-inverse);\n font-size: 0.82rem;\n font-weight: 600;\n cursor: pointer;\n display: flex;\n align-items: center;\n justify-content: center;\n gap: 8px;\n transition: background 0.15s;\n}\n\n.run-btn:hover:not(:disabled) {\n background: var(--mj-brand-primary-hover);\n}\n\n.run-btn:disabled {\n opacity: 0.6;\n cursor: not-allowed;\n}\n\n.save-btn {\n flex: 0 0 auto;\n padding: 10px 14px;\n background: var(--mj-bg-surface);\n border: 1px solid var(--mj-border-default);\n border-radius: 8px;\n color: var(--mj-text-secondary);\n font-size: 0.82rem;\n font-weight: 600;\n cursor: pointer;\n display: flex;\n align-items: center;\n gap: 6px;\n transition: all 0.15s;\n}\n\n.save-btn:hover {\n border-color: var(--mj-brand-primary);\n color: var(--mj-brand-primary);\n background: color-mix(in srgb, var(--mj-brand-primary) 8%, var(--mj-bg-surface));\n}\n\n/* Segmented toggles (Projection 2D/3D, Color-by) */\n.toggle-row .form-group { min-width: 0; }\n\n.seg-toggle {\n display: inline-flex;\n gap: 2px;\n padding: 3px;\n background: var(--mj-bg-surface-sunken);\n border: 1px solid var(--mj-border-default);\n border-radius: 8px;\n width: 100%;\n}\n\n.seg-btn {\n flex: 1;\n padding: 5px 8px;\n border: none;\n background: transparent;\n color: var(--mj-text-secondary);\n font-size: 0.75rem;\n font-weight: 600;\n border-radius: 6px;\n cursor: pointer;\n transition: background 0.15s, color 0.15s;\n}\n\n.seg-btn:hover { color: var(--mj-text-primary); }\n\n.seg-btn.active {\n background: var(--mj-bg-surface);\n color: var(--mj-brand-primary);\n box-shadow: 0 1px 2px color-mix(in srgb, var(--mj-text-primary) 12%, transparent);\n}\n\n/* Multi-entity source selector */\n/* .multi-source-toggle / .multi-source-badge removed: the bespoke disclosure\n toggle + count pill were replaced by <mj-accordion-panel> (header/chevron)\n and the standard .mj-accordion-badge--right. */\n\n.multi-source-list {\n border: 1px solid var(--mj-border-default);\n border-radius: 8px;\n padding: 8px;\n max-height: 180px;\n overflow-y: auto;\n background: var(--mj-bg-surface);\n}\n\n.multi-source-hint {\n font-size: 0.68rem;\n color: var(--mj-text-muted);\n margin-bottom: 6px;\n line-height: 1.3;\n}\n\n.multi-source-item {\n display: flex;\n align-items: center;\n gap: 8px;\n padding: 5px 4px;\n cursor: pointer;\n font-size: 0.78rem;\n color: var(--mj-text-primary);\n}\n\n.multi-source-item:hover { background: var(--mj-bg-surface-hover); border-radius: 6px; }\n\n.multi-source-entity {\n margin-left: auto;\n font-size: 0.68rem;\n color: var(--mj-text-muted);\n}\n\n/* Metrics */\n.metrics-section {\n display: grid;\n grid-template-columns: 1fr 1fr;\n gap: 8px;\n margin-top: 14px;\n padding-top: 14px;\n border-top: 1px solid var(--mj-border-default);\n}\n\n.metric-item {\n display: flex;\n flex-direction: column;\n gap: 2px;\n}\n\n.metric-label {\n font-size: 0.65rem;\n text-transform: uppercase;\n letter-spacing: 0.5px;\n color: var(--mj-text-muted);\n}\n\n.metric-value {\n font-size: 0.85rem;\n font-weight: 600;\n color: var(--mj-text-primary);\n}\n\n.metric-value.good {\n color: var(--mj-status-success);\n}\n\n\n"] }]
886
887
  }], null, { IsRunning: [{
887
888
  type: Input
888
889
  }], Metrics: [{