mirror of
https://github.com/microsoft/semantic-kernel.git
synced 2026-10-12 02:19:06 +00:00
Copilot Chat: Fix ESLint violations + enable linter during build (#1710)
### Motivation and Context <!-- Thank you for your contribution to the semantic-kernel repo! Please help reviewers and future users, providing the following information: 1. Why is this change required? 2. What problem does it solve? 3. What scenario does it contribute to? 4. If it fixes an open issue, please link to the issue here. --> fix the ESLint violations in CopilotChat and enable the linter to run during `yarn start`/`yarn build`. **lint errors going forward will fail the build** and will need to be corrected before a PR can merge. we have already configured the linter to auto-fix on save in VSCode and for other IDEs the `yarn lint:fix` script can be used. ### Description <!-- Describe your changes, the overall approach, the underlying design. These notes will help understanding how your code works. Thanks! --> - `.node-pr.yml`: removed unneeded directory filter since `samples/apps` no longer contains a `yarn.lock`. - `.vscode/settings.json`: updated to point to the `eslintConfig` in the `package.json` rather than the shared JS file. this is required so that the plugins are resolved correctly by the extension. - `.eslintrc.js`: - removed deprecated `@typescript-eslint` rules. - removed plugins already resolved from create-react-app. - removed `standard-with-typescript` extended config in favor of more widely used configs. - `webapp/package.json`: - pinned TS version to `5.0.4`, the latest version below `5.1.0`. this is a dependency requirement of `@typescript-eslint` and otherwise the linter will not work. - re-added `eslintConfig` that now extends from the `../../eslintrc.js`. this is required so that the plugins are resolved correctly by ESLint. - `samples/apps/package.json`: - deleted since there are now no shared dependencies that need to be downloaded. - similarly, the `yarn.lock` file was removed as well. - all of the plugins/configs used in `.eslintrc.js` are already included in the [create-react-app config](https://github.com/facebook/create-react-app/blob/main/packages/eslint-config-react-app/index.js) which we extend. - **all other files**: fixed **379** errors and **42** warnings. ### Preview #### During build <img width="258" alt="image" src="https://github.com/microsoft/semantic-kernel/assets/52973358/94a42909-7cb6-48a7-8d5f-6f55a15d6025"> #### In VSCode w/ ESLint extension installed <img width="253" alt="image" src="https://github.com/microsoft/semantic-kernel/assets/52973358/182604ab-6228-49f6-8983-4ef902e7e945"> ### Contribution Checklist <!-- Before submitting this PR, please make sure: --> - [X] The code builds clean without any errors or warnings - [x] The PR follows SK Contribution Guidelines (https://github.com/microsoft/semantic-kernel/blob/main/CONTRIBUTING.md) - [X] The code follows the .NET coding conventions (https://learn.microsoft.com/dotnet/csharp/fundamentals/coding-style/coding-conventions) verified with `dotnet format` - [X] All unit tests pass, and I have added new tests where possible - [X] I didn't break anyone 😄 - [X] Bugbash to double check nothing is broken (not yet but before merging)
This commit is contained in:
@@ -33,10 +33,8 @@ jobs:
|
||||
yarndirs=()
|
||||
for lockfile in samples/apps/**/yarn.lock; do # loop over all yarn.lock files
|
||||
dir=$(dirname "$lockfile") # get the directory of the lock file
|
||||
if [[ "$dir" != "samples/apps" ]]; then # exclude samples/apps directory
|
||||
echo "Found yarn project in $dir"
|
||||
yarndirs+=("$dir") # add the directory to the yarndirs array
|
||||
fi
|
||||
echo "Found yarn project in $dir"
|
||||
yarndirs+=("$dir") # add the directory to the yarndirs array
|
||||
done
|
||||
|
||||
echo "All yarn projects found: '${yarndirs[*]}'"
|
||||
|
||||
Vendored
+1
-2
@@ -57,9 +57,8 @@
|
||||
}
|
||||
],
|
||||
"eslint.options": {
|
||||
"overrideConfigFile": ".eslintrc.js"
|
||||
"overrideConfigFile": "./package.json"
|
||||
},
|
||||
"eslint.packageManager": "yarn",
|
||||
"files.associations": {
|
||||
"*.json": "jsonc"
|
||||
},
|
||||
|
||||
+16
-29
@@ -1,46 +1,33 @@
|
||||
module.exports = {
|
||||
env: {
|
||||
browser: true,
|
||||
es2021: true,
|
||||
},
|
||||
extends: ['plugin:react/recommended', 'standard-with-typescript'],
|
||||
ignorePatterns: ['build', '.*.js', '*.config.js', 'node_modules'],
|
||||
overrides: [],
|
||||
extends: [
|
||||
'eslint:recommended',
|
||||
'plugin:react/recommended',
|
||||
'plugin:react-hooks/recommended',
|
||||
'plugin:@typescript-eslint/recommended',
|
||||
'plugin:@typescript-eslint/recommended-requiring-type-checking',
|
||||
'plugin:@typescript-eslint/strict',
|
||||
],
|
||||
ignorePatterns: ['build', '.*.js', 'node_modules'],
|
||||
parserOptions: {
|
||||
project: './tsconfig.json',
|
||||
ecmaVersion: 'latest',
|
||||
sourceType: 'module',
|
||||
},
|
||||
plugins: ['react', '@typescript-eslint', 'import', 'react-hooks', 'react-security'],
|
||||
rules: {
|
||||
'@typescript-eslint/brace-style': ['off'],
|
||||
'@typescript-eslint/space-before-function-paren': [
|
||||
'error',
|
||||
{ anonymous: 'always', named: 'never', asyncArrow: 'always' },
|
||||
],
|
||||
'@typescript-eslint/semi': ['error', 'always'],
|
||||
'@typescript-eslint/array-type': ['error', { default: 'array-simple' }],
|
||||
'@typescript-eslint/triple-slash-reference': ['error', { types: 'prefer-import' }],
|
||||
'@typescript-eslint/indent': ['off'],
|
||||
'@typescript-eslint/comma-dangle': ['error', 'always-multiline'],
|
||||
'@typescript-eslint/non-nullable-type-assertion-style': 'off',
|
||||
'@typescript-eslint/strict-boolean-expressions': 'off',
|
||||
'@typescript-eslint/member-delimiter-style': [
|
||||
'error',
|
||||
{
|
||||
multiline: {
|
||||
delimiter: 'semi',
|
||||
requireLast: true,
|
||||
},
|
||||
singleline: {
|
||||
delimiter: 'semi',
|
||||
requireLast: false,
|
||||
},
|
||||
},
|
||||
],
|
||||
'@typescript-eslint/explicit-function-return-type': 'off',
|
||||
'react/jsx-props-no-spreading': 'warn',
|
||||
'react-hooks/rules-of-hooks': 'error',
|
||||
'react-hooks/exhaustive-deps': 'warn',
|
||||
'@typescript-eslint/consistent-type-imports': 'off',
|
||||
'@typescript-eslint/no-empty-function': 'off',
|
||||
'@typescript-eslint/no-explicit-any': 'off',
|
||||
'react/react-in-jsx-scope': 'off',
|
||||
'react/prop-types': 'off',
|
||||
'react/jsx-props-no-spreading': 'off',
|
||||
},
|
||||
settings: {
|
||||
react: {
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
# Disable ESLint in all environments
|
||||
DISABLE_ESLINT_PLUGIN=true
|
||||
@@ -8,6 +8,7 @@
|
||||
"auth:mac": "better-vsts-npm-auth -config .npmrc",
|
||||
"depcheck": "depcheck --ignores=\"@types/*,typescript\" --ignore-dirs=\".vscode,.vs,.git,node_modules\" --skip-missing",
|
||||
"lint": "eslint src",
|
||||
"lint:fix": "eslint src --fix",
|
||||
"prettify": "prettier --write \"src/**/*.{ts,tsx,js,jsx,json,scss,css,html,svg}\"",
|
||||
"serve": "serve -s build",
|
||||
"start": "react-scripts start",
|
||||
@@ -44,10 +45,17 @@
|
||||
"http-server": "^14.1.1",
|
||||
"prettier": "^2.8.1",
|
||||
"serve": "^14.2.0",
|
||||
"typescript": "*",
|
||||
"typescript": "5.0.4",
|
||||
"vsts-npm-auth": "^0.42.1",
|
||||
"workbox-window": "^6.5.4"
|
||||
},
|
||||
"eslintConfig": {
|
||||
"extends": [
|
||||
"react-app",
|
||||
"react-app/jest",
|
||||
"../../.eslintrc.js"
|
||||
]
|
||||
},
|
||||
"browserslist": {
|
||||
"production": [
|
||||
">0.2%",
|
||||
|
||||
@@ -1,11 +1,6 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
import {
|
||||
AuthenticatedTemplate,
|
||||
UnauthenticatedTemplate,
|
||||
useIsAuthenticated,
|
||||
useMsal,
|
||||
} from '@azure/msal-react';
|
||||
import { AuthenticatedTemplate, UnauthenticatedTemplate, useIsAuthenticated, useMsal } from '@azure/msal-react';
|
||||
import { Subtitle1, makeStyles, shorthands, tokens } from '@fluentui/react-components';
|
||||
import { Alert } from '@fluentui/react-components/unstable';
|
||||
import { Dismiss16Regular } from '@fluentui/react-icons';
|
||||
@@ -79,21 +74,19 @@ const App: FC = () => {
|
||||
dispatch(setLoggedInUserId(account.homeAccountId));
|
||||
|
||||
if (appState === AppState.LoadingChats) {
|
||||
// Load all chats from the backend.
|
||||
async function loadChats() {
|
||||
if (await chat.loadChats()) {
|
||||
// Load all chats from memory
|
||||
void chat.loadChats().then((succeeded) => {
|
||||
if (succeeded) {
|
||||
setAppState(AppState.Chat);
|
||||
}
|
||||
}
|
||||
|
||||
loadChats();
|
||||
});
|
||||
}
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [instance, inProgress, isAuthenticated, appState]);
|
||||
|
||||
const onDismissAlert = (key: string) => {
|
||||
dispatch(removeAlert(key));
|
||||
const onDismissAlert = (index: number) => {
|
||||
dispatch(removeAlert(index));
|
||||
};
|
||||
|
||||
// TODO: handle error case of missing account information
|
||||
@@ -114,34 +107,40 @@ const App: FC = () => {
|
||||
<Subtitle1 as="h1">Copilot Chat</Subtitle1>
|
||||
<div className={classes.cornerItems}>
|
||||
<PluginGallery />
|
||||
<UserSettings setLoadingState={() => setAppState(AppState.SigningOut)} />
|
||||
<UserSettings
|
||||
setLoadingState={() => {
|
||||
setAppState(AppState.SigningOut);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{alerts &&
|
||||
Object.keys(alerts).map((key) => {
|
||||
const alert = alerts[key];
|
||||
return (
|
||||
<Alert
|
||||
intent={alert.type}
|
||||
action={{
|
||||
icon: (
|
||||
<Dismiss16Regular
|
||||
aria-label="dismiss message"
|
||||
onClick={() => onDismissAlert(key)}
|
||||
color="black"
|
||||
/>
|
||||
),
|
||||
}}
|
||||
key={key}
|
||||
>
|
||||
{alert.message}
|
||||
</Alert>
|
||||
);
|
||||
})}
|
||||
{alerts.map(({ type, message }, index) => {
|
||||
return (
|
||||
<Alert
|
||||
intent={type}
|
||||
action={{
|
||||
icon: (
|
||||
<Dismiss16Regular
|
||||
aria-label="dismiss message"
|
||||
onClick={() => {
|
||||
onDismissAlert(index);
|
||||
}}
|
||||
color="black"
|
||||
/>
|
||||
),
|
||||
}}
|
||||
key={`${index}-${type}`}
|
||||
>
|
||||
{message}
|
||||
</Alert>
|
||||
);
|
||||
})}
|
||||
{appState === AppState.ProbeForBackend && (
|
||||
<BackendProbe
|
||||
uri={process.env.REACT_APP_BACKEND_URI as string}
|
||||
onBackendFound={() => setAppState(AppState.LoadingChats)}
|
||||
onBackendFound={() => {
|
||||
setAppState(AppState.LoadingChats);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{appState === AppState.LoadingChats && <Loading text="Loading Chats..." />}
|
||||
|
||||
@@ -18,7 +18,7 @@ export const FileUploader: React.FC<FileUploaderProps> = forwardRef<HTMLInputEle
|
||||
const classes = useClasses();
|
||||
|
||||
const onChange = React.useCallback(
|
||||
(event: React.SyntheticEvent<Element>) => {
|
||||
(event: React.SyntheticEvent) => {
|
||||
const target = event.target as HTMLInputElement;
|
||||
const selectedFiles = target.files;
|
||||
event.stopPropagation();
|
||||
@@ -50,3 +50,5 @@ export const FileUploader: React.FC<FileUploaderProps> = forwardRef<HTMLInputEle
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
FileUploader.displayName = 'FileUploader';
|
||||
|
||||
@@ -70,7 +70,7 @@ const useClasses = makeStyles({
|
||||
interface ChatInputProps {
|
||||
isDraggingOver?: boolean;
|
||||
onDragLeave: React.DragEventHandler<HTMLDivElement | HTMLTextAreaElement>;
|
||||
onSubmit: (options: GetResponseOptions) => void;
|
||||
onSubmit: (options: GetResponseOptions) => Promise<void>;
|
||||
}
|
||||
|
||||
export const ChatInput: React.FC<ChatInputProps> = ({ isDraggingOver, onDragLeave, onSubmit }) => {
|
||||
@@ -89,17 +89,21 @@ export const ChatInput: React.FC<ChatInputProps> = ({ isDraggingOver, onDragLeav
|
||||
React.useEffect(() => {
|
||||
async function initSpeechRecognizer() {
|
||||
const speechService = new SpeechService(process.env.REACT_APP_BACKEND_URI as string);
|
||||
var response = await speechService.getSpeechTokenAsync(
|
||||
const response = await speechService.getSpeechTokenAsync(
|
||||
await AuthHelper.getSKaaSAccessToken(instance, inProgress),
|
||||
);
|
||||
if (response.isSuccess) {
|
||||
const recognizer = await speechService.getSpeechRecognizerAsyncWithValidKey(response);
|
||||
const recognizer = speechService.getSpeechRecognizerAsyncWithValidKey(response);
|
||||
setRecognizer(recognizer);
|
||||
}
|
||||
}
|
||||
|
||||
initSpeechRecognizer();
|
||||
}, [instance, inProgress]);
|
||||
initSpeechRecognizer().catch((e) => {
|
||||
const errorDetails = e instanceof Error ? e.message : String(e);
|
||||
const errorMessage = `Unable to initialize speech recognizer. Details: ${errorDetails}`;
|
||||
dispatch(addAlert({ message: errorMessage, type: AlertType.Error }));
|
||||
});
|
||||
}, [dispatch, instance, inProgress]);
|
||||
|
||||
React.useEffect(() => {
|
||||
const chatState = conversations[selectedId];
|
||||
@@ -120,28 +124,30 @@ export const ChatInput: React.FC<ChatInputProps> = ({ isDraggingOver, onDragLeav
|
||||
}
|
||||
};
|
||||
|
||||
const handleImport = async (dragAndDropFile?: File) => {
|
||||
setDocumentImporting(true);
|
||||
const handleImport = (dragAndDropFile?: File) => {
|
||||
const file = dragAndDropFile ?? documentFileRef.current?.files?.[0];
|
||||
if (file) {
|
||||
await chat.importDocument(selectedId, file);
|
||||
setDocumentImporting(true);
|
||||
chat.importDocument(selectedId, file).finally(() => {
|
||||
setDocumentImporting(false);
|
||||
});
|
||||
}
|
||||
setDocumentImporting(false);
|
||||
|
||||
// Reset the file input so that the onChange event will
|
||||
// be triggered even if the same file is selected again.
|
||||
documentFileRef.current!.value = '';
|
||||
if (documentFileRef.current?.value) {
|
||||
documentFileRef.current.value = '';
|
||||
}
|
||||
};
|
||||
|
||||
const handleSubmit = (value: string, messageType: ChatMessageType = ChatMessageType.Message) => {
|
||||
try {
|
||||
if (value.trim() === '') {
|
||||
return; // only submit if value is not empty
|
||||
}
|
||||
onSubmit({ value, messageType, chatId: selectedId });
|
||||
setValue('');
|
||||
dispatch(editConversationInput({ id: selectedId, newInput: '' }));
|
||||
} catch (error) {
|
||||
if (value.trim() === '') {
|
||||
return; // only submit if value is not empty
|
||||
}
|
||||
|
||||
setValue('');
|
||||
dispatch(editConversationInput({ id: selectedId, newInput: '' }));
|
||||
onSubmit({ value, messageType, chatId: selectedId }).catch((error) => {
|
||||
const message = `Error submitting chat input: ${(error as Error).message}`;
|
||||
log(message);
|
||||
dispatch(
|
||||
@@ -150,17 +156,19 @@ export const ChatInput: React.FC<ChatInputProps> = ({ isDraggingOver, onDragLeav
|
||||
message,
|
||||
}),
|
||||
);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const handleDrop = async (e: React.DragEvent<HTMLTextAreaElement>) => {
|
||||
const handleDrop = (e: React.DragEvent<HTMLTextAreaElement>) => {
|
||||
onDragLeave(e);
|
||||
await handleImport(e.dataTransfer?.files[0]);
|
||||
handleImport(e.dataTransfer.files[0]);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={classes.root}>
|
||||
<div className={classes.typingIndicator}><ChatStatus /></div>
|
||||
<div className={classes.typingIndicator}>
|
||||
<ChatStatus />
|
||||
</div>
|
||||
<div className={classes.content}>
|
||||
<Textarea
|
||||
id="chat-input"
|
||||
@@ -175,12 +183,14 @@ export const ChatInput: React.FC<ChatInputProps> = ({ isDraggingOver, onDragLeav
|
||||
onDrop={handleDrop}
|
||||
onFocus={() => {
|
||||
// update the locally stored value to the current value
|
||||
const chatInput = document.getElementById('chat-input') as HTMLTextAreaElement;
|
||||
const chatInput = document.getElementById('chat-input');
|
||||
if (chatInput) {
|
||||
setValue(chatInput.value);
|
||||
setValue((chatInput as HTMLTextAreaElement).value);
|
||||
}
|
||||
// User is considered typing if the input is in focus
|
||||
dispatch(updateUserIsTyping({ userId: account!.homeAccountId!, chatId: selectedId, isTyping: true }));
|
||||
dispatch(
|
||||
updateUserIsTyping({ userId: account?.homeAccountId, chatId: selectedId, isTyping: true }),
|
||||
);
|
||||
}}
|
||||
onChange={(_event, data) => {
|
||||
if (isDraggingOver) {
|
||||
@@ -198,7 +208,9 @@ export const ChatInput: React.FC<ChatInputProps> = ({ isDraggingOver, onDragLeav
|
||||
}}
|
||||
onBlur={() => {
|
||||
// User is considered not typing if the input is not in focus
|
||||
dispatch(updateUserIsTyping({ userId: account!.homeAccountId!, chatId: selectedId, isTyping: false }));
|
||||
dispatch(
|
||||
updateUserIsTyping({ userId: account?.homeAccountId, chatId: selectedId, isTyping: false }),
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
@@ -211,7 +223,9 @@ export const ChatInput: React.FC<ChatInputProps> = ({ isDraggingOver, onDragLeav
|
||||
style={{ display: 'none' }}
|
||||
accept=".txt,.pdf"
|
||||
multiple={false}
|
||||
onChange={() => handleImport()}
|
||||
onChange={() => {
|
||||
handleImport();
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
disabled={documentImporting}
|
||||
@@ -230,7 +244,13 @@ export const ChatInput: React.FC<ChatInputProps> = ({ isDraggingOver, onDragLeav
|
||||
onClick={handleSpeech}
|
||||
/>
|
||||
)}
|
||||
<Button appearance="transparent" icon={<SendRegular />} onClick={() => handleSubmit(value)} />
|
||||
<Button
|
||||
appearance="transparent"
|
||||
icon={<SendRegular />}
|
||||
onClick={() => {
|
||||
handleSubmit(value);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -64,7 +64,7 @@ export const ChatResourceList: React.FC<ChatResourceListProps> = ({ chatId }) =>
|
||||
const [resources, setResources] = React.useState<ChatMemorySource[]>([]);
|
||||
|
||||
React.useEffect(() => {
|
||||
chat.getChatMemorySources(chatId).then((sources) => {
|
||||
void chat.getChatMemorySources(chatId).then((sources) => {
|
||||
setResources(sources);
|
||||
});
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
@@ -95,7 +95,7 @@ function useTable(resources: ChatMemorySource[]) {
|
||||
sortDirection: getSortDirection(columnId),
|
||||
});
|
||||
|
||||
const columns: TableColumnDefinition<TableItem>[] = [
|
||||
const columns: Array<TableColumnDefinition<TableItem>> = [
|
||||
createTableColumn<TableItem>({
|
||||
columnId: 'name',
|
||||
renderHeaderCell: () => (
|
||||
@@ -139,7 +139,9 @@ function useTable(resources: ChatMemorySource[]) {
|
||||
Access
|
||||
</TableHeaderCell>
|
||||
),
|
||||
renderCell: (item) => <TableCell key={item.id + item.name}>{getAccessString(item.chatId)}</TableCell>,
|
||||
renderCell: (item) => (
|
||||
<TableCell key={`${item.id} ${item.name.label}`}>{getAccessString(item.chatId)}</TableCell>
|
||||
),
|
||||
compare: (a, b) => {
|
||||
const aAccess = getAccessString(a.chatId);
|
||||
const bAccess = getAccessString(b.chatId);
|
||||
|
||||
@@ -97,8 +97,8 @@ export const ChatRoom: React.FC = () => {
|
||||
|
||||
const chatInput: IChatMessage = {
|
||||
timestamp: new Date().getTime(),
|
||||
userId: account?.homeAccountId,
|
||||
userName: (account?.name ?? account?.username) as string,
|
||||
userId: account.homeAccountId,
|
||||
userName: account.name ?? account.username,
|
||||
content: options.value,
|
||||
type: options.messageType,
|
||||
authorRole: AuthorRoles.User,
|
||||
|
||||
@@ -13,8 +13,7 @@ import {
|
||||
Popover,
|
||||
PopoverSurface,
|
||||
PopoverTrigger,
|
||||
SelectTabData,
|
||||
SelectTabEvent,
|
||||
SelectTabEventHandler,
|
||||
shorthands,
|
||||
Tab,
|
||||
TabList,
|
||||
@@ -87,7 +86,7 @@ export const ChatWindow: React.FC = () => {
|
||||
const dispatch = useAppDispatch();
|
||||
const { conversations, selectedId } = useAppSelector((state: RootState) => state.conversations);
|
||||
const chatName = conversations[selectedId].title;
|
||||
const [title, setTitle] = useState<string | undefined>(selectedId ?? undefined);
|
||||
const [title = '', setTitle] = useState<string | undefined>(selectedId);
|
||||
const [isEditing, setIsEditing] = useState<boolean>(false);
|
||||
const { instance, inProgress } = useMsal();
|
||||
|
||||
@@ -95,28 +94,23 @@ export const ChatWindow: React.FC = () => {
|
||||
|
||||
const onSave = async () => {
|
||||
if (chatName !== title) {
|
||||
try {
|
||||
await chatService.editChatAsync(
|
||||
conversations[selectedId].id,
|
||||
title!,
|
||||
await AuthHelper.getSKaaSAccessToken(instance, inProgress),
|
||||
);
|
||||
|
||||
dispatch(editConversationTitle({ id: selectedId ?? '', newTitle: title ?? '' }));
|
||||
} catch (e: any) {
|
||||
const errorMessage = `Unable to retrieve chat to change title. Details: ${e.message ?? e}`;
|
||||
dispatch(addAlert({ message: errorMessage, type: AlertType.Error }));
|
||||
}
|
||||
await chatService.editChatAsync(
|
||||
conversations[selectedId].id,
|
||||
title,
|
||||
await AuthHelper.getSKaaSAccessToken(instance, inProgress),
|
||||
);
|
||||
|
||||
dispatch(editConversationTitle({ id: selectedId, newTitle: title }));
|
||||
}
|
||||
setIsEditing(!isEditing);
|
||||
};
|
||||
|
||||
const [selectedTab, setSelectedTab] = React.useState<TabValue>('chat');
|
||||
const onTabSelect = (_event: SelectTabEvent, data: SelectTabData) => {
|
||||
const onTabSelect: SelectTabEventHandler = (_event, data) => {
|
||||
setSelectedTab(data.value);
|
||||
};
|
||||
|
||||
const onClose = async () => {
|
||||
const onClose = () => {
|
||||
setTitle(chatName);
|
||||
setIsEditing(!isEditing);
|
||||
};
|
||||
@@ -125,9 +119,14 @@ export const ChatWindow: React.FC = () => {
|
||||
setTitle(data.value);
|
||||
};
|
||||
|
||||
const handleKeyDown = (event: any) => {
|
||||
const handleKeyDown: React.KeyboardEventHandler<HTMLElement> = (event) => {
|
||||
if (event.key === 'Enter') {
|
||||
onSave();
|
||||
onSave().catch((e: any) => {
|
||||
const errorMessage = `Unable to retrieve chat to change title. Details: ${
|
||||
e instanceof Error ? e.message : String(e)
|
||||
}`;
|
||||
dispatch(addAlert({ message: errorMessage, type: AlertType.Error }));
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
@@ -157,7 +156,7 @@ export const ChatWindow: React.FC = () => {
|
||||
icon={isEditing ? <Edit24Filled /> : <EditRegular />}
|
||||
appearance="transparent"
|
||||
onClick={onClose}
|
||||
disabled={title === undefined || !title}
|
||||
disabled={!title}
|
||||
aria-label="Edit conversation name"
|
||||
/>
|
||||
</Tooltip>
|
||||
@@ -188,7 +187,7 @@ export const ChatWindow: React.FC = () => {
|
||||
<AvatarGroupItem name={user.id} key={user.id} />
|
||||
))}
|
||||
</AvatarGroupPopover>
|
||||
<ShareBotMenu chatId={selectedId} chatTitle={title || ''} />
|
||||
<ShareBotMenu chatId={selectedId} chatTitle={title} />
|
||||
</div>
|
||||
</div>
|
||||
{selectedTab === 'chat' && <ChatRoom />}
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
import { FC, useCallback } from 'react';
|
||||
import React, { FC, useCallback } from 'react';
|
||||
|
||||
import { Button, Menu, MenuItem, MenuList, MenuPopover, MenuTrigger, Tooltip } from '@fluentui/react-components';
|
||||
import { ArrowDownloadRegular, PeopleTeamAddRegular, ShareRegular } from '@fluentui/react-icons';
|
||||
import React from 'react';
|
||||
import { useChat } from '../../libs/useChat';
|
||||
import { useFile } from '../../libs/useFile';
|
||||
import { InvitationCreateDialog } from './invitation-dialog/InvitationCreateDialog';
|
||||
@@ -17,16 +16,17 @@ interface ShareBotMenuProps {
|
||||
export const ShareBotMenu: FC<ShareBotMenuProps> = ({ chatId, chatTitle }) => {
|
||||
const chat = useChat();
|
||||
const { downloadFile } = useFile();
|
||||
const [ isGettingInvitationId, setIsGettingInvitationId ] = React.useState(false);
|
||||
const [isGettingInvitationId, setIsGettingInvitationId] = React.useState(false);
|
||||
|
||||
const onDownloadBotClick = useCallback(async () => {
|
||||
const onDownloadBotClick = useCallback(() => {
|
||||
// TODO: Add a loading indicator
|
||||
const content = await chat.downloadBot(chatId);
|
||||
downloadFile(
|
||||
`chat-history-${chatTitle}-${new Date().toISOString()}.json`,
|
||||
JSON.stringify(content),
|
||||
'text/json',
|
||||
);
|
||||
void chat.downloadBot(chatId).then((content) => {
|
||||
downloadFile(
|
||||
`chat-history-${chatTitle}-${new Date().toISOString()}.json`,
|
||||
JSON.stringify(content),
|
||||
'text/json',
|
||||
);
|
||||
});
|
||||
}, [chat, chatId, chatTitle, downloadFile]);
|
||||
|
||||
return (
|
||||
@@ -42,16 +42,25 @@ export const ShareBotMenu: FC<ShareBotMenuProps> = ({ chatId, chatTitle }) => {
|
||||
<MenuItem icon={<ArrowDownloadRegular />} onClick={onDownloadBotClick}>
|
||||
Download your Bot
|
||||
</MenuItem>
|
||||
<MenuItem icon={<PeopleTeamAddRegular />} onClick={() => setIsGettingInvitationId(true)}>
|
||||
<MenuItem
|
||||
icon={<PeopleTeamAddRegular />}
|
||||
onClick={() => {
|
||||
setIsGettingInvitationId(true);
|
||||
}}
|
||||
>
|
||||
Invite others to your Bot
|
||||
</MenuItem>
|
||||
</MenuList>
|
||||
</MenuPopover>
|
||||
</Menu>
|
||||
{isGettingInvitationId &&
|
||||
{isGettingInvitationId && (
|
||||
<InvitationCreateDialog
|
||||
onCancel={() => setIsGettingInvitationId(false)}
|
||||
chatId={chatId} />}
|
||||
onCancel={() => {
|
||||
setIsGettingInvitationId(false);
|
||||
}}
|
||||
chatId={chatId}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
+2
-2
@@ -75,14 +75,14 @@ export const ChatHistoryItem: React.FC<ChatHistoryItemProps> = ({ message, getRe
|
||||
const chat = useChat();
|
||||
const { conversations, selectedId } = useAppSelector((state: RootState) => state.conversations);
|
||||
|
||||
const isMe = message.authorRole === AuthorRoles.User && message.userId === account?.homeAccountId!;
|
||||
const isMe = message.authorRole === AuthorRoles.User && message.userId === account?.homeAccountId;
|
||||
const isBot = message.authorRole === AuthorRoles.Bot;
|
||||
const user = chat.getChatUserById(message.userName, selectedId, conversations[selectedId].users);
|
||||
const fullName = user?.fullName ?? message.userName;
|
||||
|
||||
const avatar = isBot
|
||||
? { image: { src: conversations[selectedId].botProfilePicture } }
|
||||
: { name: fullName, color: 'colorful' as 'colorful' };
|
||||
: { name: fullName, color: 'colorful' as const };
|
||||
|
||||
let content: JSX.Element;
|
||||
if (isBot && message.type === ChatMessageType.Plan) {
|
||||
|
||||
@@ -89,14 +89,13 @@ export const ChatList: FC = () => {
|
||||
if (filterText !== '') {
|
||||
// Reapply search string to the updated conversations list.
|
||||
const filteredConversations: Conversations = {};
|
||||
for (var key in conversations) {
|
||||
for (const key in conversations) {
|
||||
if (conversations[key].title.toLowerCase().includes(filterText.toLowerCase())) {
|
||||
filteredConversations[key] = conversations[key];
|
||||
}
|
||||
}
|
||||
setConversationsView(filteredConversations);
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
// If no search string, show full conversations list.
|
||||
setConversationsView(conversations);
|
||||
}
|
||||
@@ -111,7 +110,7 @@ export const ChatList: FC = () => {
|
||||
setIsFiltering(false);
|
||||
};
|
||||
|
||||
const onSearch = (ev: any, data: InputOnChangeData) => {
|
||||
const onSearch = (ev: React.ChangeEvent<HTMLInputElement>, data: InputOnChangeData) => {
|
||||
ev.preventDefault();
|
||||
setFilterText(data.value);
|
||||
};
|
||||
@@ -154,7 +153,7 @@ export const ChatList: FC = () => {
|
||||
return (
|
||||
<TreeItem
|
||||
key={id}
|
||||
leaf
|
||||
itemType="leaf"
|
||||
style={isSelected ? { background: tokens.colorNeutralBackground1 } : undefined}
|
||||
>
|
||||
<ChatListItem
|
||||
@@ -168,7 +167,7 @@ export const ChatList: FC = () => {
|
||||
? 'Sent a file'
|
||||
: isPlan(lastMessage.content)
|
||||
? 'Click to view proposed plan'
|
||||
: (lastMessage.content as string)
|
||||
: lastMessage.content
|
||||
: 'Click to start the chat'
|
||||
}
|
||||
botProfilePicture={convo.botProfilePicture}
|
||||
|
||||
@@ -25,19 +25,21 @@ export const NewBotMenu: FC = () => {
|
||||
const fileUploaderRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
const onAddChat = () => {
|
||||
chat.createChat();
|
||||
setIsNewBotMenuOpen(false);
|
||||
void chat.createChat().then(() => {
|
||||
setIsNewBotMenuOpen(false);
|
||||
});
|
||||
};
|
||||
|
||||
const onUpload = useCallback(
|
||||
(file: File) => {
|
||||
fileHandler
|
||||
.loadFile<Bot>(file, chat.uploadBot)
|
||||
.catch((error) =>
|
||||
dispatch(
|
||||
addAlert({ message: `Failed to parse uploaded file. ${error.message}`, type: AlertType.Error }),
|
||||
),
|
||||
);
|
||||
fileHandler.loadFile<Bot>(file, chat.uploadBot).catch((error) =>
|
||||
dispatch(
|
||||
addAlert({
|
||||
message: `Failed to parse uploaded file. ${error instanceof Error ? error.message : ''}`,
|
||||
type: AlertType.Error,
|
||||
}),
|
||||
),
|
||||
);
|
||||
setIsNewBotMenuOpen(false);
|
||||
},
|
||||
[fileHandler, chat, dispatch],
|
||||
@@ -60,7 +62,9 @@ export const NewBotMenu: FC = () => {
|
||||
<Button
|
||||
icon={<BotAdd20Regular />}
|
||||
appearance="transparent"
|
||||
onClick={() => setIsNewBotMenuOpen(!isNewBotMenuOpen)}
|
||||
onClick={() => {
|
||||
setIsNewBotMenuOpen(!isNewBotMenuOpen);
|
||||
}}
|
||||
/>
|
||||
</Tooltip>
|
||||
</MenuTrigger>
|
||||
@@ -69,10 +73,7 @@ export const NewBotMenu: FC = () => {
|
||||
<MenuItem icon={<BotAdd20Regular />} onClick={onAddChat}>
|
||||
Add a new Bot
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
icon={<ArrowUploadRegular />}
|
||||
onClick={() => fileUploaderRef.current?.click()}
|
||||
>
|
||||
<MenuItem icon={<ArrowUploadRegular />} onClick={() => fileUploaderRef.current?.click()}>
|
||||
<div>Upload a Bot</div>
|
||||
<FileUploader
|
||||
ref={fileUploaderRef}
|
||||
|
||||
+4
-3
@@ -32,9 +32,10 @@ export const InvitationCreateDialog: React.FC<InvitationCreateDialogProps> = ({
|
||||
|
||||
const classes = useStyles();
|
||||
|
||||
const copyId = useCallback(async () => {
|
||||
navigator.clipboard.writeText(chatId);
|
||||
setIsIdCopied(true);
|
||||
const copyId = useCallback(() => {
|
||||
void navigator.clipboard.writeText(chatId).then(() => {
|
||||
setIsIdCopied(true);
|
||||
});
|
||||
}, [chatId]);
|
||||
|
||||
// Copy the chatId to clipboard by default when component mounts.
|
||||
|
||||
+42
-24
@@ -1,13 +1,25 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
import { Button, DialogActions, DialogBody, DialogContent, DialogSurface, DialogTitle, Divider, Input, Label, makeStyles, tokens } from "@fluentui/react-components";
|
||||
import React from "react";
|
||||
import { useChat } from "../../../libs/useChat";
|
||||
import {
|
||||
Button,
|
||||
DialogActions,
|
||||
DialogBody,
|
||||
DialogContent,
|
||||
DialogSurface,
|
||||
DialogTitle,
|
||||
Divider,
|
||||
Input,
|
||||
Label,
|
||||
makeStyles,
|
||||
tokens,
|
||||
} from '@fluentui/react-components';
|
||||
import React from 'react';
|
||||
import { useChat } from '../../../libs/useChat';
|
||||
|
||||
const useStyles = makeStyles({
|
||||
content: {
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
rowGap: tokens.spacingVerticalMNudge,
|
||||
},
|
||||
divider: {
|
||||
@@ -23,30 +35,30 @@ interface InvitationJoinDialogProps {
|
||||
export const InvitationJoinDialog: React.FC<InvitationJoinDialogProps> = ({ onCloseDialog }) => {
|
||||
const chat = useChat();
|
||||
const [errorOccurred, setErrorOccurred] = React.useState<boolean>(false);
|
||||
const [errorMessage, setErrorMessage] = React.useState<string>("");
|
||||
const [errorMessage, setErrorMessage] = React.useState<string>('');
|
||||
|
||||
const classes = useStyles();
|
||||
|
||||
const handleSubmit = async (ev: React.FormEvent<HTMLFormElement>) => {
|
||||
const handleSubmit = (ev: React.FormEvent<HTMLFormElement>) => {
|
||||
ev.preventDefault();
|
||||
setErrorOccurred(false);
|
||||
|
||||
const chatId = ev.currentTarget.elements.namedItem("chat-id-input") as HTMLInputElement;
|
||||
const chatId = ev.currentTarget.elements.namedItem('chat-id-input') as HTMLInputElement;
|
||||
|
||||
const { success, message } = await chat.joinChat(chatId.value);
|
||||
|
||||
if (success) {
|
||||
onCloseDialog();
|
||||
} else {
|
||||
setErrorOccurred(true);
|
||||
setErrorMessage(message);
|
||||
}
|
||||
void chat.joinChat(chatId.value).then(({ success, message }) => {
|
||||
if (success) {
|
||||
onCloseDialog();
|
||||
} else {
|
||||
setErrorOccurred(true);
|
||||
setErrorMessage(message);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<DialogSurface>
|
||||
<form onSubmit={ handleSubmit }>
|
||||
<form onSubmit={handleSubmit}>
|
||||
<DialogBody>
|
||||
<DialogTitle>Join a Bot</DialogTitle>
|
||||
<DialogContent className={classes.content}>
|
||||
@@ -56,16 +68,22 @@ export const InvitationJoinDialog: React.FC<InvitationJoinDialogProps> = ({ onCl
|
||||
<Input required type="text" id="chat-id-input" />
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button appearance="secondary" onClick={onCloseDialog}>Cancel</Button>
|
||||
<Button type="submit" appearance="primary">Join</Button>
|
||||
<Button appearance="secondary" onClick={onCloseDialog}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button type="submit" appearance="primary">
|
||||
Join
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</DialogBody>
|
||||
</form>
|
||||
{errorOccurred && <div>
|
||||
<Divider className={classes.divider} />
|
||||
<Label size="large">{errorMessage}</Label>
|
||||
</div>}
|
||||
{errorOccurred && (
|
||||
<div>
|
||||
<Divider className={classes.divider} />
|
||||
<Label size="large">{errorMessage}</Label>
|
||||
</div>
|
||||
)}
|
||||
</DialogSurface>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
};
|
||||
|
||||
+13
-9
@@ -22,6 +22,7 @@ import { Constants } from '../../../Constants';
|
||||
import { IPlanInput } from '../../../libs/models/Plan';
|
||||
import { CopilotChatTokens } from '../../../styles';
|
||||
import { PlanStepInput } from './PlanStepInput';
|
||||
import { Plan } from './PlanViewer';
|
||||
|
||||
const useClasses = makeStyles({
|
||||
card: {
|
||||
@@ -74,10 +75,12 @@ const useClasses = makeStyles({
|
||||
});
|
||||
|
||||
interface PlanStepCardProps {
|
||||
// See Semantic Kernel's Plan object for field definitions (step === Plan).
|
||||
// Plan.cs: https://github.com/microsoft/semantic-kernel/blob/df07fc6f28853a481dd6f47e60d39a52fc6c9967/dotnet/src/SemanticKernel/Planning/Plan.cs#
|
||||
// Not explicitly defining the type here to avoid additional overhead of property maintenance.
|
||||
step: any;
|
||||
/* eslint-disable
|
||||
@typescript-eslint/no-unsafe-assignment,
|
||||
@typescript-eslint/no-unsafe-member-access,
|
||||
@typescript-eslint/no-unsafe-call
|
||||
*/
|
||||
step: Plan;
|
||||
enableEdits: boolean;
|
||||
enableStepDelete: boolean;
|
||||
onDeleteStep: (index: number) => void;
|
||||
@@ -101,7 +104,8 @@ export const PlanStepCard: React.FC<PlanStepCardProps> = ({ step, enableEdits, e
|
||||
<CardHeader
|
||||
header={
|
||||
<Body1>
|
||||
<b className={classes.header}>Step {step.index + 1} •</b> {step.skill_name}.{step.name}
|
||||
<b className={classes.header}>Step {(step.index as number) + 1} •</b> {step.skill_name}.
|
||||
{step.name}
|
||||
<br />
|
||||
</Body1>
|
||||
}
|
||||
@@ -122,9 +126,9 @@ export const PlanStepCard: React.FC<PlanStepCardProps> = ({ step, enableEdits, e
|
||||
<DialogBody>
|
||||
<DialogTitle>Are you sure you want to delete this step?</DialogTitle>
|
||||
<DialogContent>
|
||||
Deleting this step could disrupt the plan's initial logic and cause
|
||||
errors in subsequent steps. Make sure the next steps don't depend on
|
||||
this step's outputs.
|
||||
{
|
||||
"Deleting this step could disrupt the plan's initial logic and cause errors in subsequent steps. Make sure the next steps don't depend on this step's outputs."
|
||||
}
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<DialogTrigger disableButtonEnhancement>
|
||||
@@ -141,7 +145,7 @@ export const PlanStepCard: React.FC<PlanStepCardProps> = ({ step, enableEdits, e
|
||||
appearance="primary"
|
||||
onClick={() => {
|
||||
setOpenDialog(false);
|
||||
onDeleteStep(step.index);
|
||||
onDeleteStep(step.index as number);
|
||||
}}
|
||||
>
|
||||
Yes, Delete Step
|
||||
|
||||
+1
-1
@@ -62,7 +62,7 @@ export const PlanStepInput: React.FC<PlanStepInputProps> = ({ input, onEdit, ena
|
||||
style={{ width: input.Value.length * 6, minWidth: '75px' }}
|
||||
placeholder={input.Value}
|
||||
value={inputValue}
|
||||
onChange={(event: any) => {
|
||||
onChange={(event) => {
|
||||
setInputValue(event.target.value);
|
||||
}}
|
||||
onKeyDown={(event) => {
|
||||
|
||||
+34
-9
@@ -38,16 +38,30 @@ interface PlanViewerProps {
|
||||
getResponse: (options: GetResponseOptions) => Promise<void>;
|
||||
}
|
||||
|
||||
/**
|
||||
* See Semantic Kernel's `Plan` object below for full definition.
|
||||
* Not explicitly defining the type here to avoid additional overhead of property maintenance.
|
||||
* https://github.com/microsoft/semantic-kernel/blob/df07fc6f28853a481dd6f47e60d39a52fc6c9967/dotnet/src/SemanticKernel/Planning/Plan.cs#
|
||||
*/
|
||||
|
||||
/*
|
||||
eslint-disable
|
||||
@typescript-eslint/no-unsafe-assignment,
|
||||
@typescript-eslint/no-unsafe-member-access,
|
||||
@typescript-eslint/no-unsafe-call,
|
||||
*/
|
||||
export type Plan = any;
|
||||
|
||||
export const PlanViewer: React.FC<PlanViewerProps> = ({ message, messageIndex, getResponse }) => {
|
||||
const classes = useClasses();
|
||||
const dispatch = useAppDispatch();
|
||||
const { selectedId } = useAppSelector((state: RootState) => state.conversations);
|
||||
|
||||
// Track original plan from user message
|
||||
const parsedContent = JSON.parse(message.content);
|
||||
const parsedContent: Plan = JSON.parse(message.content);
|
||||
const originalPlan = parsedContent.proposedPlan;
|
||||
|
||||
var planState = message.state ?? parsedContent.state;
|
||||
const planState = message.state ?? parsedContent.state;
|
||||
|
||||
// If plan came from ActionPlanner, use parameters from top-level plan state
|
||||
// TODO: Can remove this after consuming nugets with #997 fixed
|
||||
@@ -56,8 +70,8 @@ export const PlanViewer: React.FC<PlanViewerProps> = ({ message, messageIndex, g
|
||||
}
|
||||
|
||||
const userIntentPrefix = 'User Intent:User intent: ';
|
||||
const userIntentIndex = originalPlan.description.indexOf(userIntentPrefix);
|
||||
const description =
|
||||
const userIntentIndex = originalPlan.description.indexOf(userIntentPrefix) as number;
|
||||
const description: string =
|
||||
userIntentIndex !== -1
|
||||
? originalPlan.description.substring(userIntentIndex + userIntentPrefix.length).trim()
|
||||
: '';
|
||||
@@ -68,7 +82,7 @@ export const PlanViewer: React.FC<PlanViewerProps> = ({ message, messageIndex, g
|
||||
dispatch(
|
||||
updateMessageState({
|
||||
newMessageState: planState,
|
||||
messageIndex: messageIndex,
|
||||
messageIndex,
|
||||
chatId: selectedId,
|
||||
}),
|
||||
);
|
||||
@@ -103,7 +117,7 @@ export const PlanViewer: React.FC<PlanViewerProps> = ({ message, messageIndex, g
|
||||
// Invoke plan
|
||||
await getResponse({
|
||||
value: planState === PlanState.PlanApproved ? 'Yes, proceed' : 'No, cancel',
|
||||
contextVariables: contextVariables,
|
||||
contextVariables,
|
||||
messageType: ChatMessageType.Plan,
|
||||
chatId: selectedId,
|
||||
});
|
||||
@@ -124,7 +138,7 @@ export const PlanViewer: React.FC<PlanViewerProps> = ({ message, messageIndex, g
|
||||
return (
|
||||
<PlanStepCard
|
||||
key={`Plan step: ${index}`}
|
||||
step={{ ...step, index: index }}
|
||||
step={{ ...step, index }}
|
||||
enableEdits={planState === PlanState.PlanApprovalRequired}
|
||||
enableStepDelete={plan.steps.length > 1}
|
||||
onDeleteStep={onDeleteStep}
|
||||
@@ -135,10 +149,21 @@ export const PlanViewer: React.FC<PlanViewerProps> = ({ message, messageIndex, g
|
||||
<>
|
||||
Would you like to proceed with the plan?
|
||||
<div className={classes.buttons}>
|
||||
<Button appearance="secondary" onClick={() => onPlanAction(PlanState.PlanRejected)}>
|
||||
<Button
|
||||
appearance="secondary"
|
||||
onClick={() => {
|
||||
void onPlanAction(PlanState.PlanRejected);
|
||||
}}
|
||||
>
|
||||
No, cancel plan
|
||||
</Button>
|
||||
<Button type="submit" appearance="primary" onClick={() => onPlanAction(PlanState.PlanApproved)}>
|
||||
<Button
|
||||
type="submit"
|
||||
appearance="primary"
|
||||
onClick={() => {
|
||||
void onPlanAction(PlanState.PlanApproved);
|
||||
}}
|
||||
>
|
||||
Yes, proceed
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
+1
-1
@@ -35,7 +35,7 @@ export const PromptDetails: React.FC<IPromptDetailsProps> = ({ message }) => {
|
||||
{(!message.prompt)
|
||||
? 'No prompt available. The response is either a plan proposal or a hard-coded response.'
|
||||
: message.prompt.split('\n').map(
|
||||
(paragraph, idx) => <p key={'prompt-details-'+idx}>{paragraph}</p>
|
||||
(paragraph, idx) => <p key={`prompt-details-${idx}`}>{paragraph}</p>,
|
||||
)
|
||||
}
|
||||
</DialogContent>
|
||||
|
||||
+1
-1
@@ -23,7 +23,7 @@ const useStyles = makeStyles({
|
||||
},
|
||||
});
|
||||
|
||||
export const TypingIndicator: React.FC<{}> = () => {
|
||||
export const TypingIndicator: React.FC = () => {
|
||||
const classes = useStyles();
|
||||
|
||||
return (
|
||||
|
||||
+5
-5
@@ -20,17 +20,17 @@ interface TypingIndicatorRendererProps {
|
||||
export const TypingIndicatorRenderer: React.FC<TypingIndicatorRendererProps> = ({ isBotTyping, numberOfUsersTyping }) => {
|
||||
const classes = useClasses();
|
||||
|
||||
let message = "";
|
||||
let message = '';
|
||||
if (isBotTyping) {
|
||||
if (numberOfUsersTyping === 0) {
|
||||
message = "Bot is typing";
|
||||
message = 'Bot is typing';
|
||||
} else if (numberOfUsersTyping === 1) {
|
||||
message = `Bot and 1 user are typing`;
|
||||
message = 'Bot and 1 user are typing';
|
||||
} else {
|
||||
message = `Bot and ${numberOfUsersTyping} users are typing`;
|
||||
}
|
||||
} else if (numberOfUsersTyping === 1) {
|
||||
message = "1 user is typing";
|
||||
message = '1 user is typing';
|
||||
} else if (numberOfUsersTyping > 1) {
|
||||
message = `${numberOfUsersTyping} users are typing`;
|
||||
}
|
||||
@@ -46,5 +46,5 @@ export const TypingIndicatorRenderer: React.FC<TypingIndicatorRendererProps> = (
|
||||
</div>
|
||||
);
|
||||
|
||||
return <Animation name="slideInCubic" keyframeParams={{ distance: '2.4rem' }} children={typingIndicator} />;
|
||||
return <Animation name="slideInCubic" keyframeParams={{ distance: '2.4rem' }}>{typingIndicator}</Animation>;
|
||||
};
|
||||
|
||||
@@ -39,9 +39,9 @@ export const UserSettings: FC<IUserSettingsProps> = ({ setLoadingState }) => {
|
||||
|
||||
const account = instance.getActiveAccount();
|
||||
|
||||
const onLogout = useCallback(async () => {
|
||||
const onLogout = useCallback(() => {
|
||||
setLoadingState();
|
||||
await AuthHelper.logoutAsync(instance);
|
||||
AuthHelper.logoutAsync(instance);
|
||||
resetState();
|
||||
}, [instance, setLoadingState]);
|
||||
|
||||
|
||||
+42
-42
@@ -65,18 +65,18 @@ export const PluginConnector: React.FC<PluginConnectorProps> = ({
|
||||
}) => {
|
||||
const classes = useClasses();
|
||||
|
||||
const usernameRequired = authRequirements.username;
|
||||
const emailRequired = authRequirements.email;
|
||||
const passwordRequired = authRequirements.password;
|
||||
const accessTokenRequired = authRequirements.personalAccessToken;
|
||||
const msalRequired = authRequirements.Msal;
|
||||
const oauthRequired = authRequirements.OAuth;
|
||||
const usernameRequired = !!authRequirements.username;
|
||||
const emailRequired = !!authRequirements.email;
|
||||
const passwordRequired = !!authRequirements.password;
|
||||
const accessTokenRequired = !!authRequirements.personalAccessToken;
|
||||
const msalRequired = !!authRequirements.Msal;
|
||||
const oauthRequired = !!authRequirements.OAuth;
|
||||
|
||||
const [username, setUsername] = useState('');
|
||||
const [email, setEmail] = useState('');
|
||||
const [password, setPassword] = useState('');
|
||||
const [accessToken, setAccessToken] = useState('');
|
||||
const [apiPropertiesInput, setApiRequirementsInput] = useState(apiProperties);
|
||||
const [apiPropertiesInput, setApiRequirementsInput] = useState(apiProperties ?? {});
|
||||
|
||||
const [open, setOpen] = useState(false);
|
||||
const [errorMessage, setErrorMessage] = useState<string | undefined>();
|
||||
@@ -84,38 +84,39 @@ export const PluginConnector: React.FC<PluginConnectorProps> = ({
|
||||
const dispatch = useAppDispatch();
|
||||
const { instance, inProgress } = useMsal();
|
||||
|
||||
const handleSubmit = async (event: FormEvent) => {
|
||||
const handleSubmit = (event: FormEvent) => {
|
||||
event.preventDefault();
|
||||
try {
|
||||
if (msalRequired) {
|
||||
const token = await TokenHelper.getAccessTokenUsingMsal(inProgress, instance, authRequirements.scopes!);
|
||||
dispatch(
|
||||
connectPlugin({
|
||||
plugin: name,
|
||||
accessToken: token,
|
||||
apiProperties: apiPropertiesInput,
|
||||
}),
|
||||
);
|
||||
} else if (oauthRequired) {
|
||||
// TODO: implement OAuth Flow
|
||||
} else {
|
||||
// Basic Auth or PAT
|
||||
dispatch(
|
||||
connectPlugin({
|
||||
plugin: name,
|
||||
username: username,
|
||||
email: email,
|
||||
password: password,
|
||||
accessToken: accessToken,
|
||||
apiProperties: apiPropertiesInput,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
setOpen(false);
|
||||
} catch (_e) {
|
||||
setErrorMessage(`Could not authenticate to ${name}. Check your permissions and try again.`);
|
||||
if (msalRequired) {
|
||||
TokenHelper.getAccessTokenUsingMsal(inProgress, instance, authRequirements.scopes ?? [])
|
||||
.then((token) => {
|
||||
dispatch(
|
||||
connectPlugin({
|
||||
plugin: name,
|
||||
accessToken: token,
|
||||
apiProperties: apiPropertiesInput,
|
||||
}),
|
||||
);
|
||||
})
|
||||
.catch(() => {
|
||||
setErrorMessage(`Could not authenticate to ${name}. Check your permissions and try again.`);
|
||||
});
|
||||
} else if (oauthRequired) {
|
||||
// TODO: implement OAuth Flow
|
||||
} else {
|
||||
// Basic Auth or PAT
|
||||
dispatch(
|
||||
connectPlugin({
|
||||
plugin: name,
|
||||
username,
|
||||
email,
|
||||
password,
|
||||
accessToken,
|
||||
apiProperties: apiPropertiesInput,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
setOpen(false);
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -161,7 +162,7 @@ export const PluginConnector: React.FC<PluginConnectorProps> = ({
|
||||
<>
|
||||
To continue, you will authorize the following:{' '}
|
||||
<div className={classes.scopes}>
|
||||
{authRequirements.scopes?.map((scope) => {
|
||||
{authRequirements.scopes.map((scope) => {
|
||||
return <Text key={scope}>{scope}</Text>;
|
||||
})}
|
||||
</div>
|
||||
@@ -173,8 +174,7 @@ export const PluginConnector: React.FC<PluginConnectorProps> = ({
|
||||
{(msalRequired || oauthRequired) && (
|
||||
<Body1>
|
||||
{' '}
|
||||
You will be prompted into sign in with {publisher} on the next screen if you haven't
|
||||
already provided prior consent.
|
||||
{`You will be prompted into sign in with ${publisher} on the next screen if you haven't already provided prior consent.`}
|
||||
</Body1>
|
||||
)}
|
||||
{usernameRequired && (
|
||||
@@ -243,9 +243,9 @@ export const PluginConnector: React.FC<PluginConnectorProps> = ({
|
||||
{apiProperties && (
|
||||
<>
|
||||
<Body1Strong> Configuration </Body1Strong>
|
||||
<Body1>Some additional information is required to enable {name}'s REST APIs.</Body1>
|
||||
<Body1>{`Some additional information is required to enable ${name}'s REST APIs.`}</Body1>
|
||||
{Object.keys(apiProperties).map((property) => {
|
||||
const propertyDetails = apiPropertiesInput![property];
|
||||
const propertyDetails = apiPropertiesInput[property];
|
||||
return (
|
||||
<div className={classes.section} key={property}>
|
||||
<Input
|
||||
|
||||
+1
-1
@@ -59,7 +59,7 @@ export const PluginGallery: React.FC = () => {
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={(_event, data) => setOpen(data.open)}>
|
||||
<Dialog open={open} onOpenChange={(_event, data) => { setOpen(data.open); }}>
|
||||
<DialogTrigger>
|
||||
<Button appearance="transparent" icon={<AppsAddIn24Regular color="white" />} />
|
||||
</DialogTrigger>
|
||||
|
||||
@@ -5,9 +5,9 @@
|
||||
export function convertToAnchorTags(htmlString: string) {
|
||||
// Regular expression to match links, excluding any HTML tags at the end
|
||||
// Since response from bot is plain text, sometimes line breaks and other html tags are included in the response for readability.
|
||||
var linkRegex = /(?:https?):\/\/(\w+:?\w*)?(\S+)(:\d+)?(\/|\/([\w#!:.?+=&%!\-/]))?(?=(<br|<p|<div|<span)\s*\/>|$)/g;
|
||||
const linkRegex = /(?:https?):\/\/(\w+:?\w*)?(\S+)(:\d+)?(\/|\/([\w#!:.?+=&%!\-/]))?(?=(<br|<p|<div|<span)\s*\/>|$)/g;
|
||||
|
||||
var result = htmlString.replace(linkRegex, function (link) {
|
||||
const result = htmlString.replace(linkRegex, function (link) {
|
||||
// Parse URL first -- URL class handles cybersecurity concerns related to URL parsing and manipulation
|
||||
const safeHref = new URL(link).toString();
|
||||
|
||||
|
||||
@@ -13,19 +13,19 @@ const BackendProbe: FC<IData> = ({ uri, onBackendFound }) => {
|
||||
const timer = setInterval(() => {
|
||||
const requestUrl = new URL('healthz', uri);
|
||||
const fetchAsync = async () => {
|
||||
try {
|
||||
var result = await fetch(requestUrl);
|
||||
const result = await fetch(requestUrl);
|
||||
|
||||
if (result.ok) {
|
||||
onBackendFound();
|
||||
}
|
||||
} catch { }
|
||||
if (result.ok) {
|
||||
onBackendFound();
|
||||
}
|
||||
};
|
||||
|
||||
fetchAsync();
|
||||
void fetchAsync();
|
||||
}, 3000);
|
||||
|
||||
return () => clearInterval(timer);
|
||||
return () => {
|
||||
clearInterval(timer);
|
||||
};
|
||||
});
|
||||
|
||||
return (
|
||||
@@ -33,13 +33,12 @@ const BackendProbe: FC<IData> = ({ uri, onBackendFound }) => {
|
||||
<Title3>Looking for your backend</Title3>
|
||||
<Spinner />
|
||||
<Body1>
|
||||
This sample expects to find a Semantic Kernel service from <strong>samples/apps/copilot-chat-app/webapi</strong>{' '}
|
||||
running at <strong>{uri}</strong>
|
||||
This sample expects to find a Semantic Kernel service from{' '}
|
||||
<strong>samples/apps/copilot-chat-app/webapi</strong> running at <strong>{uri}</strong>
|
||||
</Body1>
|
||||
<Body1>
|
||||
Run your Semantic Kernel service locally using{' '}
|
||||
Visual Studio, Visual Studio Code or by typing the following command:{' '}
|
||||
<strong>dotnet run</strong>
|
||||
Run your Semantic Kernel service locally using Visual Studio, Visual Studio Code or by typing the
|
||||
following command: <strong>dotnet run</strong>
|
||||
</Body1>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -12,13 +12,13 @@ export const Login: React.FC = () => {
|
||||
<div style={{ padding: 40, gap: 10, display: 'flex', flexDirection: 'column', alignItems: 'flex-start' }}>
|
||||
<Title3>Login with your Microsoft Account</Title3>
|
||||
<Body1>
|
||||
Don't have an account? Create one for free at{' '}
|
||||
{'Don\'t have an account? Create one for free at'}{' '}
|
||||
<a href="https://account.microsoft.com/" target="_blank" rel="noreferrer">
|
||||
https://account.microsoft.com/
|
||||
</a>
|
||||
</Body1>
|
||||
|
||||
<Button style={{ padding: 0 }} appearance="transparent" onClick={() => instance.loginRedirect()}>
|
||||
<Button style={{ padding: 0 }} appearance="transparent" onClick={() => { instance.loginRedirect().catch(() => { }); }}>
|
||||
<Image src={signInLogo} />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
+1
-2
@@ -18,8 +18,7 @@ const MissingEnvVariablesError: FC<IData> = ({ missingVariables }) => {
|
||||
</div>
|
||||
<div style={{ padding: 80, gap: 20, display: 'flex', flexDirection: 'column', alignItems: 'center' }}>
|
||||
<Title3>
|
||||
Please ensure your ".env" file is set up correctly with all environment variables defined in
|
||||
".env.example" then restart the app.
|
||||
{'Please ensure your ".env" file is set up correctly with all environment variables defined in ".env.example" then restart the app.'}
|
||||
</Title3>
|
||||
<Body1>You are missing the following variables: {missingVariables.join(', ')}</Body1>
|
||||
</div>
|
||||
|
||||
@@ -19,7 +19,7 @@ if (!localStorage.getItem('debug')) {
|
||||
|
||||
let container: HTMLElement | null = null;
|
||||
|
||||
document.addEventListener('DOMContentLoaded', async () => {
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
if (!container) {
|
||||
container = document.getElementById('root');
|
||||
if (!container) {
|
||||
@@ -30,13 +30,13 @@ document.addEventListener('DOMContentLoaded', async () => {
|
||||
const missingEnvVariables = getMissingEnvVariables();
|
||||
const validEnvFile = missingEnvVariables.length === 0;
|
||||
|
||||
var msalInstance: IPublicClientApplication | null = null;
|
||||
let msalInstance: IPublicClientApplication | null = null;
|
||||
if (validEnvFile) {
|
||||
msalInstance = new PublicClientApplication(AuthHelper.msalConfig);
|
||||
|
||||
msalInstance.handleRedirectPromise().then((response) => {
|
||||
void msalInstance.handleRedirectPromise().then((response) => {
|
||||
if (response) {
|
||||
msalInstance?.setActiveAccount(response?.account);
|
||||
msalInstance?.setActiveAccount(response.account);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -46,6 +46,7 @@ document.addEventListener('DOMContentLoaded', async () => {
|
||||
{!validEnvFile && <MissingEnvVariablesError missingVariables={missingEnvVariables} />}
|
||||
{validEnvFile && (
|
||||
<ReduxProvider store={store}>
|
||||
{/* eslint-disable-next-line @typescript-eslint/no-non-null-assertion */}
|
||||
<MsalProvider instance={msalInstance!}>
|
||||
<FluentProvider className="app-container" theme={webLightTheme}>
|
||||
<App />
|
||||
|
||||
@@ -5,7 +5,7 @@ import {
|
||||
EndSessionRequest,
|
||||
IPublicClientApplication,
|
||||
InteractionStatus,
|
||||
LogLevel
|
||||
LogLevel,
|
||||
} from '@azure/msal-browser';
|
||||
import debug from 'debug';
|
||||
import { Constants } from '../../Constants';
|
||||
@@ -54,7 +54,10 @@ const logoutRequest: EndSessionRequest = {
|
||||
};
|
||||
|
||||
const ssoSilentRequest = async (msalInstance: IPublicClientApplication) => {
|
||||
await msalInstance.ssoSilent({ account: msalInstance.getActiveAccount() || undefined, scopes: Constants.msal.semanticKernelScopes });
|
||||
await msalInstance.ssoSilent({
|
||||
account: msalInstance.getActiveAccount() ?? undefined,
|
||||
scopes: Constants.msal.semanticKernelScopes,
|
||||
});
|
||||
};
|
||||
|
||||
const loginAsync = async (instance: IPublicClientApplication) => {
|
||||
@@ -65,7 +68,7 @@ const loginAsync = async (instance: IPublicClientApplication) => {
|
||||
}
|
||||
};
|
||||
|
||||
const logoutAsync = async (instance: IPublicClientApplication) => {
|
||||
const logoutAsync = (instance: IPublicClientApplication) => {
|
||||
if (Constants.msal.method === 'popup') {
|
||||
void instance.logoutPopup(logoutRequest);
|
||||
}
|
||||
|
||||
@@ -16,20 +16,21 @@ enum TokenErrors {
|
||||
export const getAccessTokenUsingMsal = async (
|
||||
inProgress: InteractionStatus,
|
||||
msalInstance: IPublicClientApplication,
|
||||
scopes: Array<string>,
|
||||
extraScopesToConsent?: Array<string>,
|
||||
scopes: string[],
|
||||
extraScopesToConsent?: string[],
|
||||
) => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
const account = msalInstance.getActiveAccount()!;
|
||||
const accessTokenRequest: PopupRequest = {
|
||||
authority: `https://login.microsoftonline.com/${account.tenantId}`,
|
||||
scopes: scopes,
|
||||
extraScopesToConsent: extraScopesToConsent,
|
||||
account: account,
|
||||
scopes,
|
||||
extraScopesToConsent,
|
||||
account,
|
||||
};
|
||||
|
||||
return await acquireToken(accessTokenRequest, msalInstance, inProgress).catch((e) => {
|
||||
if (e.message === TokenErrors.InteractionInProgress) {
|
||||
return interactionInProgressHandler(inProgress, msalInstance, accessTokenRequest);
|
||||
return await acquireToken(accessTokenRequest, msalInstance, inProgress).catch(async (e) => {
|
||||
if (e instanceof Error && e.message === (TokenErrors.InteractionInProgress as string)) {
|
||||
return await interactionInProgressHandler(inProgress, msalInstance, accessTokenRequest);
|
||||
}
|
||||
|
||||
throw e;
|
||||
@@ -41,7 +42,7 @@ const acquireToken = async (
|
||||
msalInstance: IPublicClientApplication,
|
||||
interactionStatus: InteractionStatus,
|
||||
) => {
|
||||
return msalInstance
|
||||
return await msalInstance
|
||||
.acquireTokenSilent(accessTokenRequest)
|
||||
.then(function (accessTokenResponse) {
|
||||
// Acquire token silent success
|
||||
@@ -55,7 +56,7 @@ const acquireToken = async (
|
||||
// throw a new error to be handled in the caller above
|
||||
throw new Error(TokenErrors.InteractionInProgress);
|
||||
} else {
|
||||
return msalInstance
|
||||
return await msalInstance
|
||||
.acquireTokenPopup({ ...accessTokenRequest })
|
||||
.then(function (accessTokenResponse) {
|
||||
// Acquire token interactive success
|
||||
@@ -63,11 +64,11 @@ const acquireToken = async (
|
||||
})
|
||||
.catch(function (error) {
|
||||
// Acquire token interactive failure
|
||||
throw new Error(`Received error while retrieving access token: ${error}`);
|
||||
throw new Error(`Received error while retrieving access token: ${error as string}`);
|
||||
});
|
||||
}
|
||||
}
|
||||
throw new Error(`Received error while retrieving access token: ${error}`);
|
||||
throw new Error(`Received error while retrieving access token: ${error as string}`);
|
||||
});
|
||||
};
|
||||
|
||||
@@ -78,24 +79,20 @@ const interactionInProgressHandler = async (
|
||||
) => {
|
||||
// Polls the interaction status from the application
|
||||
// state and resolves when it's equal to "None".
|
||||
await waitFor(() => interactionStatus === InteractionStatus.None);
|
||||
waitFor(() => interactionStatus === InteractionStatus.None);
|
||||
|
||||
// Wait is over, call acquireToken again to re-try acquireTokenSilent
|
||||
return await acquireToken(accessTokenRequest, msalInstance, interactionStatus);
|
||||
};
|
||||
|
||||
const waitFor = (hasInteractionCompleted: () => boolean) => {
|
||||
const checkInteraction = (resolve: (arg0: null) => void, _reject: any) => {
|
||||
const interactionInProgress = !hasInteractionCompleted();
|
||||
while (interactionInProgress) {
|
||||
setTimeout(() => {
|
||||
hasInteractionCompleted();
|
||||
}, 500);
|
||||
const checkInteraction = () => {
|
||||
if (!hasInteractionCompleted()) {
|
||||
setTimeout(checkInteraction, 500);
|
||||
}
|
||||
resolve(null);
|
||||
};
|
||||
|
||||
return new Promise(checkInteraction);
|
||||
checkInteraction();
|
||||
};
|
||||
|
||||
export const TokenHelper = {
|
||||
|
||||
@@ -3,4 +3,4 @@
|
||||
export interface IChatParticipant {
|
||||
userId: string;
|
||||
chatId: string;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
export interface IChatSession {
|
||||
id: string;
|
||||
title: string;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
export type IPlanInput = {
|
||||
export interface IPlanInput {
|
||||
// These have to be capitalized to match the server response
|
||||
Key: string;
|
||||
Value: string;
|
||||
};
|
||||
}
|
||||
|
||||
export enum PlanState {
|
||||
NoOp,
|
||||
|
||||
@@ -5,7 +5,7 @@ export interface IAskResult {
|
||||
variables: Variable[];
|
||||
}
|
||||
|
||||
export type Variable = {
|
||||
export interface Variable {
|
||||
key: string;
|
||||
value: string;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,19 +1,19 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
export const SK_HTTP_HEADER_COMPLETION_MODEL: string = 'x-ms-sk-completion-model';
|
||||
export const SK_HTTP_HEADER_COMPLETION_ENDPOINT: string = 'x-ms-sk-completion-endpoint';
|
||||
export const SK_HTTP_HEADER_COMPLETION_BACKEND: string = 'x-ms-sk-completion-backend';
|
||||
export const SK_HTTP_HEADER_COMPLETION_KEY: string = 'x-ms-sk-completion-key';
|
||||
|
||||
export const SK_HTTP_HEADER_EMBEDDING_MODEL: string = 'x-ms-sk-embedding-model';
|
||||
export const SK_HTTP_HEADER_EMBEDDING_ENDPOINT: string = 'x-ms-sk-embedding-endpoint';
|
||||
export const SK_HTTP_HEADER_EMBEDDING_BACKEND: string = 'x-ms-sk-embedding-backend';
|
||||
export const SK_HTTP_HEADER_EMBEDDING_KEY: string = 'x-ms-sk-embedding-key';
|
||||
export const SK_HTTP_HEADER_COMPLETION_MODEL = 'x-ms-sk-completion-model';
|
||||
export const SK_HTTP_HEADER_COMPLETION_ENDPOINT = 'x-ms-sk-completion-endpoint';
|
||||
export const SK_HTTP_HEADER_COMPLETION_BACKEND = 'x-ms-sk-completion-backend';
|
||||
export const SK_HTTP_HEADER_COMPLETION_KEY = 'x-ms-sk-completion-key';
|
||||
|
||||
export const SK_HTTP_HEADER_MSGRAPH: string = 'x-ms-sk-msgraph';
|
||||
export const SK_HTTP_HEADER_EMBEDDING_MODEL = 'x-ms-sk-embedding-model';
|
||||
export const SK_HTTP_HEADER_EMBEDDING_ENDPOINT = 'x-ms-sk-embedding-endpoint';
|
||||
export const SK_HTTP_HEADER_EMBEDDING_BACKEND = 'x-ms-sk-embedding-backend';
|
||||
export const SK_HTTP_HEADER_EMBEDDING_KEY = 'x-ms-sk-embedding-key';
|
||||
|
||||
export const SK_HTTP_HEADER_MSGRAPH = 'x-ms-sk-msgraph';
|
||||
|
||||
export interface IBackendConfig {
|
||||
backend: number; //OpenAI = 1, Azure OpenAI = 0
|
||||
backend: number; // OpenAI = 1, Azure OpenAI = 0
|
||||
label: string;
|
||||
deploymentOrModelId: string;
|
||||
endpoint: string;
|
||||
@@ -25,4 +25,4 @@ export interface IKeyConfig {
|
||||
|
||||
embeddingConfig: IBackendConfig;
|
||||
completionConfig: IBackendConfig;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
import { AdditionalApiProperties, AuthHeaderTags } from '../../redux/features/plugins/PluginsState';
|
||||
import { Plugin } from '../../redux/features/plugins/PluginsState';
|
||||
|
||||
interface ServiceRequest {
|
||||
commandPath: string;
|
||||
@@ -16,11 +16,7 @@ export class BaseService {
|
||||
protected readonly getResponseAsync = async <T>(
|
||||
request: ServiceRequest,
|
||||
accessToken: string,
|
||||
enabledPlugins?: {
|
||||
headerTag: AuthHeaderTags;
|
||||
authData: string;
|
||||
apiProperties?: AdditionalApiProperties;
|
||||
}[],
|
||||
enabledPlugins?: Plugin[],
|
||||
): Promise<T> => {
|
||||
const { commandPath, method, body } = request;
|
||||
const isFormData = body instanceof FormData;
|
||||
@@ -30,20 +26,19 @@ export class BaseService {
|
||||
});
|
||||
|
||||
if (!isFormData) {
|
||||
headers.append(`Content-Type`, 'application/json');
|
||||
headers.append('Content-Type', 'application/json');
|
||||
}
|
||||
|
||||
// API key auth for private hosted instances
|
||||
if (process.env.REACT_APP_SK_API_KEY) {
|
||||
headers.append(`x-sk-api-key`, process.env.REACT_APP_SK_API_KEY as string);
|
||||
headers.append('x-sk-api-key', process.env.REACT_APP_SK_API_KEY);
|
||||
}
|
||||
|
||||
if (enabledPlugins && enabledPlugins.length > 0) {
|
||||
// For each enabled plugin, pass its auth information as a customer header
|
||||
// to the backend so the server can authenticate to the plugin
|
||||
for (var idx in enabledPlugins) {
|
||||
var plugin = enabledPlugins[idx];
|
||||
headers.append(`x-sk-copilot-${plugin.headerTag}-auth`, plugin.authData);
|
||||
for (const plugin of enabledPlugins) {
|
||||
headers.append(`x-sk-copilot-${plugin.headerTag}-auth`, plugin.authData ?? '');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -52,26 +47,27 @@ export class BaseService {
|
||||
const response = await fetch(requestUrl, {
|
||||
method: method ?? 'GET',
|
||||
body: isFormData ? body : JSON.stringify(body),
|
||||
headers: headers,
|
||||
headers,
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const responseText = await response.text();
|
||||
const errorMessage =
|
||||
`${response.status}: ${response.statusText}` + (responseText ? ` => ${responseText}` : '');
|
||||
const errorMessage = `${response.status}: ${response.statusText}${
|
||||
responseText ? ` => ${responseText}` : ''
|
||||
}`;
|
||||
|
||||
throw Object.assign(new Error(errorMessage));
|
||||
}
|
||||
|
||||
return noResponseBodyStatusCodes.includes(response.status) ? ({} as T) : ((await response.json()) as T);
|
||||
} catch (e) {
|
||||
var additional_error_msg = '';
|
||||
return (noResponseBodyStatusCodes.includes(response.status) ? {} : await response.json()) as T;
|
||||
} catch (e: any) {
|
||||
let additionalErrorMsg = '';
|
||||
if (e instanceof TypeError) {
|
||||
// fetch() will reject with a TypeError when a network error is encountered.
|
||||
additional_error_msg =
|
||||
additionalErrorMsg =
|
||||
'\n\nPlease check that your backend is running and that it is accessible by the app';
|
||||
}
|
||||
throw Object.assign(new Error(e + additional_error_msg));
|
||||
throw Object.assign(new Error(`${e as string} ${additionalErrorMsg}`));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,14 +1,12 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
import { Bot } from '../models/Bot';
|
||||
import { IChatSession } from '../models/ChatSession';
|
||||
import { BaseService } from './BaseService';
|
||||
|
||||
export class BotService extends BaseService {
|
||||
public downloadAsync = async (
|
||||
chatId: string,
|
||||
accessToken: string,
|
||||
): Promise<any> => {
|
||||
const result = await this.getResponseAsync<any>(
|
||||
public downloadAsync = async (chatId: string, accessToken: string) => {
|
||||
const result = await this.getResponseAsync<object>(
|
||||
{
|
||||
commandPath: `bot/download/${chatId}`,
|
||||
method: 'GET',
|
||||
@@ -19,9 +17,9 @@ export class BotService extends BaseService {
|
||||
return result;
|
||||
};
|
||||
|
||||
public uploadAsync = async (bot: Bot, userId: string, accessToken: string): Promise<any> => {
|
||||
public uploadAsync = async (bot: Bot, userId: string, accessToken: string) => {
|
||||
// TODO: return type
|
||||
const result = await this.getResponseAsync<any>(
|
||||
const result = await this.getResponseAsync<IChatSession>(
|
||||
{
|
||||
commandPath: `bot/upload?userId=${userId}`,
|
||||
method: 'Post',
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
import { AdditionalApiProperties, AuthHeaderTags } from '../../redux/features/plugins/PluginsState';
|
||||
import { Plugin } from '../../redux/features/plugins/PluginsState';
|
||||
import { ChatMemorySource } from '../models/ChatMemorySource';
|
||||
import { IChatMessage } from '../models/ChatMessage';
|
||||
import { IChatParticipant } from '../models/ChatParticipant';
|
||||
@@ -11,21 +11,17 @@ import { IAskResult } from '../semantic-kernel/model/AskResult';
|
||||
import { BaseService } from './BaseService';
|
||||
|
||||
export class ChatService extends BaseService {
|
||||
public createChatAsync = async (
|
||||
userId: string,
|
||||
title: string,
|
||||
accessToken: string,
|
||||
): Promise<IChatSession> => {
|
||||
public createChatAsync = async (userId: string, title: string, accessToken: string): Promise<IChatSession> => {
|
||||
const body = {
|
||||
userId: userId,
|
||||
title: title,
|
||||
userId,
|
||||
title,
|
||||
};
|
||||
|
||||
const result = await this.getResponseAsync<IChatSession>(
|
||||
{
|
||||
commandPath: `chatSession/create`,
|
||||
commandPath: 'chatSession/create',
|
||||
method: 'POST',
|
||||
body: body,
|
||||
body,
|
||||
},
|
||||
accessToken,
|
||||
);
|
||||
@@ -78,14 +74,14 @@ export class ChatService extends BaseService {
|
||||
public editChatAsync = async (chatId: string, title: string, accessToken: string): Promise<any> => {
|
||||
const body: IChatSession = {
|
||||
id: chatId,
|
||||
title: title,
|
||||
title,
|
||||
};
|
||||
|
||||
const result = await this.getResponseAsync<any>(
|
||||
const result = await this.getResponseAsync<IChatSession>(
|
||||
{
|
||||
commandPath: `chatSession/edit`,
|
||||
commandPath: 'chatSession/edit',
|
||||
method: 'POST',
|
||||
body: body,
|
||||
body,
|
||||
},
|
||||
accessToken,
|
||||
);
|
||||
@@ -96,33 +92,27 @@ export class ChatService extends BaseService {
|
||||
public getBotResponseAsync = async (
|
||||
ask: IAsk,
|
||||
accessToken: string,
|
||||
enabledPlugins?: {
|
||||
headerTag: AuthHeaderTags;
|
||||
authData: string;
|
||||
apiProperties?: AdditionalApiProperties;
|
||||
}[],
|
||||
enabledPlugins?: Plugin[],
|
||||
): Promise<IAskResult> => {
|
||||
// If skill requires any additional api properties, append to context
|
||||
if (enabledPlugins && enabledPlugins.length > 0) {
|
||||
const openApiSkillVariables: IAskVariables[] = [];
|
||||
|
||||
for (var idx in enabledPlugins) {
|
||||
var plugin = enabledPlugins[idx];
|
||||
|
||||
for (const plugin of enabledPlugins) {
|
||||
if (plugin.apiProperties) {
|
||||
const apiProperties = plugin.apiProperties;
|
||||
|
||||
for (var property in apiProperties) {
|
||||
for (const property in apiProperties) {
|
||||
const propertyDetails = apiProperties[property];
|
||||
|
||||
if (propertyDetails.required && !propertyDetails.value) {
|
||||
throw new Error(`Missing required property ${property} for ${plugin} skill.`);
|
||||
throw new Error(`Missing required property ${property} for ${plugin.name} skill.`);
|
||||
}
|
||||
|
||||
if (propertyDetails.value) {
|
||||
openApiSkillVariables.push({
|
||||
key: `${property}`,
|
||||
value: apiProperties[property].value!,
|
||||
value: propertyDetails.value,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -134,7 +124,7 @@ export class ChatService extends BaseService {
|
||||
|
||||
const result = await this.getResponseAsync<IAskResult>(
|
||||
{
|
||||
commandPath: `chat`,
|
||||
commandPath: 'chat',
|
||||
method: 'POST',
|
||||
body: ask,
|
||||
},
|
||||
@@ -147,15 +137,15 @@ export class ChatService extends BaseService {
|
||||
|
||||
public joinChatAsync = async (userId: string, chatId: string, accessToken: string): Promise<IChatSession> => {
|
||||
const body: IChatParticipant = {
|
||||
userId: userId,
|
||||
chatId: chatId,
|
||||
userId,
|
||||
chatId,
|
||||
};
|
||||
|
||||
await this.getResponseAsync<any>(
|
||||
{
|
||||
commandPath: `chatParticipant/join`,
|
||||
commandPath: 'chatParticipant/join',
|
||||
method: 'POST',
|
||||
body: body,
|
||||
body,
|
||||
},
|
||||
accessToken,
|
||||
);
|
||||
@@ -176,7 +166,7 @@ export class ChatService extends BaseService {
|
||||
};
|
||||
|
||||
public getAllChatParticipantsAsync = async (chatId: string, accessToken: string): Promise<IChatUser[]> => {
|
||||
const result = await this.getResponseAsync<any>(
|
||||
const result = await this.getResponseAsync<IChatParticipant[]>(
|
||||
{
|
||||
commandPath: `chatParticipant/getAllParticipants/${chatId}`,
|
||||
method: 'GET',
|
||||
@@ -184,15 +174,14 @@ export class ChatService extends BaseService {
|
||||
accessToken,
|
||||
);
|
||||
|
||||
const chatUsers: IChatUser[] = result.map((participant: any) => {
|
||||
return {
|
||||
id: participant.userId,
|
||||
online: false,
|
||||
fullName: '', // The user's full name is not returned from the server
|
||||
emailAddress: '', // The user's email address is not returned from the server
|
||||
isTyping: false,
|
||||
} as IChatUser;
|
||||
});
|
||||
const chatUsers = result.map<IChatUser>((participant) => ({
|
||||
id: participant.userId,
|
||||
online: false,
|
||||
fullName: '', // The user's full name is not returned from the server
|
||||
emailAddress: '', // The user's email address is not returned from the server
|
||||
isTyping: false,
|
||||
photo: '',
|
||||
}));
|
||||
|
||||
return chatUsers;
|
||||
};
|
||||
|
||||
@@ -13,7 +13,7 @@ export class SpeechService extends BaseService {
|
||||
getSpeechTokenAsync = async (accessToken: string): Promise<TokenResponse> => {
|
||||
const result = await this.getResponseAsync<TokenResponse>(
|
||||
{
|
||||
commandPath: `speechToken`,
|
||||
commandPath: 'speechToken',
|
||||
method: 'GET',
|
||||
},
|
||||
accessToken,
|
||||
@@ -22,20 +22,20 @@ export class SpeechService extends BaseService {
|
||||
return result;
|
||||
};
|
||||
|
||||
getSpeechRecognizerAsyncWithValidKey = async (response: TokenResponse) => {
|
||||
getSpeechRecognizerAsyncWithValidKey = (response: TokenResponse) => {
|
||||
const { token, region, isSuccess } = response;
|
||||
|
||||
if(isSuccess) {
|
||||
|
||||
if (isSuccess) {
|
||||
return this.generateSpeechRecognizer(token, region);
|
||||
}
|
||||
|
||||
return;
|
||||
return undefined;
|
||||
};
|
||||
|
||||
private generateSpeechRecognizer ( token: string, region: string) {
|
||||
private generateSpeechRecognizer(token: string, region: string) {
|
||||
const speechConfig = speechSdk.SpeechConfig.fromAuthorizationToken(token, region);
|
||||
speechConfig.speechRecognitionLanguage = 'en-US';
|
||||
const audioConfig = speechSdk.AudioConfig.fromDefaultMicrophoneInput();
|
||||
return new speechSdk.SpeechRecognizer(speechConfig, audioConfig);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ import {
|
||||
setConversations,
|
||||
setSelectedConversation,
|
||||
} from '../redux/features/conversations/conversationsSlice';
|
||||
import { AuthHeaderTags } from '../redux/features/plugins/PluginsState';
|
||||
import { Plugin } from '../redux/features/plugins/PluginsState';
|
||||
import { AuthHelper } from './auth/AuthHelper';
|
||||
import { AlertType } from './models/AlertType';
|
||||
import { Bot } from './models/Bot';
|
||||
@@ -49,10 +49,13 @@ export const useChat = () => {
|
||||
|
||||
const botProfilePictures: string[] = [botIcon1, botIcon2, botIcon3, botIcon4, botIcon5];
|
||||
|
||||
const homeAccountId = account?.homeAccountId ?? '';
|
||||
const emailAddress = account?.username ?? '';
|
||||
const fullName = account?.name ?? emailAddress;
|
||||
const loggedInUser: IChatUser = {
|
||||
id: account?.homeAccountId || '',
|
||||
fullName: (account?.name ?? account?.username) || '',
|
||||
emailAddress: account?.username || '',
|
||||
id: homeAccountId,
|
||||
fullName,
|
||||
emailAddress,
|
||||
photo: undefined, // TODO: Make call to Graph /me endpoint to load photo
|
||||
online: true,
|
||||
isTyping: false,
|
||||
@@ -70,18 +73,9 @@ export const useChat = () => {
|
||||
const accessToken = await AuthHelper.getSKaaSAccessToken(instance, inProgress);
|
||||
try {
|
||||
await chatService
|
||||
.createChatAsync(
|
||||
account?.homeAccountId!,
|
||||
chatTitle,
|
||||
accessToken,
|
||||
)
|
||||
.createChatAsync(homeAccountId, chatTitle, accessToken)
|
||||
.then(async (result: IChatSession) => {
|
||||
const chatMessages = await chatService.getChatMessagesAsync(
|
||||
result.id,
|
||||
0,
|
||||
1,
|
||||
accessToken,
|
||||
);
|
||||
const chatMessages = await chatService.getChatMessagesAsync(result.id, 0, 1, accessToken);
|
||||
|
||||
const newChat: ChatState = {
|
||||
id: result.id,
|
||||
@@ -97,7 +91,7 @@ export const useChat = () => {
|
||||
return newChat.id;
|
||||
});
|
||||
} catch (e: any) {
|
||||
const errorMessage = `Unable to create new chat. Details: ${e.message ?? e}`;
|
||||
const errorMessage = `Unable to create new chat. Details: ${getErrorDetails(e)}`;
|
||||
dispatch(addAlert({ message: errorMessage, type: AlertType.Error }));
|
||||
}
|
||||
};
|
||||
@@ -108,11 +102,11 @@ export const useChat = () => {
|
||||
variables: [
|
||||
{
|
||||
key: 'userId',
|
||||
value: account?.homeAccountId!,
|
||||
value: homeAccountId,
|
||||
},
|
||||
{
|
||||
key: 'userName',
|
||||
value: account?.name ?? account?.username!,
|
||||
value: fullName,
|
||||
},
|
||||
{
|
||||
key: 'chatId',
|
||||
@@ -136,7 +130,7 @@ export const useChat = () => {
|
||||
getEnabledPlugins(),
|
||||
);
|
||||
} catch (e: any) {
|
||||
const errorMessage = `Unable to generate bot response. Details: ${e.message ?? e}`;
|
||||
const errorMessage = `Unable to generate bot response. Details: ${getErrorDetails(e)}`;
|
||||
dispatch(addAlert({ message: errorMessage, type: AlertType.Error }));
|
||||
}
|
||||
};
|
||||
@@ -144,26 +138,14 @@ export const useChat = () => {
|
||||
const loadChats = async () => {
|
||||
const accessToken = await AuthHelper.getSKaaSAccessToken(instance, inProgress);
|
||||
try {
|
||||
const chatSessions = await chatService.getAllChatsAsync(
|
||||
account?.homeAccountId!,
|
||||
accessToken,
|
||||
);
|
||||
const chatSessions = await chatService.getAllChatsAsync(homeAccountId, accessToken);
|
||||
|
||||
if (chatSessions.length > 0) {
|
||||
const loadedConversations: Conversations = {};
|
||||
for (const index in chatSessions) {
|
||||
const chatSession = chatSessions[index];
|
||||
const chatMessages = await chatService.getChatMessagesAsync(
|
||||
chatSession.id,
|
||||
0,
|
||||
100,
|
||||
accessToken,
|
||||
);
|
||||
for (const chatSession of chatSessions) {
|
||||
const chatMessages = await chatService.getChatMessagesAsync(chatSession.id, 0, 100, accessToken);
|
||||
|
||||
const chatUsers = await chatService.getAllChatParticipantsAsync(
|
||||
chatSession.id,
|
||||
accessToken,
|
||||
);
|
||||
const chatUsers = await chatService.getAllChatParticipantsAsync(chatSession.id, accessToken);
|
||||
|
||||
loadedConversations[chatSession.id] = {
|
||||
id: chatSession.id,
|
||||
@@ -185,7 +167,7 @@ export const useChat = () => {
|
||||
|
||||
return true;
|
||||
} catch (e: any) {
|
||||
const errorMessage = `Unable to load chats. Details: ${e.message ?? e}`;
|
||||
const errorMessage = `Unable to load chats. Details: ${getErrorDetails(e)}`;
|
||||
dispatch(addAlert({ message: errorMessage, type: AlertType.Error }));
|
||||
|
||||
return false;
|
||||
@@ -194,24 +176,21 @@ export const useChat = () => {
|
||||
|
||||
const downloadBot = async (chatId: string) => {
|
||||
try {
|
||||
return botService.downloadAsync(chatId, await AuthHelper.getSKaaSAccessToken(instance, inProgress));
|
||||
return await botService.downloadAsync(chatId, await AuthHelper.getSKaaSAccessToken(instance, inProgress));
|
||||
} catch (e: any) {
|
||||
const errorMessage = `Unable to download the bot. Details: ${e.message ?? e}`;
|
||||
const errorMessage = `Unable to download the bot. Details: ${getErrorDetails(e)}`;
|
||||
dispatch(addAlert({ message: errorMessage, type: AlertType.Error }));
|
||||
}
|
||||
|
||||
return undefined;
|
||||
};
|
||||
|
||||
const uploadBot = async (bot: Bot) => {
|
||||
const accessToken = await AuthHelper.getSKaaSAccessToken(instance, inProgress);
|
||||
botService
|
||||
.uploadAsync(bot, account?.homeAccountId || '', accessToken)
|
||||
.uploadAsync(bot, homeAccountId, accessToken)
|
||||
.then(async (chatSession: IChatSession) => {
|
||||
const chatMessages = await chatService.getChatMessagesAsync(
|
||||
chatSession.id,
|
||||
0,
|
||||
100,
|
||||
accessToken,
|
||||
);
|
||||
const chatMessages = await chatService.getChatMessagesAsync(chatSession.id, 0, 100, accessToken);
|
||||
|
||||
const newChat = {
|
||||
id: chatSession.id,
|
||||
@@ -225,7 +204,7 @@ export const useChat = () => {
|
||||
dispatch(addConversation(newChat));
|
||||
})
|
||||
.catch((e: any) => {
|
||||
const errorMessage = `Unable to upload the bot. Details: ${e.message ?? e}`;
|
||||
const errorMessage = `Unable to upload the bot. Details: ${getErrorDetails(e)}`;
|
||||
dispatch(addAlert({ message: errorMessage, type: AlertType.Error }));
|
||||
});
|
||||
};
|
||||
@@ -241,7 +220,7 @@ export const useChat = () => {
|
||||
await AuthHelper.getSKaaSAccessToken(instance, inProgress),
|
||||
);
|
||||
} catch (e: any) {
|
||||
const errorMessage = `Unable to get chat files. Details: ${e.message ?? e}`;
|
||||
const errorMessage = `Unable to get chat files. Details: ${getErrorDetails(e)}`;
|
||||
dispatch(addAlert({ message: errorMessage, type: AlertType.Error }));
|
||||
}
|
||||
|
||||
@@ -251,14 +230,14 @@ export const useChat = () => {
|
||||
const importDocument = async (chatId: string, file: File) => {
|
||||
try {
|
||||
await documentImportService.importDocumentAsync(
|
||||
account!.homeAccountId!,
|
||||
(account!.name ?? account!.username) as string,
|
||||
homeAccountId,
|
||||
fullName,
|
||||
chatId,
|
||||
file,
|
||||
await AuthHelper.getSKaaSAccessToken(instance, inProgress),
|
||||
);
|
||||
} catch (e: any) {
|
||||
const errorMessage = `Failed to upload document. Details: ${e.message ?? e}`;
|
||||
const errorMessage = `Failed to upload document. Details: ${getErrorDetails(e)}`;
|
||||
dispatch(addAlert({ message: errorMessage, type: AlertType.Error }));
|
||||
}
|
||||
};
|
||||
@@ -269,46 +248,18 @@ export const useChat = () => {
|
||||
* that the server can use to authenticate to the downstream APIs
|
||||
*/
|
||||
const getEnabledPlugins = () => {
|
||||
const enabledPlugins: { headerTag: AuthHeaderTags; authData: string; apiProperties?: any }[] = [];
|
||||
|
||||
Object.entries(plugins).map((entry) => {
|
||||
const plugin = entry[1];
|
||||
|
||||
if (plugin.enabled) {
|
||||
enabledPlugins.push({
|
||||
headerTag: plugin.headerTag,
|
||||
authData: plugin.authData!,
|
||||
apiProperties: plugin.apiProperties,
|
||||
});
|
||||
}
|
||||
|
||||
return entry;
|
||||
});
|
||||
|
||||
return enabledPlugins;
|
||||
return Object.values<Plugin>(plugins).filter((plugin) => plugin.enabled);
|
||||
};
|
||||
|
||||
const joinChat = async (chatId: string) => {
|
||||
const accessToken = await AuthHelper.getSKaaSAccessToken(instance, inProgress);
|
||||
try {
|
||||
await chatService.joinChatAsync(
|
||||
account!.homeAccountId!,
|
||||
chatId,
|
||||
accessToken
|
||||
).then(async (result: IChatSession) => {
|
||||
await chatService.joinChatAsync(homeAccountId, chatId, accessToken).then(async (result: IChatSession) => {
|
||||
// Get chat messages
|
||||
const chatMessages = await chatService.getChatMessagesAsync(
|
||||
result.id,
|
||||
0,
|
||||
100,
|
||||
accessToken
|
||||
);
|
||||
const chatMessages = await chatService.getChatMessagesAsync(result.id, 0, 100, accessToken);
|
||||
|
||||
// Get chat users
|
||||
const chatUsers = await chatService.getAllChatParticipantsAsync(
|
||||
result.id,
|
||||
accessToken,
|
||||
);
|
||||
const chatUsers = await chatService.getAllChatParticipantsAsync(result.id, accessToken);
|
||||
|
||||
const newChat: ChatState = {
|
||||
id: result.id,
|
||||
@@ -322,13 +273,13 @@ export const useChat = () => {
|
||||
|
||||
dispatch(addConversation(newChat));
|
||||
});
|
||||
} catch (error: any) {
|
||||
const errorMessage = `Error joining chat ${chatId}: ${(error as Error).message}`;
|
||||
} catch (e: any) {
|
||||
const errorMessage = `Error joining chat ${chatId}. Details: ${getErrorDetails(e)}`;
|
||||
return { success: false, message: errorMessage };
|
||||
}
|
||||
|
||||
return { success: true, message: '' };
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
getChatUserById,
|
||||
@@ -342,3 +293,7 @@ export const useChat = () => {
|
||||
joinChat,
|
||||
};
|
||||
};
|
||||
|
||||
function getErrorDetails(e: any) {
|
||||
return e instanceof Error ? e.message : String(e);
|
||||
}
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
export const useFile = () => {
|
||||
function loadFile<T>(file: File, loadCallBack: (data: T) => Promise<void>): Promise<T> {
|
||||
return new Promise((resolve, reject) => {
|
||||
async function loadFile<T>(file: File, loadCallBack: (data: T) => Promise<void>): Promise<T> {
|
||||
return await new Promise((resolve, reject) => {
|
||||
const fileReader = new FileReader();
|
||||
fileReader.onload = async (event: ProgressEvent<FileReader>) => {
|
||||
const content = event?.target?.result as string;
|
||||
const content = event.target?.result as string;
|
||||
try {
|
||||
const parsedData = JSON.parse(content) as T;
|
||||
await loadCallBack(parsedData);
|
||||
|
||||
@@ -4,5 +4,5 @@ export const isPlan = (object: string) => {
|
||||
// backslash has to be escaped since it's a JSON string
|
||||
// eslint-disable-next-line
|
||||
const planPrefix = `proposedPlan\":`;
|
||||
return object.indexOf(planPrefix) !== -1;
|
||||
return object.includes(planPrefix);
|
||||
};
|
||||
|
||||
@@ -1,22 +1,23 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
import { combineReducers, createAction, Reducer } from '@reduxjs/toolkit';
|
||||
import { AnyAction, combineReducers, createAction, Reducer } from '@reduxjs/toolkit';
|
||||
import appReducer from '../features/app/appSlice';
|
||||
import conversationsReducer from '../features/conversations/conversationsSlice';
|
||||
import pluginsReducer from '../features/plugins/pluginsSlice';
|
||||
import { RootState } from './store';
|
||||
|
||||
// Define a top-level root state reset action
|
||||
export const resetApp = createAction('resetApp');
|
||||
|
||||
// Define a root reducer that combines all the reducers
|
||||
const rootReducer: Reducer = combineReducers({
|
||||
const rootReducer: Reducer<RootState> = combineReducers({
|
||||
app: appReducer,
|
||||
conversations: conversationsReducer,
|
||||
plugins: pluginsReducer,
|
||||
});
|
||||
|
||||
// Define a special resetApp reducer that handles resetting the entire state
|
||||
export const resetAppReducer: Reducer = (state: any, action: any) => {
|
||||
export const resetAppReducer = (state: RootState | undefined, action: AnyAction) => {
|
||||
if (action.type === resetApp.type) {
|
||||
state = {
|
||||
// Explicitly call each individual reducer with undefined state.
|
||||
|
||||
@@ -1,24 +1,36 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
import { configureStore } from '@reduxjs/toolkit';
|
||||
import { AnyAction, Dispatch, MiddlewareAPI, MiddlewareArray, ThunkMiddleware, configureStore } from '@reduxjs/toolkit';
|
||||
import { AppState } from '../features/app/AppState';
|
||||
import { ConversationsState } from '../features/conversations/ConversationsState';
|
||||
import { registerSignalREvents, signalRMiddleware, startSignalRConnection } from '../features/message-relay/signalRMiddleware';
|
||||
import {
|
||||
registerSignalREvents,
|
||||
signalRMiddleware,
|
||||
startSignalRConnection,
|
||||
} from '../features/message-relay/signalRMiddleware';
|
||||
import { PluginsState } from '../features/plugins/PluginsState';
|
||||
import resetStateReducer, { resetApp } from './rootReducer';
|
||||
|
||||
export const store = configureStore({
|
||||
export type StoreMiddlewareAPI = MiddlewareAPI<Dispatch, RootState>;
|
||||
export type Store = typeof store;
|
||||
export const store = configureStore<
|
||||
RootState,
|
||||
AnyAction,
|
||||
MiddlewareArray<
|
||||
[ThunkMiddleware<RootState>, (store: StoreMiddlewareAPI) => (next: Dispatch) => (action: any) => any]
|
||||
>
|
||||
>({
|
||||
reducer: resetStateReducer,
|
||||
middleware: (getDefaultMiddleware) => getDefaultMiddleware().concat(signalRMiddleware)
|
||||
middleware: (getDefaultMiddleware) => getDefaultMiddleware().concat(signalRMiddleware),
|
||||
});
|
||||
|
||||
export type RootState = {
|
||||
export interface RootState {
|
||||
app: AppState;
|
||||
conversations: ConversationsState;
|
||||
plugins: PluginsState;
|
||||
};
|
||||
}
|
||||
|
||||
export const getSelectedChatID = () : string => {
|
||||
export const getSelectedChatID = (): string => {
|
||||
return store.getState().conversations.selectedId;
|
||||
};
|
||||
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
import { AlertType } from '../../../libs/models/AlertType';
|
||||
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
export interface AppState {
|
||||
alerts?: Alerts;
|
||||
alerts: Alert[];
|
||||
}
|
||||
|
||||
export type Alert = {
|
||||
export interface Alert {
|
||||
message: string;
|
||||
type: AlertType;
|
||||
};
|
||||
|
||||
export type Alerts = { [key: string]: Alert };
|
||||
}
|
||||
|
||||
@@ -2,36 +2,33 @@
|
||||
|
||||
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
|
||||
import { AlertType } from '../../../libs/models/AlertType';
|
||||
import { Alert, Alerts, AppState } from './AppState';
|
||||
import { Alert, AppState } from './AppState';
|
||||
|
||||
const initialState: AppState = {
|
||||
alerts: {
|
||||
'0': {
|
||||
alerts: [
|
||||
{
|
||||
message:
|
||||
'SK Copilot is designed for internal use only. By using this chat bot, you agree to not to share confidential or customer information or store sensitive information in chat history. Further, you agree that SK Copilot can collect and retain your chat history for service improvement.',
|
||||
type: AlertType.Info,
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
export const appSlice = createSlice({
|
||||
name: 'app',
|
||||
initialState,
|
||||
reducers: {
|
||||
setAlerts: (state: AppState, action: PayloadAction<Alerts>) => {
|
||||
setAlerts: (state: AppState, action: PayloadAction<Alert[]>) => {
|
||||
state.alerts = action.payload;
|
||||
},
|
||||
addAlert: (state: AppState, action: PayloadAction<Alert>) => {
|
||||
const keys = Object.keys(state.alerts ?? []);
|
||||
let newIndex = keys.length.toString();
|
||||
if (keys.length >= 3) {
|
||||
newIndex = keys[0];
|
||||
delete state.alerts![newIndex];
|
||||
if (state.alerts.length === 3) {
|
||||
state.alerts.shift();
|
||||
}
|
||||
state.alerts = { ...state.alerts, [newIndex]: action.payload };
|
||||
state.alerts.push(action.payload);
|
||||
},
|
||||
removeAlert: (state: AppState, action: PayloadAction<string>) => {
|
||||
if (state.alerts) delete state.alerts[action.payload];
|
||||
removeAlert: (state: AppState, action: PayloadAction<number>) => {
|
||||
state.alerts.splice(action.payload, 1);
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@@ -12,4 +12,4 @@ export interface ChatState {
|
||||
lastUpdatedTimestamp?: number;
|
||||
input: string;
|
||||
isBotTyping: boolean;
|
||||
}
|
||||
}
|
||||
|
||||
+3
-5
@@ -3,9 +3,7 @@
|
||||
import { IChatMessage } from '../../../libs/models/ChatMessage';
|
||||
import { ChatState } from './ChatState';
|
||||
|
||||
export type Conversations = {
|
||||
[key: string]: ChatState;
|
||||
};
|
||||
export type Conversations = Record<string, ChatState>;
|
||||
|
||||
export interface ConversationsState {
|
||||
conversations: Conversations;
|
||||
@@ -19,10 +17,10 @@ export const initialState: ConversationsState = {
|
||||
loggedInUserId: '',
|
||||
};
|
||||
|
||||
export type UpdateConversationPayload = {
|
||||
export interface UpdateConversationPayload {
|
||||
id: string;
|
||||
messages: IChatMessage[];
|
||||
};
|
||||
}
|
||||
|
||||
export interface ConversationTitleChange {
|
||||
id: string;
|
||||
|
||||
+36
-29
@@ -38,22 +38,25 @@ export const conversationsSlice: Slice<ConversationsState> = createSlice({
|
||||
state.selectedId = action.payload;
|
||||
},
|
||||
addConversation: (state: ConversationsState, action: PayloadAction<ChatState>) => {
|
||||
const newId = action.payload.id ?? '';
|
||||
const newId = action.payload.id;
|
||||
state.conversations = { [newId]: action.payload, ...state.conversations };
|
||||
state.selectedId = newId;
|
||||
},
|
||||
addUserToConversation: (state: ConversationsState, action: PayloadAction<{ user: IChatUser, chatId: string }>) => {
|
||||
addUserToConversation: (
|
||||
state: ConversationsState,
|
||||
action: PayloadAction<{ user: IChatUser; chatId: string }>,
|
||||
) => {
|
||||
const { user, chatId } = action.payload;
|
||||
state.conversations[chatId].users.push(user);
|
||||
},
|
||||
/*
|
||||
* updateConversationFromUser() and updateConversationFromServer() both update the conversations state.
|
||||
* However they are for different purposes. The former action is for updating the conversation from the
|
||||
* webapp and will be captured by the SignalR middleware and the payload will be broadcasted to all clients
|
||||
* in the same group.
|
||||
* The updateConversationFromServer() action is triggered by the SignalR middleware when a response is received
|
||||
* from the webapi.
|
||||
*/
|
||||
* updateConversationFromUser() and updateConversationFromServer() both update the conversations state.
|
||||
* However they are for different purposes. The former action is for updating the conversation from the
|
||||
* webapp and will be captured by the SignalR middleware and the payload will be broadcasted to all clients
|
||||
* in the same group.
|
||||
* The updateConversationFromServer() action is triggered by the SignalR middleware when a response is received
|
||||
* from the webapi.
|
||||
*/
|
||||
updateConversationFromUser: (
|
||||
state: ConversationsState,
|
||||
action: PayloadAction<{ message: IChatMessage; chatId?: string }>,
|
||||
@@ -79,22 +82,31 @@ export const conversationsSlice: Slice<ConversationsState> = createSlice({
|
||||
frontLoadChat(state, id);
|
||||
},
|
||||
/*
|
||||
* updateUserIsTyping() and updateUserIsTypingFromServer() both update a user's typing state.
|
||||
* However they are for different purposes. The former action is for updating an user's typing state from
|
||||
* the webapp and will be captured by the SignalR middleware and the payload will be broadcasted to all clients
|
||||
* in the same group.
|
||||
* The updateUserIsTypingFromServer() action is triggered by the SignalR middleware when a state is received
|
||||
* from the webapi.
|
||||
*/
|
||||
updateUserIsTyping: (state: ConversationsState, action: PayloadAction<{ userId: string; chatId: string; isTyping: boolean }>) => {
|
||||
* updateUserIsTyping() and updateUserIsTypingFromServer() both update a user's typing state.
|
||||
* However they are for different purposes. The former action is for updating an user's typing state from
|
||||
* the webapp and will be captured by the SignalR middleware and the payload will be broadcasted to all clients
|
||||
* in the same group.
|
||||
* The updateUserIsTypingFromServer() action is triggered by the SignalR middleware when a state is received
|
||||
* from the webapi.
|
||||
*/
|
||||
updateUserIsTyping: (
|
||||
state: ConversationsState,
|
||||
action: PayloadAction<{ userId: string; chatId: string; isTyping: boolean }>,
|
||||
) => {
|
||||
const { userId, chatId, isTyping } = action.payload;
|
||||
updateUserTypingState(state, userId, chatId, isTyping);
|
||||
},
|
||||
updateUserIsTypingFromServer: (state: ConversationsState, action: PayloadAction<{ userId: string; chatId: string; isTyping: boolean }>) => {
|
||||
updateUserIsTypingFromServer: (
|
||||
state: ConversationsState,
|
||||
action: PayloadAction<{ userId: string; chatId: string; isTyping: boolean }>,
|
||||
) => {
|
||||
const { userId, chatId, isTyping } = action.payload;
|
||||
updateUserTypingState(state, userId, chatId, isTyping);
|
||||
},
|
||||
updateBotIsTypingFromServer: (state: ConversationsState, action: PayloadAction<{ chatId: string; isTyping: boolean }>) => {
|
||||
updateBotIsTypingFromServer: (
|
||||
state: ConversationsState,
|
||||
action: PayloadAction<{ chatId: string; isTyping: boolean }>,
|
||||
) => {
|
||||
const { chatId, isTyping } = action.payload;
|
||||
const conversation = state.conversations[chatId];
|
||||
conversation.isBotTyping = isTyping;
|
||||
@@ -120,8 +132,8 @@ export default conversationsSlice.reducer;
|
||||
|
||||
const frontLoadChat = (state: ConversationsState, id: string) => {
|
||||
const conversation = state.conversations[id];
|
||||
delete state.conversations[id];
|
||||
state.conversations = { [id]: conversation, ...state.conversations };
|
||||
const { [id]: _, ...rest } = state.conversations;
|
||||
state.conversations = { [id]: conversation, ...rest };
|
||||
};
|
||||
|
||||
const updateConversation = (state: ConversationsState, chatId: string, message: IChatMessage) => {
|
||||
@@ -129,15 +141,10 @@ const updateConversation = (state: ConversationsState, chatId: string, message:
|
||||
frontLoadChat(state, chatId);
|
||||
};
|
||||
|
||||
const updateUserTypingState = (
|
||||
state: ConversationsState,
|
||||
userId: string,
|
||||
chatId: string,
|
||||
isTyping: boolean
|
||||
) => {
|
||||
const updateUserTypingState = (state: ConversationsState, userId: string, chatId: string, isTyping: boolean) => {
|
||||
const conversation = state.conversations[chatId];
|
||||
const user = conversation.users.find(u => u.id === userId);
|
||||
const user = conversation.users.find((u) => u.id === userId);
|
||||
if (user) {
|
||||
user.isTyping = isTyping;
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
+56
-38
@@ -1,6 +1,7 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
import * as signalR from '@microsoft/signalr';
|
||||
import { AnyAction, Dispatch } from '@reduxjs/toolkit';
|
||||
import { AlertType } from '../../../libs/models/AlertType';
|
||||
import { IChatUser } from '../../../libs/models/ChatUser';
|
||||
import { PlanState } from '../../../libs/models/Plan';
|
||||
@@ -8,7 +9,7 @@ import { IAskResult } from '../../../libs/semantic-kernel/model/AskResult';
|
||||
import { addAlert } from '../app/appSlice';
|
||||
import { ChatState } from '../conversations/ChatState';
|
||||
import { AuthorRoles, ChatMessageType, IChatMessage } from './../../../libs/models/ChatMessage';
|
||||
import { getSelectedChatID } from './../../app/store';
|
||||
import { Store, StoreMiddlewareAPI, getSelectedChatID } from './../../app/store';
|
||||
|
||||
// These have to match the callback names used in the backend
|
||||
const enum SignalRCallbackMethods {
|
||||
@@ -22,9 +23,19 @@ const enum SignalRCallbackMethods {
|
||||
ChatEdited = 'ChatEdited',
|
||||
}
|
||||
|
||||
// The action sent to the SignalR middleware.
|
||||
interface SignalRAction extends AnyAction {
|
||||
payload: {
|
||||
message?: IChatMessage;
|
||||
userId?: string;
|
||||
isTyping?: boolean;
|
||||
id?: string;
|
||||
};
|
||||
}
|
||||
|
||||
// Set up a SignalR connection to the messageRelayHub on the server
|
||||
const setupSignalRConnectionToChatHub = () => {
|
||||
const connectionHubUrl = new URL('/messageRelayHub', process.env.REACT_APP_BACKEND_URI as string);
|
||||
const connectionHubUrl = new URL('/messageRelayHub', process.env.REACT_APP_BACKEND_URI);
|
||||
const signalRConnectionOptions = {
|
||||
skipNegotiation: true,
|
||||
transport: signalR.HttpTransportType.WebSockets,
|
||||
@@ -33,7 +44,7 @@ const setupSignalRConnectionToChatHub = () => {
|
||||
|
||||
// Create the connection instance
|
||||
// withAutomaticReconnect will automatically try to reconnect and generate a new socket connection if needed
|
||||
var hubConnection = new signalR.HubConnectionBuilder()
|
||||
const hubConnection = new signalR.HubConnectionBuilder()
|
||||
.withUrl(connectionHubUrl.toString(), signalRConnectionOptions)
|
||||
.withAutomaticReconnect()
|
||||
.withHubProtocol(new signalR.JsonHubProtocol())
|
||||
@@ -51,48 +62,51 @@ const setupSignalRConnectionToChatHub = () => {
|
||||
|
||||
const hubConnection = setupSignalRConnectionToChatHub();
|
||||
|
||||
const registerCommonSignalConnectionEvents = async (store: any) => {
|
||||
const registerCommonSignalConnectionEvents = (store: Store) => {
|
||||
// Re-establish the connection if connection dropped
|
||||
hubConnection.onclose((error: any) => {
|
||||
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: errorMessage, type: AlertType.Error }));
|
||||
store.dispatch(addAlert({ message: String(errorMessage), type: AlertType.Error }));
|
||||
console.log(errorMessage, error);
|
||||
}
|
||||
});
|
||||
|
||||
hubConnection.onreconnecting((error: any) => {
|
||||
hubConnection.onreconnecting((error) => {
|
||||
if (hubConnection.state === signalR.HubConnectionState.Reconnecting) {
|
||||
const errorMessage = 'Connection lost due to error. Reconnecting...';
|
||||
store.dispatch(addAlert({ message: errorMessage, type: AlertType.Info }));
|
||||
store.dispatch(addAlert({ message: String(errorMessage), type: AlertType.Info }));
|
||||
console.log(errorMessage, error);
|
||||
}
|
||||
});
|
||||
|
||||
hubConnection.onreconnected((connectionId: any) => {
|
||||
hubConnection.onreconnected((connectionId = '') => {
|
||||
if (hubConnection.state === signalR.HubConnectionState.Connected) {
|
||||
const message = 'Connection reestablished.';
|
||||
store.dispatch(addAlert({ message: message, type: AlertType.Success }));
|
||||
store.dispatch(addAlert({ message, type: AlertType.Success }));
|
||||
console.log(message + ` Connected with connectionId ${connectionId}`);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export const startSignalRConnection = async (store: any) => {
|
||||
try {
|
||||
registerCommonSignalConnectionEvents(store);
|
||||
await hubConnection.start();
|
||||
console.assert(hubConnection.state === signalR.HubConnectionState.Connected);
|
||||
console.log('SignalR connection established');
|
||||
} catch (err) {
|
||||
console.assert(hubConnection.state === signalR.HubConnectionState.Disconnected);
|
||||
console.error('SignalR Connection Error: ', err);
|
||||
setTimeout(() => startSignalRConnection(store), 5000);
|
||||
}
|
||||
export const startSignalRConnection = (store: Store) => {
|
||||
registerCommonSignalConnectionEvents(store);
|
||||
hubConnection
|
||||
.start()
|
||||
.then(() => {
|
||||
console.assert(hubConnection.state === signalR.HubConnectionState.Connected);
|
||||
console.log('SignalR connection established');
|
||||
})
|
||||
.catch((err) => {
|
||||
console.assert(hubConnection.state === signalR.HubConnectionState.Disconnected);
|
||||
console.error('SignalR Connection Error: ', err);
|
||||
setTimeout(() => {
|
||||
startSignalRConnection(store);
|
||||
}, 5000);
|
||||
});
|
||||
};
|
||||
|
||||
export const signalRMiddleware = (store: any) => {
|
||||
return (next: any) => async (action: any) => {
|
||||
export const signalRMiddleware = (store: StoreMiddlewareAPI) => {
|
||||
return (next: Dispatch) => (action: SignalRAction) => {
|
||||
// Call the next dispatch method in the middleware chain before performing any async logic
|
||||
const result = next(action);
|
||||
|
||||
@@ -101,25 +115,29 @@ export const signalRMiddleware = (store: any) => {
|
||||
case 'conversations/updateConversationFromUser':
|
||||
hubConnection
|
||||
.invoke('SendMessageAsync', getSelectedChatID(), action.payload.message)
|
||||
.catch((err) => store.dispatch(addAlert({ message: err, type: AlertType.Error })));
|
||||
.catch((err) => store.dispatch(addAlert({ message: String(err), type: AlertType.Error })));
|
||||
break;
|
||||
case 'conversations/updateUserIsTyping':
|
||||
const { userId, isTyping } = action.payload;
|
||||
hubConnection
|
||||
.invoke('SendUserTypingStateAsync', getSelectedChatID(), userId, isTyping)
|
||||
.catch((err) => store.dispatch(addAlert({ message: err, type: AlertType.Error })));
|
||||
.invoke(
|
||||
'SendUserTypingStateAsync',
|
||||
getSelectedChatID(),
|
||||
action.payload.userId,
|
||||
action.payload.isTyping,
|
||||
)
|
||||
.catch((err) => store.dispatch(addAlert({ message: String(err), type: AlertType.Error })));
|
||||
break;
|
||||
case 'conversations/setConversations':
|
||||
Promise.all(
|
||||
Object.keys(action.payload).map(async (id) => {
|
||||
await hubConnection.invoke('AddClientToGroupAsync', id);
|
||||
}),
|
||||
).catch((err) => store.dispatch(addAlert({ message: err, type: AlertType.Error })));
|
||||
).catch((err) => store.dispatch(addAlert({ message: String(err), type: AlertType.Error })));
|
||||
break;
|
||||
case 'conversations/addConversation':
|
||||
hubConnection
|
||||
.invoke('AddClientToGroupAsync', action.payload.id)
|
||||
.catch((err) => store.dispatch(addAlert({ message: err, type: AlertType.Error })));
|
||||
.catch((err) => store.dispatch(addAlert({ message: String(err), type: AlertType.Error })));
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -127,17 +145,16 @@ export const signalRMiddleware = (store: any) => {
|
||||
};
|
||||
};
|
||||
|
||||
export const registerSignalREvents = async (store: any) => {
|
||||
export const registerSignalREvents = (store: Store) => {
|
||||
hubConnection.on(SignalRCallbackMethods.ReceiveMessage, (message: IChatMessage, chatId: string) => {
|
||||
store.dispatch({ type: 'conversations/updateConversationFromServer', payload: { message, chatId } });
|
||||
});
|
||||
|
||||
hubConnection.on(SignalRCallbackMethods.ReceiveResponse, (askResult: IAskResult, chatId: string) => {
|
||||
const loggedInUserId = store.getState().conversations.loggedInUserId;
|
||||
const originalMessageUserId = askResult.variables.find((v) => v.key === 'userId')?.value;
|
||||
const loggedInUserId: string = store.getState().conversations.loggedInUserId;
|
||||
const originalMessageUserId: string | undefined = askResult.variables.find((v) => v.key === 'userId')?.value;
|
||||
const isPlanForLoggedInUser = loggedInUserId === originalMessageUserId;
|
||||
const messageType =
|
||||
Number(askResult.variables.find((v) => v.key === 'messageType')?.value) ?? ChatMessageType.Message;
|
||||
const messageType = Number(askResult.variables.find((v) => v.key === 'messageType')?.value) as ChatMessageType;
|
||||
|
||||
const message = {
|
||||
type: messageType,
|
||||
@@ -158,13 +175,14 @@ export const registerSignalREvents = async (store: any) => {
|
||||
});
|
||||
|
||||
hubConnection.on(SignalRCallbackMethods.UserJoined, (chatId: string, userId: string) => {
|
||||
const user = {
|
||||
const user: IChatUser = {
|
||||
id: userId,
|
||||
online: false,
|
||||
fullName: '',
|
||||
emailAddress: '',
|
||||
isTyping: false,
|
||||
} as IChatUser;
|
||||
photo: '',
|
||||
};
|
||||
store.dispatch({ type: 'conversations/addUserToConversation', payload: { user, chatId } });
|
||||
});
|
||||
|
||||
@@ -200,6 +218,6 @@ export const registerSignalREvents = async (store: any) => {
|
||||
}),
|
||||
);
|
||||
}
|
||||
store.dispatch({ type: 'conversations/editConversationTitle', payload: { id: id, newTitle: title } });
|
||||
store.dispatch({ type: 'conversations/editConversationTitle', payload: { id, newTitle: title } });
|
||||
});
|
||||
};
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { Constants } from '../../../Constants';
|
||||
import GithubIcon from '../../../assets/plugin-icons/github.png';
|
||||
import JiraIcon from '../../../assets/plugin-icons/jira.png';
|
||||
import GraphIcon from '../../../assets/plugin-icons/ms-graph.png';
|
||||
import KlarnaIcon from '../../../assets/plugin-icons/klarna.png';
|
||||
import GraphIcon from '../../../assets/plugin-icons/ms-graph.png';
|
||||
|
||||
/*
|
||||
* For each OpenAPI Spec you're supporting in the Kernel,
|
||||
@@ -22,7 +22,7 @@ export const enum AuthHeaderTags {
|
||||
Klarna = 'klarna',
|
||||
}
|
||||
|
||||
export type PluginAuthRequirements = {
|
||||
export interface PluginAuthRequirements {
|
||||
username?: boolean;
|
||||
email?: boolean;
|
||||
password?: boolean;
|
||||
@@ -31,21 +31,22 @@ export type PluginAuthRequirements = {
|
||||
Msal?: boolean;
|
||||
scopes?: string[];
|
||||
helpLink?: string;
|
||||
};
|
||||
}
|
||||
|
||||
// Additional information required to enable OpenAPI skills, i.e., server-url
|
||||
export type AdditionalApiProperties = {
|
||||
// Key should be the property name and in kebab case (valid format for request header),
|
||||
// make sure it matches exactly with the property name the API requires
|
||||
[key: string]: {
|
||||
// Key should be the property name and in kebab case (valid format for request header),
|
||||
// make sure it matches exactly with the property name the API requires
|
||||
export type AdditionalApiProperties = Record<
|
||||
string,
|
||||
{
|
||||
required: boolean;
|
||||
helpLink?: string;
|
||||
value?: string;
|
||||
description?: string;
|
||||
};
|
||||
};
|
||||
}
|
||||
>;
|
||||
|
||||
export type Plugin = {
|
||||
export interface Plugin {
|
||||
name: Plugins;
|
||||
publisher: string;
|
||||
description: string;
|
||||
@@ -55,15 +56,10 @@ export type Plugin = {
|
||||
icon: string; // Can be imported as shown above or direct URL
|
||||
authData?: string; // token or encoded auth header value
|
||||
apiProperties?: AdditionalApiProperties;
|
||||
};
|
||||
|
||||
export interface PluginsState {
|
||||
MsGraph: Plugin;
|
||||
Jira: Plugin;
|
||||
GitHub: Plugin;
|
||||
Klarna: Plugin;
|
||||
}
|
||||
|
||||
export type PluginsState = Record<keyof typeof Plugins, Plugin>;
|
||||
|
||||
export const initialState: PluginsState = {
|
||||
MsGraph: {
|
||||
name: Plugins.MsGraph,
|
||||
@@ -80,7 +76,8 @@ export const initialState: PluginsState = {
|
||||
Jira: {
|
||||
name: Plugins.Jira,
|
||||
publisher: 'Atlassian',
|
||||
description: 'Authorize Copilot Chat to link with Jira and retrieve specific issues by providing the issue key.',
|
||||
description:
|
||||
'Authorize Copilot Chat to link with Jira and retrieve specific issues by providing the issue key.',
|
||||
enabled: false,
|
||||
authRequirements: {
|
||||
email: true,
|
||||
@@ -126,20 +123,19 @@ export const initialState: PluginsState = {
|
||||
Klarna: {
|
||||
name: Plugins.Klarna,
|
||||
publisher: 'Klarna',
|
||||
description:
|
||||
'Search and compare prices from thousands of online shops.',
|
||||
description: 'Search and compare prices from thousands of online shops.',
|
||||
enabled: false,
|
||||
authRequirements: { },
|
||||
authRequirements: {},
|
||||
icon: KlarnaIcon,
|
||||
headerTag: AuthHeaderTags.Klarna,
|
||||
},
|
||||
};
|
||||
|
||||
export type EnablePluginPayload = {
|
||||
export interface EnablePluginPayload {
|
||||
plugin: Plugins;
|
||||
username?: string;
|
||||
email?: string;
|
||||
password?: string;
|
||||
accessToken?: string;
|
||||
apiProperties?: AdditionalApiProperties;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -8,8 +8,8 @@ export const pluginsState = createSlice({
|
||||
initialState,
|
||||
reducers: {
|
||||
connectPlugin: (state: PluginsState, action: PayloadAction<EnablePluginPayload>) => {
|
||||
var plugin: Plugin;
|
||||
var authData = action.payload.accessToken;
|
||||
let plugin: Plugin;
|
||||
let authData = action.payload.accessToken;
|
||||
|
||||
switch (action.payload.plugin) {
|
||||
case Plugins.MsGraph:
|
||||
@@ -17,7 +17,7 @@ export const pluginsState = createSlice({
|
||||
break;
|
||||
case Plugins.Jira:
|
||||
plugin = state.Jira;
|
||||
authData = `${action.payload.email}:${action.payload.accessToken}`;
|
||||
authData = `${action.payload.email as string}:${action.payload.accessToken as string}`;
|
||||
break;
|
||||
case Plugins.GitHub:
|
||||
plugin = state.GitHub;
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,16 +0,0 @@
|
||||
{
|
||||
"devDependencies": {
|
||||
"@typescript-eslint/eslint-plugin": "^5.60.0",
|
||||
"@typescript-eslint/parser": "^5.60.0",
|
||||
"eslint": "^8.43.0",
|
||||
"eslint-config-react-app": "^7.0.1",
|
||||
"eslint-config-standard-with-typescript": "^35.0.0",
|
||||
"eslint-plugin-import": "^2.27.5",
|
||||
"eslint-plugin-n": "^16.0.1",
|
||||
"eslint-plugin-promise": "^6.1.1",
|
||||
"eslint-plugin-react": "^7.32.2",
|
||||
"eslint-plugin-react-hooks": "^4.6.0",
|
||||
"eslint-plugin-react-security": "^0.1.0",
|
||||
"typescript": "5.0.4"
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user