Skip to content

Commit

Permalink
feat(ResponseActions): Add ResponseActions to Messages
Browse files Browse the repository at this point in the history
Messages should have buttons for feedback and other actions. Users should be able to configure as many of these as they wish on a per message basis.
  • Loading branch information
rebeccaalpert committed Sep 27, 2024
1 parent a6cfe66 commit 7f419dd
Show file tree
Hide file tree
Showing 11 changed files with 260 additions and 13 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,19 @@ const initialMessages: MessageProps[] = [
{
role: 'bot',
content: markdown,
name: 'Bot'
name: 'Bot',
actions: {
// eslint-disable-next-line no-console
positive: { onClick: () => console.log('Good response') },
// eslint-disable-next-line no-console
negative: { onClick: () => console.log('Bad response') },
// eslint-disable-next-line no-console
copy: { onClick: () => console.log('Copy') },
// eslint-disable-next-line no-console
share: { onClick: () => console.log('Share') },
// eslint-disable-next-line no-console
listen: { onClick: () => console.log('Listen') }
}
}
];

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,19 @@ export const BasicDemo: React.FunctionComponent = () => {
{
role: 'bot',
content: 'Great, I can reference this attachment throughout our conversation.',
name: 'Bot'
name: 'Bot',
actions: {
// eslint-disable-next-line no-console
positive: { onClick: () => console.log('Good response') },
// eslint-disable-next-line no-console
negative: { onClick: () => console.log('Bad response') },
// eslint-disable-next-line no-console
copy: { onClick: () => console.log('Copy') },
// eslint-disable-next-line no-console
share: { onClick: () => console.log('Share') },
// eslint-disable-next-line no-console
listen: { onClick: () => console.log('Listen') }
}
}
];

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,19 @@ const messages: MessageProps[] = [
{
role: 'bot',
content: markdown,
name: 'Bot'
name: 'Bot',
actions: {
// eslint-disable-next-line no-console
positive: { onClick: () => console.log('Good response') },
// eslint-disable-next-line no-console
negative: { onClick: () => console.log('Bad response') },
// eslint-disable-next-line no-console
copy: { onClick: () => console.log('Copy') },
// eslint-disable-next-line no-console
share: { onClick: () => console.log('Share') },
// eslint-disable-next-line no-console
listen: { onClick: () => console.log('Listen') }
}
}
];

Expand Down
11 changes: 9 additions & 2 deletions packages/module/src/Message/Message.scss
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@
// Chatbot Message
// ============================================================================
.pf-chatbot__message {
--pf-t--chatbot-message--type--background--color--default: var(--pf-t--global--background--color--action--plain--hover); // needs to be updated to the correct semantic token when it exists
--pf-t--chatbot-message--type--background--color--default: var(
--pf-t--global--background--color--action--plain--hover
); // needs to be updated to the correct semantic token when it exists
--pf-t--chatbot-message--type--background--color--primary: var(--pf-t--global--color--brand--default);
--pf-t--chatbot-message--type--padding: var(--pf-t--global--spacer--sm);
--pf-t--chatbot-message--type--text--color--default: var(--pf-t--global--text--color--regular);
Expand All @@ -13,7 +15,7 @@
display: flex;
align-items: flex-start;
gap: var(--pf-t--global--spacer--lg);
padding-bottom: var(--pf-t--global--spacer--sm);
padding-bottom: var(--pf-t--global--spacer--2xl);

// Avatar
// --------------------------------------------------------------------------
Expand Down Expand Up @@ -80,6 +82,11 @@
gap: var(--pf-t--global--font--size--sm);
color: var(--pf-t--chatbot-message--type--text--color--default);
}

&-and-actions {
display: grid;
gap: var(--pf-t--global--spacer--sm);
}
}

@import './MessageLoading';
Expand Down
29 changes: 21 additions & 8 deletions packages/module/src/Message/Message.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import MessageLoading from './MessageLoading';
import CodeBlockMessage from './CodeBlockMessage/CodeBlockMessage';
import TextMessage from './TextMessage/TextMessage';
import FileDetailsLabel from '../FileDetailsLabel/FileDetailsLabel';
import ResponseActions, { ActionProps } from '../ResponseActions/ResponseActions';

export interface MessageProps {
/** Role of the user sending the message */
Expand All @@ -33,6 +34,14 @@ export interface MessageProps {
onAttachmentClick?: () => void;
/** Callback for when attachment label is closed */
onAttachmentClose?: (attachmentId: string) => void;
/** Props for message actions, such as feedback (positive or negative), copy button, share, and listen */
actions?: {
positive?: ActionProps;
negative?: ActionProps;
copy?: ActionProps;
share?: ActionProps;
listen?: ActionProps;
};
}

export const Message: React.FunctionComponent<MessageProps> = ({
Expand All @@ -45,7 +54,8 @@ export const Message: React.FunctionComponent<MessageProps> = ({
attachmentId,
attachmentName,
onAttachmentClick,
onAttachmentClose
onAttachmentClose,
actions
}: MessageProps) => {
// Configure default values

Expand Down Expand Up @@ -79,13 +89,16 @@ export const Message: React.FunctionComponent<MessageProps> = ({
<Timestamp>{timestamp}</Timestamp>
</div>
<div className="pf-chatbot__message-response">
{isLoading ? (
<MessageLoading />
) : (
<Markdown components={{ p: TextMessage, code: CodeBlockMessage }} remarkPlugins={[remarkGfm]}>
{content}
</Markdown>
)}
<div className="pf-chatbot__message-and-actions">
{isLoading ? (
<MessageLoading />
) : (
<Markdown components={{ p: TextMessage, code: CodeBlockMessage }} remarkPlugins={[remarkGfm]}>
{content}
</Markdown>
)}
{!isLoading && actions && <ResponseActions actions={actions} />}
</div>
{attachmentName && (
<div className="pf-chatbot__message-attachment">
<FileDetailsLabel fileName={attachmentName} onClick={onAttachmentClick} onClose={onClose} />
Expand Down
57 changes: 57 additions & 0 deletions packages/module/src/ResponseActions/ResponseActionButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import React from 'react';
import { Button, Icon, Tooltip, TooltipProps } from '@patternfly/react-core';

export interface ResponseActionButtonProps {
/** Aria-label for the button */
ariaLabel?: string;
/** Icon for the button */
icon: React.ReactNode;
/** On-click handler for the button */
onClick?: ((event: MouseEvent | React.MouseEvent<Element, MouseEvent> | KeyboardEvent) => void) | undefined;
/** Class name for the button */
className?: string;
/** Props to control if the attach button should be disabled */
isDisabled?: boolean;
/** Content shown in the tooltip */
tooltipContent?: string;
/** Props to control the PF Tooltip component */
tooltipProps?: TooltipProps;
}

export const ResponseActionButton: React.FunctionComponent<ResponseActionButtonProps> = ({
ariaLabel,
className,
icon,
isDisabled,
onClick,
tooltipContent,
tooltipProps
}) => (
<Tooltip
id={`pf-chatbot__tooltip-response-action-${tooltipContent}`}
content={tooltipContent}
position="bottom"
entryDelay={tooltipProps?.entryDelay || 0}
exitDelay={tooltipProps?.exitDelay || 0}
distance={tooltipProps?.distance || 8}
animationDuration={tooltipProps?.animationDuration || 0}
{...tooltipProps}
>
<Button
variant="plain"
className={`pf-chatbot__button--response-action ${className ?? ''}`}
aria-describedby={`pf-chatbot__tooltip-response-action-${tooltipContent}`}
aria-label={ariaLabel}
icon={
<Icon isInline size="lg">
{icon}
</Icon>
}
isDisabled={isDisabled}
onClick={onClick}
size="sm"
></Button>
</Tooltip>
);

export default ResponseActionButton;
26 changes: 26 additions & 0 deletions packages/module/src/ResponseActions/ResponseActions.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
.pf-chatbot__response-actions {
display: grid;
gap: var(--pf-t--global--spacer--xs);
grid-template-columns: repeat(auto-fit, minmax(0, max-content));

.pf-v6-c-button {
border-radius: var(--pf-t--global--border--radius--pill);
width: 2.3125rem;
height: 2.3125rem;
display: flex;
align-items: center;
justify-content: center;

.pf-v6-c-button__icon {
color: var(--pf-t--global--icon--color--subtle);
}

// Interactive states
&:hover,
&:focus {
.pf-v6-c-button__icon {
color: var(--pf-t--global--icon--color--subtle);
}
}
}
}
101 changes: 101 additions & 0 deletions packages/module/src/ResponseActions/ResponseActions.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
import React from 'react';
import {
CopyIcon,
ExternalLinkAltIcon,
VolumeUpIcon,
OutlinedThumbsUpIcon,
OutlinedThumbsDownIcon
} from '@patternfly/react-icons';
import ResponseActionButton from './ResponseActionButton';
import { TooltipProps } from '@patternfly/react-core';

export interface ActionProps {
/** Aria-label for the button */
ariaLabel?: string;
/** On-click handler for the button */
onClick?: ((event: MouseEvent | React.MouseEvent<Element, MouseEvent> | KeyboardEvent) => void) | undefined;
/** Class name for the button */
className?: string;
/** Props to control if the attach button should be disabled */
isDisabled?: boolean;
/** Content shown in the tooltip */
tooltipContent?: string;
/** Props to control the PF Tooltip component */
tooltipProps?: TooltipProps;
}

export interface ResponseActionProps {
/** Props for message actions, such as feedback (positive or negative), copy button, share, and listen */
actions: {
positive?: ActionProps;
negative?: ActionProps;
copy?: ActionProps;
share?: ActionProps;
listen?: ActionProps;
};
}

export const ResponseActions: React.FunctionComponent<ResponseActionProps> = ({ actions }) => {
const { positive, negative, copy, share, listen } = actions;
return (
<div className="pf-chatbot__response-actions">
{positive && (
<ResponseActionButton
ariaLabel={positive.ariaLabel ?? 'Good response'}
onClick={positive.onClick}
className={positive.className}
isDisabled={positive.isDisabled}
tooltipContent={positive.tooltipContent ?? 'Good response'}
tooltipProps={positive.tooltipProps}
icon={<OutlinedThumbsUpIcon />}
></ResponseActionButton>
)}
{negative && (
<ResponseActionButton
ariaLabel={negative.ariaLabel ?? 'Bad response'}
onClick={negative.onClick}
className={negative.className}
isDisabled={negative.isDisabled}
tooltipContent={negative.tooltipContent ?? 'Bad response'}
tooltipProps={negative.tooltipProps}
icon={<OutlinedThumbsDownIcon />}
></ResponseActionButton>
)}
{copy && (
<ResponseActionButton
ariaLabel={copy.ariaLabel ?? 'Copy'}
onClick={copy.onClick}
className={copy.className}
isDisabled={copy.isDisabled}
tooltipContent={copy.tooltipContent ?? 'Copy'}
tooltipProps={copy.tooltipProps}
icon={<CopyIcon />}
></ResponseActionButton>
)}
{share && (
<ResponseActionButton
ariaLabel={share.ariaLabel ?? 'Share'}
onClick={share.onClick}
className={share.className}
isDisabled={share.isDisabled}
tooltipContent={share.tooltipContent ?? 'Share'}
tooltipProps={share.tooltipProps}
icon={<ExternalLinkAltIcon />}
></ResponseActionButton>
)}
{listen && (
<ResponseActionButton
ariaLabel={listen.ariaLabel ?? 'Listen'}
onClick={listen.onClick}
className={listen.className}
isDisabled={listen.isDisabled}
tooltipContent={listen.tooltipContent ?? 'Listen'}
tooltipProps={listen.tooltipProps}
icon={<VolumeUpIcon />}
></ResponseActionButton>
)}
</div>
);
};

export default ResponseActions;
3 changes: 3 additions & 0 deletions packages/module/src/ResponseActions/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export { default } from './ResponseActions';

export * from './ResponseActions';
3 changes: 3 additions & 0 deletions packages/module/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,9 @@ export * from './MessageBox';
export { default as PreviewAttachment } from './PreviewAttachment';
export * from './PreviewAttachment';

export { default as ResponseActions } from './ResponseActions';
export * from './ResponseActions';

export { default as SourceDetailsMenuItem } from './SourceDetailsMenuItem';
export * from './SourceDetailsMenuItem';

Expand Down
1 change: 1 addition & 0 deletions packages/module/src/main.scss
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
@import './Message/Message';
@import './ChatbotPopover/ChatbotPopover';
@import './SourceDetailsMenuItem/SourceDetailsMenuItem';
@import './ResponseActions/ResponseActions';

:where(:root) {
// ============================================================================
Expand Down

0 comments on commit 7f419dd

Please sign in to comment.