@memberjunction/ng-clustering 5.43.0 → 5.45.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/lib/cluster-config-panel.component.d.ts +2 -2
- package/dist/lib/cluster-config-panel.component.d.ts.map +1 -1
- package/dist/lib/cluster-config-panel.component.dom.test.d.ts +2 -0
- package/dist/lib/cluster-config-panel.component.dom.test.d.ts.map +1 -0
- package/dist/lib/cluster-config-panel.component.dom.test.js +147 -0
- package/dist/lib/cluster-config-panel.component.dom.test.js.map +1 -0
- package/dist/lib/cluster-config-panel.component.js +52 -51
- package/dist/lib/cluster-config-panel.component.js.map +1 -1
- package/dist/lib/cluster-scatter.component.dom.test.d.ts +2 -0
- package/dist/lib/cluster-scatter.component.dom.test.d.ts.map +1 -0
- package/dist/lib/cluster-scatter.component.dom.test.js +102 -0
- package/dist/lib/cluster-scatter.component.dom.test.js.map +1 -0
- package/dist/lib/cluster-scatter.component.js +89 -95
- package/dist/lib/cluster-scatter.component.js.map +1 -1
- package/dist/lib/clustering.module.d.ts +2 -1
- package/dist/lib/clustering.module.d.ts.map +1 -1
- package/dist/lib/clustering.module.js +9 -2
- package/dist/lib/clustering.module.js.map +1 -1
- package/dist/lib/view-type/cluster-view-prop-sheet.component.dom.test.d.ts +2 -0
- package/dist/lib/view-type/cluster-view-prop-sheet.component.dom.test.d.ts.map +1 -0
- package/dist/lib/view-type/cluster-view-prop-sheet.component.dom.test.js +64 -0
- package/dist/lib/view-type/cluster-view-prop-sheet.component.dom.test.js.map +1 -0
- package/package.json +12 -10
|
@@ -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
|
-
/**
|
|
241
|
-
|
|
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 @@
|
|
|
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
|
|
70
|
-
i0.ɵɵelementStart(0, "span",
|
|
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(
|
|
75
|
+
const ctx_r2 = i0.ɵɵnextContext(4);
|
|
75
76
|
i0.ɵɵadvance();
|
|
76
77
|
i0.ɵɵtextInterpolate(ctx_r2.SelectedDocIDs.length);
|
|
77
78
|
} }
|
|
78
|
-
function
|
|
79
|
-
i0.ɵɵ
|
|
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
|
|
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",
|
|
90
|
-
i0.ɵɵlistener("change", function
|
|
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",
|
|
101
|
+
i0.ɵɵelementStart(2, "span", 29);
|
|
93
102
|
i0.ɵɵtext(3);
|
|
94
103
|
i0.ɵɵelementEnd();
|
|
95
|
-
i0.ɵɵconditionalCreate(4,
|
|
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
|
|
108
|
-
i0.ɵɵelementStart(0, "div",
|
|
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,
|
|
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, "
|
|
121
|
-
i0.ɵɵlistener("
|
|
122
|
-
i0.ɵɵ
|
|
123
|
-
i0.ɵɵ
|
|
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.ɵɵ
|
|
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",
|
|
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",
|
|
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",
|
|
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",
|
|
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",
|
|
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",
|
|
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",
|
|
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",
|
|
272
|
+
i0.ɵɵelementStart(0, "div", 36)(1, "span", 37);
|
|
272
273
|
i0.ɵɵtext(2, "Outliers");
|
|
273
274
|
i0.ɵɵelementEnd();
|
|
274
|
-
i0.ɵɵelementStart(3, "span",
|
|
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",
|
|
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",
|
|
287
|
+
i0.ɵɵelementStart(4, "span", 38);
|
|
287
288
|
i0.ɵɵtext(5);
|
|
288
289
|
i0.ɵɵelementEnd()();
|
|
289
|
-
i0.ɵɵelementStart(6, "div",
|
|
290
|
+
i0.ɵɵelementStart(6, "div", 36)(7, "span", 37);
|
|
290
291
|
i0.ɵɵtext(8, "Clusters");
|
|
291
292
|
i0.ɵɵelementEnd();
|
|
292
|
-
i0.ɵɵelementStart(9, "span",
|
|
293
|
+
i0.ɵɵelementStart(9, "span", 38);
|
|
293
294
|
i0.ɵɵtext(10);
|
|
294
295
|
i0.ɵɵelementEnd()();
|
|
295
|
-
i0.ɵɵelementStart(11, "div",
|
|
296
|
+
i0.ɵɵelementStart(11, "div", 36)(12, "span", 37);
|
|
296
297
|
i0.ɵɵtext(13, "Records");
|
|
297
298
|
i0.ɵɵelementEnd();
|
|
298
|
-
i0.ɵɵelementStart(14, "span",
|
|
299
|
+
i0.ɵɵelementStart(14, "span", 38);
|
|
299
300
|
i0.ɵɵtext(15);
|
|
300
301
|
i0.ɵɵelementEnd()();
|
|
301
|
-
i0.ɵɵelementStart(16, "div",
|
|
302
|
+
i0.ɵɵelementStart(16, "div", 36)(17, "span", 37);
|
|
302
303
|
i0.ɵɵtext(18, "Time");
|
|
303
304
|
i0.ɵɵelementEnd();
|
|
304
|
-
i0.ɵɵelementStart(19, "span",
|
|
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",
|
|
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,
|
|
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
|
-
/**
|
|
711
|
-
|
|
712
|
-
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"], ["
|
|
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
|
|
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: [{
|