Skip to content

Commit

Permalink
📝 RLFH UX Skeleton (#31)
Browse files Browse the repository at this point in the history
### Motivation and Context

<!-- Thank you for your contribution to the copilot-chat 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.
-->

Added RLFH UI to demonstrate users can easily add this capability to
their chatbot if they wanted.

### Description

<!-- Describe your changes, the overall approach, the underlying design.
These notes will help understanding how your code works. Thanks! -->

> This feature is for demonstration purposes only. We don't actually
hook up to the server to store the human feedback or send it to the
model.

Details:

- RLFH actions will only show on the most recent chat message in which
the author is `bot`.
- If user takes action, icon will be rendered to reflect action.
- Data is only stored in frontend state. If app refreshes, all RLFH
across all chats will be reset.

Actions on chat message:

![image](https:/microsoft/chat-copilot/assets/125500434/1b730018-ccf3-4356-a173-9c7d50c0cec8)

Once user takes actions:

![image](https:/microsoft/chat-copilot/assets/125500434/0d691a1a-970a-4847-98cb-c6f05e23941f)

Future work 
- Add ability to turn of RLHF in settings dialog + add help link to docs
there

### Contribution Checklist

<!-- Before submitting this PR, please make sure: -->

- [x] The code builds clean without any errors or warnings
- [x] The PR follows the [Contribution
Guidelines](https:/microsoft/copilot-chat/blob/main/CONTRIBUTING.md)
and the [pre-submission formatting
script](https:/microsoft/copilot-chat/blob/main/CONTRIBUTING.md#development-scripts)
raises no violations
~~- [ ] All unit tests pass, and I have added new tests where possible~~
- [x] I didn't break anyone 😄
  • Loading branch information
teresaqhoang authored Jul 25, 2023
1 parent 5f84e0f commit 46f48cc
Show file tree
Hide file tree
Showing 7 changed files with 156 additions and 6 deletions.
17 changes: 15 additions & 2 deletions webapp/src/components/chat/chat-history/ChatHistoryItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,19 @@

import { Persona, Text, makeStyles, mergeClasses, shorthands, tokens } from '@fluentui/react-components';
import React from 'react';
import { AuthorRoles, ChatMessageType, IChatMessage } from '../../../libs/models/ChatMessage';
import { AuthorRoles, ChatMessageType, IChatMessage, UserFeedback } from '../../../libs/models/ChatMessage';
import { GetResponseOptions, useChat } from '../../../libs/useChat';
import { useAppSelector } from '../../../redux/app/hooks';
import { RootState } from '../../../redux/app/store';
import { Breakpoints } from '../../../styles';
import { Breakpoints, customTokens } from '../../../styles';
import { timestampToDateString } from '../../utils/TextUtils';
import { PlanViewer } from '../plan-viewer/PlanViewer';
import { PromptDetails } from '../prompt-details/PromptDetails';
import { ChatHistoryDocumentContent } from './ChatHistoryDocumentContent';
import { ChatHistoryTextContent } from './ChatHistoryTextContent';
import * as utils from './../../utils/TextUtils';
import { ThumbLike16Filled, ThumbDislike24Filled } from '@fluentui/react-icons';
import { UserFeedbackActions } from './UserFeedbackActions';

const useClasses = makeStyles({
root: {
Expand All @@ -23,6 +25,7 @@ const useClasses = makeStyles({
...Breakpoints.small({
maxWidth: '100%',
}),
...shorthands.gap(customTokens.spacingHorizontalXS),
},
debug: {
position: 'absolute',
Expand Down Expand Up @@ -91,6 +94,13 @@ export const ChatHistoryItem: React.FC<ChatHistoryItemProps> = ({ message, getRe
content = <ChatHistoryTextContent message={message} />;
}

// TODO: hookup to backend
// Currently for demonstration purposes only, no feedback is actually sent to kernel / model
const showShowRLHFMessage =
message.userFeedback === UserFeedback.Requested &&
messageIndex === conversations[selectedId].messages.length - 1 &&
message.userId === 'bot';

return (
<div
className={isMe ? mergeClasses(classes.root, classes.alignEnd) : classes.root}
Expand All @@ -107,7 +117,10 @@ export const ChatHistoryItem: React.FC<ChatHistoryItemProps> = ({ message, getRe
{isBot && <PromptDetails message={message} />}
</div>
{content}
{showShowRLHFMessage && <UserFeedbackActions messageIndex={messageIndex} />}
</div>
{message.userFeedback === UserFeedback.Positive && <ThumbLike16Filled color="gray" />}
{message.userFeedback === UserFeedback.Negative && <ThumbDislike24Filled color="gray" />}
</div>
);
};
63 changes: 63 additions & 0 deletions webapp/src/components/chat/chat-history/UserFeedbackActions.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import { Button, Text, Tooltip, makeStyles } from '@fluentui/react-components';
import { useCallback } from 'react';
import { UserFeedback } from '../../../libs/models/ChatMessage';
import { useAppDispatch, useAppSelector } from '../../../redux/app/hooks';
import { RootState } from '../../../redux/app/store';
import { setUserFeedback } from '../../../redux/features/conversations/conversationsSlice';
import { ThumbDislike16, ThumbLike16 } from '../../shared/BundledIcons';

const useClasses = makeStyles({
root: {
display: 'flex',
'place-content': 'flex-end',
alignItems: 'center',
},
});

interface IUserFeedbackProps {
messageIndex: number;
}

export const UserFeedbackActions: React.FC<IUserFeedbackProps> = ({ messageIndex }) => {
const classes = useClasses();

const dispatch = useAppDispatch();
const { selectedId } = useAppSelector((state: RootState) => state.conversations);

const onUserFeedbackProvided = useCallback(
(positive: boolean) => {
dispatch(
setUserFeedback({
userFeedback: positive ? UserFeedback.Positive : UserFeedback.Negative,
messageIndex,
chatId: selectedId,
}),
);
},
[dispatch, messageIndex, selectedId],
);

return (
<div className={classes.root}>
<Text color="gray" size={200}>
AI-generated content may be incorrect
</Text>
<Tooltip content={'Like bot message'} relationship="label">
<Button
icon={<ThumbLike16 />}
appearance="transparent"
aria-label="Edit"
onClick={() => onUserFeedbackProvided(true)}
/>
</Tooltip>
<Tooltip content={'Dislike bot message'} relationship="label">
<Button
icon={<ThumbDislike16 />}
appearance="transparent"
aria-label="Edit"
onClick={() => onUserFeedbackProvided(false)}
/>
</Tooltip>
</div>
);
};
46 changes: 46 additions & 0 deletions webapp/src/components/shared/BundledIcons.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import {
Add20Filled,
Add20Regular,
AppsAddIn24Filled,
AppsAddIn24Regular,
ArrowDownload16Filled,
ArrowDownload16Regular,
BotAdd20Filled,
BotAdd20Regular,
Checkmark20Filled,
Checkmark20Regular,
Delete16Filled,
Delete16Regular,
Dismiss16Filled,
Dismiss16Regular,
Dismiss20Filled,
Dismiss20Regular,
EditFilled,
EditRegular,
Filter20Filled,
Filter20Regular,
Info16Filled,
Info16Regular,
Share20Filled,
Share20Regular,
ThumbDislike16Filled,
ThumbDislike16Regular,
ThumbLike16Filled,
ThumbLike16Regular,
bundleIcon,
} from '@fluentui/react-icons';

export const Add20 = bundleIcon(Add20Filled, Add20Regular);
export const AppsAddIn24 = bundleIcon(AppsAddIn24Filled, AppsAddIn24Regular);
export const BotAdd20 = bundleIcon(BotAdd20Filled, BotAdd20Regular);
export const Checkmark20 = bundleIcon(Checkmark20Filled, Checkmark20Regular);
export const Delete16 = bundleIcon(Delete16Filled, Delete16Regular);
export const Dismiss16 = bundleIcon(Dismiss16Filled, Dismiss16Regular);
export const Dismiss20 = bundleIcon(Dismiss20Filled, Dismiss20Regular);
export const Filter20 = bundleIcon(Filter20Filled, Filter20Regular);
export const Edit = bundleIcon(EditFilled, EditRegular);
export const ArrowDownload16 = bundleIcon(ArrowDownload16Filled, ArrowDownload16Regular);
export const Share20 = bundleIcon(Share20Filled, Share20Regular);
export const Info16 = bundleIcon(Info16Filled, Info16Regular);
export const ThumbLike16 = bundleIcon(ThumbLike16Filled, ThumbLike16Regular);
export const ThumbDislike16 = bundleIcon(ThumbDislike16Filled, ThumbDislike16Regular);
12 changes: 12 additions & 0 deletions webapp/src/libs/models/ChatMessage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,16 @@ export enum ChatMessageType {
Document,
}

/**
* States for RLHF
*/
export enum UserFeedback {
Unknown,
Requested,
Positive,
Negative,
}

export interface IChatMessage {
type: ChatMessageType;
timestamp: number;
Expand All @@ -41,4 +51,6 @@ export interface IChatMessage {
authorRole: AuthorRoles;
debug?: string;
state?: PlanState;
// TODO: Persistent RLHF, view only right now
userFeedback?: UserFeedback;
}
2 changes: 1 addition & 1 deletion webapp/src/redux/features/app/appSlice.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ const initialState: AppState = {
alerts: [
{
message:
'Copilot chat 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 Copilot chat can collect and retain your chat history for service improvement.',
'By using Chat Copilot, you agree to protect sensitive data, not store it in chat, and allow chat history collection for service improvements. This tool is for internal use only.',
type: AlertType.Info,
},
],
Expand Down
18 changes: 16 additions & 2 deletions webapp/src/redux/features/conversations/conversationsSlice.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// Copyright (c) Microsoft. All rights reserved.

import { createSlice, PayloadAction, Slice } from '@reduxjs/toolkit';
import { IChatMessage } from '../../../libs/models/ChatMessage';
import { ChatMessageType, IChatMessage, UserFeedback } from '../../../libs/models/ChatMessage';
import { IChatUser } from '../../../libs/models/ChatUser';
import { PlanState } from '../../../libs/models/Plan';
import { ChatState } from './ChatState';
Expand Down Expand Up @@ -112,6 +112,15 @@ export const conversationsSlice: Slice<ConversationsState> = createSlice({
const conversation = state.conversations[chatId];
conversation.botResponseStatus = status;
},
setUserFeedback: (
state: ConversationsState,
action: PayloadAction<{ userFeedback: UserFeedback; messageIndex: number; chatId?: string }>,
) => {
const { userFeedback, messageIndex, chatId } = action.payload;
const id = chatId ?? state.selectedId;
state.conversations[id].messages[messageIndex].userFeedback = userFeedback;
frontLoadChat(state, id);
},
},
});

Expand All @@ -128,6 +137,7 @@ export const {
updateUserIsTypingFromServer,
updateBotResponseStatusFromServer,
setUsersLoaded,
setUserFeedback,
} = conversationsSlice.actions;

export default conversationsSlice.reducer;
Expand All @@ -139,7 +149,11 @@ const frontLoadChat = (state: ConversationsState, id: string) => {
};

const updateConversation = (state: ConversationsState, chatId: string, message: IChatMessage) => {
state.conversations[chatId].messages.push(message);
const requestUserFeedback = message.userId === 'bot' && message.type === ChatMessageType.Message;
state.conversations[chatId].messages.push({
...message,
userFeedback: requestUserFeedback ? UserFeedback.Requested : undefined,
});
frontLoadChat(state, chatId);
};

Expand Down
4 changes: 3 additions & 1 deletion webapp/src/styles.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { BrandVariants, GriffelStyle, createLightTheme, tokens } from '@fluentui/react-components';
import { BrandVariants, GriffelStyle, createLightTheme, themeToTokensObject, tokens } from '@fluentui/react-components';

const semanticKernelBrandRamp: BrandVariants = {
10: '#060103',
Expand All @@ -21,6 +21,8 @@ const semanticKernelBrandRamp: BrandVariants = {

export const semanticKernelLightTheme = createLightTheme(semanticKernelBrandRamp);

export const customTokens = themeToTokensObject(semanticKernelLightTheme);

export const Breakpoints = {
small: (style: GriffelStyle): Record<string, GriffelStyle> => {
return { '@media (max-width: 744px)': style };
Expand Down

0 comments on commit 46f48cc

Please sign in to comment.