Skip to content

Commit

Permalink
Chat bubbles
Browse files Browse the repository at this point in the history
  • Loading branch information
radekkaluzik committed Mar 26, 2024
1 parent 8aa6713 commit a533ced
Show file tree
Hide file tree
Showing 19 changed files with 1,959 additions and 108 deletions.
1,728 changes: 1,620 additions & 108 deletions package-lock.json

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions packages/module/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
"@patternfly/react-core": "^5.1.2",
"@patternfly/react-icons": "^5.1.2",
"react-jss": "^10.10.0",
"react-markdown": "^9.0.1",
"clsx": "^2.1.0"
},
"peerDependencies": {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import React from 'react';
import AssistantMessageEntry from '@patternfly/virtual-assistant/dist/dynamic/AssistantMessageEntry';
// import { From } from '../../../../../../src/types/Message';

export const AssistantMessage: React.FunctionComponent = () => (
<AssistantMessageEntry
message={ { content: "Whatever", options: [], messageId: "1", from: From.ASSISTANT, isLoading: false } }
preview={true}
blockInput={false}
ask={(option: { title: "This is Title", payload: "This is Payload" }) => null}
/>
);
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import React from 'react';
import AssistantMessageEntry from '@patternfly/virtual-assistant/dist/dynamic/AssistantMessageEntry';
// import { From } from '../../../../../../src/types/Message';

export const AssistantMessageWithFolowup: React.FunctionComponent = () => (
<AssistantMessageEntry
message={ { content: "Whatever", options: [ { title: "This is Title", payload: "This is Payload" }, { title: "This is Title 2", payload: "This is Payload 2" } ], messageId: "1", from: From.ASSISTANT, isLoading: false } }
preview={true}
blockInput={false}
ask={(option: { title: "This is Title", payload: "This is Payload" }) => null}
/>
);
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import React from 'react';
import UserMessageEntry from '@patternfly/virtual-assistant/dist/dynamic/UserMessageEntry';
import { From } from '@patternfly/virtual-assistant/dist/dynamic/types';

export const UserMessage: React.FunctionComponent = () => (
<UserMessageEntry
message={ { content: "Whatever", from: From.USER } }
/>
);
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@ sourceLink: https://github.com/patternfly/virtual-assistant/blob/main/packages/m
import VirtualAssistant from '@patternfly/virtual-assistant/dist/dynamic/VirtualAssistant';
import VirtualAssistantAction from '@patternfly/virtual-assistant/dist/dynamic/VirtualAssistantAction';
import { AngleDownIcon } from '@patternfly/react-icons';
import AssistantMessageEntry from '@patternfly/virtual-assistant/dist/dynamic/AssistantMessageEntry';
import UserMessageEntry from '@patternfly/virtual-assistant/dist/dynamic/UserMessageEntry';


The **virtual assistant** component renders body of the virtual assistant window.

Expand Down Expand Up @@ -61,3 +64,11 @@ Custom actions can be added to the assistant body using the `actions` property.
```js file="./VirtualAssistantWithActions.tsx"

```

### User Message (with configurable icon)

Random text blah blah blah.

```js file="./UserMessage.tsx"

```
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import { MessageProps } from '../types/MessageProps';
import React, { FunctionComponent } from 'react';
import { Icon, Label, Split, SplitItem, TextContent } from '@patternfly/react-core';
import ChatbotIcon from '../ChatbotIcon';

import { AssistantMessage, MessageOption } from '../types/Message';
import { TextEntry } from '../TextEntry';

interface AssistantMessageProps extends MessageProps<AssistantMessage> {
ask: (option: MessageOption) => unknown;
preview: boolean;
blockInput: boolean;
}

const OPTION_COLORS = [ 'red' ] as const;

export const AssistantMessageEntry: FunctionComponent<AssistantMessageProps> = ({ message, ask, preview, blockInput }) => {
if (!message.content && !message.options) {
return null;
}

return (
<div className="pf-v5-u-mb-md">
{message.content && (
<Split className="astro-chatbot">
<SplitItem>
<Icon size="lg" className="pf-v5-u-mr-sm pf-v5-u-pt-md">
<ChatbotIcon />
</Icon>
</SplitItem>
<SplitItem className="bubble pf-u-background-color-200">
<TextContent className="pf-v5-u-font-size-sm">
<TextEntry content={message.content} preview={preview} />
</TextContent>
</SplitItem>
</Split>
)}

{message.options && (
<Split>
<SplitItem className="astro-chatbot pf-v5-u-ml-xl pf-v5-u-mt-md">
{message.options.map((option, index) => (
<Label
className="pf-u-m-xs"
key={option.title}
color={OPTION_COLORS[index % OPTION_COLORS.length]}
render={({ className, content, componentRef }) => (
<a
className={`${className} ${blockInput ? 'astro-option-disabled' : ''}`}
ref={componentRef}
onClick={() => blockInput || ask(option)}
>
{content}
</a>
)}
>
{option.title}
</Label>
))}
</SplitItem>
</Split>
)}
</div>
);
};

export default AssistantMessageEntry;
2 changes: 2 additions & 0 deletions packages/module/src/AssistantMessageEntry/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export { default } from './AssistantMessageEntry';
export * from './AssistantMessageEntry';
35 changes: 35 additions & 0 deletions packages/module/src/ChatbotIcon/ChatbotIcon.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import React from 'react';

const ChatBotIcon = () => (
<svg width="34" height="34" viewBox="0 0 38 38">
<defs>
<style>{`.st0{fill:#A30000;}`}</style>
</defs>
<path
className="st0"
d="M22.2,8.4h-0.6l0.8-2.9l-0.9-0.3l-0.9,3.1H9.7L8.3,5.3L7.4,5.7l1.2,2.7H7.9c-1.9,0-3.4,1.5-3.4,3.4v12.8
c0,1.9,1.5,3.4,3.4,3.4h14.3c1.9,0,3.4-1.5,3.4-3.4V11.8C25.5,9.9,24,8.4,22.2,8.4z M24,23.6L24,23.6c0,1.6-1.3,2.9-2.9,2.9H8.9
c-1.6,0-2.9-1.3-2.9-2.9V12.7c0-1.6,1.3-2.9,2.9-2.9h12.2c1.6,0,2.9,1.3,2.9,2.9V23.6z"
/>
<circle className="st0" cx="10.5" cy="15.9" r="1.9" />
<circle className="st0" cx="19.5" cy="15.9" r="1.9" />
<g>
<g>
<path
className="st0"
d="M13.9,24.3c-1.9,0-2.1-0.1-2.2-0.2c-0.4-0.2-0.9-0.6-1.6-1.3c-0.3-0.2-0.8-0.7-0.8-0.7s0-0.7,0.2-0.9
c0.2-0.2,0.8-0.3,0.8-0.3s0.2,0.2,0.4,0.3c0.1,0.1,0.3,0.2,0.4,0.4c0.4,0.4,1,0.9,1.3,1c0.7,0.1,4.5,0,7.9-0.1c0,0,0.4,0,0.4,0.8
c0,0.5-0.3,0.7-0.3,0.7c-1.7,0-3,0.1-3.9,0.1C15.3,24.3,14.5,24.3,13.9,24.3z M12.2,22.7L12.2,22.7z"
/>
</g>
</g>
<path className="st0" d="M2.3,14.4H3v9H2.3c-1.2,0-2.3-1-2.3-2.3v-4.5C0,15.4,1,14.4,2.3,14.4z" />
<path className="st0" d="M27.8,14.4c1.2,0,2.3,1,2.3,2.3v4.5c0,1.2-1,2.3-2.3,2.3H27v-9L27.8,14.4L27.8,14.4z" />
<circle className="st0" cx="7.6" cy="4.5" r="1.6" />
<circle className="st0" cx="22.4" cy="4.5" r="1.6" />
<g>
<polygon className="st0" points="18.7,5 16.1,5 14.2,2.8 13,5 11,5 11,4 12.4,4 14,1.1 16.5,4 18.7,4 " />
</g>
</svg>
);
export default ChatBotIcon;
2 changes: 2 additions & 0 deletions packages/module/src/ChatbotIcon/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export { default } from './ChatbotIcon';
export * from './ChatbotIcon';
33 changes: 33 additions & 0 deletions packages/module/src/TextEntry/TextEntry.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import React, { FunctionComponent } from 'react';

import ReactMarkdown from 'react-markdown';

interface TextEntryProps {
content: string;
preview: boolean;
}

export const TextEntry: FunctionComponent<TextEntryProps> = ({ content, preview }) => (
<ReactMarkdown
components={{
a: ({ ...props }) => {
let href = props.href;
if (href && href.startsWith('/')) {
if (preview) {
href = `/preview${href}`;
}
href = `${window.location.origin}${href}`;
}
return (
<a {...props} href={href} target="_blank" rel="noopener noreferrer">
{props.children}
</a>
);
},
}}
>
{content}
</ReactMarkdown>
);

export default TextEntry;
2 changes: 2 additions & 0 deletions packages/module/src/TextEntry/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export { default } from './TextEntry';
export * from './TextEntry';
22 changes: 22 additions & 0 deletions packages/module/src/UserMessageEntry/UserMessageEntry.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import React, { FunctionComponent } from 'react';
import { Icon, Split, SplitItem, TextContent } from '@patternfly/react-core';
import OutlinedUserIcon from '@patternfly/react-icons/dist/dynamic/icons/outlined-user-icon';
import { MessageProps } from '../types/MessageProps';
import { UserMessage } from '../types/Message';

export const UserMessageEntry: FunctionComponent<MessageProps<UserMessage>> = ({ message }) => (
<React.Fragment>
<Split className="astro-user pf-v5-u-mb-md pf-v5-u-align-items-flex-start pf-v5-u-justify-content-flex-end">
<SplitItem className="bubble bubble-user">
<TextContent className="pf-v5-u-color-300 pf-v5-u-font-size-sm">{message.content}</TextContent>
</SplitItem>
<SplitItem>
<Icon size="lg" className="pf-v5-u-ml-sm pf-v5-u-pt-xs">
<OutlinedUserIcon />
</Icon>
</SplitItem>
</Split>
</React.Fragment>
);

export default UserMessageEntry;
2 changes: 2 additions & 0 deletions packages/module/src/UserMessageEntry/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export { default } from './UserMessageEntry';
export * from './UserMessageEntry';
15 changes: 15 additions & 0 deletions packages/module/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,20 @@
// this file is autogenerated by generate-index.js, modifying it manually will have no effect

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

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

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

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

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

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

Expand Down
47 changes: 47 additions & 0 deletions packages/module/src/types/Command.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
export enum CommandType {
REDIRECT = 'redirect',
FINISH_CONVERSATION = 'core_finish_conversation',
TOUR_START = 'tour_start',
FEEDBACK_MODAL = 'feedback_modal',
FEEDBACK = 'feedback',
THUMBS = 'thumbs',
}

interface BaseCommand {
type: unknown;
params: unknown;
}

export interface FinishConversationCommand extends BaseCommand {
type: CommandType.FINISH_CONVERSATION;
}

export interface RedirectCommand extends BaseCommand {
type: CommandType.REDIRECT;
params: {
url: string;
};
}

export interface TourStartCommand extends BaseCommand {
type: CommandType.TOUR_START;
}

export interface FeedbackModalCommand extends BaseCommand {
type: CommandType.FEEDBACK_MODAL;
}

export interface FeedbackCommand extends BaseCommand {
type: CommandType.FEEDBACK;
params: {
summary: string;
description: string;
labels: string[];
};
}

export interface ThumbsCommand extends BaseCommand {
type: CommandType.THUMBS;
}

export type Command = FinishConversationCommand | RedirectCommand | TourStartCommand | FeedbackCommand | FeedbackModalCommand | ThumbsCommand;
62 changes: 62 additions & 0 deletions packages/module/src/types/Message.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import { Command } from './Command';

export enum From {
ASSISTANT = 'assistant',
USER = 'user',
FEEDBACK = 'feedback',
SYSTEM = 'system',
INTERFACE = 'interface',
THUMBS = 'thumbs',
}

// Base
interface BaseMessage {
from: unknown;
content: string;
}

export interface MessageOption {
title?: string;
payload: string;
}

// Brand Assistant
export interface AssistantMessage extends BaseMessage {
messageId: string;
from: From.ASSISTANT;
options?: MessageOption[];
command?: Command;
isLoading: boolean;
}

// Brand User
export interface UserMessage extends BaseMessage {
from: From.USER;
}

// Brand feedback
export interface FeedbackMessage extends BaseMessage {
messageId: string;
from: From.FEEDBACK;
isLoading: boolean;
}

// System messages
export interface SystemMessage extends BaseMessage {
from: From.SYSTEM;
type: string;
additionalContent?: string[];
}

// Banners
export interface Banner extends BaseMessage {
from: From.INTERFACE;
type: string;
additionalContent?: string[];
}

export interface Thumbs {
from: From.THUMBS;
}

export type Message = AssistantMessage | UserMessage | FeedbackMessage | SystemMessage | Banner | Thumbs;
3 changes: 3 additions & 0 deletions packages/module/src/types/MessageProps.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export interface MessageProps<Message> {
message: Message;
}
2 changes: 2 additions & 0 deletions packages/module/src/types/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export default undefined
export * from './Message'

0 comments on commit a533ced

Please sign in to comment.