@memberjunction/ng-dashboards 5.23.0 → 5.24.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/AI/components/autotagging/autotagging-pipeline-resource.component.d.ts +677 -5
- package/dist/AI/components/autotagging/autotagging-pipeline-resource.component.d.ts.map +1 -1
- package/dist/AI/components/autotagging/autotagging-pipeline-resource.component.js +6879 -1873
- package/dist/AI/components/autotagging/autotagging-pipeline-resource.component.js.map +1 -1
- package/dist/AI/components/duplicates/duplicate-detection-resource.component.d.ts +46 -1
- package/dist/AI/components/duplicates/duplicate-detection-resource.component.d.ts.map +1 -1
- package/dist/AI/components/duplicates/duplicate-detection-resource.component.js +758 -491
- package/dist/AI/components/duplicates/duplicate-detection-resource.component.js.map +1 -1
- package/dist/AI/components/vectors/vector-management-resource.component.d.ts +19 -0
- package/dist/AI/components/vectors/vector-management-resource.component.d.ts.map +1 -1
- package/dist/AI/components/vectors/vector-management-resource.component.js +410 -208
- package/dist/AI/components/vectors/vector-management-resource.component.js.map +1 -1
- package/dist/DataExplorer/data-explorer-dashboard.component.d.ts.map +1 -1
- package/dist/DataExplorer/data-explorer-dashboard.component.js +17 -17
- package/dist/DataExplorer/data-explorer-dashboard.component.js.map +1 -1
- package/dist/Integration/components/activity/activity.component.d.ts.map +1 -1
- package/dist/Integration/components/activity/activity.component.js +1 -0
- package/dist/Integration/components/activity/activity.component.js.map +1 -1
- package/dist/Integration/components/connections/connections.component.d.ts.map +1 -1
- package/dist/Integration/components/connections/connections.component.js +1 -0
- package/dist/Integration/components/connections/connections.component.js.map +1 -1
- package/dist/Integration/components/mapping-workspace/mapping-workspace.component.d.ts.map +1 -1
- package/dist/Integration/components/mapping-workspace/mapping-workspace.component.js +1 -0
- package/dist/Integration/components/mapping-workspace/mapping-workspace.component.js.map +1 -1
- package/dist/Integration/components/overview/overview.component.d.ts.map +1 -1
- package/dist/Integration/components/overview/overview.component.js +1 -0
- package/dist/Integration/components/overview/overview.component.js.map +1 -1
- package/dist/Integration/components/pipelines/pipelines.component.d.ts.map +1 -1
- package/dist/Integration/components/pipelines/pipelines.component.js +1 -0
- package/dist/Integration/components/pipelines/pipelines.component.js.map +1 -1
- package/dist/Integration/components/schedules/schedules.component.d.ts.map +1 -1
- package/dist/Integration/components/schedules/schedules.component.js +1 -0
- package/dist/Integration/components/schedules/schedules.component.js.map +1 -1
- package/dist/KnowledgeHub/components/analytics/analytics-resource.component.d.ts +411 -0
- package/dist/KnowledgeHub/components/analytics/analytics-resource.component.d.ts.map +1 -0
- package/dist/KnowledgeHub/components/analytics/analytics-resource.component.js +4266 -0
- package/dist/KnowledgeHub/components/analytics/analytics-resource.component.js.map +1 -0
- package/dist/KnowledgeHub/components/clusters/cluster-visualization-resource.component.d.ts +35 -1
- package/dist/KnowledgeHub/components/clusters/cluster-visualization-resource.component.d.ts.map +1 -1
- package/dist/KnowledgeHub/components/clusters/cluster-visualization-resource.component.js +186 -13
- package/dist/KnowledgeHub/components/clusters/cluster-visualization-resource.component.js.map +1 -1
- package/dist/KnowledgeHub/components/config/knowledge-config-resource.component.d.ts +1 -0
- package/dist/KnowledgeHub/components/config/knowledge-config-resource.component.d.ts.map +1 -1
- package/dist/KnowledgeHub/components/config/knowledge-config-resource.component.js +188 -165
- package/dist/KnowledgeHub/components/config/knowledge-config-resource.component.js.map +1 -1
- package/dist/KnowledgeHub/components/scheduling/scheduling-resource.component.d.ts +75 -0
- package/dist/KnowledgeHub/components/scheduling/scheduling-resource.component.d.ts.map +1 -0
- package/dist/KnowledgeHub/components/scheduling/scheduling-resource.component.js +601 -0
- package/dist/KnowledgeHub/components/scheduling/scheduling-resource.component.js.map +1 -0
- package/dist/KnowledgeHub/components/search/knowledge-search-resource.component.d.ts +93 -12
- package/dist/KnowledgeHub/components/search/knowledge-search-resource.component.d.ts.map +1 -1
- package/dist/KnowledgeHub/components/search/knowledge-search-resource.component.js +637 -107
- package/dist/KnowledgeHub/components/search/knowledge-search-resource.component.js.map +1 -1
- package/dist/KnowledgeHub/index.d.ts +2 -0
- package/dist/KnowledgeHub/index.d.ts.map +1 -1
- package/dist/KnowledgeHub/index.js +2 -0
- package/dist/KnowledgeHub/index.js.map +1 -1
- package/dist/__tests__/analytics-resource.test.d.ts +2 -0
- package/dist/__tests__/analytics-resource.test.d.ts.map +1 -0
- package/dist/__tests__/analytics-resource.test.js +181 -0
- package/dist/__tests__/analytics-resource.test.js.map +1 -0
- package/dist/__tests__/scheduling.test.d.ts +2 -0
- package/dist/__tests__/scheduling.test.d.ts.map +1 -0
- package/dist/__tests__/scheduling.test.js +205 -0
- package/dist/__tests__/scheduling.test.js.map +1 -0
- package/dist/ai-dashboards.module.d.ts +18 -14
- package/dist/ai-dashboards.module.d.ts.map +1 -1
- package/dist/ai-dashboards.module.js +25 -5
- package/dist/ai-dashboards.module.js.map +1 -1
- package/dist/public-api.d.ts +1 -0
- package/dist/public-api.d.ts.map +1 -1
- package/dist/public-api.js +1 -0
- package/dist/public-api.js.map +1 -1
- package/dist/shared/entity-field-display.d.ts +44 -0
- package/dist/shared/entity-field-display.d.ts.map +1 -0
- package/dist/shared/entity-field-display.js +118 -0
- package/dist/shared/entity-field-display.js.map +1 -0
- package/package.json +47 -46
|
@@ -37,6 +37,10 @@ export declare class ClusterVisualizationResourceComponent extends BaseResourceC
|
|
|
37
37
|
EntityOptions: ClusterConfigPanelEntityOption[];
|
|
38
38
|
/** Entity document options for the selected entity (shown when 2+) */
|
|
39
39
|
EntityDocOptions: ClusterConfigPanelEntityDocOption[];
|
|
40
|
+
/** Ordered field keys for prioritized display in scatter tooltip/detail */
|
|
41
|
+
FieldPriority: string[];
|
|
42
|
+
/** Map of field names to human-readable display names */
|
|
43
|
+
FieldDisplayNames: Record<string, string>;
|
|
40
44
|
SavedVisualizations: SavedClusterVisualization[];
|
|
41
45
|
ActiveSavedId: string | null;
|
|
42
46
|
private userSettingEntity;
|
|
@@ -78,7 +82,11 @@ export declare class ClusterVisualizationResourceComponent extends BaseResourceC
|
|
|
78
82
|
private fetchVectorsForEntity;
|
|
79
83
|
/** Parse the JSON metadata string from the vector DB into a record */
|
|
80
84
|
private parseVectorMetadata;
|
|
81
|
-
/**
|
|
85
|
+
/**
|
|
86
|
+
* Build a human-readable label from vector metadata using entity field metadata.
|
|
87
|
+
* Combines all IsNameField fields in Sequence order (e.g., "Sarah Chen" from FirstName + LastName).
|
|
88
|
+
* Falls back to heuristic field detection when entity metadata isn't available.
|
|
89
|
+
*/
|
|
82
90
|
private buildLabel;
|
|
83
91
|
/**
|
|
84
92
|
* After clustering completes, send sample records per cluster to an LLM for naming.
|
|
@@ -87,6 +95,22 @@ export declare class ClusterVisualizationResourceComponent extends BaseResourceC
|
|
|
87
95
|
private requestClusterLabelsFromLLM;
|
|
88
96
|
/** Build a text block describing sample records per cluster for the LLM prompt */
|
|
89
97
|
private buildClusterDataForPrompt;
|
|
98
|
+
/**
|
|
99
|
+
* Handle inline label edits from the scatter component legend.
|
|
100
|
+
* Updates the ClusterLabels cache and marks the label as user-edited.
|
|
101
|
+
*/
|
|
102
|
+
OnLabelEdited(event: {
|
|
103
|
+
ClusterId: number;
|
|
104
|
+
OldLabel: string;
|
|
105
|
+
NewLabel: string;
|
|
106
|
+
}): void;
|
|
107
|
+
/**
|
|
108
|
+
* Compute the prioritized field order and display names from entity metadata.
|
|
109
|
+
* Sets both FieldPriority and FieldDisplayNames.
|
|
110
|
+
* Returns field names sorted: IsNameField first, then DefaultInView by Sequence,
|
|
111
|
+
* then remaining fields by Sequence.
|
|
112
|
+
*/
|
|
113
|
+
private ComputeFieldPriority;
|
|
90
114
|
/** Apply cluster labels to the result's Clusters array (sets the Label property) */
|
|
91
115
|
private applyLabelsToResult;
|
|
92
116
|
/**
|
|
@@ -96,6 +120,16 @@ export declare class ClusterVisualizationResourceComponent extends BaseResourceC
|
|
|
96
120
|
private stripVectorsFromResult;
|
|
97
121
|
/** Load saved visualizations from UserInfoEngine settings */
|
|
98
122
|
private loadSavedVisualizations;
|
|
123
|
+
/**
|
|
124
|
+
* Auto-save the current session to localStorage so it can be restored
|
|
125
|
+
* when the user navigates away and comes back.
|
|
126
|
+
*/
|
|
127
|
+
private saveLastSession;
|
|
128
|
+
/**
|
|
129
|
+
* Restore the last session from localStorage if no saved visualization is active.
|
|
130
|
+
* This handles the "navigate away and come back" case.
|
|
131
|
+
*/
|
|
132
|
+
private restoreLastSession;
|
|
99
133
|
/** Persist saved visualizations to UserInfoEngine settings */
|
|
100
134
|
private persistSavedVisualizations;
|
|
101
135
|
static ɵfac: i0.ɵɵFactoryDeclaration<ClusterVisualizationResourceComponent, never>;
|
package/dist/KnowledgeHub/components/clusters/cluster-visualization-resource.component.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cluster-visualization-resource.component.d.ts","sourceRoot":"","sources":["../../../../src/KnowledgeHub/components/clusters/cluster-visualization-resource.component.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,EAAgC,SAAS,EAAE,aAAa,EAAqB,MAAM,eAAe,CAAC;AAG1G,OAAO,EAAE,YAAY,EAAmE,MAAM,+BAA+B,CAAC;AAE9H,OAAO,EAAE,qBAAqB,EAAqB,MAAM,2BAA2B,CAAC;AAGrF,OAAO,EACH,aAAa,EACb,8BAA8B,EAC9B,iCAAiC,EAEjC,0BAA0B,EAC1B,cAAc,EACd,YAAY,EACZ,YAAY,EACZ,yBAAyB,
|
|
1
|
+
{"version":3,"file":"cluster-visualization-resource.component.d.ts","sourceRoot":"","sources":["../../../../src/KnowledgeHub/components/clusters/cluster-visualization-resource.component.ts"],"names":[],"mappings":"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"}
|
|
@@ -29,7 +29,6 @@ import * as i0 from "@angular/core";
|
|
|
29
29
|
import * as i1 from "@memberjunction/ng-clustering";
|
|
30
30
|
const _c0 = ["scatterPlot"];
|
|
31
31
|
const _c1 = () => [];
|
|
32
|
-
const _c2 = () => ["Entity", "Name", "Description"];
|
|
33
32
|
function ClusterVisualizationResourceComponent_For_8_Template(rf, ctx) { if (rf & 1) {
|
|
34
33
|
const _r2 = i0.ɵɵgetCurrentView();
|
|
35
34
|
i0.ɵɵelementStart(0, "div", 16);
|
|
@@ -103,7 +102,25 @@ function ClusterVisualizationResourceComponent_Conditional_17_Template(rf, ctx)
|
|
|
103
102
|
i0.ɵɵadvance(5);
|
|
104
103
|
i0.ɵɵtextInterpolate(ctx_r3.ComputationTimeFormatted);
|
|
105
104
|
} }
|
|
106
|
-
|
|
105
|
+
/**
|
|
106
|
+
* Build an environment-scoped storage key so cluster data does not bleed
|
|
107
|
+
* across different database environments that share the same browser.
|
|
108
|
+
* Uses the GraphQL endpoint origin as the environment fingerprint.
|
|
109
|
+
*/
|
|
110
|
+
function buildEnvScopedKey(base) {
|
|
111
|
+
try {
|
|
112
|
+
const origin = Metadata.Provider.ConfigData?.URL;
|
|
113
|
+
if (origin) {
|
|
114
|
+
// Use just the origin portion (protocol + host) so path differences don't fragment keys
|
|
115
|
+
const url = new URL(origin);
|
|
116
|
+
return `${base}_${url.origin}`;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
catch { /* fall through */ }
|
|
120
|
+
return base;
|
|
121
|
+
}
|
|
122
|
+
const SAVED_CLUSTERS_BASE_KEY = 'KnowledgeHub_SavedClusters';
|
|
123
|
+
const LAST_SESSION_BASE_KEY = 'KnowledgeHub_LastClusterSession';
|
|
107
124
|
let ClusterVisualizationResourceComponent = class ClusterVisualizationResourceComponent extends BaseResourceComponent {
|
|
108
125
|
scatterPlot;
|
|
109
126
|
cdr = inject(ChangeDetectorRef);
|
|
@@ -136,6 +153,10 @@ let ClusterVisualizationResourceComponent = class ClusterVisualizationResourceCo
|
|
|
136
153
|
EntityOptions = [];
|
|
137
154
|
/** Entity document options for the selected entity (shown when 2+) */
|
|
138
155
|
EntityDocOptions = [];
|
|
156
|
+
/** Ordered field keys for prioritized display in scatter tooltip/detail */
|
|
157
|
+
FieldPriority = [];
|
|
158
|
+
/** Map of field names to human-readable display names */
|
|
159
|
+
FieldDisplayNames = {};
|
|
139
160
|
// Saved visualizations
|
|
140
161
|
SavedVisualizations = [];
|
|
141
162
|
ActiveSavedId = null;
|
|
@@ -147,6 +168,14 @@ let ClusterVisualizationResourceComponent = class ClusterVisualizationResourceCo
|
|
|
147
168
|
async ngAfterViewInit() {
|
|
148
169
|
await this.loadEntityOptions();
|
|
149
170
|
this.loadSavedVisualizations();
|
|
171
|
+
this.restoreLastSession();
|
|
172
|
+
this.navigationService.SetAgentContext(this, {
|
|
173
|
+
IsVisualizationLoaded: !!this.Result,
|
|
174
|
+
VisualizationTitle: this.VisualizationTitle || null,
|
|
175
|
+
ClusterCount: this.Result?.Clusters?.length ?? 0,
|
|
176
|
+
TotalPoints: this.Result?.Points?.length ?? 0,
|
|
177
|
+
});
|
|
178
|
+
this.NotifyLoadComplete();
|
|
150
179
|
}
|
|
151
180
|
ngOnDestroy() {
|
|
152
181
|
this.destroy$.next();
|
|
@@ -179,6 +208,8 @@ let ClusterVisualizationResourceComponent = class ClusterVisualizationResourceCo
|
|
|
179
208
|
this.IsRunning = true;
|
|
180
209
|
this.ActiveConfig = config;
|
|
181
210
|
this.ClusterLabels = [];
|
|
211
|
+
// Auto-hide detail panel from previous visualization
|
|
212
|
+
this.scatterPlot?.CloseDetailPanel();
|
|
182
213
|
// Update entity doc options if entity changed
|
|
183
214
|
this.updateEntityDocOptions(config.EntityName);
|
|
184
215
|
this.cdr.detectChanges();
|
|
@@ -188,9 +219,12 @@ let ClusterVisualizationResourceComponent = class ClusterVisualizationResourceCo
|
|
|
188
219
|
// Run clustering (client-side UMAP + K-Means/DBSCAN)
|
|
189
220
|
this.Result = await this.clusteringService.RunClustering(vectors, config);
|
|
190
221
|
this.VisualizationTitle = `${config.EntityName} — ${config.Algorithm === 'kmeans' ? 'K-Means' : 'DBSCAN'}`;
|
|
222
|
+
this.FieldPriority = this.ComputeFieldPriority(config.EntityName);
|
|
191
223
|
// Fire LLM cluster naming in the background (non-blocking).
|
|
192
224
|
// Clusters render immediately; labels appear when LLM responds.
|
|
193
225
|
this.requestClusterLabelsFromLLM().catch(err => console.warn('[ClusterVisualization] Background naming failed:', err));
|
|
226
|
+
// Auto-save session state so it can be restored after navigation
|
|
227
|
+
this.saveLastSession();
|
|
194
228
|
}
|
|
195
229
|
catch (error) {
|
|
196
230
|
console.error('[ClusterVisualization] Pipeline error:', error);
|
|
@@ -242,6 +276,7 @@ let ClusterVisualizationResourceComponent = class ClusterVisualizationResourceCo
|
|
|
242
276
|
}
|
|
243
277
|
/** Select a saved visualization — restore from cache if available, otherwise re-run */
|
|
244
278
|
async OnSelectSaved(saved) {
|
|
279
|
+
this.scatterPlot?.CloseDetailPanel();
|
|
245
280
|
this.ActiveSavedId = saved.Id;
|
|
246
281
|
this.VisualizationTitle = saved.Name;
|
|
247
282
|
// Reconstruct config
|
|
@@ -282,6 +317,7 @@ let ClusterVisualizationResourceComponent = class ClusterVisualizationResourceCo
|
|
|
282
317
|
}
|
|
283
318
|
/** Start a new analysis (clear current) */
|
|
284
319
|
OnNewAnalysis() {
|
|
320
|
+
this.scatterPlot?.CloseDetailPanel();
|
|
285
321
|
this.ActiveSavedId = null;
|
|
286
322
|
this.Result = null;
|
|
287
323
|
this.ClusterLabels = [];
|
|
@@ -399,10 +435,38 @@ let ClusterVisualizationResourceComponent = class ClusterVisualizationResourceCo
|
|
|
399
435
|
return {};
|
|
400
436
|
}
|
|
401
437
|
}
|
|
402
|
-
/**
|
|
438
|
+
/**
|
|
439
|
+
* Build a human-readable label from vector metadata using entity field metadata.
|
|
440
|
+
* Combines all IsNameField fields in Sequence order (e.g., "Sarah Chen" from FirstName + LastName).
|
|
441
|
+
* Falls back to heuristic field detection when entity metadata isn't available.
|
|
442
|
+
*/
|
|
403
443
|
buildLabel(metadata) {
|
|
404
|
-
|
|
405
|
-
|
|
444
|
+
const entityName = metadata['Entity'];
|
|
445
|
+
if (entityName) {
|
|
446
|
+
try {
|
|
447
|
+
const md = new Metadata();
|
|
448
|
+
const entityInfo = md.Entities.find(e => e.Name === entityName);
|
|
449
|
+
if (entityInfo) {
|
|
450
|
+
// Combine all IsNameField fields in Sequence order
|
|
451
|
+
const nameFields = entityInfo.Fields
|
|
452
|
+
.filter(f => f.IsNameField)
|
|
453
|
+
.sort((a, b) => (a.Sequence ?? 9999) - (b.Sequence ?? 9999));
|
|
454
|
+
if (nameFields.length > 0) {
|
|
455
|
+
const parts = nameFields
|
|
456
|
+
.map(f => metadata[f.Name])
|
|
457
|
+
.filter(v => v != null && v.trim() !== '');
|
|
458
|
+
if (parts.length > 0)
|
|
459
|
+
return parts.join(' ');
|
|
460
|
+
}
|
|
461
|
+
// Single NameField fallback
|
|
462
|
+
if (entityInfo.NameField && metadata[entityInfo.NameField.Name]) {
|
|
463
|
+
return metadata[entityInfo.NameField.Name];
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
catch { /* metadata not available, fall through */ }
|
|
468
|
+
}
|
|
469
|
+
// Heuristic fallbacks
|
|
406
470
|
return metadata['Name']
|
|
407
471
|
|| metadata['Title']
|
|
408
472
|
|| metadata['Description']?.substring(0, 60)
|
|
@@ -475,6 +539,62 @@ let ClusterVisualizationResourceComponent = class ClusterVisualizationResourceCo
|
|
|
475
539
|
}
|
|
476
540
|
return lines.join('\n');
|
|
477
541
|
}
|
|
542
|
+
/**
|
|
543
|
+
* Handle inline label edits from the scatter component legend.
|
|
544
|
+
* Updates the ClusterLabels cache and marks the label as user-edited.
|
|
545
|
+
*/
|
|
546
|
+
OnLabelEdited(event) {
|
|
547
|
+
const existing = this.ClusterLabels.find(l => l.ClusterId === event.ClusterId);
|
|
548
|
+
if (existing) {
|
|
549
|
+
existing.Label = event.NewLabel;
|
|
550
|
+
existing.IsUserEdited = true;
|
|
551
|
+
}
|
|
552
|
+
else {
|
|
553
|
+
this.ClusterLabels.push({
|
|
554
|
+
ClusterId: event.ClusterId,
|
|
555
|
+
Label: event.NewLabel,
|
|
556
|
+
IsUserEdited: true,
|
|
557
|
+
});
|
|
558
|
+
}
|
|
559
|
+
this.cdr.detectChanges();
|
|
560
|
+
}
|
|
561
|
+
/**
|
|
562
|
+
* Compute the prioritized field order and display names from entity metadata.
|
|
563
|
+
* Sets both FieldPriority and FieldDisplayNames.
|
|
564
|
+
* Returns field names sorted: IsNameField first, then DefaultInView by Sequence,
|
|
565
|
+
* then remaining fields by Sequence.
|
|
566
|
+
*/
|
|
567
|
+
ComputeFieldPriority(entityName) {
|
|
568
|
+
try {
|
|
569
|
+
const md = new Metadata();
|
|
570
|
+
const entityInfo = md.Entities.find(e => e.Name === entityName);
|
|
571
|
+
if (!entityInfo)
|
|
572
|
+
return [];
|
|
573
|
+
const internalKeys = new Set([
|
|
574
|
+
'ID', 'Entity', 'EntityIcon', 'RecordID', 'TemplateID',
|
|
575
|
+
'__mj_UpdatedAt', '__mj_CreatedAt',
|
|
576
|
+
]);
|
|
577
|
+
// Build display names map
|
|
578
|
+
const displayNames = {};
|
|
579
|
+
for (const f of entityInfo.Fields) {
|
|
580
|
+
displayNames[f.Name] = f.DisplayNameOrName;
|
|
581
|
+
}
|
|
582
|
+
this.FieldDisplayNames = displayNames;
|
|
583
|
+
return entityInfo.Fields
|
|
584
|
+
.filter(f => !internalKeys.has(f.Name) && !f.IsVirtual && !f.IsPrimaryKey)
|
|
585
|
+
.sort((a, b) => {
|
|
586
|
+
if (a.IsNameField !== b.IsNameField)
|
|
587
|
+
return a.IsNameField ? -1 : 1;
|
|
588
|
+
if (a.DefaultInView !== b.DefaultInView)
|
|
589
|
+
return a.DefaultInView ? -1 : 1;
|
|
590
|
+
return (a.Sequence ?? 9999) - (b.Sequence ?? 9999);
|
|
591
|
+
})
|
|
592
|
+
.map(f => f.Name);
|
|
593
|
+
}
|
|
594
|
+
catch {
|
|
595
|
+
return [];
|
|
596
|
+
}
|
|
597
|
+
}
|
|
478
598
|
/** Apply cluster labels to the result's Clusters array (sets the Label property) */
|
|
479
599
|
applyLabelsToResult() {
|
|
480
600
|
if (!this.Result || this.ClusterLabels.length === 0)
|
|
@@ -502,8 +622,9 @@ let ClusterVisualizationResourceComponent = class ClusterVisualizationResourceCo
|
|
|
502
622
|
/** Load saved visualizations from UserInfoEngine settings */
|
|
503
623
|
loadSavedVisualizations() {
|
|
504
624
|
try {
|
|
625
|
+
const key = buildEnvScopedKey(SAVED_CLUSTERS_BASE_KEY);
|
|
505
626
|
const engine = UserInfoEngine.Instance;
|
|
506
|
-
const setting = engine.UserSettings.find(s => s.Setting ===
|
|
627
|
+
const setting = engine.UserSettings.find(s => s.Setting === key);
|
|
507
628
|
if (setting?.Value) {
|
|
508
629
|
this.userSettingEntity = setting;
|
|
509
630
|
this.SavedVisualizations = JSON.parse(setting.Value);
|
|
@@ -515,6 +636,57 @@ let ClusterVisualizationResourceComponent = class ClusterVisualizationResourceCo
|
|
|
515
636
|
}
|
|
516
637
|
this.cdr.detectChanges();
|
|
517
638
|
}
|
|
639
|
+
/**
|
|
640
|
+
* Auto-save the current session to localStorage so it can be restored
|
|
641
|
+
* when the user navigates away and comes back.
|
|
642
|
+
*/
|
|
643
|
+
saveLastSession() {
|
|
644
|
+
if (!this.Result)
|
|
645
|
+
return;
|
|
646
|
+
try {
|
|
647
|
+
const session = {
|
|
648
|
+
Result: this.stripVectorsFromResult(this.Result),
|
|
649
|
+
ClusterLabels: this.ClusterLabels,
|
|
650
|
+
Config: this.ActiveConfig,
|
|
651
|
+
Title: this.VisualizationTitle,
|
|
652
|
+
Viewport: this.scatterPlot?.GetViewportTransform() ?? null,
|
|
653
|
+
};
|
|
654
|
+
localStorage.setItem(buildEnvScopedKey(LAST_SESSION_BASE_KEY), JSON.stringify(session));
|
|
655
|
+
}
|
|
656
|
+
catch {
|
|
657
|
+
// localStorage quota exceeded or not available — non-critical
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
/**
|
|
661
|
+
* Restore the last session from localStorage if no saved visualization is active.
|
|
662
|
+
* This handles the "navigate away and come back" case.
|
|
663
|
+
*/
|
|
664
|
+
restoreLastSession() {
|
|
665
|
+
if (this.Result || this.ActiveSavedId)
|
|
666
|
+
return; // Already showing something
|
|
667
|
+
try {
|
|
668
|
+
const raw = localStorage.getItem(buildEnvScopedKey(LAST_SESSION_BASE_KEY));
|
|
669
|
+
if (!raw)
|
|
670
|
+
return;
|
|
671
|
+
const session = JSON.parse(raw);
|
|
672
|
+
this.Result = session.Result;
|
|
673
|
+
this.ClusterLabels = session.ClusterLabels ?? [];
|
|
674
|
+
this.ActiveConfig = session.Config;
|
|
675
|
+
this.VisualizationTitle = session.Title ?? 'Restored Session';
|
|
676
|
+
this.FieldPriority = this.ComputeFieldPriority(session.Config.EntityName);
|
|
677
|
+
this.applyLabelsToResult();
|
|
678
|
+
this.cdr.detectChanges();
|
|
679
|
+
// Restore viewport after a tick to let the scatter component render
|
|
680
|
+
if (session.Viewport) {
|
|
681
|
+
setTimeout(() => {
|
|
682
|
+
this.scatterPlot?.SetViewportTransform(session.Viewport);
|
|
683
|
+
}, 50);
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
catch {
|
|
687
|
+
localStorage.removeItem(buildEnvScopedKey(LAST_SESSION_BASE_KEY));
|
|
688
|
+
}
|
|
689
|
+
}
|
|
518
690
|
/** Persist saved visualizations to UserInfoEngine settings */
|
|
519
691
|
async persistSavedVisualizations() {
|
|
520
692
|
try {
|
|
@@ -523,15 +695,16 @@ let ClusterVisualizationResourceComponent = class ClusterVisualizationResourceCo
|
|
|
523
695
|
if (!userId)
|
|
524
696
|
return;
|
|
525
697
|
if (!this.userSettingEntity) {
|
|
698
|
+
const key = buildEnvScopedKey(SAVED_CLUSTERS_BASE_KEY);
|
|
526
699
|
const engine = UserInfoEngine.Instance;
|
|
527
|
-
const existing = engine.UserSettings.find(s => s.Setting ===
|
|
700
|
+
const existing = engine.UserSettings.find(s => s.Setting === key);
|
|
528
701
|
if (existing) {
|
|
529
702
|
this.userSettingEntity = existing;
|
|
530
703
|
}
|
|
531
704
|
else {
|
|
532
705
|
this.userSettingEntity = await md.GetEntityObject('MJ: User Settings');
|
|
533
706
|
this.userSettingEntity.UserID = userId;
|
|
534
|
-
this.userSettingEntity.Setting =
|
|
707
|
+
this.userSettingEntity.Setting = buildEnvScopedKey(SAVED_CLUSTERS_BASE_KEY);
|
|
535
708
|
}
|
|
536
709
|
}
|
|
537
710
|
this.userSettingEntity.Value = JSON.stringify(this.SavedVisualizations);
|
|
@@ -547,7 +720,7 @@ let ClusterVisualizationResourceComponent = class ClusterVisualizationResourceCo
|
|
|
547
720
|
} if (rf & 2) {
|
|
548
721
|
let _t;
|
|
549
722
|
i0.ɵɵqueryRefresh(_t = i0.ɵɵloadQuery()) && (ctx.scatterPlot = _t.first);
|
|
550
|
-
} }, standalone: false, features: [i0.ɵɵInheritDefinitionFeature], decls: 22, vars:
|
|
723
|
+
} }, standalone: false, features: [i0.ɵɵInheritDefinitionFeature], decls: 22, vars: 13, consts: [["scatterPlot", ""], [1, "cluster-viz-container"], [1, "sidebar"], [1, "sidebar-header"], [1, "fa-solid", "fa-circle-nodes"], [1, "saved-list"], [1, "saved-item", 3, "active"], [1, "no-saved"], [1, "new-btn", 3, "click"], [1, "fa-solid", "fa-plus"], [1, "main-area"], [1, "topbar"], [1, "topbar-title"], [1, "scatter-area"], [3, "PointClicked", "PointHovered", "OpenRecordRequested", "LabelEdited", "Points", "Clusters", "IsLoading", "EntityName"], [3, "RunClustering", "SaveVisualization", "IsRunning", "Metrics", "EntityOptions", "EntityDocOptions"], [1, "saved-item", 3, "click"], [1, "saved-name"], [1, "saved-meta"], ["title", "Delete", 1, "delete-btn", 3, "click"], [1, "fa-solid", "fa-times"], [1, "fa-solid", "fa-bookmark"], [1, "metric"], [1, "fa-solid", "fa-circle-dot"], [1, "metric-value"], [1, "fa-solid", "fa-layer-group"], [1, "fa-solid", "fa-chart-line"], [1, "fa-solid", "fa-clock"]], template: function ClusterVisualizationResourceComponent_Template(rf, ctx) { if (rf & 1) {
|
|
551
724
|
const _r1 = i0.ɵɵgetCurrentView();
|
|
552
725
|
i0.ɵɵelementStart(0, "div", 1)(1, "div", 2)(2, "div", 3)(3, "h2");
|
|
553
726
|
i0.ɵɵelement(4, "i", 4);
|
|
@@ -568,7 +741,7 @@ let ClusterVisualizationResourceComponent = class ClusterVisualizationResourceCo
|
|
|
568
741
|
i0.ɵɵconditionalCreate(17, ClusterVisualizationResourceComponent_Conditional_17_Template, 20, 6);
|
|
569
742
|
i0.ɵɵelementEnd();
|
|
570
743
|
i0.ɵɵelementStart(18, "div", 13)(19, "mj-cluster-scatter", 14, 0);
|
|
571
|
-
i0.ɵɵlistener("PointClicked", function ClusterVisualizationResourceComponent_Template_mj_cluster_scatter_PointClicked_19_listener($event) { i0.ɵɵrestoreView(_r1); return i0.ɵɵresetView(ctx.OnPointClicked($event)); })("PointHovered", function ClusterVisualizationResourceComponent_Template_mj_cluster_scatter_PointHovered_19_listener($event) { i0.ɵɵrestoreView(_r1); return i0.ɵɵresetView(ctx.OnPointHovered($event)); })("OpenRecordRequested", function ClusterVisualizationResourceComponent_Template_mj_cluster_scatter_OpenRecordRequested_19_listener($event) { i0.ɵɵrestoreView(_r1); return i0.ɵɵresetView(ctx.OnOpenRecord($event)); });
|
|
744
|
+
i0.ɵɵlistener("PointClicked", function ClusterVisualizationResourceComponent_Template_mj_cluster_scatter_PointClicked_19_listener($event) { i0.ɵɵrestoreView(_r1); return i0.ɵɵresetView(ctx.OnPointClicked($event)); })("PointHovered", function ClusterVisualizationResourceComponent_Template_mj_cluster_scatter_PointHovered_19_listener($event) { i0.ɵɵrestoreView(_r1); return i0.ɵɵresetView(ctx.OnPointHovered($event)); })("OpenRecordRequested", function ClusterVisualizationResourceComponent_Template_mj_cluster_scatter_OpenRecordRequested_19_listener($event) { i0.ɵɵrestoreView(_r1); return i0.ɵɵresetView(ctx.OnOpenRecord($event)); })("LabelEdited", function ClusterVisualizationResourceComponent_Template_mj_cluster_scatter_LabelEdited_19_listener($event) { i0.ɵɵrestoreView(_r1); return i0.ɵɵresetView(ctx.OnLabelEdited($event)); });
|
|
572
745
|
i0.ɵɵelementEnd();
|
|
573
746
|
i0.ɵɵelementStart(21, "mj-cluster-config-panel", 15);
|
|
574
747
|
i0.ɵɵlistener("RunClustering", function ClusterVisualizationResourceComponent_Template_mj_cluster_config_panel_RunClustering_21_listener($event) { i0.ɵɵrestoreView(_r1); return i0.ɵɵresetView(ctx.OnRunClustering($event)); })("SaveVisualization", function ClusterVisualizationResourceComponent_Template_mj_cluster_config_panel_SaveVisualization_21_listener() { i0.ɵɵrestoreView(_r1); return i0.ɵɵresetView(ctx.OnSaveVisualization()); });
|
|
@@ -583,7 +756,7 @@ let ClusterVisualizationResourceComponent = class ClusterVisualizationResourceCo
|
|
|
583
756
|
i0.ɵɵadvance();
|
|
584
757
|
i0.ɵɵconditional(ctx.HasResult ? 17 : -1);
|
|
585
758
|
i0.ɵɵadvance(2);
|
|
586
|
-
i0.ɵɵproperty("Points", (ctx.Result == null ? null : ctx.Result.Points) ?? i0.ɵɵpureFunction0(11, _c1))("Clusters", (ctx.Result == null ? null : ctx.Result.Clusters) ?? i0.ɵɵpureFunction0(12, _c1))("IsLoading", ctx.IsRunning)("
|
|
759
|
+
i0.ɵɵproperty("Points", (ctx.Result == null ? null : ctx.Result.Points) ?? i0.ɵɵpureFunction0(11, _c1))("Clusters", (ctx.Result == null ? null : ctx.Result.Clusters) ?? i0.ɵɵpureFunction0(12, _c1))("IsLoading", ctx.IsRunning)("EntityName", ctx.ActiveConfig.EntityName);
|
|
587
760
|
i0.ɵɵadvance(2);
|
|
588
761
|
i0.ɵɵproperty("IsRunning", ctx.IsRunning)("Metrics", ctx.Metrics)("EntityOptions", ctx.EntityOptions)("EntityDocOptions", ctx.EntityDocOptions);
|
|
589
762
|
} }, dependencies: [i1.ClusterScatterComponent, i1.ClusterConfigPanelComponent], styles: ["[_nghost-%COMP%] {\n display: flex;\n width: 100%;\n height: 100%;\n}\n\n.cluster-viz-container[_ngcontent-%COMP%] {\n display: flex;\n width: 100%;\n height: 100%;\n overflow: hidden;\n}\n\n\n\n\n\n\n.sidebar[_ngcontent-%COMP%] {\n width: 260px;\n background: var(--mj-bg-surface);\n border-right: 1px solid var(--mj-border-default);\n display: flex;\n flex-direction: column;\n flex-shrink: 0;\n}\n\n.sidebar-header[_ngcontent-%COMP%] {\n padding: 20px;\n border-bottom: 1px solid var(--mj-border-default);\n}\n\n.sidebar-header[_ngcontent-%COMP%] h2[_ngcontent-%COMP%] {\n font-size: 0.95rem;\n font-weight: 600;\n display: flex;\n align-items: center;\n gap: 8px;\n margin: 0;\n color: var(--mj-text-primary);\n}\n\n.sidebar-header[_ngcontent-%COMP%] h2[_ngcontent-%COMP%] i[_ngcontent-%COMP%] {\n color: var(--mj-brand-primary);\n}\n\n.saved-list[_ngcontent-%COMP%] {\n flex: 1;\n overflow-y: auto;\n padding: 12px;\n}\n\n.saved-item[_ngcontent-%COMP%] {\n padding: 12px 14px;\n border-radius: 8px;\n cursor: pointer;\n margin-bottom: 4px;\n transition: background 0.15s;\n position: relative;\n}\n\n.saved-item[_ngcontent-%COMP%]:hover {\n background: var(--mj-bg-surface-hover);\n}\n\n.saved-item.active[_ngcontent-%COMP%] {\n background: color-mix(in srgb, var(--mj-brand-primary) 10%, var(--mj-bg-surface));\n border: 1px solid color-mix(in srgb, var(--mj-brand-primary) 30%, transparent);\n}\n\n.saved-name[_ngcontent-%COMP%] {\n font-size: 0.85rem;\n font-weight: 500;\n margin-bottom: 3px;\n color: var(--mj-text-primary);\n padding-right: 20px;\n}\n\n.saved-meta[_ngcontent-%COMP%] {\n font-size: 0.7rem;\n color: var(--mj-text-muted);\n display: flex;\n gap: 10px;\n}\n\n.delete-btn[_ngcontent-%COMP%] {\n position: absolute;\n top: 10px;\n right: 10px;\n background: none;\n border: none;\n color: var(--mj-text-muted);\n cursor: pointer;\n font-size: 0.7rem;\n padding: 2px 4px;\n opacity: 0;\n transition: opacity 0.15s, color 0.15s;\n}\n\n.saved-item[_ngcontent-%COMP%]:hover .delete-btn[_ngcontent-%COMP%] {\n opacity: 1;\n}\n\n.delete-btn[_ngcontent-%COMP%]:hover {\n color: var(--mj-status-error);\n}\n\n.no-saved[_ngcontent-%COMP%] {\n display: flex;\n flex-direction: column;\n align-items: center;\n justify-content: center;\n padding: 40px 20px;\n color: var(--mj-text-muted);\n text-align: center;\n}\n\n.no-saved[_ngcontent-%COMP%] i[_ngcontent-%COMP%] {\n font-size: 1.5rem;\n margin-bottom: 8px;\n opacity: 0.4;\n}\n\n.no-saved[_ngcontent-%COMP%] p[_ngcontent-%COMP%] {\n font-size: 0.8rem;\n margin: 0;\n}\n\n.new-btn[_ngcontent-%COMP%] {\n margin: 12px;\n padding: 10px;\n border: 1px dashed var(--mj-border-strong);\n border-radius: 8px;\n background: transparent;\n color: var(--mj-text-secondary);\n font-size: 0.8rem;\n cursor: pointer;\n display: flex;\n align-items: center;\n justify-content: center;\n gap: 6px;\n transition: all 0.15s;\n}\n\n.new-btn[_ngcontent-%COMP%]:hover {\n border-color: var(--mj-brand-primary);\n color: var(--mj-brand-primary);\n}\n\n\n\n\n\n\n.main-area[_ngcontent-%COMP%] {\n flex: 1;\n position: relative;\n display: flex;\n flex-direction: column;\n min-width: 0;\n}\n\n\n\n.topbar[_ngcontent-%COMP%] {\n height: 48px;\n background: var(--mj-bg-surface);\n border-bottom: 1px solid var(--mj-border-default);\n display: flex;\n align-items: center;\n padding: 0 20px;\n gap: 24px;\n flex-shrink: 0;\n}\n\n.topbar-title[_ngcontent-%COMP%] {\n font-size: 0.9rem;\n font-weight: 600;\n margin-right: auto;\n color: var(--mj-text-primary);\n}\n\n.metric[_ngcontent-%COMP%] {\n display: flex;\n align-items: center;\n gap: 6px;\n font-size: 0.75rem;\n color: var(--mj-text-secondary);\n}\n\n.metric[_ngcontent-%COMP%] i[_ngcontent-%COMP%] {\n font-size: 0.7rem;\n}\n\n.metric-value[_ngcontent-%COMP%] {\n color: var(--mj-text-primary);\n font-weight: 600;\n}\n\n.metric-value.good[_ngcontent-%COMP%] {\n color: var(--mj-status-success);\n}\n\n\n\n.scatter-area[_ngcontent-%COMP%] {\n flex: 1;\n position: relative;\n overflow: hidden;\n}"] });
|
|
@@ -594,12 +767,12 @@ ClusterVisualizationResourceComponent = __decorate([
|
|
|
594
767
|
export { ClusterVisualizationResourceComponent };
|
|
595
768
|
(() => { (typeof ngDevMode === "undefined" || ngDevMode) && i0.ɵsetClassMetadata(ClusterVisualizationResourceComponent, [{
|
|
596
769
|
type: Component,
|
|
597
|
-
args: [{ standalone: false, selector: 'app-cluster-visualization-resource', template: "<div class=\"cluster-viz-container\">\n <!-- Left Sidebar: Saved Visualizations -->\n <div class=\"sidebar\">\n <div class=\"sidebar-header\">\n <h2><i class=\"fa-solid fa-circle-nodes\"></i> Saved Clusters</h2>\n </div>\n <div class=\"saved-list\">\n @for (saved of SavedVisualizations; track TrackSavedBy($index, saved)) {\n <div class=\"saved-item\"\n [class.active]=\"IsActiveSaved(saved)\"\n (click)=\"OnSelectSaved(saved)\">\n <div class=\"saved-name\">{{ saved.Name }}</div>\n <div class=\"saved-meta\">\n <span>{{ saved.EntityName }}</span>\n <span>{{ FormatDate(saved.CreatedAt) }}</span>\n </div>\n <button class=\"delete-btn\"\n title=\"Delete\"\n (click)=\"OnDeleteSaved(saved, $event)\">\n <i class=\"fa-solid fa-times\"></i>\n </button>\n </div>\n }\n\n @if (SavedVisualizations.length === 0) {\n <div class=\"no-saved\">\n <i class=\"fa-solid fa-bookmark\"></i>\n <p>No saved visualizations yet</p>\n </div>\n }\n </div>\n <button class=\"new-btn\" (click)=\"OnNewAnalysis()\">\n <i class=\"fa-solid fa-plus\"></i> New Cluster Analysis\n </button>\n </div>\n\n <!-- Main Area -->\n <div class=\"main-area\">\n <!-- Top metrics bar -->\n <div class=\"topbar\">\n <div class=\"topbar-title\">{{ VisualizationTitle }}</div>\n @if (HasResult) {\n <div class=\"metric\">\n <i class=\"fa-solid fa-circle-dot\"></i>\n Records: <span class=\"metric-value\">{{ Result!.Metrics.RecordCount }}</span>\n </div>\n <div class=\"metric\">\n <i class=\"fa-solid fa-layer-group\"></i>\n Clusters: <span class=\"metric-value\">{{ Result!.Metrics.ClusterCount }}</span>\n </div>\n <div class=\"metric\">\n <i class=\"fa-solid fa-chart-line\"></i>\n Silhouette:\n <span class=\"metric-value\" [class.good]=\"IsSilhouetteGood\">\n {{ SilhouetteScoreFormatted }}\n </span>\n </div>\n <div class=\"metric\">\n <i class=\"fa-solid fa-clock\"></i>\n Computed: <span class=\"metric-value\">{{ ComputationTimeFormatted }}</span>\n </div>\n }\n </div>\n\n <!-- Scatter plot area -->\n <div class=\"scatter-area\">\n <mj-cluster-scatter\n #scatterPlot\n [Points]=\"Result?.Points ?? []\"\n [Clusters]=\"Result?.Clusters ?? []\"\n [IsLoading]=\"IsRunning\"\n [
|
|
770
|
+
args: [{ standalone: false, selector: 'app-cluster-visualization-resource', template: "<div class=\"cluster-viz-container\">\n <!-- Left Sidebar: Saved Visualizations -->\n <div class=\"sidebar\">\n <div class=\"sidebar-header\">\n <h2><i class=\"fa-solid fa-circle-nodes\"></i> Saved Clusters</h2>\n </div>\n <div class=\"saved-list\">\n @for (saved of SavedVisualizations; track TrackSavedBy($index, saved)) {\n <div class=\"saved-item\"\n [class.active]=\"IsActiveSaved(saved)\"\n (click)=\"OnSelectSaved(saved)\">\n <div class=\"saved-name\">{{ saved.Name }}</div>\n <div class=\"saved-meta\">\n <span>{{ saved.EntityName }}</span>\n <span>{{ FormatDate(saved.CreatedAt) }}</span>\n </div>\n <button class=\"delete-btn\"\n title=\"Delete\"\n (click)=\"OnDeleteSaved(saved, $event)\">\n <i class=\"fa-solid fa-times\"></i>\n </button>\n </div>\n }\n\n @if (SavedVisualizations.length === 0) {\n <div class=\"no-saved\">\n <i class=\"fa-solid fa-bookmark\"></i>\n <p>No saved visualizations yet</p>\n </div>\n }\n </div>\n <button class=\"new-btn\" (click)=\"OnNewAnalysis()\">\n <i class=\"fa-solid fa-plus\"></i> New Cluster Analysis\n </button>\n </div>\n\n <!-- Main Area -->\n <div class=\"main-area\">\n <!-- Top metrics bar -->\n <div class=\"topbar\">\n <div class=\"topbar-title\">{{ VisualizationTitle }}</div>\n @if (HasResult) {\n <div class=\"metric\">\n <i class=\"fa-solid fa-circle-dot\"></i>\n Records: <span class=\"metric-value\">{{ Result!.Metrics.RecordCount }}</span>\n </div>\n <div class=\"metric\">\n <i class=\"fa-solid fa-layer-group\"></i>\n Clusters: <span class=\"metric-value\">{{ Result!.Metrics.ClusterCount }}</span>\n </div>\n <div class=\"metric\">\n <i class=\"fa-solid fa-chart-line\"></i>\n Silhouette:\n <span class=\"metric-value\" [class.good]=\"IsSilhouetteGood\">\n {{ SilhouetteScoreFormatted }}\n </span>\n </div>\n <div class=\"metric\">\n <i class=\"fa-solid fa-clock\"></i>\n Computed: <span class=\"metric-value\">{{ ComputationTimeFormatted }}</span>\n </div>\n }\n </div>\n\n <!-- Scatter plot area -->\n <div class=\"scatter-area\">\n <mj-cluster-scatter\n #scatterPlot\n [Points]=\"Result?.Points ?? []\"\n [Clusters]=\"Result?.Clusters ?? []\"\n [IsLoading]=\"IsRunning\"\n [EntityName]=\"ActiveConfig.EntityName\"\n (PointClicked)=\"OnPointClicked($event)\"\n (PointHovered)=\"OnPointHovered($event)\"\n (OpenRecordRequested)=\"OnOpenRecord($event)\"\n (LabelEdited)=\"OnLabelEdited($event)\">\n </mj-cluster-scatter>\n\n <!-- Floating config panel -->\n <mj-cluster-config-panel\n [IsRunning]=\"IsRunning\"\n [Metrics]=\"Metrics\"\n [EntityOptions]=\"EntityOptions\"\n [EntityDocOptions]=\"EntityDocOptions\"\n (RunClustering)=\"OnRunClustering($event)\"\n (SaveVisualization)=\"OnSaveVisualization()\">\n </mj-cluster-config-panel>\n </div>\n </div>\n</div>\n", styles: [":host {\n display: flex;\n width: 100%;\n height: 100%;\n}\n\n.cluster-viz-container {\n display: flex;\n width: 100%;\n height: 100%;\n overflow: hidden;\n}\n\n/* ============================================================\n Left Sidebar\n ============================================================ */\n\n.sidebar {\n width: 260px;\n background: var(--mj-bg-surface);\n border-right: 1px solid var(--mj-border-default);\n display: flex;\n flex-direction: column;\n flex-shrink: 0;\n}\n\n.sidebar-header {\n padding: 20px;\n border-bottom: 1px solid var(--mj-border-default);\n}\n\n.sidebar-header h2 {\n font-size: 0.95rem;\n font-weight: 600;\n display: flex;\n align-items: center;\n gap: 8px;\n margin: 0;\n color: var(--mj-text-primary);\n}\n\n.sidebar-header h2 i {\n color: var(--mj-brand-primary);\n}\n\n.saved-list {\n flex: 1;\n overflow-y: auto;\n padding: 12px;\n}\n\n.saved-item {\n padding: 12px 14px;\n border-radius: 8px;\n cursor: pointer;\n margin-bottom: 4px;\n transition: background 0.15s;\n position: relative;\n}\n\n.saved-item:hover {\n background: var(--mj-bg-surface-hover);\n}\n\n.saved-item.active {\n background: color-mix(in srgb, var(--mj-brand-primary) 10%, var(--mj-bg-surface));\n border: 1px solid color-mix(in srgb, var(--mj-brand-primary) 30%, transparent);\n}\n\n.saved-name {\n font-size: 0.85rem;\n font-weight: 500;\n margin-bottom: 3px;\n color: var(--mj-text-primary);\n padding-right: 20px;\n}\n\n.saved-meta {\n font-size: 0.7rem;\n color: var(--mj-text-muted);\n display: flex;\n gap: 10px;\n}\n\n.delete-btn {\n position: absolute;\n top: 10px;\n right: 10px;\n background: none;\n border: none;\n color: var(--mj-text-muted);\n cursor: pointer;\n font-size: 0.7rem;\n padding: 2px 4px;\n opacity: 0;\n transition: opacity 0.15s, color 0.15s;\n}\n\n.saved-item:hover .delete-btn {\n opacity: 1;\n}\n\n.delete-btn:hover {\n color: var(--mj-status-error);\n}\n\n.no-saved {\n display: flex;\n flex-direction: column;\n align-items: center;\n justify-content: center;\n padding: 40px 20px;\n color: var(--mj-text-muted);\n text-align: center;\n}\n\n.no-saved i {\n font-size: 1.5rem;\n margin-bottom: 8px;\n opacity: 0.4;\n}\n\n.no-saved p {\n font-size: 0.8rem;\n margin: 0;\n}\n\n.new-btn {\n margin: 12px;\n padding: 10px;\n border: 1px dashed var(--mj-border-strong);\n border-radius: 8px;\n background: transparent;\n color: var(--mj-text-secondary);\n font-size: 0.8rem;\n cursor: pointer;\n display: flex;\n align-items: center;\n justify-content: center;\n gap: 6px;\n transition: all 0.15s;\n}\n\n.new-btn:hover {\n border-color: var(--mj-brand-primary);\n color: var(--mj-brand-primary);\n}\n\n/* ============================================================\n Main Area\n ============================================================ */\n\n.main-area {\n flex: 1;\n position: relative;\n display: flex;\n flex-direction: column;\n min-width: 0;\n}\n\n/* Top bar */\n.topbar {\n height: 48px;\n background: var(--mj-bg-surface);\n border-bottom: 1px solid var(--mj-border-default);\n display: flex;\n align-items: center;\n padding: 0 20px;\n gap: 24px;\n flex-shrink: 0;\n}\n\n.topbar-title {\n font-size: 0.9rem;\n font-weight: 600;\n margin-right: auto;\n color: var(--mj-text-primary);\n}\n\n.metric {\n display: flex;\n align-items: center;\n gap: 6px;\n font-size: 0.75rem;\n color: var(--mj-text-secondary);\n}\n\n.metric i {\n font-size: 0.7rem;\n}\n\n.metric-value {\n color: var(--mj-text-primary);\n font-weight: 600;\n}\n\n.metric-value.good {\n color: var(--mj-status-success);\n}\n\n/* Scatter area */\n.scatter-area {\n flex: 1;\n position: relative;\n overflow: hidden;\n}\n"] }]
|
|
598
771
|
}], null, { scatterPlot: [{
|
|
599
772
|
type: ViewChild,
|
|
600
773
|
args: ['scatterPlot']
|
|
601
774
|
}] }); })();
|
|
602
|
-
(() => { (typeof ngDevMode === "undefined" || ngDevMode) && i0.ɵsetClassDebugInfo(ClusterVisualizationResourceComponent, { className: "ClusterVisualizationResourceComponent", filePath: "src/KnowledgeHub/components/clusters/cluster-visualization-resource.component.ts", lineNumber:
|
|
775
|
+
(() => { (typeof ngDevMode === "undefined" || ngDevMode) && i0.ɵsetClassDebugInfo(ClusterVisualizationResourceComponent, { className: "ClusterVisualizationResourceComponent", filePath: "src/KnowledgeHub/components/clusters/cluster-visualization-resource.component.ts", lineNumber: 63 }); })();
|
|
603
776
|
/** Tree-shaking prevention */
|
|
604
777
|
export function LoadClusterVisualizationResource() {
|
|
605
778
|
// Prevents tree-shaking of the component
|