@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,297 @@
1
+ /*
2
+ Copyright 2025 Cognizant Technology Solutions Corp, www.cognizant.com.
3
+
4
+ Licensed under the Apache License, Version 2.0 (the "License");
5
+ you may not use this file except in compliance with the License.
6
+ You may obtain a copy of the License at
7
+
8
+ http://www.apache.org/licenses/LICENSE-2.0
9
+
10
+ Unless required by applicable law or agreed to in writing, software
11
+ distributed under the License is distributed on an "AS IS" BASIS,
12
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ See the License for the specific language governing permissions and
14
+ limitations under the License.
15
+ */
16
+ /**
17
+ * Graph layout algorithms and associated functions for the agent network.
18
+ */
19
+ import dagre from "dagre";
20
+ import cloneDeep from "lodash-es/cloneDeep.js";
21
+ import { MarkerType } from "reactflow";
22
+ import { NODE_HEIGHT, NODE_WIDTH } from "./AgentNode.js";
23
+ import { BASE_RADIUS, DEFAULT_FRONTMAN_X_POS, DEFAULT_FRONTMAN_Y_POS, LEVEL_SPACING } from "./const.js";
24
+ import { cleanUpAgentName, KNOWN_MESSAGE_TYPES_FOR_PLASMA } from "../AgentChat/Utils.js";
25
+ export const MAX_GLOBAL_THOUGHT_BUBBLES = 5;
26
+ export const addThoughtBubbleEdge = (thoughtBubbleEdges, conversationId, edge) => {
27
+ // Add with timestamp for age-based cleanup
28
+ thoughtBubbleEdges.set(conversationId, {
29
+ edge,
30
+ timestamp: Date.now(),
31
+ });
32
+ // Enforce max limit - remove oldest if over limit
33
+ if (thoughtBubbleEdges.size > MAX_GLOBAL_THOUGHT_BUBBLES) {
34
+ const entries = Array.from(thoughtBubbleEdges.entries());
35
+ const sorted = entries.sort((a, b) => a[1].timestamp - b[1].timestamp);
36
+ const toRemove = sorted.slice(0, sorted.length - MAX_GLOBAL_THOUGHT_BUBBLES);
37
+ toRemove.forEach(([id]) => {
38
+ thoughtBubbleEdges.delete(id);
39
+ });
40
+ }
41
+ };
42
+ export const removeThoughtBubbleEdge = (thoughtBubbleEdges, conversationId) => {
43
+ thoughtBubbleEdges.delete(conversationId);
44
+ };
45
+ export const getThoughtBubbleEdges = (thoughtBubbleEdges) => {
46
+ return Array.from(thoughtBubbleEdges.values()).map((item) => item.edge);
47
+ };
48
+ // Helper function for plasma edges to check if two agents are in the same conversation
49
+ const areInSameConversation = (conversations, sourceAgent, targetAgent) => {
50
+ if (!conversations)
51
+ return false;
52
+ return conversations
53
+ .filter((conversation) => KNOWN_MESSAGE_TYPES_FOR_PLASMA.includes(conversation.type))
54
+ .some((conversation) => conversation.agents.has(sourceAgent) && conversation.agents.has(targetAgent));
55
+ };
56
+ // #region: Constants
57
+ // Name for custom node
58
+ const AGENT_NODE_TYPE_NAME = "agentNode";
59
+ // #endregion: Constants
60
+ /**
61
+ * Returns the "origins" (node names) of the _immediate_ parents of a node in the agent network. Grandparents and
62
+ * higher are not included.
63
+ *
64
+ * @param node Node ID for which to find parents
65
+ * @param parentAgents Full list of parent agents in the network
66
+ * @returns The IDs of the immediate parent nodes for the given node or empty array if no parents are found (frontman)
67
+ */
68
+ const getParents = (node, parentAgents) => {
69
+ return parentAgents.filter((agent) => agent.tools.includes(node)).map((parentNode) => parentNode.origin);
70
+ };
71
+ // "Parent agents" are those that have tools, aka "child agents"
72
+ const getParentAgents = (agentsInNetwork) => agentsInNetwork.length === 1 ? agentsInNetwork : agentsInNetwork.filter((agent) => agent.tools?.length > 0);
73
+ // "Child agents" are those that are declared as tools by other agents
74
+ const getChildAgents = (parentAgents) => {
75
+ const childAgentsSet = new Set();
76
+ parentAgents.forEach((agent) => {
77
+ agent?.tools?.forEach((tool) => {
78
+ childAgentsSet.add(tool);
79
+ });
80
+ });
81
+ return childAgentsSet;
82
+ };
83
+ // Frontman is defined as the agent that has no parent. There should be only one in the graph.
84
+ // Frontman is one of the parent agents.
85
+ // There is no explicit field that tells us which agent is the frontman, so we determine it by checking
86
+ // which parent agent does not appear in the set of child agents.
87
+ const getFrontman = (parentAgents, childAgents) => parentAgents.find((agent) => !childAgents.has(agent.origin));
88
+ // Generates the properties for an edge in the graph.
89
+ // Common for both radial and linear layouts.
90
+ const getEdgeProperties = (sourceId, targetId, sourceHandle, targetHandle, isAnimated) => {
91
+ return {
92
+ animated: false,
93
+ id: `${targetId}-edge-${sourceId}`,
94
+ markerEnd: { type: MarkerType.ArrowClosed, width: 30, height: 30 },
95
+ source: sourceId,
96
+ sourceHandle,
97
+ target: targetId,
98
+ targetHandle,
99
+ type: isAnimated ? "plasmaEdge" : undefined,
100
+ };
101
+ };
102
+ export const layoutRadial = (agentCounts, agentsInNetwork, currentConversations, // For plasma edges (live) and node highlighting
103
+ isAwaitingLlm, thoughtBubbleEdges) => {
104
+ const nodesInNetwork = [];
105
+ const edgesInNetwork = [];
106
+ // Compute depth of each node using breadth-first traversal
107
+ const nodeDepths = new Map();
108
+ const queue = [];
109
+ const parentAgents = getParentAgents(agentsInNetwork);
110
+ const childAgents = getChildAgents(parentAgents);
111
+ const frontman = getFrontman(parentAgents, childAgents);
112
+ if (frontman) {
113
+ // Add the frontman node to the network
114
+ queue.push({ id: frontman.origin, depth: 0 });
115
+ nodeDepths.set(frontman.origin, 0);
116
+ }
117
+ // Perform a breadth-first traversal of the tree to compute the depth of each node.
118
+ while (queue.length > 0) {
119
+ const { id: currentNodeId, depth } = queue.shift();
120
+ agentsInNetwork.forEach(({ origin: nodeId }) => {
121
+ // For each node, check if it is a direct child of the current node in the queue.
122
+ const parentIds = getParents(nodeId, parentAgents);
123
+ if (parentIds?.includes(currentNodeId) && !nodeDepths.has(nodeId)) {
124
+ // It's a child of the current node so set its depth to the depth of the current node + 1.
125
+ nodeDepths.set(nodeId, depth + 1);
126
+ queue.push({ id: nodeId, depth: depth + 1 });
127
+ }
128
+ });
129
+ }
130
+ // Construct a map where keys are depths and values are arrays of node IDs at that depth.
131
+ const nodesByDepth = new Map();
132
+ nodeDepths.forEach((depth, nodeId) => {
133
+ if (!nodesByDepth.has(depth)) {
134
+ nodesByDepth.set(depth, []);
135
+ }
136
+ nodesByDepth.get(depth).push(nodeId);
137
+ });
138
+ // Assign layout positions based on depth & spread angles per level
139
+ // Use currentConversations for plasma edges and node highlighting (cleared at network end)
140
+ nodesByDepth.forEach((nodeIds, depth) => {
141
+ const radius = BASE_RADIUS + depth * LEVEL_SPACING;
142
+ const angleStep = (2 * Math.PI) / nodeIds.length; // Divide full circle among nodes at this level
143
+ nodeIds.forEach((nodeId, index) => {
144
+ const angle = index * angleStep; // Spread nodes evenly in their depth level
145
+ const x = DEFAULT_FRONTMAN_X_POS + radius * Math.cos(angle);
146
+ const y = DEFAULT_FRONTMAN_Y_POS + radius * Math.sin(angle);
147
+ const isFrontman = frontman?.origin === nodeId;
148
+ const parentNodes = getParents(nodeId, parentAgents);
149
+ // Create an edge from each parent node to this node
150
+ for (const parentNode of parentNodes) {
151
+ const graphNode = nodesInNetwork.find((node) => node.id === parentNode);
152
+ // Determine if the agent is left or right of its parent node for symmetrical layout
153
+ if (graphNode) {
154
+ const dx = x - graphNode.position.x;
155
+ const dy = y - graphNode.position.y;
156
+ let sourceHandle;
157
+ let targetHandle;
158
+ // Determine the handle based on the direction of the node relative to its parent
159
+ if (Math.abs(dx) > Math.abs(dy)) {
160
+ // More horizontal: use left/right handles
161
+ const isLeftOfParent = dx < 0;
162
+ sourceHandle = isLeftOfParent ? `${graphNode.id}-left-handle` : `${graphNode.id}-right-handle`;
163
+ targetHandle = isLeftOfParent ? `${nodeId}-right-handle` : `${nodeId}-left-handle`;
164
+ }
165
+ else {
166
+ // More vertical: use top/bottom handles
167
+ const isAboveParent = dy < 0;
168
+ sourceHandle = isAboveParent ? `${graphNode.id}-top-handle` : `${graphNode.id}-bottom-handle`;
169
+ targetHandle = isAboveParent ? `${nodeId}-bottom-handle` : `${nodeId}-top-handle`;
170
+ }
171
+ // Plasma edges based on currentConversations (live, cleared at network end)
172
+ const isEdgeAnimated = areInSameConversation(currentConversations, nodeId, graphNode.id);
173
+ // Add edge from parent to node
174
+ if (!isAwaitingLlm || isEdgeAnimated) {
175
+ edgesInNetwork.push(getEdgeProperties(graphNode.id, nodeId, sourceHandle, targetHandle, isEdgeAnimated));
176
+ }
177
+ }
178
+ }
179
+ nodesInNetwork.push({
180
+ id: nodeId,
181
+ type: AGENT_NODE_TYPE_NAME,
182
+ data: {
183
+ agentCounts,
184
+ agentName: cleanUpAgentName(nodeId),
185
+ depth,
186
+ displayAs: agentsInNetwork.find((a) => a.origin === nodeId)?.display_as,
187
+ // Use current conversations for node highlighting (cleared at end)
188
+ getConversations: () => currentConversations,
189
+ isAwaitingLlm,
190
+ },
191
+ position: isFrontman ? { x: DEFAULT_FRONTMAN_X_POS, y: DEFAULT_FRONTMAN_Y_POS } : { x, y },
192
+ style: {
193
+ border: "none",
194
+ background: "transparent",
195
+ boxShadow: "none",
196
+ padding: 0,
197
+ margin: 0,
198
+ },
199
+ });
200
+ });
201
+ });
202
+ // Add thought bubble edges from cache to avoid duplicates across layout recalculations
203
+ const bubbleEdges = getThoughtBubbleEdges(thoughtBubbleEdges);
204
+ const thoughtBubbleEdgesToAdd = bubbleEdges.filter((edge) => edgesInNetwork.every((existing) => existing.id !== edge.id && edge.type === "thoughtBubbleEdge"));
205
+ edgesInNetwork.push(...thoughtBubbleEdgesToAdd);
206
+ return { nodes: nodesInNetwork, edges: edgesInNetwork };
207
+ };
208
+ export const layoutLinear = (agentCounts, agentsInNetwork, currentConversations, // For plasma edges (live) and node highlighting
209
+ isAwaitingLlm, thoughtBubbleEdges) => {
210
+ const nodesInNetwork = [];
211
+ const edgesInNetwork = [];
212
+ // Do these calculations outside the loop for efficiency
213
+ const parentAgents = getParentAgents(agentsInNetwork);
214
+ const childAgents = getChildAgents(parentAgents);
215
+ const frontman = getFrontman(parentAgents, childAgents);
216
+ agentsInNetwork.forEach(({ origin: originOfNode }) => {
217
+ const parentIds = getParents(originOfNode, parentAgents);
218
+ const isFrontman = frontman?.origin === originOfNode;
219
+ nodesInNetwork.push({
220
+ id: originOfNode,
221
+ type: AGENT_NODE_TYPE_NAME,
222
+ data: {
223
+ agentCounts,
224
+ agentName: cleanUpAgentName(originOfNode),
225
+ displayAs: agentsInNetwork.find((a) => a.origin === originOfNode)?.display_as,
226
+ // Use current conversations for node highlighting (cleared at end)
227
+ getConversations: () => currentConversations,
228
+ isAwaitingLlm,
229
+ depth: undefined, // Depth will be computed later
230
+ },
231
+ position: isFrontman ? { x: DEFAULT_FRONTMAN_X_POS, y: DEFAULT_FRONTMAN_Y_POS } : { x: 0, y: 0 },
232
+ style: {
233
+ border: "none",
234
+ background: "transparent",
235
+ boxShadow: "none",
236
+ padding: 0,
237
+ margin: 0,
238
+ },
239
+ });
240
+ if (!isFrontman) {
241
+ for (const parentNode of parentIds) {
242
+ // Add edges from parents to node
243
+ const isEdgeAnimated = areInSameConversation(currentConversations, parentNode, originOfNode);
244
+ // Include all edges here, since dagre needs them to compute the layout correctly.
245
+ // We will filter them later if we're in "awaiting LLM" mode.
246
+ edgesInNetwork.push(getEdgeProperties(parentNode, originOfNode, `${parentNode}-right-handle`, `${originOfNode}-left-handle`, isEdgeAnimated));
247
+ }
248
+ }
249
+ });
250
+ // Add thought bubble edges from cache to avoid duplicates across layout recalculations
251
+ const bubbleEdges = getThoughtBubbleEdges(thoughtBubbleEdges);
252
+ const thoughtBubbleEdgesToAdd = bubbleEdges.filter((edge) => edgesInNetwork.every((existing) => existing.id !== edge.id && edge.type === "thoughtBubbleEdge"));
253
+ edgesInNetwork.push(...thoughtBubbleEdgesToAdd);
254
+ const dagreGraph = new dagre.graphlib.Graph();
255
+ dagreGraph.setDefaultEdgeLabel(() => ({}));
256
+ // Configure for left-to-right layout
257
+ dagreGraph.setGraph({ rankdir: "LR" });
258
+ // Don't want to update nodes directly in existing flow so make a copy
259
+ const nodesTmp = cloneDeep(nodesInNetwork);
260
+ nodesTmp.forEach((node) => {
261
+ dagreGraph.setNode(node.id, { width: NODE_WIDTH, height: NODE_HEIGHT });
262
+ });
263
+ // Add edges to the dagre graph
264
+ edgesInNetwork.forEach((edge) => {
265
+ dagreGraph.setEdge(edge.source, edge.target);
266
+ });
267
+ // Compute the layout using dagre
268
+ dagre.layout(dagreGraph);
269
+ // Get x positions for the nodes in nodesTmp. Keep only unique values and sort numerically
270
+ const xPositions = Array.from(new Set(nodesTmp.map((node) => dagreGraph.node(node.id).x))).sort((a, b) => a - b);
271
+ // Convert dagre's layout to what our flow graph needs
272
+ nodesTmp.forEach((node) => {
273
+ const nodeWithPosition = dagreGraph.node(node.id);
274
+ // We are shifting the dagre node position (anchor=center center) to the top left
275
+ // so it matches the React Flow node anchor point (top left).
276
+ node.position = {
277
+ x: nodeWithPosition.x - NODE_WIDTH / 2,
278
+ y: nodeWithPosition.y - NODE_HEIGHT / 2,
279
+ };
280
+ // Depth is index of x position in xPositions array
281
+ // Create a new data object with updated depth
282
+ node.data = {
283
+ ...node.data,
284
+ depth: xPositions.indexOf(nodeWithPosition.x),
285
+ };
286
+ });
287
+ // If we're in "awaiting LLM" mode, we filter edges to only include those that are between conversation agents.
288
+ // Use currentConversations (plasma edges are live and clear at network end)
289
+ const filteredEdges = isAwaitingLlm
290
+ ? edgesInNetwork.filter((edge) => areInSameConversation(currentConversations, edge.source, edge.target))
291
+ : edgesInNetwork;
292
+ // Add thought bubble edges from cache to avoid duplicates across layout recalculations
293
+ const globalBubbleEdges = getThoughtBubbleEdges(thoughtBubbleEdges);
294
+ const thoughtBubbles = globalBubbleEdges.filter((edge) => filteredEdges.every((existing) => existing.id !== edge.id));
295
+ filteredEdges.push(...thoughtBubbles);
296
+ return { nodes: nodesTmp, edges: filteredEdges };
297
+ };
@@ -0,0 +1,17 @@
1
+ import { JSX as ReactJSX } from "react";
2
+ interface MultiAgentAcceleratorProps {
3
+ readonly userInfo: {
4
+ userName: string;
5
+ userImage: string;
6
+ };
7
+ readonly backendNeuroSanApiUrl: string;
8
+ readonly darkMode: boolean;
9
+ }
10
+ /**
11
+ * Main Multi-Agent Accelerator component that contains the sidebar, agent flow, and chat components.
12
+ * @param backendNeuroSanApiUrl Initial URL of the backend Neuro-San API. User can change this in the UI.
13
+ * @param darkMode Whether dark mode is enabled.
14
+ * @param userInfo Information about the current user, including userName and userImage.
15
+ */
16
+ export declare const MultiAgentAccelerator: ({ backendNeuroSanApiUrl, darkMode, userInfo, }: MultiAgentAcceleratorProps) => ReactJSX.Element;
17
+ export {};
@@ -0,0 +1,208 @@
1
+ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } 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 { StopCircle } from "@mui/icons-material";
18
+ import Box from "@mui/material/Box";
19
+ import Grid from "@mui/material/Grid";
20
+ import Slide from "@mui/material/Slide";
21
+ import { useCallback, useEffect, useRef, useState } from "react";
22
+ import { ReactFlowProvider } from "reactflow";
23
+ import { AgentFlow } from "./AgentFlow.js";
24
+ import { Sidebar } from "./Sidebar.js";
25
+ import { getAgentNetworks, getConnectivity } from "../../controller/agent/Agent.js";
26
+ import { processChatChunk } from "../../utils/agentConversations.js";
27
+ import { useLocalStorage } from "../../utils/useLocalStorage.js";
28
+ import { ChatCommon } from "../AgentChat/ChatCommon.js";
29
+ import { SmallLlmChatButton } from "../AgentChat/LlmChatButton.js";
30
+ import { cleanUpAgentName } from "../AgentChat/Utils.js";
31
+ import { closeNotification, NotificationType, sendNotification } from "../Common/notification.js";
32
+ /**
33
+ * Main Multi-Agent Accelerator component that contains the sidebar, agent flow, and chat components.
34
+ * @param backendNeuroSanApiUrl Initial URL of the backend Neuro-San API. User can change this in the UI.
35
+ * @param darkMode Whether dark mode is enabled.
36
+ * @param userInfo Information about the current user, including userName and userImage.
37
+ */
38
+ export const MultiAgentAccelerator = ({ backendNeuroSanApiUrl, darkMode, userInfo, }) => {
39
+ // Animation time for the left and right panels to slide in or out when launching the animation
40
+ const GROW_ANIMATION_TIME_MS = 800;
41
+ // Stores whether are currently awaiting LLM response (for knowing when to show spinners)
42
+ const [isAwaitingLlm, setIsAwaitingLlm] = useState(false);
43
+ // Track streaming state - controls thought bubble cleanup timer, and enables "zen mode" (hides outer panels after
44
+ // animation)
45
+ const [isStreaming, setIsStreaming] = useState(false);
46
+ const [networks, setNetworks] = useState([]);
47
+ const [agentsInNetwork, setAgentsInNetwork] = useState([]);
48
+ const [selectedNetwork, setSelectedNetwork] = useState(null);
49
+ // Track whether we've shown the info popup so we don't keep bugging the user with it
50
+ const [haveShownPopup, setHaveShownPopup] = useState(false);
51
+ const [customURLLocalStorage, setCustomURLLocalStorage] = useLocalStorage("customAgentNetworkURL", null);
52
+ // An extra set of quotes is making it in the string in local storage.
53
+ const [neuroSanURL, setNeuroSanURL] = useState(customURLLocalStorage?.replaceAll('"', "") || backendNeuroSanApiUrl);
54
+ const agentCountsRef = useRef(new Map());
55
+ const conversationsRef = useRef(null);
56
+ const [currentConversations, setCurrentConversations] = useState(null);
57
+ // State to hold thought bubble edges - avoids duplicates across layout recalculations
58
+ const [thoughtBubbleEdges, setThoughtBubbleEdges] = useState(new Map());
59
+ const customURLCallback = useCallback((url) => {
60
+ setNeuroSanURL(url || backendNeuroSanApiUrl);
61
+ setCustomURLLocalStorage(url === "" ? null : url);
62
+ }, [backendNeuroSanApiUrl, setCustomURLLocalStorage]);
63
+ const resetState = useCallback(() => {
64
+ setThoughtBubbleEdges(new Map());
65
+ setIsStreaming(false);
66
+ }, []);
67
+ // Reference to the ChatCommon component to allow external stop button to call its handleStop method
68
+ const chatRef = useRef(null);
69
+ // Handle external stop button click - stops streaming and exits zen mode
70
+ const handleExternalStop = useCallback(() => {
71
+ chatRef.current?.handleStop();
72
+ resetState();
73
+ }, []);
74
+ useEffect(() => {
75
+ async function getNetworks() {
76
+ try {
77
+ const networksTmp = await getAgentNetworks(neuroSanURL);
78
+ const sortedNetworks = networksTmp?.sort((a, b) => a.localeCompare(b));
79
+ setNetworks(sortedNetworks);
80
+ // Set the first network as the selected network
81
+ setSelectedNetwork(sortedNetworks[0]);
82
+ closeNotification();
83
+ }
84
+ catch (e) {
85
+ sendNotification(NotificationType.error, "Connection error",
86
+ // eslint-disable-next-line max-len
87
+ `Unable to get list of Agent Networks. Verify that ${neuroSanURL} is a valid Multi-Agent Accelerator Server. Error: ${e}.`);
88
+ setNetworks([]);
89
+ setSelectedNetwork(null);
90
+ }
91
+ }
92
+ void getNetworks();
93
+ }, [neuroSanURL]);
94
+ useEffect(() => {
95
+ ;
96
+ (async () => {
97
+ if (selectedNetwork) {
98
+ try {
99
+ const connectivity = await getConnectivity(neuroSanURL, selectedNetwork, userInfo.userName);
100
+ const agentsInNetworkSorted = connectivity.connectivity_info
101
+ .concat()
102
+ .sort((a, b) => a?.origin.localeCompare(b?.origin));
103
+ setAgentsInNetwork(agentsInNetworkSorted);
104
+ }
105
+ catch (e) {
106
+ const networkName = cleanUpAgentName(selectedNetwork);
107
+ sendNotification(NotificationType.error, "Connection error",
108
+ // eslint-disable-next-line max-len
109
+ `Unable to get agent list for "${networkName}". Verify that ${neuroSanURL} is a valid Multi-Agent Accelerator Server. Error: ${e}.`);
110
+ setAgentsInNetwork([]);
111
+ }
112
+ }
113
+ })();
114
+ }, [neuroSanURL, selectedNetwork]);
115
+ // Set up handler to allow Escape key to stop the interaction with the LLM.
116
+ useEffect(() => {
117
+ if (!isAwaitingLlm) {
118
+ return undefined;
119
+ }
120
+ const onKeyDown = (e) => {
121
+ if (e.key === "Escape") {
122
+ handleExternalStop();
123
+ }
124
+ };
125
+ window.addEventListener("keydown", onKeyDown);
126
+ return () => window.removeEventListener("keydown", onKeyDown);
127
+ }, [isAwaitingLlm, handleExternalStop]);
128
+ // Effect to exit zen mode when streaming ends
129
+ useEffect(() => {
130
+ if (!isAwaitingLlm) {
131
+ setIsStreaming(false);
132
+ }
133
+ }, [isAwaitingLlm]);
134
+ const onChunkReceived = useCallback((chunk) => {
135
+ const result = processChatChunk(chunk, agentCountsRef.current, conversationsRef.current);
136
+ if (result.success) {
137
+ agentCountsRef.current = result.newCounts;
138
+ conversationsRef.current = result.newConversations;
139
+ setCurrentConversations(result.newConversations);
140
+ }
141
+ return result.success;
142
+ }, []);
143
+ const onStreamingStarted = useCallback(() => {
144
+ // Show info popup only once per session
145
+ if (!haveShownPopup) {
146
+ sendNotification(NotificationType.info, "Agents working", "Click the stop button or hit Escape to exit.");
147
+ setHaveShownPopup(true);
148
+ }
149
+ // Mark that streaming has started
150
+ setIsStreaming(true);
151
+ }, [haveShownPopup]);
152
+ const onStreamingComplete = useCallback(() => {
153
+ // When streaming is complete, clean up any refs and state
154
+ conversationsRef.current = null;
155
+ agentCountsRef.current = new Map();
156
+ setCurrentConversations(null);
157
+ resetState();
158
+ }, []);
159
+ const getLeftPanel = () => {
160
+ return (_jsx(Slide, { id: "multi-agent-accelerator-grid-sidebar-slide", in: !isAwaitingLlm, direction: "right", timeout: GROW_ANIMATION_TIME_MS, onExited: () => {
161
+ setIsStreaming(true);
162
+ }, children: _jsx(Grid, { id: "multi-agent-accelerator-grid-sidebar", size: isStreaming ? 0 : 3.25, sx: {
163
+ height: "100%",
164
+ }, children: _jsx(Sidebar, { customURLLocalStorage: customURLLocalStorage, customURLCallback: customURLCallback, id: "multi-agent-accelerator-sidebar", isAwaitingLlm: isAwaitingLlm, networks: networks, selectedNetwork: selectedNetwork, setSelectedNetwork: setSelectedNetwork }) }) }));
165
+ };
166
+ const getCenterPanel = () => {
167
+ return (_jsx(Grid, { id: "multi-agent-accelerator-grid-agent-flow", size: isStreaming ? 18 : 8.25, sx: {
168
+ height: "100%",
169
+ }, children: _jsx(ReactFlowProvider, { children: _jsx(Box, { id: "multi-agent-accelerator-agent-flow-container", sx: {
170
+ display: "flex",
171
+ justifyContent: "center",
172
+ alignItems: "center",
173
+ width: "100%",
174
+ height: "100%",
175
+ maxWidth: 1000,
176
+ margin: "0 auto",
177
+ }, children: _jsx(AgentFlow, { agentCounts: agentCountsRef.current, agentsInNetwork: agentsInNetwork, id: "multi-agent-accelerator-agent-flow", currentConversations: currentConversations, isAwaitingLlm: isAwaitingLlm, isStreaming: isStreaming, thoughtBubbleEdges: thoughtBubbleEdges, setThoughtBubbleEdges: setThoughtBubbleEdges }) }) }) }));
178
+ };
179
+ const getRightPanel = () => {
180
+ return (_jsx(Slide, { id: "multi-agent-accelerator-grid-agent-chat-common-slide", in: !isAwaitingLlm, direction: "left", timeout: GROW_ANIMATION_TIME_MS, onExited: () => {
181
+ setIsStreaming(true);
182
+ }, children: _jsx(Grid, { id: "multi-agent-accelerator-grid-agent-chat-common", size: isStreaming ? 0 : 6.5, sx: {
183
+ height: "100%",
184
+ }, children: _jsx(ChatCommon, { ref: chatRef, neuroSanURL: neuroSanURL, id: "agent-network-ui", currentUser: userInfo.userName, userImage: userInfo.userImage, setIsAwaitingLlm: setIsAwaitingLlm, isAwaitingLlm: isAwaitingLlm, targetAgent: selectedNetwork, onChunkReceived: onChunkReceived, onStreamingComplete: onStreamingComplete, onStreamingStarted: onStreamingStarted, clearChatOnNewAgent: true, backgroundColor: darkMode ? "var(--bs-dark-mode-dim)" : "var(--bs-secondary-blue)" }) }) }));
185
+ };
186
+ const getStopButton = () => {
187
+ return (_jsx(_Fragment, { children: isAwaitingLlm && (_jsx(Box, { id: "stop-button-container", sx: {
188
+ position: "absolute",
189
+ bottom: "1rem",
190
+ right: "1rem",
191
+ zIndex: 10,
192
+ }, children: _jsx(SmallLlmChatButton, { "aria-label": "Stop", disabled: !isAwaitingLlm, id: "stop-output-button", onClick: handleExternalStop, posBottom: 8, posRight: 23, children: _jsx(StopCircle, { fontSize: "small", id: "stop-button-icon", sx: { color: "var(--bs-white)" } }) }) })) }));
193
+ };
194
+ return (_jsxs(Grid, { id: "multi-agent-accelerator-grid", container: true, columns: 18, sx: {
195
+ border: "solid 1px #CFCFDC",
196
+ borderRadius: "var(--bs-border-radius)",
197
+ display: "flex",
198
+ flex: 1,
199
+ height: "85%",
200
+ marginTop: "1rem",
201
+ overflow: "hidden",
202
+ padding: "1rem",
203
+ background: darkMode ? "var(--bs-dark-mode-dim)" : "var(--bs-white)",
204
+ color: darkMode ? "var(--bs-white)" : "var(--bs-primary)",
205
+ justifyContent: isAwaitingLlm ? "center" : "unset",
206
+ position: "relative",
207
+ }, children: [getLeftPanel(), getCenterPanel(), getRightPanel(), getStopButton()] }));
208
+ };
@@ -0,0 +1,3 @@
1
+ import { FC } from "react";
2
+ import { EdgeProps } from "reactflow";
3
+ export declare const PlasmaEdge: FC<EdgeProps>;
@@ -0,0 +1,124 @@
1
+ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } 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 { useEffect, useRef } from "react";
18
+ import { getBezierPath } from "reactflow";
19
+ function createFunnelParticleOnPath(pathEl, canvasOffset, baseProgress) {
20
+ // Prettier and ESlint conflict over this
21
+ // eslint-disable-next-line newline-per-chained-call
22
+ const green = getComputedStyle(document.documentElement).getPropertyValue("--bs-green").trim();
23
+ const totalLength = pathEl.getTotalLength();
24
+ const speed = 0.02 + Math.random() * 0.003;
25
+ const life = 100;
26
+ const initialLife = life;
27
+ let progress = baseProgress;
28
+ let oscAngle = Math.random() * Math.PI * 2;
29
+ const oscSpeed = 0.1 + Math.random() * 0.05;
30
+ const maxAmp = 16 + Math.random() * 8;
31
+ let remainingLife = life;
32
+ let basePoint = pathEl.getPointAtLength(progress * totalLength);
33
+ const update = () => {
34
+ remainingLife -= 1;
35
+ progress += speed;
36
+ oscAngle += oscSpeed;
37
+ const _length = Math.min(progress * totalLength, totalLength);
38
+ basePoint = pathEl.getPointAtLength(_length);
39
+ };
40
+ const draw = (ctx) => {
41
+ const t = progress;
42
+ const taper = Math.max(0.75, 1 - t);
43
+ const amp = maxAmp * taper;
44
+ const delta = 1;
45
+ const p1 = pathEl.getPointAtLength(Math.max(0, totalLength * t));
46
+ const p2 = pathEl.getPointAtLength(Math.min(totalLength, totalLength * t + delta));
47
+ const angle = Math.atan2(p2.y - p1.y, p2.x - p1.x) + Math.PI / 2;
48
+ const offsetX = Math.cos(angle) * Math.sin(oscAngle) * amp;
49
+ const offsetY = Math.sin(angle) * Math.sin(oscAngle) * amp;
50
+ const x = basePoint.x + offsetX - canvasOffset.x;
51
+ const y = basePoint.y + offsetY - canvasOffset.y;
52
+ const alpha = Math.max(0, remainingLife / initialLife);
53
+ const pulse = 0.7 + 0.3 * Math.abs(Math.sin(oscAngle * 1.5));
54
+ ctx.save();
55
+ ctx.beginPath();
56
+ ctx.globalAlpha = alpha * 0.9 * pulse;
57
+ ctx.shadowBlur = 8 + 8 * pulse; // Lowered for performance
58
+ ctx.shadowColor = green;
59
+ ctx.fillStyle = green;
60
+ ctx.arc(x, y, 2, 0, Math.PI * 2);
61
+ ctx.fill();
62
+ ctx.globalAlpha = 1;
63
+ ctx.shadowBlur = 0;
64
+ ctx.restore();
65
+ };
66
+ const isAlive = () => progress * totalLength < totalLength * 0.98;
67
+ return { update, draw, isAlive };
68
+ }
69
+ export const PlasmaEdge = ({ sourceX, sourceY, targetX, targetY, sourcePosition, targetPosition, }) => {
70
+ const canvasRef = useRef(null);
71
+ const pathRef = useRef(null);
72
+ const animationRef = useRef();
73
+ const particles = useRef([]);
74
+ const [edgePath] = getBezierPath({
75
+ sourceX,
76
+ sourceY,
77
+ targetX,
78
+ targetY,
79
+ sourcePosition,
80
+ targetPosition,
81
+ });
82
+ const padding = 40;
83
+ const x = Math.min(sourceX, targetX) - padding;
84
+ const y = Math.min(sourceY, targetY) - padding;
85
+ const width = Math.abs(targetX - sourceX) + padding * 2;
86
+ const height = Math.abs(targetY - sourceY) + padding * 2;
87
+ useEffect(() => {
88
+ const canvas = canvasRef.current;
89
+ const pathEl = pathRef.current;
90
+ if (!canvas || !pathEl)
91
+ return undefined;
92
+ const ctx = canvas.getContext("2d");
93
+ const dpr = window.devicePixelRatio || 1;
94
+ canvas.width = width * dpr;
95
+ canvas.height = height * dpr;
96
+ canvas.style.width = `${width}px`;
97
+ canvas.style.height = `${height}px`;
98
+ ctx.setTransform(1, 0, 0, 1, 0, 0);
99
+ ctx.scale(dpr, dpr);
100
+ const canvasOffset = { x, y };
101
+ const MAX_PARTICLES = 250;
102
+ const PARTICLES_PER_FRAME = 25;
103
+ const animate = () => {
104
+ ctx.clearRect(0, 0, width, height);
105
+ for (let i = 0; i < PARTICLES_PER_FRAME; i += 1) {
106
+ if (particles.current.length < MAX_PARTICLES) {
107
+ const t = Math.random();
108
+ if (Math.random() < 1 - t) {
109
+ particles.current.push(createFunnelParticleOnPath(pathEl, canvasOffset, t));
110
+ }
111
+ }
112
+ }
113
+ particles.current.forEach((p) => {
114
+ p.update();
115
+ p.draw(ctx);
116
+ });
117
+ particles.current = particles.current.filter((p) => p.isAlive());
118
+ animationRef.current = requestAnimationFrame(animate);
119
+ };
120
+ animate();
121
+ return () => cancelAnimationFrame(animationRef.current);
122
+ }, [edgePath, width, height, x, y]);
123
+ return (_jsxs(_Fragment, { children: [_jsx("foreignObject", { id: `foreign-object-${x}-${y}`, width: width, height: height, x: x, y: y, style: { pointerEvents: "none", overflow: "visible" }, children: _jsx("canvas", { id: `canvas-${x}-${y}`, ref: canvasRef }) }), _jsx("path", { id: `path-${edgePath}`, ref: pathRef, d: edgePath, fill: "none", stroke: "none", style: { position: "absolute", visibility: "hidden" } })] }));
124
+ };
@@ -0,0 +1,12 @@
1
+ import { FC } from "react";
2
+ interface SidebarProps {
3
+ customURLCallback: (url: string) => void;
4
+ customURLLocalStorage?: string;
5
+ id: string;
6
+ isAwaitingLlm: boolean;
7
+ networks: string[];
8
+ selectedNetwork: string;
9
+ setSelectedNetwork: (network: string) => void;
10
+ }
11
+ export declare const Sidebar: FC<SidebarProps>;
12
+ export {};