var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
    var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
    if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
    else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
    return c > 3 && r && Object.defineProperty(target, key, r), r;
};
/**
 * @fileoverview Knowledge Hub Clusters Dashboard Tab
 *
 * Full-page cluster visualization with:
 * - Left sidebar: saved cluster visualizations (persisted via UserInfoEngine)
 * - Main area: SVG scatter plot (mj-cluster-scatter)
 * - Floating config panel (mj-cluster-config-panel)
 * - Top metrics bar
 *
 * Registered as BaseResourceComponent for the Knowledge Hub application.
 */
import { Component, ChangeDetectorRef, inject, ViewChild, Input, Output, EventEmitter } from '@angular/core';
import { Subject } from 'rxjs';
import { CompositeKey, Metadata } from '@memberjunction/core';
import { UserInfoEngine, KnowledgeHubMetadataEngine } from '@memberjunction/core-entities';
import { RegisterClass } from '@memberjunction/global';
import { BaseResourceComponent, NavigationService, ActivityService } from '@memberjunction/ng-shared';
import { GraphQLAIClient } from '@memberjunction/graphql-dataprovider';
import { AIEngineBase } from '@memberjunction/ai-engine-base';
import { DefaultClusterConfig, } from '@memberjunction/ng-clustering';
import { ClusteringService } from '@memberjunction/ng-clustering';
import { buildClusterAgentContext, capClusterList, resolveSavedVisualization, buildClusterNotFoundError, } from './cluster-agent-context';
import { validateEnumParam, validateStringParam } from '../../../shared/agent-tool-validation';
import * as i0 from "@angular/core";
import * as i1 from "@angular/common";
import * as i2 from "@memberjunction/ng-ui-components";
import * as i3 from "@memberjunction/ng-clustering";
const _c0 = ["scatterPlot"];
const _c1 = () => [];
function ClusterVisualizationResourceComponent_Conditional_0_ng_container_0_Template(rf, ctx) { if (rf & 1) {
    i0.ɵɵelementContainer(0);
} }
function ClusterVisualizationResourceComponent_Conditional_0_Template(rf, ctx) { if (rf & 1) {
    i0.ɵɵtemplate(0, ClusterVisualizationResourceComponent_Conditional_0_ng_container_0_Template, 1, 0, "ng-container", 2);
} if (rf & 2) {
    i0.ɵɵnextContext();
    const clusterBody_r1 = i0.ɵɵreference(3);
    i0.ɵɵproperty("ngTemplateOutlet", clusterBody_r1);
} }
function ClusterVisualizationResourceComponent_Conditional_1_ng_container_8_Template(rf, ctx) { if (rf & 1) {
    i0.ɵɵelementContainer(0);
} }
function ClusterVisualizationResourceComponent_Conditional_1_Template(rf, ctx) { if (rf & 1) {
    const _r2 = i0.ɵɵgetCurrentView();
    i0.ɵɵelementStart(0, "mj-page-layout")(1, "mj-page-header", 3)(2, "div", 4)(3, "button", 5);
    i0.ɵɵlistener("click", function ClusterVisualizationResourceComponent_Conditional_1_Template_button_click_3_listener() { i0.ɵɵrestoreView(_r2); const ctx_r2 = i0.ɵɵnextContext(); return i0.ɵɵresetView(ctx_r2.OnNewAnalysis()); });
    i0.ɵɵelement(4, "i", 6);
    i0.ɵɵelementStart(5, "span", 7);
    i0.ɵɵtext(6, "New Analysis");
    i0.ɵɵelementEnd()()()();
    i0.ɵɵelementStart(7, "mj-page-body", 8);
    i0.ɵɵtemplate(8, ClusterVisualizationResourceComponent_Conditional_1_ng_container_8_Template, 1, 0, "ng-container", 2);
    i0.ɵɵelementEnd()();
} if (rf & 2) {
    i0.ɵɵnextContext();
    const clusterBody_r1 = i0.ɵɵreference(3);
    i0.ɵɵadvance(7);
    i0.ɵɵproperty("Flex", true)("Padding", false);
    i0.ɵɵadvance();
    i0.ɵɵproperty("ngTemplateOutlet", clusterBody_r1);
} }
function ClusterVisualizationResourceComponent_ng_template_2_For_8_Template(rf, ctx) { if (rf & 1) {
    const _r5 = i0.ɵɵgetCurrentView();
    i0.ɵɵelementStart(0, "div", 23);
    i0.ɵɵlistener("click", function ClusterVisualizationResourceComponent_ng_template_2_For_8_Template_div_click_0_listener() { const saved_r6 = i0.ɵɵrestoreView(_r5).$implicit; const ctx_r2 = i0.ɵɵnextContext(2); return i0.ɵɵresetView(ctx_r2.OnSelectSaved(saved_r6)); });
    i0.ɵɵelementStart(1, "div", 24);
    i0.ɵɵtext(2);
    i0.ɵɵelementEnd();
    i0.ɵɵelementStart(3, "div", 25)(4, "span");
    i0.ɵɵtext(5);
    i0.ɵɵelementEnd();
    i0.ɵɵelementStart(6, "span");
    i0.ɵɵtext(7);
    i0.ɵɵelementEnd()();
    i0.ɵɵelementStart(8, "button", 26);
    i0.ɵɵlistener("click", function ClusterVisualizationResourceComponent_ng_template_2_For_8_Template_button_click_8_listener($event) { const saved_r6 = i0.ɵɵrestoreView(_r5).$implicit; const ctx_r2 = i0.ɵɵnextContext(2); return i0.ɵɵresetView(ctx_r2.OnDeleteSaved(saved_r6, $event)); });
    i0.ɵɵelement(9, "i", 27);
    i0.ɵɵelementEnd()();
} if (rf & 2) {
    const saved_r6 = ctx.$implicit;
    const ctx_r2 = i0.ɵɵnextContext(2);
    i0.ɵɵclassProp("active", ctx_r2.IsActiveSaved(saved_r6));
    i0.ɵɵadvance(2);
    i0.ɵɵtextInterpolate(saved_r6.Name);
    i0.ɵɵadvance(3);
    i0.ɵɵtextInterpolate(saved_r6.EntityName);
    i0.ɵɵadvance(2);
    i0.ɵɵtextInterpolate(ctx_r2.FormatDate(saved_r6.CreatedAt));
} }
function ClusterVisualizationResourceComponent_ng_template_2_Conditional_9_Template(rf, ctx) { if (rf & 1) {
    i0.ɵɵelementStart(0, "div", 15);
    i0.ɵɵelement(1, "i", 28);
    i0.ɵɵelementStart(2, "p");
    i0.ɵɵtext(3, "No saved visualizations yet");
    i0.ɵɵelementEnd()();
} }
function ClusterVisualizationResourceComponent_ng_template_2_Conditional_14_Template(rf, ctx) { if (rf & 1) {
    i0.ɵɵelementStart(0, "div", 29);
    i0.ɵɵelement(1, "i", 30);
    i0.ɵɵtext(2, " Records: ");
    i0.ɵɵelementStart(3, "span", 31);
    i0.ɵɵtext(4);
    i0.ɵɵelementEnd()();
    i0.ɵɵelementStart(5, "div", 29);
    i0.ɵɵelement(6, "i", 32);
    i0.ɵɵtext(7, " Clusters: ");
    i0.ɵɵelementStart(8, "span", 31);
    i0.ɵɵtext(9);
    i0.ɵɵelementEnd()();
    i0.ɵɵelementStart(10, "div", 29);
    i0.ɵɵelement(11, "i", 33);
    i0.ɵɵtext(12, " Silhouette: ");
    i0.ɵɵelementStart(13, "span", 31);
    i0.ɵɵtext(14);
    i0.ɵɵelementEnd()();
    i0.ɵɵelementStart(15, "div", 29);
    i0.ɵɵelement(16, "i", 34);
    i0.ɵɵtext(17, " Computed: ");
    i0.ɵɵelementStart(18, "span", 31);
    i0.ɵɵtext(19);
    i0.ɵɵelementEnd()();
} if (rf & 2) {
    const ctx_r2 = i0.ɵɵnextContext(2);
    i0.ɵɵadvance(4);
    i0.ɵɵtextInterpolate(ctx_r2.Result.Metrics.RecordCount);
    i0.ɵɵadvance(5);
    i0.ɵɵtextInterpolate(ctx_r2.Result.Metrics.ClusterCount);
    i0.ɵɵadvance(4);
    i0.ɵɵclassProp("good", ctx_r2.IsSilhouetteGood);
    i0.ɵɵadvance();
    i0.ɵɵtextInterpolate1(" ", ctx_r2.SilhouetteScoreFormatted, " ");
    i0.ɵɵadvance(5);
    i0.ɵɵtextInterpolate(ctx_r2.ComputationTimeFormatted);
} }
function ClusterVisualizationResourceComponent_ng_template_2_Conditional_18_Template(rf, ctx) { if (rf & 1) {
    i0.ɵɵelementStart(0, "div", 21);
    i0.ɵɵelement(1, "i", 35);
    i0.ɵɵelementStart(2, "span");
    i0.ɵɵtext(3);
    i0.ɵɵelementEnd()();
} if (rf & 2) {
    const ctx_r2 = i0.ɵɵnextContext(2);
    i0.ɵɵadvance(3);
    i0.ɵɵtextInterpolate(ctx_r2.RunError);
} }
function ClusterVisualizationResourceComponent_ng_template_2_Template(rf, ctx) { if (rf & 1) {
    const _r4 = i0.ɵɵgetCurrentView();
    i0.ɵɵelementStart(0, "div", 9)(1, "div", 10)(2, "div", 11)(3, "h2");
    i0.ɵɵelement(4, "i", 12);
    i0.ɵɵtext(5, " Saved Visualizations");
    i0.ɵɵelementEnd()();
    i0.ɵɵelementStart(6, "div", 13);
    i0.ɵɵrepeaterCreate(7, ClusterVisualizationResourceComponent_ng_template_2_For_8_Template, 10, 5, "div", 14, i0.ɵɵcomponentInstance().TrackSavedBy, true);
    i0.ɵɵconditionalCreate(9, ClusterVisualizationResourceComponent_ng_template_2_Conditional_9_Template, 4, 0, "div", 15);
    i0.ɵɵelementEnd()();
    i0.ɵɵelementStart(10, "div", 16)(11, "div", 17)(12, "div", 18);
    i0.ɵɵtext(13);
    i0.ɵɵelementEnd();
    i0.ɵɵconditionalCreate(14, ClusterVisualizationResourceComponent_ng_template_2_Conditional_14_Template, 20, 6);
    i0.ɵɵelementEnd();
    i0.ɵɵelementStart(15, "div", 19)(16, "mj-cluster-scatter", 20, 1);
    i0.ɵɵlistener("PointClicked", function ClusterVisualizationResourceComponent_ng_template_2_Template_mj_cluster_scatter_PointClicked_16_listener($event) { i0.ɵɵrestoreView(_r4); const ctx_r2 = i0.ɵɵnextContext(); return i0.ɵɵresetView(ctx_r2.OnPointClicked($event)); })("PointHovered", function ClusterVisualizationResourceComponent_ng_template_2_Template_mj_cluster_scatter_PointHovered_16_listener($event) { i0.ɵɵrestoreView(_r4); const ctx_r2 = i0.ɵɵnextContext(); return i0.ɵɵresetView(ctx_r2.OnPointHovered($event)); })("OpenRecordRequested", function ClusterVisualizationResourceComponent_ng_template_2_Template_mj_cluster_scatter_OpenRecordRequested_16_listener($event) { i0.ɵɵrestoreView(_r4); const ctx_r2 = i0.ɵɵnextContext(); return i0.ɵɵresetView(ctx_r2.OnOpenRecord($event)); })("LabelEdited", function ClusterVisualizationResourceComponent_ng_template_2_Template_mj_cluster_scatter_LabelEdited_16_listener($event) { i0.ɵɵrestoreView(_r4); const ctx_r2 = i0.ɵɵnextContext(); return i0.ɵɵresetView(ctx_r2.OnLabelEdited($event)); });
    i0.ɵɵelementEnd();
    i0.ɵɵconditionalCreate(18, ClusterVisualizationResourceComponent_ng_template_2_Conditional_18_Template, 4, 1, "div", 21);
    i0.ɵɵelementStart(19, "mj-cluster-config-panel", 22);
    i0.ɵɵlistener("RunClustering", function ClusterVisualizationResourceComponent_ng_template_2_Template_mj_cluster_config_panel_RunClustering_19_listener($event) { i0.ɵɵrestoreView(_r4); const ctx_r2 = i0.ɵɵnextContext(); return i0.ɵɵresetView(ctx_r2.OnRunClustering($event)); })("DimensionsChanged", function ClusterVisualizationResourceComponent_ng_template_2_Template_mj_cluster_config_panel_DimensionsChanged_19_listener($event) { i0.ɵɵrestoreView(_r4); const ctx_r2 = i0.ɵɵnextContext(); return i0.ɵɵresetView(ctx_r2.OnDimensionsChanged($event)); })("SaveVisualization", function ClusterVisualizationResourceComponent_ng_template_2_Template_mj_cluster_config_panel_SaveVisualization_19_listener() { i0.ɵɵrestoreView(_r4); const ctx_r2 = i0.ɵɵnextContext(); return i0.ɵɵresetView(ctx_r2.OnSaveVisualization()); });
    i0.ɵɵelementEnd()()()();
} if (rf & 2) {
    const ctx_r2 = i0.ɵɵnextContext();
    i0.ɵɵadvance(7);
    i0.ɵɵrepeater(ctx_r2.SavedVisualizations);
    i0.ɵɵadvance(2);
    i0.ɵɵconditional(ctx_r2.SavedVisualizations.length === 0 ? 9 : -1);
    i0.ɵɵadvance(4);
    i0.ɵɵtextInterpolate(ctx_r2.VisualizationTitle);
    i0.ɵɵadvance();
    i0.ɵɵconditional(ctx_r2.HasResult ? 14 : -1);
    i0.ɵɵadvance(2);
    i0.ɵɵproperty("Points", (ctx_r2.Result == null ? null : ctx_r2.Result.Points) ?? i0.ɵɵpureFunction0(14, _c1))("Clusters", (ctx_r2.Result == null ? null : ctx_r2.Result.Clusters) ?? i0.ɵɵpureFunction0(15, _c1))("IsLoading", ctx_r2.IsRunning)("ColorBy", ctx_r2.ActiveConfig.ColorBy ?? "cluster")("EntityName", ctx_r2.ActiveConfig.EntityName);
    i0.ɵɵadvance(2);
    i0.ɵɵconditional(ctx_r2.RunError ? 18 : -1);
    i0.ɵɵadvance();
    i0.ɵɵproperty("IsRunning", ctx_r2.IsRunning)("Metrics", ctx_r2.Metrics)("EntityOptions", ctx_r2.EntityOptions)("EntityDocOptions", ctx_r2.EntityDocOptions)("AllEntityDocOptions", ctx_r2.AllEntityDocOptions);
} }
/**
 * Build an environment-scoped storage key so cluster data does not bleed
 * across different database environments that share the same browser.
 * Uses the GraphQL endpoint origin as the environment fingerprint.
 */
function buildEnvScopedKey(base) {
    try {
        const origin = Metadata.Provider.ConfigData?.URL; // global-provider-ok: free helper function — no component context
        if (origin) {
            // Use just the origin portion (protocol + host) so path differences don't fragment keys
            const url = new URL(origin);
            return `${base}_${url.origin}`;
        }
    }
    catch { /* fall through */ }
    return base;
}
const SAVED_CLUSTERS_BASE_KEY = 'KnowledgeHub_SavedClusters';
const LAST_SESSION_BASE_KEY = 'KnowledgeHub_LastClusterSession';
let ClusterVisualizationResourceComponent = class ClusterVisualizationResourceComponent extends BaseResourceComponent {
    scatterPlot;
    cdr = inject(ChangeDetectorRef);
    clusteringService = inject(ClusteringService);
    activityService = inject(ActivityService);
    navigationService = inject(NavigationService);
    destroy$ = new Subject();
    /** LLM-generated cluster labels for the current result */
    ClusterLabels = [];
    /**
     * When true, this component is embedded inside the Visualize host surface.
     * In that case the host owns the resource lifecycle (NotifyLoadComplete,
     * agent context) and record navigation, so this component suppresses those
     * and instead emits open-record intents via {@link OpenRecordRequested}.
     */
    Embedded = false;
    /** Emitted (only when Embedded) to ask the host to open an entity record. */
    OpenRecordRequested = new EventEmitter();
    // ================================================================
    // Resource overrides
    // ================================================================
    async GetResourceDisplayName(_data) {
        return 'Clusters';
    }
    async GetResourceIconClass(_data) {
        return 'fa-solid fa-circle-nodes';
    }
    // ================================================================
    // State
    // ================================================================
    /** Current visualization result */
    Result = null;
    /** Whether a clustering run is in progress */
    IsRunning = false;
    /** The active cluster config */
    ActiveConfig = DefaultClusterConfig();
    /** Title for the top bar */
    VisualizationTitle = 'New Cluster Analysis';
    /** Entity options for the config panel dropdown */
    EntityOptions = [];
    /** Entity document options for the selected entity (shown when 2+) */
    EntityDocOptions = [];
    /** All entity documents across entities (for the multi-entity source selector) */
    AllEntityDocOptions = [];
    /** User-facing error from the last run (e.g. multi-entity embedding mismatch) */
    RunError = null;
    /** Ordered field keys for prioritized display in scatter tooltip/detail */
    FieldPriority = [];
    /** Map of field names to human-readable display names */
    FieldDisplayNames = {};
    // Saved visualizations
    SavedVisualizations = [];
    ActiveSavedId = null;
    // User setting entity for persistence
    userSettingEntity = null;
    // ================================================================
    // Lifecycle
    // ================================================================
    async ngAfterViewInit() {
        await this.loadEntityOptions();
        this.loadSavedVisualizations();
        this.restoreLastSession();
        // When embedded in the Visualize host, the host owns agent context +
        // the resource load lifecycle; skip them here to avoid double-reporting.
        if (!this.Embedded) {
            this.emitAgentContext();
            this.registerAgentTools();
            this.NotifyLoadComplete();
        }
    }
    // ================================================================
    // Agent context + client tools
    // ================================================================
    /**
     * Publish the current clustering state to the AI agent. Re-emitted whenever
     * a run completes, a saved visualization is selected, the source entity or
     * algorithm changes, or the analysis is reset — so the streamed context
     * tracks what's on screen. No-op when embedded (the Visualize host owns
     * reporting). Deepened to Data-Explorer depth: per-cluster summaries (id +
     * resolved label + point count), the active source/algorithm/dimensions
     * config, the silhouette quality score, the available source entities, and
     * the saved-visualization landscape (id+name of the active one).
     */
    emitAgentContext() {
        if (this.Embedded) {
            return;
        }
        const activeSaved = this.ActiveSavedId
            ? this.SavedVisualizations.find(s => s.Id === this.ActiveSavedId)
            : undefined;
        this.navigationService.SetAgentContext(this, buildClusterAgentContext({
            IsVisualizationLoaded: this.HasResult,
            VisualizationTitle: this.VisualizationTitle || null,
            IsRunning: this.IsRunning,
            RunError: this.RunError,
            ConfigEntityName: this.ActiveConfig.EntityName || '',
            ConfigAlgorithm: this.ActiveConfig.Algorithm,
            ConfigDimensions: this.ActiveConfig.Dimensions ?? 2,
            ClusterCount: this.Result?.Clusters?.length ?? 0,
            TotalPoints: this.Result?.Points?.length ?? 0,
            Clusters: this.buildClusterSummaries(),
            SilhouetteScore: this.Result?.Metrics?.SilhouetteScore ?? null,
            AvailableEntityNames: this.EntityOptions.map(o => o.Name),
            SavedVisualizationCount: this.SavedVisualizations.length,
            SavedVisualizationNames: this.SavedVisualizations.map(s => s.Name),
            ActiveSavedId: this.ActiveSavedId,
            ActiveSavedName: activeSaved?.Name ?? null,
        }));
    }
    /**
     * Build per-cluster summaries (id, resolved label, point count) for the
     * agent context. The label prefers an LLM-generated / user-edited
     * {@link ClusterLabels} entry, then the cluster's own Label, then a generic
     * "Cluster N".
     */
    buildClusterSummaries() {
        if (!this.Result || this.Result.Clusters.length === 0) {
            return [];
        }
        return this.Result.Clusters.map(cluster => {
            const labelEntry = this.ClusterLabels.find(l => l.ClusterId === cluster.Id);
            const label = labelEntry?.Label || cluster.Label || `Cluster ${cluster.Id}`;
            const pointCount = this.Result.Points.filter(p => p.ClusterId === cluster.Id).length;
            return { ClusterId: cluster.Id, Label: label, PointCount: pointCount };
        });
    }
    /**
     * 🚨 SAFETY BOUNDARY (Knowledge Hub · Clusters)
     * ------------------------------------------------------------------
     * The agent may RE-RUN / RESET / RE-PROJECT / RE-NAME a clustering
     * visualization, SET the source entity & algorithm before a run, and
     * OPEN a saved visualization or the underlying entity record. All of
     * these are idempotent recomputes or read-only navigations.
     *
     * Intentionally NOT exposed:
     *   - DELETE a saved visualization (OnDeleteSaved — destructive, irreversible)
     *   - SAVE a visualization (OnSaveVisualization — writes a User Setting row;
     *     left to the deliberate UI action)
     * Tolerant handlers: every Handler returns { Success, Data?, ErrorMessage? }
     * and never throws; agent-supplied references resolve id→name→contains.
     */
    registerAgentTools() {
        this.navigationService.SetAgentClientTools(this, [
            {
                Name: 'RegenerateClusters',
                Description: 'Re-run the clustering analysis using the currently configured entity, algorithm, and parameters.',
                ParameterSchema: { type: 'object', properties: {} },
                Handler: async () => {
                    if (this.IsRunning) {
                        return { Success: false, ErrorMessage: 'A clustering run is already in progress' };
                    }
                    await this.OnRunClustering(this.ActiveConfig);
                    return this.RunError
                        ? { Success: false, ErrorMessage: this.RunError }
                        : { Success: true, Data: { ClusterCount: this.Result?.Clusters?.length ?? 0, TotalPoints: this.Result?.Points?.length ?? 0 } };
                },
            },
            {
                Name: 'ResetClusterAnalysis',
                Description: 'Clear the current cluster visualization and start a fresh analysis.',
                ParameterSchema: { type: 'object', properties: {} },
                Handler: async () => {
                    this.OnNewAnalysis();
                    return { Success: true };
                },
            },
            {
                Name: 'SetClusterSourceEntity',
                Description: 'Set the source entity that the next clustering run will pull vectors from (by name). Does not run the analysis — call RegenerateClusters after.',
                ParameterSchema: {
                    type: 'object',
                    properties: { entityName: { type: 'string', description: 'The entity name to cluster (e.g. "Companies").' } },
                    required: ['entityName'],
                },
                Handler: async (params) => {
                    const check = validateStringParam(params['entityName'], 'entityName');
                    if (!check.ok) {
                        return check.result;
                    }
                    const candidates = this.EntityOptions.map(o => ({ Id: o.Name, Name: o.Name }));
                    const match = resolveSavedVisualization(check.value, candidates);
                    if (!match) {
                        return buildClusterNotFoundError(check.value, candidates.map(c => c.Name), 'source entity');
                    }
                    this.OnEntitySourceChanged(match.Name);
                    return { Success: true, Data: { ConfigEntityName: match.Name } };
                },
            },
            {
                Name: 'SetClusterAlgorithm',
                Description: 'Set the clustering algorithm for the next run (kmeans or dbscan). Does not run the analysis — call RegenerateClusters after.',
                ParameterSchema: {
                    type: 'object',
                    properties: { algorithm: { type: 'string', enum: ['kmeans', 'dbscan'] } },
                    required: ['algorithm'],
                },
                Handler: async (params) => {
                    const check = validateEnumParam(params['algorithm'], ['kmeans', 'dbscan'], 'algorithm');
                    if (!check.ok) {
                        return check.result;
                    }
                    this.ActiveConfig = { ...this.ActiveConfig, Algorithm: check.value };
                    this.emitAgentContext();
                    this.cdr.detectChanges();
                    return { Success: true, Data: { ConfigAlgorithm: check.value } };
                },
            },
            {
                Name: 'SetClusterDimensions',
                Description: 'Switch the projection between 2D and 3D. Re-projects immediately when a visualization is already loaded.',
                ParameterSchema: {
                    type: 'object',
                    properties: { dimensions: { type: 'number', enum: [2, 3] } },
                    required: ['dimensions'],
                },
                Handler: async (params) => {
                    const dims = params['dimensions'];
                    if (dims !== 2 && dims !== 3) {
                        return { Success: false, ErrorMessage: 'dimensions must be 2 or 3.' };
                    }
                    this.OnDimensionsChanged(dims);
                    return { Success: true, Data: { ConfigDimensions: dims } };
                },
            },
            {
                Name: 'OpenSavedVisualization',
                Description: 'Open a previously saved cluster visualization from the sidebar, by id or name. Restores it instantly when cached.',
                ParameterSchema: {
                    type: 'object',
                    properties: { reference: { type: 'string', description: 'The saved visualization id or display name.' } },
                    required: ['reference'],
                },
                Handler: async (params) => {
                    const check = validateStringParam(params['reference'], 'reference');
                    if (!check.ok) {
                        return check.result;
                    }
                    const match = resolveSavedVisualization(check.value, this.SavedVisualizations);
                    if (!match) {
                        return buildClusterNotFoundError(check.value, this.SavedVisualizations.map(s => s.Name), 'saved visualization');
                    }
                    await this.OnSelectSaved(match);
                    return { Success: true, Data: { Name: match.Name, ClusterCount: this.Result?.Clusters?.length ?? 0 } };
                },
            },
            {
                Name: 'ListSavedVisualizations',
                Description: 'List the saved cluster visualizations available in the sidebar (bounded).',
                ParameterSchema: { type: 'object', properties: {} },
                Handler: async () => {
                    return {
                        Success: true,
                        Data: {
                            SavedVisualizations: capClusterList(this.SavedVisualizations.map(s => ({ Id: s.Id, Name: s.Name }))),
                            TotalCount: this.SavedVisualizations.length,
                        },
                    };
                },
            },
        ]);
    }
    /**
     * Set the source entity for the next clustering run and refresh the dependent
     * document options. Idempotent; emits agent context. Exposed to the agent via
     * SetClusterSourceEntity and usable from the config panel.
     */
    OnEntitySourceChanged(entityName) {
        this.ActiveConfig = { ...this.ActiveConfig, EntityName: entityName, EntityDocumentID: '' };
        this.updateEntityDocOptions(entityName);
        this.emitAgentContext();
        this.cdr.detectChanges();
    }
    ngOnDestroy() {
        super.ngOnDestroy();
        this.destroy$.next();
        this.destroy$.complete();
    }
    // ================================================================
    // Getters
    // ================================================================
    get Metrics() {
        return this.Result?.Metrics ?? null;
    }
    get HasResult() {
        return this.Result != null && this.Result.Points.length > 0;
    }
    get SilhouetteScoreFormatted() {
        return this.Result?.Metrics?.SilhouetteScore?.toFixed(2) ?? '--';
    }
    get IsSilhouetteGood() {
        return (this.Result?.Metrics?.SilhouetteScore ?? 0) >= 0.5;
    }
    get ComputationTimeFormatted() {
        const ms = this.Result?.Metrics?.ComputationTimeMs ?? 0;
        return ms >= 1000 ? `${(ms / 1000).toFixed(1)}s` : `${ms}ms`;
    }
    // ================================================================
    // Public Methods
    // ================================================================
    /** Handle "Run Clustering" from the config panel */
    async OnRunClustering(config) {
        this.IsRunning = true;
        this.ActiveConfig = config;
        this.ClusterLabels = [];
        this.RunError = null;
        // Auto-hide detail panel from previous visualization
        this.scatterPlot?.CloseDetailPanel();
        // Update entity doc options if entity changed
        this.updateEntityDocOptions(config.EntityName);
        this.cdr.detectChanges();
        const activityID = this.activityService.Start('Cluster analysis', {
            icon: 'fa-solid fa-circle-nodes',
            detail: `${config.EntityName || 'Multiple sources'} · ${config.Algorithm === 'kmeans' ? 'K-Means' : 'DBSCAN'}`,
        });
        try {
            // Fetch vectors from the vector database
            const vectors = await this.fetchVectorsForEntity(config);
            // Run clustering (client-side UMAP + K-Means/DBSCAN)
            this.Result = await this.clusteringService.RunClustering(vectors, config);
            this.activityService.Complete(activityID, 'success', `${this.Result.Points.length} points · ${this.Result.Clusters.length} clusters`);
            this.VisualizationTitle = `${config.EntityName} — ${config.Algorithm === 'kmeans' ? 'K-Means' : 'DBSCAN'}`;
            this.FieldPriority = this.ComputeFieldPriority(config.EntityName);
            // Fire LLM cluster naming in the background (non-blocking).
            // Clusters render immediately; labels appear when LLM responds.
            this.requestClusterLabelsFromLLM().catch(err => console.warn('[ClusterVisualization] Background naming failed:', err));
            // Auto-save session state so it can be restored after navigation
            this.saveLastSession();
        }
        catch (error) {
            console.error('[ClusterVisualization] Pipeline error:', error);
            this.Result = null;
            this.RunError = error instanceof Error ? error.message : String(error);
            this.activityService.Complete(activityID, 'error', this.RunError);
        }
        finally {
            this.IsRunning = false;
            this.emitAgentContext();
            this.cdr.detectChanges();
        }
    }
    /**
     * Re-run when the user flips 2D⇄3D so the projection updates immediately.
     * A 3D layout needs a Z coordinate that only a fresh projection produces, so
     * toggling alone wouldn't change the existing plot.
     */
    OnDimensionsChanged(dims) {
        this.ActiveConfig = { ...this.ActiveConfig, Dimensions: dims };
        if (this.Result && this.Result.Points.length > 0 && !this.IsRunning) {
            void this.OnRunClustering(this.ActiveConfig);
        }
    }
    /** Handle point click — log for now */
    OnPointClicked(_point) {
        // Detail panel is handled by the scatter component internally
    }
    /** Handle point hover */
    OnPointHovered(_point) {
        // Tooltip display handled by scatter component
    }
    /** Navigate to the entity record when "Open Record" is clicked in the detail panel */
    OnOpenRecord(point) {
        const entityName = point.Metadata?.['Entity'];
        const recordID = point.Metadata?.['RecordID'];
        if (!entityName || !recordID)
            return;
        // When embedded, defer navigation to the host (shared drilldown owner).
        if (this.Embedded) {
            this.OpenRecordRequested.emit({ EntityName: entityName, RecordID: recordID });
            return;
        }
        const compositeKey = new CompositeKey();
        compositeKey.SimpleLoadFromURLSegment(recordID);
        this.navigationService.OpenEntityRecord(entityName, compositeKey);
    }
    /** Save the current visualization */
    async OnSaveVisualization() {
        if (!this.Result)
            return;
        const id = crypto.randomUUID();
        const saved = {
            Id: id,
            Name: this.VisualizationTitle,
            EntityName: this.ActiveConfig.EntityName,
            Algorithm: this.ActiveConfig.Algorithm,
            Params: { ...this.ActiveConfig },
            CreatedAt: new Date().toISOString(),
            Result: this.stripVectorsFromResult(this.Result),
            Viewport: this.scatterPlot?.GetViewportTransform(),
            ClusterLabels: this.ClusterLabels.length > 0 ? [...this.ClusterLabels] : undefined,
        };
        this.SavedVisualizations = [saved, ...this.SavedVisualizations];
        this.ActiveSavedId = id;
        await this.persistSavedVisualizations();
        this.cdr.detectChanges();
    }
    /** Select a saved visualization — restore from cache if available, otherwise re-run */
    async OnSelectSaved(saved) {
        this.scatterPlot?.CloseDetailPanel();
        this.ActiveSavedId = saved.Id;
        this.VisualizationTitle = saved.Name;
        // Reconstruct config
        const config = {
            ...DefaultClusterConfig(),
            ...saved.Params,
            EntityName: saved.EntityName,
            Algorithm: saved.Algorithm,
        };
        this.ActiveConfig = config;
        // If we have cached results, restore instantly without re-running
        if (saved.Result) {
            this.Result = saved.Result;
            this.ClusterLabels = saved.ClusterLabels ?? [];
            this.applyLabelsToResult();
            this.cdr.detectChanges();
            // Restore viewport after a tick (scatter needs to render first)
            if (saved.Viewport) {
                setTimeout(() => {
                    this.scatterPlot?.SetViewportTransform(saved.Viewport);
                    this.cdr.detectChanges();
                }, 50);
            }
            this.emitAgentContext();
            return;
        }
        // No cached results — re-run from scratch (OnRunClustering re-emits context)
        await this.OnRunClustering(config);
    }
    /** Delete a saved visualization */
    async OnDeleteSaved(saved, event) {
        event.stopPropagation();
        this.SavedVisualizations = this.SavedVisualizations.filter(s => s.Id !== saved.Id);
        if (this.ActiveSavedId === saved.Id) {
            this.ActiveSavedId = null;
        }
        await this.persistSavedVisualizations();
        this.cdr.detectChanges();
    }
    /** Start a new analysis (clear current) */
    OnNewAnalysis() {
        this.scatterPlot?.CloseDetailPanel();
        this.ActiveSavedId = null;
        this.Result = null;
        this.ClusterLabels = [];
        this.VisualizationTitle = 'New Cluster Analysis';
        this.ActiveConfig = DefaultClusterConfig();
        this.emitAgentContext();
        this.cdr.detectChanges();
    }
    IsActiveSaved(saved) {
        return this.ActiveSavedId === saved.Id;
    }
    TrackSavedBy(_index, saved) {
        return saved.Id;
    }
    FormatDate(iso) {
        try {
            return new Date(iso).toLocaleDateString();
        }
        catch {
            return iso;
        }
    }
    // ================================================================
    // Private Methods
    // ================================================================
    /** Populate the entity options for the config panel dropdown */
    async loadEntityOptions() {
        try {
            // Use KnowledgeHubMetadataEngine for cached entity document data
            const engine = KnowledgeHubMetadataEngine.Instance;
            await engine.Config(false);
            const entityNames = engine.GetEntitiesWithDocuments();
            if (entityNames.length > 0) {
                this.EntityOptions = entityNames.map(name => ({ Name: name }));
            }
            // Build the cross-entity document list that powers the multi-entity
            // source selector (each option tagged with its owning entity).
            const allDocs = [];
            for (const name of entityNames) {
                const docs = engine.GetEntityDocumentsForEntity(name).filter(d => d.Status === 'Active');
                for (const d of docs) {
                    allDocs.push({ ID: d.ID, Name: d.Name, EntityName: name });
                }
            }
            this.AllEntityDocOptions = allDocs;
            // Set default entity if config is blank
            if (this.EntityOptions.length > 0 && !this.ActiveConfig.EntityName) {
                this.ActiveConfig.EntityName = this.EntityOptions[0].Name;
            }
            // Populate entity doc options for the default entity
            this.updateEntityDocOptions(this.ActiveConfig.EntityName);
        }
        catch (error) {
            console.warn('[ClusterVisualization] Error loading entity options:', error);
        }
        this.cdr.detectChanges();
    }
    /** Update entity document options when the selected entity changes */
    updateEntityDocOptions(entityName) {
        if (!entityName) {
            this.EntityDocOptions = [];
            return;
        }
        const engine = KnowledgeHubMetadataEngine.Instance;
        const docs = engine.GetEntityDocumentsForEntity(entityName)
            .filter(d => d.Status === 'Active');
        this.EntityDocOptions = docs.map(d => ({ ID: d.ID, Name: d.Name }));
        // Auto-select the first doc (or clear if none)
        if (docs.length > 0 && !this.ActiveConfig.EntityDocumentID) {
            this.ActiveConfig.EntityDocumentID = docs[0].ID;
        }
        else if (docs.length === 0) {
            this.ActiveConfig.EntityDocumentID = '';
        }
    }
    /**
     * Fetch vectors from Entity Document Runs via RunView.
     * Entity Documents store vectorized entity data; we query for runs
     * that match the requested entity.
     */
    async fetchVectorsForEntity(config) {
        const docIDs = this.resolveDocIDs(config);
        if (docIDs.length === 0) {
            return [];
        }
        const isMulti = docIDs.length > 1;
        const docEntityMap = new Map(this.AllEntityDocOptions.map(d => [d.ID, d.EntityName ?? '']));
        const provider = this.ProviderToUse;
        const aiClient = new GraphQLAIClient(provider);
        const vectors = [];
        let expectedLen = -1;
        for (const docID of docIDs) {
            const result = await aiClient.FetchEntityVectors({
                entityDocumentID: docID,
                maxRecords: config.MaxRecords,
                filter: config.Filter || undefined,
            });
            if (!result.Success || result.Results.length === 0)
                continue;
            for (const item of result.Results) {
                if (!item.Values || item.Values.length === 0)
                    continue;
                // Hard-block multi-entity embedding mismatches: vectors of different
                // dimensionalities live in different spaces and aren't co-clusterable.
                if (isMulti) {
                    if (expectedLen === -1) {
                        expectedLen = item.Values.length;
                    }
                    else if (item.Values.length !== expectedLen) {
                        throw new Error('The selected documents use different embedding models (vector sizes differ), ' +
                            'so their points are not comparable. Pick documents that share the same embedding model.');
                    }
                }
                const metadata = this.parseVectorMetadata(item.Metadata);
                // Ensure each point knows its source entity for color-by-entity.
                if (isMulti && !metadata['EntityName']) {
                    metadata['EntityName'] = docEntityMap.get(docID) ?? metadata['Entity'] ?? '';
                }
                const label = this.buildLabel(metadata);
                vectors.push({
                    Key: item.ID,
                    Label: label,
                    Vector: item.Values,
                    Metadata: metadata,
                });
            }
        }
        return vectors;
    }
    /** Resolve which entity-document IDs to source vectors from for a run. */
    resolveDocIDs(config) {
        if (config.EntityDocumentIDs && config.EntityDocumentIDs.length > 0) {
            return config.EntityDocumentIDs;
        }
        if (config.EntityDocumentID) {
            return [config.EntityDocumentID];
        }
        const engine = KnowledgeHubMetadataEngine.Instance;
        const docs = engine.GetEntityDocumentsForEntity(config.EntityName).filter(d => d.Status === 'Active');
        return docs.length > 0 ? [docs[0].ID] : [];
    }
    /** Parse the JSON metadata string from the vector DB into a record */
    parseVectorMetadata(metadataJson) {
        try {
            return JSON.parse(metadataJson);
        }
        catch {
            return {};
        }
    }
    /**
     * Build a human-readable label from vector metadata using entity field metadata.
     * Combines all IsNameField fields in Sequence order (e.g., "Sarah Chen" from FirstName + LastName).
     * Falls back to heuristic field detection when entity metadata isn't available.
     */
    buildLabel(metadata) {
        const entityName = metadata['Entity'];
        if (entityName) {
            try {
                const md = this.ProviderToUse;
                const entityInfo = md.Entities.find(e => e.Name === entityName);
                if (entityInfo) {
                    // Combine all IsNameField fields in Sequence order
                    const nameFields = entityInfo.Fields
                        .filter(f => f.IsNameField)
                        .sort((a, b) => (a.Sequence ?? 9999) - (b.Sequence ?? 9999));
                    if (nameFields.length > 0) {
                        const parts = nameFields
                            .map(f => metadata[f.Name])
                            .filter(v => v != null && v.trim() !== '');
                        if (parts.length > 0)
                            return parts.join(' ');
                    }
                    // Single NameField fallback
                    if (entityInfo.NameField && metadata[entityInfo.NameField.Name]) {
                        return metadata[entityInfo.NameField.Name];
                    }
                }
            }
            catch { /* metadata not available, fall through */ }
        }
        // Heuristic fallbacks
        return metadata['Name']
            || metadata['Title']
            || metadata['Description']?.substring(0, 60)
            || metadata['RecordID']
            || 'Unknown';
    }
    // ================================================================
    // LLM Cluster Naming
    // ================================================================
    /**
     * After clustering completes, send sample records per cluster to an LLM for naming.
     * Updates ClusterLabels and refreshes the view.
     */
    async requestClusterLabelsFromLLM() {
        if (!this.Result || this.Result.Clusters.length === 0)
            return;
        try {
            const clusterData = this.buildClusterDataForPrompt(this.Result);
            if (!clusterData)
                return;
            // AIEngineBase is deferred at startup; ensure it's loaded before reading .Prompts.
            await AIEngineBase.Instance.EnsureLoaded();
            // Look up the "Cluster Naming" prompt from AIEngineBase cached metadata
            const promptEntity = AIEngineBase.Instance.Prompts.find(p => p.Name === 'Cluster Naming');
            if (!promptEntity) {
                console.warn('[ClusterVisualization] "Cluster Naming" prompt not found — run metadata sync');
                return;
            }
            const provider = this.ProviderToUse;
            const aiClient = new GraphQLAIClient(provider);
            const result = await aiClient.RunAIPrompt({
                promptId: promptEntity.ID,
                data: { clusterData },
            });
            if (result.success && result.parsedResult) {
                const labels = result.parsedResult;
                this.ClusterLabels = labels.map(l => ({
                    ClusterId: l.clusterId,
                    Label: l.label,
                    IsUserEdited: false,
                }));
                this.applyLabelsToResult();
                this.cdr.detectChanges();
            }
        }
        catch (error) {
            console.warn('[ClusterVisualization] LLM cluster naming failed:', error);
        }
    }
    /** Build a text block describing sample records per cluster for the LLM prompt */
    buildClusterDataForPrompt(result) {
        if (!result.Clusters || result.Clusters.length === 0)
            return null;
        const lines = [];
        for (const cluster of result.Clusters) {
            // Get points in this cluster, take up to 5 samples
            const clusterPoints = result.Points.filter(p => p.ClusterId === cluster.Id);
            const samples = clusterPoints.slice(0, 5);
            if (samples.length === 0)
                continue;
            lines.push(`### Cluster ${cluster.Id} (${clusterPoints.length} records)`);
            for (const point of samples) {
                const meta = point.Metadata || {};
                // Include the most informative metadata fields
                const fields = ['Name', 'Title', 'Description', 'Entity', 'Status', 'Type']
                    .filter(f => meta[f])
                    .map(f => `${f}: ${meta[f]}`)
                    .join(', ');
                lines.push(`- ${fields || point.Label}`);
            }
            lines.push('');
        }
        return lines.join('\n');
    }
    /**
     * Handle inline label edits from the scatter component legend.
     * Updates the ClusterLabels cache and marks the label as user-edited.
     */
    OnLabelEdited(event) {
        const existing = this.ClusterLabels.find(l => l.ClusterId === event.ClusterId);
        if (existing) {
            existing.Label = event.NewLabel;
            existing.IsUserEdited = true;
        }
        else {
            this.ClusterLabels.push({
                ClusterId: event.ClusterId,
                Label: event.NewLabel,
                IsUserEdited: true,
            });
        }
        this.cdr.detectChanges();
    }
    /**
     * Compute the prioritized field order and display names from entity metadata.
     * Sets both FieldPriority and FieldDisplayNames.
     * Returns field names sorted: IsNameField first, then DefaultInView by Sequence,
     * then remaining fields by Sequence.
     */
    ComputeFieldPriority(entityName) {
        try {
            const md = this.ProviderToUse;
            const entityInfo = md.Entities.find(e => e.Name === entityName);
            if (!entityInfo)
                return [];
            const internalKeys = new Set([
                'ID', 'Entity', 'EntityIcon', 'RecordID', 'TemplateID',
                '__mj_UpdatedAt', '__mj_CreatedAt',
            ]);
            // Build display names map
            const displayNames = {};
            for (const f of entityInfo.Fields) {
                displayNames[f.Name] = f.DisplayNameOrName;
            }
            this.FieldDisplayNames = displayNames;
            return entityInfo.Fields
                .filter(f => !internalKeys.has(f.Name) && !f.IsVirtual && !f.IsPrimaryKey)
                .sort((a, b) => {
                if (a.IsNameField !== b.IsNameField)
                    return a.IsNameField ? -1 : 1;
                if (a.DefaultInView !== b.DefaultInView)
                    return a.DefaultInView ? -1 : 1;
                return (a.Sequence ?? 9999) - (b.Sequence ?? 9999);
            })
                .map(f => f.Name);
        }
        catch {
            return [];
        }
    }
    /** Apply cluster labels to the result's Clusters array (sets the Label property) */
    applyLabelsToResult() {
        if (!this.Result || this.ClusterLabels.length === 0)
            return;
        for (const label of this.ClusterLabels) {
            const cluster = this.Result.Clusters.find(c => c.Id === label.ClusterId);
            if (cluster) {
                cluster.Label = label.Label;
            }
        }
    }
    /**
     * Strip full vector arrays from the result before saving (they're large and not needed for display).
     * Keeps points with 2D coordinates and metadata but removes the original high-dimensional vectors.
     */
    stripVectorsFromResult(result) {
        return {
            ...result,
            Points: result.Points.map(p => ({
                ...p,
                Vector: [], // Strip the high-dimensional vector — only 2D coords (X, Y) matter for display
            })),
        };
    }
    /** Load saved visualizations from UserInfoEngine settings */
    loadSavedVisualizations() {
        try {
            const key = buildEnvScopedKey(SAVED_CLUSTERS_BASE_KEY);
            const engine = UserInfoEngine.Instance;
            const setting = engine.UserSettings.find(s => s.Setting === key);
            if (setting?.Value) {
                this.userSettingEntity = setting;
                this.SavedVisualizations = JSON.parse(setting.Value);
            }
        }
        catch (error) {
            console.warn('[ClusterVisualization] Error loading saved visualizations:', error);
            this.SavedVisualizations = [];
        }
        this.cdr.detectChanges();
    }
    /**
     * Auto-save the current session to localStorage so it can be restored
     * when the user navigates away and comes back.
     */
    saveLastSession() {
        if (!this.Result)
            return;
        try {
            const session = {
                Result: this.stripVectorsFromResult(this.Result),
                ClusterLabels: this.ClusterLabels,
                Config: this.ActiveConfig,
                Title: this.VisualizationTitle,
                Viewport: this.scatterPlot?.GetViewportTransform() ?? null,
            };
            localStorage.setItem(buildEnvScopedKey(LAST_SESSION_BASE_KEY), JSON.stringify(session));
        }
        catch {
            // localStorage quota exceeded or not available — non-critical
        }
    }
    /**
     * Restore the last session from localStorage if no saved visualization is active.
     * This handles the "navigate away and come back" case.
     */
    restoreLastSession() {
        if (this.Result || this.ActiveSavedId)
            return; // Already showing something
        try {
            const raw = localStorage.getItem(buildEnvScopedKey(LAST_SESSION_BASE_KEY));
            if (!raw)
                return;
            const session = JSON.parse(raw);
            this.Result = session.Result;
            this.ClusterLabels = session.ClusterLabels ?? [];
            this.ActiveConfig = session.Config;
            this.VisualizationTitle = session.Title ?? 'Restored Session';
            this.FieldPriority = this.ComputeFieldPriority(session.Config.EntityName);
            this.applyLabelsToResult();
            this.cdr.detectChanges();
            // Restore viewport after a tick to let the scatter component render
            if (session.Viewport) {
                setTimeout(() => {
                    this.scatterPlot?.SetViewportTransform(session.Viewport);
                }, 50);
            }
        }
        catch {
            localStorage.removeItem(buildEnvScopedKey(LAST_SESSION_BASE_KEY));
        }
    }
    /** Persist saved visualizations to UserInfoEngine settings */
    async persistSavedVisualizations() {
        try {
            const md = this.ProviderToUse;
            const userId = md.CurrentUser?.ID;
            if (!userId)
                return;
            if (!this.userSettingEntity) {
                const key = buildEnvScopedKey(SAVED_CLUSTERS_BASE_KEY);
                const engine = UserInfoEngine.Instance;
                const existing = engine.UserSettings.find(s => s.Setting === key);
                if (existing) {
                    this.userSettingEntity = existing;
                }
                else {
                    this.userSettingEntity = await md.GetEntityObject('MJ: User Settings');
                    this.userSettingEntity.UserID = userId;
                    this.userSettingEntity.Setting = buildEnvScopedKey(SAVED_CLUSTERS_BASE_KEY);
                }
            }
            this.userSettingEntity.Value = JSON.stringify(this.SavedVisualizations);
            await this.userSettingEntity.Save();
        }
        catch (error) {
            console.warn('[ClusterVisualization] Error saving visualizations:', error);
        }
    }
    static ɵfac = /*@__PURE__*/ (() => { let ɵClusterVisualizationResourceComponent_BaseFactory; return function ClusterVisualizationResourceComponent_Factory(__ngFactoryType__) { return (ɵClusterVisualizationResourceComponent_BaseFactory || (ɵClusterVisualizationResourceComponent_BaseFactory = i0.ɵɵgetInheritedFactory(ClusterVisualizationResourceComponent)))(__ngFactoryType__ || ClusterVisualizationResourceComponent); }; })();
    static ɵcmp = /*@__PURE__*/ i0.ɵɵdefineComponent({ type: ClusterVisualizationResourceComponent, selectors: [["app-cluster-visualization-resource"]], viewQuery: function ClusterVisualizationResourceComponent_Query(rf, ctx) { if (rf & 1) {
            i0.ɵɵviewQuery(_c0, 5);
        } if (rf & 2) {
            let _t;
            i0.ɵɵqueryRefresh(_t = i0.ɵɵloadQuery()) && (ctx.scatterPlot = _t.first);
        } }, inputs: { Embedded: "Embedded" }, outputs: { OpenRecordRequested: "OpenRecordRequested" }, standalone: false, features: [i0.ɵɵInheritDefinitionFeature], decls: 4, vars: 1, consts: [["clusterBody", ""], ["scatterPlot", ""], [4, "ngTemplateOutlet"], ["Title", "Cluster Visualization", "Icon", "fa-solid fa-circle-nodes", "Subtitle", "Explore semantic clusters in your knowledge base"], ["actions", ""], ["mjButton", "", "variant", "primary", "size", "sm", 3, "click"], [1, "fa-solid", "fa-plus"], [1, "action-btn-label"], [3, "Flex", "Padding"], [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, "main-area"], [1, "topbar"], [1, "topbar-title"], [1, "scatter-area"], [3, "PointClicked", "PointHovered", "OpenRecordRequested", "LabelEdited", "Points", "Clusters", "IsLoading", "ColorBy", "EntityName"], [1, "cluster-run-error"], [3, "RunClustering", "DimensionsChanged", "SaveVisualization", "IsRunning", "Metrics", "EntityOptions", "EntityDocOptions", "AllEntityDocOptions"], [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"], [1, "fa-solid", "fa-triangle-exclamation"]], template: function ClusterVisualizationResourceComponent_Template(rf, ctx) { if (rf & 1) {
            i0.ɵɵconditionalCreate(0, ClusterVisualizationResourceComponent_Conditional_0_Template, 1, 1, "ng-container")(1, ClusterVisualizationResourceComponent_Conditional_1_Template, 9, 3, "mj-page-layout");
            i0.ɵɵtemplate(2, ClusterVisualizationResourceComponent_ng_template_2_Template, 20, 16, "ng-template", null, 0, i0.ɵɵtemplateRefExtractor);
        } if (rf & 2) {
            i0.ɵɵconditional(ctx.Embedded ? 0 : 1);
        } }, dependencies: [i1.NgTemplateOutlet, i2.MJButtonDirective, i2.MJPageHeaderComponent, i2.MJPageLayoutComponent, i2.MJPageBodyComponent, i3.ClusterScatterComponent, i3.ClusterConfigPanelComponent], styles: ["[_nghost-%COMP%] {\n    display: block;\n    width: 100%;\n    height: 100%;\n}\n\n.cluster-viz-container[_ngcontent-%COMP%] {\n    display: flex;\n    flex: 1;\n    \n\n\n\n\n\n    height: 100%;\n    min-height: 0;\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.cluster-run-error[_ngcontent-%COMP%] {\n    position: absolute;\n    top: 12px;\n    left: 50%;\n    transform: translateX(-50%);\n    z-index: 20;\n    display: flex;\n    align-items: center;\n    gap: 8px;\n    max-width: 70%;\n    padding: 10px 14px;\n    background: var(--mj-status-error-bg);\n    border: 1px solid var(--mj-status-error-border);\n    color: var(--mj-status-error-text);\n    border-radius: 8px;\n    font-size: 0.82rem;\n    box-shadow: 0 4px 12px color-mix(in srgb, var(--mj-text-primary) 12%, transparent);\n}\n\n\n\n.scatter-area[_ngcontent-%COMP%] {\n    flex: 1;\n    position: relative;\n    overflow: hidden;\n    min-height: 0;\n}\n\n\n\n.scatter-area[_ngcontent-%COMP%]    > mj-cluster-scatter[_ngcontent-%COMP%] {\n    position: absolute;\n    inset: 0;\n    display: block;\n    overflow: hidden;\n}\n\n\n\n\n\n\n.scatter-area[_ngcontent-%COMP%]    > mj-cluster-config-panel[_ngcontent-%COMP%] {\n    position: absolute;\n    inset: 0;\n    pointer-events: none;\n    z-index: 10;\n}\n.scatter-area[_ngcontent-%COMP%]     .config-panel {\n    pointer-events: auto;\n    max-height: calc(100% - 24px);\n    overflow-y: auto;\n    overscroll-behavior: contain;\n}"] });
};
ClusterVisualizationResourceComponent = __decorate([
    RegisterClass(BaseResourceComponent, 'ClusterVisualizationResource')
], ClusterVisualizationResourceComponent);
export { ClusterVisualizationResourceComponent };
(() => { (typeof ngDevMode === "undefined" || ngDevMode) && i0.ɵsetClassMetadata(ClusterVisualizationResourceComponent, [{
        type: Component,
        args: [{ standalone: false, selector: 'app-cluster-visualization-resource', template: "@if (Embedded) {\n  <ng-container *ngTemplateOutlet=\"clusterBody\"></ng-container>\n} @else {\n<mj-page-layout>\n  <mj-page-header\n    Title=\"Cluster Visualization\"\n    Icon=\"fa-solid fa-circle-nodes\"\n    Subtitle=\"Explore semantic clusters in your knowledge base\">\n    <div actions>\n      <button mjButton variant=\"primary\" size=\"sm\" (click)=\"OnNewAnalysis()\">\n        <i class=\"fa-solid fa-plus\"></i> <span class=\"action-btn-label\">New Analysis</span>\n      </button>\n    </div>\n  </mj-page-header>\n\n  <mj-page-body [Flex]=\"true\" [Padding]=\"false\">\n    <ng-container *ngTemplateOutlet=\"clusterBody\"></ng-container>\n  </mj-page-body>\n</mj-page-layout>\n}\n\n<ng-template #clusterBody>\n<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 Visualizations</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    </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                [ColorBy]=\"ActiveConfig.ColorBy ?? 'cluster'\"\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            <!-- Run error banner (e.g. multi-entity embedding mismatch) -->\n            @if (RunError) {\n                <div class=\"cluster-run-error\">\n                    <i class=\"fa-solid fa-triangle-exclamation\"></i>\n                    <span>{{ RunError }}</span>\n                </div>\n            }\n\n            <!-- Floating config panel -->\n            <mj-cluster-config-panel\n                [IsRunning]=\"IsRunning\"\n                [Metrics]=\"Metrics\"\n                [EntityOptions]=\"EntityOptions\"\n                [EntityDocOptions]=\"EntityDocOptions\"\n                [AllEntityDocOptions]=\"AllEntityDocOptions\"\n                (RunClustering)=\"OnRunClustering($event)\"\n                (DimensionsChanged)=\"OnDimensionsChanged($event)\"\n                (SaveVisualization)=\"OnSaveVisualization()\">\n            </mj-cluster-config-panel>\n        </div>\n    </div>\n</div>\n</ng-template>\n", styles: [":host {\n    display: block;\n    width: 100%;\n    height: 100%;\n}\n\n.cluster-viz-container {\n    display: flex;\n    flex: 1;\n    /* height:100% so we fill the host whether the parent is a flex container\n       (non-embedded, inside mj-page-body[Flex]) OR a plain block host\n       (embedded inside Visualize, where :host is display:block and flex:1\n       would otherwise be ignored, collapsing the area and clipping the\n       floating config panel). */\n    height: 100%;\n    min-height: 0;\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/* Run error banner */\n.cluster-run-error {\n    position: absolute;\n    top: 12px;\n    left: 50%;\n    transform: translateX(-50%);\n    z-index: 20;\n    display: flex;\n    align-items: center;\n    gap: 8px;\n    max-width: 70%;\n    padding: 10px 14px;\n    background: var(--mj-status-error-bg);\n    border: 1px solid var(--mj-status-error-border);\n    color: var(--mj-status-error-text);\n    border-radius: 8px;\n    font-size: 0.82rem;\n    box-shadow: 0 4px 12px color-mix(in srgb, var(--mj-text-primary) 12%, transparent);\n}\n\n/* Scatter area */\n.scatter-area {\n    flex: 1;\n    position: relative;\n    overflow: hidden;\n    min-height: 0;\n}\n\n/* The scatter SVG / empty-state should fill and be clipped to the area \u2026 */\n.scatter-area > mj-cluster-scatter {\n    position: absolute;\n    inset: 0;\n    display: block;\n    overflow: hidden;\n}\n\n/* \u2026 but the floating Configuration panel must never be clipped by the area's\n   overflow. Constrain its height to the visible area and let its own body scroll\n   when the Filter (Optional) section makes it tall. (Panel lives in the generic\n   ng-clustering package; we only adjust its containment from here.) */\n.scatter-area > mj-cluster-config-panel {\n    position: absolute;\n    inset: 0;\n    pointer-events: none;\n    z-index: 10;\n}\n.scatter-area ::ng-deep .config-panel {\n    pointer-events: auto;\n    max-height: calc(100% - 24px);\n    overflow-y: auto;\n    overscroll-behavior: contain;\n}\n"] }]
    }], null, { scatterPlot: [{
            type: ViewChild,
            args: ['scatterPlot']
        }], Embedded: [{
            type: Input
        }], OpenRecordRequested: [{
            type: Output
        }] }); })();
(() => { (typeof ngDevMode === "undefined" || ngDevMode) && i0.ɵsetClassDebugInfo(ClusterVisualizationResourceComponent, { className: "ClusterVisualizationResourceComponent", filePath: "src/KnowledgeHub/components/clusters/cluster-visualization-resource.component.ts", lineNumber: 71 }); })();
/** Tree-shaking prevention */
export function LoadClusterVisualizationResource() {
    // Prevents tree-shaking of the component
}
//# sourceMappingURL=cluster-visualization-resource.component.js.map