@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.
- package/dist/components/AgentChat/ChatCommon.d.ts +94 -0
- package/dist/components/AgentChat/ChatCommon.js +581 -0
- package/dist/components/AgentChat/ControlButtons.d.ts +16 -0
- package/dist/components/AgentChat/ControlButtons.js +24 -0
- package/dist/components/AgentChat/FormattedMarkdown.d.ts +32 -0
- package/dist/components/AgentChat/FormattedMarkdown.js +82 -0
- package/dist/components/AgentChat/Greetings.d.ts +1 -0
- package/dist/components/AgentChat/Greetings.js +38 -0
- package/dist/components/AgentChat/LlmChatButton.d.ts +12 -0
- package/dist/components/AgentChat/LlmChatButton.js +33 -0
- package/dist/components/AgentChat/SendButton.d.ts +12 -0
- package/dist/components/AgentChat/SendButton.js +28 -0
- package/dist/components/AgentChat/SyntaxHighlighterThemes.d.ts +14 -0
- package/dist/components/AgentChat/SyntaxHighlighterThemes.js +27 -0
- package/dist/components/AgentChat/Types.d.ts +17 -0
- package/dist/components/AgentChat/Types.js +26 -0
- package/dist/components/AgentChat/UserQueryDisplay.d.ts +5 -0
- package/dist/components/AgentChat/UserQueryDisplay.js +33 -0
- package/dist/components/AgentChat/Utils.d.ts +11 -0
- package/dist/components/AgentChat/Utils.js +64 -0
- package/dist/components/AgentChat/VoiceChat/MicrophoneButton.d.ts +29 -0
- package/dist/components/AgentChat/VoiceChat/MicrophoneButton.js +55 -0
- package/dist/components/AgentChat/VoiceChat/VoiceChat.d.ts +33 -0
- package/dist/components/AgentChat/VoiceChat/VoiceChat.js +180 -0
- package/dist/components/Authentication/Auth.d.ts +14 -0
- package/dist/components/Authentication/Auth.js +58 -0
- package/dist/components/ChatBot/ChatBot.d.ts +20 -0
- package/dist/components/ChatBot/ChatBot.js +75 -0
- package/dist/components/Common/Breadcrumbs.d.ts +6 -0
- package/dist/components/Common/Breadcrumbs.js +36 -0
- package/dist/components/Common/LlmChatOptionsButton.d.ts +9 -0
- package/dist/components/Common/LlmChatOptionsButton.js +31 -0
- package/dist/components/Common/LoadingSpinner.d.ts +10 -0
- package/dist/components/Common/LoadingSpinner.js +24 -0
- package/dist/components/Common/MUIAccordion.d.ts +17 -0
- package/dist/components/Common/MUIAccordion.js +76 -0
- package/dist/components/Common/MUIAlert.d.ts +11 -0
- package/dist/components/Common/MUIAlert.js +41 -0
- package/dist/components/Common/MUIDialog.d.ts +16 -0
- package/dist/components/Common/MUIDialog.js +40 -0
- package/dist/components/Common/Navbar.d.ts +15 -0
- package/dist/components/Common/Navbar.js +137 -0
- package/dist/components/Common/PageLoader.d.ts +5 -0
- package/dist/components/Common/PageLoader.js +26 -0
- package/dist/components/Common/Snackbar.d.ts +5 -0
- package/dist/components/Common/Snackbar.js +84 -0
- package/dist/components/Common/confirmationModal.d.ts +14 -0
- package/dist/components/Common/confirmationModal.js +65 -0
- package/dist/components/Common/notification.d.ts +18 -0
- package/dist/components/Common/notification.js +79 -0
- package/dist/components/ErrorPage/ErrorBoundary.d.ts +38 -0
- package/dist/components/ErrorPage/ErrorBoundary.js +77 -0
- package/dist/components/ErrorPage/ErrorPage.d.ts +12 -0
- package/dist/components/ErrorPage/ErrorPage.js +46 -0
- package/dist/components/MultiAgentAccelerator/AgentFlow.d.ts +21 -0
- package/dist/components/MultiAgentAccelerator/AgentFlow.js +394 -0
- package/dist/components/MultiAgentAccelerator/AgentNode.d.ts +18 -0
- package/dist/components/MultiAgentAccelerator/AgentNode.js +129 -0
- package/dist/components/MultiAgentAccelerator/GraphLayouts.d.ts +33 -0
- package/dist/components/MultiAgentAccelerator/GraphLayouts.js +297 -0
- package/dist/components/MultiAgentAccelerator/MultiAgentAccelerator.d.ts +17 -0
- package/dist/components/MultiAgentAccelerator/MultiAgentAccelerator.js +208 -0
- package/dist/components/MultiAgentAccelerator/PlasmaEdge.d.ts +3 -0
- package/dist/components/MultiAgentAccelerator/PlasmaEdge.js +124 -0
- package/dist/components/MultiAgentAccelerator/Sidebar.d.ts +12 -0
- package/dist/components/MultiAgentAccelerator/Sidebar.js +204 -0
- package/dist/components/MultiAgentAccelerator/ThoughtBubbleEdge.d.ts +12 -0
- package/dist/components/MultiAgentAccelerator/ThoughtBubbleEdge.js +15 -0
- package/dist/components/MultiAgentAccelerator/ThoughtBubbleOverlay.d.ts +11 -0
- package/dist/components/MultiAgentAccelerator/ThoughtBubbleOverlay.js +466 -0
- package/dist/components/MultiAgentAccelerator/const.d.ts +7 -0
- package/dist/components/MultiAgentAccelerator/const.js +39 -0
- package/dist/const.d.ts +10 -0
- package/dist/const.js +30 -0
- package/dist/controller/agent/Agent.d.ts +56 -0
- package/dist/controller/agent/Agent.js +162 -0
- package/dist/controller/llm/LlmChat.d.ts +18 -0
- package/dist/controller/llm/LlmChat.js +65 -0
- package/dist/controller/llm/endpoints.d.ts +1 -0
- package/dist/controller/llm/endpoints.js +17 -0
- package/dist/generated/neuro-san/NeuroSanClient.d.ts +413 -0
- package/dist/generated/neuro-san/NeuroSanClient.js +28 -0
- package/dist/index.d.ts +37 -0
- package/dist/index.js +52 -0
- package/dist/state/UserInfo.d.ts +16 -0
- package/dist/state/UserInfo.js +27 -0
- package/dist/state/environment.d.ts +18 -0
- package/dist/state/environment.js +33 -0
- package/dist/tsconfig.build.tsbuildinfo +1 -0
- package/dist/utils/Authentication.d.ts +31 -0
- package/dist/utils/Authentication.js +94 -0
- package/dist/utils/BrowserNavigation.d.ts +5 -0
- package/dist/utils/BrowserNavigation.js +22 -0
- package/dist/utils/Theme.d.ts +7 -0
- package/dist/utils/Theme.js +7 -0
- package/dist/utils/agentConversations.d.ts +24 -0
- package/dist/utils/agentConversations.js +113 -0
- package/dist/utils/text.d.ts +28 -0
- package/dist/utils/text.js +64 -0
- package/dist/utils/title.d.ts +1 -0
- package/dist/utils/title.js +20 -0
- package/dist/utils/types.d.ts +17 -0
- package/dist/utils/types.js +16 -0
- package/dist/utils/useLocalStorage.d.ts +1 -0
- package/dist/utils/useLocalStorage.js +55 -0
- package/dist/utils/zIndexLayers.d.ts +2 -0
- package/dist/utils/zIndexLayers.js +29 -0
- 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,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 {};
|