Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add ChatTimeline components #9

Merged
merged 28 commits into from
Jul 25, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
90203d9
Observability AI Assistant plugin
dgieselaar Jul 15, 2023
798b1c3
Observability AI Assistant Service + Client
dgieselaar Jul 18, 2023
5f96a6c
Add Storybook setup
CoenWarmer Jul 19, 2023
8e5e894
Add AskAiAssistantButton
CoenWarmer Jul 19, 2023
09f821b
Add Assistant Avatar
CoenWarmer Jul 19, 2023
378cc90
First pass of InsightPanel
CoenWarmer Jul 19, 2023
6d7bbcf
Merge pull request #6 from CoenWarmer/storybook
dgieselaar Jul 19, 2023
9279a0b
newline in package.json
dgieselaar Jul 19, 2023
7e2525d
Update AskAssistantButton to include Sparkles from Eui
CoenWarmer Jul 19, 2023
4fc20ac
[CI] Auto-commit changed files from 'node scripts/lint_ts_projects --…
kibanamachine Jul 19, 2023
b06714b
[CI] Auto-commit changed files from 'node scripts/generate codeowners'
kibanamachine Jul 19, 2023
0cd6dde
[CI] Auto-commit changed files from 'node scripts/build_plugin_list_d…
kibanamachine Jul 19, 2023
e27e590
Add plugin to limits.yml
dgieselaar Jul 19, 2023
26dcb4e
Merge branch 'obs-ai-assistant' of github.com:dgieselaar/kibana into …
dgieselaar Jul 19, 2023
8a30a08
Fix i18n check
dgieselaar Jul 19, 2023
a9c59b6
Added Insight component
CoenWarmer Jul 19, 2023
e3a67c8
Merge branch 'obs-ai-assistant' into storybook
CoenWarmer Jul 20, 2023
d391008
Update i18n translation base key
CoenWarmer Jul 20, 2023
cc0f643
Merge branch 'storybook' of github.com:CoenWarmer/kibana into storybook
CoenWarmer Jul 20, 2023
3c96673
Add builders for Messages
CoenWarmer Jul 20, 2023
c0decc6
Add hooks
CoenWarmer Jul 20, 2023
1a3d74c
Add security as dependency
CoenWarmer Jul 20, 2023
990d6d1
Update imports
CoenWarmer Jul 20, 2023
fa10766
First pass of Chat interface
CoenWarmer Jul 20, 2023
0b66523
Add chat timeline
CoenWarmer Jul 25, 2023
43aede2
Merge branch 'obs-ai-assistant' of github.com:dgieselaar/kibana into …
CoenWarmer Jul 25, 2023
a6527b4
Integrate changes
CoenWarmer Jul 25, 2023
d4d2ad3
Remove background for EA
CoenWarmer Jul 25, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 3 additions & 12 deletions x-pack/plugins/observability_ai_assistant/kibana.jsonc
Original file line number Diff line number Diff line change
Expand Up @@ -6,18 +6,9 @@
"id": "observabilityAIAssistant",
"server": true,
"browser": true,
"configPath": [
"xpack",
"observabilityAIAssistant"
],
"requiredPlugins": [
"triggersActionsUi",
"actions",
"security"
],
"requiredBundles": [
"kibanaReact"
],
"configPath": ["xpack", "observabilityAIAssistant"],
"requiredPlugins": ["triggersActionsUi", "actions", "security"],
"requiredBundles": ["kibanaReact"],
"optionalPlugins": [],
"extraPublicDirs": []
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,11 @@
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import React from 'react';
import React, { ReactNode } from 'react';

export interface AssistantAvatarProps {
size: keyof typeof sizeMap;
size?: keyof typeof sizeMap;
children?: ReactNode;
}

export const sizeMap = {
Expand All @@ -18,7 +19,7 @@ export const sizeMap = {
xs: 16,
};

export function AssistantAvatar({ size }: AssistantAvatarProps) {
export function AssistantAvatar({ size = 's' }: AssistantAvatarProps) {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/

import React from 'react';
import { i18n } from '@kbn/i18n';
import { css } from '@emotion/react';
import { EuiText, EuiComment } 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';

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' }
),
Comment on lines +32 to +43
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think we need these, as it's a display issue, not a translation issue if that makes sense (in any case I think it will fail CI because you're declaring a message multiple times)

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll merge this in and we can fix it later.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oversight during copy pasting. Will fix 😃

};

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

export function ChatItem({
currentUser,
dateFormat,
index,
message,
onFeedbackClick,
}: ChatItemProps) {
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;
}
`
: ''
}
>
{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} />}
/>
)}
</>
) : null}
</EuiComment>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/

import React from 'react';
import { UserAvatar } from '@kbn/user-profile-components';
import { EuiAvatar, EuiLoadingSpinner } from '@elastic/eui';
import type { AuthenticatedUser } from '@kbn/security-plugin/common';
import { AssistantAvatar } from '../assistant_avatar';
import { MessageRole } from '../../../common/types';

interface ChatAvatarProps {
currentUser?: AuthenticatedUser | undefined;
role: MessageRole;
}

export function ChatItemAvatar({ currentUser, role }: ChatAvatarProps) {
switch (role) {
case MessageRole.User:
return currentUser ? (
<UserAvatar user={currentUser} size="m" data-test-subj="userMenuAvatar" />
) : (
<EuiLoadingSpinner size="xl" />
);

case MessageRole.Assistant:
case MessageRole.Elastic:
case MessageRole.Function:
return <EuiAvatar name="Elastic Assistant" iconType={AssistantAvatar} color="subdued" />;

case MessageRole.System:
return <EuiAvatar name="system" iconType="dot" color="subdued" />;

default:
return null;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/

import moment from 'moment';
import { i18n } from '@kbn/i18n';
import { Message, MessageRole } from '../../../common/types';

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

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

case MessageRole.Assistant:
case MessageRole.Elastic:
case MessageRole.Function:
return i18n.translate(
'xpack.observabilityAiAssistant.chatTimeline.messages.elasticAssistant.responded',
{
defaultMessage: 'responded on {date}',
values: {
date: moment(message['@timestamp']).format(dateFormat),
},
}
);

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,
},
});

default:
return '';
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/

import React, { useState } from 'react';
import { ComponentStory } from '@storybook/react';
import { EuiButton, EuiSpacer } from '@elastic/eui';

import { ChatTimeline as Component, ChatTimelineProps } from './chat_timeline';
import {
buildAssistantInnerMessage,
buildElasticInnerMessage,
buildMessage,
buildSystemInnerMessage,
buildUserInnerMessage,
} from '../../utils/builders';

export default {
component: Component,
title: 'app/Molecules/ChatTimeline',
parameters: {
backgrounds: {
default: 'white',
values: [{ name: 'white', value: '#fff' }],
},
},
argTypes: {},
};

const Template: ComponentStory<typeof Component> = (props: ChatTimelineProps) => {
const [count, setCount] = useState(0);

return (
<>
<Component {...props} messages={props.messages.filter((_, index) => index <= count)} />

<EuiSpacer />

<EuiButton
onClick={() => setCount(count >= 0 && count < props.messages.length - 1 ? count + 1 : 0)}
>
Add message
</EuiButton>
</>
);
};

const currentDate = new Date();

const defaultProps = {
messages: [
buildMessage({
'@timestamp': String(new Date(currentDate.getTime())),
message: buildSystemInnerMessage(),
}),
buildMessage({
'@timestamp': String(new Date(currentDate.getTime() + 1000)),
message: buildUserInnerMessage(),
}),
buildMessage({
'@timestamp': String(new Date(currentDate.getTime() + 2000)),
message: buildAssistantInnerMessage(),
}),
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.' }),
}),
],
};

export const ChatTimeline = Template.bind({});
ChatTimeline.args = defaultProps;
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/

import React from 'react';
import { EuiCommentList } from '@elastic/eui';
import { useKibana } from '../../hooks/use_kibana';
import { useCurrentUser } from '../../hooks/use_current_user';
import { Message } from '../../../common/types';
import { ChatItem } from './chat_item';

export interface ChatTimelineProps {
messages: Message[];
}

export function ChatTimeline({ messages = [] }: ChatTimelineProps) {
const { uiSettings } = useKibana().services;
const currentUser = useCurrentUser();

const dateFormat = uiSettings?.get('dateFormat');

return (
<EuiCommentList>
{messages.map((message, index) => (
<ChatItem
currentUser={currentUser}
dateFormat={dateFormat}
index={index}
message={message}
onFeedbackClick={() => {}}
/>
))}
</EuiCommentList>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ import {
EuiFlexItem,
EuiHorizontalRule,
EuiIcon,
EuiPanel,
EuiSpacer,
EuiText,
} from '@elastic/eui';
Expand All @@ -24,7 +23,7 @@ interface Props {

export function MessagePanel(props: Props) {
return (
<EuiPanel color="subdued" hasShadow={false}>
<>
{props.body}
{props.error ? (
<>
Expand All @@ -51,6 +50,6 @@ export function MessagePanel(props: Props) {
{props.controls}
</>
) : null}
</EuiPanel>
</>
);
}
Loading