@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.
Files changed (78) hide show
  1. package/dist/AI/components/autotagging/autotagging-pipeline-resource.component.d.ts +677 -5
  2. package/dist/AI/components/autotagging/autotagging-pipeline-resource.component.d.ts.map +1 -1
  3. package/dist/AI/components/autotagging/autotagging-pipeline-resource.component.js +6879 -1873
  4. package/dist/AI/components/autotagging/autotagging-pipeline-resource.component.js.map +1 -1
  5. package/dist/AI/components/duplicates/duplicate-detection-resource.component.d.ts +46 -1
  6. package/dist/AI/components/duplicates/duplicate-detection-resource.component.d.ts.map +1 -1
  7. package/dist/AI/components/duplicates/duplicate-detection-resource.component.js +758 -491
  8. package/dist/AI/components/duplicates/duplicate-detection-resource.component.js.map +1 -1
  9. package/dist/AI/components/vectors/vector-management-resource.component.d.ts +19 -0
  10. package/dist/AI/components/vectors/vector-management-resource.component.d.ts.map +1 -1
  11. package/dist/AI/components/vectors/vector-management-resource.component.js +410 -208
  12. package/dist/AI/components/vectors/vector-management-resource.component.js.map +1 -1
  13. package/dist/DataExplorer/data-explorer-dashboard.component.d.ts.map +1 -1
  14. package/dist/DataExplorer/data-explorer-dashboard.component.js +17 -17
  15. package/dist/DataExplorer/data-explorer-dashboard.component.js.map +1 -1
  16. package/dist/Integration/components/activity/activity.component.d.ts.map +1 -1
  17. package/dist/Integration/components/activity/activity.component.js +1 -0
  18. package/dist/Integration/components/activity/activity.component.js.map +1 -1
  19. package/dist/Integration/components/connections/connections.component.d.ts.map +1 -1
  20. package/dist/Integration/components/connections/connections.component.js +1 -0
  21. package/dist/Integration/components/connections/connections.component.js.map +1 -1
  22. package/dist/Integration/components/mapping-workspace/mapping-workspace.component.d.ts.map +1 -1
  23. package/dist/Integration/components/mapping-workspace/mapping-workspace.component.js +1 -0
  24. package/dist/Integration/components/mapping-workspace/mapping-workspace.component.js.map +1 -1
  25. package/dist/Integration/components/overview/overview.component.d.ts.map +1 -1
  26. package/dist/Integration/components/overview/overview.component.js +1 -0
  27. package/dist/Integration/components/overview/overview.component.js.map +1 -1
  28. package/dist/Integration/components/pipelines/pipelines.component.d.ts.map +1 -1
  29. package/dist/Integration/components/pipelines/pipelines.component.js +1 -0
  30. package/dist/Integration/components/pipelines/pipelines.component.js.map +1 -1
  31. package/dist/Integration/components/schedules/schedules.component.d.ts.map +1 -1
  32. package/dist/Integration/components/schedules/schedules.component.js +1 -0
  33. package/dist/Integration/components/schedules/schedules.component.js.map +1 -1
  34. package/dist/KnowledgeHub/components/analytics/analytics-resource.component.d.ts +411 -0
  35. package/dist/KnowledgeHub/components/analytics/analytics-resource.component.d.ts.map +1 -0
  36. package/dist/KnowledgeHub/components/analytics/analytics-resource.component.js +4266 -0
  37. package/dist/KnowledgeHub/components/analytics/analytics-resource.component.js.map +1 -0
  38. package/dist/KnowledgeHub/components/clusters/cluster-visualization-resource.component.d.ts +35 -1
  39. package/dist/KnowledgeHub/components/clusters/cluster-visualization-resource.component.d.ts.map +1 -1
  40. package/dist/KnowledgeHub/components/clusters/cluster-visualization-resource.component.js +186 -13
  41. package/dist/KnowledgeHub/components/clusters/cluster-visualization-resource.component.js.map +1 -1
  42. package/dist/KnowledgeHub/components/config/knowledge-config-resource.component.d.ts +1 -0
  43. package/dist/KnowledgeHub/components/config/knowledge-config-resource.component.d.ts.map +1 -1
  44. package/dist/KnowledgeHub/components/config/knowledge-config-resource.component.js +188 -165
  45. package/dist/KnowledgeHub/components/config/knowledge-config-resource.component.js.map +1 -1
  46. package/dist/KnowledgeHub/components/scheduling/scheduling-resource.component.d.ts +75 -0
  47. package/dist/KnowledgeHub/components/scheduling/scheduling-resource.component.d.ts.map +1 -0
  48. package/dist/KnowledgeHub/components/scheduling/scheduling-resource.component.js +601 -0
  49. package/dist/KnowledgeHub/components/scheduling/scheduling-resource.component.js.map +1 -0
  50. package/dist/KnowledgeHub/components/search/knowledge-search-resource.component.d.ts +93 -12
  51. package/dist/KnowledgeHub/components/search/knowledge-search-resource.component.d.ts.map +1 -1
  52. package/dist/KnowledgeHub/components/search/knowledge-search-resource.component.js +637 -107
  53. package/dist/KnowledgeHub/components/search/knowledge-search-resource.component.js.map +1 -1
  54. package/dist/KnowledgeHub/index.d.ts +2 -0
  55. package/dist/KnowledgeHub/index.d.ts.map +1 -1
  56. package/dist/KnowledgeHub/index.js +2 -0
  57. package/dist/KnowledgeHub/index.js.map +1 -1
  58. package/dist/__tests__/analytics-resource.test.d.ts +2 -0
  59. package/dist/__tests__/analytics-resource.test.d.ts.map +1 -0
  60. package/dist/__tests__/analytics-resource.test.js +181 -0
  61. package/dist/__tests__/analytics-resource.test.js.map +1 -0
  62. package/dist/__tests__/scheduling.test.d.ts +2 -0
  63. package/dist/__tests__/scheduling.test.d.ts.map +1 -0
  64. package/dist/__tests__/scheduling.test.js +205 -0
  65. package/dist/__tests__/scheduling.test.js.map +1 -0
  66. package/dist/ai-dashboards.module.d.ts +18 -14
  67. package/dist/ai-dashboards.module.d.ts.map +1 -1
  68. package/dist/ai-dashboards.module.js +25 -5
  69. package/dist/ai-dashboards.module.js.map +1 -1
  70. package/dist/public-api.d.ts +1 -0
  71. package/dist/public-api.d.ts.map +1 -1
  72. package/dist/public-api.js +1 -0
  73. package/dist/public-api.js.map +1 -1
  74. package/dist/shared/entity-field-display.d.ts +44 -0
  75. package/dist/shared/entity-field-display.d.ts.map +1 -0
  76. package/dist/shared/entity-field-display.js +118 -0
  77. package/dist/shared/entity-field-display.js.map +1 -0
  78. 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
- /** Build a human-readable label from vector metadata (prefers Name field, falls back to RecordID) */
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>;
@@ -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":"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"}
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
- const SAVED_CLUSTERS_KEY = 'KnowledgeHub_SavedClusters';
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
- /** Build a human-readable label from vector metadata (prefers Name field, falls back to RecordID) */
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
- // The vector sync process stores display fields directly in metadata.
405
- // Prefer Name, then Title, then Description, then fall back to RecordID.
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 === SAVED_CLUSTERS_KEY);
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 === SAVED_CLUSTERS_KEY);
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 = SAVED_CLUSTERS_KEY;
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: 14, 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", "Points", "Clusters", "IsLoading", "TooltipFields"], [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) {
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)("TooltipFields", i0.ɵɵpureFunction0(13, _c2));
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 [TooltipFields]=\"['Entity', 'Name', 'Description']\"\n (PointClicked)=\"OnPointClicked($event)\"\n (PointHovered)=\"OnPointHovered($event)\"\n (OpenRecordRequested)=\"OnOpenRecord($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"] }]
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: 44 }); })();
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