Skip to content

Commit

Permalink
Add Actions menu to ChatItems
Browse files Browse the repository at this point in the history
  • Loading branch information
CoenWarmer committed Jul 25, 2023
1 parent d01bbef commit 889f1a8
Show file tree
Hide file tree
Showing 6 changed files with 242 additions and 70 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -5,89 +5,212 @@
* 2.0.
*/

import React from 'react';
import React, { useState } from 'react';
import { noop } from 'lodash';
import { i18n } from '@kbn/i18n';
import { css } from '@emotion/react';
import { EuiText, EuiComment } from '@elastic/eui';
import {
EuiButtonIcon,
EuiComment,
EuiContextMenuItem,
EuiContextMenuPanel,
EuiPopover,
} from '@elastic/eui';
import type { AuthenticatedUser } from '@kbn/security-plugin/common';
import { MessageRole, Message } from '../../../common/types';
import { ChatItemAvatar } from './chat_item_avatar';
import { ChatItemTitle } from './chat_item_title';
import { MessagePanel } from '../message_panel/message_panel';
import { FeedbackButtons, Feedback } from '../feedback_buttons';
import { MessageText } from '../message_panel/message_text';
import { useKibana } from '../../hooks/use_kibana';

const roleMap = {
[MessageRole.User]: i18n.translate(
'xpack.observabilityAiAssistant.chatTimeline.messages.userLabel',
{ defaultMessage: 'You' }
),
[MessageRole.System]: i18n.translate(
'xpack.observabilityAiAssistant.chatTimeline.messages.systemLabel',
{ defaultMessage: 'System' }
),
[MessageRole.Assistant]: i18n.translate(
'xpack.observabilityAiAssistant.chatTimeline.messages.assistantLabel',
{ defaultMessage: 'Elastic Assistant' }
),
[MessageRole.Function]: i18n.translate(
'xpack.observabilityAiAssistant.chatTimeline.messages.functionLabel',
{ defaultMessage: 'Elastic Assistant' }
),
[MessageRole.Event]: i18n.translate(
'xpack.observabilityAiAssistant.chatTimeline.messages.functionLabel',
{ defaultMessage: 'Elastic Assistant' }
),
[MessageRole.Elastic]: i18n.translate(
'xpack.observabilityAiAssistant.chatTimeline.messages.functionLabel',
{ defaultMessage: 'Elastic Assistant' }
),
};
export interface ChatItemAction {
id: string;
label: string;
icon?: string;
handler: () => void;
}

export interface ChatItemProps {
currentUser: AuthenticatedUser | undefined;
dateFormat: string;
index: number;
isLoading: boolean;
message: Message;
onEditMessage?: (id: string) => void;
onFeedbackClick: (feedback: Feedback) => void;
onRegenerateMessage?: (id: string) => void;
}

export function ChatItem({
currentUser,
dateFormat,
index,
isLoading,
message,
onFeedbackClick,
onEditMessage,
onRegenerateMessage,
}: ChatItemProps) {
const {
notifications: { toasts },
} = useKibana().services;
const [isActionsPopoverOpen, setIsActionsPopover] = useState(false);

const handleClickActions = () => {
setIsActionsPopover(!isActionsPopoverOpen);
};

const actionsMap: Record<MessageRole, ChatItemAction[]> = {
[MessageRole.User]: [
{
id: 'edit',
label: i18n.translate('xpack.observabilityAiAssistant.chatTimeline.actions.editMessage', {
defaultMessage: 'Edit message',
}),
handler: () => {
onEditMessage?.(message['@timestamp']);
setIsActionsPopover(false);
},
},
],
[MessageRole.Function]: [
{
id: 'edit',
label: i18n.translate('xpack.observabilityAiAssistant.chatTimeline.actions.editFunction', {
defaultMessage: 'Edit function',
}),
handler: () => {
onEditMessage?.(message['@timestamp']);
setIsActionsPopover(false);
},
},
],
[MessageRole.Assistant]: [
{
id: 'copy',
label: i18n.translate('xpack.observabilityAiAssistant.chatTimeline.actions.copyMessage', {
defaultMessage: 'Copy message',
}),
handler: message.message.content
? async () => {
try {
await navigator.clipboard.writeText(message.message.content || '');
toasts.addSuccess(
i18n.translate(
'xpack.observabilityAiAssistant.chatTimeline.actions.copyMessageSuccess',
{
defaultMessage: 'Copied to clipboard',
}
)
);
setIsActionsPopover(false);
} catch (error) {
toasts.addError(
error,
i18n.translate(
'xpack.observabilityAiAssistant.chatTimeline.actions.copyMessageError',
{
defaultMessage: 'Error while copying to clipboard',
}
)
);
setIsActionsPopover(false);
}
}
: noop,
},
{
id: 'regenerate',
label: i18n.translate('xpack.observabilityAiAssistant.chatTimeline.actions.regenerate', {
defaultMessage: 'Regenerate response',
}),
handler: () => {
onRegenerateMessage?.(message['@timestamp']);
setIsActionsPopover(false);
},
},
],
[MessageRole.System]: [],
[MessageRole.Event]: [],
[MessageRole.Elastic]: [],
};

return (
<EuiComment
key={message['@timestamp']}
event={<ChatItemTitle message={message} index={index} dateFormat={dateFormat} />}
timelineAvatar={<ChatItemAvatar currentUser={currentUser} role={message.message.role} />}
username={roleMap[message.message.role]}
css={
message.message.role !== MessageRole.User
? css`
.euiCommentEvent__body {
padding: 0;
}
`
: ''
event={
<ChatItemTitle
actionsTrigger={
actionsMap[message.message.role].length ? (
<EuiPopover
anchorPosition="downLeft"
button={
<EuiButtonIcon
aria-label={i18n.translate('xpack.observabilityAiAssistant.insight.actions', {
defaultMessage: 'Actions',
})}
color="text"
display="empty"
iconType="boxesHorizontal"
size="s"
onClick={handleClickActions}
/>
}
panelPaddingSize="s"
closePopover={handleClickActions}
isOpen={isActionsPopoverOpen}
>
<EuiContextMenuPanel
size="s"
items={actionsMap[message.message.role]?.map(({ id, icon, label, handler }) => (
<EuiContextMenuItem key={id} icon={icon} onClick={handler}>
{label}
</EuiContextMenuItem>
))}
/>
</EuiPopover>
) : null
}
message={message}
index={index}
dateFormat={dateFormat}
/>
}
timelineAvatar={<ChatItemAvatar currentUser={currentUser} role={message.message.role} />}
username={getRoleTranslation(message.message.role)}
>
{message.message.content ? (
<>
{message.message.role === MessageRole.User ? (
<EuiText size="s">
<p>{message.message.content}</p>
</EuiText>
) : (
<MessagePanel
body={message.message.content}
controls={<FeedbackButtons onClickFeedback={onFeedbackClick} />}
/>
)}
</>
<MessagePanel
body={<MessageText content={message.message.content} loading={isLoading} />}
controls={
message.message.role !== MessageRole.User ? (
<FeedbackButtons onClickFeedback={onFeedbackClick} />
) : null
}
/>
) : null}
</EuiComment>
);
}

const getRoleTranslation = (role: MessageRole) => {
if (role === MessageRole.User) {
return i18n.translate('xpack.observabilityAiAssistant.chatTimeline.messages.user.label', {
defaultMessage: 'You',
});
}

if (role === MessageRole.System) {
return i18n.translate('xpack.observabilityAiAssistant.chatTimeline.messages.system.label', {
defaultMessage: 'System',
});
}

return i18n.translate(
'xpack.observabilityAiAssistant.chatTimeline.messages.elasticAssistant.label',
{
defaultMessage: 'Elastic Assistant',
}
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -5,21 +5,26 @@
* 2.0.
*/

import React, { ReactNode } from 'react';
import moment from 'moment';
import { i18n } from '@kbn/i18n';
import { euiThemeVars } from '@kbn/ui-theme';
import { Message, MessageRole } from '../../../common/types';

interface ChatItemTitleProps {
actionsTrigger?: ReactNode;
dateFormat: string;
index: number;
message: Message;
}

export function ChatItemTitle({ dateFormat, index, message }: ChatItemTitleProps) {
export function ChatItemTitle({ actionsTrigger, dateFormat, index, message }: ChatItemTitleProps) {
let content: string = '';

switch (message.message.role) {
case MessageRole.User:
if (index === 0) {
return i18n.translate(
content = i18n.translate(
'xpack.observabilityAiAssistant.chatTimeline.messages.user.createdNewConversation',
{
defaultMessage: 'created a new conversation on {date}',
Expand All @@ -29,21 +34,22 @@ export function ChatItemTitle({ dateFormat, index, message }: ChatItemTitleProps
}
);
} else {
return i18n.translate(
content = i18n.translate(
'xpack.observabilityAiAssistant.chatTimeline.messages.user.addedPrompt',
{
defaultMessage: 'added a prompt on {date}',
defaultMessage: 'added a message on {date}',
values: {
date: moment(message['@timestamp']).format(dateFormat),
},
}
);
}
break;

case MessageRole.Assistant:
case MessageRole.Elastic:
case MessageRole.Function:
return i18n.translate(
content = i18n.translate(
'xpack.observabilityAiAssistant.chatTimeline.messages.elasticAssistant.responded',
{
defaultMessage: 'responded on {date}',
Expand All @@ -52,17 +58,34 @@ export function ChatItemTitle({ dateFormat, index, message }: ChatItemTitleProps
},
}
);
break;

case MessageRole.System:
return i18n.translate('xpack.observabilityAiAssistant.chatTimeline.messages.system.added', {
defaultMessage: 'added {thing} on {date}',
values: {
date: moment(message['@timestamp']).format(dateFormat),
thing: message.message.content,
},
});
content = i18n.translate(
'xpack.observabilityAiAssistant.chatTimeline.messages.system.added',
{
defaultMessage: 'added {thing} on {date}',
values: {
date: moment(message['@timestamp']).format(dateFormat),
thing: message.message.content,
},
}
);
break;

default:
return '';
content = '';
break;
}
return (
<>
{content}

{actionsTrigger ? (
<div css={{ position: 'absolute', top: 2, right: euiThemeVars.euiSizeS }}>
{actionsTrigger}
</div>
) : null}
</>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -62,15 +62,30 @@ const defaultProps = {
}),
buildMessage({
'@timestamp': String(new Date(currentDate.getTime() + 2000)),
message: buildAssistantInnerMessage(),
message: buildAssistantInnerMessage({
content: `In computer programming and mathematics, a function is a fundamental concept that represents a relationship between input values and output values. It takes one or more input values (also known as arguments or parameters) and processes them to produce a result, which is the output of the function. The input values are passed to the function, and the function performs a specific set of operations or calculations on those inputs to produce the desired output.
A function is often defined with a name, which serves as an identifier to call and use the function in the code. It can be thought of as a reusable block of code that can be executed whenever needed, and it helps in organizing code and making it more modular and maintainable.`,
}),
}),
buildMessage({
'@timestamp': String(new Date(currentDate.getTime() + 3000)),
message: buildUserInnerMessage({ content: 'How does it work?' }),
}),
buildMessage({
'@timestamp': String(new Date(currentDate.getTime() + 4000)),
message: buildElasticInnerMessage({ content: 'Here you go.' }),
message: buildElasticInnerMessage({
content: `The way functions work depends on whether we are talking about mathematical functions or programming functions. Let's explore both:
Mathematical Functions:
In mathematics, a function maps input values to corresponding output values based on a specific rule or expression. The general process of how a mathematical function works can be summarized as follows:
Step 1: Input - You provide an input value to the function, denoted as 'x' in the notation f(x). This value represents the independent variable.
Step 2: Processing - The function takes the input value and applies a specific rule or algorithm to it. This rule is defined by the function itself and varies depending on the function's expression.
Step 3: Output - After processing the input, the function produces an output value, denoted as 'f(x)' or 'y'. This output represents the dependent variable and is the result of applying the function's rule to the input.
Step 4: Uniqueness - A well-defined mathematical function ensures that each input value corresponds to exactly one output value. In other words, the function should yield the same output for the same input whenever it is called.`,
}),
}),
],
};
Expand Down
Loading

0 comments on commit 889f1a8

Please sign in to comment.