@memberjunction/ng-clustering 5.38.0 → 5.40.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 (49) hide show
  1. package/dist/__tests__/cluster-view-type.test.d.ts +2 -0
  2. package/dist/__tests__/cluster-view-type.test.d.ts.map +1 -0
  3. package/dist/__tests__/cluster-view-type.test.js +63 -0
  4. package/dist/__tests__/cluster-view-type.test.js.map +1 -0
  5. package/dist/lib/cluster-config-panel.component.d.ts +39 -1
  6. package/dist/lib/cluster-config-panel.component.d.ts.map +1 -1
  7. package/dist/lib/cluster-config-panel.component.js +287 -88
  8. package/dist/lib/cluster-config-panel.component.js.map +1 -1
  9. package/dist/lib/cluster-scatter.component.d.ts +53 -1
  10. package/dist/lib/cluster-scatter.component.d.ts.map +1 -1
  11. package/dist/lib/cluster-scatter.component.js +385 -135
  12. package/dist/lib/cluster-scatter.component.js.map +1 -1
  13. package/dist/lib/clustering.module.d.ts +7 -4
  14. package/dist/lib/clustering.module.d.ts.map +1 -1
  15. package/dist/lib/clustering.module.js +20 -3
  16. package/dist/lib/clustering.module.js.map +1 -1
  17. package/dist/lib/clustering.service.d.ts +63 -3
  18. package/dist/lib/clustering.service.d.ts.map +1 -1
  19. package/dist/lib/clustering.service.js +169 -48
  20. package/dist/lib/clustering.service.js.map +1 -1
  21. package/dist/lib/clustering.types.d.ts +17 -2
  22. package/dist/lib/clustering.types.d.ts.map +1 -1
  23. package/dist/lib/clustering.types.js +3 -0
  24. package/dist/lib/clustering.types.js.map +1 -1
  25. package/dist/lib/view-type/cluster-view-prop-sheet.component.d.ts +31 -0
  26. package/dist/lib/view-type/cluster-view-prop-sheet.component.d.ts.map +1 -0
  27. package/dist/lib/view-type/cluster-view-prop-sheet.component.js +185 -0
  28. package/dist/lib/view-type/cluster-view-prop-sheet.component.js.map +1 -0
  29. package/dist/lib/view-type/cluster-view-renderer.component.d.ts +143 -0
  30. package/dist/lib/view-type/cluster-view-renderer.component.d.ts.map +1 -0
  31. package/dist/lib/view-type/cluster-view-renderer.component.js +508 -0
  32. package/dist/lib/view-type/cluster-view-renderer.component.js.map +1 -0
  33. package/dist/lib/view-type/cluster-view-type.d.ts +35 -0
  34. package/dist/lib/view-type/cluster-view-type.d.ts.map +1 -0
  35. package/dist/lib/view-type/cluster-view-type.js +51 -0
  36. package/dist/lib/view-type/cluster-view-type.js.map +1 -0
  37. package/dist/lib/view-type/cluster-view.types.d.ts +31 -0
  38. package/dist/lib/view-type/cluster-view.types.d.ts.map +1 -0
  39. package/dist/lib/view-type/cluster-view.types.js +29 -0
  40. package/dist/lib/view-type/cluster-view.types.js.map +1 -0
  41. package/dist/lib/view-type/entity-document-availability.engine.d.ts +80 -0
  42. package/dist/lib/view-type/entity-document-availability.engine.d.ts.map +1 -0
  43. package/dist/lib/view-type/entity-document-availability.engine.js +149 -0
  44. package/dist/lib/view-type/entity-document-availability.engine.js.map +1 -0
  45. package/dist/public-api.d.ts +5 -0
  46. package/dist/public-api.d.ts.map +1 -1
  47. package/dist/public-api.js +6 -0
  48. package/dist/public-api.js.map +1 -1
  49. package/package.json +11 -3
@@ -0,0 +1,185 @@
1
+ import { Component, Input, Output, EventEmitter, ViewEncapsulation } from '@angular/core';
2
+ import { EntityInfo } from '@memberjunction/core';
3
+ import { toClusterViewConfig } from './cluster-view.types';
4
+ import * as i0 from "@angular/core";
5
+ import * as i1 from "@angular/forms";
6
+ function ClusterViewPropSheetComponent_Conditional_9_Template(rf, ctx) { if (rf & 1) {
7
+ const _r1 = i0.ɵɵgetCurrentView();
8
+ i0.ɵɵelementStart(0, "label", 1)(1, "span");
9
+ i0.ɵɵtext(2, "Clusters (K)");
10
+ i0.ɵɵelementEnd();
11
+ i0.ɵɵelementStart(3, "input", 11);
12
+ i0.ɵɵlistener("input", function ClusterViewPropSheetComponent_Conditional_9_Template_input_input_3_listener($event) { i0.ɵɵrestoreView(_r1); const ctx_r1 = i0.ɵɵnextContext(); return i0.ɵɵresetView(ctx_r1.patch({ k: ctx_r1.asInt($event.target.value, ctx_r1.cfg.k) })); });
13
+ i0.ɵɵelementEnd()();
14
+ } if (rf & 2) {
15
+ const ctx_r1 = i0.ɵɵnextContext();
16
+ i0.ɵɵadvance(3);
17
+ i0.ɵɵproperty("value", ctx_r1.cfg.k);
18
+ } }
19
+ /**
20
+ * ClusterViewPropSheetComponent
21
+ * -----------------------------
22
+ * The configuration prop-sheet for the Cluster view type — honors {@link IViewPropSheet}.
23
+ * Lets the user pick the algorithm, cluster count, 2D/3D layout, legend coloring, record cap,
24
+ * and LLM naming. Emits the updated config (as the opaque host map) on every change so the host
25
+ * can persist it into `UserView.DisplayState.viewTypeConfigs` and re-render the cluster.
26
+ */
27
+ export class ClusterViewPropSheetComponent {
28
+ constructor() {
29
+ this.entity = null;
30
+ this._config = {};
31
+ this.configChange = new EventEmitter();
32
+ /** The current parsed config (defaults applied) bound by the template. */
33
+ this.cfg = toClusterViewConfig({});
34
+ }
35
+ set config(value) {
36
+ this._config = value ?? {};
37
+ this.cfg = toClusterViewConfig(this._config);
38
+ }
39
+ get config() {
40
+ return this._config;
41
+ }
42
+ /** Apply a partial change, re-emit the full config map to the host. */
43
+ patch(change) {
44
+ this.cfg = { ...this.cfg, ...change };
45
+ this._config = { ...this.cfg };
46
+ this.configChange.emit(this._config);
47
+ }
48
+ asAlgorithm(v) {
49
+ return v === 'dbscan' ? 'dbscan' : 'kmeans';
50
+ }
51
+ asDimensions(v) {
52
+ return String(v) === '3' ? 3 : 2;
53
+ }
54
+ asColorBy(v) {
55
+ return v === 'entity' ? 'entity' : 'cluster';
56
+ }
57
+ asInt(v, fallback) {
58
+ const n = parseInt(v, 10);
59
+ return Number.isNaN(n) ? fallback : n;
60
+ }
61
+ static { this.ɵfac = function ClusterViewPropSheetComponent_Factory(__ngFactoryType__) { return new (__ngFactoryType__ || ClusterViewPropSheetComponent)(); }; }
62
+ static { this.ɵcmp = /*@__PURE__*/ i0.ɵɵdefineComponent({ type: ClusterViewPropSheetComponent, selectors: [["mj-cluster-view-prop-sheet"]], inputs: { entity: "entity", config: "config" }, outputs: { configChange: "configChange" }, standalone: false, decls: 34, vars: 8, consts: [[1, "cluster-prop-sheet"], [1, "cluster-prop-row"], [1, "mj-input", 3, "change", "value"], ["value", "kmeans"], ["value", "dbscan"], [3, "value"], ["value", "cluster"], ["value", "entity"], ["type", "number", "min", "50", "max", "5000", "step", "50", 1, "mj-input", 3, "input", "value"], [1, "cluster-prop-row", "cluster-prop-row--inline"], ["type", "checkbox", 1, "mj-checkbox", 3, "change", "checked"], ["type", "number", "min", "2", "max", "20", 1, "mj-input", 3, "input", "value"]], template: function ClusterViewPropSheetComponent_Template(rf, ctx) { if (rf & 1) {
63
+ i0.ɵɵelementStart(0, "div", 0)(1, "label", 1)(2, "span");
64
+ i0.ɵɵtext(3, "Algorithm");
65
+ i0.ɵɵelementEnd();
66
+ i0.ɵɵelementStart(4, "select", 2);
67
+ i0.ɵɵlistener("change", function ClusterViewPropSheetComponent_Template_select_change_4_listener($event) { return ctx.patch({ algorithm: ctx.asAlgorithm($event.target.value) }); });
68
+ i0.ɵɵelementStart(5, "option", 3);
69
+ i0.ɵɵtext(6, "K-Means");
70
+ i0.ɵɵelementEnd();
71
+ i0.ɵɵelementStart(7, "option", 4);
72
+ i0.ɵɵtext(8, "DBSCAN");
73
+ i0.ɵɵelementEnd()()();
74
+ i0.ɵɵconditionalCreate(9, ClusterViewPropSheetComponent_Conditional_9_Template, 4, 1, "label", 1);
75
+ i0.ɵɵelementStart(10, "label", 1)(11, "span");
76
+ i0.ɵɵtext(12, "Layout");
77
+ i0.ɵɵelementEnd();
78
+ i0.ɵɵelementStart(13, "select", 2);
79
+ i0.ɵɵlistener("change", function ClusterViewPropSheetComponent_Template_select_change_13_listener($event) { return ctx.patch({ dimensions: ctx.asDimensions($event.target.value) }); });
80
+ i0.ɵɵelementStart(14, "option", 5);
81
+ i0.ɵɵtext(15, "2D");
82
+ i0.ɵɵelementEnd();
83
+ i0.ɵɵelementStart(16, "option", 5);
84
+ i0.ɵɵtext(17, "3D");
85
+ i0.ɵɵelementEnd()()();
86
+ i0.ɵɵelementStart(18, "label", 1)(19, "span");
87
+ i0.ɵɵtext(20, "Color by");
88
+ i0.ɵɵelementEnd();
89
+ i0.ɵɵelementStart(21, "select", 2);
90
+ i0.ɵɵlistener("change", function ClusterViewPropSheetComponent_Template_select_change_21_listener($event) { return ctx.patch({ colorBy: ctx.asColorBy($event.target.value) }); });
91
+ i0.ɵɵelementStart(22, "option", 6);
92
+ i0.ɵɵtext(23, "Cluster");
93
+ i0.ɵɵelementEnd();
94
+ i0.ɵɵelementStart(24, "option", 7);
95
+ i0.ɵɵtext(25, "Entity");
96
+ i0.ɵɵelementEnd()()();
97
+ i0.ɵɵelementStart(26, "label", 1)(27, "span");
98
+ i0.ɵɵtext(28, "Max records");
99
+ i0.ɵɵelementEnd();
100
+ i0.ɵɵelementStart(29, "input", 8);
101
+ i0.ɵɵlistener("input", function ClusterViewPropSheetComponent_Template_input_input_29_listener($event) { return ctx.patch({ maxRecords: ctx.asInt($event.target.value, ctx.cfg.maxRecords) }); });
102
+ i0.ɵɵelementEnd()();
103
+ i0.ɵɵelementStart(30, "label", 9)(31, "input", 10);
104
+ i0.ɵɵlistener("change", function ClusterViewPropSheetComponent_Template_input_change_31_listener($event) { return ctx.patch({ nameClusters: $event.target.checked }); });
105
+ i0.ɵɵelementEnd();
106
+ i0.ɵɵelementStart(32, "span");
107
+ i0.ɵɵtext(33, "Name clusters with AI");
108
+ i0.ɵɵelementEnd()()();
109
+ } if (rf & 2) {
110
+ i0.ɵɵadvance(4);
111
+ i0.ɵɵproperty("value", ctx.cfg.algorithm);
112
+ i0.ɵɵadvance(5);
113
+ i0.ɵɵconditional(ctx.cfg.algorithm === "kmeans" ? 9 : -1);
114
+ i0.ɵɵadvance(4);
115
+ i0.ɵɵproperty("value", ctx.cfg.dimensions);
116
+ i0.ɵɵadvance();
117
+ i0.ɵɵproperty("value", 2);
118
+ i0.ɵɵadvance(2);
119
+ i0.ɵɵproperty("value", 3);
120
+ i0.ɵɵadvance(5);
121
+ i0.ɵɵproperty("value", ctx.cfg.colorBy);
122
+ i0.ɵɵadvance(8);
123
+ i0.ɵɵproperty("value", ctx.cfg.maxRecords);
124
+ i0.ɵɵadvance(2);
125
+ i0.ɵɵproperty("checked", ctx.cfg.nameClusters);
126
+ } }, dependencies: [i1.NgSelectOption, i1.ɵNgSelectMultipleOption], styles: ["\n .cluster-prop-sheet { display: flex; flex-direction: column; gap: 12px; padding: 12px; }\n .cluster-prop-row { display: flex; flex-direction: column; gap: 4px; font-size: 13px; color: var(--mj-text-secondary); }\n .cluster-prop-row--inline { flex-direction: row; align-items: center; gap: 8px; }\n "], encapsulation: 2 }); }
127
+ }
128
+ (() => { (typeof ngDevMode === "undefined" || ngDevMode) && i0.ɵsetClassMetadata(ClusterViewPropSheetComponent, [{
129
+ type: Component,
130
+ args: [{ standalone: false, selector: 'mj-cluster-view-prop-sheet', encapsulation: ViewEncapsulation.None, template: `
131
+ <div class="cluster-prop-sheet">
132
+ <label class="cluster-prop-row">
133
+ <span>Algorithm</span>
134
+ <select class="mj-input" [value]="cfg.algorithm" (change)="patch({ algorithm: asAlgorithm($any($event.target).value) })">
135
+ <option value="kmeans">K-Means</option>
136
+ <option value="dbscan">DBSCAN</option>
137
+ </select>
138
+ </label>
139
+
140
+ @if (cfg.algorithm === 'kmeans') {
141
+ <label class="cluster-prop-row">
142
+ <span>Clusters (K)</span>
143
+ <input class="mj-input" type="number" min="2" max="20" [value]="cfg.k"
144
+ (input)="patch({ k: asInt($any($event.target).value, cfg.k) })" />
145
+ </label>
146
+ }
147
+
148
+ <label class="cluster-prop-row">
149
+ <span>Layout</span>
150
+ <select class="mj-input" [value]="cfg.dimensions" (change)="patch({ dimensions: asDimensions($any($event.target).value) })">
151
+ <option [value]="2">2D</option>
152
+ <option [value]="3">3D</option>
153
+ </select>
154
+ </label>
155
+
156
+ <label class="cluster-prop-row">
157
+ <span>Color by</span>
158
+ <select class="mj-input" [value]="cfg.colorBy" (change)="patch({ colorBy: asColorBy($any($event.target).value) })">
159
+ <option value="cluster">Cluster</option>
160
+ <option value="entity">Entity</option>
161
+ </select>
162
+ </label>
163
+
164
+ <label class="cluster-prop-row">
165
+ <span>Max records</span>
166
+ <input class="mj-input" type="number" min="50" max="5000" step="50" [value]="cfg.maxRecords"
167
+ (input)="patch({ maxRecords: asInt($any($event.target).value, cfg.maxRecords) })" />
168
+ </label>
169
+
170
+ <label class="cluster-prop-row cluster-prop-row--inline">
171
+ <input class="mj-checkbox" type="checkbox" [checked]="cfg.nameClusters"
172
+ (change)="patch({ nameClusters: $any($event.target).checked })" />
173
+ <span>Name clusters with AI</span>
174
+ </label>
175
+ </div>
176
+ `, styles: ["\n .cluster-prop-sheet { display: flex; flex-direction: column; gap: 12px; padding: 12px; }\n .cluster-prop-row { display: flex; flex-direction: column; gap: 4px; font-size: 13px; color: var(--mj-text-secondary); }\n .cluster-prop-row--inline { flex-direction: row; align-items: center; gap: 8px; }\n "] }]
177
+ }], null, { entity: [{
178
+ type: Input
179
+ }], config: [{
180
+ type: Input
181
+ }], configChange: [{
182
+ type: Output
183
+ }] }); })();
184
+ (() => { (typeof ngDevMode === "undefined" || ngDevMode) && i0.ɵsetClassDebugInfo(ClusterViewPropSheetComponent, { className: "ClusterViewPropSheetComponent", filePath: "src/lib/view-type/cluster-view-prop-sheet.component.ts", lineNumber: 71 }); })();
185
+ //# sourceMappingURL=cluster-view-prop-sheet.component.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cluster-view-prop-sheet.component.js","sourceRoot":"","sources":["../../../src/lib/view-type/cluster-view-prop-sheet.component.ts"],"names":[],"mappings":"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","sourcesContent":["import { Component, Input, Output, EventEmitter, ViewEncapsulation } from '@angular/core';\nimport { EntityInfo } from '@memberjunction/core';\nimport { IViewPropSheet } from '@memberjunction/ng-entity-viewer';\nimport { ClusterViewConfig, toClusterViewConfig } from './cluster-view.types';\n\n/**\n * ClusterViewPropSheetComponent\n * -----------------------------\n * The configuration prop-sheet for the Cluster view type — honors {@link IViewPropSheet}.\n * Lets the user pick the algorithm, cluster count, 2D/3D layout, legend coloring, record cap,\n * and LLM naming. Emits the updated config (as the opaque host map) on every change so the host\n * can persist it into `UserView.DisplayState.viewTypeConfigs` and re-render the cluster.\n */\n@Component({\n standalone: false,\n selector: 'mj-cluster-view-prop-sheet',\n encapsulation: ViewEncapsulation.None,\n template: `\n <div class=\"cluster-prop-sheet\">\n <label class=\"cluster-prop-row\">\n <span>Algorithm</span>\n <select class=\"mj-input\" [value]=\"cfg.algorithm\" (change)=\"patch({ algorithm: asAlgorithm($any($event.target).value) })\">\n <option value=\"kmeans\">K-Means</option>\n <option value=\"dbscan\">DBSCAN</option>\n </select>\n </label>\n\n @if (cfg.algorithm === 'kmeans') {\n <label class=\"cluster-prop-row\">\n <span>Clusters (K)</span>\n <input class=\"mj-input\" type=\"number\" min=\"2\" max=\"20\" [value]=\"cfg.k\"\n (input)=\"patch({ k: asInt($any($event.target).value, cfg.k) })\" />\n </label>\n }\n\n <label class=\"cluster-prop-row\">\n <span>Layout</span>\n <select class=\"mj-input\" [value]=\"cfg.dimensions\" (change)=\"patch({ dimensions: asDimensions($any($event.target).value) })\">\n <option [value]=\"2\">2D</option>\n <option [value]=\"3\">3D</option>\n </select>\n </label>\n\n <label class=\"cluster-prop-row\">\n <span>Color by</span>\n <select class=\"mj-input\" [value]=\"cfg.colorBy\" (change)=\"patch({ colorBy: asColorBy($any($event.target).value) })\">\n <option value=\"cluster\">Cluster</option>\n <option value=\"entity\">Entity</option>\n </select>\n </label>\n\n <label class=\"cluster-prop-row\">\n <span>Max records</span>\n <input class=\"mj-input\" type=\"number\" min=\"50\" max=\"5000\" step=\"50\" [value]=\"cfg.maxRecords\"\n (input)=\"patch({ maxRecords: asInt($any($event.target).value, cfg.maxRecords) })\" />\n </label>\n\n <label class=\"cluster-prop-row cluster-prop-row--inline\">\n <input class=\"mj-checkbox\" type=\"checkbox\" [checked]=\"cfg.nameClusters\"\n (change)=\"patch({ nameClusters: $any($event.target).checked })\" />\n <span>Name clusters with AI</span>\n </label>\n </div>\n `,\n styles: [`\n .cluster-prop-sheet { display: flex; flex-direction: column; gap: 12px; padding: 12px; }\n .cluster-prop-row { display: flex; flex-direction: column; gap: 4px; font-size: 13px; color: var(--mj-text-secondary); }\n .cluster-prop-row--inline { flex-direction: row; align-items: center; gap: 8px; }\n `],\n})\nexport class ClusterViewPropSheetComponent implements IViewPropSheet<Record<string, unknown>> {\n @Input() entity: EntityInfo | null = null;\n\n private _config: Record<string, unknown> = {};\n @Input()\n set config(value: Record<string, unknown>) {\n this._config = value ?? {};\n this.cfg = toClusterViewConfig(this._config);\n }\n get config(): Record<string, unknown> {\n return this._config;\n }\n\n @Output() configChange = new EventEmitter<Record<string, unknown>>();\n\n /** The current parsed config (defaults applied) bound by the template. */\n public cfg: ClusterViewConfig = toClusterViewConfig({});\n\n /** Apply a partial change, re-emit the full config map to the host. */\n patch(change: Partial<ClusterViewConfig>): void {\n this.cfg = { ...this.cfg, ...change };\n this._config = { ...this.cfg } as unknown as Record<string, unknown>;\n this.configChange.emit(this._config);\n }\n\n asAlgorithm(v: string): 'kmeans' | 'dbscan' {\n return v === 'dbscan' ? 'dbscan' : 'kmeans';\n }\n asDimensions(v: string): 2 | 3 {\n return String(v) === '3' ? 3 : 2;\n }\n asColorBy(v: string): 'cluster' | 'entity' {\n return v === 'entity' ? 'entity' : 'cluster';\n }\n asInt(v: string, fallback: number): number {\n const n = parseInt(v, 10);\n return Number.isNaN(n) ? fallback : n;\n }\n}\n"]}
@@ -0,0 +1,143 @@
1
+ import { EventEmitter, ChangeDetectorRef, OnInit, OnDestroy } from '@angular/core';
2
+ import { EntityInfo } from '@memberjunction/core';
3
+ import { BaseAngularComponent } from '@memberjunction/ng-base-types';
4
+ import { IViewRenderer } from '@memberjunction/ng-entity-viewer';
5
+ import { ClusterPoint, ClusterInfo } from '../clustering.types';
6
+ import { ClusterViewConfig } from './cluster-view.types';
7
+ import * as i0 from "@angular/core";
8
+ /**
9
+ * ClusterViewRendererComponent
10
+ * ----------------------------
11
+ * The Cluster **view type** renderer — a thin {@link IViewRenderer} adapter that lets any
12
+ * entity with vectors be visualized as a cluster scatter directly inside `mj-entity-viewer`,
13
+ * reusing the exact same {@link ClusteringService} + `mj-cluster-scatter` the Knowledge Hub
14
+ * "Visualize" cluster surface uses.
15
+ *
16
+ * The host feeds it the standard renderer inputs (entity / records / filterText / config) via
17
+ * `setInput`, and listens for `recordSelected` / `recordOpened`. When the entity or config
18
+ * changes it re-runs the clustering pipeline and rebinds the scatter.
19
+ *
20
+ * **Why this mirrors the Knowledge Hub resource rather than the server `RunClusterAnalysis`
21
+ * path:** the server analysis returns points keyed only by record ID (so labels render as
22
+ * "Unknown"), produces generic "Cluster N" names, and picks an Entity Document
23
+ * non-deterministically when none is supplied (causing flip-flop "no vectors" results). This
24
+ * renderer instead:
25
+ * 1. Resolves the entity's **first active Entity Document explicitly** (deterministic source).
26
+ * 2. Fetches that document's vectors (with their JSON metadata) via {@link GraphQLAIClient.FetchEntityVectors}.
27
+ * 3. Builds real point labels from the entity's `IsNameField` fields ({@link buildLabel}).
28
+ * 4. Clusters in-browser via {@link ClusteringService.RunClustering} (client owns vectors + labels).
29
+ * 5. Requests + applies **LLM cluster names** so clusters read semantically, not "Cluster 1..5".
30
+ *
31
+ * Inputs use the camelCase names mandated by the {@link IViewRenderer} contract (the host
32
+ * binds them by those exact names), rather than MJ's usual PascalCase for public members.
33
+ */
34
+ export declare class ClusterViewRendererComponent extends BaseAngularComponent implements IViewRenderer<Record<string, unknown>>, OnInit, OnDestroy {
35
+ private cdr;
36
+ private _entity;
37
+ set entity(value: EntityInfo | null);
38
+ get entity(): EntityInfo | null;
39
+ /** The view's records — used to map a clicked point back to its record object. */
40
+ records: Record<string, unknown>[];
41
+ selectedRecordId: string | null;
42
+ filterText: string | null;
43
+ private _config;
44
+ set config(value: Record<string, unknown>);
45
+ get config(): Record<string, unknown>;
46
+ recordSelected: EventEmitter<unknown>;
47
+ recordOpened: EventEmitter<unknown>;
48
+ configChanged: EventEmitter<Record<string, unknown>>;
49
+ /** The current parsed config (defaults applied). */
50
+ activeConfig: ClusterViewConfig;
51
+ points: ClusterPoint[];
52
+ clusters: ClusterInfo[];
53
+ /** Ordered field keys for prioritized display in the scatter tooltip / detail panel. */
54
+ fieldPriority: string[];
55
+ /** Map of field names → human-readable display names for the scatter tooltip / detail panel. */
56
+ fieldDisplayNames: Record<string, string>;
57
+ isLoading: boolean;
58
+ errorMessage: string | null;
59
+ private recluster$;
60
+ private destroy$;
61
+ private clusteringService;
62
+ /**
63
+ * Monotonically increasing token captured at the start of each run. Because runs are async
64
+ * and overlapping (entity/config can change mid-flight), each run compares the token it
65
+ * captured against the latest before applying results — a stale run silently bails so it
66
+ * cannot clobber a newer one.
67
+ */
68
+ private runToken;
69
+ constructor(cdr: ChangeDetectorRef);
70
+ ngOnInit(): void;
71
+ ngOnDestroy(): void;
72
+ private scheduleRecluster;
73
+ /**
74
+ * Run the clustering pipeline for the current entity + config and rebind the scatter.
75
+ *
76
+ * Mirrors the Knowledge Hub "Visualize" flow: resolve a deterministic Entity Document →
77
+ * fetch its vectors with metadata → build real labels → cluster client-side → request LLM
78
+ * cluster names. Guarded by {@link runToken} so an overlapping/stale run never clobbers a
79
+ * newer one.
80
+ */
81
+ private runClustering;
82
+ /** True when a newer run has started since `token` was captured — the caller should bail. */
83
+ private isStale;
84
+ /** Apply a successful clustering result to the view (no-op if the run is stale). */
85
+ private applyResult;
86
+ /** Apply an error state to the view (no-op if the run is stale). */
87
+ private applyError;
88
+ /**
89
+ * Resolve the {@link GraphQLDataProvider} backing this component's metadata provider, or
90
+ * `null` when the active provider is not a GraphQL transport (e.g. non-browser).
91
+ */
92
+ private resolveGraphQLProvider;
93
+ /**
94
+ * Resolve the entity's first active Entity Document ID **explicitly** (deterministic),
95
+ * loading the availability engine cache if needed. Returns `null` when the entity has no
96
+ * active document.
97
+ */
98
+ private resolveEntityDocumentID;
99
+ /**
100
+ * Fetch the entity document's vectors (with metadata) and build {@link ClusterInputVector}s,
101
+ * deriving each point's display label from the entity's name fields.
102
+ */
103
+ private fetchVectors;
104
+ /** Parse the JSON metadata string stored alongside a vector into a flat string map. */
105
+ private parseVectorMetadata;
106
+ /**
107
+ * Build a human-readable label for a point from the entity's name-field metadata. Combines
108
+ * all `IsNameField` fields in Sequence order (e.g. "Sarah Chen" from FirstName + LastName),
109
+ * then falls back to the single NameField, then to common heuristic keys, then to the record
110
+ * ID — so points show real names instead of "Unknown".
111
+ */
112
+ private buildLabel;
113
+ /**
114
+ * Compute the prioritized field order + display names for the scatter tooltip / detail panel.
115
+ * Orders: name fields first, then default-in-view fields, then the rest by Sequence; skips
116
+ * internal/virtual/primary-key fields. Also populates {@link fieldDisplayNames}.
117
+ */
118
+ private computeFieldPriority;
119
+ /** Build the engine {@link ClusterConfig} from the entity + the view-type config. */
120
+ private buildClusterConfig;
121
+ /**
122
+ * After clustering completes, send sample records per cluster to the "Cluster Naming" AI
123
+ * prompt and apply the returned semantic labels to {@link clusters}. Best-effort: failures
124
+ * are logged and leave the provisional "Cluster N" labels in place. No-op if the run became
125
+ * stale while the LLM call was in flight.
126
+ */
127
+ private requestClusterLabelsFromLLM;
128
+ /** Build a text block describing sample records per cluster for the LLM naming prompt. */
129
+ private buildClusterDataForPrompt;
130
+ /** Apply LLM-generated labels onto the live {@link clusters} array by cluster ID. */
131
+ private applyLabelsToClusters;
132
+ onPointClicked(point: ClusterPoint): void;
133
+ onOpenRecordRequested(point: ClusterPoint): void;
134
+ /**
135
+ * Resolve the host record corresponding to a scatter point. The vector key is the Entity
136
+ * Record Document ID, so match on the point metadata's `RecordID` (the underlying entity
137
+ * record's primary key) first, then fall back to a direct key match.
138
+ */
139
+ private recordForPoint;
140
+ static ɵfac: i0.ɵɵFactoryDeclaration<ClusterViewRendererComponent, never>;
141
+ static ɵcmp: i0.ɵɵComponentDeclaration<ClusterViewRendererComponent, "mj-cluster-view-renderer", never, { "entity": { "alias": "entity"; "required": false; }; "records": { "alias": "records"; "required": false; }; "selectedRecordId": { "alias": "selectedRecordId"; "required": false; }; "filterText": { "alias": "filterText"; "required": false; }; "config": { "alias": "config"; "required": false; }; }, { "recordSelected": "recordSelected"; "recordOpened": "recordOpened"; "configChanged": "configChanged"; }, never, never, false, never>;
142
+ }
143
+ //# sourceMappingURL=cluster-view-renderer.component.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cluster-view-renderer.component.d.ts","sourceRoot":"","sources":["../../../src/lib/view-type/cluster-view-renderer.component.ts"],"names":[],"mappings":"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"}