@cognizant-ai-lab/ui-common 1.5.1 → 1.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/AgentChat/ChatCommon/ChatCommon.d.ts +16 -6
- package/dist/components/AgentChat/ChatCommon/ChatCommon.js +250 -166
- package/dist/components/AgentChat/ChatCommon/ChatHistory.d.ts +1 -7
- package/dist/components/AgentChat/ChatCommon/ChatHistory.js +33 -22
- package/dist/components/AgentChat/ChatCommon/Conversation.d.ts +3 -5
- package/dist/components/AgentChat/ChatCommon/Conversation.js +35 -57
- package/dist/components/AgentChat/ChatCommon/ConversationTurn.d.ts +9 -5
- package/dist/components/AgentChat/ChatCommon/ConversationTurn.js +3 -2
- package/dist/components/AgentChat/ChatCommon/FormattedMarkdown.js +5 -3
- package/dist/components/AgentChat/ChatCommon/SampleQueries.d.ts +3 -0
- package/dist/components/AgentChat/ChatCommon/SampleQueries.js +6 -3
- package/dist/components/AgentChat/ChatCommon/Thinking.d.ts +12 -0
- package/dist/components/AgentChat/ChatCommon/Thinking.js +51 -0
- package/dist/components/AgentChat/Common/LlmChatButton.d.ts +2 -2
- package/dist/components/AgentChat/Common/Types.d.ts +6 -5
- package/dist/components/AgentChat/Common/Types.js +5 -0
- package/dist/components/AgentChat/Common/Utils.d.ts +1 -1
- package/dist/components/AgentChat/Common/Utils.js +13 -7
- package/dist/components/ChatBot/ChatBot.d.ts +0 -4
- package/dist/components/ChatBot/ChatBot.js +2 -2
- package/dist/components/Common/AccordionLite.d.ts +14 -0
- package/dist/components/Common/AccordionLite.js +25 -0
- package/dist/components/Common/ConfirmationModal.d.ts +1 -1
- package/dist/components/Common/CustomerLogo.js +1 -3
- package/dist/components/Common/MUIAlert.d.ts +1 -0
- package/dist/components/Common/MUIAlert.js +3 -4
- package/dist/components/MultiAgentAccelerator/AgentFlow.d.ts +1 -0
- package/dist/components/MultiAgentAccelerator/AgentFlow.js +154 -59
- package/dist/components/MultiAgentAccelerator/AgentNode.d.ts +1 -0
- package/dist/components/MultiAgentAccelerator/AgentNode.js +46 -45
- package/dist/components/MultiAgentAccelerator/GraphLayouts.js +12 -4
- package/dist/components/MultiAgentAccelerator/MultiAgentAccelerator.js +103 -24
- package/dist/components/Settings/ApiKeyInput.d.ts +16 -0
- package/dist/components/Settings/ApiKeyInput.js +70 -0
- package/dist/components/Settings/SettingsDialog.js +30 -3
- package/dist/controller/llm/Providers.d.ts +2 -0
- package/dist/controller/llm/Providers.js +41 -0
- package/dist/index.d.ts +0 -1
- package/dist/index.js +0 -1
- package/dist/state/Settings.d.ts +2 -0
- package/dist/state/Settings.js +1 -0
- package/dist/tsconfig.build.tsbuildinfo +1 -1
- package/package.json +2 -2
- package/dist/components/AgentChat/ChatCommon/AgentConnectivity.d.ts +0 -14
- package/dist/components/AgentChat/ChatCommon/AgentConnectivity.js +0 -23
- package/dist/components/AgentChat/ChatCommon/AgentIntro.d.ts +0 -12
- package/dist/components/AgentChat/ChatCommon/AgentIntro.js +0 -19
- package/dist/components/AgentChat/ChatCommon/AgentMetadata.d.ts +0 -14
- package/dist/components/AgentChat/ChatCommon/AgentMetadata.js +0 -43
- package/dist/components/AgentChat/ChatCommon/Const.d.ts +0 -1
- package/dist/components/AgentChat/ChatCommon/Const.js +0 -2
- package/dist/components/AgentChat/ChatCommon/Greetings.d.ts +0 -1
- package/dist/components/AgentChat/ChatCommon/Greetings.js +0 -38
- package/dist/components/AgentChat/ChatCommon/UserQueryDisplay.d.ts +0 -7
- package/dist/components/AgentChat/ChatCommon/UserQueryDisplay.js +0 -32
- package/dist/components/Common/LlmChatOptionsButton.d.ts +0 -6
- package/dist/components/Common/LlmChatOptionsButton.js +0 -31
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { jsx as _jsx, jsxs as _jsxs
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
/*
|
|
3
3
|
Copyright 2025 Cognizant Technology Solutions Corp, www.cognizant.com.
|
|
4
4
|
|
|
@@ -18,41 +18,40 @@ limitations under the License.
|
|
|
18
18
|
* See main function description.
|
|
19
19
|
*/
|
|
20
20
|
import { AIMessage, HumanMessage } from "@langchain/core/messages";
|
|
21
|
-
import AccountTreeIcon from "@mui/icons-material/AccountTree";
|
|
22
21
|
import ClearIcon from "@mui/icons-material/Clear";
|
|
23
22
|
import CloseIcon from "@mui/icons-material/Close";
|
|
24
|
-
import
|
|
25
|
-
import WrapTextIcon from "@mui/icons-material/WrapText";
|
|
23
|
+
import TuneIcon from "@mui/icons-material/Tune";
|
|
26
24
|
import Box from "@mui/material/Box";
|
|
25
|
+
import Checkbox from "@mui/material/Checkbox";
|
|
27
26
|
import CircularProgress from "@mui/material/CircularProgress";
|
|
28
27
|
import IconButton from "@mui/material/IconButton";
|
|
29
28
|
import Input from "@mui/material/Input";
|
|
30
29
|
import InputAdornment from "@mui/material/InputAdornment";
|
|
30
|
+
import ListItemIcon from "@mui/material/ListItemIcon";
|
|
31
|
+
import ListItemText from "@mui/material/ListItemText";
|
|
32
|
+
import Menu from "@mui/material/Menu";
|
|
33
|
+
import MenuItem from "@mui/material/MenuItem";
|
|
31
34
|
import { useTheme } from "@mui/material/styles";
|
|
32
|
-
import Tooltip from "@mui/material/Tooltip";
|
|
33
35
|
import Typography from "@mui/material/Typography";
|
|
36
|
+
import { isEmpty } from "lodash-es";
|
|
34
37
|
import { useCallback, useEffect, useImperativeHandle, useMemo, useRef, useState, } from "react";
|
|
35
38
|
import { v4 as uuid } from "uuid";
|
|
36
|
-
import { AgentIntro } from "./AgentIntro.js";
|
|
37
|
-
import { AgentMetadata } from "./AgentMetadata.js";
|
|
38
39
|
import { ChatHistory } from "./ChatHistory.js";
|
|
39
|
-
import { AGENT_IMAGE } from "./Const.js";
|
|
40
40
|
import { ControlButtons } from "./ControlButtons.js";
|
|
41
41
|
import { Conversation } from "./Conversation.js";
|
|
42
42
|
import { MessageRole } from "./ConversationTurn.js";
|
|
43
|
+
import { SampleQueries } from "./SampleQueries.js";
|
|
43
44
|
import { SendButton } from "./SendButton.js";
|
|
45
|
+
import { Thinking } from "./Thinking.js";
|
|
44
46
|
import { sendChatQuery } from "../../../controller/agent/Agent.js";
|
|
45
47
|
import { sendLlmRequest, StreamingUnit } from "../../../controller/llm/LlmChat.js";
|
|
46
48
|
import { ChatMessageType } from "../../../generated/neuro-san/NeuroSanClient.js";
|
|
47
49
|
import { useAgentChatHistoryStore } from "../../../state/ChatHistory.js";
|
|
48
50
|
import { hasOnlyWhitespace } from "../../../utils/text.js";
|
|
49
|
-
import {
|
|
50
|
-
import { isLegacyAgentType } from "../Common/Types.js";
|
|
51
|
+
import { givesFinalAnswer, isLegacyAgentType } from "../Common/Types.js";
|
|
51
52
|
import { chatMessageFromChunk, checkError, cleanUpAgentName, removeTrailingUuid } from "../Common/Utils.js";
|
|
52
53
|
import { MicrophoneButton } from "../VoiceChat/MicrophoneButton.js";
|
|
53
54
|
import { cleanupAndStopSpeechRecognition, setupSpeechRecognition } from "../VoiceChat/VoiceChat.js";
|
|
54
|
-
// Key for the chat history, which gets special treatment; always visible even if "show thinking" is off.
|
|
55
|
-
const CHAT_HISTORY_KEY = "chat-history-accordion";
|
|
56
55
|
// Define fancy EMPTY constant to avoid linter error about using object literals as default props
|
|
57
56
|
const EMPTY = {};
|
|
58
57
|
// How many times to retry the entire agent interaction process. Some networks have a well-defined success condition.
|
|
@@ -64,15 +63,15 @@ const MAX_AGENT_RETRIES = 3;
|
|
|
64
63
|
* @returns The final answer from the agent, if it exists or undefined if it doesn't
|
|
65
64
|
*/
|
|
66
65
|
const extractFinalAnswer = (response) => /Final Answer: (?<finalAnswerText>.*)/su.exec(response)?.groups?.["finalAnswerText"];
|
|
67
|
-
// Maximum number of
|
|
68
|
-
const
|
|
66
|
+
// Maximum number of turns to save
|
|
67
|
+
export const MAX_TURNS = 50;
|
|
69
68
|
/**
|
|
70
69
|
* Common chat component for agent chat. This component is used by all agent chat components to provide a consistent
|
|
71
70
|
* experience for users when chatting with agents. It handles user input as well as displaying and nicely formatting
|
|
72
71
|
* agent responses. Customization for inputs and outputs is provided via event handlers-like props.
|
|
73
72
|
*/
|
|
74
73
|
export const ChatCommon = ({ ref, ...props }) => {
|
|
75
|
-
const { customAgentGreetings = EMPTY, agentPlaceholders = EMPTY, backgroundColor, currentUser, extraParams, extraSlyData, id, isAwaitingLlm, legacyAgentEndpoint, neuroSanURL, onChunkReceived, onClose, onSend, onStreamingComplete, onStreamingStarted, setIsAwaitingLlm, setPreviousResponse,
|
|
74
|
+
const { customAgentGreetings = EMPTY, agentPlaceholders = EMPTY, backgroundColor, currentUser, extraParams, extraSlyData, id, isAwaitingLlm, legacyAgentEndpoint, missingApiKeys = [], networkDescription, neuroSanURL, onChunkReceived, onClose, onSend, onStreamingComplete, onStreamingStarted, sampleQueries, setIsAwaitingLlm, setPreviousResponse, selectedNetwork, title, } = props;
|
|
76
75
|
// MUI theme
|
|
77
76
|
const theme = useTheme();
|
|
78
77
|
// User LLM chat input
|
|
@@ -94,21 +93,20 @@ export const ChatCommon = ({ ref, ...props }) => {
|
|
|
94
93
|
const [autoScrollEnabled, setAutoScrollEnabled] = useState(true);
|
|
95
94
|
// Whether to wrap output text
|
|
96
95
|
const [shouldWrapOutput, setShouldWrapOutput] = useState(true);
|
|
97
|
-
//
|
|
98
|
-
const
|
|
96
|
+
// Options menu control
|
|
97
|
+
const [optionsMenuAnchorEl, setOptionsMenuAnchorEl] = useState(null);
|
|
98
|
+
const [optionsMenuOpen, setOptionsMenuOpen] = useState(false);
|
|
99
99
|
// Persistent agent chat history store, which is where we store both kinds of chat histories
|
|
100
100
|
// (see store implementation for details)
|
|
101
|
-
const storedChatHistory = useAgentChatHistoryStore((state) => state?.history?.[
|
|
101
|
+
const storedChatHistory = useAgentChatHistoryStore((state) => selectedNetwork ? state?.history?.[selectedNetwork] : undefined);
|
|
102
102
|
const agentChatHistory = useMemo(() => storedChatHistory ?? { chatHistory: [], chatContext: null, slyData: {} }, [storedChatHistory]);
|
|
103
103
|
// Access store for context items
|
|
104
104
|
const updateChatContext = useAgentChatHistoryStore((state) => state.updateChatContext);
|
|
105
105
|
const updateChatHistory = useAgentChatHistoryStore((state) => state.updateChatHistory);
|
|
106
106
|
const updateSlyData = useAgentChatHistoryStore((state) => state.updateSlyData);
|
|
107
107
|
const resetHistory = useAgentChatHistoryStore((state) => state.resetHistory);
|
|
108
|
-
// Ref
|
|
109
|
-
const
|
|
110
|
-
// Track state of "show thinking" toggle
|
|
111
|
-
const [showThinking, setShowThinking] = useState(false);
|
|
108
|
+
// Ref copy of current turns, so we can safely use it in callbacks without worrying about stale closures
|
|
109
|
+
const turnsRef = useRef([]);
|
|
112
110
|
// Microphone state for voice input
|
|
113
111
|
const [isMicOn, setIsMicOn] = useState(false);
|
|
114
112
|
// Ref for speech recognition
|
|
@@ -136,7 +134,7 @@ export const ChatCommon = ({ ref, ...props }) => {
|
|
|
136
134
|
};
|
|
137
135
|
// Keeps track of whether the agent completed its task
|
|
138
136
|
const succeeded = useRef(false);
|
|
139
|
-
const agentDisplayName = useMemo(() => cleanUpAgentName(removeTrailingUuid(
|
|
137
|
+
const agentDisplayName = useMemo(() => cleanUpAgentName(removeTrailingUuid(selectedNetwork)), [selectedNetwork]);
|
|
140
138
|
useEffect(() => {
|
|
141
139
|
// Set up speech recognition
|
|
142
140
|
const handlers = setupSpeechRecognition(setChatInput, setVoiceInputState, speechRecognitionRef);
|
|
@@ -152,53 +150,42 @@ export const ChatCommon = ({ ref, ...props }) => {
|
|
|
152
150
|
const container = chatOutputRef.current;
|
|
153
151
|
if (!container)
|
|
154
152
|
return;
|
|
155
|
-
// Scroll the final answer into view
|
|
156
|
-
if (finalAnswerRef.current && !isAwaitingLlm) {
|
|
157
|
-
container.scrollTop = finalAnswerRef.current.offsetTop - 50;
|
|
158
|
-
return;
|
|
159
|
-
}
|
|
160
153
|
// Live-streaming auto-scroll
|
|
161
154
|
if (autoScrollEnabled) {
|
|
162
155
|
container.scrollTop = container.scrollHeight;
|
|
163
156
|
}
|
|
164
157
|
}, [autoScrollEnabled, isAwaitingLlm, turns]);
|
|
158
|
+
// Keep a ref copy of the turns array
|
|
159
|
+
useEffect(() => {
|
|
160
|
+
turnsRef.current = turns;
|
|
161
|
+
}, [turns]);
|
|
165
162
|
const addTurn = useCallback((turn) => {
|
|
166
163
|
setTurns((current) => {
|
|
167
164
|
const next = [...current, turn];
|
|
168
|
-
return next.length >
|
|
165
|
+
return next.length > MAX_TURNS ? next.slice(-MAX_TURNS) : next;
|
|
169
166
|
});
|
|
170
167
|
}, []);
|
|
171
168
|
// We use this to update the same "turn" as chunks come in from legacy agents
|
|
172
169
|
const legacyTurnIdRef = useRef(null);
|
|
173
|
-
const
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
id: legacyTurnIdRef.current,
|
|
186
|
-
role: MessageRole.LegacyAgent,
|
|
187
|
-
text: currentResponse.current,
|
|
188
|
-
alwaysShow: true,
|
|
189
|
-
});
|
|
190
|
-
}
|
|
191
|
-
else {
|
|
192
|
-
// We already have a turn for this response, so just update the text of that turn.
|
|
193
|
-
setTurns((prev) => prev.map((t) => (t.id === legacyTurnIdRef.current ? { ...t, text: currentResponse.current } : t)));
|
|
194
|
-
}
|
|
195
|
-
// Check for Final Answer from legacy agent
|
|
196
|
-
const finalAnswerMatch = extractFinalAnswer(currentResponse.current);
|
|
197
|
-
if (finalAnswerMatch) {
|
|
198
|
-
lastAIMessage.current = finalAnswerMatch;
|
|
199
|
-
}
|
|
200
|
-
return;
|
|
170
|
+
const handleLegacyAgentChunk = useCallback((chunk) => {
|
|
171
|
+
currentResponse.current += chunk;
|
|
172
|
+
if (!legacyTurnIdRef.current) {
|
|
173
|
+
// We don't yet have a turn for this response, so create one. On subsequent chunks, we'll just
|
|
174
|
+
// update the text of this turn.
|
|
175
|
+
legacyTurnIdRef.current = uuid();
|
|
176
|
+
addTurn({
|
|
177
|
+
id: legacyTurnIdRef.current,
|
|
178
|
+
messageType: ChatMessageType.AGENT,
|
|
179
|
+
role: MessageRole.Agent,
|
|
180
|
+
text: currentResponse.current,
|
|
181
|
+
});
|
|
201
182
|
}
|
|
183
|
+
else {
|
|
184
|
+
// We already have a turn for this response, so just update the text of that turn.
|
|
185
|
+
setTurns((prev) => prev.map((t) => (t.id === legacyTurnIdRef.current ? { ...t, text: currentResponse.current } : t)));
|
|
186
|
+
}
|
|
187
|
+
}, [addTurn]);
|
|
188
|
+
const handleNeuroSanAgentChunk = useCallback((chunk) => {
|
|
202
189
|
// For Neuro-san agents, we expect a ChatMessage structure in the chunk.
|
|
203
190
|
const chatMessage = chatMessageFromChunk(chunk);
|
|
204
191
|
if (!chatMessage) {
|
|
@@ -206,50 +193,64 @@ export const ChatCommon = ({ ref, ...props }) => {
|
|
|
206
193
|
// But don't want to spam output by logging errors for every bad message.
|
|
207
194
|
return;
|
|
208
195
|
}
|
|
209
|
-
// Keep track of AI messages. The last one is (by definition) the "final answer" from the agents.
|
|
210
|
-
if (chatMessage.type === ChatMessageType.AI && chatMessage.text) {
|
|
211
|
-
lastAIMessage.current = chatMessage.text;
|
|
212
|
-
}
|
|
213
196
|
// Shallow merge existing slyData with incoming chatMessage.sly_data
|
|
214
197
|
if (chatMessage.sly_data) {
|
|
215
|
-
updateSlyData(
|
|
198
|
+
updateSlyData(selectedNetwork, chatMessage.sly_data);
|
|
216
199
|
}
|
|
217
200
|
// It's a ChatMessage. Does it have chat context? Only AGENT_FRAMEWORK messages can have chat context.
|
|
218
201
|
if (chatMessage.type === ChatMessageType.AGENT_FRAMEWORK && chatMessage.chat_context) {
|
|
219
202
|
// Save the chat context, potentially overwriting any previous ones we received during this session.
|
|
220
203
|
// We only care about the last one received.
|
|
221
|
-
updateChatContext(
|
|
204
|
+
updateChatContext(selectedNetwork, chatMessage.chat_context);
|
|
222
205
|
}
|
|
223
206
|
// Check if there is an error block in the "structure" field of the chat message.
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
});
|
|
234
|
-
succeeded.current = false;
|
|
235
|
-
}
|
|
207
|
+
const errorMessage = checkError(chatMessage.structure);
|
|
208
|
+
if (errorMessage) {
|
|
209
|
+
// If there is an error block, display it.
|
|
210
|
+
addTurn({
|
|
211
|
+
id: uuid(),
|
|
212
|
+
role: MessageRole.Warning,
|
|
213
|
+
text: errorMessage,
|
|
214
|
+
});
|
|
215
|
+
succeeded.current = false;
|
|
236
216
|
}
|
|
237
|
-
else if (chatMessage?.text?.trim()
|
|
238
|
-
// Not an error, so output it if it has text
|
|
239
|
-
//
|
|
240
|
-
//
|
|
217
|
+
else if (chatMessage?.text?.trim().length > 0 || !isEmpty(chatMessage.structure)) {
|
|
218
|
+
// Not an error, so output it if it has text or a structure.
|
|
219
|
+
// This is the normal happy path for an incoming message.
|
|
220
|
+
// The backend sometimes sends messages with no text content, and we don't want to display those to the
|
|
221
|
+
// user. Agent name is the last tool in the origin array. If it's not there, use a default name.
|
|
241
222
|
const agentName = chatMessage.origin?.length > 0
|
|
242
223
|
? cleanUpAgentName(chatMessage.origin[chatMessage.origin.length - 1].tool)
|
|
243
|
-
: "Agent
|
|
224
|
+
: "Agent";
|
|
244
225
|
addTurn({
|
|
226
|
+
agentName,
|
|
245
227
|
id: uuid(),
|
|
228
|
+
messageType: chatMessage.type,
|
|
246
229
|
role: MessageRole.Agent,
|
|
247
|
-
|
|
230
|
+
structure: chatMessage.structure,
|
|
248
231
|
text: chatMessage.text,
|
|
249
232
|
});
|
|
250
|
-
|
|
233
|
+
if (chatMessage?.text?.trim().length > 0) {
|
|
234
|
+
// Append to current response if present
|
|
235
|
+
currentResponse.current += chatMessage.text;
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
}, [addTurn, selectedNetwork, updateChatContext, updateSlyData]);
|
|
239
|
+
/**
|
|
240
|
+
* Handle a chunk of response from the server. Called each time the server streams a chunk.
|
|
241
|
+
*/
|
|
242
|
+
const handleChunk = useCallback((chunk) => {
|
|
243
|
+
// Give container a chance to process the chunk first
|
|
244
|
+
const onChunkReceivedResult = onChunkReceived?.(chunk) ?? true;
|
|
245
|
+
succeeded.current = succeeded.current || onChunkReceivedResult;
|
|
246
|
+
if (isLegacyAgentType(selectedNetwork)) {
|
|
247
|
+
// For legacy agents, we either get plain text or Markdown. Just output it as-is.
|
|
248
|
+
handleLegacyAgentChunk(chunk);
|
|
249
|
+
}
|
|
250
|
+
else {
|
|
251
|
+
handleNeuroSanAgentChunk(chunk);
|
|
251
252
|
}
|
|
252
|
-
}, [onChunkReceived,
|
|
253
|
+
}, [onChunkReceived, selectedNetwork, handleNeuroSanAgentChunk, handleLegacyAgentChunk]);
|
|
253
254
|
/**
|
|
254
255
|
* Reset the state of the component. This is called after a request is completed, regardless of success or failure.
|
|
255
256
|
*/
|
|
@@ -257,12 +258,10 @@ export const ChatCommon = ({ ref, ...props }) => {
|
|
|
257
258
|
// Reset state, whatever happened during request
|
|
258
259
|
setIsAwaitingLlm(false);
|
|
259
260
|
setChatInput("");
|
|
260
|
-
|
|
261
|
-
finalAnswerRef.current = null;
|
|
262
|
-
setPreviousResponse?.(targetAgent, currentResponse.current);
|
|
261
|
+
setPreviousResponse?.(selectedNetwork, currentResponse.current);
|
|
263
262
|
currentResponse.current = "";
|
|
264
263
|
legacyTurnIdRef.current = null;
|
|
265
|
-
}, [setIsAwaitingLlm, setPreviousResponse,
|
|
264
|
+
}, [setIsAwaitingLlm, setPreviousResponse, selectedNetwork]);
|
|
266
265
|
/*
|
|
267
266
|
* The main logic for sending a query to the server, with retries on errors.
|
|
268
267
|
*/
|
|
@@ -275,7 +274,7 @@ export const ChatCommon = ({ ref, ...props }) => {
|
|
|
275
274
|
// Increment the attempt number and set the state to indicate we're awaiting a response
|
|
276
275
|
attemptNumber += 1;
|
|
277
276
|
// Check which agent type we are dealing with
|
|
278
|
-
if (isLegacyAgentType(
|
|
277
|
+
if (isLegacyAgentType(selectedNetwork)) {
|
|
279
278
|
// It's a legacy agent (these go directly to the LLM and are different from
|
|
280
279
|
// the Neuro-san agents).
|
|
281
280
|
// Send the chat query to the server. This will block until the stream ends from the server
|
|
@@ -285,7 +284,7 @@ export const ChatCommon = ({ ref, ...props }) => {
|
|
|
285
284
|
// It's a Neuro-san agent.
|
|
286
285
|
// Some coded tools (data generator...) expect the username provided in slyData.
|
|
287
286
|
const slyDataWithUserName = { ...agentChatHistory?.slyData, ...extraSlyData, login: currentUser };
|
|
288
|
-
await sendChatQuery(neuroSanURL, controller?.current.signal, query,
|
|
287
|
+
await sendChatQuery(neuroSanURL, controller?.current.signal, query, selectedNetwork, handleChunk, agentChatHistory.chatContext, slyDataWithUserName, currentUser, StreamingUnit.Line);
|
|
289
288
|
}
|
|
290
289
|
}
|
|
291
290
|
catch (error) {
|
|
@@ -303,7 +302,6 @@ export const ChatCommon = ({ ref, ...props }) => {
|
|
|
303
302
|
id: uuid(),
|
|
304
303
|
role: MessageRole.Error,
|
|
305
304
|
text: `Error occurred: ${error}`,
|
|
306
|
-
alwaysShow: true,
|
|
307
305
|
});
|
|
308
306
|
}
|
|
309
307
|
}
|
|
@@ -318,12 +316,83 @@ export const ChatCommon = ({ ref, ...props }) => {
|
|
|
318
316
|
handleChunk,
|
|
319
317
|
legacyAgentEndpoint,
|
|
320
318
|
neuroSanURL,
|
|
321
|
-
|
|
319
|
+
selectedNetwork,
|
|
322
320
|
]);
|
|
321
|
+
const getFinalAnswerErrorTurn = () => ({
|
|
322
|
+
id: uuid(),
|
|
323
|
+
role: MessageRole.Error,
|
|
324
|
+
text: "The agent did not provide a final answer in the expected format. This is an internal error.",
|
|
325
|
+
});
|
|
326
|
+
const handleFinalAnswerLegacyAgent = useCallback(() => {
|
|
327
|
+
const currentTurns = turnsRef.current;
|
|
328
|
+
// Prefer the most recent matching turn
|
|
329
|
+
const idx = currentTurns.reduceRight((foundIndex, turn, i) => foundIndex !== -1 || extractFinalAnswer(turn.text) === undefined ? foundIndex : i, -1);
|
|
330
|
+
if (idx === -1) {
|
|
331
|
+
if (givesFinalAnswer(selectedNetwork)) {
|
|
332
|
+
// This agent is supposed to give final answers, but didn't this time. An error.
|
|
333
|
+
setTurns((prev) => [...prev, getFinalAnswerErrorTurn()]);
|
|
334
|
+
return;
|
|
335
|
+
}
|
|
336
|
+
else {
|
|
337
|
+
// Use the last received turn as the final answer
|
|
338
|
+
const lastTurn = currentTurns.slice(-1)[0];
|
|
339
|
+
if (!lastTurn)
|
|
340
|
+
return;
|
|
341
|
+
// Just set the last turn as the final answer
|
|
342
|
+
setTurns((prev) => prev.map((turn) => (turn.id === lastTurn.id ? { ...turn, role: MessageRole.FinalAnswer } : turn)));
|
|
343
|
+
// Save it to chat history
|
|
344
|
+
updateChatHistory(selectedNetwork, [new AIMessage({ content: lastTurn.text, id: uuid() })]);
|
|
345
|
+
return;
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
const sourceTurn = currentTurns[idx];
|
|
349
|
+
// Save item to chat history (same as original behavior)
|
|
350
|
+
updateChatHistory(selectedNetwork, [new AIMessage({ content: sourceTurn.text, id: uuid() })]);
|
|
351
|
+
// Extract the final answer from the turn.
|
|
352
|
+
const finalAnswer = extractFinalAnswer(sourceTurn.text)?.trim();
|
|
353
|
+
// Update the turn to be a final answer turn, and add a new final answer turn with just the final answer text.
|
|
354
|
+
setTurns((prev) => {
|
|
355
|
+
const sourceTurnIndex = prev.findIndex(({ id: itemId }) => itemId === sourceTurn.id);
|
|
356
|
+
const updated = sourceTurnIndex === -1
|
|
357
|
+
? [...prev]
|
|
358
|
+
: prev.map((turn, index) => (index === sourceTurnIndex ? { ...turn, text: sourceTurn.text } : turn));
|
|
359
|
+
// Add explicit final answer as a new terminal turn
|
|
360
|
+
updated.push({
|
|
361
|
+
id: uuid(),
|
|
362
|
+
role: MessageRole.FinalAnswer,
|
|
363
|
+
text: finalAnswer,
|
|
364
|
+
});
|
|
365
|
+
return updated;
|
|
366
|
+
});
|
|
367
|
+
}, [selectedNetwork, updateChatHistory]);
|
|
368
|
+
/**
|
|
369
|
+
* Extract the final answer from the turns for a Neuro-san agent. For Neuro-san agents, we expect the final answer
|
|
370
|
+
* to be the most recent turn messageType === ChatMessageType.AGENT_FRAMEWORK.
|
|
371
|
+
*/
|
|
372
|
+
const handleFinalAnswerNeuroSanAgent = useCallback(() => {
|
|
373
|
+
// Get current turns snapshot
|
|
374
|
+
const currentTurns = turnsRef.current;
|
|
375
|
+
// Find the most recent turn that is from the agent framework, which should be the one that contains the
|
|
376
|
+
// final answer.
|
|
377
|
+
const idx = currentTurns.reduceRight((found, turn, i) => (found !== -1 || turn.messageType !== ChatMessageType.AGENT_FRAMEWORK ? found : i), -1);
|
|
378
|
+
// Check for final answer
|
|
379
|
+
if (idx === -1) {
|
|
380
|
+
// No final answer found in the turns. Should never happen for a Neuro-san agent.
|
|
381
|
+
setTurns((prev) => [...prev, getFinalAnswerErrorTurn()]);
|
|
382
|
+
return;
|
|
383
|
+
}
|
|
384
|
+
// Extract final answer from that turn
|
|
385
|
+
const finalAnswerTurn = currentTurns[idx];
|
|
386
|
+
// Update relevant turn to be the final answer
|
|
387
|
+
setTurns((prev) => prev.map((turn, i) => (i === idx ? { ...turn, role: MessageRole.FinalAnswer } : turn)));
|
|
388
|
+
// Save final answer to chat history
|
|
389
|
+
const finalAnswerContent = finalAnswerTurn.text || JSON.stringify(finalAnswerTurn.structure, null, 2);
|
|
390
|
+
updateChatHistory(selectedNetwork, [new AIMessage({ content: finalAnswerContent, id: uuid() })]);
|
|
391
|
+
}, [selectedNetwork, updateChatHistory]);
|
|
323
392
|
const handleSend = useCallback(async (query) => {
|
|
324
393
|
// Record user query in chat history. Discard anything beyond MAX_CHAT_HISTORY_ITEMS
|
|
325
394
|
const userQueryMessage = new HumanMessage({ content: query, id: uuid() });
|
|
326
|
-
updateChatHistory(
|
|
395
|
+
updateChatHistory(selectedNetwork, [userQueryMessage]);
|
|
327
396
|
// Allow parent to intercept and modify the query before sending if needed
|
|
328
397
|
const queryToSend = onSend?.(query) ?? query;
|
|
329
398
|
// Save query for "regenerate" use. Again we save the real user input, not the modified query. It will again
|
|
@@ -338,56 +407,34 @@ export const ChatCommon = ({ ref, ...props }) => {
|
|
|
338
407
|
id: uuid(),
|
|
339
408
|
role: MessageRole.User,
|
|
340
409
|
text: query,
|
|
341
|
-
alwaysShow: true,
|
|
342
|
-
});
|
|
343
|
-
// Add ID block for agent
|
|
344
|
-
addTurn({
|
|
345
|
-
agentDisplayName,
|
|
346
|
-
agentName: targetAgent,
|
|
347
|
-
alwaysShow: true,
|
|
348
|
-
id: uuid(),
|
|
349
|
-
role: MessageRole.AgentHeader,
|
|
350
|
-
text: agentDisplayName,
|
|
351
410
|
});
|
|
352
411
|
// Allow clients to do something when streaming starts
|
|
353
412
|
onStreamingStarted?.();
|
|
354
413
|
// Set up the abort controller
|
|
355
414
|
controller.current = new AbortController();
|
|
356
415
|
setIsAwaitingLlm(true);
|
|
357
|
-
addTurn({
|
|
358
|
-
agentName: `Contacting ${agentDisplayName}...`,
|
|
359
|
-
id: uuid(),
|
|
360
|
-
role: MessageRole.Agent,
|
|
361
|
-
text: `Query: ${queryToSend}`,
|
|
362
|
-
});
|
|
363
416
|
try {
|
|
364
417
|
// Invoke the logic to send the request and retry as necessary
|
|
365
418
|
const wasAborted = await doRetryLoop(queryToSend);
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
updateChatHistory(targetAgent, [new AIMessage({ content: lastAIMessage.current, id: uuid() })]);
|
|
419
|
+
// Abort condition is handled elsewhere
|
|
420
|
+
if (!wasAborted) {
|
|
421
|
+
if (succeeded.current) {
|
|
422
|
+
// Success: infer final answer depending on agent type
|
|
423
|
+
if (isLegacyAgentType(selectedNetwork)) {
|
|
424
|
+
handleFinalAnswerLegacyAgent();
|
|
425
|
+
}
|
|
426
|
+
else {
|
|
427
|
+
handleFinalAnswerNeuroSanAgent();
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
else {
|
|
431
|
+
// Exhausted retries without success. Display error to user.
|
|
432
|
+
addTurn({
|
|
433
|
+
id: uuid(),
|
|
434
|
+
role: MessageRole.Error,
|
|
435
|
+
text: `Gave up after ${MAX_AGENT_RETRIES} attempts.`,
|
|
436
|
+
});
|
|
385
437
|
}
|
|
386
|
-
}
|
|
387
|
-
else if (isLegacyAgentType(targetAgent) && currentResponse.current.length > 0) {
|
|
388
|
-
// It's a legacy agent that didn't provide a "Final Answer", so just record the whole response
|
|
389
|
-
// as the bot answer in that case.
|
|
390
|
-
updateChatHistory(targetAgent, [new AIMessage({ content: currentResponse.current, id: uuid() })]);
|
|
391
438
|
}
|
|
392
439
|
}
|
|
393
440
|
finally {
|
|
@@ -397,14 +444,15 @@ export const ChatCommon = ({ ref, ...props }) => {
|
|
|
397
444
|
}
|
|
398
445
|
}, [
|
|
399
446
|
addTurn,
|
|
400
|
-
agentDisplayName,
|
|
401
447
|
doRetryLoop,
|
|
448
|
+
handleFinalAnswerLegacyAgent,
|
|
449
|
+
handleFinalAnswerNeuroSanAgent,
|
|
402
450
|
onSend,
|
|
403
451
|
onStreamingComplete,
|
|
404
452
|
onStreamingStarted,
|
|
405
453
|
resetState,
|
|
406
454
|
setIsAwaitingLlm,
|
|
407
|
-
|
|
455
|
+
selectedNetwork,
|
|
408
456
|
updateChatHistory,
|
|
409
457
|
]);
|
|
410
458
|
const handleStop = useCallback(() => {
|
|
@@ -412,7 +460,6 @@ export const ChatCommon = ({ ref, ...props }) => {
|
|
|
412
460
|
controller?.current?.abort();
|
|
413
461
|
controller.current = null;
|
|
414
462
|
addTurn({
|
|
415
|
-
alwaysShow: true,
|
|
416
463
|
id: uuid(),
|
|
417
464
|
role: MessageRole.Warning,
|
|
418
465
|
text: "Request cancelled.",
|
|
@@ -429,31 +476,35 @@ export const ChatCommon = ({ ref, ...props }) => {
|
|
|
429
476
|
// Enable regenerate button when there is a previous query to resent, and we're not awaiting a response
|
|
430
477
|
const shouldEnableRegenerateButton = previousUserQuery && !isAwaitingLlm;
|
|
431
478
|
// Enable Clear Chat button if not awaiting response and there is chat output to clear
|
|
432
|
-
const enableClearChatButton = !isAwaitingLlm && turns.length > 0;
|
|
433
|
-
const getPlaceholder = () =>
|
|
479
|
+
const enableClearChatButton = !isAwaitingLlm && (turns.length > 0 || agentChatHistory?.chatHistory?.length > 0);
|
|
480
|
+
const getPlaceholder = () => selectedNetwork ? agentPlaceholders[selectedNetwork] || `Chat with ${agentDisplayName}` : null;
|
|
434
481
|
const handleClearChat = useCallback(() => {
|
|
435
482
|
setTurns([]);
|
|
436
|
-
resetHistory(
|
|
483
|
+
resetHistory(selectedNetwork);
|
|
437
484
|
setPreviousUserQuery("");
|
|
438
485
|
currentResponse.current = "";
|
|
439
|
-
|
|
440
|
-
}, [resetHistory, targetAgent]);
|
|
486
|
+
}, [resetHistory, selectedNetwork]);
|
|
441
487
|
// Expose the handleStop and handleClearChat methods to parent components via ref for external control
|
|
442
488
|
useImperativeHandle(ref, () => ({
|
|
443
489
|
handleStop,
|
|
444
490
|
handleClearChat,
|
|
445
491
|
}), [handleStop, handleClearChat]);
|
|
446
|
-
const
|
|
492
|
+
const getErrorOverlay = (errorText) => (_jsx(Box, { id: "chat-disabled-overlay", sx: {
|
|
493
|
+
position: "absolute",
|
|
494
|
+
top: 0,
|
|
495
|
+
left: 0,
|
|
496
|
+
right: 0,
|
|
497
|
+
bottom: 0,
|
|
498
|
+
zIndex: theme.zIndex.modal - 1,
|
|
499
|
+
cursor: "not-allowed",
|
|
500
|
+
// Capture all pointer events to prevent interaction with the chat when no agent is selected
|
|
501
|
+
pointerEvents: "all",
|
|
502
|
+
}, children: _jsx(Typography, { sx: {
|
|
447
503
|
position: "absolute",
|
|
448
|
-
top:
|
|
449
|
-
left:
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
zIndex: theme.zIndex.modal - 1,
|
|
453
|
-
cursor: "not-allowed",
|
|
454
|
-
// Capture all pointer events to prevent interaction with the chat when no agent is selected
|
|
455
|
-
pointerEvents: "all",
|
|
456
|
-
} }) }));
|
|
504
|
+
top: "50%",
|
|
505
|
+
left: "50%",
|
|
506
|
+
transform: "translate(-50%, -50%)",
|
|
507
|
+
}, children: errorText }) }));
|
|
457
508
|
const getTitle = () => (_jsxs(Box, { id: `llm-chat-title-container-${id}`, sx: {
|
|
458
509
|
alignItems: "center",
|
|
459
510
|
borderTopLeftRadius: "var(--bs-border-radius)",
|
|
@@ -465,31 +516,59 @@ export const ChatCommon = ({ ref, ...props }) => {
|
|
|
465
516
|
paddingTop: "0.25rem",
|
|
466
517
|
paddingBottom: "0.25rem",
|
|
467
518
|
}, children: [_jsx(Typography, { id: `llm-chat-title-${id}-text`, sx: { fontSize: "0.9rem" }, children: title }), onClose && (_jsx(IconButton, { "data-testid": `close-button-${id}`, id: `close-button-${id}`, onClick: onClose, children: _jsx(CloseIcon, { id: `close-icon-${id}` }) }))] }));
|
|
468
|
-
const
|
|
469
|
-
|
|
519
|
+
const getOptionsMenuButton = () => (_jsx(Box, { sx: {
|
|
520
|
+
position: "absolute",
|
|
521
|
+
top: "0.25rem",
|
|
522
|
+
right: "0.0rem",
|
|
523
|
+
}, children: _jsx(IconButton, { onClick: (e) => {
|
|
524
|
+
setOptionsMenuAnchorEl(e.currentTarget);
|
|
525
|
+
setOptionsMenuOpen(true);
|
|
526
|
+
}, children: _jsx(TuneIcon, { sx: { fontSize: "1.2rem" } }) }) }));
|
|
527
|
+
const agentGreeting = customAgentGreetings[selectedNetwork] ?? "Hi, how can I help?";
|
|
528
|
+
const handleOptionsMenuClose = () => {
|
|
529
|
+
setOptionsMenuAnchorEl(null);
|
|
530
|
+
setOptionsMenuOpen(false);
|
|
531
|
+
};
|
|
532
|
+
const handleToggleAutoScroll = () => {
|
|
533
|
+
setAutoScrollEnabled((prev) => !prev);
|
|
534
|
+
};
|
|
535
|
+
const handleToggleWrapOutput = () => {
|
|
536
|
+
setShouldWrapOutput((prev) => !prev);
|
|
537
|
+
};
|
|
538
|
+
const getOptionsMenu = () => (_jsxs(Menu, { id: `${id}-options-menu`, anchorEl: optionsMenuAnchorEl, open: optionsMenuOpen, onClose: handleOptionsMenuClose, slotProps: {
|
|
539
|
+
list: {
|
|
540
|
+
dense: true,
|
|
541
|
+
sx: {
|
|
542
|
+
py: 0,
|
|
543
|
+
"& .MuiMenuItem-root": { minHeight: 30, py: 0.5, px: 1 },
|
|
544
|
+
"& .MuiCheckbox-root": { p: 0.5 },
|
|
545
|
+
"& .MuiListItemText-primary": { fontSize: "smaller" },
|
|
546
|
+
},
|
|
547
|
+
},
|
|
548
|
+
}, children: [_jsxs(MenuItem, { onClick: handleToggleAutoScroll, children: [_jsx(ListItemIcon, { children: _jsx(Checkbox, { checked: autoScrollEnabled, tabIndex: -1, sx: { pointerEvents: "none" } }) }), _jsx(ListItemText, { primary: "Auto-scroll output" })] }), _jsxs(MenuItem, { onClick: handleToggleWrapOutput, children: [_jsx(ListItemIcon, { children: _jsx(Checkbox, { checked: shouldWrapOutput, tabIndex: -1, sx: { pointerEvents: "none" } }) }), _jsx(ListItemText, { primary: "Wrap output" })] })] }));
|
|
470
549
|
const getResponseBox = () => (_jsxs(Box, { id: "llm-response-div", sx: {
|
|
471
550
|
...divStyle,
|
|
472
551
|
border: "var(--bs-border-width) var(--bs-border-style)",
|
|
473
552
|
borderRadius: "var(--bs-border-radius)",
|
|
474
553
|
display: "flex",
|
|
475
554
|
flexGrow: 1,
|
|
476
|
-
|
|
477
|
-
margin: "10px",
|
|
555
|
+
marginLeft: "10px",
|
|
478
556
|
position: "relative",
|
|
479
557
|
overflowY: "auto",
|
|
480
|
-
}, children: [
|
|
481
|
-
backgroundColor
|
|
482
|
-
borderWidth: "1px",
|
|
558
|
+
}, children: [_jsxs(Box, { id: "llm-responses", ref: chatOutputRef, sx: {
|
|
559
|
+
backgroundColor,
|
|
483
560
|
borderRadius: "0.5rem",
|
|
484
|
-
fontSize: "
|
|
485
|
-
|
|
486
|
-
overflowY: "auto", // Enable vertical scrollbar
|
|
561
|
+
fontSize: "16px",
|
|
562
|
+
overflowY: "auto",
|
|
487
563
|
paddingBottom: "60px",
|
|
488
|
-
paddingTop: "7.5px",
|
|
489
564
|
paddingLeft: "15px",
|
|
490
565
|
paddingRight: "15px",
|
|
566
|
+
paddingTop: "7.5px",
|
|
567
|
+
scrollbarGutter: "stable",
|
|
491
568
|
width: "100%",
|
|
492
|
-
}, tabIndex: -1, children: [agentChatHistory?.chatHistory?.length > 0 && (_jsx(ChatHistory, {
|
|
569
|
+
}, tabIndex: -1, children: [getOptionsMenu(), getOptionsMenuButton(), agentChatHistory?.chatHistory?.length > 0 && (_jsx(ChatHistory, { id: id, messages: agentChatHistory.chatHistory })), _jsxs(Box, { sx: { marginBottom: "0.5rem", marginTop: "1rem", color: "var(--bs-gray)" }, children: [_jsxs(Typography, { component: "span", sx: { fontWeight: 700 }, variant: "inherit", children: [selectedNetwork, networkDescription && ":"] }), networkDescription && (_jsxs(Typography, { component: "span", sx: { ml: 0.5 }, variant: "inherit", children: [" ", networkDescription] }))] }), _jsx(Box, { sx: { marginBottom: "0.5rem", marginTop: "1rem" }, children: agentGreeting }), _jsx(SampleQueries, { disabled: isAwaitingLlm, handleSend: handleSend, sampleQueries: sampleQueries }), _jsx(Conversation, { id: id, includeAgentMessages: !givesFinalAnswer(selectedNetwork), shouldWrapOutput: shouldWrapOutput, turns: turns }), !isAwaitingLlm && turns.length > 0 && (
|
|
570
|
+
// Only show thinking once streaming is complete
|
|
571
|
+
_jsx(Thinking, { id: id, turns: turns })), isAwaitingLlm && (_jsxs(Box, { id: "awaitingOutputContainer", sx: { display: "flex", alignItems: "center", fontSize: "smaller" }, children: [_jsx("span", { id: "working-span", style: { marginRight: "1rem" }, children: "Working..." }), _jsx(CircularProgress, { id: "awaitingOutputSpinner", sx: {
|
|
493
572
|
color: "var(--bs-primary)",
|
|
494
573
|
}, size: "1rem" })] }))] }), _jsx(ControlButtons, { enableClearChatButton: enableClearChatButton, handleClearChat: handleClearChat, handleSend: handleSend, handleStop: handleStop, isAwaitingLlm: isAwaitingLlm, previousUserQuery: previousUserQuery, shouldEnableRegenerateButton: shouldEnableRegenerateButton })] }));
|
|
495
574
|
const getUserInputBox = () => (_jsxs(Box, { id: "user-input-div", sx: {
|
|
@@ -503,7 +582,7 @@ export const ChatCommon = ({ ref, ...props }) => {
|
|
|
503
582
|
borderRadius: "var(--bs-border-radius)",
|
|
504
583
|
display: "flex",
|
|
505
584
|
flexGrow: 1,
|
|
506
|
-
fontSize: "
|
|
585
|
+
fontSize: "17px",
|
|
507
586
|
marginRight: "0.75rem",
|
|
508
587
|
paddingBottom: "0.5rem",
|
|
509
588
|
paddingTop: "0.5rem",
|
|
@@ -531,8 +610,8 @@ export const ChatCommon = ({ ref, ...props }) => {
|
|
|
531
610
|
flexDirection: "column",
|
|
532
611
|
flexGrow: 1,
|
|
533
612
|
height: "100%",
|
|
534
|
-
opacity:
|
|
535
|
-
pointerEvents:
|
|
613
|
+
opacity: selectedNetwork ? 1 : 0.4,
|
|
614
|
+
pointerEvents: selectedNetwork ? "auto" : "none",
|
|
536
615
|
position: "relative",
|
|
537
616
|
}, children: [title && getTitle(), getResponseBox(), getUserInputBox()] }));
|
|
538
617
|
return (_jsx(Box, { id: `llm-chat-${id}`, sx: {
|
|
@@ -541,5 +620,10 @@ export const ChatCommon = ({ ref, ...props }) => {
|
|
|
541
620
|
flexGrow: 1,
|
|
542
621
|
height: "100%",
|
|
543
622
|
position: "relative",
|
|
544
|
-
}, children:
|
|
623
|
+
}, children: selectedNetwork
|
|
624
|
+
? missingApiKeys?.length === 0
|
|
625
|
+
? getChatBox()
|
|
626
|
+
: getErrorOverlay(`API key(s) required for: ${missingApiKeys.join(", ")}. ` +
|
|
627
|
+
"Please add the required key(s) in Settings to use this Network.")
|
|
628
|
+
: getErrorOverlay("Please select a Network from the list to start the chat.") }));
|
|
545
629
|
};
|