@memberjunction/ng-dashboards 6.1.2 → 6.2.0-edge.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 (99) hide show
  1. package/dist/AI/components/vectors/vector-management-resource.component.d.ts +6 -1
  2. package/dist/AI/components/vectors/vector-management-resource.component.d.ts.map +1 -1
  3. package/dist/AI/components/vectors/vector-management-resource.component.js +99 -45
  4. package/dist/AI/components/vectors/vector-management-resource.component.js.map +1 -1
  5. package/dist/DataExplorer/components/navigation-panel/navigation-panel.component.d.ts +4 -0
  6. package/dist/DataExplorer/components/navigation-panel/navigation-panel.component.d.ts.map +1 -1
  7. package/dist/DataExplorer/components/navigation-panel/navigation-panel.component.js +16 -6
  8. package/dist/DataExplorer/components/navigation-panel/navigation-panel.component.js.map +1 -1
  9. package/dist/DataExplorer/data-explorer-dashboard.component.d.ts +4 -0
  10. package/dist/DataExplorer/data-explorer-dashboard.component.d.ts.map +1 -1
  11. package/dist/DataExplorer/data-explorer-dashboard.component.js +14 -4
  12. package/dist/DataExplorer/data-explorer-dashboard.component.js.map +1 -1
  13. package/dist/DataExplorer/services/explorer-state.service.js +2 -2
  14. package/dist/DataExplorer/services/explorer-state.service.js.map +1 -1
  15. package/dist/DevTools/graphql-console.component.d.ts.map +1 -1
  16. package/dist/DevTools/graphql-console.component.js +2 -0
  17. package/dist/DevTools/graphql-console.component.js.map +1 -1
  18. package/dist/Integration/components/connections/connections.component.d.ts.map +1 -1
  19. package/dist/Integration/components/connections/connections.component.js +2 -0
  20. package/dist/Integration/components/connections/connections.component.js.map +1 -1
  21. package/dist/KnowledgeHub/components/feature-pipelines/feature-pipelines-resource.component.d.ts +4 -12
  22. package/dist/KnowledgeHub/components/feature-pipelines/feature-pipelines-resource.component.d.ts.map +1 -1
  23. package/dist/KnowledgeHub/components/feature-pipelines/feature-pipelines-resource.component.js +39 -78
  24. package/dist/KnowledgeHub/components/feature-pipelines/feature-pipelines-resource.component.js.map +1 -1
  25. package/dist/Lists/components/lists-browse-resource.component.d.ts +3 -0
  26. package/dist/Lists/components/lists-browse-resource.component.d.ts.map +1 -1
  27. package/dist/Lists/components/lists-browse-resource.component.js +7 -6
  28. package/dist/Lists/components/lists-browse-resource.component.js.map +1 -1
  29. package/dist/Lists/components/lists-my-lists-resource.component.d.ts +3 -0
  30. package/dist/Lists/components/lists-my-lists-resource.component.d.ts.map +1 -1
  31. package/dist/Lists/components/lists-my-lists-resource.component.js +7 -6
  32. package/dist/Lists/components/lists-my-lists-resource.component.js.map +1 -1
  33. package/dist/PredictiveStudio/at-risk.view-models.d.ts +41 -3
  34. package/dist/PredictiveStudio/at-risk.view-models.d.ts.map +1 -1
  35. package/dist/PredictiveStudio/at-risk.view-models.js +99 -6
  36. package/dist/PredictiveStudio/at-risk.view-models.js.map +1 -1
  37. package/dist/PredictiveStudio/components/ps-catalog.component.d.ts +4 -1
  38. package/dist/PredictiveStudio/components/ps-catalog.component.d.ts.map +1 -1
  39. package/dist/PredictiveStudio/components/ps-catalog.component.js +61 -37
  40. package/dist/PredictiveStudio/components/ps-catalog.component.js.map +1 -1
  41. package/dist/PredictiveStudio/components/ps-experiments.component.d.ts +12 -0
  42. package/dist/PredictiveStudio/components/ps-experiments.component.d.ts.map +1 -1
  43. package/dist/PredictiveStudio/components/ps-experiments.component.js +426 -184
  44. package/dist/PredictiveStudio/components/ps-experiments.component.js.map +1 -1
  45. package/dist/PredictiveStudio/components/ps-pipeline-wizard.component.d.ts +84 -0
  46. package/dist/PredictiveStudio/components/ps-pipeline-wizard.component.d.ts.map +1 -0
  47. package/dist/PredictiveStudio/components/ps-pipeline-wizard.component.js +1397 -0
  48. package/dist/PredictiveStudio/components/ps-pipeline-wizard.component.js.map +1 -0
  49. package/dist/PredictiveStudio/components/ps-pipelines.component.d.ts +66 -34
  50. package/dist/PredictiveStudio/components/ps-pipelines.component.d.ts.map +1 -1
  51. package/dist/PredictiveStudio/components/ps-pipelines.component.js +2456 -743
  52. package/dist/PredictiveStudio/components/ps-pipelines.component.js.map +1 -1
  53. package/dist/PredictiveStudio/components/ps-predictions-grid.component.d.ts +194 -0
  54. package/dist/PredictiveStudio/components/ps-predictions-grid.component.d.ts.map +1 -0
  55. package/dist/PredictiveStudio/components/ps-predictions-grid.component.js +1902 -0
  56. package/dist/PredictiveStudio/components/ps-predictions-grid.component.js.map +1 -0
  57. package/dist/PredictiveStudio/components/ps-production.component.d.ts +17 -1
  58. package/dist/PredictiveStudio/components/ps-production.component.d.ts.map +1 -1
  59. package/dist/PredictiveStudio/components/ps-production.component.js +653 -372
  60. package/dist/PredictiveStudio/components/ps-production.component.js.map +1 -1
  61. package/dist/PredictiveStudio/components/ps-registry.component.d.ts +21 -61
  62. package/dist/PredictiveStudio/components/ps-registry.component.d.ts.map +1 -1
  63. package/dist/PredictiveStudio/components/ps-registry.component.js +214 -681
  64. package/dist/PredictiveStudio/components/ps-registry.component.js.map +1 -1
  65. package/dist/PredictiveStudio/components/ps-run-experiment-modal.component.d.ts +49 -0
  66. package/dist/PredictiveStudio/components/ps-run-experiment-modal.component.d.ts.map +1 -0
  67. package/dist/PredictiveStudio/components/ps-run-experiment-modal.component.js +658 -0
  68. package/dist/PredictiveStudio/components/ps-run-experiment-modal.component.js.map +1 -0
  69. package/dist/PredictiveStudio/predictive-studio-score-history.service.d.ts +42 -0
  70. package/dist/PredictiveStudio/predictive-studio-score-history.service.d.ts.map +1 -0
  71. package/dist/PredictiveStudio/predictive-studio-score-history.service.js +90 -0
  72. package/dist/PredictiveStudio/predictive-studio-score-history.service.js.map +1 -0
  73. package/dist/PredictiveStudio/predictive-studio.view-models.d.ts +14 -2
  74. package/dist/PredictiveStudio/predictive-studio.view-models.d.ts.map +1 -1
  75. package/dist/PredictiveStudio/predictive-studio.view-models.js +41 -5
  76. package/dist/PredictiveStudio/predictive-studio.view-models.js.map +1 -1
  77. package/dist/PredictiveStudio/resources/ps-models-resource.component.d.ts +9 -1
  78. package/dist/PredictiveStudio/resources/ps-models-resource.component.d.ts.map +1 -1
  79. package/dist/PredictiveStudio/resources/ps-models-resource.component.js +178 -65
  80. package/dist/PredictiveStudio/resources/ps-models-resource.component.js.map +1 -1
  81. package/dist/PredictiveStudio/resources/ps-predictions-resource.component.d.ts +9 -1
  82. package/dist/PredictiveStudio/resources/ps-predictions-resource.component.d.ts.map +1 -1
  83. package/dist/PredictiveStudio/resources/ps-predictions-resource.component.js +307 -369
  84. package/dist/PredictiveStudio/resources/ps-predictions-resource.component.js.map +1 -1
  85. package/dist/PredictiveStudio/resources/ps-resource-base.js +1 -1
  86. package/dist/PredictiveStudio/resources/ps-resource-base.js.map +1 -1
  87. package/dist/PredictiveStudio/resources/ps-studio-resource.component.d.ts +17 -1
  88. package/dist/PredictiveStudio/resources/ps-studio-resource.component.d.ts.map +1 -1
  89. package/dist/PredictiveStudio/resources/ps-studio-resource.component.js +309 -138
  90. package/dist/PredictiveStudio/resources/ps-studio-resource.component.js.map +1 -1
  91. package/dist/VersionHistory/components/graph-resource.component.d.ts +1 -0
  92. package/dist/VersionHistory/components/graph-resource.component.d.ts.map +1 -1
  93. package/dist/VersionHistory/components/graph-resource.component.js +6 -5
  94. package/dist/VersionHistory/components/graph-resource.component.js.map +1 -1
  95. package/dist/predictive-studio-dashboards.module.d.ts +10 -8
  96. package/dist/predictive-studio-dashboards.module.d.ts.map +1 -1
  97. package/dist/predictive-studio-dashboards.module.js +13 -3
  98. package/dist/predictive-studio-dashboards.module.js.map +1 -1
  99. package/package.json +64 -63
@@ -1,20 +1,17 @@
1
1
  import { ChangeDetectorRef, Component, Input, ViewEncapsulation, inject } from '@angular/core';
2
2
  import { CommonModule } from '@angular/common';
3
- import { MJButtonDirective } from '@memberjunction/ng-ui-components';
3
+ import { AngularSplitModule } from 'angular-split';
4
4
  import { MJNotificationService } from '@memberjunction/ng-notifications';
5
5
  import { UUIDsEqual } from '@memberjunction/global';
6
- import { PredictiveStudioPromoteModelOperation, } from '@memberjunction/core-entities';
7
- import { PS_LIFECYCLE_STEPS } from '../predictive-studio.types';
8
- import { PS_FEATURE_DOMINANCE_THRESHOLD, maxFeatureImportance, metricsToDisplay, overfitGap, parseFeatureImportance, parseMetrics, primaryAuc, } from '../predictive-studio.view-models';
9
- import { PSConfirmModalComponent } from './ps-confirm-modal.component';
10
- import { humanizeFeatureName } from '../at-risk.view-models';
6
+ import { UserInfoEngine } from '@memberjunction/core-entities';
7
+ import { PSModelDetailComponent } from '@memberjunction/ng-core-entity-forms';
8
+ import { primaryAuc, primaryModelScore, formatMetricValue } from '../predictive-studio.view-models';
11
9
  import * as i0 from "@angular/core";
10
+ import * as i1 from "angular-split";
12
11
  const _forTrack0 = ($index, $item) => $item.id;
13
- const _forTrack1 = ($index, $item) => $item.key;
14
- const _forTrack2 = ($index, $item) => $item.name;
15
12
  function PSRegistryComponent_Conditional_1_Template(rf, ctx) { if (rf & 1) {
16
- i0.ɵɵelementStart(0, "div", 1)(1, "span", 4);
17
- i0.ɵɵelement(2, "i", 5);
13
+ i0.ɵɵelementStart(0, "div", 1)(1, "span", 3);
14
+ i0.ɵɵelement(2, "i", 4);
18
15
  i0.ɵɵelementEnd();
19
16
  i0.ɵɵelementStart(3, "h3");
20
17
  i0.ɵɵtext(4, "No trained models yet");
@@ -23,377 +20,117 @@ function PSRegistryComponent_Conditional_1_Template(rf, ctx) { if (rf & 1) {
23
20
  i0.ɵɵtext(6, "Train a pipeline to register your first immutable model. Each successful training run produces a versioned model you can validate, publish, and score with here.");
24
21
  i0.ɵɵelementEnd()();
25
22
  } }
26
- function PSRegistryComponent_Conditional_2_For_9_Template(rf, ctx) { if (rf & 1) {
27
- const _r1 = i0.ɵɵgetCurrentView();
28
- i0.ɵɵelementStart(0, "div", 39);
29
- i0.ɵɵlistener("click", function PSRegistryComponent_Conditional_2_For_9_Template_div_click_0_listener() { const m_r2 = i0.ɵɵrestoreView(_r1).$implicit; const ctx_r2 = i0.ɵɵnextContext(2); return i0.ɵɵresetView(ctx_r2.select(m_r2.id)); });
30
- i0.ɵɵelementStart(1, "div", 40);
31
- i0.ɵɵelement(2, "i", 15);
23
+ function PSRegistryComponent_Conditional_2_Conditional_1_Template(rf, ctx) { if (rf & 1) {
24
+ const _r2 = i0.ɵɵgetCurrentView();
25
+ i0.ɵɵelementStart(0, "div", 6)(1, "button", 13);
26
+ i0.ɵɵlistener("click", function PSRegistryComponent_Conditional_2_Conditional_1_Template_button_click_1_listener() { i0.ɵɵrestoreView(_r2); const ctx_r2 = i0.ɵɵnextContext(2); return i0.ɵɵresetView(ctx_r2.toggleList()); });
27
+ i0.ɵɵelement(2, "i", 14);
28
+ i0.ɵɵelementEnd();
29
+ i0.ɵɵelementStart(3, "div", 15);
30
+ i0.ɵɵelement(4, "i", 4);
31
+ i0.ɵɵelementEnd()();
32
+ } }
33
+ function PSRegistryComponent_Conditional_2_Conditional_3_For_12_Template(rf, ctx) { if (rf & 1) {
34
+ const _r5 = i0.ɵɵgetCurrentView();
35
+ i0.ɵɵelementStart(0, "div", 24);
36
+ i0.ɵɵlistener("click", function PSRegistryComponent_Conditional_2_Conditional_3_For_12_Template_div_click_0_listener() { const m_r6 = i0.ɵɵrestoreView(_r5).$implicit; const ctx_r2 = i0.ɵɵnextContext(3); return i0.ɵɵresetView(ctx_r2.select(m_r6.id)); });
37
+ i0.ɵɵelementStart(1, "div", 25);
38
+ i0.ɵɵelement(2, "i", 26);
32
39
  i0.ɵɵelementEnd();
33
- i0.ɵɵelementStart(3, "div", 41)(4, "div", 42);
40
+ i0.ɵɵelementStart(3, "div", 27)(4, "div", 28);
34
41
  i0.ɵɵtext(5);
35
42
  i0.ɵɵelementEnd();
36
- i0.ɵɵelementStart(6, "div", 43);
43
+ i0.ɵɵelementStart(6, "div", 29);
37
44
  i0.ɵɵtext(7);
38
45
  i0.ɵɵelementEnd()();
39
- i0.ɵɵelementStart(8, "div", 44)(9, "div", 28);
46
+ i0.ɵɵelementStart(8, "div", 30)(9, "div", 31);
40
47
  i0.ɵɵtext(10);
41
48
  i0.ɵɵelementEnd();
42
- i0.ɵɵelementStart(11, "div", 45);
49
+ i0.ɵɵelementStart(11, "div", 32);
43
50
  i0.ɵɵtext(12);
44
51
  i0.ɵɵelementEnd()()();
45
52
  } if (rf & 2) {
46
- const m_r2 = ctx.$implicit;
47
- const ctx_r2 = i0.ɵɵnextContext(2);
48
- i0.ɵɵclassProp("sel", m_r2.id === ctx_r2.selectedId)("arc", m_r2.status === "Archived");
53
+ const m_r6 = ctx.$implicit;
54
+ const ctx_r2 = i0.ɵɵnextContext(3);
55
+ i0.ɵɵclassProp("sel", m_r6.id === ctx_r2.selectedId)("arc", m_r6.status === "Archived");
49
56
  i0.ɵɵadvance();
50
- i0.ɵɵclassMap(m_r2.iconClass);
57
+ i0.ɵɵclassMap(m_r6.iconClass);
51
58
  i0.ɵɵadvance(4);
52
- i0.ɵɵtextInterpolate(m_r2.name);
53
- i0.ɵɵadvance(2);
54
- i0.ɵɵtextInterpolate2("v", m_r2.version, " \u00B7 ", m_r2.algorithm);
55
- i0.ɵɵadvance(3);
56
- i0.ɵɵtextInterpolate(m_r2.holdoutAuc);
57
- i0.ɵɵadvance();
58
- i0.ɵɵclassMap(ctx_r2.statusClass(m_r2.status));
59
- i0.ɵɵadvance();
60
- i0.ɵɵtextInterpolate(m_r2.status);
61
- } }
62
- function PSRegistryComponent_Conditional_2_For_30_Conditional_2_Template(rf, ctx) { if (rf & 1) {
63
- i0.ɵɵelement(0, "i", 48);
64
- } }
65
- function PSRegistryComponent_Conditional_2_For_30_Conditional_3_Template(rf, ctx) { if (rf & 1) {
66
- i0.ɵɵtext(0);
67
- } if (rf & 2) {
68
- const $index_r4 = i0.ɵɵnextContext().$index;
69
- i0.ɵɵtextInterpolate1(" ", $index_r4 + 1, " ");
70
- } }
71
- function PSRegistryComponent_Conditional_2_For_30_Conditional_5_Template(rf, ctx) { if (rf & 1) {
72
- i0.ɵɵelement(0, "span", 49);
73
- } }
74
- function PSRegistryComponent_Conditional_2_For_30_Template(rf, ctx) { if (rf & 1) {
75
- i0.ɵɵelementStart(0, "div", 46)(1, "span", 47);
76
- i0.ɵɵconditionalCreate(2, PSRegistryComponent_Conditional_2_For_30_Conditional_2_Template, 1, 0, "i", 48)(3, PSRegistryComponent_Conditional_2_For_30_Conditional_3_Template, 1, 1);
77
- i0.ɵɵelementEnd();
78
- i0.ɵɵtext(4);
79
- i0.ɵɵelementEnd();
80
- i0.ɵɵconditionalCreate(5, PSRegistryComponent_Conditional_2_For_30_Conditional_5_Template, 1, 0, "span", 49);
81
- } if (rf & 2) {
82
- const step_r5 = ctx.$implicit;
83
- const ɵ$index_87_r6 = ctx.$index;
84
- const ɵ$count_87_r7 = ctx.$count;
85
- const ctx_r2 = i0.ɵɵnextContext(2);
86
- i0.ɵɵclassProp("done", ctx_r2.stepState(step_r5) === "done")("curr", ctx_r2.stepState(step_r5) === "curr");
87
- i0.ɵɵadvance(2);
88
- i0.ɵɵconditional(ctx_r2.stepState(step_r5) === "done" ? 2 : 3);
89
- i0.ɵɵadvance(2);
90
- i0.ɵɵtextInterpolate1(" ", step_r5, " ");
91
- i0.ɵɵadvance();
92
- i0.ɵɵconditional(!(ɵ$index_87_r6 === ɵ$count_87_r7 - 1) ? 5 : -1);
93
- } }
94
- function PSRegistryComponent_Conditional_2_Conditional_55_For_2_Template(rf, ctx) { if (rf & 1) {
95
- i0.ɵɵelementStart(0, "div", 50)(1, "span", 25);
96
- i0.ɵɵtext(2);
97
- i0.ɵɵelementEnd();
98
- i0.ɵɵelementStart(3, "strong");
99
- i0.ɵɵtext(4);
100
- i0.ɵɵelementEnd()();
101
- } if (rf & 2) {
102
- const s_r8 = ctx.$implicit;
103
- i0.ɵɵadvance(2);
104
- i0.ɵɵtextInterpolate(s_r8.label);
59
+ i0.ɵɵtextInterpolate(m_r6.name);
105
60
  i0.ɵɵadvance(2);
106
- i0.ɵɵtextInterpolate(s_r8.value);
107
- } }
108
- function PSRegistryComponent_Conditional_2_Conditional_55_Template(rf, ctx) { if (rf & 1) {
109
- i0.ɵɵelementStart(0, "div", 30);
110
- i0.ɵɵrepeaterCreate(1, PSRegistryComponent_Conditional_2_Conditional_55_For_2_Template, 5, 2, "div", 50, _forTrack1);
111
- i0.ɵɵelementEnd();
112
- } if (rf & 2) {
113
- const ctx_r2 = i0.ɵɵnextContext(2);
114
- i0.ɵɵadvance();
115
- i0.ɵɵrepeater(ctx_r2.secondaryMetrics);
116
- } }
117
- function PSRegistryComponent_Conditional_2_Conditional_56_Template(rf, ctx) { if (rf & 1) {
118
- i0.ɵɵelementStart(0, "div", 31);
119
- i0.ɵɵtext(1, "No secondary metrics recorded for this model.");
120
- i0.ɵɵelementEnd();
121
- } }
122
- function PSRegistryComponent_Conditional_2_Conditional_57_Template(rf, ctx) { if (rf & 1) {
123
- i0.ɵɵelementStart(0, "div", 24);
124
- i0.ɵɵtext(1, " Train\u2013holdout gap of ");
125
- i0.ɵɵelementStart(2, "strong");
126
- i0.ɵɵtext(3);
127
- i0.ɵɵelementEnd();
128
- i0.ɵɵtext(4);
129
- i0.ɵɵelementEnd();
130
- } if (rf & 2) {
131
- const ctx_r2 = i0.ɵɵnextContext(2);
61
+ i0.ɵɵtextInterpolate2("v", m_r6.version, " \u2022 ", m_r6.algorithm);
132
62
  i0.ɵɵadvance(3);
133
- i0.ɵɵtextInterpolate(ctx_r2.gapText);
63
+ i0.ɵɵtextInterpolate(m_r6.holdoutScore);
134
64
  i0.ɵɵadvance();
135
- i0.ɵɵtextInterpolate1(" ", ctx_r2.gapVerdict, ". ");
136
- } }
137
- function PSRegistryComponent_Conditional_2_Conditional_65_For_1_Template(rf, ctx) { if (rf & 1) {
138
- i0.ɵɵelementStart(0, "div", 51)(1, "span", 52);
139
- i0.ɵɵtext(2);
140
- i0.ɵɵelementEnd();
141
- i0.ɵɵelementStart(3, "div", 53);
142
- i0.ɵɵelement(4, "span");
143
- i0.ɵɵelementEnd();
144
- i0.ɵɵelementStart(5, "span", 54);
145
- i0.ɵɵtext(6);
146
- i0.ɵɵelementEnd()();
147
- } if (rf & 2) {
148
- const f_r9 = ctx.$implicit;
65
+ i0.ɵɵclassMap(ctx_r2.statusClass(m_r6.status));
149
66
  i0.ɵɵadvance();
150
- i0.ɵɵproperty("title", f_r9.name);
151
- i0.ɵɵadvance();
152
- i0.ɵɵtextInterpolate(f_r9.name);
153
- i0.ɵɵadvance(2);
154
- i0.ɵɵstyleProp("width", f_r9.pct, "%");
155
- i0.ɵɵclassProp("warn", f_r9.warning);
156
- i0.ɵɵadvance(2);
157
- i0.ɵɵtextInterpolate(f_r9.value);
67
+ i0.ɵɵtextInterpolate(m_r6.status);
158
68
  } }
159
- function PSRegistryComponent_Conditional_2_Conditional_65_Template(rf, ctx) { if (rf & 1) {
160
- i0.ɵɵrepeaterCreate(0, PSRegistryComponent_Conditional_2_Conditional_65_For_1_Template, 7, 7, "div", 51, _forTrack2);
161
- } if (rf & 2) {
162
- const ctx_r2 = i0.ɵɵnextContext(2);
163
- i0.ɵɵrepeater(ctx_r2.importance);
164
- } }
165
- function PSRegistryComponent_Conditional_2_Conditional_66_Template(rf, ctx) { if (rf & 1) {
166
- i0.ɵɵelementStart(0, "div", 32);
167
- i0.ɵɵtext(1, "No feature-importance data was recorded for this model.");
168
- i0.ɵɵelementEnd();
169
- } }
170
- function PSRegistryComponent_Conditional_2_Conditional_67_Conditional_3_Template(rf, ctx) { if (rf & 1) {
171
- i0.ɵɵelementStart(0, "strong");
172
- i0.ɵɵtext(1, "Leakage flag raised.");
173
- i0.ɵɵelementEnd();
174
- i0.ɵɵtext(2, " A single feature dominates \u2014 top importance is ");
175
- i0.ɵɵelementStart(3, "strong");
176
- i0.ɵɵtext(4);
69
+ function PSRegistryComponent_Conditional_2_Conditional_3_Template(rf, ctx) { if (rf & 1) {
70
+ const _r4 = i0.ɵɵgetCurrentView();
71
+ i0.ɵɵelementStart(0, "as-split-area", 8)(1, "div", 16)(2, "div", 17)(3, "h3");
72
+ i0.ɵɵtext(4, "ML Models");
177
73
  i0.ɵɵelementEnd();
178
- i0.ɵɵtext(5, ", at/above the ");
179
- i0.ɵɵelementStart(6, "strong");
74
+ i0.ɵɵelementStart(5, "div", 18)(6, "span", 19);
180
75
  i0.ɵɵtext(7);
181
76
  i0.ɵɵelementEnd();
182
- i0.ɵɵtext(8, " dominance threshold. ");
183
- i0.ɵɵelementStart(9, "div", 56);
184
- i0.ɵɵelement(10, "i", 57);
185
- i0.ɵɵtext(11, " Sign-off with a reason is required to publish this model.");
186
- i0.ɵɵelementEnd();
77
+ i0.ɵɵelementStart(8, "button", 20);
78
+ i0.ɵɵlistener("click", function PSRegistryComponent_Conditional_2_Conditional_3_Template_button_click_8_listener() { i0.ɵɵrestoreView(_r4); const ctx_r2 = i0.ɵɵnextContext(2); return i0.ɵɵresetView(ctx_r2.toggleList()); });
79
+ i0.ɵɵelement(9, "i", 21);
80
+ i0.ɵɵelementEnd()()();
81
+ i0.ɵɵelementStart(10, "div", 22);
82
+ i0.ɵɵrepeaterCreate(11, PSRegistryComponent_Conditional_2_Conditional_3_For_12_Template, 13, 13, "div", 23, _forTrack0);
83
+ i0.ɵɵelementEnd()()();
187
84
  } if (rf & 2) {
188
- const ctx_r2 = i0.ɵɵnextContext(3);
85
+ const ctx_r2 = i0.ɵɵnextContext(2);
86
+ i0.ɵɵproperty("size", ctx_r2.listSizePct)("minSize", 18)("maxSize", 50);
87
+ i0.ɵɵadvance(7);
88
+ i0.ɵɵtextInterpolate(ctx_r2.models.length);
189
89
  i0.ɵɵadvance(4);
190
- i0.ɵɵtextInterpolate2("", ctx_r2.topFeatureName, " at ", ctx_r2.topFeatureValue);
191
- i0.ɵɵadvance(3);
192
- i0.ɵɵtextInterpolate(ctx_r2.dominanceThreshold);
90
+ i0.ɵɵrepeater(ctx_r2.models);
193
91
  } }
194
- function PSRegistryComponent_Conditional_2_Conditional_67_Conditional_4_Template(rf, ctx) { if (rf & 1) {
195
- i0.ɵɵelementStart(0, "strong");
196
- i0.ɵɵtext(1, "Leakage gate clear.");
197
- i0.ɵɵelementEnd();
198
- i0.ɵɵtext(2, " No single feature dominates \u2014 top importance is ");
199
- i0.ɵɵelementStart(3, "strong");
200
- i0.ɵɵtext(4);
201
- i0.ɵɵelementEnd();
202
- i0.ɵɵtext(5, ", below the ");
203
- i0.ɵɵelementStart(6, "strong");
204
- i0.ɵɵtext(7);
205
- i0.ɵɵelementEnd();
206
- i0.ɵɵtext(8, " dominance threshold. ");
207
- i0.ɵɵelementStart(9, "div", 56);
208
- i0.ɵɵelement(10, "i", 48);
209
- i0.ɵɵtext(11, " Sign-off required before any Validated \u2192 Published promotion.");
92
+ function PSRegistryComponent_Conditional_2_Conditional_6_Template(rf, ctx) { if (rf & 1) {
93
+ const _r7 = i0.ɵɵgetCurrentView();
94
+ i0.ɵɵelementStart(0, "ps-model-detail", 33);
95
+ i0.ɵɵlistener("statusChanged", function PSRegistryComponent_Conditional_2_Conditional_6_Template_ps_model_detail_statusChanged_0_listener($event) { i0.ɵɵrestoreView(_r7); const ctx_r2 = i0.ɵɵnextContext(2); return i0.ɵɵresetView(ctx_r2.onModelStatusChanged($event)); });
210
96
  i0.ɵɵelementEnd();
211
- } if (rf & 2) {
212
- const ctx_r2 = i0.ɵɵnextContext(3);
213
- i0.ɵɵadvance(4);
214
- i0.ɵɵtextInterpolate2("", ctx_r2.topFeatureName, " at ", ctx_r2.topFeatureValue);
215
- i0.ɵɵadvance(3);
216
- i0.ɵɵtextInterpolate(ctx_r2.dominanceThreshold);
217
- } }
218
- function PSRegistryComponent_Conditional_2_Conditional_67_Template(rf, ctx) { if (rf & 1) {
219
- i0.ɵɵelementStart(0, "div", 55);
220
- i0.ɵɵelement(1, "i");
221
- i0.ɵɵelementStart(2, "div");
222
- i0.ɵɵconditionalCreate(3, PSRegistryComponent_Conditional_2_Conditional_67_Conditional_3_Template, 12, 3)(4, PSRegistryComponent_Conditional_2_Conditional_67_Conditional_4_Template, 12, 3);
223
- i0.ɵɵelementEnd()();
224
97
  } if (rf & 2) {
225
98
  const ctx_r2 = i0.ɵɵnextContext(2);
226
- i0.ɵɵclassProp("success", !ctx_r2.leakageFlagged)("warn", ctx_r2.leakageFlagged);
227
- i0.ɵɵadvance();
228
- i0.ɵɵclassMap(ctx_r2.leakageFlagged ? "fa-solid fa-triangle-exclamation" : "fa-solid fa-shield-halved");
229
- i0.ɵɵadvance(2);
230
- i0.ɵɵconditional(ctx_r2.leakageFlagged ? 3 : 4);
231
- } }
232
- function PSRegistryComponent_Conditional_2_Conditional_72_Template(rf, ctx) { if (rf & 1) {
233
- const _r10 = i0.ɵɵgetCurrentView();
234
- i0.ɵɵelementStart(0, "button", 58);
235
- i0.ɵɵlistener("click", function PSRegistryComponent_Conditional_2_Conditional_72_Template_button_click_0_listener() { i0.ɵɵrestoreView(_r10); const ctx_r2 = i0.ɵɵnextContext(2); return i0.ɵɵresetView(ctx_r2.requestPromote("Validated")); });
236
- i0.ɵɵelement(1, "i", 59);
237
- i0.ɵɵtext(2, " Mark Validated");
238
- i0.ɵɵelementEnd();
99
+ i0.ɵɵproperty("model", ctx_r2.selectedEntity)("displayName", ctx_r2.engine ? ctx_r2.engine.ModelDisplayName(ctx_r2.selectedEntity) : undefined)("provider", ctx_r2.provider)("currentUser", ctx_r2.currentUser);
239
100
  } }
240
- function PSRegistryComponent_Conditional_2_Conditional_73_Template(rf, ctx) { if (rf & 1) {
241
- const _r11 = i0.ɵɵgetCurrentView();
242
- i0.ɵɵelementStart(0, "button", 60);
243
- i0.ɵɵlistener("click", function PSRegistryComponent_Conditional_2_Conditional_73_Template_button_click_0_listener() { i0.ɵɵrestoreView(_r11); const ctx_r2 = i0.ɵɵnextContext(2); return i0.ɵɵresetView(ctx_r2.requestPromote("Published")); });
244
- i0.ɵɵelement(1, "i", 61);
245
- i0.ɵɵtext(2, " Promote to Published");
246
- i0.ɵɵelementEnd();
247
- } }
248
- function PSRegistryComponent_Conditional_2_Conditional_74_Template(rf, ctx) { if (rf & 1) {
249
- const _r12 = i0.ɵɵgetCurrentView();
250
- i0.ɵɵelementStart(0, "button", 62);
251
- i0.ɵɵlistener("click", function PSRegistryComponent_Conditional_2_Conditional_74_Template_button_click_0_listener() { i0.ɵɵrestoreView(_r12); const ctx_r2 = i0.ɵɵnextContext(2); return i0.ɵɵresetView(ctx_r2.requestPromote("Archived")); });
252
- i0.ɵɵelement(1, "i", 63);
253
- i0.ɵɵtext(2, " Archive");
254
- i0.ɵɵelementEnd();
101
+ function PSRegistryComponent_Conditional_2_Conditional_7_Template(rf, ctx) { if (rf & 1) {
102
+ i0.ɵɵelementStart(0, "div", 12)(1, "p", 34);
103
+ i0.ɵɵtext(2, "Select a model to view details.");
104
+ i0.ɵɵelementEnd()();
255
105
  } }
256
106
  function PSRegistryComponent_Conditional_2_Template(rf, ctx) { if (rf & 1) {
257
- i0.ɵɵelementStart(0, "div", 2)(1, "div", 6)(2, "div", 7)(3, "h3");
258
- i0.ɵɵtext(4, "ML Models");
259
- i0.ɵɵelementEnd();
260
- i0.ɵɵelementStart(5, "span", 8);
261
- i0.ɵɵtext(6);
262
- i0.ɵɵelementEnd()();
263
- i0.ɵɵelementStart(7, "div", 9);
264
- i0.ɵɵrepeaterCreate(8, PSRegistryComponent_Conditional_2_For_9_Template, 13, 13, "div", 10, _forTrack0);
265
- i0.ɵɵelementEnd()();
266
- i0.ɵɵelementStart(10, "div", 11)(11, "div", 12)(12, "div", 13)(13, "div", 14);
267
- i0.ɵɵelement(14, "i", 15);
268
- i0.ɵɵelementEnd();
269
- i0.ɵɵelementStart(15, "div", 16)(16, "h2", 17);
270
- i0.ɵɵtext(17);
271
- i0.ɵɵelementStart(18, "span", 18);
272
- i0.ɵɵtext(19);
273
- i0.ɵɵelementEnd()();
274
- i0.ɵɵelementStart(20, "div", 19);
275
- i0.ɵɵtext(21);
276
- i0.ɵɵelementEnd()();
277
- i0.ɵɵelementStart(22, "span", 20);
278
- i0.ɵɵtext(23);
279
- i0.ɵɵelementEnd()()();
280
- i0.ɵɵelementStart(24, "div", 12)(25, "div", 21)(26, "div", 22);
281
- i0.ɵɵtext(27, "Lifecycle");
282
- i0.ɵɵelementEnd();
283
- i0.ɵɵelementStart(28, "div", 23);
284
- i0.ɵɵrepeaterCreate(29, PSRegistryComponent_Conditional_2_For_30_Template, 6, 7, null, null, i0.ɵɵrepeaterTrackByIdentity);
285
- i0.ɵɵelementEnd();
286
- i0.ɵɵelementStart(31, "div", 24);
287
- i0.ɵɵtext(32, " Models are immutable once registered; promotion only changes lifecycle state, never weights. ");
288
- i0.ɵɵelementEnd()()();
289
- i0.ɵɵelementStart(33, "div", 12)(34, "div", 7)(35, "h3");
290
- i0.ɵɵtext(36, "Performance");
291
- i0.ɵɵelementEnd();
292
- i0.ɵɵelementStart(37, "span", 25);
293
- i0.ɵɵtext(38, "Holdout = held-out test fold, never seen in training");
294
- i0.ɵɵelementEnd()();
295
- i0.ɵɵelementStart(39, "div", 21)(40, "div", 26)(41, "div", 27)(42, "div", 22);
296
- i0.ɵɵtext(43, "Train AUC");
297
- i0.ɵɵelementEnd();
298
- i0.ɵɵelementStart(44, "div", 28);
299
- i0.ɵɵtext(45);
300
- i0.ɵɵelementEnd();
301
- i0.ɵɵelementStart(46, "div", 25);
302
- i0.ɵɵtext(47, "in-sample \u00B7 optimistic");
303
- i0.ɵɵelementEnd()();
304
- i0.ɵɵelementStart(48, "div", 29)(49, "div", 22);
305
- i0.ɵɵtext(50, "Holdout AUC");
306
- i0.ɵɵelementEnd();
307
- i0.ɵɵelementStart(51, "div", 28);
308
- i0.ɵɵtext(52);
309
- i0.ɵɵelementEnd();
310
- i0.ɵɵelementStart(53, "div", 25);
311
- i0.ɵɵtext(54, "out-of-sample \u00B7 the honest number");
312
- i0.ɵɵelementEnd()()();
313
- i0.ɵɵconditionalCreate(55, PSRegistryComponent_Conditional_2_Conditional_55_Template, 3, 0, "div", 30)(56, PSRegistryComponent_Conditional_2_Conditional_56_Template, 2, 0, "div", 31);
314
- i0.ɵɵconditionalCreate(57, PSRegistryComponent_Conditional_2_Conditional_57_Template, 5, 2, "div", 24);
315
- i0.ɵɵelementEnd()();
316
- i0.ɵɵelementStart(58, "div", 12)(59, "div", 7)(60, "h3");
317
- i0.ɵɵtext(61, "Feature Importance");
318
- i0.ɵɵelementEnd();
319
- i0.ɵɵelementStart(62, "span", 25);
320
- i0.ɵɵtext(63);
321
- i0.ɵɵelementEnd()();
322
- i0.ɵɵelementStart(64, "div", 21);
323
- i0.ɵɵconditionalCreate(65, PSRegistryComponent_Conditional_2_Conditional_65_Template, 2, 0)(66, PSRegistryComponent_Conditional_2_Conditional_66_Template, 2, 0, "div", 32);
324
- i0.ɵɵelementEnd()();
325
- i0.ɵɵconditionalCreate(67, PSRegistryComponent_Conditional_2_Conditional_67_Template, 5, 7, "div", 33);
326
- i0.ɵɵelementStart(68, "div", 12)(69, "div", 34)(70, "span", 35);
327
- i0.ɵɵtext(71);
328
- i0.ɵɵelementEnd();
329
- i0.ɵɵconditionalCreate(72, PSRegistryComponent_Conditional_2_Conditional_72_Template, 3, 0, "button", 36);
330
- i0.ɵɵconditionalCreate(73, PSRegistryComponent_Conditional_2_Conditional_73_Template, 3, 0, "button", 37);
331
- i0.ɵɵconditionalCreate(74, PSRegistryComponent_Conditional_2_Conditional_74_Template, 3, 0, "button", 38);
107
+ const _r1 = i0.ɵɵgetCurrentView();
108
+ i0.ɵɵelementStart(0, "div", 5);
109
+ i0.ɵɵconditionalCreate(1, PSRegistryComponent_Conditional_2_Conditional_1_Template, 5, 0, "div", 6);
110
+ i0.ɵɵelementStart(2, "as-split", 7);
111
+ i0.ɵɵlistener("dragEnd", function PSRegistryComponent_Conditional_2_Template_as_split_dragEnd_2_listener($event) { i0.ɵɵrestoreView(_r1); const ctx_r2 = i0.ɵɵnextContext(); return i0.ɵɵresetView(ctx_r2.onSplitDragEnd($event.sizes)); });
112
+ i0.ɵɵconditionalCreate(3, PSRegistryComponent_Conditional_2_Conditional_3_Template, 13, 4, "as-split-area", 8);
113
+ i0.ɵɵelementStart(4, "as-split-area", 9)(5, "div", 10);
114
+ i0.ɵɵconditionalCreate(6, PSRegistryComponent_Conditional_2_Conditional_6_Template, 1, 4, "ps-model-detail", 11)(7, PSRegistryComponent_Conditional_2_Conditional_7_Template, 3, 0, "div", 12);
332
115
  i0.ɵɵelementEnd()()()();
333
116
  } if (rf & 2) {
334
117
  const ctx_r2 = i0.ɵɵnextContext();
335
- i0.ɵɵadvance(6);
336
- i0.ɵɵtextInterpolate(ctx_r2.models.length);
337
- i0.ɵɵadvance(2);
338
- i0.ɵɵrepeater(ctx_r2.models);
339
- i0.ɵɵadvance(9);
340
- i0.ɵɵtextInterpolate1("", ctx_r2.selected.name, " ");
341
- i0.ɵɵadvance(2);
342
- i0.ɵɵtextInterpolate1("v", ctx_r2.selected.version);
343
- i0.ɵɵadvance(2);
344
- i0.ɵɵtextInterpolate2("", ctx_r2.selected.algorithm, "", ctx_r2.targetSuffix, " \u00B7 immutable snapshot");
118
+ i0.ɵɵclassProp("list-collapsed", ctx_r2.isListCollapsed);
345
119
  i0.ɵɵadvance();
346
- i0.ɵɵclassMap(ctx_r2.statusBadgeClass(ctx_r2.selected.status));
347
- i0.ɵɵadvance();
348
- i0.ɵɵtextInterpolate(ctx_r2.selected.status);
349
- i0.ɵɵadvance(6);
350
- i0.ɵɵrepeater(ctx_r2.lifecycleSteps);
351
- i0.ɵɵadvance(16);
352
- i0.ɵɵtextInterpolate(ctx_r2.trainAuc);
353
- i0.ɵɵadvance(7);
354
- i0.ɵɵtextInterpolate(ctx_r2.selected.holdoutAuc);
355
- i0.ɵɵadvance(3);
356
- i0.ɵɵconditional(ctx_r2.secondaryMetrics.length > 0 ? 55 : 56);
357
- i0.ɵɵadvance(2);
358
- i0.ɵɵconditional(ctx_r2.gapText ? 57 : -1);
359
- i0.ɵɵadvance(6);
360
- i0.ɵɵtextInterpolate(ctx_r2.importanceCaption);
361
- i0.ɵɵadvance(2);
362
- i0.ɵɵconditional(ctx_r2.importance.length > 0 ? 65 : 66);
363
- i0.ɵɵadvance(2);
364
- i0.ɵɵconditional(ctx_r2.importance.length > 0 ? 67 : -1);
365
- i0.ɵɵadvance(4);
366
- i0.ɵɵtextInterpolate(ctx_r2.actionHint);
120
+ i0.ɵɵconditional(ctx_r2.isListCollapsed ? 1 : -1);
367
121
  i0.ɵɵadvance();
368
- i0.ɵɵconditional(ctx_r2.canValidate ? 72 : -1);
122
+ i0.ɵɵproperty("gutterSize", 6);
369
123
  i0.ɵɵadvance();
370
- i0.ɵɵconditional(ctx_r2.canPublish ? 73 : -1);
124
+ i0.ɵɵconditional(!ctx_r2.isListCollapsed ? 3 : -1);
371
125
  i0.ɵɵadvance();
372
- i0.ɵɵconditional(ctx_r2.canArchive ? 74 : -1);
373
- } }
374
- function PSRegistryComponent_Conditional_3_Template(rf, ctx) { if (rf & 1) {
375
- const _r13 = i0.ɵɵgetCurrentView();
376
- i0.ɵɵelementStart(0, "ps-confirm-modal", 64);
377
- i0.ɵɵlistener("confirmed", function PSRegistryComponent_Conditional_3_Template_ps_confirm_modal_confirmed_0_listener($event) { i0.ɵɵrestoreView(_r13); const ctx_r2 = i0.ɵɵnextContext(); return i0.ɵɵresetView(ctx_r2.confirmPromote($event)); })("cancelled", function PSRegistryComponent_Conditional_3_Template_ps_confirm_modal_cancelled_0_listener() { i0.ɵɵrestoreView(_r13); const ctx_r2 = i0.ɵɵnextContext(); return i0.ɵɵresetView(ctx_r2.cancelPromote()); });
378
- i0.ɵɵelement(1, "div", 65);
379
- i0.ɵɵelementEnd();
380
- } if (rf & 2) {
381
- const ctx_r2 = i0.ɵɵnextContext();
382
- i0.ɵɵproperty("title", ctx_r2.pendingTitle)("icon", ctx_r2.pendingIcon)("confirmIcon", ctx_r2.pendingIcon)("confirmLabel", ctx_r2.pendingConfirmLabel)("variant", ctx_r2.pendingVariant)("showReason", ctx_r2.pending.leakageFlagged || ctx_r2.pending.targetStatus === "Archived")("reasonRequired", ctx_r2.pending.leakageFlagged)("reasonLabel", ctx_r2.pending.leakageFlagged ? "Leakage sign-off reason" : "Reason (optional)")("reasonPlaceholder", ctx_r2.pending.leakageFlagged ? "Explain why this model is safe to publish despite the leakage flag\u2026" : "Add an optional note for the record\u2026")("busy", ctx_r2.busy);
383
- i0.ɵɵadvance();
384
- i0.ɵɵproperty("innerHTML", ctx_r2.pendingMessage, i0.ɵɵsanitizeHtml);
126
+ i0.ɵɵproperty("size", ctx_r2.isListCollapsed ? 100 : ctx_r2.detailSizePct)("minSize", 50);
127
+ i0.ɵɵadvance(2);
128
+ i0.ɵɵconditional(ctx_r2.selectedEntity ? 6 : 7);
385
129
  } }
386
130
  /**
387
- * Model Registry panel: master list of trained ML Models + a rich detail pane with a lifecycle stepper
388
- * (Draft → Validated → Published → Archived), train-vs-holdout metric comparison, feature-importance
389
- * bars, a leakage sign-off gate, and lifecycle actions. Fully live against `MJ: ML Models`:
390
- *
391
- * - **Feature importance** is parsed from `model.FeatureImportance` (sorted by |value|, top 6, bars).
392
- * - **Performance metrics** come from `model.Metrics` (train) + `model.HoldoutMetrics` (holdout) —
393
- * only the metrics actually recorded are shown.
394
- * - **Promote / Archive** call the {@link PredictiveStudioPromoteModelOperation} Remote Op behind a
395
- * confirmation modal. On a leakage-flagged model, the modal surfaces the sign-off requirement and
396
- * captures a required reason; on success the engine refreshes and the list re-renders reactively.
131
+ * Model Registry panel: master list of trained ML Models + a rich detail pane rendered via the unified
132
+ * {@link PSModelDetailComponent}. Includes resizable split layout with user preference persistence via
133
+ * UserInfoEngine and collapse/expand support.
397
134
  *
398
135
  * 100% entity-agnostic — model names derive from the producing pipeline, never any business entity.
399
136
  */
@@ -407,15 +144,70 @@ export class PSRegistryComponent {
407
144
  notifications = inject(MJNotificationService);
408
145
  models = [];
409
146
  selectedId = '';
410
- lifecycleSteps = PS_LIFECYCLE_STEPS;
411
- dominanceThreshold = PS_FEATURE_DOMINANCE_THRESHOLD.toFixed(2);
412
- /** Pending confirmation (null when no modal is open). */
413
- pending = null;
414
- /** Remote Op in flight — drives the modal spinner + disables the buttons. */
415
- busy = false;
147
+ // Resizable split state
148
+ listSizePct = 28;
149
+ detailSizePct = 72;
150
+ isListCollapsed = false;
151
+ _initialModelId;
152
+ set initialModelId(id) {
153
+ this._initialModelId = id;
154
+ if (id) {
155
+ if (this.models.length === 0 || !this.models.some((m) => UUIDsEqual(m.id, id))) {
156
+ this.buildModels();
157
+ }
158
+ if (this.models.some((m) => UUIDsEqual(m.id, id))) {
159
+ this.selectedId = id;
160
+ }
161
+ this.cdr.detectChanges();
162
+ }
163
+ }
164
+ get initialModelId() {
165
+ return this._initialModelId;
166
+ }
416
167
  ngOnInit() {
168
+ const saved = UserInfoEngine.Instance.GetSetting('mj.predictiveStudio.registry.layout');
169
+ if (saved) {
170
+ try {
171
+ const prefs = JSON.parse(saved);
172
+ if (typeof prefs.listSizePct === 'number')
173
+ this.listSizePct = prefs.listSizePct;
174
+ if (typeof prefs.detailSizePct === 'number')
175
+ this.detailSizePct = prefs.detailSizePct;
176
+ if (typeof prefs.isListCollapsed === 'boolean')
177
+ this.isListCollapsed = prefs.isListCollapsed;
178
+ }
179
+ catch {
180
+ // ignore malformed pref
181
+ }
182
+ }
417
183
  this.buildModels();
418
- this.selectedId = this.models[0]?.id ?? '';
184
+ if (this._initialModelId && this.models.some((m) => UUIDsEqual(m.id, this._initialModelId))) {
185
+ this.selectedId = this._initialModelId;
186
+ }
187
+ else {
188
+ this.selectedId = this.models[0]?.id ?? '';
189
+ }
190
+ }
191
+ // ---- splitter resizing & persistence ----
192
+ onSplitDragEnd(sizes) {
193
+ if (Array.isArray(sizes) && sizes.length === 2 && typeof sizes[0] === 'number' && typeof sizes[1] === 'number') {
194
+ this.listSizePct = Math.round(sizes[0]);
195
+ this.detailSizePct = Math.round(sizes[1]);
196
+ this.saveLayoutPrefs();
197
+ }
198
+ }
199
+ toggleList() {
200
+ this.isListCollapsed = !this.isListCollapsed;
201
+ this.saveLayoutPrefs();
202
+ this.cdr.markForCheck();
203
+ }
204
+ saveLayoutPrefs() {
205
+ const prefs = {
206
+ listSizePct: this.listSizePct,
207
+ detailSizePct: this.detailSizePct,
208
+ isListCollapsed: this.isListCollapsed,
209
+ };
210
+ UserInfoEngine.Instance.SetSettingDebounced('mj.predictiveStudio.registry.layout', JSON.stringify(prefs));
419
211
  }
420
212
  // ---- selection + master list ----
421
213
  select(id) {
@@ -425,61 +217,8 @@ export class PSRegistryComponent {
425
217
  return this.models.find((m) => m.id === this.selectedId) ?? this.models[0] ?? this.placeholder();
426
218
  }
427
219
  get selectedEntity() {
428
- return this.engine?.Models.find((m) => UUIDsEqual(m.ID, this.selectedId));
429
- }
430
- // ---- live detail: feature importance + metrics ----
431
- /** Top-6 feature importance bars parsed live from the selected model's `FeatureImportance` JSON, with humanized display names. */
432
- get importance() {
433
- return parseFeatureImportance(this.selectedEntity?.FeatureImportance, 6).map((b) => ({
434
- ...b,
435
- name: humanizeFeatureName(b.name),
436
- }));
437
- }
438
- get importanceCaption() {
439
- const n = this.importance.length;
440
- return n === 0 ? 'no data' : `normalized · top ${n}`;
441
- }
442
- get topFeatureName() {
443
- return this.importance[0]?.name ?? '—';
444
- }
445
- get topFeatureValue() {
446
- return this.importance[0]?.value ?? '—';
447
- }
448
- /** True when a single feature's |importance| meets/exceeds the dominance threshold. */
449
- get leakageFlagged() {
450
- const max = maxFeatureImportance(this.selectedEntity?.FeatureImportance);
451
- return max != null && max >= PS_FEATURE_DOMINANCE_THRESHOLD;
452
- }
453
- /** Holdout AUC hero number, formatted, or '—'. */
454
- get trainAuc() {
455
- const train = parseMetrics(this.selectedEntity?.Metrics).AUC;
456
- return train != null ? train.toFixed(3) : '—';
220
+ return this.engine?.Models?.find((m) => UUIDsEqual(m.ID, this.selectedId));
457
221
  }
458
- /** Secondary metric tiles (precision/recall/F1/log-loss/…) — only those actually recorded. */
459
- get secondaryMetrics() {
460
- const e = this.selectedEntity;
461
- if (!e)
462
- return [];
463
- // Prefer holdout metrics for the honest secondary numbers; fall back to training metrics.
464
- const holdout = metricsToDisplay(parseMetrics(e.HoldoutMetrics));
465
- return holdout.length > 0 ? holdout : metricsToDisplay(parseMetrics(e.Metrics));
466
- }
467
- /** The formatted train–holdout overfit gap, or '' when not computable. */
468
- get gapText() {
469
- const gap = overfitGap(this.selectedEntity ?? { Metrics: null, HoldoutMetrics: null });
470
- return gap == null ? '' : Math.abs(gap).toFixed(3);
471
- }
472
- get gapVerdict() {
473
- const gap = overfitGap(this.selectedEntity ?? { Metrics: null, HoldoutMetrics: null });
474
- if (gap == null)
475
- return '';
476
- return gap > 0.1 ? 'is wide — watch for overfitting' : 'is within tolerance — no overfitting flag';
477
- }
478
- get targetSuffix() {
479
- const target = this.selectedEntity?.TargetVariable;
480
- return target ? ` · predicts ${target}` : '';
481
- }
482
- // ---- lifecycle stepper ----
483
222
  statusClass(status) {
484
223
  switch (status) {
485
224
  case 'Published': return 'pub';
@@ -489,122 +228,9 @@ export class PSRegistryComponent {
489
228
  default: return 'dr';
490
229
  }
491
230
  }
492
- statusBadgeClass(status) {
493
- switch (status) {
494
- case 'Published': return 'green';
495
- case 'Validated': return 'blue';
496
- case 'Archived': return 'gray';
497
- default: return 'amber';
498
- }
499
- }
500
- /** Stepper state for a lifecycle step relative to the selected model's status. */
501
- stepState(step) {
502
- const order = PS_LIFECYCLE_STEPS.indexOf(this.selected.status);
503
- const idx = PS_LIFECYCLE_STEPS.indexOf(step);
504
- if (order < 0)
505
- return 'todo';
506
- if (idx < order)
507
- return 'done';
508
- if (idx === order)
509
- return 'curr';
510
- return 'todo';
511
- }
512
- // ---- promote/archive availability ----
513
- get canValidate() {
514
- return this.selected.status === 'Draft';
515
- }
516
- get canPublish() {
517
- return this.selected.status === 'Validated' || this.selected.status === 'Draft';
518
- }
519
- get canArchive() {
520
- return this.selected.status === 'Published' || this.selected.status === 'Validated';
521
- }
522
- get actionHint() {
523
- switch (this.selected.status) {
524
- case 'Published': return 'This model is live. Archiving detaches its scoring bindings.';
525
- case 'Validated': return 'Validated and ready to publish — publishing makes it available for scoring.';
526
- case 'Draft': return 'Newly registered. Validate it before publishing to production.';
527
- case 'Archived': return 'Archived — superseded by a newer model.';
528
- default: return '';
529
- }
530
- }
531
- // ---- promote/archive flow (Remote Op) ----
532
- /** Open the confirmation modal for a lifecycle transition. */
533
- requestPromote(targetStatus) {
534
- const entity = this.selectedEntity;
535
- if (!entity)
536
- return;
537
- this.pending = {
538
- modelId: entity.ID,
539
- modelName: this.selected.name,
540
- targetStatus,
541
- leakageFlagged: targetStatus === 'Published' && this.leakageFlagged,
542
- };
543
- }
544
- cancelPromote() {
545
- if (this.busy)
546
- return;
547
- this.pending = null;
548
- }
549
- get pendingTitle() {
550
- if (!this.pending)
551
- return '';
552
- return this.pending.targetStatus === 'Archived' ? 'Archive model' : `Promote to ${this.pending.targetStatus}`;
553
- }
554
- get pendingIcon() {
555
- if (!this.pending)
556
- return 'fa-solid fa-check';
557
- return this.pending.targetStatus === 'Archived' ? 'fa-solid fa-box-archive' : 'fa-solid fa-arrow-up';
558
- }
559
- get pendingConfirmLabel() {
560
- if (!this.pending)
561
- return 'Confirm';
562
- return this.pending.targetStatus === 'Archived' ? 'Archive' : `Promote to ${this.pending.targetStatus}`;
563
- }
564
- get pendingVariant() {
565
- if (!this.pending)
566
- return 'info';
567
- if (this.pending.targetStatus === 'Archived')
568
- return 'warn';
569
- return this.pending.leakageFlagged ? 'warn' : 'info';
570
- }
571
- get pendingMessage() {
572
- if (!this.pending)
573
- return '';
574
- const name = escapeHtml(this.pending.modelName);
575
- if (this.pending.targetStatus === 'Archived') {
576
- return `Archive <strong>${name}</strong>? This detaches any active scoring bindings and removes it from production scoring. The model artifact remains immutable and recoverable.`;
577
- }
578
- if (this.pending.leakageFlagged) {
579
- return `<strong>${name}</strong> is flagged for possible target leakage. Publishing requires an explicit sign-off — confirm you understand the risk and provide a reason below.`;
580
- }
581
- return `Promote <strong>${name}</strong> to <strong>${this.pending.targetStatus}</strong>? This changes only the lifecycle state — the trained weights never change.`;
582
- }
583
- /** Run the promote Remote Op, then refresh the engine + close on success. */
584
- async confirmPromote(reason) {
585
- if (!this.pending || this.busy)
586
- return;
587
- this.busy = true;
588
- const { modelId, modelName, targetStatus, leakageFlagged } = this.pending;
589
- try {
590
- const op = new PredictiveStudioPromoteModelOperation();
591
- const result = await op.Execute({ modelId, targetStatus, signOff: leakageFlagged ? true : undefined, reason: reason || undefined }, { provider: this.provider ?? undefined, user: this.currentUser ?? undefined });
592
- if (result.Success && result.Output?.promoted) {
593
- this.notifications.CreateSimpleNotification(`${modelName} → ${result.Output.status}`, 'success', 3500);
594
- await this.refreshAfterMutation();
595
- this.pending = null;
596
- }
597
- else {
598
- this.notifications.CreateSimpleNotification(result.ErrorMessage || `Could not promote ${modelName} (status: ${result.Output?.status ?? 'unchanged'}).`, 'error', 5000);
599
- }
600
- }
601
- catch (e) {
602
- this.notifications.CreateSimpleNotification(`Promotion failed: ${e instanceof Error ? e.message : String(e)}`, 'error', 5000);
603
- }
604
- finally {
605
- this.busy = false;
606
- this.cdr.detectChanges();
607
- }
231
+ // ---- lifecycle change handling ----
232
+ async onModelStatusChanged(evt) {
233
+ await this.refreshAfterMutation();
608
234
  }
609
235
  /** Force-refresh the engine's cached models, then rebuild the master list. */
610
236
  async refreshAfterMutation() {
@@ -622,35 +248,36 @@ export class PSRegistryComponent {
622
248
  }
623
249
  toVM(m) {
624
250
  const holdout = primaryAuc(m);
251
+ const score = primaryModelScore(m);
252
+ const formattedScore = score != null ? formatMetricValue(score.key, score.value) : (holdout != null ? holdout.toFixed(3) : '—');
625
253
  return {
626
254
  id: m.ID,
627
- name: this.engine.ModelDisplayName(m),
255
+ name: this.engine?.ModelDisplayName ? this.engine.ModelDisplayName(m) : (m.Pipeline || `Model v${m.Version}`),
628
256
  version: m.Version,
629
- algorithm: this.engine.AlgorithmName(m.AlgorithmID),
630
- holdoutAuc: holdout != null ? holdout.toFixed(3) : '—',
257
+ algorithm: this.engine?.AlgorithmName ? this.engine.AlgorithmName(m.AlgorithmID) : (m.Algorithm || 'Algorithm'),
258
+ holdoutAuc: holdout != null ? holdout.toFixed(3) : formattedScore,
259
+ holdoutScore: formattedScore,
260
+ scoreLabel: score?.label ?? 'AUC',
631
261
  status: m.Status,
632
262
  iconClass: 'xgb',
633
263
  };
634
264
  }
635
265
  placeholder() {
636
- return { id: '', name: 'No model', version: 0, algorithm: '—', holdoutAuc: '—', status: 'Draft', iconClass: 'xgb' };
266
+ return { id: '', name: 'No model', version: 0, algorithm: '—', holdoutAuc: '—', holdoutScore: '—', scoreLabel: 'AUC', status: 'Draft', iconClass: 'xgb' };
637
267
  }
638
268
  static ɵfac = function PSRegistryComponent_Factory(__ngFactoryType__) { return new (__ngFactoryType__ || PSRegistryComponent)(); };
639
- static ɵcmp = /*@__PURE__*/ i0.ɵɵdefineComponent({ type: PSRegistryComponent, selectors: [["ps-registry"]], inputs: { engine: "engine", provider: "provider", currentUser: "currentUser" }, decls: 4, vars: 2, consts: [["data-testid", "ps-registry-panel", 1, "ps-panel", "ps-registry"], ["data-testid", "ps-registry-empty", 1, "ps-empty"], [1, "md-layout"], [3, "title", "icon", "confirmIcon", "confirmLabel", "variant", "showReason", "reasonRequired", "reasonLabel", "reasonPlaceholder", "busy"], [1, "ps-empty-ico"], [1, "fa-solid", "fa-cubes"], ["data-testid", "ps-registry-list", 1, "ps-card", "mlist"], [1, "ps-card-head"], [1, "ps-badge", "gray"], [1, "ps-card-body", 2, "padding", "8px"], ["data-testid", "ps-registry-row", 1, "mrow", 3, "sel", "arc"], ["data-testid", "ps-registry-detail", 1, "ps-col", "detail"], [1, "ps-card"], [1, "ps-card-body", "dh"], [1, "big-ico"], [1, "fa-solid", "fa-cube"], [2, "flex", "1"], ["data-testid", "ps-registry-detail-name"], [1, "ps-tag", "ps-mono"], [1, "ps-muted", "ps-small", "sub"], [1, "ps-badge"], [1, "ps-card-body"], [1, "ps-section-title"], [1, "ps-stepper"], [1, "ps-small", "ps-muted", 2, "margin-top", "10px"], [1, "ps-muted", "ps-small"], [1, "metric-pair"], [1, "mtile"], [1, "v"], [1, "mtile", "honest"], [1, "stat-bar"], [1, "ps-small", "ps-muted", 2, "margin-top", "6px"], [1, "ps-small", "ps-muted"], [1, "ps-callout", "gate", 3, "success", "warn"], [1, "ps-card-body", "ps-row", 2, "align-items", "center"], [1, "ps-muted", "ps-small", 2, "flex", "1"], ["mjButton", "", "variant", "secondary", "size", "sm", "data-testid", "ps-registry-validate"], ["mjButton", "", "variant", "primary", "size", "sm", "data-testid", "ps-registry-promote"], ["mjButton", "", "variant", "secondary", "size", "sm", "data-testid", "ps-registry-archive"], ["data-testid", "ps-registry-row", 1, "mrow", 3, "click"], [1, "ico"], [2, "flex", "1", "min-width", "0"], [1, "nm"], [1, "ln2", "ps-muted", "ps-small"], [1, "auc"], [1, "st"], [1, "ps-step"], [1, "pip"], [1, "fa-solid", "fa-check"], [1, "ln"], [1, "b"], [1, "ps-fbar"], [1, "name", "ps-small", 3, "title"], [1, "track"], [1, "ps-mono"], [1, "ps-callout", "gate"], [1, "ps-small", 2, "margin-top", "6px"], [1, "fa-solid", "fa-pen"], ["mjButton", "", "variant", "secondary", "size", "sm", "data-testid", "ps-registry-validate", 3, "click"], [1, "fa-solid", "fa-circle-check"], ["mjButton", "", "variant", "primary", "size", "sm", "data-testid", "ps-registry-promote", 3, "click"], [1, "fa-solid", "fa-arrow-up"], ["mjButton", "", "variant", "secondary", "size", "sm", "data-testid", "ps-registry-archive", 3, "click"], [1, "fa-solid", "fa-box-archive"], [3, "confirmed", "cancelled", "title", "icon", "confirmIcon", "confirmLabel", "variant", "showReason", "reasonRequired", "reasonLabel", "reasonPlaceholder", "busy"], [3, "innerHTML"]], template: function PSRegistryComponent_Template(rf, ctx) { if (rf & 1) {
269
+ static ɵcmp = /*@__PURE__*/ i0.ɵɵdefineComponent({ type: PSRegistryComponent, selectors: [["ps-registry"]], inputs: { engine: "engine", provider: "provider", currentUser: "currentUser", initialModelId: "initialModelId" }, decls: 3, vars: 1, consts: [["data-testid", "ps-registry-panel", 1, "ps-panel", "ps-registry"], ["data-testid", "ps-registry-empty", 1, "ps-empty"], [1, "reg-layout", 3, "list-collapsed"], [1, "ps-empty-ico"], [1, "fa-solid", "fa-cubes"], [1, "reg-layout"], ["role", "region", "aria-label", "Model registry list (collapsed)", 1, "reg-collapsed-strip"], ["direction", "horizontal", "unit", "percent", 1, "reg-splitter", 3, "dragEnd", "gutterSize"], [3, "size", "minSize", "maxSize"], [3, "size", "minSize"], ["data-testid", "ps-registry-detail", 1, "ps-col", "detail"], [3, "model", "displayName", "provider", "currentUser"], [1, "ps-empty", 2, "padding", "40px 20px"], ["type", "button", "aria-label", "Expand models list", "title", "Expand list", 1, "reg-rail-collapse", 3, "click"], [1, "fa-solid", "fa-chevron-right"], [1, "reg-collapsed-strip-label"], ["data-testid", "ps-registry-list", 1, "ps-card", "mlist"], [1, "ps-card-head"], [2, "display", "flex", "gap", "6px", "align-items", "center"], [1, "ps-badge", "gray"], ["type", "button", "title", "Collapse list", "aria-label", "Collapse list", 1, "reg-collapse-btn", 3, "click"], [1, "fa-solid", "fa-chevron-left"], [1, "ps-card-body", 2, "padding", "8px"], ["data-testid", "ps-registry-row", 1, "mrow", 3, "sel", "arc"], ["data-testid", "ps-registry-row", 1, "mrow", 3, "click"], [1, "ico"], [1, "fa-solid", "fa-cube"], [2, "flex", "1", "min-width", "0"], [1, "nm"], [1, "ln2", "ps-muted", "ps-small"], [1, "auc"], [1, "v"], [1, "st"], [3, "statusChanged", "model", "displayName", "provider", "currentUser"], [1, "ps-muted"]], template: function PSRegistryComponent_Template(rf, ctx) { if (rf & 1) {
640
270
  i0.ɵɵelementStart(0, "div", 0);
641
- i0.ɵɵconditionalCreate(1, PSRegistryComponent_Conditional_1_Template, 7, 0, "div", 1)(2, PSRegistryComponent_Conditional_2_Template, 75, 19, "div", 2);
642
- i0.ɵɵconditionalCreate(3, PSRegistryComponent_Conditional_3_Template, 2, 11, "ps-confirm-modal", 3);
271
+ i0.ɵɵconditionalCreate(1, PSRegistryComponent_Conditional_1_Template, 7, 0, "div", 1)(2, PSRegistryComponent_Conditional_2_Template, 8, 8, "div", 2);
643
272
  i0.ɵɵelementEnd();
644
273
  } if (rf & 2) {
645
274
  i0.ɵɵadvance();
646
275
  i0.ɵɵconditional(ctx.models.length === 0 ? 1 : 2);
647
- i0.ɵɵadvance(2);
648
- i0.ɵɵconditional(ctx.pending ? 3 : -1);
649
- } }, dependencies: [CommonModule, MJButtonDirective, PSConfirmModalComponent], styles: ["/* ============================================================================\n Predictive Studio \u2014 shared panel styles (design-token compliant)\n Maps the mockup design system onto MemberJunction semantic --mj-* tokens.\n Loaded by every PS panel component (styleUrls) under ViewEncapsulation.None,\n so the class names are global within the PS dashboard subtree.\n No hardcoded colors except the intentional categorical node/algorithm palette\n (declared once as --ps-* custom props below) which must stay distinct across\n light/dark to remain readable as diagram accents.\n ============================================================================ */\n\n/* Categorical accents for DAG node types + algorithm chips. These are\n data-visualization colors that must remain distinct regardless of theme\n (allowed exception #3 in the design-token rules). */\n.ps-panel {\n --ps-node-source: #6b7280;\n --ps-node-feature: #2050c8;\n --ps-node-embedding: #7b3ff0;\n --ps-node-target: #1f9d57;\n --ps-node-algorithm: #d98213;\n /* budget-gauge categorical accents (reuse the node palette so light/dark stay distinct) */\n --ps-gauge-cost: var(--ps-node-algorithm);\n --ps-gauge-iterations: var(--ps-node-feature);\n}\n\n.ps-panel {\n display: block;\n height: 100%;\n}\n\n/* ---- generic layout helpers ---- */\n.ps-row { display: flex; gap: 16px; }\n.ps-col { display: flex; flex-direction: column; gap: 16px; }\n.ps-wrap { flex-wrap: wrap; }\n.ps-spacer { flex: 1; }\n.ps-muted { color: var(--mj-text-muted); }\n.ps-small { font-size: 12.5px; }\n.ps-mono { font-family: ui-monospace, 'SF Mono', Menlo, Consolas, monospace; font-size: 12.5px; }\n.ps-section-title {\n font-size: 12px; text-transform: uppercase; letter-spacing: .06em;\n color: var(--mj-text-muted); margin: 4px 0 2px; font-weight: 700;\n}\n\n/* ---- cards ---- */\n.ps-card {\n background: var(--mj-bg-surface);\n border: 1px solid var(--mj-border-default);\n border-radius: 8px;\n overflow: hidden;\n}\n.ps-card-head {\n display: flex; align-items: center; gap: 10px;\n padding: 14px 16px; border-bottom: 1px solid var(--mj-border-subtle);\n}\n.ps-card-head h3 { font-size: 14.5px; margin: 0; font-weight: 650; color: var(--mj-text-primary); }\n.ps-card-body { padding: 16px; }\n\n/* ---- KPI tiles ---- */\n.ps-kpi {\n background: var(--mj-bg-surface); border: 1px solid var(--mj-border-default);\n border-radius: 8px; padding: 16px 18px;\n}\n.ps-kpi .label { font-size: 12px; color: var(--mj-text-muted); font-weight: 600; }\n.ps-kpi .val { font-size: 26px; font-weight: 720; letter-spacing: -.02em; margin-top: 2px; color: var(--mj-text-primary); }\n\n/* ---- badges ---- */\n.ps-badge {\n display: inline-flex; align-items: center; gap: 5px;\n padding: 2px 9px; border-radius: 999px; font-size: 11.5px; font-weight: 650; line-height: 1.7;\n}\n.ps-badge.gray { background: var(--mj-bg-surface-sunken); color: var(--mj-text-secondary); }\n.ps-badge.blue { background: var(--mj-status-info-bg); color: var(--mj-status-info-text); }\n.ps-badge.green { background: var(--mj-status-success-bg); color: var(--mj-status-success-text); }\n.ps-badge.amber { background: var(--mj-status-warning-bg); color: var(--mj-status-warning-text); }\n.ps-badge.red { background: var(--mj-status-error-bg); color: var(--mj-status-error-text); }\n.ps-dot { width: 7px; height: 7px; border-radius: 50%; display: inline-block; }\n\n/* recommendation-level chips (the ranking matrix payoff) */\n.ps-lvl {\n display: inline-flex; align-items: center; justify-content: center;\n min-width: 92px; padding: 4px 10px; border-radius: 7px;\n font-weight: 650; font-size: 11.5px; color: var(--mj-text-inverse);\n}\n.ps-lvl-Primary { background: var(--mj-status-success); }\n.ps-lvl-Strong { background: var(--mj-status-info); }\n.ps-lvl-Viable { background: var(--mj-status-warning); }\n.ps-lvl-Weak { background: var(--mj-text-disabled); }\n.ps-lvl-NotRecommended { background: var(--mj-bg-surface-sunken); color: var(--mj-text-muted); }\n\n/* ---- tags ---- */\n.ps-tag {\n display: inline-block; padding: 2px 8px; border-radius: 6px;\n background: var(--mj-bg-surface-sunken); color: var(--mj-text-secondary);\n font-size: 11.5px; font-weight: 600;\n}\n.ps-tag.muted { opacity: .6; }\n.ps-divider { height: 1px; background: var(--mj-border-subtle); margin: 8px 0; }\n\n/* ---- tables ---- */\n.ps-table { width: 100%; border-collapse: collapse; font-size: 13px; }\n.ps-table th {\n text-align: left; font-size: 11.5px; text-transform: uppercase; letter-spacing: .04em;\n color: var(--mj-text-muted); font-weight: 700; padding: 9px 12px;\n border-bottom: 1px solid var(--mj-border-default);\n}\n.ps-table td { padding: 11px 12px; border-bottom: 1px solid var(--mj-border-subtle); color: var(--mj-text-primary); }\n.ps-table tr:hover td { background: var(--mj-bg-surface-hover); }\n.ps-table .num { text-align: right; font-variant-numeric: tabular-nums; }\n\n.ps-rank {\n width: 22px; height: 22px; border-radius: 6px; background: var(--mj-bg-surface-sunken);\n display: inline-grid; place-items: center; font-weight: 700; font-size: 11.5px; color: var(--mj-text-secondary);\n}\n.ps-rank.gold { background: var(--mj-status-warning-bg); color: var(--mj-status-warning-text); }\n\n/* ---- progress bars ---- */\n.ps-bar { height: 8px; border-radius: 999px; background: var(--mj-bg-surface-sunken); overflow: hidden; }\n.ps-bar > span { display: block; height: 100%; border-radius: 999px; background: var(--mj-brand-primary); }\n.ps-bar.warn > span { background: var(--mj-status-warning); }\n.ps-bar.run > span { background: var(--mj-brand-primary); }\n\n.ps-fbar { display: grid; grid-template-columns: minmax(150px, 1.3fr) 2fr 56px; align-items: center; gap: 14px; padding: 8px 0; }\n.ps-fbar + .ps-fbar { border-top: 1px solid var(--mj-border-subtle); }\n.ps-fbar .track { height: 10px; border-radius: var(--mj-radius-full); background: var(--mj-bg-surface-sunken); overflow: hidden; }\n.ps-fbar .track > span { display: block; height: 100%; border-radius: var(--mj-radius-full); background: var(--mj-brand-primary); }\n.ps-fbar .track > span.warn { background: var(--mj-status-warning); }\n.ps-fbar .name { color: var(--mj-text-primary); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }\n.ps-fbar > .ps-mono { text-align: right; color: var(--mj-text-secondary); font-variant-numeric: tabular-nums; }\n\n/* gauge */\n.ps-gauge {\n position: relative; width: 64px; height: 64px; border-radius: 50%; flex: 0 0 auto;\n background: conic-gradient(var(--ps-gauge-color, var(--mj-brand-primary)) calc(var(--ps-p, 40) * 1%), var(--mj-bg-surface-sunken) 0);\n display: grid; place-items: center;\n}\n.ps-gauge::after { content: ''; position: absolute; inset: 9px; background: var(--mj-bg-surface); border-radius: 50%; }\n.ps-gauge .g-val { position: relative; font-weight: 720; font-size: 14px; color: var(--mj-text-primary); }\n\n/* warning / info callouts */\n.ps-callout { display: flex; gap: 11px; padding: 12px 14px; border-radius: 10px; border: 1px solid; }\n.ps-callout.warn { background: var(--mj-status-warning-bg); border-color: var(--mj-status-warning-border); color: var(--mj-status-warning-text); }\n.ps-callout.info { background: var(--mj-status-info-bg); border-color: var(--mj-status-info-border); color: var(--mj-status-info-text); }\n.ps-callout.success { background: var(--mj-status-success-bg); border-color: var(--mj-status-success-border); color: var(--mj-status-success-text); }\n.ps-callout i { font-size: 16px; margin-top: 1px; }\n\n/* lifecycle stepper */\n.ps-stepper { display: flex; align-items: center; gap: 6px; flex-wrap: wrap; }\n.ps-step { display: flex; align-items: center; gap: 6px; font-size: 12px; font-weight: 600; color: var(--mj-text-muted); }\n.ps-step .pip {\n width: 22px; height: 22px; border-radius: 50%; border: 2px solid var(--mj-border-strong);\n display: grid; place-items: center; font-size: 11px;\n}\n.ps-step.done { color: var(--mj-status-success-text); }\n.ps-step.done .pip { background: var(--mj-status-success); border-color: var(--mj-status-success); color: var(--mj-text-inverse); }\n.ps-step.curr { color: var(--mj-brand-primary); }\n.ps-step.curr .pip { border-color: var(--mj-brand-primary); color: var(--mj-brand-primary); }\n.ps-step .ln { width: 26px; height: 2px; background: var(--mj-border-strong); }\n\n/* ---- DAG flow canvas ---- */\n.ps-flow {\n position: relative;\n background:\n linear-gradient(var(--mj-border-subtle) 1px, transparent 1px) 0 0 / 22px 22px,\n linear-gradient(90deg, var(--mj-border-subtle) 1px, transparent 1px) 0 0 / 22px 22px,\n var(--mj-bg-surface-card);\n border: 1px solid var(--mj-border-default); border-radius: 10px;\n min-height: 620px; overflow: hidden;\n}\n.ps-node {\n position: absolute; width: 190px; background: var(--mj-bg-surface);\n border: 1px solid var(--mj-border-strong); border-left-width: 3px; border-radius: 8px;\n cursor: pointer;\n}\n.ps-node .nh {\n display: flex; align-items: center; gap: 8px; padding: 9px 11px;\n border-bottom: 1px solid var(--mj-border-subtle); font-weight: 650; font-size: 12.5px; color: var(--mj-text-primary);\n}\n.ps-node .nh i.tile {\n width: 24px; height: 24px; border-radius: 6px; display: grid; place-items: center;\n color: var(--mj-text-inverse); font-size: 12px; flex: 0 0 auto;\n}\n.ps-node .nh .ps-tag { margin-left: auto; }\n.ps-node .nb { padding: 9px 11px; font-size: 12px; color: var(--mj-text-secondary); }\n.ps-node .nb-row { display: flex; justify-content: space-between; gap: 8px; padding: 2px 0; }\n.ps-node .nb-row .k { color: var(--mj-text-muted); }\n.ps-node.src { border-left-color: var(--ps-node-source); }\n.ps-node.src .nh i.tile { background: var(--ps-node-source); }\n.ps-node.feat { border-left-color: var(--ps-node-feature); }\n.ps-node.feat .nh i.tile { background: var(--ps-node-feature); }\n.ps-node.emb { border-left-color: var(--ps-node-embedding); }\n.ps-node.emb .nh i.tile { background: var(--ps-node-embedding); }\n.ps-node.target { border-left-color: var(--ps-node-target); }\n.ps-node.target .nh i.tile { background: var(--ps-node-target); }\n.ps-node.algo { border-left-color: var(--ps-node-algorithm); }\n.ps-node.algo .nh i.tile { background: var(--ps-node-algorithm); }\n.ps-node.output { border-left-color: var(--mj-status-success); }\n.ps-node.output .nh i.tile { background: var(--mj-status-success); }\n.ps-node.selected {\n border-color: var(--mj-brand-primary);\n box-shadow: 0 0 0 3px color-mix(in srgb, var(--mj-brand-primary) 18%, transparent);\n}\n.ps-node .port {\n position: absolute; width: 11px; height: 11px; border-radius: 50%;\n background: var(--mj-bg-surface); border: 2px solid var(--mj-brand-primary);\n top: 50%; transform: translateY(-50%);\n}\n.ps-node .port.in { left: -6px; }\n.ps-node .port.out { right: -6px; }\nsvg.ps-edges { position: absolute; inset: 0; width: 100%; height: 100%; pointer-events: none; }\nsvg.ps-edges path { stroke: color-mix(in srgb, var(--mj-brand-primary) 45%, transparent); stroke-width: 2; fill: none; }\n\n/* palette chips */\n.ps-pchip {\n display: inline-flex; align-items: center; gap: 7px; padding: 6px 11px; border-radius: 8px;\n border: 1px dashed var(--mj-border-strong); background: var(--mj-bg-surface);\n font-size: 12.5px; font-weight: 600; color: var(--mj-text-secondary); cursor: grab;\n}\n.ps-pchip i { font-size: 12px; color: var(--mj-text-inverse); width: 20px; height: 20px; border-radius: 5px; display: grid; place-items: center; }\n.ps-pchip.s i { background: var(--ps-node-source); }\n.ps-pchip.f i { background: var(--ps-node-feature); }\n.ps-pchip.e i { background: var(--ps-node-embedding); }\n.ps-pchip.t i { background: var(--ps-node-target); }\n.ps-pchip.a i { background: var(--ps-node-algorithm); }\n\n/* segmented control */\n.ps-seg { display: inline-flex; border: 1px solid var(--mj-border-default); border-radius: 8px; overflow: hidden; }\n.ps-seg button {\n border: none; background: var(--mj-bg-surface); color: var(--mj-text-secondary);\n padding: 6px 12px; font-size: 12.5px; font-weight: 600; cursor: pointer;\n border-right: 1px solid var(--mj-border-default);\n}\n.ps-seg button:last-child { border-right: none; }\n.ps-seg button.on { background: var(--mj-brand-primary); color: var(--mj-text-inverse); }\n\n/* field control */\n.ps-field { margin-bottom: 14px; }\n.ps-field > label {\n display: block; font-size: 11px; text-transform: uppercase; letter-spacing: .04em;\n color: var(--mj-text-muted); font-weight: 700; margin-bottom: 6px;\n}\n.ps-input {\n width: 100%; padding: 8px 10px; border: 1px solid var(--mj-border-default);\n border-radius: 6px; font-size: 13px; background: var(--mj-bg-surface); color: var(--mj-text-primary);\n box-sizing: border-box;\n}\n.ps-input:focus { outline: none; border-color: var(--mj-brand-primary); box-shadow: 0 0 0 3px color-mix(in srgb, var(--mj-brand-primary) 12%, transparent); }\n\n/* loading container */\n.ps-loading { display: flex; align-items: center; justify-content: center; padding: 60px 0; }\n\n/* ---- empty / prompt states ---- */\n.ps-empty {\n display: flex; flex-direction: column; align-items: center; justify-content: center;\n text-align: center; gap: 10px; padding: 56px 24px; color: var(--mj-text-muted);\n}\n.ps-empty .ps-empty-ico {\n width: 60px; height: 60px; border-radius: 16px; display: grid; place-items: center;\n background: var(--mj-bg-surface-sunken); color: var(--mj-text-disabled); font-size: 24px;\n}\n.ps-empty h3 { margin: 4px 0 0; font-size: 16px; font-weight: 650; color: var(--mj-text-secondary); }\n.ps-empty p { margin: 0; max-width: 420px; font-size: 13px; line-height: 1.5; }\n\n/* ---- confirm modal ---- */\n.ps-modal-backdrop {\n position: fixed; inset: 0; z-index: 1000;\n background: var(--mj-bg-overlay);\n display: flex; align-items: center; justify-content: center; padding: 24px;\n}\n.ps-modal {\n width: 100%; max-width: 480px; background: var(--mj-bg-surface);\n border: 1px solid var(--mj-border-default); border-radius: 14px;\n box-shadow: var(--mj-shadow-lg, var(--mj-shadow-sm)); overflow: hidden;\n}\n.ps-modal-head {\n display: flex; align-items: center; gap: 12px; padding: 18px 20px;\n border-bottom: 1px solid var(--mj-border-subtle);\n}\n.ps-modal-head .ico {\n width: 38px; height: 38px; border-radius: 10px; display: grid; place-items: center;\n font-size: 16px; flex: 0 0 auto;\n background: var(--mj-status-info-bg); color: var(--mj-status-info-text);\n}\n.ps-modal-head.warn .ico { background: var(--mj-status-warning-bg); color: var(--mj-status-warning-text); }\n.ps-modal-head.danger .ico { background: var(--mj-status-error-bg); color: var(--mj-status-error-text); }\n.ps-modal-head h3 { margin: 0; font-size: 15.5px; font-weight: 650; color: var(--mj-text-primary); }\n.ps-modal-body { padding: 18px 20px; font-size: 13.5px; line-height: 1.55; color: var(--mj-text-secondary); }\n.ps-modal-body .ps-field { margin-top: 14px; margin-bottom: 0; }\n.ps-modal-foot {\n display: flex; align-items: center; gap: 10px; padding: 14px 20px;\n border-top: 1px solid var(--mj-border-subtle); background: var(--mj-bg-surface-card);\n}\n.ps-modal-foot .ps-spacer { flex: 1; }\n", "/* The panel fills its section; the list and detail columns each scroll on their own. */\n.ps-registry { display: flex; flex-direction: column; height: 100%; min-height: 0; }\n.ps-registry .md-layout {\n flex: 1; min-height: 0;\n display: grid; grid-template-columns: 300px 1fr; grid-template-rows: minmax(0, 1fr); gap: 18px;\n}\n@media (max-width: 1080px) {\n .ps-registry .md-layout { grid-template-columns: 1fr; grid-template-rows: minmax(0, 1fr) minmax(0, 1.4fr); }\n}\n\n/* model list \u2014 its own scroll column */\n.ps-registry .mlist { min-height: 0; overflow-y: auto; align-self: start; max-height: 100%; }\n\n/* detail \u2014 its own scroll column, independent of the list. NOTE: .detail is also a\n .ps-col (display:flex column), so its card children ARE flex items \u2014 without\n flex-shrink:0 they compress to fit (scrollHeight == clientHeight, verified live) and\n the column never scrolls. The child rule is load-bearing, not decorative. */\n.ps-registry .detail { min-height: 0; overflow-y: auto; padding-right: 2px; }\n.ps-registry .detail > * { flex: 0 0 auto; }\n.ps-registry .mrow {\n display: flex; align-items: center; gap: 10px; padding: 10px; border-radius: 8px; cursor: pointer; transition: background .12s;\n}\n.ps-registry .mrow:hover { background: var(--mj-bg-surface-hover); }\n.ps-registry .mrow.sel { background: color-mix(in srgb, var(--mj-brand-primary) 10%, var(--mj-bg-surface)); box-shadow: inset 3px 0 0 var(--mj-brand-primary); }\n.ps-registry .mrow.arc { opacity: .72; }\n.ps-registry .mrow .ico {\n width: 30px; height: 30px; border-radius: 7px; display: grid; place-items: center; color: var(--mj-text-inverse); font-size: 13px; flex: 0 0 auto;\n}\n.ps-registry .mrow .ico.xgb { background: var(--ps-node-feature); }\n.ps-registry .mrow .ico.lgbm { background: var(--ps-node-algorithm); }\n.ps-registry .mrow .ico.rf { background: var(--ps-node-source); }\n.ps-registry .mrow .nm { font-weight: 600; color: var(--mj-text-primary); font-size: 13px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }\n.ps-registry .mrow .auc { text-align: right; flex: 0 0 auto; }\n.ps-registry .mrow .auc .v { font-weight: 700; color: var(--mj-text-primary); font-variant-numeric: tabular-nums; }\n.ps-registry .mrow .auc .st { font-size: 10.5px; font-weight: 700; text-transform: uppercase; letter-spacing: .03em; }\n.ps-registry .mrow .auc .st.pub { color: var(--mj-status-success-text); }\n.ps-registry .mrow .auc .st.val { color: var(--mj-status-info-text); }\n.ps-registry .mrow .auc .st.dr { color: var(--mj-text-muted); }\n.ps-registry .mrow .auc .st.arc { color: var(--mj-text-disabled); }\n\n/* detail */\n.ps-registry .dh { display: flex; align-items: center; gap: 14px; }\n.ps-registry .big-ico {\n width: 48px; height: 48px; border-radius: 12px; display: grid; place-items: center;\n background: var(--ps-node-feature); color: var(--mj-text-inverse); font-size: 20px; flex: 0 0 auto;\n}\n.ps-registry .dh h2 { font-size: 18px; color: var(--mj-text-primary); display: flex; align-items: center; gap: 8px; }\n.ps-registry .dh .sub { margin-top: 2px; }\n\n.ps-registry .metric-pair { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; }\n.ps-registry .mtile { border: 1px solid var(--mj-border-default); border-radius: 9px; padding: 14px; }\n.ps-registry .mtile .v { font-size: 28px; font-weight: 720; color: var(--mj-text-primary); letter-spacing: -.02em; }\n.ps-registry .mtile.honest { border-color: var(--mj-brand-primary); background: color-mix(in srgb, var(--mj-brand-primary) 6%, var(--mj-bg-surface)); }\n.ps-registry .mtile.honest .v { color: var(--mj-brand-primary); }\n.ps-registry .stat-bar { display: flex; gap: 10px; margin-top: 14px; flex-wrap: wrap; }\n.ps-registry .stat-bar .b { display: flex; flex-direction: column; gap: 2px; padding: 8px 12px; border: 1px solid var(--mj-border-subtle); border-radius: 8px; flex: 1; min-width: 90px; }\n.ps-registry .stat-bar .b strong { color: var(--mj-text-primary); font-size: 15px; }\n\n.ps-registry .gate { align-items: flex-start; }\n.ps-registry .gate i { font-size: 20px; }\n\n.ps-registry .lineage { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }\n.ps-registry .ln-card { border: 1px solid var(--mj-border-default); border-radius: 9px; padding: 10px 12px; min-width: 120px; }\n.ps-registry .ln-card.cur { border-color: var(--mj-brand-primary); background: color-mix(in srgb, var(--mj-brand-primary) 6%, var(--mj-bg-surface)); }\n.ps-registry .ln-card .t { font-size: 10.5px; text-transform: uppercase; letter-spacing: .04em; color: var(--mj-text-muted); font-weight: 700; }\n.ps-registry .ln-card .n { font-weight: 650; color: var(--mj-text-primary); font-size: 13px; margin: 2px 0; }\n.ps-registry .ln-arrow { color: var(--mj-text-disabled); }\n"], encapsulation: 2 });
276
+ } }, dependencies: [CommonModule, AngularSplitModule, i1.SplitComponent, i1.SplitAreaComponent, PSModelDetailComponent], styles: ["/* ============================================================================\n Predictive Studio \u2014 shared panel styles (design-token compliant)\n Maps the mockup design system onto MemberJunction semantic --mj-* tokens.\n Loaded by every PS panel component (styleUrls) under ViewEncapsulation.None,\n so the class names are global within the PS dashboard subtree.\n No hardcoded colors except the intentional categorical node/algorithm palette\n (declared once as --ps-* custom props below) which must stay distinct across\n light/dark to remain readable as diagram accents.\n ============================================================================ */\n\n/* Categorical accents for DAG node types + algorithm chips. These are\n data-visualization colors that must remain distinct regardless of theme\n (allowed exception #3 in the design-token rules). */\n.ps-panel {\n --ps-node-source: #6b7280;\n --ps-node-feature: #2050c8;\n --ps-node-embedding: #7b3ff0;\n --ps-node-target: #1f9d57;\n --ps-node-algorithm: #d98213;\n /* budget-gauge categorical accents (reuse the node palette so light/dark stay distinct) */\n --ps-gauge-cost: var(--ps-node-algorithm);\n --ps-gauge-iterations: var(--ps-node-feature);\n}\n\n.ps-panel {\n display: block;\n height: 100%;\n}\n\n/* ---- generic layout helpers ---- */\n.ps-row { display: flex; gap: 16px; }\n.ps-col { display: flex; flex-direction: column; gap: 16px; }\n.ps-wrap { flex-wrap: wrap; }\n.ps-spacer { flex: 1; }\n.ps-muted { color: var(--mj-text-muted); }\n.ps-small { font-size: 12.5px; }\n.ps-mono { font-family: ui-monospace, 'SF Mono', Menlo, Consolas, monospace; font-size: 12.5px; }\n.ps-section-title {\n font-size: 12px; text-transform: uppercase; letter-spacing: .06em;\n color: var(--mj-text-muted); margin: 4px 0 2px; font-weight: 700;\n}\n\n/* ---- cards ---- */\n.ps-card {\n background: var(--mj-bg-surface);\n border: 1px solid var(--mj-border-default);\n border-radius: 8px;\n overflow: hidden;\n}\n.ps-card-head {\n display: flex; align-items: center; gap: 10px;\n padding: 14px 16px; border-bottom: 1px solid var(--mj-border-subtle);\n}\n.ps-card-head h3 { font-size: 14.5px; margin: 0; font-weight: 650; color: var(--mj-text-primary); }\n.ps-card-body { padding: 16px; }\n\n/* ---- KPI tiles ---- */\n.ps-kpi {\n background: var(--mj-bg-surface); border: 1px solid var(--mj-border-default);\n border-radius: 8px; padding: 16px 18px;\n}\n.ps-kpi .label { font-size: 12px; color: var(--mj-text-muted); font-weight: 600; }\n.ps-kpi .val { font-size: 26px; font-weight: 720; letter-spacing: -.02em; margin-top: 2px; color: var(--mj-text-primary); }\n\n/* ---- badges ---- */\n.ps-badge {\n display: inline-flex; align-items: center; gap: 5px;\n padding: 2px 9px; border-radius: 999px; font-size: 11.5px; font-weight: 650; line-height: 1.7;\n}\n.ps-badge.gray { background: var(--mj-bg-surface-sunken); color: var(--mj-text-secondary); }\n.ps-badge.blue { background: var(--mj-status-info-bg); color: var(--mj-status-info-text); }\n.ps-badge.green { background: var(--mj-status-success-bg); color: var(--mj-status-success-text); }\n.ps-badge.amber { background: var(--mj-status-warning-bg); color: var(--mj-status-warning-text); }\n.ps-badge.red { background: var(--mj-status-error-bg); color: var(--mj-status-error-text); }\n.ps-dot { width: 7px; height: 7px; border-radius: 50%; display: inline-block; }\n\n/* recommendation-level chips (the ranking matrix payoff) */\n.ps-lvl {\n display: inline-flex; align-items: center; justify-content: center;\n min-width: 92px; padding: 4px 10px; border-radius: 7px;\n font-weight: 650; font-size: 11.5px; color: var(--mj-text-inverse);\n}\n.ps-lvl-Primary { background: var(--mj-status-success); }\n.ps-lvl-Strong { background: var(--mj-status-info); }\n.ps-lvl-Viable { background: var(--mj-status-warning); }\n.ps-lvl-Weak { background: var(--mj-text-disabled); }\n.ps-lvl-NotRecommended { background: var(--mj-bg-surface-sunken); color: var(--mj-text-muted); }\n\n/* ---- tags ---- */\n.ps-tag {\n display: inline-block; padding: 2px 8px; border-radius: 6px;\n background: var(--mj-bg-surface-sunken); color: var(--mj-text-secondary);\n font-size: 11.5px; font-weight: 600;\n}\n.ps-tag.muted { opacity: .6; }\n.ps-divider { height: 1px; background: var(--mj-border-subtle); margin: 8px 0; }\n\n/* ---- tables ---- */\n.ps-table { width: 100%; border-collapse: collapse; font-size: 13px; }\n.ps-table th {\n text-align: left; font-size: 11.5px; text-transform: uppercase; letter-spacing: .04em;\n color: var(--mj-text-muted); font-weight: 700; padding: 9px 12px;\n border-bottom: 1px solid var(--mj-border-default);\n}\n.ps-table td { padding: 11px 12px; border-bottom: 1px solid var(--mj-border-subtle); color: var(--mj-text-primary); }\n.ps-table tr:hover td { background: var(--mj-bg-surface-hover); }\n.ps-table .num { text-align: right; font-variant-numeric: tabular-nums; }\n\n.ps-rank {\n width: 22px; height: 22px; border-radius: 6px; background: var(--mj-bg-surface-sunken);\n display: inline-grid; place-items: center; font-weight: 700; font-size: 11.5px; color: var(--mj-text-secondary);\n}\n.ps-rank.gold { background: var(--mj-status-warning-bg); color: var(--mj-status-warning-text); }\n\n/* ---- progress bars ---- */\n.ps-bar { height: 8px; border-radius: 999px; background: var(--mj-bg-surface-sunken); overflow: hidden; }\n.ps-bar > span { display: block; height: 100%; border-radius: 999px; background: var(--mj-brand-primary); }\n.ps-bar.warn > span { background: var(--mj-status-warning); }\n.ps-bar.run > span { background: var(--mj-brand-primary); }\n\n.ps-fbar { display: grid; grid-template-columns: minmax(150px, 1.3fr) 2fr 56px; align-items: center; gap: 14px; padding: 8px 0; }\n.ps-fbar + .ps-fbar { border-top: 1px solid var(--mj-border-subtle); }\n.ps-fbar .track { height: 10px; border-radius: var(--mj-radius-full); background: var(--mj-bg-surface-sunken); overflow: hidden; }\n.ps-fbar .track > span { display: block; height: 100%; border-radius: var(--mj-radius-full); background: var(--mj-brand-primary); }\n.ps-fbar .track > span.warn { background: var(--mj-status-warning); }\n.ps-fbar .name { color: var(--mj-text-primary); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }\n.ps-fbar > .ps-mono { text-align: right; color: var(--mj-text-secondary); font-variant-numeric: tabular-nums; }\n\n/* gauge */\n.ps-gauge {\n position: relative; width: 64px; height: 64px; border-radius: 50%; flex: 0 0 auto;\n background: conic-gradient(var(--ps-gauge-color, var(--mj-brand-primary)) calc(var(--ps-p, 40) * 1%), var(--mj-bg-surface-sunken) 0);\n display: grid; place-items: center;\n}\n.ps-gauge::after { content: ''; position: absolute; inset: 9px; background: var(--mj-bg-surface); border-radius: 50%; }\n.ps-gauge .g-val { position: relative; font-weight: 720; font-size: 14px; color: var(--mj-text-primary); }\n\n/* warning / info callouts */\n.ps-callout { display: flex; gap: 11px; padding: 12px 14px; border-radius: 10px; border: 1px solid; }\n.ps-callout.warn { background: var(--mj-status-warning-bg); border-color: var(--mj-status-warning-border); color: var(--mj-status-warning-text); }\n.ps-callout.info { background: var(--mj-status-info-bg); border-color: var(--mj-status-info-border); color: var(--mj-status-info-text); }\n.ps-callout.success { background: var(--mj-status-success-bg); border-color: var(--mj-status-success-border); color: var(--mj-status-success-text); }\n.ps-callout i { font-size: 16px; margin-top: 1px; }\n\n/* lifecycle stepper */\n.ps-stepper { display: flex; align-items: center; gap: 6px; flex-wrap: wrap; }\n.ps-step { display: flex; align-items: center; gap: 6px; font-size: 12px; font-weight: 600; color: var(--mj-text-muted); }\n.ps-step .pip {\n width: 22px; height: 22px; border-radius: 50%; border: 2px solid var(--mj-border-strong);\n display: grid; place-items: center; font-size: 11px;\n}\n.ps-step.done { color: var(--mj-status-success-text); }\n.ps-step.done .pip { background: var(--mj-status-success); border-color: var(--mj-status-success); color: var(--mj-text-inverse); }\n.ps-step.curr { color: var(--mj-brand-primary); }\n.ps-step.curr .pip { border-color: var(--mj-brand-primary); color: var(--mj-brand-primary); }\n.ps-step .ln { width: 26px; height: 2px; background: var(--mj-border-strong); }\n\n/* ---- DAG flow canvas ---- */\n.ps-flow {\n position: relative;\n background:\n linear-gradient(var(--mj-border-subtle) 1px, transparent 1px) 0 0 / 22px 22px,\n linear-gradient(90deg, var(--mj-border-subtle) 1px, transparent 1px) 0 0 / 22px 22px,\n var(--mj-bg-surface-card);\n border: 1px solid var(--mj-border-default); border-radius: 10px;\n min-height: 620px; overflow: hidden;\n}\n.ps-node {\n position: absolute; width: 190px; background: var(--mj-bg-surface);\n border: 1px solid var(--mj-border-strong); border-left-width: 3px; border-radius: 8px;\n cursor: pointer;\n}\n.ps-node .nh {\n display: flex; align-items: center; gap: 8px; padding: 9px 11px;\n border-bottom: 1px solid var(--mj-border-subtle); font-weight: 650; font-size: 12.5px; color: var(--mj-text-primary);\n}\n.ps-node .nh i.tile {\n width: 24px; height: 24px; border-radius: 6px; display: grid; place-items: center;\n color: var(--mj-text-inverse); font-size: 12px; flex: 0 0 auto;\n}\n.ps-node .nh .ps-tag { margin-left: auto; }\n.ps-node .nb { padding: 9px 11px; font-size: 12px; color: var(--mj-text-secondary); }\n.ps-node .nb-row { display: flex; justify-content: space-between; gap: 8px; padding: 2px 0; }\n.ps-node .nb-row .k { color: var(--mj-text-muted); }\n.ps-node.src { border-left-color: var(--ps-node-source); }\n.ps-node.src .nh i.tile { background: var(--ps-node-source); }\n.ps-node.feat { border-left-color: var(--ps-node-feature); }\n.ps-node.feat .nh i.tile { background: var(--ps-node-feature); }\n.ps-node.emb { border-left-color: var(--ps-node-embedding); }\n.ps-node.emb .nh i.tile { background: var(--ps-node-embedding); }\n.ps-node.target { border-left-color: var(--ps-node-target); }\n.ps-node.target .nh i.tile { background: var(--ps-node-target); }\n.ps-node.algo { border-left-color: var(--ps-node-algorithm); }\n.ps-node.algo .nh i.tile { background: var(--ps-node-algorithm); }\n.ps-node.output { border-left-color: var(--mj-status-success); }\n.ps-node.output .nh i.tile { background: var(--mj-status-success); }\n.ps-node.selected {\n border-color: var(--mj-brand-primary);\n box-shadow: 0 0 0 3px color-mix(in srgb, var(--mj-brand-primary) 18%, transparent);\n}\n.ps-node .port {\n position: absolute; width: 11px; height: 11px; border-radius: 50%;\n background: var(--mj-bg-surface); border: 2px solid var(--mj-brand-primary);\n top: 50%; transform: translateY(-50%);\n}\n.ps-node .port.in { left: -6px; }\n.ps-node .port.out { right: -6px; }\nsvg.ps-edges { position: absolute; inset: 0; width: 100%; height: 100%; pointer-events: none; }\nsvg.ps-edges path { stroke: color-mix(in srgb, var(--mj-brand-primary) 45%, transparent); stroke-width: 2; fill: none; }\n\n/* palette chips */\n.ps-pchip {\n display: inline-flex; align-items: center; gap: 7px; padding: 6px 11px; border-radius: 8px;\n border: 1px dashed var(--mj-border-strong); background: var(--mj-bg-surface);\n font-size: 12.5px; font-weight: 600; color: var(--mj-text-secondary); cursor: grab;\n}\n.ps-pchip i { font-size: 12px; color: var(--mj-text-inverse); width: 20px; height: 20px; border-radius: 5px; display: grid; place-items: center; }\n.ps-pchip.s i { background: var(--ps-node-source); }\n.ps-pchip.f i { background: var(--ps-node-feature); }\n.ps-pchip.e i { background: var(--ps-node-embedding); }\n.ps-pchip.t i { background: var(--ps-node-target); }\n.ps-pchip.a i { background: var(--ps-node-algorithm); }\n\n/* segmented control */\n.ps-seg { display: inline-flex; border: 1px solid var(--mj-border-default); border-radius: 8px; overflow: hidden; }\n.ps-seg button {\n border: none; background: var(--mj-bg-surface); color: var(--mj-text-secondary);\n padding: 6px 12px; font-size: 12.5px; font-weight: 600; cursor: pointer;\n border-right: 1px solid var(--mj-border-default);\n}\n.ps-seg button:last-child { border-right: none; }\n.ps-seg button.on { background: var(--mj-brand-primary); color: var(--mj-text-inverse); }\n\n/* field control */\n.ps-field { margin-bottom: 14px; }\n.ps-field > label {\n display: block; font-size: 11px; text-transform: uppercase; letter-spacing: .04em;\n color: var(--mj-text-muted); font-weight: 700; margin-bottom: 6px;\n}\n.ps-input {\n width: 100%; padding: 8px 10px; border: 1px solid var(--mj-border-default);\n border-radius: 6px; font-size: 13px; background: var(--mj-bg-surface); color: var(--mj-text-primary);\n box-sizing: border-box;\n}\n.ps-input:focus { outline: none; border-color: var(--mj-brand-primary); box-shadow: 0 0 0 3px color-mix(in srgb, var(--mj-brand-primary) 12%, transparent); }\n\n/* loading container */\n.ps-loading { display: flex; align-items: center; justify-content: center; padding: 60px 0; }\n\n/* ---- empty / prompt states ---- */\n.ps-empty {\n display: flex; flex-direction: column; align-items: center; justify-content: center;\n text-align: center; gap: 10px; padding: 56px 24px; color: var(--mj-text-muted);\n}\n.ps-empty .ps-empty-ico {\n width: 60px; height: 60px; border-radius: 16px; display: grid; place-items: center;\n background: var(--mj-bg-surface-sunken); color: var(--mj-text-disabled); font-size: 24px;\n}\n.ps-empty h3 { margin: 4px 0 0; font-size: 16px; font-weight: 650; color: var(--mj-text-secondary); }\n.ps-empty p { margin: 0; max-width: 420px; font-size: 13px; line-height: 1.5; }\n\n/* ---- confirm modal ---- */\n.ps-modal-backdrop {\n position: fixed; inset: 0; z-index: 1000;\n background: var(--mj-bg-overlay);\n display: flex; align-items: center; justify-content: center; padding: 24px;\n}\n.ps-modal {\n width: 100%; max-width: 480px; background: var(--mj-bg-surface);\n border: 1px solid var(--mj-border-default); border-radius: 14px;\n box-shadow: var(--mj-shadow-lg, var(--mj-shadow-sm)); overflow: hidden;\n}\n.ps-modal-head {\n display: flex; align-items: center; gap: 12px; padding: 18px 20px;\n border-bottom: 1px solid var(--mj-border-subtle);\n}\n.ps-modal-head .ico {\n width: 38px; height: 38px; border-radius: 10px; display: grid; place-items: center;\n font-size: 16px; flex: 0 0 auto;\n background: var(--mj-status-info-bg); color: var(--mj-status-info-text);\n}\n.ps-modal-head.warn .ico { background: var(--mj-status-warning-bg); color: var(--mj-status-warning-text); }\n.ps-modal-head.danger .ico { background: var(--mj-status-error-bg); color: var(--mj-status-error-text); }\n.ps-modal-head h3 { margin: 0; font-size: 15.5px; font-weight: 650; color: var(--mj-text-primary); }\n.ps-modal-body { padding: 18px 20px; font-size: 13.5px; line-height: 1.55; color: var(--mj-text-secondary); }\n.ps-modal-body .ps-field { margin-top: 14px; margin-bottom: 0; }\n.ps-modal-foot {\n display: flex; align-items: center; gap: 10px; padding: 14px 20px;\n border-top: 1px solid var(--mj-border-subtle); background: var(--mj-bg-surface-card);\n}\n.ps-modal-foot .ps-spacer { flex: 1; }\n", "/* The panel fills its section; the list and detail columns each scroll on their own. */\n.ps-registry {\n display: flex;\n flex-direction: column;\n height: 100%;\n min-height: 0;\n width: 100%;\n overflow: hidden;\n}\n\n/* Master-Detail Resizable Splitter Layout */\n.ps-registry .reg-layout {\n display: flex;\n flex-direction: row;\n flex: 1;\n min-height: 0;\n height: 100%;\n overflow: hidden;\n position: relative;\n background: var(--mj-bg-page);\n}\n\n.ps-registry .reg-splitter {\n width: 100%;\n height: 100%;\n background: var(--mj-bg-page);\n}\n\n.ps-registry .reg-collapsed-strip {\n width: 44px;\n flex: 0 0 44px;\n background: var(--mj-bg-surface);\n border-right: 1px solid var(--mj-border-default);\n display: flex;\n flex-direction: column;\n align-items: center;\n gap: 12px;\n padding: 10px 0;\n z-index: 10;\n}\n\n.ps-registry .reg-rail-collapse {\n background: transparent;\n border: 1px solid var(--mj-border-default);\n color: var(--mj-text-secondary);\n border-radius: var(--mj-radius-sm);\n width: 28px;\n height: 28px;\n display: flex;\n align-items: center;\n justify-content: center;\n cursor: pointer;\n transition: all 0.15s ease;\n}\n\n.ps-registry .reg-rail-collapse:hover {\n background: var(--mj-bg-surface-hover);\n color: var(--mj-text-primary);\n border-color: var(--mj-brand-primary);\n}\n\n.ps-registry .reg-collapsed-strip-label {\n color: var(--mj-text-muted);\n font-size: 16px;\n margin-top: 8px;\n}\n\n.ps-registry .reg-collapse-btn {\n background: transparent;\n border: 1px solid transparent;\n color: var(--mj-text-muted);\n border-radius: var(--mj-radius-sm);\n width: 26px;\n height: 26px;\n display: flex;\n align-items: center;\n justify-content: center;\n cursor: pointer;\n transition: all 0.15s ease;\n}\n\n.ps-registry .reg-collapse-btn:hover {\n background: var(--mj-bg-surface-hover);\n color: var(--mj-text-primary);\n border-color: var(--mj-border-default);\n}\n\n/* model list \u2014 its own scroll column */\n.ps-registry .mlist {\n min-height: 0;\n overflow-y: auto;\n align-self: stretch;\n height: 100%;\n border-radius: 0;\n border: none;\n border-right: 1px solid var(--mj-border-default);\n}\n\n/* detail \u2014 its own scroll column */\n.ps-registry .detail {\n min-height: 0;\n height: 100%;\n overflow-y: auto;\n padding: 16px 20px;\n}\n\n.ps-registry .mrow {\n display: flex;\n align-items: center;\n gap: 10px;\n padding: 10px;\n border-radius: 8px;\n cursor: pointer;\n transition: background 0.12s;\n}\n.ps-registry .mrow:hover {\n background: var(--mj-bg-surface-hover);\n}\n.ps-registry .mrow.sel {\n background: color-mix(in srgb, var(--mj-brand-primary) 10%, var(--mj-bg-surface));\n box-shadow: inset 3px 0 0 var(--mj-brand-primary);\n}\n.ps-registry .mrow.arc {\n opacity: 0.72;\n}\n.ps-registry .mrow .ico {\n width: 30px;\n height: 30px;\n border-radius: 7px;\n display: grid;\n place-items: center;\n color: var(--mj-text-inverse);\n font-size: 13px;\n flex: 0 0 auto;\n}\n.ps-registry .mrow .ico.xgb {\n background: var(--ps-node-feature);\n}\n.ps-registry .mrow .ico.lgbm {\n background: var(--ps-node-algorithm);\n}\n.ps-registry .mrow .ico.rf {\n background: var(--ps-node-source);\n}\n.ps-registry .mrow .nm {\n font-weight: 600;\n color: var(--mj-text-primary);\n font-size: 13px;\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n}\n.ps-registry .mrow .auc {\n text-align: right;\n flex: 0 0 auto;\n}\n.ps-registry .mrow .auc .v {\n font-weight: 700;\n color: var(--mj-text-primary);\n font-variant-numeric: tabular-nums;\n}\n.ps-registry .mrow .auc .st {\n font-size: 10.5px;\n font-weight: 700;\n text-transform: uppercase;\n letter-spacing: 0.03em;\n}\n.ps-registry .mrow .auc .st.pub {\n color: var(--mj-status-success-text);\n}\n.ps-registry .mrow .auc .st.val {\n color: var(--mj-status-info-text);\n}\n.ps-registry .mrow .auc .st.dr {\n color: var(--mj-text-muted);\n}\n.ps-registry .mrow .auc .st.arc {\n color: var(--mj-text-disabled);\n}\n"], encapsulation: 2 });
650
277
  }
651
278
  (() => { (typeof ngDevMode === "undefined" || ngDevMode) && i0.ɵsetClassMetadata(PSRegistryComponent, [{
652
279
  type: Component,
653
- args: [{ standalone: true, selector: 'ps-registry', imports: [CommonModule, MJButtonDirective, PSConfirmModalComponent], encapsulation: ViewEncapsulation.None, template: `
280
+ args: [{ standalone: true, selector: 'ps-registry', imports: [CommonModule, AngularSplitModule, PSModelDetailComponent], encapsulation: ViewEncapsulation.None, template: `
654
281
  <div class="ps-panel ps-registry" data-testid="ps-registry-panel">
655
282
  @if (models.length === 0) {
656
283
  <div class="ps-empty" data-testid="ps-registry-empty">
@@ -659,174 +286,80 @@ export class PSRegistryComponent {
659
286
  <p>Train a pipeline to register your first immutable model. Each successful training run produces a versioned model you can validate, publish, and score with here.</p>
660
287
  </div>
661
288
  } @else {
662
- <div class="md-layout">
663
- <!-- master list -->
664
- <div class="ps-card mlist" data-testid="ps-registry-list">
665
- <div class="ps-card-head"><h3>ML Models</h3><span class="ps-badge gray">{{ models.length }}</span></div>
666
- <div class="ps-card-body" style="padding:8px">
667
- @for (m of models; track m.id) {
668
- <div class="mrow" data-testid="ps-registry-row" [class.sel]="m.id === selectedId" [class.arc]="m.status === 'Archived'" (click)="select(m.id)">
669
- <div class="ico" [class]="m.iconClass"><i class="fa-solid fa-cube"></i></div>
670
- <div style="flex:1;min-width:0">
671
- <div class="nm">{{ m.name }}</div>
672
- <div class="ln2 ps-muted ps-small">v{{ m.version }} · {{ m.algorithm }}</div>
673
- </div>
674
- <div class="auc">
675
- <div class="v">{{ m.holdoutAuc }}</div>
676
- <div class="st" [class]="statusClass(m.status)">{{ m.status }}</div>
677
- </div>
678
- </div>
679
- }
289
+ <div class="reg-layout" [class.list-collapsed]="isListCollapsed">
290
+ @if (isListCollapsed) {
291
+ <div class="reg-collapsed-strip" role="region" aria-label="Model registry list (collapsed)">
292
+ <button class="reg-rail-collapse" type="button" (click)="toggleList()" aria-label="Expand models list" title="Expand list">
293
+ <i class="fa-solid fa-chevron-right"></i>
294
+ </button>
295
+ <div class="reg-collapsed-strip-label"><i class="fa-solid fa-cubes"></i></div>
680
296
  </div>
681
- </div>
297
+ }
682
298
 
683
- <!-- detail -->
684
- <div class="ps-col detail" data-testid="ps-registry-detail">
685
- <div class="ps-card">
686
- <div class="ps-card-body dh">
687
- <div class="big-ico"><i class="fa-solid fa-cube"></i></div>
688
- <div style="flex:1">
689
- <h2 data-testid="ps-registry-detail-name">{{ selected.name }} <span class="ps-tag ps-mono">v{{ selected.version }}</span></h2>
690
- <div class="ps-muted ps-small sub">{{ selected.algorithm }}{{ targetSuffix }} · immutable snapshot</div>
691
- </div>
692
- <span class="ps-badge" [class]="statusBadgeClass(selected.status)">{{ selected.status }}</span>
693
- </div>
694
- </div>
695
-
696
- <!-- lifecycle -->
697
- <div class="ps-card">
698
- <div class="ps-card-body">
699
- <div class="ps-section-title">Lifecycle</div>
700
- <div class="ps-stepper">
701
- @for (step of lifecycleSteps; track step; let last = $last) {
702
- <div class="ps-step" [class.done]="stepState(step) === 'done'" [class.curr]="stepState(step) === 'curr'">
703
- <span class="pip">@if (stepState(step) === 'done') { <i class="fa-solid fa-check"></i> } @else { {{ $index + 1 }} }</span>
704
- {{ step }}
299
+ <as-split direction="horizontal" class="reg-splitter" unit="percent" [gutterSize]="6" (dragEnd)="onSplitDragEnd($event.sizes)">
300
+ @if (!isListCollapsed) {
301
+ <as-split-area [size]="listSizePct" [minSize]="18" [maxSize]="50">
302
+ <!-- master list -->
303
+ <div class="ps-card mlist" data-testid="ps-registry-list">
304
+ <div class="ps-card-head">
305
+ <h3>ML Models</h3>
306
+ <div style="display:flex;gap:6px;align-items:center">
307
+ <span class="ps-badge gray">{{ models.length }}</span>
308
+ <button class="reg-collapse-btn" type="button" (click)="toggleList()" title="Collapse list" aria-label="Collapse list">
309
+ <i class="fa-solid fa-chevron-left"></i>
310
+ </button>
705
311
  </div>
706
- @if (!last) { <span class="ln"></span> }
707
- }
708
- </div>
709
- <div class="ps-small ps-muted" style="margin-top:10px">
710
- Models are immutable once registered; promotion only changes lifecycle state, never weights.
711
- </div>
712
- </div>
713
- </div>
714
-
715
- <!-- performance -->
716
- <div class="ps-card">
717
- <div class="ps-card-head"><h3>Performance</h3><span class="ps-muted ps-small">Holdout = held-out test fold, never seen in training</span></div>
718
- <div class="ps-card-body">
719
- <div class="metric-pair">
720
- <div class="mtile"><div class="ps-section-title">Train AUC</div><div class="v">{{ trainAuc }}</div><div class="ps-muted ps-small">in-sample · optimistic</div></div>
721
- <div class="mtile honest"><div class="ps-section-title">Holdout AUC</div><div class="v">{{ selected.holdoutAuc }}</div><div class="ps-muted ps-small">out-of-sample · the honest number</div></div>
722
- </div>
723
- @if (secondaryMetrics.length > 0) {
724
- <div class="stat-bar">
725
- @for (s of secondaryMetrics; track s.key) {
726
- <div class="b"><span class="ps-muted ps-small">{{ s.label }}</span><strong>{{ s.value }}</strong></div>
727
- }
728
312
  </div>
729
- } @else {
730
- <div class="ps-small ps-muted" style="margin-top:6px">No secondary metrics recorded for this model.</div>
731
- }
732
- @if (gapText) {
733
- <div class="ps-small ps-muted" style="margin-top:10px">
734
- Train–holdout gap of <strong>{{ gapText }}</strong> {{ gapVerdict }}.
313
+ <div class="ps-card-body" style="padding:8px">
314
+ @for (m of models; track m.id) {
315
+ <div class="mrow" data-testid="ps-registry-row" [class.sel]="m.id === selectedId" [class.arc]="m.status === 'Archived'" (click)="select(m.id)">
316
+ <div class="ico" [class]="m.iconClass"><i class="fa-solid fa-cube"></i></div>
317
+ <div style="flex:1;min-width:0">
318
+ <div class="nm">{{ m.name }}</div>
319
+ <div class="ln2 ps-muted ps-small">v{{ m.version }} &bull; {{ m.algorithm }}</div>
320
+ </div>
321
+ <div class="auc">
322
+ <div class="v">{{ m.holdoutScore }}</div>
323
+ <div class="st" [class]="statusClass(m.status)">{{ m.status }}</div>
324
+ </div>
325
+ </div>
326
+ }
735
327
  </div>
736
- }
737
- </div>
738
- </div>
739
-
740
- <!-- feature importance -->
741
- <div class="ps-card">
742
- <div class="ps-card-head"><h3>Feature Importance</h3><span class="ps-muted ps-small">{{ importanceCaption }}</span></div>
743
- <div class="ps-card-body">
744
- @if (importance.length > 0) {
745
- @for (f of importance; track f.name) {
746
- <div class="ps-fbar">
747
- <span class="name ps-small" [title]="f.name">{{ f.name }}</span>
748
- <div class="track"><span [class.warn]="f.warning" [style.width.%]="f.pct"></span></div>
749
- <span class="ps-mono">{{ f.value }}</span>
750
- </div>
751
- }
752
- } @else {
753
- <div class="ps-small ps-muted">No feature-importance data was recorded for this model.</div>
754
- }
755
- </div>
756
- </div>
757
-
758
- <!-- leakage sign-off gate -->
759
- @if (importance.length > 0) {
760
- <div class="ps-callout gate" [class.success]="!leakageFlagged" [class.warn]="leakageFlagged">
761
- <i [class]="leakageFlagged ? 'fa-solid fa-triangle-exclamation' : 'fa-solid fa-shield-halved'"></i>
762
- <div>
763
- @if (leakageFlagged) {
764
- <strong>Leakage flag raised.</strong> A single feature dominates — top importance is
765
- <strong>{{ topFeatureName }} at {{ topFeatureValue }}</strong>, at/above the
766
- <strong>{{ dominanceThreshold }}</strong> dominance threshold.
767
- <div class="ps-small" style="margin-top:6px"><i class="fa-solid fa-pen"></i> Sign-off with a reason is required to publish this model.</div>
768
- } @else {
769
- <strong>Leakage gate clear.</strong> No single feature dominates — top importance is
770
- <strong>{{ topFeatureName }} at {{ topFeatureValue }}</strong>, below the
771
- <strong>{{ dominanceThreshold }}</strong> dominance threshold.
772
- <div class="ps-small" style="margin-top:6px"><i class="fa-solid fa-check"></i> Sign-off required before any Validated → Published promotion.</div>
773
- }
774
328
  </div>
775
- </div>
329
+ </as-split-area>
776
330
  }
777
331
 
778
- <!-- actions -->
779
- <div class="ps-card">
780
- <div class="ps-card-body ps-row" style="align-items:center">
781
- <span class="ps-muted ps-small" style="flex:1">{{ actionHint }}</span>
782
- @if (canValidate) {
783
- <button mjButton variant="secondary" size="sm" data-testid="ps-registry-validate" (click)="requestPromote('Validated')"><i class="fa-solid fa-circle-check"></i> Mark Validated</button>
784
- }
785
- @if (canPublish) {
786
- <button mjButton variant="primary" size="sm" data-testid="ps-registry-promote" (click)="requestPromote('Published')"><i class="fa-solid fa-arrow-up"></i> Promote to Published</button>
787
- }
788
- @if (canArchive) {
789
- <button mjButton variant="secondary" size="sm" data-testid="ps-registry-archive" (click)="requestPromote('Archived')"><i class="fa-solid fa-box-archive"></i> Archive</button>
332
+ <as-split-area [size]="isListCollapsed ? 100 : detailSizePct" [minSize]="50">
333
+ <!-- detail via unified PSModelDetailComponent -->
334
+ <div class="ps-col detail" data-testid="ps-registry-detail">
335
+ @if (selectedEntity) {
336
+ <ps-model-detail
337
+ [model]="selectedEntity"
338
+ [displayName]="engine ? engine.ModelDisplayName(selectedEntity) : undefined"
339
+ [provider]="provider"
340
+ [currentUser]="currentUser"
341
+ (statusChanged)="onModelStatusChanged($event)">
342
+ </ps-model-detail>
343
+ } @else {
344
+ <div class="ps-empty" style="padding: 40px 20px;">
345
+ <p class="ps-muted">Select a model to view details.</p>
346
+ </div>
790
347
  }
791
348
  </div>
792
- </div>
793
- </div>
349
+ </as-split-area>
350
+ </as-split>
794
351
  </div>
795
352
  }
796
-
797
- @if (pending) {
798
- <ps-confirm-modal
799
- [title]="pendingTitle"
800
- [icon]="pendingIcon"
801
- [confirmIcon]="pendingIcon"
802
- [confirmLabel]="pendingConfirmLabel"
803
- [variant]="pendingVariant"
804
- [showReason]="pending.leakageFlagged || pending.targetStatus === 'Archived'"
805
- [reasonRequired]="pending.leakageFlagged"
806
- [reasonLabel]="pending.leakageFlagged ? 'Leakage sign-off reason' : 'Reason (optional)'"
807
- [reasonPlaceholder]="pending.leakageFlagged ? 'Explain why this model is safe to publish despite the leakage flag…' : 'Add an optional note for the record…'"
808
- [busy]="busy"
809
- (confirmed)="confirmPromote($event)"
810
- (cancelled)="cancelPromote()">
811
- <div [innerHTML]="pendingMessage"></div>
812
- </ps-confirm-modal>
813
- }
814
353
  </div>
815
- `, styles: ["/* ============================================================================\n Predictive Studio \u2014 shared panel styles (design-token compliant)\n Maps the mockup design system onto MemberJunction semantic --mj-* tokens.\n Loaded by every PS panel component (styleUrls) under ViewEncapsulation.None,\n so the class names are global within the PS dashboard subtree.\n No hardcoded colors except the intentional categorical node/algorithm palette\n (declared once as --ps-* custom props below) which must stay distinct across\n light/dark to remain readable as diagram accents.\n ============================================================================ */\n\n/* Categorical accents for DAG node types + algorithm chips. These are\n data-visualization colors that must remain distinct regardless of theme\n (allowed exception #3 in the design-token rules). */\n.ps-panel {\n --ps-node-source: #6b7280;\n --ps-node-feature: #2050c8;\n --ps-node-embedding: #7b3ff0;\n --ps-node-target: #1f9d57;\n --ps-node-algorithm: #d98213;\n /* budget-gauge categorical accents (reuse the node palette so light/dark stay distinct) */\n --ps-gauge-cost: var(--ps-node-algorithm);\n --ps-gauge-iterations: var(--ps-node-feature);\n}\n\n.ps-panel {\n display: block;\n height: 100%;\n}\n\n/* ---- generic layout helpers ---- */\n.ps-row { display: flex; gap: 16px; }\n.ps-col { display: flex; flex-direction: column; gap: 16px; }\n.ps-wrap { flex-wrap: wrap; }\n.ps-spacer { flex: 1; }\n.ps-muted { color: var(--mj-text-muted); }\n.ps-small { font-size: 12.5px; }\n.ps-mono { font-family: ui-monospace, 'SF Mono', Menlo, Consolas, monospace; font-size: 12.5px; }\n.ps-section-title {\n font-size: 12px; text-transform: uppercase; letter-spacing: .06em;\n color: var(--mj-text-muted); margin: 4px 0 2px; font-weight: 700;\n}\n\n/* ---- cards ---- */\n.ps-card {\n background: var(--mj-bg-surface);\n border: 1px solid var(--mj-border-default);\n border-radius: 8px;\n overflow: hidden;\n}\n.ps-card-head {\n display: flex; align-items: center; gap: 10px;\n padding: 14px 16px; border-bottom: 1px solid var(--mj-border-subtle);\n}\n.ps-card-head h3 { font-size: 14.5px; margin: 0; font-weight: 650; color: var(--mj-text-primary); }\n.ps-card-body { padding: 16px; }\n\n/* ---- KPI tiles ---- */\n.ps-kpi {\n background: var(--mj-bg-surface); border: 1px solid var(--mj-border-default);\n border-radius: 8px; padding: 16px 18px;\n}\n.ps-kpi .label { font-size: 12px; color: var(--mj-text-muted); font-weight: 600; }\n.ps-kpi .val { font-size: 26px; font-weight: 720; letter-spacing: -.02em; margin-top: 2px; color: var(--mj-text-primary); }\n\n/* ---- badges ---- */\n.ps-badge {\n display: inline-flex; align-items: center; gap: 5px;\n padding: 2px 9px; border-radius: 999px; font-size: 11.5px; font-weight: 650; line-height: 1.7;\n}\n.ps-badge.gray { background: var(--mj-bg-surface-sunken); color: var(--mj-text-secondary); }\n.ps-badge.blue { background: var(--mj-status-info-bg); color: var(--mj-status-info-text); }\n.ps-badge.green { background: var(--mj-status-success-bg); color: var(--mj-status-success-text); }\n.ps-badge.amber { background: var(--mj-status-warning-bg); color: var(--mj-status-warning-text); }\n.ps-badge.red { background: var(--mj-status-error-bg); color: var(--mj-status-error-text); }\n.ps-dot { width: 7px; height: 7px; border-radius: 50%; display: inline-block; }\n\n/* recommendation-level chips (the ranking matrix payoff) */\n.ps-lvl {\n display: inline-flex; align-items: center; justify-content: center;\n min-width: 92px; padding: 4px 10px; border-radius: 7px;\n font-weight: 650; font-size: 11.5px; color: var(--mj-text-inverse);\n}\n.ps-lvl-Primary { background: var(--mj-status-success); }\n.ps-lvl-Strong { background: var(--mj-status-info); }\n.ps-lvl-Viable { background: var(--mj-status-warning); }\n.ps-lvl-Weak { background: var(--mj-text-disabled); }\n.ps-lvl-NotRecommended { background: var(--mj-bg-surface-sunken); color: var(--mj-text-muted); }\n\n/* ---- tags ---- */\n.ps-tag {\n display: inline-block; padding: 2px 8px; border-radius: 6px;\n background: var(--mj-bg-surface-sunken); color: var(--mj-text-secondary);\n font-size: 11.5px; font-weight: 600;\n}\n.ps-tag.muted { opacity: .6; }\n.ps-divider { height: 1px; background: var(--mj-border-subtle); margin: 8px 0; }\n\n/* ---- tables ---- */\n.ps-table { width: 100%; border-collapse: collapse; font-size: 13px; }\n.ps-table th {\n text-align: left; font-size: 11.5px; text-transform: uppercase; letter-spacing: .04em;\n color: var(--mj-text-muted); font-weight: 700; padding: 9px 12px;\n border-bottom: 1px solid var(--mj-border-default);\n}\n.ps-table td { padding: 11px 12px; border-bottom: 1px solid var(--mj-border-subtle); color: var(--mj-text-primary); }\n.ps-table tr:hover td { background: var(--mj-bg-surface-hover); }\n.ps-table .num { text-align: right; font-variant-numeric: tabular-nums; }\n\n.ps-rank {\n width: 22px; height: 22px; border-radius: 6px; background: var(--mj-bg-surface-sunken);\n display: inline-grid; place-items: center; font-weight: 700; font-size: 11.5px; color: var(--mj-text-secondary);\n}\n.ps-rank.gold { background: var(--mj-status-warning-bg); color: var(--mj-status-warning-text); }\n\n/* ---- progress bars ---- */\n.ps-bar { height: 8px; border-radius: 999px; background: var(--mj-bg-surface-sunken); overflow: hidden; }\n.ps-bar > span { display: block; height: 100%; border-radius: 999px; background: var(--mj-brand-primary); }\n.ps-bar.warn > span { background: var(--mj-status-warning); }\n.ps-bar.run > span { background: var(--mj-brand-primary); }\n\n.ps-fbar { display: grid; grid-template-columns: minmax(150px, 1.3fr) 2fr 56px; align-items: center; gap: 14px; padding: 8px 0; }\n.ps-fbar + .ps-fbar { border-top: 1px solid var(--mj-border-subtle); }\n.ps-fbar .track { height: 10px; border-radius: var(--mj-radius-full); background: var(--mj-bg-surface-sunken); overflow: hidden; }\n.ps-fbar .track > span { display: block; height: 100%; border-radius: var(--mj-radius-full); background: var(--mj-brand-primary); }\n.ps-fbar .track > span.warn { background: var(--mj-status-warning); }\n.ps-fbar .name { color: var(--mj-text-primary); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }\n.ps-fbar > .ps-mono { text-align: right; color: var(--mj-text-secondary); font-variant-numeric: tabular-nums; }\n\n/* gauge */\n.ps-gauge {\n position: relative; width: 64px; height: 64px; border-radius: 50%; flex: 0 0 auto;\n background: conic-gradient(var(--ps-gauge-color, var(--mj-brand-primary)) calc(var(--ps-p, 40) * 1%), var(--mj-bg-surface-sunken) 0);\n display: grid; place-items: center;\n}\n.ps-gauge::after { content: ''; position: absolute; inset: 9px; background: var(--mj-bg-surface); border-radius: 50%; }\n.ps-gauge .g-val { position: relative; font-weight: 720; font-size: 14px; color: var(--mj-text-primary); }\n\n/* warning / info callouts */\n.ps-callout { display: flex; gap: 11px; padding: 12px 14px; border-radius: 10px; border: 1px solid; }\n.ps-callout.warn { background: var(--mj-status-warning-bg); border-color: var(--mj-status-warning-border); color: var(--mj-status-warning-text); }\n.ps-callout.info { background: var(--mj-status-info-bg); border-color: var(--mj-status-info-border); color: var(--mj-status-info-text); }\n.ps-callout.success { background: var(--mj-status-success-bg); border-color: var(--mj-status-success-border); color: var(--mj-status-success-text); }\n.ps-callout i { font-size: 16px; margin-top: 1px; }\n\n/* lifecycle stepper */\n.ps-stepper { display: flex; align-items: center; gap: 6px; flex-wrap: wrap; }\n.ps-step { display: flex; align-items: center; gap: 6px; font-size: 12px; font-weight: 600; color: var(--mj-text-muted); }\n.ps-step .pip {\n width: 22px; height: 22px; border-radius: 50%; border: 2px solid var(--mj-border-strong);\n display: grid; place-items: center; font-size: 11px;\n}\n.ps-step.done { color: var(--mj-status-success-text); }\n.ps-step.done .pip { background: var(--mj-status-success); border-color: var(--mj-status-success); color: var(--mj-text-inverse); }\n.ps-step.curr { color: var(--mj-brand-primary); }\n.ps-step.curr .pip { border-color: var(--mj-brand-primary); color: var(--mj-brand-primary); }\n.ps-step .ln { width: 26px; height: 2px; background: var(--mj-border-strong); }\n\n/* ---- DAG flow canvas ---- */\n.ps-flow {\n position: relative;\n background:\n linear-gradient(var(--mj-border-subtle) 1px, transparent 1px) 0 0 / 22px 22px,\n linear-gradient(90deg, var(--mj-border-subtle) 1px, transparent 1px) 0 0 / 22px 22px,\n var(--mj-bg-surface-card);\n border: 1px solid var(--mj-border-default); border-radius: 10px;\n min-height: 620px; overflow: hidden;\n}\n.ps-node {\n position: absolute; width: 190px; background: var(--mj-bg-surface);\n border: 1px solid var(--mj-border-strong); border-left-width: 3px; border-radius: 8px;\n cursor: pointer;\n}\n.ps-node .nh {\n display: flex; align-items: center; gap: 8px; padding: 9px 11px;\n border-bottom: 1px solid var(--mj-border-subtle); font-weight: 650; font-size: 12.5px; color: var(--mj-text-primary);\n}\n.ps-node .nh i.tile {\n width: 24px; height: 24px; border-radius: 6px; display: grid; place-items: center;\n color: var(--mj-text-inverse); font-size: 12px; flex: 0 0 auto;\n}\n.ps-node .nh .ps-tag { margin-left: auto; }\n.ps-node .nb { padding: 9px 11px; font-size: 12px; color: var(--mj-text-secondary); }\n.ps-node .nb-row { display: flex; justify-content: space-between; gap: 8px; padding: 2px 0; }\n.ps-node .nb-row .k { color: var(--mj-text-muted); }\n.ps-node.src { border-left-color: var(--ps-node-source); }\n.ps-node.src .nh i.tile { background: var(--ps-node-source); }\n.ps-node.feat { border-left-color: var(--ps-node-feature); }\n.ps-node.feat .nh i.tile { background: var(--ps-node-feature); }\n.ps-node.emb { border-left-color: var(--ps-node-embedding); }\n.ps-node.emb .nh i.tile { background: var(--ps-node-embedding); }\n.ps-node.target { border-left-color: var(--ps-node-target); }\n.ps-node.target .nh i.tile { background: var(--ps-node-target); }\n.ps-node.algo { border-left-color: var(--ps-node-algorithm); }\n.ps-node.algo .nh i.tile { background: var(--ps-node-algorithm); }\n.ps-node.output { border-left-color: var(--mj-status-success); }\n.ps-node.output .nh i.tile { background: var(--mj-status-success); }\n.ps-node.selected {\n border-color: var(--mj-brand-primary);\n box-shadow: 0 0 0 3px color-mix(in srgb, var(--mj-brand-primary) 18%, transparent);\n}\n.ps-node .port {\n position: absolute; width: 11px; height: 11px; border-radius: 50%;\n background: var(--mj-bg-surface); border: 2px solid var(--mj-brand-primary);\n top: 50%; transform: translateY(-50%);\n}\n.ps-node .port.in { left: -6px; }\n.ps-node .port.out { right: -6px; }\nsvg.ps-edges { position: absolute; inset: 0; width: 100%; height: 100%; pointer-events: none; }\nsvg.ps-edges path { stroke: color-mix(in srgb, var(--mj-brand-primary) 45%, transparent); stroke-width: 2; fill: none; }\n\n/* palette chips */\n.ps-pchip {\n display: inline-flex; align-items: center; gap: 7px; padding: 6px 11px; border-radius: 8px;\n border: 1px dashed var(--mj-border-strong); background: var(--mj-bg-surface);\n font-size: 12.5px; font-weight: 600; color: var(--mj-text-secondary); cursor: grab;\n}\n.ps-pchip i { font-size: 12px; color: var(--mj-text-inverse); width: 20px; height: 20px; border-radius: 5px; display: grid; place-items: center; }\n.ps-pchip.s i { background: var(--ps-node-source); }\n.ps-pchip.f i { background: var(--ps-node-feature); }\n.ps-pchip.e i { background: var(--ps-node-embedding); }\n.ps-pchip.t i { background: var(--ps-node-target); }\n.ps-pchip.a i { background: var(--ps-node-algorithm); }\n\n/* segmented control */\n.ps-seg { display: inline-flex; border: 1px solid var(--mj-border-default); border-radius: 8px; overflow: hidden; }\n.ps-seg button {\n border: none; background: var(--mj-bg-surface); color: var(--mj-text-secondary);\n padding: 6px 12px; font-size: 12.5px; font-weight: 600; cursor: pointer;\n border-right: 1px solid var(--mj-border-default);\n}\n.ps-seg button:last-child { border-right: none; }\n.ps-seg button.on { background: var(--mj-brand-primary); color: var(--mj-text-inverse); }\n\n/* field control */\n.ps-field { margin-bottom: 14px; }\n.ps-field > label {\n display: block; font-size: 11px; text-transform: uppercase; letter-spacing: .04em;\n color: var(--mj-text-muted); font-weight: 700; margin-bottom: 6px;\n}\n.ps-input {\n width: 100%; padding: 8px 10px; border: 1px solid var(--mj-border-default);\n border-radius: 6px; font-size: 13px; background: var(--mj-bg-surface); color: var(--mj-text-primary);\n box-sizing: border-box;\n}\n.ps-input:focus { outline: none; border-color: var(--mj-brand-primary); box-shadow: 0 0 0 3px color-mix(in srgb, var(--mj-brand-primary) 12%, transparent); }\n\n/* loading container */\n.ps-loading { display: flex; align-items: center; justify-content: center; padding: 60px 0; }\n\n/* ---- empty / prompt states ---- */\n.ps-empty {\n display: flex; flex-direction: column; align-items: center; justify-content: center;\n text-align: center; gap: 10px; padding: 56px 24px; color: var(--mj-text-muted);\n}\n.ps-empty .ps-empty-ico {\n width: 60px; height: 60px; border-radius: 16px; display: grid; place-items: center;\n background: var(--mj-bg-surface-sunken); color: var(--mj-text-disabled); font-size: 24px;\n}\n.ps-empty h3 { margin: 4px 0 0; font-size: 16px; font-weight: 650; color: var(--mj-text-secondary); }\n.ps-empty p { margin: 0; max-width: 420px; font-size: 13px; line-height: 1.5; }\n\n/* ---- confirm modal ---- */\n.ps-modal-backdrop {\n position: fixed; inset: 0; z-index: 1000;\n background: var(--mj-bg-overlay);\n display: flex; align-items: center; justify-content: center; padding: 24px;\n}\n.ps-modal {\n width: 100%; max-width: 480px; background: var(--mj-bg-surface);\n border: 1px solid var(--mj-border-default); border-radius: 14px;\n box-shadow: var(--mj-shadow-lg, var(--mj-shadow-sm)); overflow: hidden;\n}\n.ps-modal-head {\n display: flex; align-items: center; gap: 12px; padding: 18px 20px;\n border-bottom: 1px solid var(--mj-border-subtle);\n}\n.ps-modal-head .ico {\n width: 38px; height: 38px; border-radius: 10px; display: grid; place-items: center;\n font-size: 16px; flex: 0 0 auto;\n background: var(--mj-status-info-bg); color: var(--mj-status-info-text);\n}\n.ps-modal-head.warn .ico { background: var(--mj-status-warning-bg); color: var(--mj-status-warning-text); }\n.ps-modal-head.danger .ico { background: var(--mj-status-error-bg); color: var(--mj-status-error-text); }\n.ps-modal-head h3 { margin: 0; font-size: 15.5px; font-weight: 650; color: var(--mj-text-primary); }\n.ps-modal-body { padding: 18px 20px; font-size: 13.5px; line-height: 1.55; color: var(--mj-text-secondary); }\n.ps-modal-body .ps-field { margin-top: 14px; margin-bottom: 0; }\n.ps-modal-foot {\n display: flex; align-items: center; gap: 10px; padding: 14px 20px;\n border-top: 1px solid var(--mj-border-subtle); background: var(--mj-bg-surface-card);\n}\n.ps-modal-foot .ps-spacer { flex: 1; }\n", "/* The panel fills its section; the list and detail columns each scroll on their own. */\n.ps-registry { display: flex; flex-direction: column; height: 100%; min-height: 0; }\n.ps-registry .md-layout {\n flex: 1; min-height: 0;\n display: grid; grid-template-columns: 300px 1fr; grid-template-rows: minmax(0, 1fr); gap: 18px;\n}\n@media (max-width: 1080px) {\n .ps-registry .md-layout { grid-template-columns: 1fr; grid-template-rows: minmax(0, 1fr) minmax(0, 1.4fr); }\n}\n\n/* model list \u2014 its own scroll column */\n.ps-registry .mlist { min-height: 0; overflow-y: auto; align-self: start; max-height: 100%; }\n\n/* detail \u2014 its own scroll column, independent of the list. NOTE: .detail is also a\n .ps-col (display:flex column), so its card children ARE flex items \u2014 without\n flex-shrink:0 they compress to fit (scrollHeight == clientHeight, verified live) and\n the column never scrolls. The child rule is load-bearing, not decorative. */\n.ps-registry .detail { min-height: 0; overflow-y: auto; padding-right: 2px; }\n.ps-registry .detail > * { flex: 0 0 auto; }\n.ps-registry .mrow {\n display: flex; align-items: center; gap: 10px; padding: 10px; border-radius: 8px; cursor: pointer; transition: background .12s;\n}\n.ps-registry .mrow:hover { background: var(--mj-bg-surface-hover); }\n.ps-registry .mrow.sel { background: color-mix(in srgb, var(--mj-brand-primary) 10%, var(--mj-bg-surface)); box-shadow: inset 3px 0 0 var(--mj-brand-primary); }\n.ps-registry .mrow.arc { opacity: .72; }\n.ps-registry .mrow .ico {\n width: 30px; height: 30px; border-radius: 7px; display: grid; place-items: center; color: var(--mj-text-inverse); font-size: 13px; flex: 0 0 auto;\n}\n.ps-registry .mrow .ico.xgb { background: var(--ps-node-feature); }\n.ps-registry .mrow .ico.lgbm { background: var(--ps-node-algorithm); }\n.ps-registry .mrow .ico.rf { background: var(--ps-node-source); }\n.ps-registry .mrow .nm { font-weight: 600; color: var(--mj-text-primary); font-size: 13px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }\n.ps-registry .mrow .auc { text-align: right; flex: 0 0 auto; }\n.ps-registry .mrow .auc .v { font-weight: 700; color: var(--mj-text-primary); font-variant-numeric: tabular-nums; }\n.ps-registry .mrow .auc .st { font-size: 10.5px; font-weight: 700; text-transform: uppercase; letter-spacing: .03em; }\n.ps-registry .mrow .auc .st.pub { color: var(--mj-status-success-text); }\n.ps-registry .mrow .auc .st.val { color: var(--mj-status-info-text); }\n.ps-registry .mrow .auc .st.dr { color: var(--mj-text-muted); }\n.ps-registry .mrow .auc .st.arc { color: var(--mj-text-disabled); }\n\n/* detail */\n.ps-registry .dh { display: flex; align-items: center; gap: 14px; }\n.ps-registry .big-ico {\n width: 48px; height: 48px; border-radius: 12px; display: grid; place-items: center;\n background: var(--ps-node-feature); color: var(--mj-text-inverse); font-size: 20px; flex: 0 0 auto;\n}\n.ps-registry .dh h2 { font-size: 18px; color: var(--mj-text-primary); display: flex; align-items: center; gap: 8px; }\n.ps-registry .dh .sub { margin-top: 2px; }\n\n.ps-registry .metric-pair { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; }\n.ps-registry .mtile { border: 1px solid var(--mj-border-default); border-radius: 9px; padding: 14px; }\n.ps-registry .mtile .v { font-size: 28px; font-weight: 720; color: var(--mj-text-primary); letter-spacing: -.02em; }\n.ps-registry .mtile.honest { border-color: var(--mj-brand-primary); background: color-mix(in srgb, var(--mj-brand-primary) 6%, var(--mj-bg-surface)); }\n.ps-registry .mtile.honest .v { color: var(--mj-brand-primary); }\n.ps-registry .stat-bar { display: flex; gap: 10px; margin-top: 14px; flex-wrap: wrap; }\n.ps-registry .stat-bar .b { display: flex; flex-direction: column; gap: 2px; padding: 8px 12px; border: 1px solid var(--mj-border-subtle); border-radius: 8px; flex: 1; min-width: 90px; }\n.ps-registry .stat-bar .b strong { color: var(--mj-text-primary); font-size: 15px; }\n\n.ps-registry .gate { align-items: flex-start; }\n.ps-registry .gate i { font-size: 20px; }\n\n.ps-registry .lineage { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }\n.ps-registry .ln-card { border: 1px solid var(--mj-border-default); border-radius: 9px; padding: 10px 12px; min-width: 120px; }\n.ps-registry .ln-card.cur { border-color: var(--mj-brand-primary); background: color-mix(in srgb, var(--mj-brand-primary) 6%, var(--mj-bg-surface)); }\n.ps-registry .ln-card .t { font-size: 10.5px; text-transform: uppercase; letter-spacing: .04em; color: var(--mj-text-muted); font-weight: 700; }\n.ps-registry .ln-card .n { font-weight: 650; color: var(--mj-text-primary); font-size: 13px; margin: 2px 0; }\n.ps-registry .ln-arrow { color: var(--mj-text-disabled); }\n"] }]
354
+ `, styles: ["/* ============================================================================\n Predictive Studio \u2014 shared panel styles (design-token compliant)\n Maps the mockup design system onto MemberJunction semantic --mj-* tokens.\n Loaded by every PS panel component (styleUrls) under ViewEncapsulation.None,\n so the class names are global within the PS dashboard subtree.\n No hardcoded colors except the intentional categorical node/algorithm palette\n (declared once as --ps-* custom props below) which must stay distinct across\n light/dark to remain readable as diagram accents.\n ============================================================================ */\n\n/* Categorical accents for DAG node types + algorithm chips. These are\n data-visualization colors that must remain distinct regardless of theme\n (allowed exception #3 in the design-token rules). */\n.ps-panel {\n --ps-node-source: #6b7280;\n --ps-node-feature: #2050c8;\n --ps-node-embedding: #7b3ff0;\n --ps-node-target: #1f9d57;\n --ps-node-algorithm: #d98213;\n /* budget-gauge categorical accents (reuse the node palette so light/dark stay distinct) */\n --ps-gauge-cost: var(--ps-node-algorithm);\n --ps-gauge-iterations: var(--ps-node-feature);\n}\n\n.ps-panel {\n display: block;\n height: 100%;\n}\n\n/* ---- generic layout helpers ---- */\n.ps-row { display: flex; gap: 16px; }\n.ps-col { display: flex; flex-direction: column; gap: 16px; }\n.ps-wrap { flex-wrap: wrap; }\n.ps-spacer { flex: 1; }\n.ps-muted { color: var(--mj-text-muted); }\n.ps-small { font-size: 12.5px; }\n.ps-mono { font-family: ui-monospace, 'SF Mono', Menlo, Consolas, monospace; font-size: 12.5px; }\n.ps-section-title {\n font-size: 12px; text-transform: uppercase; letter-spacing: .06em;\n color: var(--mj-text-muted); margin: 4px 0 2px; font-weight: 700;\n}\n\n/* ---- cards ---- */\n.ps-card {\n background: var(--mj-bg-surface);\n border: 1px solid var(--mj-border-default);\n border-radius: 8px;\n overflow: hidden;\n}\n.ps-card-head {\n display: flex; align-items: center; gap: 10px;\n padding: 14px 16px; border-bottom: 1px solid var(--mj-border-subtle);\n}\n.ps-card-head h3 { font-size: 14.5px; margin: 0; font-weight: 650; color: var(--mj-text-primary); }\n.ps-card-body { padding: 16px; }\n\n/* ---- KPI tiles ---- */\n.ps-kpi {\n background: var(--mj-bg-surface); border: 1px solid var(--mj-border-default);\n border-radius: 8px; padding: 16px 18px;\n}\n.ps-kpi .label { font-size: 12px; color: var(--mj-text-muted); font-weight: 600; }\n.ps-kpi .val { font-size: 26px; font-weight: 720; letter-spacing: -.02em; margin-top: 2px; color: var(--mj-text-primary); }\n\n/* ---- badges ---- */\n.ps-badge {\n display: inline-flex; align-items: center; gap: 5px;\n padding: 2px 9px; border-radius: 999px; font-size: 11.5px; font-weight: 650; line-height: 1.7;\n}\n.ps-badge.gray { background: var(--mj-bg-surface-sunken); color: var(--mj-text-secondary); }\n.ps-badge.blue { background: var(--mj-status-info-bg); color: var(--mj-status-info-text); }\n.ps-badge.green { background: var(--mj-status-success-bg); color: var(--mj-status-success-text); }\n.ps-badge.amber { background: var(--mj-status-warning-bg); color: var(--mj-status-warning-text); }\n.ps-badge.red { background: var(--mj-status-error-bg); color: var(--mj-status-error-text); }\n.ps-dot { width: 7px; height: 7px; border-radius: 50%; display: inline-block; }\n\n/* recommendation-level chips (the ranking matrix payoff) */\n.ps-lvl {\n display: inline-flex; align-items: center; justify-content: center;\n min-width: 92px; padding: 4px 10px; border-radius: 7px;\n font-weight: 650; font-size: 11.5px; color: var(--mj-text-inverse);\n}\n.ps-lvl-Primary { background: var(--mj-status-success); }\n.ps-lvl-Strong { background: var(--mj-status-info); }\n.ps-lvl-Viable { background: var(--mj-status-warning); }\n.ps-lvl-Weak { background: var(--mj-text-disabled); }\n.ps-lvl-NotRecommended { background: var(--mj-bg-surface-sunken); color: var(--mj-text-muted); }\n\n/* ---- tags ---- */\n.ps-tag {\n display: inline-block; padding: 2px 8px; border-radius: 6px;\n background: var(--mj-bg-surface-sunken); color: var(--mj-text-secondary);\n font-size: 11.5px; font-weight: 600;\n}\n.ps-tag.muted { opacity: .6; }\n.ps-divider { height: 1px; background: var(--mj-border-subtle); margin: 8px 0; }\n\n/* ---- tables ---- */\n.ps-table { width: 100%; border-collapse: collapse; font-size: 13px; }\n.ps-table th {\n text-align: left; font-size: 11.5px; text-transform: uppercase; letter-spacing: .04em;\n color: var(--mj-text-muted); font-weight: 700; padding: 9px 12px;\n border-bottom: 1px solid var(--mj-border-default);\n}\n.ps-table td { padding: 11px 12px; border-bottom: 1px solid var(--mj-border-subtle); color: var(--mj-text-primary); }\n.ps-table tr:hover td { background: var(--mj-bg-surface-hover); }\n.ps-table .num { text-align: right; font-variant-numeric: tabular-nums; }\n\n.ps-rank {\n width: 22px; height: 22px; border-radius: 6px; background: var(--mj-bg-surface-sunken);\n display: inline-grid; place-items: center; font-weight: 700; font-size: 11.5px; color: var(--mj-text-secondary);\n}\n.ps-rank.gold { background: var(--mj-status-warning-bg); color: var(--mj-status-warning-text); }\n\n/* ---- progress bars ---- */\n.ps-bar { height: 8px; border-radius: 999px; background: var(--mj-bg-surface-sunken); overflow: hidden; }\n.ps-bar > span { display: block; height: 100%; border-radius: 999px; background: var(--mj-brand-primary); }\n.ps-bar.warn > span { background: var(--mj-status-warning); }\n.ps-bar.run > span { background: var(--mj-brand-primary); }\n\n.ps-fbar { display: grid; grid-template-columns: minmax(150px, 1.3fr) 2fr 56px; align-items: center; gap: 14px; padding: 8px 0; }\n.ps-fbar + .ps-fbar { border-top: 1px solid var(--mj-border-subtle); }\n.ps-fbar .track { height: 10px; border-radius: var(--mj-radius-full); background: var(--mj-bg-surface-sunken); overflow: hidden; }\n.ps-fbar .track > span { display: block; height: 100%; border-radius: var(--mj-radius-full); background: var(--mj-brand-primary); }\n.ps-fbar .track > span.warn { background: var(--mj-status-warning); }\n.ps-fbar .name { color: var(--mj-text-primary); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }\n.ps-fbar > .ps-mono { text-align: right; color: var(--mj-text-secondary); font-variant-numeric: tabular-nums; }\n\n/* gauge */\n.ps-gauge {\n position: relative; width: 64px; height: 64px; border-radius: 50%; flex: 0 0 auto;\n background: conic-gradient(var(--ps-gauge-color, var(--mj-brand-primary)) calc(var(--ps-p, 40) * 1%), var(--mj-bg-surface-sunken) 0);\n display: grid; place-items: center;\n}\n.ps-gauge::after { content: ''; position: absolute; inset: 9px; background: var(--mj-bg-surface); border-radius: 50%; }\n.ps-gauge .g-val { position: relative; font-weight: 720; font-size: 14px; color: var(--mj-text-primary); }\n\n/* warning / info callouts */\n.ps-callout { display: flex; gap: 11px; padding: 12px 14px; border-radius: 10px; border: 1px solid; }\n.ps-callout.warn { background: var(--mj-status-warning-bg); border-color: var(--mj-status-warning-border); color: var(--mj-status-warning-text); }\n.ps-callout.info { background: var(--mj-status-info-bg); border-color: var(--mj-status-info-border); color: var(--mj-status-info-text); }\n.ps-callout.success { background: var(--mj-status-success-bg); border-color: var(--mj-status-success-border); color: var(--mj-status-success-text); }\n.ps-callout i { font-size: 16px; margin-top: 1px; }\n\n/* lifecycle stepper */\n.ps-stepper { display: flex; align-items: center; gap: 6px; flex-wrap: wrap; }\n.ps-step { display: flex; align-items: center; gap: 6px; font-size: 12px; font-weight: 600; color: var(--mj-text-muted); }\n.ps-step .pip {\n width: 22px; height: 22px; border-radius: 50%; border: 2px solid var(--mj-border-strong);\n display: grid; place-items: center; font-size: 11px;\n}\n.ps-step.done { color: var(--mj-status-success-text); }\n.ps-step.done .pip { background: var(--mj-status-success); border-color: var(--mj-status-success); color: var(--mj-text-inverse); }\n.ps-step.curr { color: var(--mj-brand-primary); }\n.ps-step.curr .pip { border-color: var(--mj-brand-primary); color: var(--mj-brand-primary); }\n.ps-step .ln { width: 26px; height: 2px; background: var(--mj-border-strong); }\n\n/* ---- DAG flow canvas ---- */\n.ps-flow {\n position: relative;\n background:\n linear-gradient(var(--mj-border-subtle) 1px, transparent 1px) 0 0 / 22px 22px,\n linear-gradient(90deg, var(--mj-border-subtle) 1px, transparent 1px) 0 0 / 22px 22px,\n var(--mj-bg-surface-card);\n border: 1px solid var(--mj-border-default); border-radius: 10px;\n min-height: 620px; overflow: hidden;\n}\n.ps-node {\n position: absolute; width: 190px; background: var(--mj-bg-surface);\n border: 1px solid var(--mj-border-strong); border-left-width: 3px; border-radius: 8px;\n cursor: pointer;\n}\n.ps-node .nh {\n display: flex; align-items: center; gap: 8px; padding: 9px 11px;\n border-bottom: 1px solid var(--mj-border-subtle); font-weight: 650; font-size: 12.5px; color: var(--mj-text-primary);\n}\n.ps-node .nh i.tile {\n width: 24px; height: 24px; border-radius: 6px; display: grid; place-items: center;\n color: var(--mj-text-inverse); font-size: 12px; flex: 0 0 auto;\n}\n.ps-node .nh .ps-tag { margin-left: auto; }\n.ps-node .nb { padding: 9px 11px; font-size: 12px; color: var(--mj-text-secondary); }\n.ps-node .nb-row { display: flex; justify-content: space-between; gap: 8px; padding: 2px 0; }\n.ps-node .nb-row .k { color: var(--mj-text-muted); }\n.ps-node.src { border-left-color: var(--ps-node-source); }\n.ps-node.src .nh i.tile { background: var(--ps-node-source); }\n.ps-node.feat { border-left-color: var(--ps-node-feature); }\n.ps-node.feat .nh i.tile { background: var(--ps-node-feature); }\n.ps-node.emb { border-left-color: var(--ps-node-embedding); }\n.ps-node.emb .nh i.tile { background: var(--ps-node-embedding); }\n.ps-node.target { border-left-color: var(--ps-node-target); }\n.ps-node.target .nh i.tile { background: var(--ps-node-target); }\n.ps-node.algo { border-left-color: var(--ps-node-algorithm); }\n.ps-node.algo .nh i.tile { background: var(--ps-node-algorithm); }\n.ps-node.output { border-left-color: var(--mj-status-success); }\n.ps-node.output .nh i.tile { background: var(--mj-status-success); }\n.ps-node.selected {\n border-color: var(--mj-brand-primary);\n box-shadow: 0 0 0 3px color-mix(in srgb, var(--mj-brand-primary) 18%, transparent);\n}\n.ps-node .port {\n position: absolute; width: 11px; height: 11px; border-radius: 50%;\n background: var(--mj-bg-surface); border: 2px solid var(--mj-brand-primary);\n top: 50%; transform: translateY(-50%);\n}\n.ps-node .port.in { left: -6px; }\n.ps-node .port.out { right: -6px; }\nsvg.ps-edges { position: absolute; inset: 0; width: 100%; height: 100%; pointer-events: none; }\nsvg.ps-edges path { stroke: color-mix(in srgb, var(--mj-brand-primary) 45%, transparent); stroke-width: 2; fill: none; }\n\n/* palette chips */\n.ps-pchip {\n display: inline-flex; align-items: center; gap: 7px; padding: 6px 11px; border-radius: 8px;\n border: 1px dashed var(--mj-border-strong); background: var(--mj-bg-surface);\n font-size: 12.5px; font-weight: 600; color: var(--mj-text-secondary); cursor: grab;\n}\n.ps-pchip i { font-size: 12px; color: var(--mj-text-inverse); width: 20px; height: 20px; border-radius: 5px; display: grid; place-items: center; }\n.ps-pchip.s i { background: var(--ps-node-source); }\n.ps-pchip.f i { background: var(--ps-node-feature); }\n.ps-pchip.e i { background: var(--ps-node-embedding); }\n.ps-pchip.t i { background: var(--ps-node-target); }\n.ps-pchip.a i { background: var(--ps-node-algorithm); }\n\n/* segmented control */\n.ps-seg { display: inline-flex; border: 1px solid var(--mj-border-default); border-radius: 8px; overflow: hidden; }\n.ps-seg button {\n border: none; background: var(--mj-bg-surface); color: var(--mj-text-secondary);\n padding: 6px 12px; font-size: 12.5px; font-weight: 600; cursor: pointer;\n border-right: 1px solid var(--mj-border-default);\n}\n.ps-seg button:last-child { border-right: none; }\n.ps-seg button.on { background: var(--mj-brand-primary); color: var(--mj-text-inverse); }\n\n/* field control */\n.ps-field { margin-bottom: 14px; }\n.ps-field > label {\n display: block; font-size: 11px; text-transform: uppercase; letter-spacing: .04em;\n color: var(--mj-text-muted); font-weight: 700; margin-bottom: 6px;\n}\n.ps-input {\n width: 100%; padding: 8px 10px; border: 1px solid var(--mj-border-default);\n border-radius: 6px; font-size: 13px; background: var(--mj-bg-surface); color: var(--mj-text-primary);\n box-sizing: border-box;\n}\n.ps-input:focus { outline: none; border-color: var(--mj-brand-primary); box-shadow: 0 0 0 3px color-mix(in srgb, var(--mj-brand-primary) 12%, transparent); }\n\n/* loading container */\n.ps-loading { display: flex; align-items: center; justify-content: center; padding: 60px 0; }\n\n/* ---- empty / prompt states ---- */\n.ps-empty {\n display: flex; flex-direction: column; align-items: center; justify-content: center;\n text-align: center; gap: 10px; padding: 56px 24px; color: var(--mj-text-muted);\n}\n.ps-empty .ps-empty-ico {\n width: 60px; height: 60px; border-radius: 16px; display: grid; place-items: center;\n background: var(--mj-bg-surface-sunken); color: var(--mj-text-disabled); font-size: 24px;\n}\n.ps-empty h3 { margin: 4px 0 0; font-size: 16px; font-weight: 650; color: var(--mj-text-secondary); }\n.ps-empty p { margin: 0; max-width: 420px; font-size: 13px; line-height: 1.5; }\n\n/* ---- confirm modal ---- */\n.ps-modal-backdrop {\n position: fixed; inset: 0; z-index: 1000;\n background: var(--mj-bg-overlay);\n display: flex; align-items: center; justify-content: center; padding: 24px;\n}\n.ps-modal {\n width: 100%; max-width: 480px; background: var(--mj-bg-surface);\n border: 1px solid var(--mj-border-default); border-radius: 14px;\n box-shadow: var(--mj-shadow-lg, var(--mj-shadow-sm)); overflow: hidden;\n}\n.ps-modal-head {\n display: flex; align-items: center; gap: 12px; padding: 18px 20px;\n border-bottom: 1px solid var(--mj-border-subtle);\n}\n.ps-modal-head .ico {\n width: 38px; height: 38px; border-radius: 10px; display: grid; place-items: center;\n font-size: 16px; flex: 0 0 auto;\n background: var(--mj-status-info-bg); color: var(--mj-status-info-text);\n}\n.ps-modal-head.warn .ico { background: var(--mj-status-warning-bg); color: var(--mj-status-warning-text); }\n.ps-modal-head.danger .ico { background: var(--mj-status-error-bg); color: var(--mj-status-error-text); }\n.ps-modal-head h3 { margin: 0; font-size: 15.5px; font-weight: 650; color: var(--mj-text-primary); }\n.ps-modal-body { padding: 18px 20px; font-size: 13.5px; line-height: 1.55; color: var(--mj-text-secondary); }\n.ps-modal-body .ps-field { margin-top: 14px; margin-bottom: 0; }\n.ps-modal-foot {\n display: flex; align-items: center; gap: 10px; padding: 14px 20px;\n border-top: 1px solid var(--mj-border-subtle); background: var(--mj-bg-surface-card);\n}\n.ps-modal-foot .ps-spacer { flex: 1; }\n", "/* The panel fills its section; the list and detail columns each scroll on their own. */\n.ps-registry {\n display: flex;\n flex-direction: column;\n height: 100%;\n min-height: 0;\n width: 100%;\n overflow: hidden;\n}\n\n/* Master-Detail Resizable Splitter Layout */\n.ps-registry .reg-layout {\n display: flex;\n flex-direction: row;\n flex: 1;\n min-height: 0;\n height: 100%;\n overflow: hidden;\n position: relative;\n background: var(--mj-bg-page);\n}\n\n.ps-registry .reg-splitter {\n width: 100%;\n height: 100%;\n background: var(--mj-bg-page);\n}\n\n.ps-registry .reg-collapsed-strip {\n width: 44px;\n flex: 0 0 44px;\n background: var(--mj-bg-surface);\n border-right: 1px solid var(--mj-border-default);\n display: flex;\n flex-direction: column;\n align-items: center;\n gap: 12px;\n padding: 10px 0;\n z-index: 10;\n}\n\n.ps-registry .reg-rail-collapse {\n background: transparent;\n border: 1px solid var(--mj-border-default);\n color: var(--mj-text-secondary);\n border-radius: var(--mj-radius-sm);\n width: 28px;\n height: 28px;\n display: flex;\n align-items: center;\n justify-content: center;\n cursor: pointer;\n transition: all 0.15s ease;\n}\n\n.ps-registry .reg-rail-collapse:hover {\n background: var(--mj-bg-surface-hover);\n color: var(--mj-text-primary);\n border-color: var(--mj-brand-primary);\n}\n\n.ps-registry .reg-collapsed-strip-label {\n color: var(--mj-text-muted);\n font-size: 16px;\n margin-top: 8px;\n}\n\n.ps-registry .reg-collapse-btn {\n background: transparent;\n border: 1px solid transparent;\n color: var(--mj-text-muted);\n border-radius: var(--mj-radius-sm);\n width: 26px;\n height: 26px;\n display: flex;\n align-items: center;\n justify-content: center;\n cursor: pointer;\n transition: all 0.15s ease;\n}\n\n.ps-registry .reg-collapse-btn:hover {\n background: var(--mj-bg-surface-hover);\n color: var(--mj-text-primary);\n border-color: var(--mj-border-default);\n}\n\n/* model list \u2014 its own scroll column */\n.ps-registry .mlist {\n min-height: 0;\n overflow-y: auto;\n align-self: stretch;\n height: 100%;\n border-radius: 0;\n border: none;\n border-right: 1px solid var(--mj-border-default);\n}\n\n/* detail \u2014 its own scroll column */\n.ps-registry .detail {\n min-height: 0;\n height: 100%;\n overflow-y: auto;\n padding: 16px 20px;\n}\n\n.ps-registry .mrow {\n display: flex;\n align-items: center;\n gap: 10px;\n padding: 10px;\n border-radius: 8px;\n cursor: pointer;\n transition: background 0.12s;\n}\n.ps-registry .mrow:hover {\n background: var(--mj-bg-surface-hover);\n}\n.ps-registry .mrow.sel {\n background: color-mix(in srgb, var(--mj-brand-primary) 10%, var(--mj-bg-surface));\n box-shadow: inset 3px 0 0 var(--mj-brand-primary);\n}\n.ps-registry .mrow.arc {\n opacity: 0.72;\n}\n.ps-registry .mrow .ico {\n width: 30px;\n height: 30px;\n border-radius: 7px;\n display: grid;\n place-items: center;\n color: var(--mj-text-inverse);\n font-size: 13px;\n flex: 0 0 auto;\n}\n.ps-registry .mrow .ico.xgb {\n background: var(--ps-node-feature);\n}\n.ps-registry .mrow .ico.lgbm {\n background: var(--ps-node-algorithm);\n}\n.ps-registry .mrow .ico.rf {\n background: var(--ps-node-source);\n}\n.ps-registry .mrow .nm {\n font-weight: 600;\n color: var(--mj-text-primary);\n font-size: 13px;\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n}\n.ps-registry .mrow .auc {\n text-align: right;\n flex: 0 0 auto;\n}\n.ps-registry .mrow .auc .v {\n font-weight: 700;\n color: var(--mj-text-primary);\n font-variant-numeric: tabular-nums;\n}\n.ps-registry .mrow .auc .st {\n font-size: 10.5px;\n font-weight: 700;\n text-transform: uppercase;\n letter-spacing: 0.03em;\n}\n.ps-registry .mrow .auc .st.pub {\n color: var(--mj-status-success-text);\n}\n.ps-registry .mrow .auc .st.val {\n color: var(--mj-status-info-text);\n}\n.ps-registry .mrow .auc .st.dr {\n color: var(--mj-text-muted);\n}\n.ps-registry .mrow .auc .st.arc {\n color: var(--mj-text-disabled);\n}\n"] }]
816
355
  }], null, { engine: [{
817
356
  type: Input
818
357
  }], provider: [{
819
358
  type: Input
820
359
  }], currentUser: [{
821
360
  type: Input
361
+ }], initialModelId: [{
362
+ type: Input
822
363
  }] }); })();
823
- (() => { (typeof ngDevMode === "undefined" || ngDevMode) && i0.ɵsetClassDebugInfo(PSRegistryComponent, { className: "PSRegistryComponent", filePath: "src/PredictiveStudio/components/ps-registry.component.ts", lineNumber: 230 }); })();
824
- /** Minimal HTML-escape for interpolating model names into the modal's innerHTML message. */
825
- function escapeHtml(value) {
826
- return value
827
- .replace(/&/g, '&amp;')
828
- .replace(/</g, '&lt;')
829
- .replace(/>/g, '&gt;')
830
- .replace(/"/g, '&quot;');
831
- }
364
+ (() => { (typeof ngDevMode === "undefined" || ngDevMode) && i0.ɵsetClassDebugInfo(PSRegistryComponent, { className: "PSRegistryComponent", filePath: "src/PredictiveStudio/components/ps-registry.component.ts", lineNumber: 114 }); })();
832
365
  //# sourceMappingURL=ps-registry.component.js.map