@cognizant-ai-lab/ui-common 1.3.3

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 (108) hide show
  1. package/dist/components/AgentChat/ChatCommon.d.ts +94 -0
  2. package/dist/components/AgentChat/ChatCommon.js +581 -0
  3. package/dist/components/AgentChat/ControlButtons.d.ts +16 -0
  4. package/dist/components/AgentChat/ControlButtons.js +24 -0
  5. package/dist/components/AgentChat/FormattedMarkdown.d.ts +32 -0
  6. package/dist/components/AgentChat/FormattedMarkdown.js +82 -0
  7. package/dist/components/AgentChat/Greetings.d.ts +1 -0
  8. package/dist/components/AgentChat/Greetings.js +38 -0
  9. package/dist/components/AgentChat/LlmChatButton.d.ts +12 -0
  10. package/dist/components/AgentChat/LlmChatButton.js +33 -0
  11. package/dist/components/AgentChat/SendButton.d.ts +12 -0
  12. package/dist/components/AgentChat/SendButton.js +28 -0
  13. package/dist/components/AgentChat/SyntaxHighlighterThemes.d.ts +14 -0
  14. package/dist/components/AgentChat/SyntaxHighlighterThemes.js +27 -0
  15. package/dist/components/AgentChat/Types.d.ts +17 -0
  16. package/dist/components/AgentChat/Types.js +26 -0
  17. package/dist/components/AgentChat/UserQueryDisplay.d.ts +5 -0
  18. package/dist/components/AgentChat/UserQueryDisplay.js +33 -0
  19. package/dist/components/AgentChat/Utils.d.ts +11 -0
  20. package/dist/components/AgentChat/Utils.js +64 -0
  21. package/dist/components/AgentChat/VoiceChat/MicrophoneButton.d.ts +29 -0
  22. package/dist/components/AgentChat/VoiceChat/MicrophoneButton.js +55 -0
  23. package/dist/components/AgentChat/VoiceChat/VoiceChat.d.ts +33 -0
  24. package/dist/components/AgentChat/VoiceChat/VoiceChat.js +180 -0
  25. package/dist/components/Authentication/Auth.d.ts +14 -0
  26. package/dist/components/Authentication/Auth.js +58 -0
  27. package/dist/components/ChatBot/ChatBot.d.ts +20 -0
  28. package/dist/components/ChatBot/ChatBot.js +75 -0
  29. package/dist/components/Common/Breadcrumbs.d.ts +6 -0
  30. package/dist/components/Common/Breadcrumbs.js +36 -0
  31. package/dist/components/Common/LlmChatOptionsButton.d.ts +9 -0
  32. package/dist/components/Common/LlmChatOptionsButton.js +31 -0
  33. package/dist/components/Common/LoadingSpinner.d.ts +10 -0
  34. package/dist/components/Common/LoadingSpinner.js +24 -0
  35. package/dist/components/Common/MUIAccordion.d.ts +17 -0
  36. package/dist/components/Common/MUIAccordion.js +76 -0
  37. package/dist/components/Common/MUIAlert.d.ts +11 -0
  38. package/dist/components/Common/MUIAlert.js +41 -0
  39. package/dist/components/Common/MUIDialog.d.ts +16 -0
  40. package/dist/components/Common/MUIDialog.js +40 -0
  41. package/dist/components/Common/Navbar.d.ts +15 -0
  42. package/dist/components/Common/Navbar.js +137 -0
  43. package/dist/components/Common/PageLoader.d.ts +5 -0
  44. package/dist/components/Common/PageLoader.js +26 -0
  45. package/dist/components/Common/Snackbar.d.ts +5 -0
  46. package/dist/components/Common/Snackbar.js +84 -0
  47. package/dist/components/Common/confirmationModal.d.ts +14 -0
  48. package/dist/components/Common/confirmationModal.js +65 -0
  49. package/dist/components/Common/notification.d.ts +18 -0
  50. package/dist/components/Common/notification.js +79 -0
  51. package/dist/components/ErrorPage/ErrorBoundary.d.ts +38 -0
  52. package/dist/components/ErrorPage/ErrorBoundary.js +77 -0
  53. package/dist/components/ErrorPage/ErrorPage.d.ts +12 -0
  54. package/dist/components/ErrorPage/ErrorPage.js +46 -0
  55. package/dist/components/MultiAgentAccelerator/AgentFlow.d.ts +21 -0
  56. package/dist/components/MultiAgentAccelerator/AgentFlow.js +394 -0
  57. package/dist/components/MultiAgentAccelerator/AgentNode.d.ts +18 -0
  58. package/dist/components/MultiAgentAccelerator/AgentNode.js +129 -0
  59. package/dist/components/MultiAgentAccelerator/GraphLayouts.d.ts +33 -0
  60. package/dist/components/MultiAgentAccelerator/GraphLayouts.js +297 -0
  61. package/dist/components/MultiAgentAccelerator/MultiAgentAccelerator.d.ts +17 -0
  62. package/dist/components/MultiAgentAccelerator/MultiAgentAccelerator.js +208 -0
  63. package/dist/components/MultiAgentAccelerator/PlasmaEdge.d.ts +3 -0
  64. package/dist/components/MultiAgentAccelerator/PlasmaEdge.js +124 -0
  65. package/dist/components/MultiAgentAccelerator/Sidebar.d.ts +12 -0
  66. package/dist/components/MultiAgentAccelerator/Sidebar.js +204 -0
  67. package/dist/components/MultiAgentAccelerator/ThoughtBubbleEdge.d.ts +12 -0
  68. package/dist/components/MultiAgentAccelerator/ThoughtBubbleEdge.js +15 -0
  69. package/dist/components/MultiAgentAccelerator/ThoughtBubbleOverlay.d.ts +11 -0
  70. package/dist/components/MultiAgentAccelerator/ThoughtBubbleOverlay.js +466 -0
  71. package/dist/components/MultiAgentAccelerator/const.d.ts +7 -0
  72. package/dist/components/MultiAgentAccelerator/const.js +39 -0
  73. package/dist/const.d.ts +10 -0
  74. package/dist/const.js +30 -0
  75. package/dist/controller/agent/Agent.d.ts +56 -0
  76. package/dist/controller/agent/Agent.js +162 -0
  77. package/dist/controller/llm/LlmChat.d.ts +18 -0
  78. package/dist/controller/llm/LlmChat.js +65 -0
  79. package/dist/controller/llm/endpoints.d.ts +1 -0
  80. package/dist/controller/llm/endpoints.js +17 -0
  81. package/dist/generated/neuro-san/NeuroSanClient.d.ts +413 -0
  82. package/dist/generated/neuro-san/NeuroSanClient.js +28 -0
  83. package/dist/index.d.ts +37 -0
  84. package/dist/index.js +52 -0
  85. package/dist/state/UserInfo.d.ts +16 -0
  86. package/dist/state/UserInfo.js +27 -0
  87. package/dist/state/environment.d.ts +18 -0
  88. package/dist/state/environment.js +33 -0
  89. package/dist/tsconfig.build.tsbuildinfo +1 -0
  90. package/dist/utils/Authentication.d.ts +31 -0
  91. package/dist/utils/Authentication.js +94 -0
  92. package/dist/utils/BrowserNavigation.d.ts +5 -0
  93. package/dist/utils/BrowserNavigation.js +22 -0
  94. package/dist/utils/Theme.d.ts +7 -0
  95. package/dist/utils/Theme.js +7 -0
  96. package/dist/utils/agentConversations.d.ts +24 -0
  97. package/dist/utils/agentConversations.js +113 -0
  98. package/dist/utils/text.d.ts +28 -0
  99. package/dist/utils/text.js +64 -0
  100. package/dist/utils/title.d.ts +1 -0
  101. package/dist/utils/title.js +20 -0
  102. package/dist/utils/types.d.ts +17 -0
  103. package/dist/utils/types.js +16 -0
  104. package/dist/utils/useLocalStorage.d.ts +1 -0
  105. package/dist/utils/useLocalStorage.js +55 -0
  106. package/dist/utils/zIndexLayers.d.ts +2 -0
  107. package/dist/utils/zIndexLayers.js +29 -0
  108. package/package.json +69 -0
@@ -0,0 +1,46 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ /*
3
+ Copyright 2025 Cognizant Technology Solutions Corp, www.cognizant.com.
4
+
5
+ Licensed under the Apache License, Version 2.0 (the "License");
6
+ you may not use this file except in compliance with the License.
7
+ You may obtain a copy of the License at
8
+
9
+ http://www.apache.org/licenses/LICENSE-2.0
10
+
11
+ Unless required by applicable law or agreed to in writing, software
12
+ distributed under the License is distributed on an "AS IS" BASIS,
13
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ See the License for the specific language governing permissions and
15
+ limitations under the License.
16
+ */
17
+ import Box from "@mui/material/Box";
18
+ import { useRouter } from "next/router.js";
19
+ import { LOGO } from "../../const.js";
20
+ import { useEnvironmentStore } from "../../state/environment.js";
21
+ import { useUserInfoStore } from "../../state/UserInfo.js";
22
+ import { smartSignOut, useAuthentication } from "../../utils/Authentication.js";
23
+ import { NeuroAIBreadcrumbs } from "../Common/Breadcrumbs.js";
24
+ import { Navbar } from "../Common/Navbar.js";
25
+ /**
26
+ * This is the page that will be shown to users when the outer error boundary is triggered
27
+ * @param id HTML id for the <code>div</code> for this page
28
+ * @param errorText Error text to be displayed
29
+ */
30
+ export default function ErrorPage({ id, errorText }) {
31
+ const { auth0ClientId, auth0Domain, supportEmailAddress } = useEnvironmentStore();
32
+ // Access NextJS router
33
+ const router = useRouter();
34
+ // Access user info store
35
+ const { currentUser, setCurrentUser, setPicture, oidcProvider } = useUserInfoStore();
36
+ // Infer authentication type
37
+ const authenticationType = currentUser ? `ALB using ${oidcProvider}` : "NextAuth";
38
+ const { data: { user: userInfo } = {} } = useAuthentication();
39
+ async function handleSignOut() {
40
+ // Clear our state storage variables
41
+ setCurrentUser(undefined);
42
+ setPicture(undefined);
43
+ await smartSignOut(currentUser, auth0Domain, auth0ClientId, oidcProvider);
44
+ }
45
+ return (_jsxs(_Fragment, { children: [_jsx(Navbar, { id: "nav-bar", logo: LOGO, query: router.query, pathname: router.pathname, authenticationType: authenticationType, signOut: handleSignOut, supportEmailAddress: supportEmailAddress, userInfo: userInfo }), _jsxs(Box, { id: id, sx: { marginLeft: "1rem" }, children: [_jsx(NeuroAIBreadcrumbs, {}), _jsx("h4", { id: "error-header", style: { color: "var(--bs-red)", marginTop: "1rem" }, children: "Oops, an internal error occurred on our end." }), _jsxs("div", { id: "error-div-1", children: [_jsxs(Box, { id: "no-need-to-worry", sx: { marginBottom: "1.5rem" }, children: [_jsx("b", { id: "bold-1", children: "There's no need to worry \u2013 you didn't do anything wrong." }), " This is a bug in our server and we'll fix it as soon as possible."] }), _jsx(Box, { id: "try-these-steps", sx: { marginBottom: "1.5rem" }, children: "To attempt to recover from the error, try these steps:" }), _jsx(Box, { id: "refresh-page", sx: { marginBottom: "1.5rem" }, children: _jsxs("ul", { id: "error-boundary-advice-list", className: "list-decimal list-inside", children: [_jsx("li", { id: "error-advice-refresh", children: "Refresh the page to see if the error goes away." }), _jsx("li", { id: "error-advice-reload", children: "Close and re-open your browser, then navigate back to the page where you encountered the error." })] }) }), "If none of that helps, please report the following error to the LEAF team:"] }), _jsx("div", { id: "error-text-div", style: { fontFamily: "monospace", marginTop: "1.5rem", marginBottom: "1.5rem" }, children: errorText }), "More information may be available in the browser console. Please include such information in your report if possible."] })] }));
46
+ }
@@ -0,0 +1,21 @@
1
+ import { Dispatch, FC, SetStateAction } from "react";
2
+ import { Edge, EdgeProps } from "reactflow";
3
+ import { ConnectivityInfo } from "../../generated/neuro-san/NeuroSanClient.js";
4
+ import { AgentConversation } from "../../utils/agentConversations.js";
5
+ export interface AgentFlowProps {
6
+ readonly agentCounts?: Map<string, number>;
7
+ readonly agentsInNetwork: ConnectivityInfo[];
8
+ readonly currentConversations?: AgentConversation[] | null;
9
+ readonly id: string;
10
+ readonly isAwaitingLlm?: boolean;
11
+ readonly isStreaming?: boolean;
12
+ readonly thoughtBubbleEdges: Map<string, {
13
+ edge: Edge<EdgeProps>;
14
+ timestamp: number;
15
+ }>;
16
+ readonly setThoughtBubbleEdges: Dispatch<SetStateAction<Map<string, {
17
+ edge: Edge<EdgeProps>;
18
+ timestamp: number;
19
+ }>>>;
20
+ }
21
+ export declare const AgentFlow: FC<AgentFlowProps>;
@@ -0,0 +1,394 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ /*
3
+ Copyright 2025 Cognizant Technology Solutions Corp, www.cognizant.com.
4
+
5
+ Licensed under the Apache License, Version 2.0 (the "License");
6
+ you may not use this file except in compliance with the License.
7
+ You may obtain a copy of the License at
8
+
9
+ http://www.apache.org/licenses/LICENSE-2.0
10
+
11
+ Unless required by applicable law or agreed to in writing, software
12
+ distributed under the License is distributed on an "AS IS" BASIS,
13
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ See the License for the specific language governing permissions and
15
+ limitations under the License.
16
+ */
17
+ import AdjustRoundedIcon from "@mui/icons-material/AdjustRounded";
18
+ import ChatBubbleOutlineIcon from "@mui/icons-material/ChatBubbleOutline";
19
+ import HubOutlinedIcon from "@mui/icons-material/HubOutlined";
20
+ import ScatterPlotOutlinedIcon from "@mui/icons-material/ScatterPlotOutlined";
21
+ import { ToggleButton, ToggleButtonGroup, useColorScheme, useTheme } from "@mui/material";
22
+ import Box from "@mui/material/Box";
23
+ import Tooltip from "@mui/material/Tooltip";
24
+ import Typography from "@mui/material/Typography";
25
+ import { useCallback, useEffect, useMemo, useRef, useState } from "react";
26
+ import { applyNodeChanges, Background, ConnectionMode, ControlButton, Controls, ReactFlow, useReactFlow, useStore, } from "reactflow";
27
+ import { AgentNode, NODE_HEIGHT, NODE_WIDTH } from "./AgentNode.js";
28
+ import { BACKGROUND_COLORS, BACKGROUND_COLORS_DARK_IDX, BASE_RADIUS, DEFAULT_FRONTMAN_X_POS, DEFAULT_FRONTMAN_Y_POS, HEATMAP_COLORS, LEVEL_SPACING, } from "./const.js";
29
+ import { addThoughtBubbleEdge, layoutLinear, layoutRadial, removeThoughtBubbleEdge } from "./GraphLayouts.js";
30
+ import { PlasmaEdge } from "./PlasmaEdge.js";
31
+ import { ThoughtBubbleEdge } from "./ThoughtBubbleEdge.js";
32
+ import { ThoughtBubbleOverlay } from "./ThoughtBubbleOverlay.js";
33
+ import { isDarkMode } from "../../utils/Theme.js";
34
+ import { getZIndex } from "../../utils/zIndexLayers.js";
35
+ // #endregion: Types
36
+ // #region: Constants
37
+ // Timeout for thought bubbles is set to 10 seconds
38
+ const THOUGHT_BUBBLE_TIMEOUT_MS = 10_000;
39
+ // Maximum number of thought bubbles allowed on screen at any time. As more arrive, older ones are removed.
40
+ const MAX_THOUGHT_BUBBLES = 5;
41
+ // #endregion: Constants
42
+ export const AgentFlow = ({ agentCounts, agentsInNetwork, currentConversations, id, isAwaitingLlm, isStreaming, thoughtBubbleEdges, setThoughtBubbleEdges, }) => {
43
+ const theme = useTheme();
44
+ const { fitView } = useReactFlow();
45
+ // Helper functions to update thought bubble edges state immutably
46
+ const addThoughtBubbleEdgeHelper = useCallback((conversationId, edge) => {
47
+ setThoughtBubbleEdges((prev) => {
48
+ const newMap = new Map(prev);
49
+ addThoughtBubbleEdge(newMap, conversationId, edge);
50
+ return newMap;
51
+ });
52
+ }, [setThoughtBubbleEdges]);
53
+ const removeThoughtBubbleEdgeHelper = useCallback((conversationId) => {
54
+ setThoughtBubbleEdges((prev) => {
55
+ const newMap = new Map(prev);
56
+ removeThoughtBubbleEdge(newMap, conversationId);
57
+ return newMap;
58
+ });
59
+ }, [setThoughtBubbleEdges]);
60
+ const handleResize = useCallback(() => {
61
+ fitView(); // Adjusts the view to fit after resizing
62
+ }, [fitView, isAwaitingLlm]);
63
+ useEffect(() => {
64
+ window.addEventListener("resize", handleResize);
65
+ return () => window.removeEventListener("resize", handleResize);
66
+ }, [handleResize]);
67
+ // Save this as a mutable ref so child nodes see updates
68
+ const conversationsRef = useRef(currentConversations);
69
+ useEffect(() => {
70
+ conversationsRef.current = currentConversations;
71
+ }, [currentConversations]);
72
+ const [layout, setLayout] = useState("radial");
73
+ const [coloringOption, setColoringOption] = useState("depth");
74
+ const [enableRadialGuides, setEnableRadialGuides] = useState(true);
75
+ const [showThoughtBubbles, setShowThoughtBubbles] = useState(true);
76
+ // State for managing active thought bubbles with timers. Use ActiveThoughtBubble which
77
+ // references the original conversation via `conversationId`.
78
+ const [activeThoughtBubbles, setActiveThoughtBubbles] = useState([]);
79
+ // Track conversation IDs we've already processed to prevent re-adding after expiry
80
+ const processedConversationIdsRef = useRef(new Set());
81
+ // Track which bubble is currently being hovered
82
+ const hoveredBubbleIdRef = useRef(null);
83
+ const handleBubbleHoverChange = useCallback((bubbleId) => {
84
+ hoveredBubbleIdRef.current = bubbleId;
85
+ }, []);
86
+ // Clear processed conversation IDs when thought bubble edges are cleared (streaming ends)
87
+ useEffect(() => {
88
+ if (thoughtBubbleEdges.size === 0) {
89
+ processedConversationIdsRef.current.clear();
90
+ }
91
+ }, [thoughtBubbleEdges.size]);
92
+ // Effect to add and remove thought bubbles - only when we actually have conversations to process
93
+ useEffect(() => {
94
+ if (!currentConversations || currentConversations.length === 0) {
95
+ return; // Skip processing when no conversations
96
+ }
97
+ setActiveThoughtBubbles((prevBubbles) => {
98
+ const newBubbles = [];
99
+ // Check the text that user sees as one level of duplicate prevention
100
+ const processedText = new Set();
101
+ thoughtBubbleEdges.forEach((thoughtBubbleEdge) => {
102
+ const edgeText = thoughtBubbleEdge.edge.data?.text?.trim();
103
+ if (edgeText) {
104
+ // Add edge text to prevent duplicates
105
+ processedText.add(edgeText);
106
+ }
107
+ });
108
+ // Only add bubbles for conversations with unique parsed content
109
+ // TODO: Even though there are two duplicate checks here, some duplicates still get through
110
+ for (const conv of currentConversations) {
111
+ const convText = conv.text?.trim();
112
+ if (
113
+ // Has text
114
+ convText &&
115
+ // Haven't already displayed this text (duplicate check #1)
116
+ !processedText.has(convText) &&
117
+ // Haven't already displayed this conversation ID (duplicate check #2)
118
+ !processedConversationIdsRef.current.has(conv.id)) {
119
+ // Create an AgentConversation object representing the active thought bubble.
120
+ // Use the conversation id from the incoming conversation and set startedAt
121
+ // to "now" so the bubble timeout is measured from when the bubble appeared.
122
+ newBubbles.push({
123
+ agents: new Set(conv.agents),
124
+ conversationId: conv.id,
125
+ startedAt: new Date(),
126
+ type: conv.type,
127
+ // No text needed. This is for tracking which conversations are active.
128
+ });
129
+ // Mark this conversation ID as processed. This protects against duplicates if conversations have
130
+ // the same ID, although some duplicates still get through.
131
+ processedConversationIdsRef.current.add(conv.id);
132
+ const agentList = Array.from(conv.agents);
133
+ // TODO: Check if agentList has at least 2 agents. This allows us to set a source and target.
134
+ // However, this can also have more than 2 agents. We may want to think more about that case.
135
+ if (agentList.length >= 2) {
136
+ // These source and target agents are going to be useful later when we point bubbles to nodes.
137
+ const sourceAgent = agentList[0];
138
+ const targetAgent = agentList[1];
139
+ const edge = {
140
+ id: `thought-bubble-${conv.id}`,
141
+ source: sourceAgent,
142
+ target: targetAgent,
143
+ type: "thoughtBubbleEdge",
144
+ data: {
145
+ text: conv.text,
146
+ showAlways: showThoughtBubbles,
147
+ conversationId: conv.id,
148
+ // Include full agent list so overlays can point to all participants
149
+ agents: agentList,
150
+ type: conv.type, // Add conversation type for filtering
151
+ },
152
+ style: { pointerEvents: "none" },
153
+ };
154
+ addThoughtBubbleEdgeHelper(conv.id, edge);
155
+ }
156
+ }
157
+ }
158
+ if (newBubbles.length === 0) {
159
+ return prevBubbles; // No changes
160
+ }
161
+ const allBubbles = [...prevBubbles, ...newBubbles];
162
+ // If we're over the limit, first remove expired bubbles, then oldest if still needed
163
+ if (allBubbles.length > MAX_THOUGHT_BUBBLES) {
164
+ const now = Date.now();
165
+ const dropped = [];
166
+ // First, filter out expired bubbles by checking THOUGHT_BUBBLE_TIMEOUT_MS
167
+ const activeBubbles = allBubbles.filter((bubble) => {
168
+ const age = now - bubble.startedAt.getTime();
169
+ const isExpired = age >= THOUGHT_BUBBLE_TIMEOUT_MS;
170
+ if (isExpired) {
171
+ dropped.push(bubble);
172
+ }
173
+ return !isExpired;
174
+ });
175
+ // If still over the limit after removing expired, remove oldest non-expired
176
+ let finalBubbles = activeBubbles;
177
+ if (activeBubbles.length > MAX_THOUGHT_BUBBLES) {
178
+ const sorted = activeBubbles.sort((a, b) => a.startedAt.getTime() - b.startedAt.getTime());
179
+ finalBubbles = sorted.slice(-MAX_THOUGHT_BUBBLES);
180
+ const additionalDropped = sorted.slice(0, -MAX_THOUGHT_BUBBLES);
181
+ dropped.push(...additionalDropped);
182
+ }
183
+ // Clean up all dropped bubbles from edge cache
184
+ dropped.forEach((bubble) => {
185
+ removeThoughtBubbleEdgeHelper(bubble.conversationId);
186
+ });
187
+ return finalBubbles;
188
+ }
189
+ return allBubbles;
190
+ });
191
+ }, [currentConversations, addThoughtBubbleEdgeHelper, removeThoughtBubbleEdgeHelper]);
192
+ // Independent thought bubble cleanup - run timer only during streaming
193
+ useEffect(() => {
194
+ const cleanupInterval = setInterval(() => {
195
+ // Only clean up if we're currently streaming
196
+ if (!isStreaming)
197
+ return;
198
+ setActiveThoughtBubbles((prevBubbles) => {
199
+ if (prevBubbles.length === 0)
200
+ return prevBubbles;
201
+ const now = Date.now();
202
+ const filteredBubbles = prevBubbles.filter((bubble) => {
203
+ const age = now - bubble.startedAt.getTime();
204
+ const shouldKeep = age < THOUGHT_BUBBLE_TIMEOUT_MS;
205
+ // Keep bubble if it's being hovered, even if expired
206
+ const isHovered = hoveredBubbleIdRef.current === `thought-bubble-${bubble.conversationId}`;
207
+ if (isHovered) {
208
+ return true;
209
+ }
210
+ // If bubble is expiring, remove it from the edge cache
211
+ if (!shouldKeep) {
212
+ removeThoughtBubbleEdgeHelper(bubble.conversationId);
213
+ }
214
+ return shouldKeep;
215
+ });
216
+ // Only update if there are changes to prevent unnecessary re-renders
217
+ if (filteredBubbles.length !== prevBubbles.length) {
218
+ return filteredBubbles;
219
+ }
220
+ return prevBubbles;
221
+ });
222
+ }, 1000); // Check every second
223
+ return () => clearInterval(cleanupInterval);
224
+ }, [isStreaming, removeThoughtBubbleEdgeHelper]);
225
+ const { mode, systemMode } = useColorScheme();
226
+ const darkMode = isDarkMode(mode, systemMode);
227
+ // Shadow color for icon. TODO: use MUI theme system instead.
228
+ const shadowColor = darkMode ? "255, 255, 255" : "0, 0, 0";
229
+ const isHeatmap = coloringOption === "heatmap";
230
+ // Merge agents from active thought bubbles with agentsInNetwork for layout
231
+ // This ensures bubble edges persist even when agents disappear from the network
232
+ const bubbleAgentIds = useMemo(() => {
233
+ const ids = new Set();
234
+ activeThoughtBubbles.forEach((bubble) => {
235
+ bubble.agents.forEach((agentId) => ids.add(agentId));
236
+ });
237
+ return ids;
238
+ }, [activeThoughtBubbles]);
239
+ const mergedAgentsInNetwork = useMemo(() => {
240
+ // Add any missing agents from bubbles as minimal ConnectivityInfo
241
+ const existingIds = new Set(agentsInNetwork.map((a) => a.origin));
242
+ const missing = Array.from(bubbleAgentIds).filter((bubbleAgentId) => !existingIds.has(bubbleAgentId));
243
+ const minimalAgents = missing.map((missingId) => ({
244
+ origin: missingId,
245
+ tools: [],
246
+ display_as: undefined,
247
+ }));
248
+ return [...agentsInNetwork, ...minimalAgents];
249
+ }, [agentsInNetwork, bubbleAgentIds]);
250
+ // Create the flow layout depending on user preference
251
+ // Memoize layoutResult so it only recalculates when relevant data changes
252
+ const layoutResult = useMemo(() => layout === "linear"
253
+ ? layoutLinear(isHeatmap ? agentCounts : undefined, mergedAgentsInNetwork, currentConversations, isAwaitingLlm, thoughtBubbleEdges)
254
+ : layoutRadial(isHeatmap ? agentCounts : undefined, mergedAgentsInNetwork, currentConversations, isAwaitingLlm, thoughtBubbleEdges), [
255
+ layout,
256
+ coloringOption,
257
+ agentCounts,
258
+ mergedAgentsInNetwork,
259
+ currentConversations,
260
+ activeThoughtBubbles,
261
+ thoughtBubbleEdges,
262
+ isAwaitingLlm,
263
+ showThoughtBubbles,
264
+ ]);
265
+ const [nodes, setNodes] = useState(layoutResult.nodes);
266
+ // Sync up the nodes with the layout result
267
+ useEffect(() => {
268
+ setNodes(layoutResult.nodes);
269
+ }, [layoutResult.nodes]);
270
+ const edges = layoutResult.edges;
271
+ useEffect(() => {
272
+ // Schedule a fitView after the layout is set to ensure the view is adjusted correctly
273
+ setTimeout(() => {
274
+ fitView();
275
+ }, 50);
276
+ }, [agentsInNetwork, layout]);
277
+ const onNodesChange = useCallback((changes) => {
278
+ setNodes((ns) => applyNodeChanges(
279
+ // For now, we only allow dragging, no updates
280
+ changes.filter((c) => c.type === "position"), ns));
281
+ }, []);
282
+ const transform = useStore((state) => state.transform);
283
+ // Why not just a "const"? See: https://reactflow.dev/learn/customization/custom-nodes
284
+ // "It’s important that the nodeTypes are memoized or defined outside the component. Otherwise, React creates
285
+ // a new object on every render which leads to performance issues and bugs."
286
+ const nodeTypes = useMemo(() => ({
287
+ agentNode: AgentNode,
288
+ }), [AgentNode]);
289
+ const edgeTypes = useMemo(() => ({
290
+ plasmaEdge: PlasmaEdge,
291
+ thoughtBubbleEdge: ThoughtBubbleEdge,
292
+ }), [PlasmaEdge, ThoughtBubbleEdge]);
293
+ // Figure out the maximum depth of the network
294
+ const maxDepth = useMemo(() => {
295
+ return nodes?.reduce((max, node) => Math.max(node.data.depth, max), 0) + 1;
296
+ }, [nodes]);
297
+ // Generate radial guides for the network to guide the eye in the radial layout
298
+ const getRadialGuides = () => {
299
+ const circles = Array.from({ length: maxDepth }).map((_, i) => (_jsx("circle", { id: `radial-guide-${BASE_RADIUS + (i + 1) * LEVEL_SPACING}`, cx: DEFAULT_FRONTMAN_X_POS + NODE_WIDTH / 2, cy: DEFAULT_FRONTMAN_Y_POS + NODE_HEIGHT / 2, r: BASE_RADIUS + (i + 1) * LEVEL_SPACING, stroke: "var(--bs-gray-medium)", fill: "none", opacity: "0.25" }, `radial-guide-${BASE_RADIUS + (i + 1) * LEVEL_SPACING}`)));
300
+ return (_jsx("svg", { id: `${id}-radial-guides`, style: {
301
+ position: "absolute",
302
+ left: 0,
303
+ top: 0,
304
+ width: "100%",
305
+ height: "100%",
306
+ }, children: _jsx("g", { id: `${id}-radial-guides-group`, transform: `translate(${transform[0]}, ${transform[1]}) scale(${transform[2]})`, children: circles }) }));
307
+ };
308
+ // Generate Legend for depth or heatmap colors
309
+ function getLegend() {
310
+ const palette = isHeatmap ? HEATMAP_COLORS : BACKGROUND_COLORS;
311
+ const title = isHeatmap ? "Heat" : "Depth";
312
+ const length = isHeatmap ? HEATMAP_COLORS.length : Math.min(maxDepth, BACKGROUND_COLORS.length);
313
+ return (_jsxs(Box, { id: `${id}-legend`, sx: {
314
+ position: "absolute",
315
+ top: "5px",
316
+ right: "10px",
317
+ padding: "5px",
318
+ borderRadius: "5px",
319
+ boxShadow: `0 0 5px rgba(${shadowColor}, 0.3)`,
320
+ display: "flex",
321
+ alignItems: "center",
322
+ zIndex: getZIndex(2, theme),
323
+ }, children: [_jsx(Typography, { id: `${id}-legend-label`, sx: {
324
+ fontSize: "10px",
325
+ marginLeft: "0.2rem",
326
+ }, children: title }), Array.from({ length }, (_, i) => (_jsx(Box, { id: `${id}-legend-depth-${i}`, style: {
327
+ alignItems: "center",
328
+ backgroundColor: palette[i],
329
+ borderRadius: "50%",
330
+ color: i < BACKGROUND_COLORS_DARK_IDX ? "var(--bs-primary)" : "var(--bs-white)",
331
+ display: "flex",
332
+ height: "15px",
333
+ justifyContent: "center",
334
+ marginLeft: "5px",
335
+ width: "15px",
336
+ }, children: _jsx(Typography, { id: `${id}-legend-depth-${i}-text`, sx: {
337
+ fontSize: "8px",
338
+ }, children: i }) }, i))), _jsxs(ToggleButtonGroup, { id: `${id}-coloring-toggle`, value: coloringOption, exclusive: true, onChange: (_, newValue) => {
339
+ if (newValue !== null) {
340
+ setColoringOption(newValue);
341
+ }
342
+ }, sx: {
343
+ fontSize: "2rem",
344
+ zIndex: 10,
345
+ marginLeft: "0.5rem",
346
+ }, size: "small", children: [_jsx(ToggleButton, { id: `${id}-depth-toggle`, size: "small", value: "depth", sx: {
347
+ fontSize: "0.5rem",
348
+ height: "1rem",
349
+ backgroundColor: darkMode && coloringOption === "depth" ? "var(--bs-gray-medium)" : undefined,
350
+ }, children: _jsx(Typography, { id: `${id}-depth-label`, sx: {
351
+ fontSize: "10px",
352
+ }, children: "Depth" }) }), _jsx(ToggleButton, { id: `${id}-heatmap-toggle`, size: "small", value: "heatmap", sx: {
353
+ fontSize: "0.5rem",
354
+ height: "1rem",
355
+ backgroundColor: darkMode && isHeatmap ? "var(--bs-gray-medium)" : undefined,
356
+ }, children: _jsx(Typography, { id: `${id}-heatmap-label`, sx: {
357
+ fontSize: "10px",
358
+ }, children: "Heatmap" }) })] })] }));
359
+ }
360
+ // Get the background color for the control buttons based on the layout and dark mode setting
361
+ const getControlButtonBackgroundColor = (isActive) => {
362
+ return isActive ? (darkMode ? "var(--bs-gray-dark)" : "var(--bs-gray-lighter)") : undefined;
363
+ };
364
+ // Only show radial guides if radial layout is selected, radial guides are enabled, and it's not just Frontman
365
+ const shouldShowRadialGuides = enableRadialGuides && layout === "radial" && maxDepth > 1;
366
+ // Generate the control bar for the flow, including layout and radial guides toggles
367
+ const getControls = () => {
368
+ return (_jsxs(Controls, { id: "react-flow-controls", position: "top-left", style: {
369
+ position: "absolute",
370
+ top: "0px",
371
+ left: "0px",
372
+ height: "auto",
373
+ width: "auto",
374
+ }, showInteractive: true, children: [_jsx(Tooltip, { id: "radial-layout-tooltip", title: "Radial layout", placement: "right", children: _jsx("span", { id: "radial-layout-span", children: _jsx(ControlButton, { id: "radial-layout-button", onClick: () => setLayout("radial"), style: {
375
+ backgroundColor: getControlButtonBackgroundColor(layout === "radial"),
376
+ }, children: _jsx(HubOutlinedIcon, { id: "radial-layout-icon", sx: { color: darkMode ? "var(--bs-white)" : "var(--bs-dark-mode-dim)" } }) }) }) }), _jsx(Tooltip, { id: "linear-layout-tooltip", title: "Linear layout", placement: "right", children: _jsx("span", { id: "linear-layout-span", children: _jsx(ControlButton, { id: "linear-layout-button", onClick: () => setLayout("linear"), style: {
377
+ backgroundColor: getControlButtonBackgroundColor(layout === "linear"),
378
+ }, children: _jsx(ScatterPlotOutlinedIcon, { id: "linear-layout-icon", sx: { color: darkMode ? "var(--bs-white)" : "var(--bs-dark-mode-dim)" } }) }) }) }), _jsx(Tooltip, { id: "radial-guides-tooltip", title: `Enable/disable radial guides${layout === "radial" ? "" : " (only available in radial layout)"}`, placement: "right", children: _jsx("span", { id: "radial-guides-span", children: _jsx(ControlButton, { id: "radial-guides-button", onClick: () => setEnableRadialGuides(!enableRadialGuides), style: {
379
+ backgroundColor: getControlButtonBackgroundColor(enableRadialGuides),
380
+ }, disabled: layout !== "radial", children: _jsx(AdjustRoundedIcon, { id: "radial-guides-icon", sx: { color: darkMode ? "var(--bs-white)" : "var(--bs-dark-mode-dim)" } }) }) }) }), _jsx(Tooltip, { id: "thought-bubble-tooltip", title: `Toggle thought bubbles ${showThoughtBubbles ? "off" : "on"}`, placement: "right", children: _jsx("span", { id: "thought-bubble-span", children: _jsx(ControlButton, { id: "thought-bubble-button", onClick: () => setShowThoughtBubbles(!showThoughtBubbles), style: {
381
+ backgroundColor: getControlButtonBackgroundColor(showThoughtBubbles),
382
+ }, children: _jsx(ChatBubbleOutlineIcon, { id: "thought-bubble-icon", sx: { color: darkMode ? "var(--bs-white)" : "var(--bs-dark-mode-dim)" } }) }) }) })] }));
383
+ };
384
+ return (_jsxs(Box, { id: `${id}-outer-box`, sx: {
385
+ height: "100%",
386
+ width: "100%",
387
+ "& .react-flow__node": {
388
+ border: "var(--bs-border-width) var(--bs-border-style) var(--bs-black)",
389
+ borderRadius: "var(--bs-border-radius-2xl)",
390
+ },
391
+ },
392
+ // Theme the "React Flow" attribution logo according to dark mode.
393
+ className: darkMode ? "dark" : undefined, children: [_jsx(ReactFlow, { id: `${id}-react-flow`, nodes: nodes, edges: edges, onNodesChange: onNodesChange, fitView: true, nodeTypes: nodeTypes, edgeTypes: edgeTypes, connectionMode: ConnectionMode.Loose, children: !isAwaitingLlm && (_jsxs(_Fragment, { children: [agentsInNetwork?.length ? getLegend() : null, _jsx(Background, { id: `${id}-background` }), getControls(), shouldShowRadialGuides ? getRadialGuides() : null] })) }), _jsx(ThoughtBubbleOverlay, { nodes: nodes, edges: edges, showThoughtBubbles: showThoughtBubbles, isStreaming: isStreaming, onBubbleHoverChange: handleBubbleHoverChange })] }));
394
+ };
@@ -0,0 +1,18 @@
1
+ import { FC } from "react";
2
+ import { NodeProps } from "reactflow";
3
+ import { AgentConversation } from "../../utils/agentConversations.js";
4
+ export interface AgentNodeProps {
5
+ readonly agentCounts?: Map<string, number>;
6
+ readonly agentName: string;
7
+ readonly depth: number;
8
+ readonly getConversations: () => AgentConversation[] | null;
9
+ readonly isAwaitingLlm?: boolean;
10
+ readonly displayAs?: string;
11
+ }
12
+ export declare const NODE_HEIGHT = 100;
13
+ export declare const NODE_WIDTH = 100;
14
+ /**
15
+ * A node representing an agent in the network for use in react-flow.
16
+ * @param props See AgentNodeProps
17
+ */
18
+ export declare const AgentNode: FC<NodeProps<AgentNodeProps>>;
@@ -0,0 +1,129 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ /*
3
+ Copyright 2025 Cognizant Technology Solutions Corp, www.cognizant.com.
4
+
5
+ Licensed under the Apache License, Version 2.0 (the "License");
6
+ you may not use this file except in compliance with the License.
7
+ You may obtain a copy of the License at
8
+
9
+ http://www.apache.org/licenses/LICENSE-2.0
10
+
11
+ Unless required by applicable law or agreed to in writing, software
12
+ distributed under the License is distributed on an "AS IS" BASIS,
13
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ See the License for the specific language governing permissions and
15
+ limitations under the License.
16
+ */
17
+ import AutoAwesomeIcon from "@mui/icons-material/AutoAwesome";
18
+ import HandymanIcon from "@mui/icons-material/Handyman";
19
+ import PersonIcon from "@mui/icons-material/Person";
20
+ import TravelExploreIcon from "@mui/icons-material/TravelExplore";
21
+ import { Tooltip, useTheme } from "@mui/material";
22
+ import Typography from "@mui/material/Typography";
23
+ import { Handle, Position } from "reactflow";
24
+ import { BACKGROUND_COLORS, BACKGROUND_COLORS_DARK_IDX, HEATMAP_COLORS } from "./const.js";
25
+ import { getZIndex } from "../../utils/zIndexLayers.js";
26
+ // Node dimensions
27
+ export const NODE_HEIGHT = 100;
28
+ export const NODE_WIDTH = 100;
29
+ // Icon sizes
30
+ // These are used to set the size of the icons displayed in the agent nodes.
31
+ const AGENT_ICON_SIZE = "2.25rem";
32
+ const FRONTMAN_ICON_SIZE = "4.5rem";
33
+ /**
34
+ * A node representing an agent in the network for use in react-flow.
35
+ * @param props See AgentNodeProps
36
+ */
37
+ export const AgentNode = (props) => {
38
+ const theme = useTheme();
39
+ // Unpack the node-specific data
40
+ const data = props.data;
41
+ const { agentCounts, agentName, depth, displayAs, getConversations, isAwaitingLlm } = data;
42
+ const isFrontman = depth === 0;
43
+ const maxAgentCount = agentCounts ? Math.max(...Array.from(agentCounts.values())) : 0;
44
+ // Unpack the node-specific id
45
+ const agentId = props.id;
46
+ // "Active" agents are those at either end of the current communication from the incoming chat messages.
47
+ // We highlight them with a green background.
48
+ const conversations = getConversations();
49
+ const isActiveAgent = conversations?.some((conversation) => conversation.agents.has(agentId)) ?? false;
50
+ let backgroundColor;
51
+ let color;
52
+ const isHeatmap = agentCounts?.size > 0 && maxAgentCount > 0;
53
+ if (isActiveAgent) {
54
+ backgroundColor = "var(--bs-green)";
55
+ color = "var(--bs-white)";
56
+ }
57
+ else if (isHeatmap) {
58
+ const agentCount = agentCounts.has(agentId) ? agentCounts.get(agentId) : 0;
59
+ // Calculate "heat" as a fraction of the times this agent was invoked compared to the maximum agent count.
60
+ const colorIndex = Math.floor((agentCount / maxAgentCount) * (HEATMAP_COLORS.length - 1));
61
+ backgroundColor = HEATMAP_COLORS[colorIndex];
62
+ const isDarkBackground = colorIndex >= BACKGROUND_COLORS_DARK_IDX;
63
+ color = isDarkBackground ? "var(--bs-white)" : "var(--bs-dark)";
64
+ }
65
+ else {
66
+ const colorIndex = depth % BACKGROUND_COLORS.length;
67
+ backgroundColor = BACKGROUND_COLORS[colorIndex];
68
+ const isDarkBackground = colorIndex >= BACKGROUND_COLORS_DARK_IDX;
69
+ color = isDarkBackground ? "var(--bs-white)" : "var(--bs-dark)";
70
+ }
71
+ // Animation style for making active agent glow and pulse
72
+ // TODO: more idiomatic MUI/style= way of doing this?
73
+ const glowAnimation = isActiveAgent
74
+ ? `@keyframes glow {
75
+ 0% { box-shadow: 0 0 10px 4px ${backgroundColor}; opacity: 0.60; }
76
+ 50% { box-shadow: 0 0 30px 12px ${backgroundColor}; opacity: 0.90; }
77
+ 100% { box-shadow: 0 0 10px 4px ${backgroundColor}; opacity: 1.0; }
78
+ }`
79
+ : "none";
80
+ const boxShadow = isActiveAgent ? "0 0 30px 12px var(--bs-primary), 0 0 60px 24px var(--bs-primary)" : undefined;
81
+ // Hide handles when awaiting LLM response ("zen mode")
82
+ const handleVisibility = isAwaitingLlm ? "none" : "block";
83
+ // Determine which icon to display based on the agent type whether it is Frontman or not
84
+ const getDisplayAsIcon = () => {
85
+ const id = `${agentId}-icon`;
86
+ if (isFrontman) {
87
+ return (
88
+ // Use special icon and larger size for Frontman
89
+ _jsx(PersonIcon, { id: id, sx: { fontSize: FRONTMAN_ICON_SIZE } }));
90
+ }
91
+ switch (displayAs) {
92
+ case "external_agent":
93
+ return (_jsx(TravelExploreIcon, { id: id, sx: { fontSize: AGENT_ICON_SIZE } }));
94
+ case "coded_tool":
95
+ return (_jsx(HandymanIcon, { id: id, sx: { fontSize: AGENT_ICON_SIZE } }));
96
+ case "llm_agent":
97
+ default:
98
+ return (_jsx(AutoAwesomeIcon, { id: id, sx: { fontSize: AGENT_ICON_SIZE } }));
99
+ }
100
+ };
101
+ return (_jsxs(_Fragment, { children: [_jsxs("div", { id: agentId, style: {
102
+ alignItems: "center",
103
+ animation: isActiveAgent ? "glow 2.0s infinite" : "none",
104
+ backgroundColor,
105
+ borderRadius: "50%",
106
+ boxShadow,
107
+ color,
108
+ display: "flex",
109
+ height: NODE_HEIGHT * (isFrontman ? 1.25 : 1.0),
110
+ justifyContent: "center",
111
+ shapeOutside: "circle(50%)",
112
+ textAlign: "center",
113
+ width: NODE_WIDTH * (isFrontman ? 1.25 : 1.0),
114
+ zIndex: getZIndex(1, theme),
115
+ position: "relative",
116
+ }, children: [_jsx("style", { id: `${agentId}-glow-animation`, children: glowAnimation }), getDisplayAsIcon(), _jsx(Handle, { id: `${agentId}-left-handle`, position: Position.Left, type: "source", style: { display: handleVisibility } }), _jsx(Handle, { id: `${agentId}-right-handle`, position: Position.Right, type: "source", style: { display: handleVisibility } }), _jsx(Handle, { id: `${agentId}-top-handle`, position: Position.Top, type: "source", style: { display: handleVisibility } }), _jsx(Handle, { id: `${agentId}-bottom-handle`, position: Position.Bottom, type: "source", style: { display: handleVisibility } })] }), _jsx(Tooltip, { id: `${agentId}-tooltip`, title: agentName, placement: "top", disableInteractive: true, children: _jsx(Typography, { id: `${agentId}-name`, sx: {
117
+ display: "-webkit-box",
118
+ fontSize: "18px",
119
+ fontWeight: "bold",
120
+ lineHeight: "1.4em",
121
+ overflow: "hidden",
122
+ textAlign: "center",
123
+ textOverflow: "ellipsis",
124
+ WebkitBoxOrient: "vertical",
125
+ WebkitLineClamp: 2,
126
+ width: `${NODE_WIDTH}px`,
127
+ zIndex: 10,
128
+ }, children: agentName }) })] }));
129
+ };
@@ -0,0 +1,33 @@
1
+ import { Edge, EdgeProps, Node as RFNode } from "reactflow";
2
+ import { AgentNodeProps } from "./AgentNode.js";
3
+ import { ConnectivityInfo } from "../../generated/neuro-san/NeuroSanClient.js";
4
+ import { AgentConversation } from "../../utils/agentConversations.js";
5
+ export declare const MAX_GLOBAL_THOUGHT_BUBBLES = 5;
6
+ export declare const addThoughtBubbleEdge: (thoughtBubbleEdges: Map<string, {
7
+ edge: Edge<EdgeProps>;
8
+ timestamp: number;
9
+ }>, conversationId: string, edge: Edge<EdgeProps>) => void;
10
+ export declare const removeThoughtBubbleEdge: (thoughtBubbleEdges: Map<string, {
11
+ edge: Edge<EdgeProps>;
12
+ timestamp: number;
13
+ }>, conversationId: string) => void;
14
+ export declare const getThoughtBubbleEdges: (thoughtBubbleEdges: Map<string, {
15
+ edge: Edge<EdgeProps>;
16
+ timestamp: number;
17
+ }>) => Edge<EdgeProps>[];
18
+ export declare const layoutRadial: (agentCounts: Map<string, number>, agentsInNetwork: ConnectivityInfo[], currentConversations: AgentConversation[] | null, // For plasma edges (live) and node highlighting
19
+ isAwaitingLlm: boolean, thoughtBubbleEdges: Map<string, {
20
+ edge: Edge<EdgeProps>;
21
+ timestamp: number;
22
+ }>) => {
23
+ nodes: RFNode<AgentNodeProps>[];
24
+ edges: Edge<EdgeProps>[];
25
+ };
26
+ export declare const layoutLinear: (agentCounts: Map<string, number>, agentsInNetwork: ConnectivityInfo[], currentConversations: AgentConversation[] | null, // For plasma edges (live) and node highlighting
27
+ isAwaitingLlm: boolean, thoughtBubbleEdges: Map<string, {
28
+ edge: Edge<EdgeProps>;
29
+ timestamp: number;
30
+ }>) => {
31
+ nodes: RFNode<AgentNodeProps>[];
32
+ edges: Edge<EdgeProps>[];
33
+ };