diff --git a/webapi/Options/PlannerOptions.cs b/webapi/Options/PlannerOptions.cs
index 291533159..7d9c79d3f 100644
--- a/webapi/Options/PlannerOptions.cs
+++ b/webapi/Options/PlannerOptions.cs
@@ -33,7 +33,7 @@ public class MissingFunctionErrorOptions
public const string PropertyName = "Planner";
///
- /// Define if the planner must be Sequential or not.
+ /// The type of planner to used to create plan.
///
[Required]
public PlanType Type { get; set; } = PlanType.Action;
diff --git a/webapi/appsettings.json b/webapi/appsettings.json
index d3a27c729..f7d7790f4 100644
--- a/webapi/appsettings.json
+++ b/webapi/appsettings.json
@@ -52,6 +52,7 @@
// - Set Planner:Type to "Action" to use the single-step ActionPlanner
// - Set Planner:Type to "Sequential" to enable the multi-step SequentialPlanner
// Note: SequentialPlanner works best with `gpt-4`. See the "Enabling Sequential Planner" section in webapi/README.md for configuration instructions.
+ // - Set Planner:Type to "Stepwise" to enable MRKL style planning
// - Set Planner:RelevancyThreshold to a decimal between 0 and 1.0.
//
"Planner": {
diff --git a/webapp/src/Constants.ts b/webapp/src/Constants.ts
index 136499450..73db5cfde 100644
--- a/webapp/src/Constants.ts
+++ b/webapp/src/Constants.ts
@@ -4,6 +4,7 @@ export const Constants = {
app: {
name: 'Copilot',
updateCheckIntervalSeconds: 60 * 5,
+ CONNECTION_ALERT_ID: 'connection-alert',
},
msal: {
method: 'redirect', // 'redirect' | 'popup'
diff --git a/webapp/src/checkEnv.ts b/webapp/src/checkEnv.ts
index afa5a8afd..f3b322c3a 100644
--- a/webapp/src/checkEnv.ts
+++ b/webapp/src/checkEnv.ts
@@ -1,7 +1,10 @@
+/**
+ * Checks if all required environment variables are defined
+ * @returns {string[]} An array of missing environment variables
+ */
export const getMissingEnvVariables = () => {
// Should be aligned with variables defined in .env.example
const envVariables = ['REACT_APP_BACKEND_URI', 'REACT_APP_AAD_AUTHORITY', 'REACT_APP_AAD_CLIENT_ID'];
-
const missingVariables = [];
for (const variable of envVariables) {
diff --git a/webapp/src/components/token-usage/TokenUsageGraph.tsx b/webapp/src/components/token-usage/TokenUsageGraph.tsx
index 697ab01be..4ba0529cb 100644
--- a/webapp/src/components/token-usage/TokenUsageGraph.tsx
+++ b/webapp/src/components/token-usage/TokenUsageGraph.tsx
@@ -63,7 +63,8 @@ const contrastColors = [
export const TokenUsageGraph: React.FC = ({ promptView, tokenUsage }) => {
const classes = useClasses();
const { conversations, selectedId } = useAppSelector((state: RootState) => state.conversations);
- const loadingResponse = conversations[selectedId].botResponseStatus && Object.entries(tokenUsage).length === 0;
+ const loadingResponse =
+ selectedId !== '' && conversations[selectedId].botResponseStatus && Object.entries(tokenUsage).length === 0;
const responseGenerationView: TokenUsageView = {};
const memoryGenerationView: TokenUsageView = {};
diff --git a/webapp/src/components/views/BackendProbe.tsx b/webapp/src/components/views/BackendProbe.tsx
index a4eec1239..266963b86 100644
--- a/webapp/src/components/views/BackendProbe.tsx
+++ b/webapp/src/components/views/BackendProbe.tsx
@@ -20,7 +20,9 @@ const BackendProbe: FC = ({ uri, onBackendFound }) => {
}
};
- void fetchAsync();
+ fetchAsync().catch(() => {
+ // Ignore - this page is just a probe, so we don't need to show any errors if backend is not found
+ });
}, 3000);
return () => {
diff --git a/webapp/src/libs/hooks/useChat.ts b/webapp/src/libs/hooks/useChat.ts
index e4751107f..095a5b2f2 100644
--- a/webapp/src/libs/hooks/useChat.ts
+++ b/webapp/src/libs/hooks/useChat.ts
@@ -72,10 +72,9 @@ export const useChat = () => {
const createChat = async () => {
const chatTitle = `Copilot @ ${new Date().toLocaleString()}`;
- const accessToken = await AuthHelper.getSKaaSAccessToken(instance, inProgress);
try {
await chatService
- .createChatAsync(userId, chatTitle, accessToken)
+ .createChatAsync(userId, chatTitle, await AuthHelper.getSKaaSAccessToken(instance, inProgress))
.then((result: ICreateChatSessionResponse) => {
const newChat: ChatState = {
id: result.chatSession.id,
@@ -148,8 +147,8 @@ export const useChat = () => {
};
const loadChats = async () => {
- const accessToken = await AuthHelper.getSKaaSAccessToken(instance, inProgress);
try {
+ const accessToken = await AuthHelper.getSKaaSAccessToken(instance, inProgress);
const chatSessions = await chatService.getAllChatsAsync(userId, accessToken);
if (chatSessions.length > 0) {
@@ -201,10 +200,9 @@ export const useChat = () => {
};
const uploadBot = async (bot: Bot) => {
- const accessToken = await AuthHelper.getSKaaSAccessToken(instance, inProgress);
- botService
- .uploadAsync(bot, userId, accessToken)
- .then(async (chatSession: IChatSession) => {
+ try {
+ const accessToken = await AuthHelper.getSKaaSAccessToken(instance, inProgress);
+ await botService.uploadAsync(bot, userId, accessToken).then(async (chatSession: IChatSession) => {
const chatMessages = await chatService.getChatMessagesAsync(chatSession.id, 0, 100, accessToken);
const newChat = {
@@ -217,11 +215,11 @@ export const useChat = () => {
};
dispatch(addConversation(newChat));
- })
- .catch((e: any) => {
- const errorMessage = `Unable to upload the bot. Details: ${getErrorDetails(e)}`;
- dispatch(addAlert({ message: errorMessage, type: AlertType.Error }));
});
+ } catch (e: any) {
+ const errorMessage = `Unable to upload the bot. Details: ${getErrorDetails(e)}`;
+ dispatch(addAlert({ message: errorMessage, type: AlertType.Error }));
+ }
};
const getBotProfilePicture = (index: number): string => {
@@ -282,8 +280,8 @@ export const useChat = () => {
};
const joinChat = async (chatId: string) => {
- const accessToken = await AuthHelper.getSKaaSAccessToken(instance, inProgress);
try {
+ const accessToken = await AuthHelper.getSKaaSAccessToken(instance, inProgress);
await chatService.joinChatAsync(userId, chatId, accessToken).then(async (result: IChatSession) => {
// Get chat messages
const chatMessages = await chatService.getChatMessagesAsync(result.id, 0, 100, accessToken);
@@ -315,9 +313,14 @@ export const useChat = () => {
};
const editChat = async (chatId: string, title: string, syetemDescription: string, memoryBalance: number) => {
- const accessToken = await AuthHelper.getSKaaSAccessToken(instance, inProgress);
try {
- await chatService.editChatAsync(chatId, title, syetemDescription, memoryBalance, accessToken);
+ await chatService.editChatAsync(
+ chatId,
+ title,
+ syetemDescription,
+ memoryBalance,
+ await AuthHelper.getSKaaSAccessToken(instance, inProgress),
+ );
} catch (e: any) {
const errorMessage = `Error editing chat ${chatId}. Details: ${getErrorDetails(e)}`;
dispatch(addAlert({ message: errorMessage, type: AlertType.Error }));
@@ -325,9 +328,8 @@ export const useChat = () => {
};
const getServiceOptions = async () => {
- const accessToken = await AuthHelper.getSKaaSAccessToken(instance, inProgress);
try {
- return await chatService.getServiceOptionsAsync(accessToken);
+ return await chatService.getServiceOptionsAsync(await AuthHelper.getSKaaSAccessToken(instance, inProgress));
} catch (e: any) {
const errorMessage = `Error getting service options. Details: ${getErrorDetails(e)}`;
dispatch(addAlert({ message: errorMessage, type: AlertType.Error }));
diff --git a/webapp/src/redux/features/app/AppState.ts b/webapp/src/redux/features/app/AppState.ts
index a1664f69e..11cd7ecb0 100644
--- a/webapp/src/redux/features/app/AppState.ts
+++ b/webapp/src/redux/features/app/AppState.ts
@@ -13,6 +13,7 @@ export interface ActiveUserInfo {
export interface Alert {
message: string;
type: AlertType;
+ id?: string;
}
interface Feature {
diff --git a/webapp/src/redux/features/app/appSlice.ts b/webapp/src/redux/features/app/appSlice.ts
index ff95e23b3..1d4ef00f2 100644
--- a/webapp/src/redux/features/app/appSlice.ts
+++ b/webapp/src/redux/features/app/appSlice.ts
@@ -1,6 +1,7 @@
// Copyright (c) Microsoft. All rights reserved.
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
+import { Constants } from '../../../Constants';
import { ServiceOptions } from '../../../libs/models/ServiceOptions';
import { TokenUsage } from '../../../libs/models/TokenUsage';
import { ActiveUserInfo, Alert, AppState, FeatureKeys, initialState } from './AppState';
@@ -13,10 +14,14 @@ export const appSlice = createSlice({
state.alerts = action.payload;
},
addAlert: (state: AppState, action: PayloadAction) => {
- if (state.alerts.length === 3) {
- state.alerts.shift();
+ if (
+ action.payload.id == Constants.app.CONNECTION_ALERT_ID ||
+ isServerConnectionError(action.payload.message)
+ ) {
+ updateConnectionStatus(state, action.payload);
+ } else {
+ addNewAlert(state.alerts, action.payload);
}
- state.alerts.push(action.payload);
},
removeAlert: (state: AppState, action: PayloadAction) => {
state.alerts.splice(action.payload, 1);
@@ -89,3 +94,38 @@ const getTotalTokenUsage = (previousSum?: number, current?: number) => {
return previousSum + current;
};
+
+const isServerConnectionError = (message: string) => {
+ return (
+ message.includes(`Cannot send data if the connection is not in the 'Connected' State.`) ||
+ message.includes(`Server timeout elapsed without receiving a message from the server.`)
+ );
+};
+
+const addNewAlert = (alerts: Alert[], newAlert: Alert) => {
+ if (alerts.length === 3) {
+ alerts.shift();
+ }
+
+ alerts.push(newAlert);
+};
+
+const updateConnectionStatus = (state: AppState, statusUpdate: Alert) => {
+ if (isServerConnectionError(statusUpdate.message)) {
+ statusUpdate.message =
+ // Constant message so alert UI doesn't feel glitchy on every connection error from SignalR
+ 'Cannot send data due to lost connection or server timeout. Try refreshing this page to restart the connection.';
+ }
+
+ // There should only ever be one connection alert at a time,
+ // so we tag the alert with a unique ID so we can remove if needed
+ statusUpdate.id ??= Constants.app.CONNECTION_ALERT_ID;
+
+ // Remove the existing connection alert if it exists
+ const connectionAlertIndex = state.alerts.findIndex((alert) => alert.id === Constants.app.CONNECTION_ALERT_ID);
+ if (connectionAlertIndex !== -1) {
+ state.alerts.splice(connectionAlertIndex, 1);
+ }
+
+ addNewAlert(state.alerts, statusUpdate);
+};
diff --git a/webapp/src/redux/features/message-relay/signalRMiddleware.ts b/webapp/src/redux/features/message-relay/signalRMiddleware.ts
index 8ffc237ef..6beaac3fd 100644
--- a/webapp/src/redux/features/message-relay/signalRMiddleware.ts
+++ b/webapp/src/redux/features/message-relay/signalRMiddleware.ts
@@ -2,6 +2,7 @@
import * as signalR from '@microsoft/signalr';
import { AnyAction, Dispatch } from '@reduxjs/toolkit';
+import { Constants } from '../../../Constants';
import { AlertType } from '../../../libs/models/AlertType';
import { IChatUser } from '../../../libs/models/ChatUser';
import { PlanState } from '../../../libs/models/Plan';
@@ -65,7 +66,13 @@ const registerCommonSignalConnectionEvents = (store: Store) => {
hubConnection.onclose((error) => {
if (hubConnection.state === signalR.HubConnectionState.Disconnected) {
const errorMessage = 'Connection closed due to error. Try refreshing this page to restart the connection';
- store.dispatch(addAlert({ message: String(errorMessage), type: AlertType.Error }));
+ store.dispatch(
+ addAlert({
+ message: String(errorMessage),
+ type: AlertType.Error,
+ id: Constants.app.CONNECTION_ALERT_ID,
+ }),
+ );
console.log(errorMessage, error);
}
});
@@ -73,15 +80,21 @@ const registerCommonSignalConnectionEvents = (store: Store) => {
hubConnection.onreconnecting((error) => {
if (hubConnection.state === signalR.HubConnectionState.Reconnecting) {
const errorMessage = 'Connection lost due to error. Reconnecting...';
- store.dispatch(addAlert({ message: String(errorMessage), type: AlertType.Info }));
+ store.dispatch(
+ addAlert({
+ message: String(errorMessage),
+ type: AlertType.Info,
+ id: Constants.app.CONNECTION_ALERT_ID,
+ }),
+ );
console.log(errorMessage, error);
}
});
hubConnection.onreconnected((connectionId = '') => {
if (hubConnection.state === signalR.HubConnectionState.Connected) {
- const message = 'Connection reestablished.';
- store.dispatch(addAlert({ message, type: AlertType.Success }));
+ const message = 'Connection reestablished. Please refresh the page to ensure you have the latest data.';
+ store.dispatch(addAlert({ message, type: AlertType.Success, id: Constants.app.CONNECTION_ALERT_ID }));
console.log(message + ` Connected with connectionId ${connectionId}`);
}
});