@memberjunction/ng-explorer-core 6.2.0-edge.2 → 6.2.0-edge.4

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 (139) hide show
  1. package/README.md +10 -0
  2. package/dist/app-routing.module.d.ts.map +1 -1
  3. package/dist/app-routing.module.js +5 -0
  4. package/dist/app-routing.module.js.map +1 -1
  5. package/dist/generated/lazy-feature-config.d.ts +1 -1
  6. package/dist/generated/lazy-feature-config.d.ts.map +1 -1
  7. package/dist/generated/lazy-feature-config.js +12 -2
  8. package/dist/generated/lazy-feature-config.js.map +1 -1
  9. package/dist/lib/guards/moved-nav-item-redirect.d.ts +15 -0
  10. package/dist/lib/guards/moved-nav-item-redirect.d.ts.map +1 -0
  11. package/dist/lib/guards/moved-nav-item-redirect.js +37 -0
  12. package/dist/lib/guards/moved-nav-item-redirect.js.map +1 -0
  13. package/dist/lib/omnibar/omnibar-dashboard-matches.d.ts +34 -0
  14. package/dist/lib/omnibar/omnibar-dashboard-matches.d.ts.map +1 -0
  15. package/dist/lib/omnibar/omnibar-dashboard-matches.js +70 -0
  16. package/dist/lib/omnibar/omnibar-dashboard-matches.js.map +1 -0
  17. package/dist/lib/omnibar/omnibar-palette.component.d.ts +27 -2
  18. package/dist/lib/omnibar/omnibar-palette.component.d.ts.map +1 -1
  19. package/dist/lib/omnibar/omnibar-palette.component.js +126 -65
  20. package/dist/lib/omnibar/omnibar-palette.component.js.map +1 -1
  21. package/dist/lib/omnibar/omnibar-provider.d.ts +4 -0
  22. package/dist/lib/omnibar/omnibar-provider.d.ts.map +1 -1
  23. package/dist/lib/omnibar/omnibar-provider.js.map +1 -1
  24. package/dist/lib/omnibar/providers/omnibar-record.provider.d.ts +2 -2
  25. package/dist/lib/omnibar/providers/omnibar-record.provider.d.ts.map +1 -1
  26. package/dist/lib/omnibar/providers/omnibar-record.provider.js +3 -7
  27. package/dist/lib/omnibar/providers/omnibar-record.provider.js.map +1 -1
  28. package/dist/lib/omnibar/providers/omnibar-search.provider.d.ts +15 -2
  29. package/dist/lib/omnibar/providers/omnibar-search.provider.d.ts.map +1 -1
  30. package/dist/lib/omnibar/providers/omnibar-search.provider.js +59 -7
  31. package/dist/lib/omnibar/providers/omnibar-search.provider.js.map +1 -1
  32. package/dist/lib/profile/forking-setting.d.ts +8 -0
  33. package/dist/lib/profile/forking-setting.d.ts.map +1 -0
  34. package/dist/lib/profile/forking-setting.js +18 -0
  35. package/dist/lib/profile/forking-setting.js.map +1 -0
  36. package/dist/lib/profile/profile-dialog.component.d.ts +8 -0
  37. package/dist/lib/profile/profile-dialog.component.d.ts.map +1 -1
  38. package/dist/lib/profile/profile-dialog.component.js +182 -92
  39. package/dist/lib/profile/profile-dialog.component.js.map +1 -1
  40. package/dist/lib/resource-wrappers/artifact-resource.component.d.ts +23 -4
  41. package/dist/lib/resource-wrappers/artifact-resource.component.d.ts.map +1 -1
  42. package/dist/lib/resource-wrappers/artifact-resource.component.js +36 -8
  43. package/dist/lib/resource-wrappers/artifact-resource.component.js.map +1 -1
  44. package/dist/lib/resource-wrappers/chat-conversations-resource.component.d.ts +7 -18
  45. package/dist/lib/resource-wrappers/chat-conversations-resource.component.d.ts.map +1 -1
  46. package/dist/lib/resource-wrappers/chat-conversations-resource.component.js +20 -41
  47. package/dist/lib/resource-wrappers/chat-conversations-resource.component.js.map +1 -1
  48. package/dist/lib/resource-wrappers/chat-search-routing.d.ts +10 -3
  49. package/dist/lib/resource-wrappers/chat-search-routing.d.ts.map +1 -1
  50. package/dist/lib/resource-wrappers/chat-search-routing.js +11 -4
  51. package/dist/lib/resource-wrappers/chat-search-routing.js.map +1 -1
  52. package/dist/lib/resource-wrappers/dashboard-add-to-menu.component.d.ts +29 -0
  53. package/dist/lib/resource-wrappers/dashboard-add-to-menu.component.d.ts.map +1 -0
  54. package/dist/lib/resource-wrappers/dashboard-add-to-menu.component.js +141 -0
  55. package/dist/lib/resource-wrappers/dashboard-add-to-menu.component.js.map +1 -0
  56. package/dist/lib/resource-wrappers/dashboard-part-config.d.ts +28 -0
  57. package/dist/lib/resource-wrappers/dashboard-part-config.d.ts.map +1 -0
  58. package/dist/lib/resource-wrappers/dashboard-part-config.js +150 -0
  59. package/dist/lib/resource-wrappers/dashboard-part-config.js.map +1 -0
  60. package/dist/lib/resource-wrappers/dashboard-resource.component.d.ts +361 -56
  61. package/dist/lib/resource-wrappers/dashboard-resource.component.d.ts.map +1 -1
  62. package/dist/lib/resource-wrappers/dashboard-resource.component.js +1312 -361
  63. package/dist/lib/resource-wrappers/dashboard-resource.component.js.map +1 -1
  64. package/dist/lib/resource-wrappers/dashboard-source-search.d.ts +86 -0
  65. package/dist/lib/resource-wrappers/dashboard-source-search.d.ts.map +1 -0
  66. package/dist/lib/resource-wrappers/dashboard-source-search.js +136 -0
  67. package/dist/lib/resource-wrappers/dashboard-source-search.js.map +1 -0
  68. package/dist/lib/resource-wrappers/dashboard-studio-pane.d.ts +30 -0
  69. package/dist/lib/resource-wrappers/dashboard-studio-pane.d.ts.map +1 -0
  70. package/dist/lib/resource-wrappers/dashboard-studio-pane.js +55 -0
  71. package/dist/lib/resource-wrappers/dashboard-studio-pane.js.map +1 -0
  72. package/dist/lib/resource-wrappers/dashboard-studio-tools.d.ts +135 -0
  73. package/dist/lib/resource-wrappers/dashboard-studio-tools.d.ts.map +1 -0
  74. package/dist/lib/resource-wrappers/dashboard-studio-tools.js +856 -0
  75. package/dist/lib/resource-wrappers/dashboard-studio-tools.js.map +1 -0
  76. package/dist/lib/resource-wrappers/dashboard-tab-agent.d.ts +87 -0
  77. package/dist/lib/resource-wrappers/dashboard-tab-agent.d.ts.map +1 -0
  78. package/dist/lib/resource-wrappers/dashboard-tab-agent.js +125 -0
  79. package/dist/lib/resource-wrappers/dashboard-tab-agent.js.map +1 -0
  80. package/dist/lib/resource-wrappers/human-handoff-console-resource.component.d.ts +32 -0
  81. package/dist/lib/resource-wrappers/human-handoff-console-resource.component.d.ts.map +1 -0
  82. package/dist/lib/resource-wrappers/human-handoff-console-resource.component.js +114 -0
  83. package/dist/lib/resource-wrappers/human-handoff-console-resource.component.js.map +1 -0
  84. package/dist/lib/resource-wrappers/livekit-room-resource.component.d.ts +40 -6
  85. package/dist/lib/resource-wrappers/livekit-room-resource.component.d.ts.map +1 -1
  86. package/dist/lib/resource-wrappers/livekit-room-resource.component.js +287 -147
  87. package/dist/lib/resource-wrappers/livekit-room-resource.component.js.map +1 -1
  88. package/dist/lib/resource-wrappers/meetings-resource.component.d.ts +42 -0
  89. package/dist/lib/resource-wrappers/meetings-resource.component.d.ts.map +1 -0
  90. package/dist/lib/resource-wrappers/meetings-resource.component.js +225 -0
  91. package/dist/lib/resource-wrappers/meetings-resource.component.js.map +1 -0
  92. package/dist/lib/resource-wrappers/record-resource.component.d.ts +27 -2
  93. package/dist/lib/resource-wrappers/record-resource.component.d.ts.map +1 -1
  94. package/dist/lib/resource-wrappers/record-resource.component.js +58 -6
  95. package/dist/lib/resource-wrappers/record-resource.component.js.map +1 -1
  96. package/dist/lib/resource-wrappers/search-results-resource.component.d.ts.map +1 -1
  97. package/dist/lib/resource-wrappers/search-results-resource.component.js +5 -4
  98. package/dist/lib/resource-wrappers/search-results-resource.component.js.map +1 -1
  99. package/dist/lib/shell/components/tabs/component-cache-manager.d.ts.map +1 -1
  100. package/dist/lib/shell/components/tabs/component-cache-manager.js.map +1 -1
  101. package/dist/lib/shell/components/tabs/tab-container.component.d.ts +38 -0
  102. package/dist/lib/shell/components/tabs/tab-container.component.d.ts.map +1 -1
  103. package/dist/lib/shell/components/tabs/tab-container.component.js +77 -4
  104. package/dist/lib/shell/components/tabs/tab-container.component.js.map +1 -1
  105. package/dist/lib/shell/dashboard-tab-for-url.d.ts +35 -0
  106. package/dist/lib/shell/dashboard-tab-for-url.d.ts.map +1 -0
  107. package/dist/lib/shell/dashboard-tab-for-url.js +39 -0
  108. package/dist/lib/shell/dashboard-tab-for-url.js.map +1 -0
  109. package/dist/lib/shell/shell.component.d.ts +31 -2
  110. package/dist/lib/shell/shell.component.d.ts.map +1 -1
  111. package/dist/lib/shell/shell.component.js +119 -42
  112. package/dist/lib/shell/shell.component.js.map +1 -1
  113. package/dist/lib/shell/shell.module.d.ts +2 -1
  114. package/dist/lib/shell/shell.module.d.ts.map +1 -1
  115. package/dist/lib/shell/shell.module.js +6 -2
  116. package/dist/lib/shell/shell.module.js.map +1 -1
  117. package/dist/lib/single-dashboard/single-dashboard.component.js +1 -1
  118. package/dist/lib/single-dashboard/single-dashboard.component.js.map +1 -1
  119. package/dist/lib/single-record/single-record.component.d.ts +29 -3
  120. package/dist/lib/single-record/single-record.component.d.ts.map +1 -1
  121. package/dist/lib/single-record/single-record.component.js +44 -4
  122. package/dist/lib/single-record/single-record.component.js.map +1 -1
  123. package/dist/lib/template-user/template-user-banner.component.d.ts +20 -0
  124. package/dist/lib/template-user/template-user-banner.component.d.ts.map +1 -0
  125. package/dist/lib/template-user/template-user-banner.component.js +67 -0
  126. package/dist/lib/template-user/template-user-banner.component.js.map +1 -0
  127. package/dist/lib/user-notifications/user-notifications.component.d.ts +13 -0
  128. package/dist/lib/user-notifications/user-notifications.component.d.ts.map +1 -1
  129. package/dist/lib/user-notifications/user-notifications.component.js +67 -0
  130. package/dist/lib/user-notifications/user-notifications.component.js.map +1 -1
  131. package/dist/module.d.ts +61 -56
  132. package/dist/module.d.ts.map +1 -1
  133. package/dist/module.js +32 -4
  134. package/dist/module.js.map +1 -1
  135. package/dist/public-api.d.ts +2 -0
  136. package/dist/public-api.d.ts.map +1 -1
  137. package/dist/public-api.js +2 -0
  138. package/dist/public-api.js.map +1 -1
  139. package/package.json +58 -52
@@ -4,18 +4,33 @@ var __decorate = (this && this.__decorate) || function (decorators, target, key,
4
4
  else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
5
5
  return c > 3 && r && Object.defineProperty(target, key, r), r;
6
6
  };
7
- import { Component, ViewChild } from '@angular/core';
8
- import { BaseResourceComponent, BaseDashboard } from '@memberjunction/ng-shared';
9
- import { DashboardEngine } from '@memberjunction/core-entities';
10
- import { RegisterClass, MJGlobal, SafeJSONParse, UUIDsEqual } from '@memberjunction/global';
11
- import { CompositeKey, LogError } from '@memberjunction/core';
12
- import { DashboardViewerComponent } from '@memberjunction/ng-dashboard-viewer';
7
+ import { Component, ViewChild, inject } from '@angular/core';
8
+ import { BaseResourceComponent, BaseDashboard, RecentAccessService, HomeAppPinService, SafeDetectChanges, CaptureElementJpeg, BuildDashboardPinInput, DASHBOARD_PIN_RESOURCE_TYPE } from '@memberjunction/ng-shared';
9
+ import { MJNotificationService } from '@memberjunction/ng-notifications';
10
+ import { RealtimeSessionService, ArtifactPermissionService } from '@memberjunction/ng-conversations';
11
+ import { DashboardEngine, UserViewEngine, QueryEngine, ArtifactMetadataEngine, MJEnvironmentEntityExtended, UserInfoEngine } from '@memberjunction/core-entities';
12
+ import { RegisterClass, MJGlobal, SafeJSONParse, UUIDsEqual, EscapeSQLString, NormalizeUUID } from '@memberjunction/global';
13
+ import { CompositeKey, RunView, LogError } from '@memberjunction/core';
14
+ import { DefaultAgentResolver } from '@memberjunction/conversations-runtime';
15
+ import { filter, takeUntil } from 'rxjs';
16
+ import { ExtractPanelsFromLayout } from '@memberjunction/ng-dashboard-viewer';
17
+ import { BuildDashboardTabAgentContext, BuildDashboardTabAgentTools, SummarizeDashboardPanels } from './dashboard-tab-agent';
18
+ import { BuildDashboardStudioTools } from './dashboard-studio-tools';
19
+ import { SearchDashboardSources, SOURCE_KINDS } from './dashboard-source-search';
20
+ import { ParseStudioPanePrefs, PrefsFromSplitSizes, STUDIO_PANE_COPILOT_MAX_PCT, STUDIO_PANE_COPILOT_MIN_PCT, STUDIO_PANE_DEFAULTS, STUDIO_PANE_SETTING_KEY } from './dashboard-studio-pane';
13
21
  import * as i0 from "@angular/core";
14
- import * as i1 from "@angular/forms";
22
+ import * as i1 from "@memberjunction/ng-conversations";
15
23
  import * as i2 from "@memberjunction/ng-dashboards/core-dashboards.module";
16
- const _c0 = ["container"];
24
+ import * as i3 from "@memberjunction/ng-dashboard-viewer";
25
+ import * as i4 from "angular-split";
26
+ import * as i5 from "@memberjunction/ng-shared-generic";
27
+ import * as i6 from "@memberjunction/ng-ui-components";
28
+ import * as i7 from "./dashboard-add-to-menu.component";
29
+ const _c0 = ["editor"];
30
+ const _c1 = ["container"];
31
+ const _c2 = a0 => [a0];
17
32
  function DashboardResource_Conditional_1_Conditional_7_Template(rf, ctx) { if (rf & 1) {
18
- i0.ɵɵelementStart(0, "details", 11)(1, "summary");
33
+ i0.ɵɵelementStart(0, "details", 13)(1, "summary");
19
34
  i0.ɵɵtext(2, "Technical Details");
20
35
  i0.ɵɵelementEnd();
21
36
  i0.ɵɵelementStart(3, "pre");
@@ -27,16 +42,16 @@ function DashboardResource_Conditional_1_Conditional_7_Template(rf, ctx) { if (r
27
42
  i0.ɵɵtextInterpolate(ctx_r0.errorDetails);
28
43
  } }
29
44
  function DashboardResource_Conditional_1_Template(rf, ctx) { if (rf & 1) {
30
- i0.ɵɵelementStart(0, "div", 2)(1, "div", 7);
31
- i0.ɵɵelement(2, "i", 8);
45
+ i0.ɵɵelementStart(0, "div", 3)(1, "div", 9);
46
+ i0.ɵɵelement(2, "i", 10);
32
47
  i0.ɵɵelementEnd();
33
- i0.ɵɵelementStart(3, "h2", 9);
48
+ i0.ɵɵelementStart(3, "h2", 11);
34
49
  i0.ɵɵtext(4, "Unable to Load Dashboard");
35
50
  i0.ɵɵelementEnd();
36
- i0.ɵɵelementStart(5, "p", 10);
51
+ i0.ɵɵelementStart(5, "p", 12);
37
52
  i0.ɵɵtext(6);
38
53
  i0.ɵɵelementEnd();
39
- i0.ɵɵconditionalCreate(7, DashboardResource_Conditional_1_Conditional_7_Template, 5, 1, "details", 11);
54
+ i0.ɵɵconditionalCreate(7, DashboardResource_Conditional_1_Conditional_7_Template, 5, 1, "details", 13);
40
55
  i0.ɵɵelementEnd();
41
56
  } if (rf & 2) {
42
57
  const ctx_r0 = i0.ɵɵnextContext();
@@ -45,105 +60,194 @@ function DashboardResource_Conditional_1_Template(rf, ctx) { if (rf & 1) {
45
60
  i0.ɵɵadvance();
46
61
  i0.ɵɵconditional(ctx_r0.errorDetails ? 7 : -1);
47
62
  } }
48
- function DashboardResource_Conditional_2_Conditional_5_Template(rf, ctx) { if (rf & 1) {
49
- i0.ɵɵelementStart(0, "span", 15);
50
- i0.ɵɵelement(1, "i", 19);
63
+ function DashboardResource_Conditional_2_Conditional_8_Template(rf, ctx) { if (rf & 1) {
64
+ const _r3 = i0.ɵɵgetCurrentView();
65
+ i0.ɵɵelementStart(0, "button", 23);
66
+ i0.ɵɵlistener("click", function DashboardResource_Conditional_2_Conditional_8_Template_button_click_0_listener() { i0.ɵɵrestoreView(_r3); const ctx_r0 = i0.ɵɵnextContext(2); return i0.ɵɵresetView(ctx_r0.OpenShareDialog()); });
67
+ i0.ɵɵelement(1, "i", 24);
51
68
  i0.ɵɵelementEnd();
52
69
  } }
53
- function DashboardResource_Conditional_2_Conditional_7_Template(rf, ctx) { if (rf & 1) {
54
- const _r2 = i0.ɵɵgetCurrentView();
55
- i0.ɵɵelementStart(0, "button", 20);
56
- i0.ɵɵlistener("click", function DashboardResource_Conditional_2_Conditional_7_Template_button_click_0_listener() { i0.ɵɵrestoreView(_r2); const ctx_r0 = i0.ɵɵnextContext(2); return i0.ɵɵresetView(ctx_r0.openShareDialog()); });
57
- i0.ɵɵelement(1, "i", 19);
70
+ function DashboardResource_Conditional_2_Conditional_9_Conditional_12_Template(rf, ctx) { if (rf & 1) {
71
+ const _r5 = i0.ɵɵgetCurrentView();
72
+ i0.ɵɵelementStart(0, "mj-conversation-chat-area", 38);
73
+ i0.ɵɵlistener("ConversationCreated", function DashboardResource_Conditional_2_Conditional_9_Conditional_12_Template_mj_conversation_chat_area_ConversationCreated_0_listener($event) { i0.ɵɵrestoreView(_r5); const ctx_r0 = i0.ɵɵnextContext(3); return i0.ɵɵresetView(ctx_r0.OnChatConversationCreated($event)); })("RealtimeConversationReady", function DashboardResource_Conditional_2_Conditional_9_Conditional_12_Template_mj_conversation_chat_area_RealtimeConversationReady_0_listener($event) { i0.ɵɵrestoreView(_r5); const ctx_r0 = i0.ɵɵnextContext(3); return i0.ɵɵresetView(ctx_r0.OnChatRealtimeConversationReady($event)); })("PendingMessageConsumed", function DashboardResource_Conditional_2_Conditional_9_Conditional_12_Template_mj_conversation_chat_area_PendingMessageConsumed_0_listener() { i0.ɵɵrestoreView(_r5); const ctx_r0 = i0.ɵɵnextContext(3); return i0.ɵɵresetView(ctx_r0.OnChatPendingMessageConsumed()); })("navigationRequest", function DashboardResource_Conditional_2_Conditional_9_Conditional_12_Template_mj_conversation_chat_area_navigationRequest_0_listener($event) { i0.ɵɵrestoreView(_r5); const ctx_r0 = i0.ɵɵnextContext(3); return i0.ɵɵresetView(ctx_r0.OnChatNavigationRequest($event)); })("OpenEntityRecord", function DashboardResource_Conditional_2_Conditional_9_Conditional_12_Template_mj_conversation_chat_area_OpenEntityRecord_0_listener($event) { i0.ɵɵrestoreView(_r5); const ctx_r0 = i0.ɵɵnextContext(3); return i0.ɵɵresetView(ctx_r0.OnChatOpenEntityRecord($event)); });
58
74
  i0.ɵɵelementEnd();
75
+ } if (rf & 2) {
76
+ const ctx_r0 = i0.ɵɵnextContext(3);
77
+ i0.ɵɵproperty("Provider", ctx_r0.Provider)("EnvironmentId", ctx_r0.ChatEnvironmentId)("CurrentUser", ctx_r0.ProviderToUse.CurrentUser)("Conversation", ctx_r0.ChatConversation)("ConversationId", ctx_r0.ChatConversationId)("IsNewConversation", ctx_r0.ChatIsNewConversation)("PendingMessage", ctx_r0.ChatPendingMessage)("PendingAttachments", ctx_r0.ChatPendingAttachments)("SuppressNewConversationEmptyState", true)("AllowMentions", false)("OverlayMode", false)("ShowExportButton", false)("ShowShareButton", false)("ShowArtifactIndicator", false)("ShowAgentPicker", false)("ShowAgentModePicker", false)("DefaultAgentId", ctx_r0.ChatAgentId)("AllowedAgentIDs", i0.ɵɵpureFunction1(20, _c2, ctx_r0.ChatAgentId))("ApplicationId", ctx_r0.ChatApplicationId)("AppContext", ctx_r0.ChatAppContext);
59
78
  } }
60
- function DashboardResource_Conditional_2_Conditional_8_Template(rf, ctx) { if (rf & 1) {
61
- const _r3 = i0.ɵɵgetCurrentView();
62
- i0.ɵɵelementStart(0, "button", 21);
63
- i0.ɵɵlistener("click", function DashboardResource_Conditional_2_Conditional_8_Template_button_click_0_listener() { i0.ɵɵrestoreView(_r3); const ctx_r0 = i0.ɵɵnextContext(2); return i0.ɵɵresetView(ctx_r0.toggleEditMode()); });
64
- i0.ɵɵelement(1, "i", 22);
79
+ function DashboardResource_Conditional_2_Conditional_9_Conditional_13_Template(rf, ctx) { if (rf & 1) {
80
+ i0.ɵɵelementStart(0, "div", 36);
81
+ i0.ɵɵtext(1);
65
82
  i0.ɵɵelementEnd();
83
+ } if (rf & 2) {
84
+ const ctx_r0 = i0.ɵɵnextContext(3);
85
+ i0.ɵɵadvance();
86
+ i0.ɵɵtextInterpolate(ctx_r0.ChatAgentError);
66
87
  } }
67
- function DashboardResource_Conditional_2_Template(rf, ctx) { if (rf & 1) {
68
- i0.ɵɵelementStart(0, "div", 3)(1, "div", 12)(2, "span", 13);
69
- i0.ɵɵelement(3, "i", 14);
70
- i0.ɵɵtext(4);
88
+ function DashboardResource_Conditional_2_Conditional_9_Conditional_14_Template(rf, ctx) { if (rf & 1) {
89
+ i0.ɵɵelementStart(0, "div", 37);
90
+ i0.ɵɵelement(1, "mj-loading", 39);
71
91
  i0.ɵɵelementEnd();
72
- i0.ɵɵconditionalCreate(5, DashboardResource_Conditional_2_Conditional_5_Template, 2, 0, "span", 15);
92
+ } }
93
+ function DashboardResource_Conditional_2_Conditional_9_Template(rf, ctx) { if (rf & 1) {
94
+ const _r4 = i0.ɵɵgetCurrentView();
95
+ i0.ɵɵelementStart(0, "as-split-area", 22)(1, "aside", 25)(2, "div", 26)(3, "div", 27);
96
+ i0.ɵɵelement(4, "i", 28);
97
+ i0.ɵɵtext(5);
73
98
  i0.ɵɵelementEnd();
74
- i0.ɵɵelementStart(6, "div", 16);
75
- i0.ɵɵconditionalCreate(7, DashboardResource_Conditional_2_Conditional_7_Template, 2, 0, "button", 17);
76
- i0.ɵɵconditionalCreate(8, DashboardResource_Conditional_2_Conditional_8_Template, 2, 0, "button", 18);
77
- i0.ɵɵelementEnd()();
99
+ i0.ɵɵelementStart(6, "div", 29)(7, "button", 30);
100
+ i0.ɵɵlistener("click", function DashboardResource_Conditional_2_Conditional_9_Template_button_click_7_listener() { i0.ɵɵrestoreView(_r4); const ctx_r0 = i0.ɵɵnextContext(2); return i0.ɵɵresetView(ctx_r0.ToggleCopilotExpanded()); });
101
+ i0.ɵɵelement(8, "i", 31);
102
+ i0.ɵɵelementEnd();
103
+ i0.ɵɵelementStart(9, "button", 32);
104
+ i0.ɵɵlistener("click", function DashboardResource_Conditional_2_Conditional_9_Template_button_click_9_listener() { i0.ɵɵrestoreView(_r4); const ctx_r0 = i0.ɵɵnextContext(2); return i0.ɵɵresetView(ctx_r0.CloseChat()); });
105
+ i0.ɵɵelement(10, "i", 33);
106
+ i0.ɵɵelementEnd()()();
107
+ i0.ɵɵelementStart(11, "div", 34);
108
+ i0.ɵɵconditionalCreate(12, DashboardResource_Conditional_2_Conditional_9_Conditional_12_Template, 1, 22, "mj-conversation-chat-area", 35)(13, DashboardResource_Conditional_2_Conditional_9_Conditional_13_Template, 2, 1, "div", 36)(14, DashboardResource_Conditional_2_Conditional_9_Conditional_14_Template, 2, 0, "div", 37);
109
+ i0.ɵɵelementEnd()()();
78
110
  } if (rf & 2) {
79
- const ctx_r0 = i0.ɵɵnextContext();
80
- i0.ɵɵadvance(4);
81
- i0.ɵɵtextInterpolate1(" ", ctx_r0.configDashboard.Name, " ");
111
+ const ctx_r0 = i0.ɵɵnextContext(2);
112
+ i0.ɵɵproperty("size", ctx_r0.CopilotExpanded ? ctx_r0.CopilotMaxPct : ctx_r0.CopilotSizePct)("minSize", ctx_r0.CopilotMinPct)("maxSize", ctx_r0.CopilotMaxPct);
82
113
  i0.ɵɵadvance();
83
- i0.ɵɵconditional(!ctx_r0.dashboardPermissions.IsOwner && ctx_r0.dashboardPermissions.PermissionSource !== "none" ? 5 : -1);
114
+ i0.ɵɵclassProp("expanded", ctx_r0.CopilotExpanded);
115
+ i0.ɵɵadvance(4);
116
+ i0.ɵɵtextInterpolate1(" ", ctx_r0.ChatAgentName ?? "AI assistant");
84
117
  i0.ɵɵadvance(2);
85
- i0.ɵɵconditional(ctx_r0.dashboardPermissions.CanShare ? 7 : -1);
118
+ i0.ɵɵproperty("title", ctx_r0.CopilotExpanded ? "Collapse panel" : "Expand panel");
119
+ i0.ɵɵattribute("aria-label", ctx_r0.CopilotExpanded ? "Collapse panel" : "Expand panel");
86
120
  i0.ɵɵadvance();
87
- i0.ɵɵconditional(ctx_r0.dashboardPermissions.CanEdit ? 8 : -1);
121
+ i0.ɵɵclassProp("fa-up-right-and-down-left-from-center", !ctx_r0.CopilotExpanded)("fa-down-left-and-up-right-to-center", ctx_r0.CopilotExpanded);
122
+ i0.ɵɵadvance(4);
123
+ i0.ɵɵconditional(ctx_r0.ChatAgentId && ctx_r0.ProviderToUse.CurrentUser ? 12 : ctx_r0.ChatAgentError ? 13 : 14);
88
124
  } }
89
- function DashboardResource_Conditional_3_Template(rf, ctx) { if (rf & 1) {
90
- const _r4 = i0.ɵɵgetCurrentView();
91
- i0.ɵɵelementStart(0, "div", 4)(1, "div", 23)(2, "button", 24);
92
- i0.ɵɵlistener("click", function DashboardResource_Conditional_3_Template_button_click_2_listener() { i0.ɵɵrestoreView(_r4); const ctx_r0 = i0.ɵɵnextContext(); return i0.ɵɵresetView(ctx_r0.openAddPartDialog()); });
93
- i0.ɵɵelement(3, "i", 25);
94
- i0.ɵɵtext(4, " Add Part ");
125
+ function DashboardResource_Conditional_2_Template(rf, ctx) { if (rf & 1) {
126
+ const _r2 = i0.ɵɵgetCurrentView();
127
+ i0.ɵɵelementStart(0, "as-split", 14);
128
+ i0.ɵɵlistener("dragEnd", function DashboardResource_Conditional_2_Template_as_split_dragEnd_0_listener($event) { i0.ɵɵrestoreView(_r2); const ctx_r0 = i0.ɵɵnextContext(); return i0.ɵɵresetView(ctx_r0.OnCopilotSplitDragEnd($event.sizes)); });
129
+ i0.ɵɵelementStart(1, "as-split-area", 15)(2, "mj-dashboard-editor", 16, 1);
130
+ i0.ɵɵlistener("Loaded", function DashboardResource_Conditional_2_Template_mj_dashboard_editor_Loaded_2_listener($event) { i0.ɵɵrestoreView(_r2); const ctx_r0 = i0.ɵɵnextContext(); return i0.ɵɵresetView(ctx_r0.OnEditorLoaded($event)); })("LoadFailed", function DashboardResource_Conditional_2_Template_mj_dashboard_editor_LoadFailed_2_listener($event) { i0.ɵɵrestoreView(_r2); const ctx_r0 = i0.ɵɵnextContext(); return i0.ɵɵresetView(ctx_r0.OnEditorLoadFailed($event)); })("EditingChange", function DashboardResource_Conditional_2_Template_mj_dashboard_editor_EditingChange_2_listener() { i0.ɵɵrestoreView(_r2); const ctx_r0 = i0.ɵɵnextContext(); return i0.ɵɵresetView(ctx_r0.OnEditorEditingChange()); })("NameChanged", function DashboardResource_Conditional_2_Template_mj_dashboard_editor_NameChanged_2_listener($event) { i0.ɵɵrestoreView(_r2); const ctx_r0 = i0.ɵɵnextContext(); return i0.ɵɵresetView(ctx_r0.OnEditorNameChanged($event)); })("ConfigChanged", function DashboardResource_Conditional_2_Template_mj_dashboard_editor_ConfigChanged_2_listener() { i0.ɵɵrestoreView(_r2); const ctx_r0 = i0.ɵɵnextContext(); return i0.ɵɵresetView(ctx_r0.OnEditorConfigChanged()); })("ReloadedFromSaved", function DashboardResource_Conditional_2_Template_mj_dashboard_editor_ReloadedFromSaved_2_listener() { i0.ɵɵrestoreView(_r2); const ctx_r0 = i0.ɵɵnextContext(); return i0.ɵɵresetView(ctx_r0.OnEditorReloaded()); })("FavoriteChange", function DashboardResource_Conditional_2_Template_mj_dashboard_editor_FavoriteChange_2_listener() { i0.ɵɵrestoreView(_r2); const ctx_r0 = i0.ɵɵnextContext(); return i0.ɵɵresetView(ctx_r0.OnEditorFavoriteChange()); })("NavigationRequested", function DashboardResource_Conditional_2_Template_mj_dashboard_editor_NavigationRequested_2_listener($event) { i0.ɵɵrestoreView(_r2); const ctx_r0 = i0.ɵɵnextContext(); return i0.ɵɵresetView(ctx_r0.OnEditorNavigationRequested($event)); });
131
+ i0.ɵɵelementStart(4, "button", 17);
132
+ i0.ɵɵlistener("click", function DashboardResource_Conditional_2_Template_button_click_4_listener() { i0.ɵɵrestoreView(_r2); const ctx_r0 = i0.ɵɵnextContext(); return i0.ɵɵresetView(ctx_r0.ToggleChat()); });
133
+ i0.ɵɵelement(5, "i", 18);
95
134
  i0.ɵɵelementEnd();
96
- i0.ɵɵelement(5, "div", 26);
97
- i0.ɵɵelementStart(6, "div", 27)(7, "input", 28);
98
- i0.ɵɵtwoWayListener("ngModelChange", function DashboardResource_Conditional_3_Template_input_ngModelChange_7_listener($event) { i0.ɵɵrestoreView(_r4); const ctx_r0 = i0.ɵɵnextContext(); i0.ɵɵtwoWayBindingSet(ctx_r0.editingName, $event) || (ctx_r0.editingName = $event); return i0.ɵɵresetView($event); });
135
+ i0.ɵɵelementStart(6, "span", 19)(7, "mj-dashboard-add-to-menu", 20);
136
+ i0.ɵɵlistener("PinToHome", function DashboardResource_Conditional_2_Template_mj_dashboard_add_to_menu_PinToHome_7_listener() { i0.ɵɵrestoreView(_r2); const ctx_r0 = i0.ɵɵnextContext(); return i0.ɵɵresetView(ctx_r0.PinToHome()); })("Share", function DashboardResource_Conditional_2_Template_mj_dashboard_add_to_menu_Share_7_listener() { i0.ɵɵrestoreView(_r2); const ctx_r0 = i0.ɵɵnextContext(); return i0.ɵɵresetView(ctx_r0.OpenShareDialog()); });
99
137
  i0.ɵɵelementEnd();
100
- i0.ɵɵelementStart(8, "input", 29);
101
- i0.ɵɵtwoWayListener("ngModelChange", function DashboardResource_Conditional_3_Template_input_ngModelChange_8_listener($event) { i0.ɵɵrestoreView(_r4); const ctx_r0 = i0.ɵɵnextContext(); i0.ɵɵtwoWayBindingSet(ctx_r0.editingDescription, $event) || (ctx_r0.editingDescription = $event); return i0.ɵɵresetView($event); });
138
+ i0.ɵɵconditionalCreate(8, DashboardResource_Conditional_2_Conditional_8_Template, 2, 0, "button", 21);
102
139
  i0.ɵɵelementEnd()()();
103
- i0.ɵɵelementStart(9, "div", 30)(10, "button", 31);
104
- i0.ɵɵlistener("click", function DashboardResource_Conditional_3_Template_button_click_10_listener() { i0.ɵɵrestoreView(_r4); const ctx_r0 = i0.ɵɵnextContext(); return i0.ɵɵresetView(ctx_r0.saveDashboard()); });
105
- i0.ɵɵelement(11, "i", 32);
106
- i0.ɵɵtext(12, " Save ");
140
+ i0.ɵɵconditionalCreate(9, DashboardResource_Conditional_2_Conditional_9_Template, 15, 13, "as-split-area", 22);
107
141
  i0.ɵɵelementEnd();
108
- i0.ɵɵelementStart(13, "button", 33);
109
- i0.ɵɵlistener("click", function DashboardResource_Conditional_3_Template_button_click_13_listener() { i0.ɵɵrestoreView(_r4); const ctx_r0 = i0.ɵɵnextContext(); return i0.ɵɵresetView(ctx_r0.cancelEdit()); });
110
- i0.ɵɵtext(14, " Cancel ");
111
- i0.ɵɵelementEnd()()();
112
142
  } if (rf & 2) {
113
143
  const ctx_r0 = i0.ɵɵnextContext();
114
- i0.ɵɵadvance(7);
115
- i0.ɵɵtwoWayProperty("ngModel", ctx_r0.editingName);
144
+ i0.ɵɵproperty("gutterSize", ctx_r0.ChatOpen ? 6 : 0);
145
+ i0.ɵɵadvance();
146
+ i0.ɵɵproperty("size", ctx_r0.ChatOpen ? ctx_r0.CopilotExpanded ? 100 - ctx_r0.CopilotMaxPct : ctx_r0.MainSizePct : 100)("minSize", 100 - ctx_r0.CopilotMaxPct);
147
+ i0.ɵɵadvance();
148
+ i0.ɵɵproperty("Provider", ctx_r0.Provider)("DashboardId", ctx_r0.ConfigDashboardId);
149
+ i0.ɵɵadvance(2);
150
+ i0.ɵɵclassProp("is-active", ctx_r0.ChatOpen);
151
+ i0.ɵɵattribute("aria-expanded", ctx_r0.ChatOpen ? "true" : "false");
152
+ i0.ɵɵadvance(3);
153
+ i0.ɵɵproperty("IsPinned", ctx_r0.IsPinnedToHome)("CanShare", ctx_r0.CanShare);
154
+ i0.ɵɵadvance();
155
+ i0.ɵɵconditional(ctx_r0.CanShare ? 8 : -1);
116
156
  i0.ɵɵadvance();
117
- i0.ɵɵtwoWayProperty("ngModel", ctx_r0.editingDescription);
157
+ i0.ɵɵconditional(ctx_r0.ChatOpen ? 9 : -1);
158
+ } }
159
+ function DashboardResource_Conditional_5_Template(rf, ctx) { if (rf & 1) {
160
+ const _r6 = i0.ɵɵgetCurrentView();
161
+ i0.ɵɵelementStart(0, "mj-dashboard-share-dialog", 40);
162
+ i0.ɵɵlistener("Result", function DashboardResource_Conditional_5_Template_mj_dashboard_share_dialog_Result_0_listener($event) { i0.ɵɵrestoreView(_r6); const ctx_r0 = i0.ɵɵnextContext(); return i0.ɵɵresetView(ctx_r0.OnShareDialogResult($event)); });
163
+ i0.ɵɵelementEnd();
164
+ } if (rf & 2) {
165
+ const ctx_r0 = i0.ɵɵnextContext();
166
+ i0.ɵɵproperty("Visible", ctx_r0.ShowShareDialog)("Dashboard", ctx_r0.ConfigDashboard);
118
167
  } }
119
168
  function DashboardResource_Conditional_6_Template(rf, ctx) { if (rf & 1) {
120
- const _r5 = i0.ɵɵgetCurrentView();
121
- i0.ɵɵelementStart(0, "mj-dashboard-share-dialog", 34);
122
- i0.ɵɵlistener("Result", function DashboardResource_Conditional_6_Template_mj_dashboard_share_dialog_Result_0_listener($event) { i0.ɵɵrestoreView(_r5); const ctx_r0 = i0.ɵɵnextContext(); return i0.ɵɵresetView(ctx_r0.onShareDialogResult($event)); });
169
+ const _r7 = i0.ɵɵgetCurrentView();
170
+ i0.ɵɵelementStart(0, "mj-confirm-dialog", 41);
171
+ i0.ɵɵlistener("Confirmed", function DashboardResource_Conditional_6_Template_mj_confirm_dialog_Confirmed_0_listener() { i0.ɵɵrestoreView(_r7); const ctx_r0 = i0.ɵɵnextContext(); return i0.ɵɵresetView(ctx_r0.OnAgentConfirm(true)); })("Cancelled", function DashboardResource_Conditional_6_Template_mj_confirm_dialog_Cancelled_0_listener() { i0.ɵɵrestoreView(_r7); const ctx_r0 = i0.ɵɵnextContext(); return i0.ɵɵresetView(ctx_r0.OnAgentConfirm(false)); });
123
172
  i0.ɵɵelementEnd();
124
173
  } if (rf & 2) {
125
174
  const ctx_r0 = i0.ɵɵnextContext();
126
- i0.ɵɵproperty("Visible", ctx_r0.showShareDialog)("Dashboard", ctx_r0.configDashboard);
175
+ i0.ɵɵproperty("Visible", true)("Title", ctx_r0.AgentConfirm.Title)("Message", ctx_r0.AgentConfirm.Message)("ConfirmText", ctx_r0.AgentConfirm.ConfirmText);
127
176
  } }
177
+ function DashboardResource_Conditional_7_Template(rf, ctx) { if (rf & 1) {
178
+ const _r8 = i0.ɵɵgetCurrentView();
179
+ i0.ɵɵelementStart(0, "mj-confirm-dialog", 42);
180
+ i0.ɵɵlistener("Confirmed", function DashboardResource_Conditional_7_Template_mj_confirm_dialog_Confirmed_0_listener() { i0.ɵɵrestoreView(_r8); const ctx_r0 = i0.ɵɵnextContext(); return i0.ɵɵresetView(ctx_r0.OnCloseChatConfirmed()); })("Cancelled", function DashboardResource_Conditional_7_Template_mj_confirm_dialog_Cancelled_0_listener() { i0.ɵɵrestoreView(_r8); const ctx_r0 = i0.ɵɵnextContext(); return i0.ɵɵresetView(ctx_r0.OnCloseChatCancelled()); });
181
+ i0.ɵɵelementEnd();
182
+ } if (rf & 2) {
183
+ i0.ɵɵproperty("Visible", true);
184
+ } }
185
+ /** The permissions the tab reports while its editor shows no dashboard: nothing is allowed. */
186
+ const NO_DASHBOARD_PERMISSIONS = {
187
+ DashboardID: '', CanRead: false, CanEdit: false, CanDelete: false, CanShare: false, IsOwner: false, PermissionSource: 'none',
188
+ };
189
+ /** Why an agent's request or edit fails while the tab shows no Config dashboard. */
190
+ const NO_CONFIG_DASHBOARD = 'No Config dashboard is open in this tab.';
191
+ /** How long, in milliseconds, an agent's confirm dialog waits for the user's answer before it closes. */
192
+ const AGENT_CONFIRM_TIMEOUT_MS = 25_000;
193
+ /** Why an agent's request failed when the user gave no answer in time. */
194
+ const AGENT_CONFIRM_TIMED_OUT = `No answer within ${AGENT_CONFIRM_TIMEOUT_MS / 1000} s; the dialog was closed. Ask the user and try again.`;
195
+ /** The most artifacts the agent's source search reads. */
196
+ const SOURCE_SEARCH_MAX_ARTIFACTS = 500;
197
+ /** The most artifact IDs one filter of the latest-version lookup lists. */
198
+ const ARTIFACT_VERSION_FILTER_CHUNK = 200;
199
+ /** Why the tab refuses an agent's edit while the editor's Save runs. */
200
+ const SAVE_IN_PROGRESS = 'A save is in progress. Try again when it finishes.';
201
+ /**
202
+ * The Dashboards application (metadata/applications/.dashboards-application.json). The AI pane's
203
+ * conversations belong to it, and its default agent answers in the pane.
204
+ */
205
+ const DASHBOARDS_APPLICATION_ID = '4B439111-B492-4936-9E33-A428CB5725B4';
206
+ /** What the AI pane shows when the Dashboards application has no default agent of its own. */
207
+ const NO_DASHBOARD_ASSISTANT = 'No dashboard assistant is configured for the Dashboards app. Ask an administrator to set its default agent.';
128
208
  /**
129
209
  * Dashboard Resource Wrapper - displays a single dashboard in a tab
130
210
  * Extends BaseResourceComponent to work with the resource type system
131
- * Dynamically routes between code-based and config-based dashboards based on dashboard type
211
+ * Dynamically routes between code-based and config-based dashboards based on dashboard type:
212
+ * a Config dashboard shows in the shared dashboard editor (`mj-dashboard-editor`) with the AI pane
213
+ * beside it, and a Code dashboard or the Data Explorer shows in the tab's own container.
132
214
  */
133
215
  let DashboardResource = class DashboardResource extends BaseResourceComponent {
134
216
  viewContainer;
135
217
  cdr;
136
218
  componentRef = null;
219
+ recentAccess = inject(RecentAccessService);
220
+ homePins = inject(HomeAppPinService);
221
+ realtimeSession = inject(RealtimeSessionService);
222
+ artifactPermissions = inject(ArtifactPermissionService);
223
+ /** The editor that shows the Config dashboard. Undefined while the tab shows none. */
224
+ editor;
225
+ /** The panels last reported to the agent, as JSON. Null until the open dashboard is first reported. */
226
+ reportedPanelsKey = null;
137
227
  /**
138
- * A cache reattach moves this wrapper to another tab without recreating the dashboard inside it,
139
- * so the child's tab stamp — taken when we created it — has to move too. Without this the
140
- * dashboard keeps reading and writing the params of the tab it was born in, from inside a tab it
141
- * no longer belongs to.
228
+ * True once the tab registered the agent tools for the dashboard the editor loaded. Until then the
229
+ * tab reports no context, so the first report comes with the tools.
230
+ */
231
+ agentStatePublished = false;
232
+ /** True once the tab follows edit-mode requests (see watchEditModeRequests). */
233
+ watchingEditModeRequests = false;
234
+ /**
235
+ * A cache reattach moves this wrapper to another tab without recreating the dashboard inside it.
236
+ * A child resource component (a Code dashboard, the Data Explorer) has its own tab stamp, taken
237
+ * when we created it, and it has to move too. Without this the dashboard keeps reading and
238
+ * writing the params of the tab it was born in, from inside a tab it no longer belongs to. The
239
+ * editor has no tab of its own. The new tab can hold an edit-mode request (a card's Edit
240
+ * action reopens a closed tab this way), so the tab takes it here, where no load will.
142
241
  */
143
242
  onTabIdRebound(tabId) {
144
- this.rehomeChildToTab(this.componentRef?.instance, tabId);
243
+ const child = this.componentRef?.instance;
244
+ if (child instanceof BaseResourceComponent) {
245
+ this.rehomeChildToTab(child, tabId);
246
+ }
247
+ this.takeEditModeRequest(tabId);
145
248
  }
146
249
  dataLoaded = false;
250
+ /** The element that holds a Code dashboard or the Data Explorer. */
147
251
  ContainerElement;
148
252
  /** @deprecated Use {@link ContainerElement}. */
149
253
  get containerElement() {
@@ -165,67 +269,114 @@ let DashboardResource = class DashboardResource extends BaseResourceComponent {
165
269
  set errorDetails(value) {
166
270
  this.ErrorDetails = value;
167
271
  }
168
- /** Cached dashboard categories for breadcrumb navigation */
169
- categories = [];
170
- /** Reference to the dashboard viewer component (for config-based dashboards) */
171
- viewerInstance = null;
172
- /** The config-based dashboard entity (null for code-based dashboards) */
173
- ConfigDashboard = null;
272
+ /** The Config dashboard the editor shows. Null for a Code dashboard, the Data Explorer, or none. */
273
+ ConfigDashboardId = null;
274
+ /** The Config dashboard the editor shows, or null (a Code dashboard, the Data Explorer, or none). */
275
+ get ConfigDashboard() {
276
+ return this.editor?.Dashboard ?? null;
277
+ }
174
278
  /** @deprecated Use {@link ConfigDashboard}. */
175
279
  get configDashboard() {
176
280
  return this.ConfigDashboard;
177
281
  }
178
- /** @deprecated Use {@link ConfigDashboard}. */
179
- set configDashboard(value) {
180
- this.ConfigDashboard = value;
282
+ /** True while the editor is in edit mode. */
283
+ get IsEditMode() {
284
+ return this.editor?.IsEditing ?? false;
181
285
  }
182
- /** Whether we're in edit mode */
183
- IsEditMode = false;
184
286
  /** @deprecated Use {@link IsEditMode}. */
185
287
  get isEditMode() {
186
288
  return this.IsEditMode;
187
289
  }
188
- /** @deprecated Use {@link IsEditMode}. */
189
- set isEditMode(value) {
190
- this.IsEditMode = value;
191
- }
192
- /** Editing fields */
193
- EditingName = '';
194
- /** @deprecated Use {@link EditingName}. */
195
- get editingName() {
196
- return this.EditingName;
197
- }
198
- /** @deprecated Use {@link EditingName}. */
199
- set editingName(value) {
200
- this.EditingName = value;
201
- }
202
- EditingDescription = '';
203
- /** @deprecated Use {@link EditingDescription}. */
204
- get editingDescription() {
205
- return this.EditingDescription;
206
- }
207
- /** @deprecated Use {@link EditingDescription}. */
208
- set editingDescription(value) {
209
- this.EditingDescription = value;
210
- }
211
- /** Current user's permissions for this dashboard */
212
- DashboardPermissions = {
213
- DashboardID: '',
214
- CanRead: true,
215
- CanEdit: true,
216
- CanDelete: true,
217
- CanShare: true,
218
- IsOwner: true,
219
- PermissionSource: 'owner'
220
- };
221
- /** @deprecated Use {@link DashboardPermissions}. */
222
- get dashboardPermissions() {
223
- return this.DashboardPermissions;
224
- }
225
- /** @deprecated Use {@link DashboardPermissions}. */
226
- set dashboardPermissions(value) {
227
- this.DashboardPermissions = value;
290
+ /** True when the user may share the dashboard the editor shows. */
291
+ get CanShare() {
292
+ return this.editor?.Permissions.CanShare ?? false;
228
293
  }
294
+ /** The confirm dialog an agent's Request* tool opened, or null. The user, not the agent, saves or pins. */
295
+ AgentConfirm = null;
296
+ /** The request behind the open agent confirm dialog. Null while none is open. */
297
+ agentConfirmRequest = null;
298
+ // ── AI pane ──────────────────────────────────────────────────────
299
+ /** True while the AI pane shows beside the dashboard. */
300
+ ChatOpen = false;
301
+ /** True while the AI pane takes its largest width (CopilotMaxPct). */
302
+ CopilotExpanded = false;
303
+ /** The dashboard's width while the AI pane is open, in percent of the tab's width. */
304
+ MainSizePct = STUDIO_PANE_DEFAULTS.MainSizePct;
305
+ /** The AI pane's width, in percent of the tab's width. */
306
+ CopilotSizePct = STUDIO_PANE_DEFAULTS.CopilotSizePct;
307
+ /** The smallest width of the AI pane, in percent of the tab's width. */
308
+ CopilotMinPct = STUDIO_PANE_COPILOT_MIN_PCT;
309
+ /** The largest width of the AI pane, in percent of the tab's width. */
310
+ CopilotMaxPct = STUDIO_PANE_COPILOT_MAX_PCT;
311
+ /** True while the dialog that asks before a voice session ends with the AI pane is open. */
312
+ ShowCloseChatConfirm = false;
313
+ /** The agent the AI pane talks to: the Dashboards application's default agent. Null until it resolves. */
314
+ ChatAgentId = null;
315
+ /** The name of that agent, which the AI pane shows in its header. */
316
+ ChatAgentName = null;
317
+ /** Why the AI pane has no agent, or null. The pane then shows this instead of the chat. */
318
+ ChatAgentError = null;
319
+ /** The AI pane's conversation, from the moment its first message created it. */
320
+ ChatConversation = null;
321
+ /** The ID of the AI pane's conversation. */
322
+ ChatConversationId = null;
323
+ /** True until the first message creates the AI pane's conversation. */
324
+ ChatIsNewConversation = true;
325
+ /** The first message, which the chat area sends once it has created the conversation. Null after it is sent. */
326
+ ChatPendingMessage = null;
327
+ /** The files attached to the first message, sent with it. */
328
+ ChatPendingAttachments = null;
329
+ /** The shell's app context snapshot, as the chat area's AppContext input takes it. The agent reads it. */
330
+ ChatAppContext = null;
331
+ /** The application the AI pane's conversations belong to, and whose default agent answers in it. */
332
+ ChatApplicationId = DASHBOARDS_APPLICATION_ID;
333
+ /** True while the AI pane resolves its agent. */
334
+ resolvingChatAgent = false;
335
+ /** The environment of the AI pane's conversations and of the artifacts the agent searches: the tab's, else the default. */
336
+ get ChatEnvironmentId() {
337
+ const environmentId = this.Data?.Configuration?.['environmentId'];
338
+ return typeof environmentId === 'string' && environmentId ? environmentId : MJEnvironmentEntityExtended.DefaultEnvironmentID;
339
+ }
340
+ // ── Members the editor owns ──────────────────────────────────────
341
+ /** @deprecated Use {@link DashboardEditorComponent.EditingName}. */
342
+ get EditingName() { return this.editor?.EditingName ?? ''; }
343
+ /** @deprecated Use {@link DashboardEditorComponent.EditingName}. */
344
+ set EditingName(value) { if (this.editor)
345
+ this.editor.EditingName = value; }
346
+ /** @deprecated Use {@link DashboardEditorComponent.EditingName}. */
347
+ get editingName() { return this.EditingName; }
348
+ /** @deprecated Use {@link DashboardEditorComponent.EditingName}. */
349
+ set editingName(value) { this.EditingName = value; }
350
+ /** @deprecated Use {@link DashboardEditorComponent.EditingDescription}. */
351
+ get EditingDescription() { return this.editor?.EditingDescription ?? ''; }
352
+ /** @deprecated Use {@link DashboardEditorComponent.EditingDescription}. */
353
+ set EditingDescription(value) { if (this.editor)
354
+ this.editor.EditingDescription = value; }
355
+ /** @deprecated Use {@link DashboardEditorComponent.EditingDescription}. */
356
+ get editingDescription() { return this.EditingDescription; }
357
+ /** @deprecated Use {@link DashboardEditorComponent.EditingDescription}. */
358
+ set editingDescription(value) { this.EditingDescription = value; }
359
+ /** @deprecated Use {@link DashboardEditorComponent.Permissions}. */
360
+ get DashboardPermissions() { return this.editor?.Permissions ?? NO_DASHBOARD_PERMISSIONS; }
361
+ /** @deprecated Use {@link DashboardEditorComponent.Permissions}. */
362
+ get dashboardPermissions() { return this.DashboardPermissions; }
363
+ /** @deprecated Use {@link DashboardEditorComponent.ToggleEditMode}. */
364
+ ToggleEditMode() { this.editor?.ToggleEditMode(); }
365
+ /** @deprecated Use {@link DashboardEditorComponent.ToggleEditMode}. */
366
+ toggleEditMode() { this.ToggleEditMode(); }
367
+ /** @deprecated Use {@link DashboardEditorComponent.CancelEdit}. */
368
+ CancelEdit() { this.editor?.CancelEdit(); }
369
+ /** @deprecated Use {@link DashboardEditorComponent.CancelEdit}. */
370
+ cancelEdit() { this.CancelEdit(); }
371
+ /** @deprecated Use {@link DashboardEditorComponent.SaveDashboard}. */
372
+ SaveDashboard() { return this.editor?.SaveDashboard() ?? Promise.resolve(); }
373
+ /** @deprecated Use {@link DashboardEditorComponent.SaveDashboard}. */
374
+ saveDashboard() { return this.SaveDashboard(); }
375
+ /** @deprecated Use {@link DashboardEditorComponent.OpenAddPartDialog}. */
376
+ OpenAddPartDialog() { this.editor?.OpenAddPartDialog(); }
377
+ /** @deprecated Use {@link DashboardEditorComponent.OpenAddPartDialog}. */
378
+ openAddPartDialog() { this.OpenAddPartDialog(); }
379
+ // ── Share and pin ────────────────────────────────────────────────
229
380
  /** Whether the share dialog is visible */
230
381
  ShowShareDialog = false;
231
382
  /** @deprecated Use {@link ShowShareDialog}. */
@@ -236,6 +387,10 @@ let DashboardResource = class DashboardResource extends BaseResourceComponent {
236
387
  set showShareDialog(value) {
237
388
  this.ShowShareDialog = value;
238
389
  }
390
+ /** True when the open Config dashboard is pinned to Home. */
391
+ get IsPinnedToHome() {
392
+ return this.readPlacement(this.ConfigDashboard?.ID, id => this.homePins.IsPinned(DASHBOARD_PIN_RESOURCE_TYPE, { dashboardId: id }));
393
+ }
239
394
  /**
240
395
  * Sets the error state with a user-friendly message and optional technical details
241
396
  */
@@ -276,8 +431,9 @@ let DashboardResource = class DashboardResource extends BaseResourceComponent {
276
431
  this.componentRef = null;
277
432
  }
278
433
  this.clearError();
279
- this.ConfigDashboard = null;
280
- this.viewerInstance = null;
434
+ this.abortAgentConfirm('The tab opened another dashboard before the user answered.');
435
+ this.ConfigDashboardId = null;
436
+ this.agentStatePublished = false;
281
437
  this.loadDashboard();
282
438
  }
283
439
  }
@@ -285,97 +441,123 @@ let DashboardResource = class DashboardResource extends BaseResourceComponent {
285
441
  get Data() {
286
442
  return super.Data;
287
443
  }
444
+ ngOnInit() {
445
+ super.ngOnInit();
446
+ this.watchAppContext();
447
+ }
288
448
  ngOnDestroy() {
449
+ this.abortAgentConfirm('The dashboard tab was closed before the user answered.');
289
450
  super.ngOnDestroy();
290
451
  if (this.componentRef) {
291
452
  this.componentRef.destroy();
292
453
  }
293
454
  }
294
455
  // ========================================
295
- // Edit Mode Methods
456
+ // The Editor
296
457
  // ========================================
297
458
  /**
298
- * Toggle between view and edit mode
459
+ * Shows a Config dashboard in the editor, which reads it from DashboardEngine. The editor's Loaded
460
+ * or LoadFailed event finishes the load (OnEditorLoaded, OnEditorLoadFailed).
299
461
  */
300
- ToggleEditMode() {
301
- if (this.IsEditMode) {
302
- this.CancelEdit();
303
- }
304
- else {
305
- this.enterEditMode();
306
- }
307
- }
308
- /** @deprecated Use {@link ToggleEditMode}. */
309
- toggleEditMode() {
310
- return this.ToggleEditMode();
462
+ showConfigDashboard(dashboard) {
463
+ this.ContainerElement.nativeElement.innerHTML = '';
464
+ this.reportedPanelsKey = null;
465
+ this.ConfigDashboardId = dashboard.ID;
466
+ void this.loadHomePins();
467
+ this.watchEditModeRequests();
468
+ SafeDetectChanges(this.cdr);
311
469
  }
312
470
  /**
313
- * Enter edit mode
471
+ * The editor shows the dashboard and its layout is ready. The loading screen goes, the tab takes an
472
+ * edit-mode request (a new dashboard, or a card's Edit action) when it has one, and the agent gets
473
+ * its tools and the dashboard. NavigationService removes the request when the tab takes it, so a
474
+ * later load of the tab opens for viewing.
314
475
  */
315
- enterEditMode() {
316
- if (!this.ConfigDashboard)
317
- return;
318
- this.IsEditMode = true;
319
- this.EditingName = this.ConfigDashboard.Name;
320
- this.EditingDescription = this.ConfigDashboard.Description || '';
321
- // Tell the viewer to enter edit mode
322
- if (this.viewerInstance) {
323
- this.viewerInstance.isEditing = true;
476
+ OnEditorLoaded(dashboard) {
477
+ this.NotifyLoadComplete();
478
+ if (this.navigationService.TakeDashboardEditModeRequest(this.getTabId(), dashboard.ID, this.tabApplicationId())) {
479
+ this.editor?.EnterEditMode();
324
480
  }
325
- this.cdr.detectChanges();
481
+ SafeDetectChanges(this.cdr);
482
+ this.publishAgentState();
483
+ }
484
+ /** The editor could not show the dashboard: the tab shows why, and the loading screen goes. */
485
+ OnEditorLoadFailed(error) {
486
+ const name = this.cachedDashboardName(error.DashboardId);
487
+ const subject = name ? `The dashboard "${name}"` : 'The dashboard';
488
+ this.setError(`${subject} could not be loaded. There may be an issue with the dashboard configuration.`, error.Message);
489
+ this.NotifyLoadComplete();
490
+ SafeDetectChanges(this.cdr);
326
491
  }
327
492
  /**
328
- * Cancel edit mode and discard changes
493
+ * The dashboard's saved name changed, by a save here or elsewhere. The tab takes the name as its
494
+ * title and keeps its dashboard ID: ResourceRecordSaved would rewrite the record ID in a form the
495
+ * tab configuration does not have, and the tab would reload on each Save.
329
496
  */
330
- CancelEdit() {
331
- this.IsEditMode = false;
332
- // Tell the viewer to exit edit mode
333
- if (this.viewerInstance) {
334
- this.viewerInstance.isEditing = false;
497
+ OnEditorNameChanged(name) {
498
+ if (name) {
499
+ this.NotifyDisplayNameChanged(name);
335
500
  }
336
- this.cdr.detectChanges();
337
501
  }
338
- /** @deprecated Use {@link CancelEdit}. */
339
- cancelEdit() {
340
- return this.CancelEdit();
502
+ /** The editor entered or left edit mode: the tab reports it to the agent. */
503
+ OnEditorEditingChange() {
504
+ this.emitAgentContext();
505
+ }
506
+ /** The editor's parts or layout changed: the tab reports the panels to the agent when they changed. */
507
+ OnEditorConfigChanged() {
508
+ this.onViewerConfigChanged();
509
+ }
510
+ /** The editor rebuilt the saved layout after a save elsewhere: the rebuild reports nothing to the agent. */
511
+ OnEditorReloaded() {
512
+ this.treatPanelsAsReported();
513
+ }
514
+ /** The user starred or unstarred the dashboard in the editor. */
515
+ OnEditorFavoriteChange() {
516
+ this.onPlacementChanged();
517
+ }
518
+ /** A part of the dashboard asked to open something: a record, another dashboard, a query. */
519
+ OnEditorNavigationRequested(event) {
520
+ this.handleNavigationRequest(event);
341
521
  }
342
522
  /**
343
- * Save dashboard changes
523
+ * Takes the tab's edit-mode request, if it has one for this tab's dashboard, and enters edit mode in
524
+ * the editor. A tab that has not finished loading leaves the request for its load (see
525
+ * OnEditorLoaded). A tab that is already editing keeps its changes. A request for what the tab shows
526
+ * now stays for that tab's component: a cached component can still be bound to a tab ID that OpenTab
527
+ * gave to another dashboard, or to this dashboard in another application.
344
528
  */
345
- async SaveDashboard() {
346
- if (!this.ConfigDashboard || !this.viewerInstance)
529
+ takeEditModeRequest(tabId) {
530
+ const editor = this.editor;
531
+ const dashboard = editor?.Dashboard;
532
+ if (!this.LoadComplete || !editor || !dashboard || !UUIDsEqual(dashboard.ID, this.ConfigDashboardId))
347
533
  return;
348
- try {
349
- // Update dashboard name and description
350
- this.ConfigDashboard.Name = this.EditingName;
351
- this.ConfigDashboard.Description = this.EditingDescription;
352
- // Save via the viewer (which handles layout saving)
353
- await this.viewerInstance.save();
354
- // Exit edit mode
355
- this.IsEditMode = false;
356
- this.viewerInstance.isEditing = false;
357
- this.cdr.detectChanges();
358
- }
359
- catch (error) {
360
- console.error('Error saving dashboard:', error);
534
+ if (this.navigationService.TakeDashboardEditModeRequest(tabId, dashboard.ID, this.tabApplicationId()) && !editor.IsEditing) {
535
+ editor.EnterEditMode();
361
536
  }
362
537
  }
363
- /** @deprecated Use {@link SaveDashboard}. */
364
- async saveDashboard() {
365
- return this.SaveDashboard();
538
+ /** The application of the tab this component was loaded for, from the resource data the tab container gives it. */
539
+ tabApplicationId() {
540
+ const applicationId = this.Data?.Configuration?.['applicationId'];
541
+ return typeof applicationId === 'string' ? applicationId : '';
366
542
  }
367
543
  /**
368
- * Open the add panel dialog
544
+ * Enters edit mode when OpenDashboard asks this open tab to (NavigationService.DashboardEditModeRequested$).
545
+ * Starts once per tab, and stops when the tab is destroyed.
369
546
  */
370
- OpenAddPartDialog() {
371
- if (this.viewerInstance) {
372
- // Trigger the viewer's add panel flow
373
- this.viewerInstance.onAddPanelClick();
374
- }
547
+ watchEditModeRequests() {
548
+ if (this.watchingEditModeRequests)
549
+ return;
550
+ this.watchingEditModeRequests = true;
551
+ this.navigationService.DashboardEditModeRequested$.pipe(filter(tabId => UUIDsEqual(tabId, this.getTabId())), takeUntil(this.destroy$)).subscribe(tabId => this.takeEditModeRequest(tabId));
375
552
  }
376
- /** @deprecated Use {@link OpenAddPartDialog}. */
377
- openAddPartDialog() {
378
- return this.OpenAddPartDialog();
553
+ /**
554
+ * Takes the viewer's current panels as the ones last reported to the agent, so the change events
555
+ * of a reloaded layout report nothing. Does nothing before the first report.
556
+ */
557
+ treatPanelsAsReported() {
558
+ if (this.reportedPanelsKey !== null) {
559
+ this.reportedPanelsKey = JSON.stringify(this.livePanels());
560
+ }
379
561
  }
380
562
  /**
381
563
  * Open the share dialog for this dashboard
@@ -400,16 +582,655 @@ let DashboardResource = class DashboardResource extends BaseResourceComponent {
400
582
  return this.CloseShareDialog();
401
583
  }
402
584
  /**
403
- * Handle share dialog result
585
+ * Handle share dialog result. After a save the editor reads the user's permissions again, since
586
+ * sharing can change them.
404
587
  */
405
588
  OnShareDialogResult(result) {
406
589
  this.ShowShareDialog = false;
407
- if (result.Action === 'save' && this.ConfigDashboard) {
408
- // Recompute permissions after sharing changes
409
- const md = this.ProviderToUse;
410
- this.DashboardPermissions = DashboardEngine.Instance.GetDashboardPermissions(this.ConfigDashboard.ID, md.CurrentUser.ID);
590
+ if (result.Action === 'save') {
591
+ this.editor?.RefreshPermissions();
411
592
  }
412
593
  this.cdr.detectChanges();
594
+ if (result.Action === 'save') {
595
+ this.emitAgentContext();
596
+ }
597
+ }
598
+ // ========================================
599
+ // Add to menu
600
+ // ========================================
601
+ /** Pins the open dashboard to Home as a card, then adds a thumbnail of it in the background. */
602
+ async PinToHome() {
603
+ const dashboard = this.ConfigDashboard;
604
+ if (!dashboard)
605
+ return;
606
+ try {
607
+ await this.homePins.LoadPins();
608
+ if (this.homePins.AddPin(this.homePinFor(dashboard))) {
609
+ MJNotificationService.Instance.CreateSimpleNotification(`Pinned "${dashboard.Name}" to Home`, 'success', 2000);
610
+ void this.attachPinThumbnail(dashboard.ID);
611
+ }
612
+ else {
613
+ MJNotificationService.Instance.CreateSimpleNotification(`"${dashboard.Name}" is already pinned to Home`, 'info', 2000);
614
+ }
615
+ }
616
+ catch (error) {
617
+ LogError(`Dashboard tab: could not pin the dashboard to Home: ${errorMessage(error)}`);
618
+ MJNotificationService.Instance.CreateSimpleNotification('Could not pin the dashboard to Home', 'error', 3000);
619
+ }
620
+ this.onPlacementChanged();
621
+ }
622
+ /**
623
+ * Reads one placement flag (the pin) for a dashboard. False without a dashboard,
624
+ * or when the user cannot read that data (the engine throws PermissionConstrainedError).
625
+ */
626
+ readPlacement(dashboardId, read) {
627
+ if (!dashboardId)
628
+ return false;
629
+ try {
630
+ return read(dashboardId);
631
+ }
632
+ catch {
633
+ return false;
634
+ }
635
+ }
636
+ /** The Home pin for a dashboard, in the shared dashboard pin shape. */
637
+ homePinFor(dashboard) {
638
+ return BuildDashboardPinInput(dashboard);
639
+ }
640
+ /**
641
+ * Adds a thumbnail of the dashboard (the editor's body) to its pin. Best effort: without one, Home
642
+ * shows the pin's icon. Logs a failure and never rejects, because the caller does not wait for it.
643
+ */
644
+ async attachPinThumbnail(dashboardId) {
645
+ try {
646
+ const element = this.editor?.BodyElement;
647
+ const thumbnail = element ? await this.homePins.CaptureThumbnail(element) : undefined;
648
+ const pin = thumbnail ? this.homePins.FindPin(DASHBOARD_PIN_RESOURCE_TYPE, { dashboardId }) : undefined;
649
+ if (pin && thumbnail) {
650
+ this.homePins.UpdatePin(pin.Id, { Thumbnail: thumbnail });
651
+ }
652
+ }
653
+ catch (error) {
654
+ LogError(`Dashboard tab: could not add a thumbnail to the Home pin: ${errorMessage(error)}`);
655
+ }
656
+ }
657
+ /** Loads the user's Home pins, so the Add to menu can show whether this dashboard is pinned. */
658
+ async loadHomePins() {
659
+ try {
660
+ await this.homePins.LoadPins();
661
+ }
662
+ catch (error) {
663
+ LogError(`Dashboard tab: could not load the Home pins: ${errorMessage(error)}`);
664
+ }
665
+ }
666
+ /** Updates the header buttons and reports the new placement to the agent. */
667
+ onPlacementChanged() {
668
+ SafeDetectChanges(this.cdr);
669
+ this.emitAgentContext();
670
+ }
671
+ // ========================================
672
+ // AI Pane
673
+ // ========================================
674
+ /** Opens the AI pane with the widths the user saved, or closes it as CloseChat does. */
675
+ ToggleChat() {
676
+ if (this.ChatOpen) {
677
+ this.CloseChat();
678
+ return;
679
+ }
680
+ this.applyPanePrefs(this.savedPanePrefs());
681
+ this.ChatOpen = true;
682
+ void this.ensureChatAgentResolved();
683
+ SafeDetectChanges(this.cdr);
684
+ }
685
+ /**
686
+ * Closes the AI pane. Its conversation stays, so the pane shows it again when it opens. Closing the pane
687
+ * ends a voice session, so while one runs the user confirms first (OnCloseChatConfirmed).
688
+ */
689
+ CloseChat() {
690
+ if (this.realtimeSession.IsActive) {
691
+ this.ShowCloseChatConfirm = true;
692
+ }
693
+ else {
694
+ this.closeChatPane();
695
+ }
696
+ SafeDetectChanges(this.cdr);
697
+ }
698
+ /** The user confirmed: the AI pane closes, and the voice session ends with it. */
699
+ OnCloseChatConfirmed() {
700
+ this.ShowCloseChatConfirm = false;
701
+ this.closeChatPane();
702
+ SafeDetectChanges(this.cdr);
703
+ }
704
+ /** The user keeps the AI pane open, and the voice session with it. */
705
+ OnCloseChatCancelled() {
706
+ this.ShowCloseChatConfirm = false;
707
+ SafeDetectChanges(this.cdr);
708
+ }
709
+ /** Gives the AI pane its largest width, or its own width again. */
710
+ ToggleCopilotExpanded() {
711
+ this.CopilotExpanded = !this.CopilotExpanded;
712
+ SafeDetectChanges(this.cdr);
713
+ }
714
+ /**
715
+ * Keeps the widths the user dragged the split to, in this tab and in the user's settings. The AI pane then
716
+ * has that width, not its largest one. Sizes that are not two numbers change nothing.
717
+ */
718
+ OnCopilotSplitDragEnd(sizes) {
719
+ const prefs = PrefsFromSplitSizes(sizes);
720
+ if (!prefs)
721
+ return;
722
+ this.applyPanePrefs(prefs);
723
+ this.CopilotExpanded = false;
724
+ UserInfoEngine.Instance.SetSettingDebounced(STUDIO_PANE_SETTING_KEY, JSON.stringify(prefs));
725
+ SafeDetectChanges(this.cdr);
726
+ }
727
+ /**
728
+ * The chat area created the AI pane's conversation for the first message. The pane keeps the conversation
729
+ * and gives the message back to the chat area, which then sends it in that conversation.
730
+ */
731
+ OnChatConversationCreated(event) {
732
+ this.ChatConversation = event.conversation;
733
+ this.ChatConversationId = event.conversation.ID;
734
+ this.ChatIsNewConversation = false;
735
+ this.ChatPendingMessage = event.pendingMessage ?? null;
736
+ this.ChatPendingAttachments = event.pendingAttachments ?? null;
737
+ SafeDetectChanges(this.cdr);
738
+ }
739
+ /**
740
+ * A voice call started before any message, so the server created the AI pane's conversation. When the
741
+ * call ends, the pane adopts that conversation, so the chat area shows the call's transcript and the
742
+ * next message goes to it. The start of the call reports the same conversation without `select`; the
743
+ * pane leaves the chat area alone until the call ends, as the Conversations app does.
744
+ */
745
+ async OnChatRealtimeConversationReady(event) {
746
+ if (!event.select || !event.conversationId || this.ChatConversationId)
747
+ return;
748
+ const conversation = await this.loadChatConversation(event.conversationId);
749
+ if (!conversation || this.ChatConversationId)
750
+ return;
751
+ this.ChatConversation = conversation;
752
+ this.ChatConversationId = conversation.ID;
753
+ this.ChatIsNewConversation = false;
754
+ SafeDetectChanges(this.cdr);
755
+ }
756
+ /** The conversation with `conversationId` that the user can read, or null. */
757
+ async loadChatConversation(conversationId) {
758
+ const result = await RunView.FromMetadataProvider(this.ProviderToUse).RunView({
759
+ EntityName: 'MJ: Conversations',
760
+ ExtraFilter: `ID='${EscapeSQLString(conversationId)}'`,
761
+ ResultType: 'entity_object',
762
+ });
763
+ if (!result.Success) {
764
+ LogError(`DashboardResource: the AI pane could not load conversation ${conversationId}: ${result.ErrorMessage}`);
765
+ return null;
766
+ }
767
+ return result.Results?.[0] ?? null;
768
+ }
769
+ /** The chat area sent the first message. The pane drops it, so the chat area does not send it again. */
770
+ OnChatPendingMessageConsumed() {
771
+ this.ChatPendingMessage = null;
772
+ this.ChatPendingAttachments = null;
773
+ SafeDetectChanges(this.cdr);
774
+ }
775
+ /** Opens the app page that an artifact in the chat asks for. An app the metadata does not name opens the page in the current app. */
776
+ OnChatNavigationRequest(request) {
777
+ const appId = request.appName ? this.applicationIdByName(request.appName) : undefined;
778
+ void this.navigationService.OpenNavItemByName(request.navItemName, undefined, appId, { queryParams: request.queryParams })
779
+ .catch((error) => LogError(`Dashboard tab: could not open ${request.navItemName}: ${errorMessage(error)}`));
780
+ }
781
+ /** Opens a record that the chat links to. */
782
+ OnChatOpenEntityRecord(event) {
783
+ this.navigationService.OpenEntityRecord(event.entityName, event.compositeKey);
784
+ }
785
+ /** Keeps the shell's latest app context snapshot for the chat, until the tab is destroyed. Renders only while the pane is open. */
786
+ watchAppContext() {
787
+ this.navigationService.AppContextSnapshot$.pipe(takeUntil(this.destroy$)).subscribe(snapshot => {
788
+ this.ChatAppContext = snapshot ? { ...snapshot } : null;
789
+ if (this.ChatOpen) {
790
+ SafeDetectChanges(this.cdr);
791
+ }
792
+ });
793
+ }
794
+ /** The AI pane widths in the user's settings, or the defaults when the settings cannot be read. */
795
+ savedPanePrefs() {
796
+ try {
797
+ return ParseStudioPanePrefs(UserInfoEngine.Instance.GetSetting(STUDIO_PANE_SETTING_KEY));
798
+ }
799
+ catch {
800
+ return ParseStudioPanePrefs(null); // the user cannot read their settings
801
+ }
802
+ }
803
+ applyPanePrefs(prefs) {
804
+ this.MainSizePct = prefs.MainSizePct;
805
+ this.CopilotSizePct = prefs.CopilotSizePct;
806
+ }
807
+ closeChatPane() {
808
+ this.ChatOpen = false;
809
+ this.CopilotExpanded = false;
810
+ }
811
+ /** The ID of the application with this name, in any case, or undefined when the metadata has none. */
812
+ applicationIdByName(appName) {
813
+ const name = appName.trim().toLowerCase();
814
+ return this.ProviderToUse.Applications.find(app => app.Name.trim().toLowerCase() === name)?.ID;
815
+ }
816
+ /**
817
+ * Resolves the agent the AI pane talks to: the Dashboards application's default agent
818
+ * (Application.AgentSettings.DefaultAgentID). When the resolver falls back to Sage, the application has no
819
+ * agent of its own, and the pane says so instead of showing a chat. After a failure the next open tries again.
820
+ */
821
+ async ensureChatAgentResolved() {
822
+ if (this.ChatAgentId || this.resolvingChatAgent)
823
+ return;
824
+ this.resolvingChatAgent = true;
825
+ this.ChatAgentError = null;
826
+ try {
827
+ const provider = this.ProviderToUse;
828
+ const agent = await new DefaultAgentResolver().Resolve({ applicationId: this.ChatApplicationId, contextUser: provider.CurrentUser, provider });
829
+ if (agent.Name === DefaultAgentResolver.FALLBACK_AGENT_NAME) {
830
+ this.ChatAgentError = NO_DASHBOARD_ASSISTANT;
831
+ }
832
+ else {
833
+ this.ChatAgentId = agent.ID;
834
+ this.ChatAgentName = agent.Name;
835
+ }
836
+ }
837
+ catch (error) {
838
+ LogError(`Dashboard tab: could not resolve the dashboard assistant: ${errorMessage(error)}`);
839
+ this.ChatAgentError = `The dashboard assistant could not be loaded: ${errorMessage(error)}`;
840
+ }
841
+ finally {
842
+ this.resolvingChatAgent = false;
843
+ }
844
+ SafeDetectChanges(this.cdr);
845
+ }
846
+ // ========================================
847
+ // Agent Context & Client Tools
848
+ //
849
+ // 🔒 BOUNDARY: the tab's agent tools change the open dashboard in memory only. Save, Share,
850
+ // Favorite and Pin stay user actions; the Request* tools in dashboard-studio-tools.ts ask
851
+ // for them through a confirm dialog and never perform them.
852
+ // A Code dashboard is its own resource component and reports its own context and tools, so
853
+ // this tab reports nothing for it.
854
+ // ========================================
855
+ /**
856
+ * Registers the tab's tools and reports the open Config dashboard to the agent. The studio tools
857
+ * of one registration share an edit queue, so the tab registers them once for each dashboard load.
858
+ */
859
+ publishAgentState() {
860
+ this.navigationService.SetAgentClientTools(this, [
861
+ ...BuildDashboardTabAgentTools(this.agentHost()),
862
+ ...BuildDashboardStudioTools(this.studioHost()),
863
+ ]);
864
+ this.agentStatePublished = true;
865
+ this.emitAgentContext();
866
+ }
867
+ /**
868
+ * Reports the Config dashboard the editor shows to the agent. Does nothing while none is shown, and
869
+ * before the tab registered the tools for it: an edit-mode request taken on the load reports nothing
870
+ * of its own, so the load reports once.
871
+ */
872
+ emitAgentContext() {
873
+ const editor = this.editor;
874
+ const dashboard = editor?.Dashboard;
875
+ if (!this.agentStatePublished || !editor || !dashboard)
876
+ return;
877
+ const panels = this.livePanels();
878
+ this.reportedPanelsKey = JSON.stringify(panels);
879
+ this.navigationService.SetAgentContext(this, BuildDashboardTabAgentContext({
880
+ Dashboard: dashboard,
881
+ IsEditing: editor.IsEditing,
882
+ CanEdit: editor.CanEdit,
883
+ Panels: panels,
884
+ IsFavorite: editor.IsFavorite,
885
+ IsPinnedToHome: this.IsPinnedToHome,
886
+ }));
887
+ }
888
+ /** Reports the dashboard again when its panels change. A layout move or resize reports nothing. */
889
+ onViewerConfigChanged() {
890
+ if (this.reportedPanelsKey !== null && JSON.stringify(this.livePanels()) !== this.reportedPanelsKey) {
891
+ this.emitAgentContext();
892
+ }
893
+ }
894
+ /** The reads the agent tools make from this tab. */
895
+ agentHost() {
896
+ return {
897
+ CurrentDashboard: () => this.ConfigDashboard,
898
+ AccessibleDashboards: () => DashboardEngine.Instance.GetAccessibleDashboards(this.ProviderToUse.CurrentUser.ID),
899
+ Panels: dashboard => this.isOpenDashboard(dashboard) ? this.livePanels() : this.savedPanels(dashboard),
900
+ Permissions: dashboardId => DashboardEngine.Instance.GetDashboardPermissions(dashboardId, this.ProviderToUse.CurrentUser.ID),
901
+ };
902
+ }
903
+ isOpenDashboard(dashboard) {
904
+ return !!this.ConfigDashboard && UUIDsEqual(dashboard.ID, this.ConfigDashboard.ID);
905
+ }
906
+ /**
907
+ * The open dashboard's panels, read from the editor's viewer and its live layout, so unsaved edits
908
+ * count. Empty while the viewer is not ready. Best effort: a failed read never stops the context report.
909
+ */
910
+ livePanels() {
911
+ const viewer = this.editor?.Viewer;
912
+ if (!viewer)
913
+ return [];
914
+ try {
915
+ return SummarizeDashboardPanels(ExtractPanelsFromLayout(viewer.getConfig()?.layout ?? null), viewer.GetPartTypes());
916
+ }
917
+ catch {
918
+ return [];
919
+ }
920
+ }
921
+ /** The panels saved in a dashboard's layout. Empty when the layout or the part types cannot be read. */
922
+ savedPanels(dashboard) {
923
+ try {
924
+ const config = SafeJSONParse(dashboard.UIConfigDetails);
925
+ return SummarizeDashboardPanels(ExtractPanelsFromLayout(config?.layout ?? null), DashboardEngine.Instance.DashboardPartTypes);
926
+ }
927
+ catch {
928
+ return [];
929
+ }
930
+ }
931
+ // ========================================
932
+ // Studio Tools Host
933
+ // ========================================
934
+ /**
935
+ * The reads and edits the studio tools make on this tab, through its editor, and the requests they
936
+ * put to the user. DashboardStudioHost says what each member must do.
937
+ */
938
+ studioHost() {
939
+ return {
940
+ Dashboard: () => {
941
+ const dashboard = this.ConfigDashboard;
942
+ return dashboard ? { ID: dashboard.ID, Name: dashboard.Name, Description: dashboard.Description ?? null } : null;
943
+ },
944
+ GetConfig: () => this.editor?.Viewer?.getConfig() ?? null,
945
+ GetPartTypes: () => this.editor?.PartTypes ?? [],
946
+ IsEditing: () => this.editor?.IsEditing ?? false,
947
+ CanEdit: () => this.editor?.CanEdit ?? false,
948
+ HasUnsavedChanges: () => this.editor?.Viewer?.HasUnsavedChanges ?? false,
949
+ IsSaving: () => this.editor?.IsSaving ?? false,
950
+ EnterEditMode: () => this.editor?.EnterEditMode() ?? false,
951
+ ...this.studioEdits(),
952
+ GetPanelPath: panelId => this.editor?.Viewer?.GetPanelPath(panelId) ?? null,
953
+ CaptureScreenshot: maxWidth => this.editor
954
+ ? CaptureElementJpeg(this.editor.BodyElement, { maxWidth })
955
+ : Promise.reject(new Error(NO_CONFIG_DASHBOARD)),
956
+ PanelBounds: () => this.panelBounds(),
957
+ IsVoiceSessionActive: () => this.realtimeSession.IsActive,
958
+ SendVoiceFrame: (base64, mimeType) => this.realtimeSession.SendVideoFrame(base64, mimeType),
959
+ Confirm: (kind, detail) => this.askAgentConfirm(kind, detail),
960
+ SearchSources: (query, kinds, limit) => this.searchSources(query, kinds, limit),
961
+ };
962
+ }
963
+ /**
964
+ * The studio host's edits. Each runs through runAgentEdit, so it rejects with the reason when it
965
+ * cannot be made, and an edit the viewer can skip without an error is checked afterwards.
966
+ */
967
+ studioEdits() {
968
+ return {
969
+ AddPanel: (partTypeId, config, title, icon, position) => this.runAgentEdit('The assistant could not add the part', viewer => viewer.AddPanel(partTypeId, config, title, icon, position)),
970
+ RemovePanel: panelId => this.runAgentEdit('The assistant could not remove the part', viewer => {
971
+ viewer.RemovePanel(panelId);
972
+ if (viewer.GetPanel(panelId))
973
+ throw new Error('The dashboard did not remove the panel.');
974
+ }),
975
+ UpdatePanelConfig: (panelId, config, title, icon) => this.runAgentEdit('The assistant could not change the part', async (viewer) => {
976
+ await viewer.UpdatePanelConfig(panelId, config, title, icon);
977
+ if (JSON.stringify(viewer.GetPanel(panelId)?.config) !== JSON.stringify(config)) {
978
+ throw new Error('The dashboard did not change the panel.');
979
+ }
980
+ }),
981
+ ApplyLayout: layout => this.runAgentEdit('The assistant could not change the layout', async (viewer) => {
982
+ if (!viewer.getConfig())
983
+ throw new Error('The dashboard has not loaded its layout yet.');
984
+ await viewer.ApplyLayout(layout);
985
+ }),
986
+ };
987
+ }
988
+ /**
989
+ * Makes one agent edit on the editor's viewer, then reports the dashboard's parts to the agent again.
990
+ * The edit waits until the viewer's layout is ready: entering edit mode can reload the saved
991
+ * dashboard. Rejects with an Error that says why when no viewer is open, the editor's Save runs, or
992
+ * the edit fails; the user also sees a failed edit.
993
+ */
994
+ async runAgentEdit(failureMessage, edit) {
995
+ const viewer = this.editor?.Viewer;
996
+ if (!viewer)
997
+ throw new Error(NO_CONFIG_DASHBOARD);
998
+ try {
999
+ await viewer.WaitForLayoutReady();
1000
+ if (this.editor?.IsSaving)
1001
+ throw new Error(SAVE_IN_PROGRESS);
1002
+ return await edit(viewer);
1003
+ }
1004
+ catch (error) {
1005
+ LogError(`Dashboard tab: ${failureMessage}: ${errorMessage(error)}`);
1006
+ MJNotificationService.Instance.CreateSimpleNotification(failureMessage, 'error', 3000);
1007
+ throw error instanceof Error ? error : new Error(errorMessage(error));
1008
+ }
1009
+ finally {
1010
+ this.onViewerConfigChanged();
1011
+ }
1012
+ }
1013
+ /**
1014
+ * The box of each panel the tab shows, in CSS pixels from the top left corner of the element that
1015
+ * CaptureScreenshot captures (the editor's body). A panel whose content is hidden, such as one in a
1016
+ * stack's inactive tab, is left out. html-to-image draws a scrolled container from its top, so each
1017
+ * box adds the container's scroll offset. None while the editor shows no dashboard.
1018
+ */
1019
+ panelBounds() {
1020
+ const editor = this.editor;
1021
+ if (!editor)
1022
+ return [];
1023
+ const container = editor.BodyElement;
1024
+ const origin = container.getBoundingClientRect();
1025
+ const contents = new Map(Array.from(container.querySelectorAll('[data-panel-id]'), element => [element.dataset['panelId'], element]));
1026
+ return ExtractPanelsFromLayout(editor.Viewer?.getConfig()?.layout ?? null).flatMap(panel => {
1027
+ const content = contents.get(panel.id);
1028
+ if (!content || content.offsetParent === null)
1029
+ return [];
1030
+ const box = content.getBoundingClientRect();
1031
+ return [{
1032
+ panelId: panel.id,
1033
+ x: Math.round(box.left - origin.left + container.scrollLeft),
1034
+ y: Math.round(box.top - origin.top + container.scrollTop),
1035
+ width: Math.round(box.width),
1036
+ height: Math.round(box.height),
1037
+ }];
1038
+ });
1039
+ }
1040
+ // ========================================
1041
+ // Source Search for the Agent
1042
+ // ========================================
1043
+ /**
1044
+ * Searches the sources the user may show in a panel. First loads the engines that the searched kinds
1045
+ * read: the tab starts these loads with the dashboard, and a load that failed then runs again here.
1046
+ */
1047
+ async searchSources(query, kinds, limit) {
1048
+ await this.loadSourceEngines(kinds);
1049
+ return SearchDashboardSources(this.sourceSearchDeps(), query, kinds, limit);
1050
+ }
1051
+ /** The lists the source search reads, each limited to what the user may use. */
1052
+ sourceSearchDeps() {
1053
+ const provider = this.ProviderToUse;
1054
+ return {
1055
+ ListArtifacts: query => this.artifactSources(query),
1056
+ ListViews: () => viewSources(),
1057
+ ListQueries: () => querySources(provider.CurrentUser),
1058
+ ListEntities: () => entitySources(provider),
1059
+ };
1060
+ }
1061
+ /**
1062
+ * Loads the engines the source search reads for `kinds`: user views for 'view' and queries for
1063
+ * 'query'. Artifacts and entities need neither. A loaded engine returns at once.
1064
+ */
1065
+ async loadSourceEngines(kinds) {
1066
+ const provider = this.ProviderToUse;
1067
+ const loads = [];
1068
+ if (kinds.includes('view'))
1069
+ loads.push(UserViewEngine.Instance.Config(false, provider.CurrentUser, provider));
1070
+ if (kinds.includes('query'))
1071
+ loads.push(QueryEngine.Instance.Config(false, provider.CurrentUser, provider));
1072
+ await Promise.all(loads);
1073
+ }
1074
+ /**
1075
+ * Starts to load the source search engines with a Config dashboard, so that the agent's first search
1076
+ * usually finds them loaded. The dashboard does not wait for it. Never rejects: a failure is logged,
1077
+ * and the search loads the engines again.
1078
+ */
1079
+ async preloadSourceEngines() {
1080
+ try {
1081
+ await this.loadSourceEngines(SOURCE_KINDS);
1082
+ }
1083
+ catch (error) {
1084
+ LogError(`Dashboard tab: could not load the views and queries for the assistant's source search: ${errorMessage(error)}`);
1085
+ }
1086
+ }
1087
+ /**
1088
+ * The artifacts the user can read in the tab's environment (ChatEnvironmentId) whose name or description
1089
+ * contains `query` in any case (all of them for a blank query), the first SOURCE_SEARCH_MAX_ARTIFACTS by
1090
+ * name, each with its latest version number. An artifact whose versions cannot be read has none, so
1091
+ * its panel shows the latest version. Rejects when the artifacts cannot be read.
1092
+ */
1093
+ async artifactSources(query) {
1094
+ const provider = this.ProviderToUse;
1095
+ const user = provider.CurrentUser;
1096
+ await ArtifactMetadataEngine.Instance.Config(false, user, provider);
1097
+ const readable = await this.artifactPermissions.GetReadableArtifactsFilter(user.ID, user);
1098
+ const environmentId = EscapeSQLString(this.ChatEnvironmentId);
1099
+ const result = await RunView.FromMetadataProvider(provider).RunView({
1100
+ EntityName: 'MJ: Artifacts',
1101
+ ExtraFilter: `${readable} AND EnvironmentID='${environmentId}' AND (Visibility IS NULL OR Visibility='Always')${artifactQueryFilter(query)}`,
1102
+ Fields: ['ID', 'Name', 'Description', 'Type'],
1103
+ OrderBy: 'Name',
1104
+ MaxRows: SOURCE_SEARCH_MAX_ARTIFACTS,
1105
+ ResultType: 'simple',
1106
+ }, user);
1107
+ if (!result.Success)
1108
+ throw new Error(`Could not read the artifacts: ${result.ErrorMessage || 'no reason given'}`);
1109
+ const artifacts = result.Results ?? [];
1110
+ const latest = await this.latestArtifactVersions(artifacts.map(artifact => artifact.ID));
1111
+ return artifacts.map(artifact => ({
1112
+ ID: artifact.ID,
1113
+ Name: artifact.Name,
1114
+ Description: artifact.Description,
1115
+ Type: artifact.Type,
1116
+ LatestVersion: latest.get(NormalizeUUID(artifact.ID)) ?? null,
1117
+ }));
1118
+ }
1119
+ /**
1120
+ * The highest version number of each artifact, by normalized artifact ID. One RunViews call reads
1121
+ * every version of the artifacts, highest first and without the entity's row cap, with at most
1122
+ * ARTIFACT_VERSION_FILTER_CHUNK IDs in each filter. The artifacts whose versions cannot be read are
1123
+ * not in the map, and the failure is logged.
1124
+ */
1125
+ async latestArtifactVersions(artifactIds) {
1126
+ const latest = new Map();
1127
+ if (artifactIds.length === 0)
1128
+ return latest;
1129
+ const provider = this.ProviderToUse;
1130
+ try {
1131
+ const results = await RunView.FromMetadataProvider(provider).RunViews(chunked(artifactIds, ARTIFACT_VERSION_FILTER_CHUNK).map(ids => ({
1132
+ EntityName: 'MJ: Artifact Versions',
1133
+ ExtraFilter: `ArtifactID IN (${ids.map(id => `'${EscapeSQLString(id)}'`).join(',')})`,
1134
+ Fields: ['ArtifactID', 'VersionNumber'],
1135
+ OrderBy: 'VersionNumber DESC',
1136
+ IgnoreMaxRows: true,
1137
+ ResultType: 'simple',
1138
+ })), provider.CurrentUser);
1139
+ for (const result of results) {
1140
+ if (result.Success)
1141
+ addLatestVersions(latest, result.Results ?? []);
1142
+ else
1143
+ LogError(`Dashboard tab: could not read the artifact versions: ${result.ErrorMessage || 'no reason given'}`);
1144
+ }
1145
+ }
1146
+ catch (error) {
1147
+ LogError(`Dashboard tab: could not read the artifact versions: ${errorMessage(error)}`);
1148
+ }
1149
+ return latest;
1150
+ }
1151
+ // ========================================
1152
+ // Requests from the Agent
1153
+ // ========================================
1154
+ /**
1155
+ * Opens the confirm dialog for an agent's request and settles with the user's answer: true once the
1156
+ * confirmed action is done, false when the user cancels. Rejects when no Config dashboard is open,
1157
+ * when another confirmation is open, when the confirmed action fails, and when no answer comes
1158
+ * within AGENT_CONFIRM_TIMEOUT_MS; the dialog closes first.
1159
+ */
1160
+ askAgentConfirm(kind, detail) {
1161
+ const dashboard = this.ConfigDashboard;
1162
+ if (!dashboard)
1163
+ return Promise.reject(new Error(NO_CONFIG_DASHBOARD));
1164
+ if (this.agentConfirmRequest) {
1165
+ return Promise.reject(new Error('Another confirmation is already open. Wait for the user to answer it, then try again.'));
1166
+ }
1167
+ return new Promise((resolve, reject) => {
1168
+ const timer = setTimeout(() => {
1169
+ this.abortAgentConfirm(AGENT_CONFIRM_TIMED_OUT);
1170
+ SafeDetectChanges(this.cdr);
1171
+ }, AGENT_CONFIRM_TIMEOUT_MS);
1172
+ this.agentConfirmRequest = {
1173
+ answer: confirmed => {
1174
+ clearTimeout(timer);
1175
+ if (!confirmed) {
1176
+ resolve(false);
1177
+ return;
1178
+ }
1179
+ this.performAgentRequest(kind, detail).then(() => resolve(true), reject);
1180
+ },
1181
+ abort: reason => {
1182
+ clearTimeout(timer);
1183
+ reject(reason);
1184
+ },
1185
+ };
1186
+ this.AgentConfirm = { Kind: kind, ...agentConfirmText(kind, dashboard.Name, detail) };
1187
+ SafeDetectChanges(this.cdr);
1188
+ });
1189
+ }
1190
+ /** Takes the user's answer to the agent's confirm dialog. Does nothing when no dialog is open. */
1191
+ OnAgentConfirm(confirmed) {
1192
+ const request = this.closeAgentConfirm();
1193
+ SafeDetectChanges(this.cdr);
1194
+ request?.answer(confirmed);
1195
+ }
1196
+ /** Closes the agent's confirm dialog, if one is open, and fails its request with `reason`. */
1197
+ abortAgentConfirm(reason) {
1198
+ this.closeAgentConfirm()?.abort(new Error(reason));
1199
+ }
1200
+ /** Closes the agent's confirm dialog and returns its request, or null when none is open. */
1201
+ closeAgentConfirm() {
1202
+ const request = this.agentConfirmRequest;
1203
+ this.agentConfirmRequest = null;
1204
+ this.AgentConfirm = null;
1205
+ return request;
1206
+ }
1207
+ /** Does what the user confirmed, as the editor's Save and the Add to menu do it. Rejects when it did not take effect. */
1208
+ async performAgentRequest(kind, detail) {
1209
+ switch (kind) {
1210
+ case 'save':
1211
+ return this.saveForAgent(detail);
1212
+ case 'pin':
1213
+ return this.pinForAgent();
1214
+ default:
1215
+ throw new Error(`The tab cannot do the request "${String(kind)}".`);
1216
+ }
1217
+ }
1218
+ /**
1219
+ * Saves the dashboard through the editor, as its Save does, with the new name and description the
1220
+ * agent gave. Rejects with the reason the save did not happen. While a Save runs the editor refuses
1221
+ * at once and leaves its name and description fields as they are, because that Save reads them.
1222
+ */
1223
+ async saveForAgent(detail) {
1224
+ const editor = this.editor;
1225
+ const failure = editor ? await editor.Save({ Name: detail.name, Description: detail.description }) : NO_CONFIG_DASHBOARD;
1226
+ if (failure)
1227
+ throw new Error(failure);
1228
+ }
1229
+ /** Pins the dashboard to Home as the Add to menu does. Rejects when it is not pinned afterwards. */
1230
+ async pinForAgent() {
1231
+ await this.PinToHome();
1232
+ if (!this.IsPinnedToHome)
1233
+ throw new Error(`Could not pin "${this.ConfigDashboard?.Name ?? 'the dashboard'}" to Home.`);
413
1234
  }
414
1235
  /** @deprecated Use {@link OnShareDialogResult}. */
415
1236
  onShareDialogResult(result) {
@@ -432,7 +1253,7 @@ let DashboardResource = class DashboardResource extends BaseResourceComponent {
432
1253
  try {
433
1254
  // Check if this is a special dashboard type (not a database record)
434
1255
  const config = data.Configuration || {};
435
- if (config['dashboardType'] === 'DataExplorer' || data.ResourceRecordID === 'DataExplorer') {
1256
+ if (this.isDataExplorer(data)) {
436
1257
  // Special case: Data Explorer dashboard with optional entity filter
437
1258
  await this.loadDataExplorer(config['entityFilter'], config['appName'], config['appIcon']);
438
1259
  return;
@@ -442,14 +1263,17 @@ let DashboardResource = class DashboardResource extends BaseResourceComponent {
442
1263
  if (!dashboard) {
443
1264
  throw new Error(`Dashboard with ID ${data.ResourceRecordID} not found.`);
444
1265
  }
1266
+ void this.recentAccess.LogAccess('MJ: Dashboards', dashboard.ID, 'dashboard');
445
1267
  // Determine which dashboard component to load based on dashboard type
446
1268
  if (dashboard.Type === 'Code') {
447
1269
  // CODE-BASED DASHBOARD: Use registered class via DriverClass
448
1270
  await this.loadCodeBasedDashboard(dashboard);
449
1271
  }
450
1272
  else {
451
- // CONFIG-BASED DASHBOARD: Use the generic metadata-driven renderer
452
- await this.loadConfigBasedDashboard(dashboard);
1273
+ // CONFIG-BASED DASHBOARD: the shared dashboard editor shows it. Its studio tools
1274
+ // search views and queries, so their engines start to load now; the dashboard does not wait.
1275
+ void this.preloadSourceEngines();
1276
+ this.showConfigDashboard(dashboard);
453
1277
  }
454
1278
  }
455
1279
  catch (error) {
@@ -636,68 +1460,7 @@ let DashboardResource = class DashboardResource extends BaseResourceComponent {
636
1460
  return stateObject;
637
1461
  }
638
1462
  /**
639
- * Load a config-based dashboard using the new DashboardViewerComponent (Golden Layout)
640
- */
641
- async loadConfigBasedDashboard(dashboard) {
642
- try {
643
- this.ContainerElement.nativeElement.innerHTML = '';
644
- const componentRef = this.viewContainer.createComponent(DashboardViewerComponent);
645
- this.componentRef = componentRef;
646
- const instance = componentRef.instance;
647
- // Store references for external toolbar control
648
- this.viewerInstance = instance;
649
- this.ConfigDashboard = dashboard;
650
- // Compute user permissions for this dashboard
651
- const md = this.ProviderToUse;
652
- this.DashboardPermissions = DashboardEngine.Instance.GetDashboardPermissions(dashboard.ID, md.CurrentUser.ID);
653
- // Manually append the component's native element inside the div
654
- const nativeElement = this.componentRef.hostView.rootNodes[0];
655
- nativeElement.style.width = '100%';
656
- nativeElement.style.height = '100%';
657
- this.ContainerElement.nativeElement.appendChild(nativeElement);
658
- // Load categories for breadcrumb navigation (if not already loaded)
659
- if (this.categories.length === 0) {
660
- this.categories = DashboardEngine.Instance.DashboardCategories;
661
- }
662
- // Configure the viewer before assigning the dashboard. The dashboard input
663
- // starts the layout lifecycle synchronously, so event handlers need to be
664
- // wired first.
665
- instance.showToolbar = false; // We provide external toolbar
666
- instance.showBreadcrumb = false; // Already in its own tab, no breadcrumb needed
667
- instance.showOpenInTabButton = false; // Already in its own tab
668
- instance.showEditButton = false; // External toolbar handles edit
669
- instance.Categories = this.categories;
670
- // Wire up navigation events - handle navigation requests from the dashboard
671
- instance.navigationRequested.subscribe((event) => {
672
- this.handleNavigationRequest(event);
673
- });
674
- // Wire up "Open in Tab" button click
675
- instance.openInTab.subscribe((event) => {
676
- this.navigationService.OpenDashboard(event.dashboardId, event.dashboardName);
677
- });
678
- // Wire up dashboard saved event
679
- instance.dashboardSaved.subscribe((savedDashboard) => {
680
- this.ResourceRecordSaved(savedDashboard);
681
- });
682
- // Wire up error events
683
- instance.error.subscribe((errorEvent) => {
684
- console.error('Dashboard error:', errorEvent.message, errorEvent.error);
685
- });
686
- // Set the dashboard entity directly on the viewer and wait for Golden Layout
687
- // to initialize against a real container before clearing the resource loader.
688
- instance.dashboard = dashboard;
689
- await instance.waitForLayoutReady();
690
- this.NotifyLoadComplete();
691
- this.cdr.detectChanges();
692
- }
693
- catch (error) {
694
- console.error('Error loading config-based dashboard:', error);
695
- this.setError(`The dashboard "${dashboard.Name}" could not be loaded. There may be an issue with the dashboard configuration.`, error);
696
- this.NotifyLoadComplete();
697
- }
698
- }
699
- /**
700
- * Handle navigation requests from the dashboard viewer
1463
+ * Handle navigation requests from a part of the dashboard (the editor passes them on)
701
1464
  */
702
1465
  handleNavigationRequest(event) {
703
1466
  const request = event.request;
@@ -740,26 +1503,41 @@ let DashboardResource = class DashboardResource extends BaseResourceComponent {
740
1503
  }
741
1504
  }
742
1505
  /**
743
- * Get the display name for a dashboard resource
744
- * Loads the actual dashboard name from the database if available
1506
+ * The tab title: the dashboard's name from the DashboardEngine cache, else its record name from
1507
+ * the server, else the given name. A Data Explorer tab keeps the name of the app it belongs to.
745
1508
  */
746
1509
  async GetResourceDisplayName(data) {
1510
+ if (this.isDataExplorer(data)) {
1511
+ const config = data.Configuration ?? {};
1512
+ const appName = typeof config['appName'] === 'string' ? config['appName'] : '';
1513
+ return data.Name || appName || 'Data Explorer';
1514
+ }
1515
+ const dashboardId = typeof data.ResourceRecordID === 'string' ? data.ResourceRecordID : '';
1516
+ const name = dashboardId ? (this.cachedDashboardName(dashboardId) ?? await this.loadDashboardName(dashboardId)) : null;
1517
+ return name || data.Name || 'Dashboard';
1518
+ }
1519
+ /** True for the Data Explorer, which this wrapper hosts but which is not a dashboard record. */
1520
+ isDataExplorer(data) {
1521
+ const config = data.Configuration ?? {};
1522
+ return config['dashboardType'] === 'DataExplorer' || data.ResourceRecordID === 'DataExplorer';
1523
+ }
1524
+ /** The dashboard's name from the DashboardEngine cache, or null when the cache does not have it. */
1525
+ cachedDashboardName(dashboardId) {
747
1526
  try {
748
- // Try to load dashboard metadata if we have the record ID
749
- if (data.ResourceRecordID && data.ResourceRecordID.length > 0) {
750
- const md = this.ProviderToUse;
751
- const compositeKey = CompositeKey.FromID(data.ResourceRecordID); // first-pk-ok: ResourceRecordID of a Dashboards resource — core entity keyed by ID
752
- const name = await md.GetEntityRecordName('Dashboards', compositeKey);
753
- if (name) {
754
- return name;
755
- }
756
- }
1527
+ return DashboardEngine.Instance.Dashboards.find(d => UUIDsEqual(d.ID, dashboardId))?.Name || null;
757
1528
  }
758
- catch (error) {
759
- // Silently fail and use fallback
1529
+ catch {
1530
+ return null; // the user cannot read the dashboards cache
1531
+ }
1532
+ }
1533
+ /** The dashboard's record name from the server, or null when it cannot be read. */
1534
+ async loadDashboardName(dashboardId) {
1535
+ try {
1536
+ return (await this.ProviderToUse.GetEntityRecordName('MJ: Dashboards', CompositeKey.FromID(dashboardId))) || null;
1537
+ }
1538
+ catch {
1539
+ return null;
760
1540
  }
761
- // Fallback: use provided name or generic label
762
- return data.Name || 'Dashboard';
763
1541
  }
764
1542
  /**
765
1543
  * Get the icon class for dashboard resources
@@ -769,28 +1547,34 @@ let DashboardResource = class DashboardResource extends BaseResourceComponent {
769
1547
  }
770
1548
  static ɵfac = function DashboardResource_Factory(__ngFactoryType__) { return new (__ngFactoryType__ || DashboardResource)(i0.ɵɵdirectiveInject(i0.ViewContainerRef), i0.ɵɵdirectiveInject(i0.ChangeDetectorRef)); };
771
1549
  static ɵcmp = /*@__PURE__*/ i0.ɵɵdefineComponent({ type: DashboardResource, selectors: [["mj-dashboard-resource"]], viewQuery: function DashboardResource_Query(rf, ctx) { if (rf & 1) {
772
- i0.ɵɵviewQuery(_c0, 7);
1550
+ i0.ɵɵviewQuery(_c0, 5)(_c1, 7);
773
1551
  } if (rf & 2) {
774
1552
  let _t;
1553
+ i0.ɵɵqueryRefresh(_t = i0.ɵɵloadQuery()) && (ctx.editor = _t.first);
775
1554
  i0.ɵɵqueryRefresh(_t = i0.ɵɵloadQuery()) && (ctx.ContainerElement = _t.first);
776
- } }, standalone: false, features: [i0.ɵɵInheritDefinitionFeature], decls: 7, vars: 4, consts: [["container", ""], [1, "dashboard-resource-wrapper"], [1, "error-state"], [1, "viewer-toolbar"], [1, "viewer-header", "editing"], [1, "dashboard-resource-container"], [3, "Visible", "Dashboard"], [1, "error-icon"], [1, "fa-solid", "fa-triangle-exclamation"], [1, "error-title"], [1, "error-message"], [1, "error-details"], [1, "toolbar-left"], [1, "dashboard-title"], [1, "fa-solid", "fa-chart-line"], ["title", "Shared with you", 1, "shared-indicator"], [1, "toolbar-actions"], ["title", "Share Dashboard", 1, "btn-icon"], ["title", "Edit Dashboard", 1, "btn-icon"], [1, "fa-solid", "fa-share-nodes"], ["title", "Share Dashboard", 1, "btn-icon", 3, "click"], ["title", "Edit Dashboard", 1, "btn-icon", 3, "click"], [1, "fa-solid", "fa-edit"], [1, "header-left"], [1, "btn-add-part", 3, "click"], [1, "fa-solid", "fa-plus"], [1, "header-separator"], [1, "dashboard-info-edit"], ["type", "text", "placeholder", "Dashboard name", 1, "dashboard-name-input", 3, "ngModelChange", "ngModel"], ["type", "text", "placeholder", "Add a description...", 1, "dashboard-description-input", 3, "ngModelChange", "ngModel"], [1, "header-right"], [1, "btn-primary", 3, "click"], [1, "fa-solid", "fa-save"], [1, "btn-cancel", 3, "click"], [3, "Result", "Visible", "Dashboard"]], template: function DashboardResource_Template(rf, ctx) { if (rf & 1) {
777
- i0.ɵɵelementStart(0, "div", 1);
778
- i0.ɵɵconditionalCreate(1, DashboardResource_Conditional_1_Template, 8, 2, "div", 2);
779
- i0.ɵɵconditionalCreate(2, DashboardResource_Conditional_2_Template, 9, 4, "div", 3);
780
- i0.ɵɵconditionalCreate(3, DashboardResource_Conditional_3_Template, 15, 2, "div", 4);
781
- i0.ɵɵelement(4, "div", 5, 0);
782
- i0.ɵɵconditionalCreate(6, DashboardResource_Conditional_6_Template, 1, 2, "mj-dashboard-share-dialog", 6);
1555
+ } }, standalone: false, features: [i0.ɵɵInheritDefinitionFeature], decls: 8, vars: 7, consts: [["container", ""], ["editor", ""], [1, "dashboard-resource-wrapper"], [1, "error-state"], ["direction", "horizontal", "unit", "percent", 1, "studio-copilot-splitter", 3, "gutterSize"], [1, "dashboard-resource-container"], [3, "Visible", "Dashboard"], ["Type", "info", "CancelText", "Not now", 1, "agent-confirm", 3, "Visible", "Title", "Message", "ConfirmText"], ["Type", "warning", "Title", "End the voice session?", "Message", "Closing the AI assistant ends the voice session.", "ConfirmText", "Close", "CancelText", "Keep open", 1, "close-chat-confirm", 3, "Visible"], [1, "error-icon"], [1, "fa-solid", "fa-triangle-exclamation"], [1, "error-title"], [1, "error-message"], [1, "error-details"], ["direction", "horizontal", "unit", "percent", 1, "studio-copilot-splitter", 3, "dragEnd", "gutterSize"], [3, "size", "minSize"], [3, "Loaded", "LoadFailed", "EditingChange", "NameChanged", "ConfigChanged", "ReloadedFromSaved", "FavoriteChange", "NavigationRequested", "Provider", "DashboardId"], ["headerTools", "", "type", "button", "mjButton", "", "Variant", "icon", "Size", "sm", "title", "AI assistant", "aria-label", "AI assistant", 1, "dashboard-tab-ai-toggle", 3, "click"], ["aria-hidden", "true", 1, "fa-solid", "fa-robot"], ["viewActions", "", 1, "dashboard-tab-view-actions"], [3, "PinToHome", "Share", "IsPinned", "CanShare"], ["type", "button", "mjButton", "", "Variant", "icon", "Size", "sm", "title", "Share dashboard", "aria-label", "Share dashboard", 1, "dashboard-tab-share"], [3, "size", "minSize", "maxSize"], ["type", "button", "mjButton", "", "Variant", "icon", "Size", "sm", "title", "Share dashboard", "aria-label", "Share dashboard", 1, "dashboard-tab-share", 3, "click"], ["aria-hidden", "true", 1, "fa-solid", "fa-share-nodes"], ["data-testid", "dashboard-copilot", "aria-label", "AI assistant", 1, "dashboard-copilot"], [1, "dashboard-copilot-head"], [1, "dashboard-copilot-title"], [1, "fa-solid", "fa-robot"], [1, "dashboard-copilot-actions"], ["type", "button", 1, "dashboard-copilot-btn", "copilot-expand", 3, "click", "title"], [1, "fa-solid"], ["type", "button", "title", "Close assistant", "aria-label", "Close assistant", 1, "dashboard-copilot-btn", "copilot-close", 3, "click"], [1, "fa-solid", "fa-xmark"], [1, "dashboard-copilot-body"], ["ApplicationScope", "Application", "EmptyStateGreeting", "What should this dashboard show?", 3, "Provider", "EnvironmentId", "CurrentUser", "Conversation", "ConversationId", "IsNewConversation", "PendingMessage", "PendingAttachments", "SuppressNewConversationEmptyState", "AllowMentions", "OverlayMode", "ShowExportButton", "ShowShareButton", "ShowArtifactIndicator", "ShowAgentPicker", "ShowAgentModePicker", "DefaultAgentId", "AllowedAgentIDs", "ApplicationId", "AppContext"], ["role", "status", 1, "dashboard-copilot-empty"], [1, "dashboard-copilot-empty"], ["ApplicationScope", "Application", "EmptyStateGreeting", "What should this dashboard show?", 3, "ConversationCreated", "RealtimeConversationReady", "PendingMessageConsumed", "navigationRequest", "OpenEntityRecord", "Provider", "EnvironmentId", "CurrentUser", "Conversation", "ConversationId", "IsNewConversation", "PendingMessage", "PendingAttachments", "SuppressNewConversationEmptyState", "AllowMentions", "OverlayMode", "ShowExportButton", "ShowShareButton", "ShowArtifactIndicator", "ShowAgentPicker", "ShowAgentModePicker", "DefaultAgentId", "AllowedAgentIDs", "ApplicationId", "AppContext"], ["Text", "Connecting\u2026", "Size", "small"], [3, "Result", "Visible", "Dashboard"], ["Type", "info", "CancelText", "Not now", 1, "agent-confirm", 3, "Confirmed", "Cancelled", "Visible", "Title", "Message", "ConfirmText"], ["Type", "warning", "Title", "End the voice session?", "Message", "Closing the AI assistant ends the voice session.", "ConfirmText", "Close", "CancelText", "Keep open", 1, "close-chat-confirm", 3, "Confirmed", "Cancelled", "Visible"]], template: function DashboardResource_Template(rf, ctx) { if (rf & 1) {
1556
+ i0.ɵɵelementStart(0, "div", 2);
1557
+ i0.ɵɵconditionalCreate(1, DashboardResource_Conditional_1_Template, 8, 2, "div", 3);
1558
+ i0.ɵɵconditionalCreate(2, DashboardResource_Conditional_2_Template, 10, 12, "as-split", 4);
1559
+ i0.ɵɵelement(3, "div", 5, 0);
1560
+ i0.ɵɵconditionalCreate(5, DashboardResource_Conditional_5_Template, 1, 2, "mj-dashboard-share-dialog", 6);
1561
+ i0.ɵɵconditionalCreate(6, DashboardResource_Conditional_6_Template, 1, 4, "mj-confirm-dialog", 7);
1562
+ i0.ɵɵconditionalCreate(7, DashboardResource_Conditional_7_Template, 1, 1, "mj-confirm-dialog", 8);
783
1563
  i0.ɵɵelementEnd();
784
1564
  } if (rf & 2) {
785
1565
  i0.ɵɵadvance();
786
1566
  i0.ɵɵconditional(ctx.errorMessage ? 1 : -1);
787
1567
  i0.ɵɵadvance();
788
- i0.ɵɵconditional(ctx.configDashboard && !ctx.isEditMode && !ctx.errorMessage ? 2 : -1);
1568
+ i0.ɵɵconditional(ctx.ConfigDashboardId && !ctx.errorMessage ? 2 : -1);
1569
+ i0.ɵɵadvance();
1570
+ i0.ɵɵclassProp("is-hidden", !!ctx.ConfigDashboardId);
1571
+ i0.ɵɵadvance(2);
1572
+ i0.ɵɵconditional(ctx.ConfigDashboard ? 5 : -1);
1573
+ i0.ɵɵadvance();
1574
+ i0.ɵɵconditional(ctx.AgentConfirm ? 6 : -1);
789
1575
  i0.ɵɵadvance();
790
- i0.ɵɵconditional(ctx.configDashboard && ctx.isEditMode && !ctx.errorMessage ? 3 : -1);
791
- i0.ɵɵadvance(3);
792
- i0.ɵɵconditional(ctx.configDashboard ? 6 : -1);
793
- } }, dependencies: [i1.DefaultValueAccessor, i1.NgControlStatus, i1.NgModel, i2.DashboardShareDialogComponent], styles: ["[_nghost-%COMP%] {\n display: block;\n width: 100%;\n height: 100%;\n position: relative;\n overflow: hidden;\n }\n .dashboard-resource-wrapper[_ngcontent-%COMP%] {\n display: flex;\n flex-direction: column;\n height: 100%;\n width: 100%;\n }\n .dashboard-resource-container[_ngcontent-%COMP%] {\n flex: 1;\n overflow: hidden;\n min-height: 0;\n }\n\n \n .viewer-toolbar[_ngcontent-%COMP%] {\n display: flex;\n align-items: center;\n justify-content: space-between;\n padding: 12px 24px;\n background: var(--mj-bg-surface-card);\n border-bottom: 1px solid var(--mj-border-default);\n gap: 16px;\n }\n .viewer-toolbar[_ngcontent-%COMP%] .toolbar-left[_ngcontent-%COMP%] {\n display: flex;\n align-items: center;\n gap: 12px;\n }\n .viewer-toolbar[_ngcontent-%COMP%] .dashboard-title[_ngcontent-%COMP%] {\n font-size: 16px;\n font-weight: 500;\n color: var(--mj-text-primary);\n display: flex;\n align-items: center;\n gap: 8px;\n }\n .viewer-toolbar[_ngcontent-%COMP%] .dashboard-title[_ngcontent-%COMP%] i[_ngcontent-%COMP%] {\n color: var(--mj-brand-primary);\n }\n .shared-indicator[_ngcontent-%COMP%] {\n display: flex;\n align-items: center;\n justify-content: center;\n width: 24px;\n height: 24px;\n border-radius: 50%;\n background: color-mix(in srgb, var(--mj-brand-primary) 10%, var(--mj-bg-surface));\n color: var(--mj-brand-primary);\n font-size: 11px;\n }\n .viewer-toolbar[_ngcontent-%COMP%] .toolbar-actions[_ngcontent-%COMP%] {\n display: flex;\n align-items: center;\n gap: 12px;\n }\n\n \n .viewer-header[_ngcontent-%COMP%] {\n display: flex;\n align-items: center;\n justify-content: space-between;\n padding: 12px 24px;\n background: var(--mj-bg-surface-card);\n border-bottom: 1px solid var(--mj-border-default);\n transition: background 0.2s, border-color 0.2s;\n }\n .viewer-header.editing[_ngcontent-%COMP%] {\n background: linear-gradient(135deg, color-mix(in srgb, var(--mj-brand-primary) 10%, var(--mj-bg-surface)) 0%, color-mix(in srgb, var(--mj-brand-primary) 25%, var(--mj-bg-surface)) 100%);\n border-bottom: 2px solid var(--mj-brand-primary);\n }\n .viewer-header[_ngcontent-%COMP%] .header-left[_ngcontent-%COMP%] {\n display: flex;\n align-items: center;\n gap: 12px;\n flex: 1;\n }\n .viewer-header[_ngcontent-%COMP%] .header-right[_ngcontent-%COMP%] {\n display: flex;\n align-items: center;\n gap: 8px;\n }\n\n \n .btn-add-part[_ngcontent-%COMP%] {\n display: flex;\n align-items: center;\n gap: 8px;\n padding: 8px 16px;\n border: none;\n border-radius: 6px;\n background: var(--mj-brand-primary);\n color: var(--mj-text-inverse);\n font-size: 13px;\n font-weight: 500;\n cursor: pointer;\n transition: background 0.2s, transform 0.1s;\n box-shadow: 0 2px 4px color-mix(in srgb, var(--mj-brand-primary) 30%, transparent);\n }\n .btn-add-part[_ngcontent-%COMP%]:hover {\n background: var(--mj-brand-primary-hover);\n transform: translateY(-1px);\n box-shadow: 0 3px 6px color-mix(in srgb, var(--mj-brand-primary) 40%, transparent);\n }\n .btn-add-part[_ngcontent-%COMP%] i[_ngcontent-%COMP%] { font-size: 12px; }\n\n \n .header-separator[_ngcontent-%COMP%] {\n width: 1px;\n height: 28px;\n background: color-mix(in srgb, var(--mj-brand-primary) 30%, transparent);\n margin: 0 4px;\n }\n\n \n .btn-primary[_ngcontent-%COMP%] {\n display: flex;\n align-items: center;\n gap: 8px;\n padding: 10px 18px;\n border: none;\n border-radius: 6px;\n background: var(--mj-brand-primary);\n color: var(--mj-text-inverse);\n font-size: 14px;\n font-weight: 500;\n cursor: pointer;\n transition: background 0.2s;\n }\n .btn-primary[_ngcontent-%COMP%]:hover { background: var(--mj-brand-primary-hover); }\n\n .btn-icon[_ngcontent-%COMP%] {\n width: 36px;\n height: 36px;\n display: flex;\n align-items: center;\n justify-content: center;\n border: 1px solid var(--mj-border-default);\n border-radius: 6px;\n background: var(--mj-bg-surface-card);\n color: var(--mj-text-secondary);\n cursor: pointer;\n transition: all 0.2s;\n }\n .btn-icon[_ngcontent-%COMP%]:hover { background: var(--mj-bg-surface-sunken); }\n\n .btn-cancel[_ngcontent-%COMP%] {\n display: flex;\n align-items: center;\n gap: 8px;\n padding: 10px 18px;\n border: 1px solid var(--mj-border-default);\n border-radius: 6px;\n background: var(--mj-bg-surface-card);\n color: var(--mj-text-secondary);\n font-size: 14px;\n cursor: pointer;\n transition: all 0.2s;\n }\n .btn-cancel[_ngcontent-%COMP%]:hover {\n background: var(--mj-bg-surface-sunken);\n border-color: var(--mj-border-default);\n color: var(--mj-text-primary);\n }\n\n \n .dashboard-info-edit[_ngcontent-%COMP%] {\n display: flex;\n align-items: center;\n gap: 16px;\n flex: 1;\n }\n .dashboard-name-input[_ngcontent-%COMP%] {\n border: 1px solid transparent;\n border-radius: 4px;\n padding: 6px 12px;\n font-size: 16px;\n font-weight: 500;\n color: var(--mj-text-primary);\n background: rgba(255, 255, 255, 0.7);\n outline: none;\n min-width: 200px;\n max-width: 300px;\n transition: border-color 0.2s, background 0.2s, box-shadow 0.2s;\n }\n .dashboard-name-input[_ngcontent-%COMP%]:hover { background: rgba(255, 255, 255, 0.9); }\n .dashboard-name-input[_ngcontent-%COMP%]:focus {\n background: var(--mj-bg-surface-card);\n border-color: var(--mj-brand-primary);\n box-shadow: 0 0 0 2px color-mix(in srgb, var(--mj-brand-primary) 20%, transparent);\n }\n .dashboard-description-input[_ngcontent-%COMP%] {\n border: 1px solid transparent;\n border-radius: 4px;\n padding: 6px 12px;\n font-size: 13px;\n color: var(--mj-text-secondary);\n background: rgba(255, 255, 255, 0.5);\n outline: none;\n flex: 1;\n min-width: 150px;\n max-width: 400px;\n transition: border-color 0.2s, background 0.2s, box-shadow 0.2s;\n }\n .dashboard-description-input[_ngcontent-%COMP%]:hover { background: rgba(255, 255, 255, 0.8); }\n .dashboard-description-input[_ngcontent-%COMP%]:focus {\n background: var(--mj-bg-surface-card);\n border-color: var(--mj-brand-primary);\n box-shadow: 0 0 0 2px color-mix(in srgb, var(--mj-brand-primary) 20%, transparent);\n }\n .dashboard-description-input[_ngcontent-%COMP%]::placeholder {\n color: var(--mj-text-muted);\n font-style: normal;\n }\n\n \n .error-state[_ngcontent-%COMP%] {\n display: flex;\n flex-direction: column;\n align-items: center;\n justify-content: center;\n height: 100%;\n padding: 40px;\n text-align: center;\n color: var(--mj-text-secondary);\n }\n .error-icon[_ngcontent-%COMP%] {\n font-size: 64px;\n color: var(--mj-status-error);\n margin-bottom: 24px;\n opacity: 0.8;\n }\n .error-title[_ngcontent-%COMP%] {\n font-size: 24px;\n font-weight: 500;\n margin: 0 0 12px 0;\n color: var(--mj-text-primary);\n }\n .error-message[_ngcontent-%COMP%] {\n font-size: 16px;\n color: var(--mj-text-muted);\n margin: 0 0 24px 0;\n max-width: 500px;\n line-height: 1.5;\n }\n .error-details[_ngcontent-%COMP%] {\n background: var(--mj-bg-surface-sunken);\n border-radius: 8px;\n padding: 12px 16px;\n max-width: 600px;\n text-align: left;\n font-size: 13px;\n }\n .error-details[_ngcontent-%COMP%] summary[_ngcontent-%COMP%] {\n cursor: pointer;\n font-weight: 500;\n color: var(--mj-text-muted);\n margin-bottom: 8px;\n }\n .error-details[_ngcontent-%COMP%] pre[_ngcontent-%COMP%] {\n margin: 0;\n white-space: pre-wrap;\n word-break: break-word;\n color: var(--mj-status-error-text);\n font-family: 'Consolas', 'Monaco', monospace;\n font-size: 12px;\n }\n\n \n @media (max-width: 768px) {\n .viewer-header[_ngcontent-%COMP%] {\n flex-direction: column;\n gap: 12px;\n align-items: stretch;\n }\n .viewer-header[_ngcontent-%COMP%] .header-left[_ngcontent-%COMP%] { flex-wrap: wrap; }\n .dashboard-info-edit[_ngcontent-%COMP%] {\n flex-direction: column;\n align-items: stretch;\n }\n .dashboard-name-input[_ngcontent-%COMP%], \n .dashboard-description-input[_ngcontent-%COMP%] { max-width: none; }\n }"] });
1576
+ i0.ɵɵconditional(ctx.ShowCloseChatConfirm ? 7 : -1);
1577
+ } }, dependencies: [i1.ConversationChatAreaComponent, i2.DashboardShareDialogComponent, i2.DashboardEditorComponent, i3.ConfirmDialogComponent, i4.SplitComponent, i4.SplitAreaComponent, i5.LoadingComponent, i6.MJButtonDirective, i7.DashboardAddToMenuComponent], styles: ["[_nghost-%COMP%] {\n display: block;\n width: 100%;\n height: 100%;\n position: relative;\n overflow: hidden;\n }\n .dashboard-resource-wrapper[_ngcontent-%COMP%] {\n display: flex;\n flex-direction: column;\n height: 100%;\n width: 100%;\n }\n \n .studio-copilot-splitter[_ngcontent-%COMP%] {\n flex: 1;\n width: 100%;\n height: 100%;\n min-height: 0;\n min-width: 0;\n }\n \n .dashboard-resource-container[_ngcontent-%COMP%] {\n width: 100%;\n height: 100%;\n overflow: hidden;\n }\n .dashboard-resource-container.is-hidden[_ngcontent-%COMP%] { display: none; }\n\n \n .dashboard-tab-view-actions[_ngcontent-%COMP%] { display: contents; }\n \n .dashboard-tab-ai-toggle.is-active[_ngcontent-%COMP%] i[_ngcontent-%COMP%] { color: var(--mj-brand-primary); }\n\n \n .dashboard-copilot[_ngcontent-%COMP%] {\n width: 100%;\n height: 100%;\n border-left: 1px solid var(--mj-border-default);\n background: var(--mj-bg-surface);\n display: flex;\n flex-direction: column;\n min-height: 0;\n }\n .dashboard-copilot-head[_ngcontent-%COMP%] {\n display: flex;\n align-items: center;\n justify-content: space-between;\n padding: 10px 14px;\n border-bottom: 1px solid var(--mj-border-default);\n }\n .dashboard-copilot-title[_ngcontent-%COMP%] {\n display: flex;\n align-items: center;\n gap: 8px;\n font-weight: 600;\n color: var(--mj-text-primary);\n }\n .dashboard-copilot-title[_ngcontent-%COMP%] i[_ngcontent-%COMP%] { color: var(--mj-brand-primary); }\n .dashboard-copilot-actions[_ngcontent-%COMP%] {\n display: flex;\n align-items: center;\n gap: 4px;\n }\n .dashboard-copilot-btn[_ngcontent-%COMP%] {\n background: transparent;\n border: none;\n cursor: pointer;\n padding: 6px 8px;\n border-radius: 6px;\n color: var(--mj-text-muted);\n }\n .dashboard-copilot-btn[_ngcontent-%COMP%]:hover {\n background: var(--mj-bg-surface-hover);\n color: var(--mj-text-secondary);\n }\n .dashboard-copilot-body[_ngcontent-%COMP%] {\n flex: 1;\n min-height: 0;\n overflow: hidden;\n display: flex;\n flex-direction: column;\n }\n .dashboard-copilot-body[_ngcontent-%COMP%] mj-conversation-chat-area[_ngcontent-%COMP%] {\n flex: 1;\n min-height: 0;\n display: block;\n }\n .dashboard-copilot-empty[_ngcontent-%COMP%] {\n display: flex;\n align-items: center;\n justify-content: center;\n flex: 1;\n padding: 16px;\n color: var(--mj-text-secondary);\n text-align: center;\n }\n\n \n .error-state[_ngcontent-%COMP%] {\n display: flex;\n flex-direction: column;\n align-items: center;\n justify-content: center;\n height: 100%;\n padding: 40px;\n text-align: center;\n color: var(--mj-text-secondary);\n }\n .error-icon[_ngcontent-%COMP%] {\n font-size: 64px;\n color: var(--mj-status-error);\n margin-bottom: 24px;\n opacity: 0.8;\n }\n .error-title[_ngcontent-%COMP%] {\n font-size: 24px;\n font-weight: 500;\n margin: 0 0 12px 0;\n color: var(--mj-text-primary);\n }\n .error-message[_ngcontent-%COMP%] {\n font-size: 16px;\n color: var(--mj-text-muted);\n margin: 0 0 24px 0;\n max-width: 500px;\n line-height: 1.5;\n }\n .error-details[_ngcontent-%COMP%] {\n background: var(--mj-bg-surface-sunken);\n border-radius: 8px;\n padding: 12px 16px;\n max-width: 600px;\n text-align: left;\n font-size: 13px;\n }\n .error-details[_ngcontent-%COMP%] summary[_ngcontent-%COMP%] {\n cursor: pointer;\n font-weight: 500;\n color: var(--mj-text-muted);\n margin-bottom: 8px;\n }\n .error-details[_ngcontent-%COMP%] pre[_ngcontent-%COMP%] {\n margin: 0;\n white-space: pre-wrap;\n word-break: break-word;\n color: var(--mj-status-error-text);\n font-family: 'Consolas', 'Monaco', monospace;\n font-size: 12px;\n }"] });
794
1578
  };
795
1579
  DashboardResource = __decorate([
796
1580
  RegisterClass(BaseResourceComponent, 'DashboardResource')
@@ -817,91 +1601,258 @@ export { DashboardResource };
817
1601
  </div>
818
1602
  }
819
1603
 
820
- <!-- View Mode Toolbar -->
821
- @if (configDashboard && !isEditMode && !errorMessage) {
822
- <div class="viewer-toolbar">
823
- <div class="toolbar-left">
824
- <span class="dashboard-title">
825
- <i class="fa-solid fa-chart-line"></i>
826
- {{ configDashboard.Name }}
827
- </span>
828
- @if (!dashboardPermissions.IsOwner && dashboardPermissions.PermissionSource !== 'none') {
829
- <span class="shared-indicator" title="Shared with you">
830
- <i class="fa-solid fa-share-nodes"></i>
831
- </span>
832
- }
833
- </div>
834
- <div class="toolbar-actions">
835
- @if (dashboardPermissions.CanShare) {
1604
+ <!-- A Config dashboard: the shared editor, with the AI pane on its right. The editor stays in the template while the pane opens and closes. -->
1605
+ @if (ConfigDashboardId && !errorMessage) {
1606
+ <as-split direction="horizontal" class="studio-copilot-splitter" unit="percent" [gutterSize]="ChatOpen ? 6 : 0" (dragEnd)="OnCopilotSplitDragEnd($event.sizes)">
1607
+ <as-split-area [size]="ChatOpen ? (CopilotExpanded ? 100 - CopilotMaxPct : MainSizePct) : 100" [minSize]="100 - CopilotMaxPct">
1608
+ <mj-dashboard-editor
1609
+ #editor
1610
+ [Provider]="Provider"
1611
+ [DashboardId]="ConfigDashboardId"
1612
+ (Loaded)="OnEditorLoaded($event)"
1613
+ (LoadFailed)="OnEditorLoadFailed($event)"
1614
+ (EditingChange)="OnEditorEditingChange()"
1615
+ (NameChanged)="OnEditorNameChanged($event)"
1616
+ (ConfigChanged)="OnEditorConfigChanged()"
1617
+ (ReloadedFromSaved)="OnEditorReloaded()"
1618
+ (FavoriteChange)="OnEditorFavoriteChange()"
1619
+ (NavigationRequested)="OnEditorNavigationRequested($event)">
836
1620
  <button
837
- class="btn-icon"
838
- title="Share Dashboard"
839
- (click)="openShareDialog()">
840
- <i class="fa-solid fa-share-nodes"></i>
1621
+ headerTools
1622
+ type="button"
1623
+ mjButton
1624
+ Variant="icon"
1625
+ Size="sm"
1626
+ class="dashboard-tab-ai-toggle"
1627
+ [class.is-active]="ChatOpen"
1628
+ title="AI assistant"
1629
+ aria-label="AI assistant"
1630
+ [attr.aria-expanded]="ChatOpen ? 'true' : 'false'"
1631
+ (click)="ToggleChat()">
1632
+ <i class="fa-solid fa-robot" aria-hidden="true"></i>
841
1633
  </button>
842
- }
843
- @if (dashboardPermissions.CanEdit) {
844
- <button
845
- class="btn-icon"
846
- title="Edit Dashboard"
847
- (click)="toggleEditMode()">
848
- <i class="fa-solid fa-edit"></i>
849
- </button>
850
- }
851
- </div>
852
- </div>
853
- }
854
-
855
- <!-- Edit Mode Toolbar -->
856
- @if (configDashboard && isEditMode && !errorMessage) {
857
- <div class="viewer-header editing">
858
- <div class="header-left">
859
- <button class="btn-add-part" (click)="openAddPartDialog()">
860
- <i class="fa-solid fa-plus"></i>
861
- Add Part
862
- </button>
863
- <div class="header-separator"></div>
864
- <div class="dashboard-info-edit">
865
- <input
866
- type="text"
867
- class="dashboard-name-input"
868
- [(ngModel)]="editingName"
869
- placeholder="Dashboard name">
870
- <input
871
- type="text"
872
- class="dashboard-description-input"
873
- [(ngModel)]="editingDescription"
874
- placeholder="Add a description...">
875
- </div>
876
- </div>
877
- <div class="header-right">
878
- <button class="btn-primary" (click)="saveDashboard()">
879
- <i class="fa-solid fa-save"></i>
880
- Save
881
- </button>
882
- <button class="btn-cancel" (click)="cancelEdit()">
883
- Cancel
884
- </button>
885
- </div>
886
- </div>
1634
+ <span viewActions class="dashboard-tab-view-actions">
1635
+ <mj-dashboard-add-to-menu
1636
+ [IsPinned]="IsPinnedToHome"
1637
+ [CanShare]="CanShare"
1638
+ (PinToHome)="PinToHome()"
1639
+ (Share)="OpenShareDialog()">
1640
+ </mj-dashboard-add-to-menu>
1641
+ @if (CanShare) {
1642
+ <button
1643
+ type="button"
1644
+ mjButton
1645
+ Variant="icon"
1646
+ Size="sm"
1647
+ class="dashboard-tab-share"
1648
+ title="Share dashboard"
1649
+ aria-label="Share dashboard"
1650
+ (click)="OpenShareDialog()">
1651
+ <i class="fa-solid fa-share-nodes" aria-hidden="true"></i>
1652
+ </button>
1653
+ }
1654
+ </span>
1655
+ </mj-dashboard-editor>
1656
+ </as-split-area>
1657
+ @if (ChatOpen) {
1658
+ <as-split-area [size]="CopilotExpanded ? CopilotMaxPct : CopilotSizePct" [minSize]="CopilotMinPct" [maxSize]="CopilotMaxPct">
1659
+ <aside class="dashboard-copilot" [class.expanded]="CopilotExpanded" data-testid="dashboard-copilot" aria-label="AI assistant">
1660
+ <div class="dashboard-copilot-head">
1661
+ <div class="dashboard-copilot-title"><i class="fa-solid fa-robot"></i> {{ ChatAgentName ?? 'AI assistant' }}</div>
1662
+ <div class="dashboard-copilot-actions">
1663
+ <button
1664
+ type="button"
1665
+ class="dashboard-copilot-btn copilot-expand"
1666
+ [title]="CopilotExpanded ? 'Collapse panel' : 'Expand panel'"
1667
+ [attr.aria-label]="CopilotExpanded ? 'Collapse panel' : 'Expand panel'"
1668
+ (click)="ToggleCopilotExpanded()">
1669
+ <i class="fa-solid" [class.fa-up-right-and-down-left-from-center]="!CopilotExpanded" [class.fa-down-left-and-up-right-to-center]="CopilotExpanded"></i>
1670
+ </button>
1671
+ <button type="button" class="dashboard-copilot-btn copilot-close" title="Close assistant" aria-label="Close assistant" (click)="CloseChat()">
1672
+ <i class="fa-solid fa-xmark"></i>
1673
+ </button>
1674
+ </div>
1675
+ </div>
1676
+ <div class="dashboard-copilot-body">
1677
+ @if (ChatAgentId && ProviderToUse.CurrentUser) {
1678
+ <mj-conversation-chat-area
1679
+ [Provider]="Provider" [EnvironmentId]="ChatEnvironmentId" [CurrentUser]="ProviderToUse.CurrentUser"
1680
+ [Conversation]="ChatConversation" [ConversationId]="ChatConversationId" [IsNewConversation]="ChatIsNewConversation"
1681
+ [PendingMessage]="ChatPendingMessage" [PendingAttachments]="ChatPendingAttachments"
1682
+ [SuppressNewConversationEmptyState]="true" [AllowMentions]="false" [OverlayMode]="false"
1683
+ [ShowExportButton]="false" [ShowShareButton]="false" [ShowArtifactIndicator]="false"
1684
+ [ShowAgentPicker]="false" [ShowAgentModePicker]="false"
1685
+ [DefaultAgentId]="ChatAgentId" [AllowedAgentIDs]="[ChatAgentId]"
1686
+ ApplicationScope="Application" [ApplicationId]="ChatApplicationId" [AppContext]="ChatAppContext"
1687
+ EmptyStateGreeting="What should this dashboard show?"
1688
+ (ConversationCreated)="OnChatConversationCreated($event)"
1689
+ (RealtimeConversationReady)="OnChatRealtimeConversationReady($event)"
1690
+ (PendingMessageConsumed)="OnChatPendingMessageConsumed()"
1691
+ (navigationRequest)="OnChatNavigationRequest($event)"
1692
+ (OpenEntityRecord)="OnChatOpenEntityRecord($event)">
1693
+ </mj-conversation-chat-area>
1694
+ } @else if (ChatAgentError) {
1695
+ <div class="dashboard-copilot-empty" role="status">{{ ChatAgentError }}</div>
1696
+ } @else {
1697
+ <div class="dashboard-copilot-empty"><mj-loading Text="Connecting…" Size="small"></mj-loading></div>
1698
+ }
1699
+ </div>
1700
+ </aside>
1701
+ </as-split-area>
1702
+ }
1703
+ </as-split>
887
1704
  }
888
1705
 
889
- <!-- Dashboard Content Container -->
890
- <div #container class="dashboard-resource-container"></div>
1706
+ <!-- Code dashboards and the Data Explorer render here. It stays in the template, so the tab finds it when it starts. -->
1707
+ <div #container class="dashboard-resource-container" [class.is-hidden]="!!ConfigDashboardId"></div>
891
1708
 
892
1709
  <!-- Share Dashboard Dialog -->
893
- @if (configDashboard) {
1710
+ @if (ConfigDashboard) {
894
1711
  <mj-dashboard-share-dialog
895
- [Visible]="showShareDialog"
896
- [Dashboard]="configDashboard"
897
- (Result)="onShareDialogResult($event)">
1712
+ [Visible]="ShowShareDialog"
1713
+ [Dashboard]="ConfigDashboard"
1714
+ (Result)="OnShareDialogResult($event)">
898
1715
  </mj-dashboard-share-dialog>
899
1716
  }
1717
+
1718
+ <!-- Confirmation for an agent's Request* tool: the user, not the agent, saves or pins -->
1719
+ @if (AgentConfirm) {
1720
+ <mj-confirm-dialog
1721
+ class="agent-confirm"
1722
+ [Visible]="true"
1723
+ Type="info"
1724
+ [Title]="AgentConfirm.Title"
1725
+ [Message]="AgentConfirm.Message"
1726
+ [ConfirmText]="AgentConfirm.ConfirmText"
1727
+ CancelText="Not now"
1728
+ (Confirmed)="OnAgentConfirm(true)"
1729
+ (Cancelled)="OnAgentConfirm(false)">
1730
+ </mj-confirm-dialog>
1731
+ }
1732
+
1733
+ <!-- Closing the AI pane ends a voice session, so the user confirms it first -->
1734
+ @if (ShowCloseChatConfirm) {
1735
+ <mj-confirm-dialog
1736
+ class="close-chat-confirm"
1737
+ [Visible]="true"
1738
+ Type="warning"
1739
+ Title="End the voice session?"
1740
+ Message="Closing the AI assistant ends the voice session."
1741
+ ConfirmText="Close"
1742
+ CancelText="Keep open"
1743
+ (Confirmed)="OnCloseChatConfirmed()"
1744
+ (Cancelled)="OnCloseChatCancelled()">
1745
+ </mj-confirm-dialog>
1746
+ }
900
1747
  </div>
901
- `, styles: ["\n :host {\n display: block;\n width: 100%;\n height: 100%;\n position: relative;\n overflow: hidden;\n }\n .dashboard-resource-wrapper {\n display: flex;\n flex-direction: column;\n height: 100%;\n width: 100%;\n }\n .dashboard-resource-container {\n flex: 1;\n overflow: hidden;\n min-height: 0;\n }\n\n /* View Mode Toolbar */\n .viewer-toolbar {\n display: flex;\n align-items: center;\n justify-content: space-between;\n padding: 12px 24px;\n background: var(--mj-bg-surface-card);\n border-bottom: 1px solid var(--mj-border-default);\n gap: 16px;\n }\n .viewer-toolbar .toolbar-left {\n display: flex;\n align-items: center;\n gap: 12px;\n }\n .viewer-toolbar .dashboard-title {\n font-size: 16px;\n font-weight: 500;\n color: var(--mj-text-primary);\n display: flex;\n align-items: center;\n gap: 8px;\n }\n .viewer-toolbar .dashboard-title i {\n color: var(--mj-brand-primary);\n }\n .shared-indicator {\n display: flex;\n align-items: center;\n justify-content: center;\n width: 24px;\n height: 24px;\n border-radius: 50%;\n background: color-mix(in srgb, var(--mj-brand-primary) 10%, var(--mj-bg-surface));\n color: var(--mj-brand-primary);\n font-size: 11px;\n }\n .viewer-toolbar .toolbar-actions {\n display: flex;\n align-items: center;\n gap: 12px;\n }\n\n /* Edit Mode Header */\n .viewer-header {\n display: flex;\n align-items: center;\n justify-content: space-between;\n padding: 12px 24px;\n background: var(--mj-bg-surface-card);\n border-bottom: 1px solid var(--mj-border-default);\n transition: background 0.2s, border-color 0.2s;\n }\n .viewer-header.editing {\n background: linear-gradient(135deg, color-mix(in srgb, var(--mj-brand-primary) 10%, var(--mj-bg-surface)) 0%, color-mix(in srgb, var(--mj-brand-primary) 25%, var(--mj-bg-surface)) 100%);\n border-bottom: 2px solid var(--mj-brand-primary);\n }\n .viewer-header .header-left {\n display: flex;\n align-items: center;\n gap: 12px;\n flex: 1;\n }\n .viewer-header .header-right {\n display: flex;\n align-items: center;\n gap: 8px;\n }\n\n /* Add Part button */\n .btn-add-part {\n display: flex;\n align-items: center;\n gap: 8px;\n padding: 8px 16px;\n border: none;\n border-radius: 6px;\n background: var(--mj-brand-primary);\n color: var(--mj-text-inverse);\n font-size: 13px;\n font-weight: 500;\n cursor: pointer;\n transition: background 0.2s, transform 0.1s;\n box-shadow: 0 2px 4px color-mix(in srgb, var(--mj-brand-primary) 30%, transparent);\n }\n .btn-add-part:hover {\n background: var(--mj-brand-primary-hover);\n transform: translateY(-1px);\n box-shadow: 0 3px 6px color-mix(in srgb, var(--mj-brand-primary) 40%, transparent);\n }\n .btn-add-part i { font-size: 12px; }\n\n /* Header separator */\n .header-separator {\n width: 1px;\n height: 28px;\n background: color-mix(in srgb, var(--mj-brand-primary) 30%, transparent);\n margin: 0 4px;\n }\n\n /* Buttons */\n .btn-primary {\n display: flex;\n align-items: center;\n gap: 8px;\n padding: 10px 18px;\n border: none;\n border-radius: 6px;\n background: var(--mj-brand-primary);\n color: var(--mj-text-inverse);\n font-size: 14px;\n font-weight: 500;\n cursor: pointer;\n transition: background 0.2s;\n }\n .btn-primary:hover { background: var(--mj-brand-primary-hover); }\n\n .btn-icon {\n width: 36px;\n height: 36px;\n display: flex;\n align-items: center;\n justify-content: center;\n border: 1px solid var(--mj-border-default);\n border-radius: 6px;\n background: var(--mj-bg-surface-card);\n color: var(--mj-text-secondary);\n cursor: pointer;\n transition: all 0.2s;\n }\n .btn-icon:hover { background: var(--mj-bg-surface-sunken); }\n\n .btn-cancel {\n display: flex;\n align-items: center;\n gap: 8px;\n padding: 10px 18px;\n border: 1px solid var(--mj-border-default);\n border-radius: 6px;\n background: var(--mj-bg-surface-card);\n color: var(--mj-text-secondary);\n font-size: 14px;\n cursor: pointer;\n transition: all 0.2s;\n }\n .btn-cancel:hover {\n background: var(--mj-bg-surface-sunken);\n border-color: var(--mj-border-default);\n color: var(--mj-text-primary);\n }\n\n /* Dashboard info inputs */\n .dashboard-info-edit {\n display: flex;\n align-items: center;\n gap: 16px;\n flex: 1;\n }\n .dashboard-name-input {\n border: 1px solid transparent;\n border-radius: 4px;\n padding: 6px 12px;\n font-size: 16px;\n font-weight: 500;\n color: var(--mj-text-primary);\n background: rgba(255, 255, 255, 0.7);\n outline: none;\n min-width: 200px;\n max-width: 300px;\n transition: border-color 0.2s, background 0.2s, box-shadow 0.2s;\n }\n .dashboard-name-input:hover { background: rgba(255, 255, 255, 0.9); }\n .dashboard-name-input:focus {\n background: var(--mj-bg-surface-card);\n border-color: var(--mj-brand-primary);\n box-shadow: 0 0 0 2px color-mix(in srgb, var(--mj-brand-primary) 20%, transparent);\n }\n .dashboard-description-input {\n border: 1px solid transparent;\n border-radius: 4px;\n padding: 6px 12px;\n font-size: 13px;\n color: var(--mj-text-secondary);\n background: rgba(255, 255, 255, 0.5);\n outline: none;\n flex: 1;\n min-width: 150px;\n max-width: 400px;\n transition: border-color 0.2s, background 0.2s, box-shadow 0.2s;\n }\n .dashboard-description-input:hover { background: rgba(255, 255, 255, 0.8); }\n .dashboard-description-input:focus {\n background: var(--mj-bg-surface-card);\n border-color: var(--mj-brand-primary);\n box-shadow: 0 0 0 2px color-mix(in srgb, var(--mj-brand-primary) 20%, transparent);\n }\n .dashboard-description-input::placeholder {\n color: var(--mj-text-muted);\n font-style: normal;\n }\n\n /* Error state */\n .error-state {\n display: flex;\n flex-direction: column;\n align-items: center;\n justify-content: center;\n height: 100%;\n padding: 40px;\n text-align: center;\n color: var(--mj-text-secondary);\n }\n .error-icon {\n font-size: 64px;\n color: var(--mj-status-error);\n margin-bottom: 24px;\n opacity: 0.8;\n }\n .error-title {\n font-size: 24px;\n font-weight: 500;\n margin: 0 0 12px 0;\n color: var(--mj-text-primary);\n }\n .error-message {\n font-size: 16px;\n color: var(--mj-text-muted);\n margin: 0 0 24px 0;\n max-width: 500px;\n line-height: 1.5;\n }\n .error-details {\n background: var(--mj-bg-surface-sunken);\n border-radius: 8px;\n padding: 12px 16px;\n max-width: 600px;\n text-align: left;\n font-size: 13px;\n }\n .error-details summary {\n cursor: pointer;\n font-weight: 500;\n color: var(--mj-text-muted);\n margin-bottom: 8px;\n }\n .error-details pre {\n margin: 0;\n white-space: pre-wrap;\n word-break: break-word;\n color: var(--mj-status-error-text);\n font-family: 'Consolas', 'Monaco', monospace;\n font-size: 12px;\n }\n\n /* Responsive */\n @media (max-width: 768px) {\n .viewer-header {\n flex-direction: column;\n gap: 12px;\n align-items: stretch;\n }\n .viewer-header .header-left { flex-wrap: wrap; }\n .dashboard-info-edit {\n flex-direction: column;\n align-items: stretch;\n }\n .dashboard-name-input,\n .dashboard-description-input { max-width: none; }\n }\n "] }]
902
- }], () => [{ type: i0.ViewContainerRef }, { type: i0.ChangeDetectorRef }], { ContainerElement: [{
1748
+ `, styles: ["\n :host {\n display: block;\n width: 100%;\n height: 100%;\n position: relative;\n overflow: hidden;\n }\n .dashboard-resource-wrapper {\n display: flex;\n flex-direction: column;\n height: 100%;\n width: 100%;\n }\n /* The editor and the AI pane share the tab. The global MJ splitter theme styles the gutter. */\n .studio-copilot-splitter {\n flex: 1;\n width: 100%;\n height: 100%;\n min-height: 0;\n min-width: 0;\n }\n /* Holds a Code dashboard or the Data Explorer. Hidden while the editor shows a Config dashboard. */\n .dashboard-resource-container {\n width: 100%;\n height: 100%;\n overflow: hidden;\n }\n .dashboard-resource-container.is-hidden { display: none; }\n\n /* The view actions sit in the editor's header row as if they were its own buttons */\n .dashboard-tab-view-actions { display: contents; }\n /* The colour goes on the icon, never on the button: button.scss owns the button */\n .dashboard-tab-ai-toggle.is-active i { color: var(--mj-brand-primary); }\n\n /* AI pane */\n .dashboard-copilot {\n width: 100%;\n height: 100%;\n border-left: 1px solid var(--mj-border-default);\n background: var(--mj-bg-surface);\n display: flex;\n flex-direction: column;\n min-height: 0;\n }\n .dashboard-copilot-head {\n display: flex;\n align-items: center;\n justify-content: space-between;\n padding: 10px 14px;\n border-bottom: 1px solid var(--mj-border-default);\n }\n .dashboard-copilot-title {\n display: flex;\n align-items: center;\n gap: 8px;\n font-weight: 600;\n color: var(--mj-text-primary);\n }\n .dashboard-copilot-title i { color: var(--mj-brand-primary); }\n .dashboard-copilot-actions {\n display: flex;\n align-items: center;\n gap: 4px;\n }\n .dashboard-copilot-btn {\n background: transparent;\n border: none;\n cursor: pointer;\n padding: 6px 8px;\n border-radius: 6px;\n color: var(--mj-text-muted);\n }\n .dashboard-copilot-btn:hover {\n background: var(--mj-bg-surface-hover);\n color: var(--mj-text-secondary);\n }\n .dashboard-copilot-body {\n flex: 1;\n min-height: 0;\n overflow: hidden;\n display: flex;\n flex-direction: column;\n }\n .dashboard-copilot-body mj-conversation-chat-area {\n flex: 1;\n min-height: 0;\n display: block;\n }\n .dashboard-copilot-empty {\n display: flex;\n align-items: center;\n justify-content: center;\n flex: 1;\n padding: 16px;\n color: var(--mj-text-secondary);\n text-align: center;\n }\n\n /* Error state */\n .error-state {\n display: flex;\n flex-direction: column;\n align-items: center;\n justify-content: center;\n height: 100%;\n padding: 40px;\n text-align: center;\n color: var(--mj-text-secondary);\n }\n .error-icon {\n font-size: 64px;\n color: var(--mj-status-error);\n margin-bottom: 24px;\n opacity: 0.8;\n }\n .error-title {\n font-size: 24px;\n font-weight: 500;\n margin: 0 0 12px 0;\n color: var(--mj-text-primary);\n }\n .error-message {\n font-size: 16px;\n color: var(--mj-text-muted);\n margin: 0 0 24px 0;\n max-width: 500px;\n line-height: 1.5;\n }\n .error-details {\n background: var(--mj-bg-surface-sunken);\n border-radius: 8px;\n padding: 12px 16px;\n max-width: 600px;\n text-align: left;\n font-size: 13px;\n }\n .error-details summary {\n cursor: pointer;\n font-weight: 500;\n color: var(--mj-text-muted);\n margin-bottom: 8px;\n }\n .error-details pre {\n margin: 0;\n white-space: pre-wrap;\n word-break: break-word;\n color: var(--mj-status-error-text);\n font-family: 'Consolas', 'Monaco', monospace;\n font-size: 12px;\n }\n "] }]
1749
+ }], () => [{ type: i0.ViewContainerRef }, { type: i0.ChangeDetectorRef }], { editor: [{
1750
+ type: ViewChild,
1751
+ args: ['editor']
1752
+ }], ContainerElement: [{
903
1753
  type: ViewChild,
904
1754
  args: ['container', { static: true }]
905
1755
  }] }); })();
906
- (() => { (typeof ngDevMode === "undefined" || ngDevMode) && i0.ɵsetClassDebugInfo(DashboardResource, { className: "DashboardResource", filePath: "src/lib/resource-wrappers/dashboard-resource.component.ts", lineNumber: 410 }); })();
1756
+ (() => { (typeof ngDevMode === "undefined" || ngDevMode) && i0.ɵsetClassDebugInfo(DashboardResource, { className: "DashboardResource", filePath: "src/lib/resource-wrappers/dashboard-resource.component.ts", lineNumber: 429 }); })();
1757
+ function errorMessage(error) {
1758
+ return error instanceof Error ? error.message : String(error);
1759
+ }
1760
+ /** The title, question and confirm button text of the dialog that asks the user to do what the agent requests. */
1761
+ function agentConfirmText(kind, dashboardName, detail) {
1762
+ const name = `"${dashboardName}"`;
1763
+ switch (kind) {
1764
+ case 'save': {
1765
+ const rename = detail.name ? ` Its new name will be "${detail.name}".` : '';
1766
+ const describe = detail.description ? ` Its new description will be "${detail.description}".` : '';
1767
+ return { Title: 'Save dashboard?', Message: `The assistant asks to save ${name}.${rename}${describe}`, ConfirmText: 'Save' };
1768
+ }
1769
+ case 'pin':
1770
+ return { Title: 'Pin to Home?', Message: `The assistant asks to pin ${name} to your Home app.`, ConfirmText: 'Pin' };
1771
+ }
1772
+ }
1773
+ /** The user's own views and the shared views the user may see. None when the user cannot read views. */
1774
+ function viewSources() {
1775
+ const engine = UserViewEngine.Instance;
1776
+ if (engine.IsPermissionConstrained)
1777
+ return [];
1778
+ const shared = engine.GetSharedViews().filter(view => view.UserCanView);
1779
+ return [...engine.GetViewsForCurrentUser(), ...shared].map(view => ({
1780
+ ID: view.ID,
1781
+ Name: view.Name,
1782
+ Description: view.Description,
1783
+ Entity: view.Entity,
1784
+ EntityID: view.EntityID,
1785
+ }));
1786
+ }
1787
+ /** The queries the user can run. None when the user cannot read queries. */
1788
+ function querySources(user) {
1789
+ const engine = QueryEngine.Instance;
1790
+ if (engine.IsPermissionConstrained)
1791
+ return [];
1792
+ return engine.Queries.filter(query => query.UserCanRun(user).canRun).map(query => ({
1793
+ ID: query.ID,
1794
+ Name: query.Name,
1795
+ Description: query.Description,
1796
+ Category: query.Category,
1797
+ }));
1798
+ }
1799
+ /** The entities the user can read. */
1800
+ function entitySources(provider) {
1801
+ const user = provider.CurrentUser;
1802
+ return provider.Entities.filter(entity => entity.GetUserPermisions(user).CanRead).map(entity => ({
1803
+ ID: entity.ID,
1804
+ Name: entity.Name,
1805
+ DisplayName: entity.DisplayName,
1806
+ Description: entity.Description,
1807
+ }));
1808
+ }
1809
+ /**
1810
+ * The filter clause that keeps the artifacts whose name or description, in lower case, matches the query
1811
+ * as a LIKE pattern (see likeNarrowingPattern), or '' when the query is blank. The clause only narrows
1812
+ * the list: RankByQuery makes the exact match in the browser.
1813
+ */
1814
+ function artifactQueryFilter(query) {
1815
+ const pattern = likeNarrowingPattern(query);
1816
+ return pattern ? ` AND (LOWER(Name) LIKE '%${pattern}%' OR LOWER(Description) LIKE '%${pattern}%')` : '';
1817
+ }
1818
+ /**
1819
+ * The query without its outer spaces as a lower-case LIKE pattern that matches the query, and can match
1820
+ * more, in the same way on SQL Server and PostgreSQL. It needs no ESCAPE clause, which the server refuses
1821
+ * in a client's filter. Each quote, backslash and LIKE wildcard (`%`, `_`, `[`, `]`) becomes `_`, which
1822
+ * matches any one character. Each parenthesis becomes `_` too: the PostgreSQL provider rewrites date
1823
+ * functions such as `GETDATE()` in the whole filter, string literals included. Each other character that is
1824
+ * not ASCII becomes `_`, so the literal needs no `N` prefix, with two exceptions that become `%` (see
1825
+ * nonAsciiWildcard). The text is put in lower case last, when it holds only ASCII.
1826
+ */
1827
+ function likeNarrowingPattern(query) {
1828
+ const ascii = query
1829
+ .trim()
1830
+ .replace(/[\\%_[\]'()]/g, '_')
1831
+ .replace(/[\u0080-\u{10FFFF}]/gu, char => nonAsciiWildcard(char));
1832
+ return EscapeSQLString(ascii.toLowerCase());
1833
+ }
1834
+ /**
1835
+ * The LIKE wildcard for a character that is not ASCII: `_`, which matches one character, or `%` where one
1836
+ * character can be two on some database. SQL Server counts a character outside the BMP as two characters and
1837
+ * PostgreSQL as one. `İ` (U+0130) is one character, but ICU collations put it in lower case as `i` and a
1838
+ * combining dot.
1839
+ */
1840
+ function nonAsciiWildcard(char) {
1841
+ return char.length > 1 || char === '\u0130' ? '%' : '_';
1842
+ }
1843
+ /** Records in `latest` the highest version number of each artifact in `rows`, by normalized artifact ID. */
1844
+ function addLatestVersions(latest, rows) {
1845
+ for (const row of rows) {
1846
+ const key = NormalizeUUID(row.ArtifactID);
1847
+ latest.set(key, Math.max(latest.get(key) ?? 0, row.VersionNumber));
1848
+ }
1849
+ }
1850
+ /** The items in consecutive groups of at most `size`. */
1851
+ function chunked(items, size) {
1852
+ const groups = [];
1853
+ for (let start = 0; start < items.length; start += size) {
1854
+ groups.push(items.slice(start, start + size));
1855
+ }
1856
+ return groups;
1857
+ }
907
1858
  //# sourceMappingURL=dashboard-resource.component.js.map