From 0b66523b5249b4319ffcdd6aabc6eca6f91b2b31 Mon Sep 17 00:00:00 2001 From: Coen Warmer Date: Tue, 25 Jul 2023 10:52:33 +0200 Subject: [PATCH] Add chat timeline --- .../public/components/chat/chat_item.tsx | 65 ++++++++++++ .../{chat_avatar.tsx => chat_item_avatar.tsx} | 13 ++- .../components/chat/chat_item_title.tsx | 68 +++++++++++++ .../components/chat/chat_timeline.stories.tsx | 63 ++++++++++-- .../public/components/chat/chat_timeline.tsx | 98 +++---------------- .../__storybook_mocks__/use_current_user.ts | 13 --- .../hooks/__storybook_mocks__/use_kibana.ts | 20 ++++ .../public/utils/builders.ts | 60 +++++++----- 8 files changed, 261 insertions(+), 139 deletions(-) create mode 100644 x-pack/plugins/observability_ai_assistant/public/components/chat/chat_item.tsx rename x-pack/plugins/observability_ai_assistant/public/components/chat/{chat_avatar.tsx => chat_item_avatar.tsx} (71%) create mode 100644 x-pack/plugins/observability_ai_assistant/public/components/chat/chat_item_title.tsx create mode 100644 x-pack/plugins/observability_ai_assistant/public/hooks/__storybook_mocks__/use_kibana.ts diff --git a/x-pack/plugins/observability_ai_assistant/public/components/chat/chat_item.tsx b/x-pack/plugins/observability_ai_assistant/public/components/chat/chat_item.tsx new file mode 100644 index 00000000000000..b1ad9916dfc828 --- /dev/null +++ b/x-pack/plugins/observability_ai_assistant/public/components/chat/chat_item.tsx @@ -0,0 +1,65 @@ +/* + * 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 { 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'; + +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 ChatItemProps { + currentUser: AuthenticatedUser | undefined; + dateFormat: string; + index: number; + message: Message; +} + +export function ChatItem({ currentUser, dateFormat, index, message }: ChatItemProps) { + return ( + } + timelineAvatar={} + username={roleMap[message.message.role]} + > + {message.message.content ? ( + +

{message.message.content}

+
+ ) : null} +
+ ); +} diff --git a/x-pack/plugins/observability_ai_assistant/public/components/chat/chat_avatar.tsx b/x-pack/plugins/observability_ai_assistant/public/components/chat/chat_item_avatar.tsx similarity index 71% rename from x-pack/plugins/observability_ai_assistant/public/components/chat/chat_avatar.tsx rename to x-pack/plugins/observability_ai_assistant/public/components/chat/chat_item_avatar.tsx index 5aae9db530af8a..0fb624bab89089 100644 --- a/x-pack/plugins/observability_ai_assistant/public/components/chat/chat_avatar.tsx +++ b/x-pack/plugins/observability_ai_assistant/public/components/chat/chat_item_avatar.tsx @@ -13,15 +13,15 @@ import { AssistantAvatar } from '../assistant_avatar'; import { MessageRole } from '../../../common/types'; interface ChatAvatarProps { + currentUser?: AuthenticatedUser | undefined; role: MessageRole; - user?: AuthenticatedUser | undefined; } -export function ChatAvatar({ user, role }: ChatAvatarProps) { +export function ChatItemAvatar({ currentUser, role }: ChatAvatarProps) { switch (role) { case MessageRole.User: - return user ? ( - + return currentUser ? ( + ) : ( ); @@ -29,7 +29,10 @@ export function ChatAvatar({ user, role }: ChatAvatarProps) { case MessageRole.Assistant: case MessageRole.Elastic: case MessageRole.Function: - return ; + return ; + + case MessageRole.System: + return ; default: return null; diff --git a/x-pack/plugins/observability_ai_assistant/public/components/chat/chat_item_title.tsx b/x-pack/plugins/observability_ai_assistant/public/components/chat/chat_item_title.tsx new file mode 100644 index 00000000000000..393886fbef8002 --- /dev/null +++ b/x-pack/plugins/observability_ai_assistant/public/components/chat/chat_item_title.tsx @@ -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 ''; + } +} diff --git a/x-pack/plugins/observability_ai_assistant/public/components/chat/chat_timeline.stories.tsx b/x-pack/plugins/observability_ai_assistant/public/components/chat/chat_timeline.stories.tsx index b145b568666110..dea53f07da7448 100644 --- a/x-pack/plugins/observability_ai_assistant/public/components/chat/chat_timeline.stories.tsx +++ b/x-pack/plugins/observability_ai_assistant/public/components/chat/chat_timeline.stories.tsx @@ -5,28 +5,73 @@ * 2.0. */ -import React from 'react'; +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 { buildAssistantMessage, buildElasticMessage, buildUserMessage } from '../../utils/builders'; +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 = (props: ChatTimelineProps) => ( - -); +const Template: ComponentStory = (props: ChatTimelineProps) => { + const [count, setCount] = useState(0); + + return ( + <> + index <= count)} /> + + + + setCount(count >= 0 && count < props.messages.length - 1 ? count + 1 : 0)} + > + Add message + + + ); +}; + +const currentDate = new Date(); const defaultProps = { messages: [ - buildUserMessage(), - buildAssistantMessage(), - buildUserMessage(), - buildElasticMessage(), + 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.' }), + }), ], }; diff --git a/x-pack/plugins/observability_ai_assistant/public/components/chat/chat_timeline.tsx b/x-pack/plugins/observability_ai_assistant/public/components/chat/chat_timeline.tsx index e160add8e7b914..0576349a16b48f 100644 --- a/x-pack/plugins/observability_ai_assistant/public/components/chat/chat_timeline.tsx +++ b/x-pack/plugins/observability_ai_assistant/public/components/chat/chat_timeline.tsx @@ -6,104 +6,32 @@ */ import React from 'react'; -import { EuiText, EuiCommentList, EuiComment, EuiCode, EuiPanel } from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; -import { Message, MessageRole } from '../../../common/types'; +import { EuiCommentList } from '@elastic/eui'; +import { useKibana } from '../../hooks/use_kibana'; import { useCurrentUser } from '../../hooks/use_current_user'; -import { ChatAvatar } from './chat_avatar'; +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 ( {messages.map((message, index) => ( - } - > - {message.message.role === MessageRole.User && index === 0 ? ( - -

{message.message.content}

-
- ) : ( - -

{message.message.content}

-
- )} -
+ ))}
); } - -// -// {messages.map((message, index) => ( -// } -// > -// -// {message.message.role === MessageRole.User && index === 0 ? ( -// <> -// -// -//

-// -// {i18n.translate( -// 'xpack.observabilityAiAssistant.chatTimeline.messages.userInitiatedTitle.you', -// { defaultMessage: 'You' } -// )}{' '} -// -// {i18n.translate( -// 'xpack.observabilityAiAssistant.chatTimeline.messages.userInitiatedTitle.createdNewConversation', -// { -// defaultMessage: 'created a new conversation', -// } -// )} -//

-//
-//
-// -// -// ) : null} - -// -// -//

{message.message.content}

-//
-//
-//
-//
-// ))} -//
diff --git a/x-pack/plugins/observability_ai_assistant/public/hooks/__storybook_mocks__/use_current_user.ts b/x-pack/plugins/observability_ai_assistant/public/hooks/__storybook_mocks__/use_current_user.ts index bf9840151ef028..cbeba226f38358 100644 --- a/x-pack/plugins/observability_ai_assistant/public/hooks/__storybook_mocks__/use_current_user.ts +++ b/x-pack/plugins/observability_ai_assistant/public/hooks/__storybook_mocks__/use_current_user.ts @@ -5,19 +5,6 @@ * 2.0. */ -interface User { - username: string; - email?: string; - full_name?: string; - roles: readonly string[]; - enabled: boolean; - metadata?: { - _reserved: boolean; - _deprecated?: boolean; - _deprecated_reason?: string; - }; -} - export function useCurrentUser() { return { username: 'john_doe', diff --git a/x-pack/plugins/observability_ai_assistant/public/hooks/__storybook_mocks__/use_kibana.ts b/x-pack/plugins/observability_ai_assistant/public/hooks/__storybook_mocks__/use_kibana.ts new file mode 100644 index 00000000000000..8fdfe6d29c20b7 --- /dev/null +++ b/x-pack/plugins/observability_ai_assistant/public/hooks/__storybook_mocks__/use_kibana.ts @@ -0,0 +1,20 @@ +/* + * 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. + */ + +export function useKibana() { + return { + services: { + uiSettings: { + get: (setting: string) => { + if (setting === 'dateFormat') { + return 'MMM D, YYYY HH:mm'; + } + }, + }, + }, + }; +} diff --git a/x-pack/plugins/observability_ai_assistant/public/utils/builders.ts b/x-pack/plugins/observability_ai_assistant/public/utils/builders.ts index 2c34657fa07bc8..26206802fca4e8 100644 --- a/x-pack/plugins/observability_ai_assistant/public/utils/builders.ts +++ b/x-pack/plugins/observability_ai_assistant/public/utils/builders.ts @@ -9,7 +9,7 @@ import { cloneDeep } from 'lodash'; import { Message, MessageRole } from '../../common/types'; const baseMessage: Message = { - '@timestamp': String(new Date().getUTCMilliseconds()), + '@timestamp': String(Date.now()), message: { content: 'foo', name: 'bar', @@ -21,48 +21,54 @@ export function buildMessage(params: Partial = {}): Message { return cloneDeep({ ...baseMessage, ...params }); } -export function buildUserMessage(params: Partial = {}): Message { +export function buildSystemInnerMessage( + params: Partial = {} +): Message['message'] { return cloneDeep({ - ...baseMessage, - ...{ message: { content: "What's this function?", role: MessageRole.User } }, - ...params, + ...{ role: MessageRole.System, ...params }, }); } -export function buildAssistantMessage(params: Partial = {}): Message { +export function buildUserInnerMessage( + params: Partial = {} +): Message['message'] { return cloneDeep({ - ...baseMessage, - ...{ - message: { - content: 'This is "leftpad":', - role: MessageRole.Assistant, - data: { key: 'value', nestedData: { foo: 'bar' } }, - }, - }, - ...params, + ...{ content: "What's this function?", role: MessageRole.User, ...params }, }); } -export function buildElasticMessage(params: Partial = {}): Message { +export function buildAssistantInnerMessage( + params: Partial = {} +): Message['message'] { return cloneDeep({ - ...baseMessage, ...{ - message: { role: MessageRole.Elastic, data: { key: 'value', nestedData: { foo: 'bar' } } }, + content: 'This is "leftpad":', + role: MessageRole.Assistant, + data: { key: 'value', nestedData: { foo: 'bar' } }, + ...params, }, - ...params, }); } -export function buildFunctionMessage(params: Partial = {}): Message { +export function buildElasticInnerMessage( + params: Partial = {} +): Message['message'] { + return cloneDeep({ + ...{ role: MessageRole.Elastic, data: { key: 'value', nestedData: { foo: 'bar' } }, ...params }, + }); +} + +export function buildFunctionInnerMessage( + params: Partial = {} +): Message['message'] { return cloneDeep({ - ...baseMessage, ...{ - message: { role: MessageRole.Function }, - function_call: { - name: 'leftpad', - args: '{ foo: "bar" }', - trigger: MessageRole.User, - }, + role: MessageRole.Function, + }, + function_call: { + name: 'leftpad', + args: '{ foo: "bar" }', + trigger: MessageRole.User, }, ...params, });