diff --git a/x-pack/plugins/observability_ai_assistant/public/components/ask_assistant_button.stories.tsx b/x-pack/plugins/observability_ai_assistant/public/components/ask_assistant_button.stories.tsx index d7bbf498f0eec..9c0d19e3c4b75 100644 --- a/x-pack/plugins/observability_ai_assistant/public/components/ask_assistant_button.stories.tsx +++ b/x-pack/plugins/observability_ai_assistant/public/components/ask_assistant_button.stories.tsx @@ -43,7 +43,6 @@ const Template: ComponentStory = (props: AskAssistantButtonPro const defaultProps = { fill: true, - iconOnly: false, size: 'm' as EuiButtonSize, variant: 'basic' as const, }; diff --git a/x-pack/plugins/observability_ai_assistant/public/components/ask_assistant_button.tsx b/x-pack/plugins/observability_ai_assistant/public/components/ask_assistant_button.tsx index 7ec3a30a1ef3e..cbd95b9b80fe5 100644 --- a/x-pack/plugins/observability_ai_assistant/public/components/ask_assistant_button.tsx +++ b/x-pack/plugins/observability_ai_assistant/public/components/ask_assistant_button.tsx @@ -11,14 +11,14 @@ import { EuiButtonEmpty, EuiButtonSize, EuiButtonEmptySizes, - useEuiTheme, EuiToolTip, + EuiButtonIcon, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; export type AskAssistantButtonProps = ( | { - variant: 'basic' | 'iconOnly'; + variant: 'basic'; size: EuiButtonSize; fill?: boolean; flush?: false; @@ -29,14 +29,16 @@ export type AskAssistantButtonProps = ( fill?: false; flush?: 'both'; } + | { + variant: 'iconOnly'; + size: EuiButtonSize; + fill?: boolean; + flush?: false; + } ) & { onClick: () => void; }; -// In order to leverage all the styling / display code that Eui buttons provide, -// we need to have the Sparkle icon part of EuiIcons. While we wait for that to land -// we have to redo some of that logic below. Todo: cleanup once Sparkle icon lands. - export function AskAssistantButton({ fill, flush, @@ -44,31 +46,25 @@ export function AskAssistantButton({ variant, onClick, }: AskAssistantButtonProps) { - const contents = ( - <> - - {variant === 'empty' ? ' ' : null} - - {variant === 'iconOnly' - ? null - : i18n.translate('xpack.observabilityAiAssistant.askAssistantButton.buttonLabel', { - defaultMessage: 'Ask Assistant', - })} - + const buttonLabel = i18n.translate( + 'xpack.observabilityAiAssistant.askAssistantButton.buttonLabel', + { + defaultMessage: 'Ask Assistant', + } ); switch (variant) { case 'basic': return ( - - {contents} + + {buttonLabel} ); case 'empty': return ( - - {contents} + + {buttonLabel} ); @@ -86,29 +82,14 @@ export function AskAssistantButton({ } )} > - - {contents} - + ); } } - -// This icon is temporary and should be removed once it lands in Eui. -function SparkleIcon({ size, color }: { size: 'xs' | 's' | 'm'; color: 'white' | 'blue' }) { - const { euiTheme } = useEuiTheme(); - - return ( - - - - ); -} diff --git a/x-pack/plugins/observability_ai_assistant/public/components/feedback_buttons.tsx b/x-pack/plugins/observability_ai_assistant/public/components/feedback_buttons.tsx new file mode 100644 index 0000000000000..08b58a85bfadf --- /dev/null +++ b/x-pack/plugins/observability_ai_assistant/public/components/feedback_buttons.tsx @@ -0,0 +1,62 @@ +/* + * 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 { EuiButtonEmpty, EuiFlexGroup, EuiFlexItem, EuiText } from '@elastic/eui'; + +export type Feedback = 'positive' | 'negative'; + +interface FeedbackButtonsProps { + onClickFeedback: (feedback: Feedback) => void; +} + +export function FeedbackButtons({ onClickFeedback }: FeedbackButtonsProps) { + return ( + + + + + {i18n.translate('xpack.observabilityAiAssistant.insight.feedbackButtons.title', { + defaultMessage: 'Was this helpful?', + })} + + + + + + + + onClickFeedback('positive')} + > + {i18n.translate('xpack.observabilityAiAssistant.insight.feedbackButtons.positive', { + defaultMessage: 'Yes', + })} + + + + + onClickFeedback('negative')} + > + {i18n.translate('xpack.observabilityAiAssistant.insight.feedbackButtons.negative', { + defaultMessage: 'No', + })} + + + + + + ); +} diff --git a/x-pack/plugins/observability_ai_assistant/public/components/insight/insight.stories.tsx b/x-pack/plugins/observability_ai_assistant/public/components/insight/insight.stories.tsx new file mode 100644 index 0000000000000..8ca01d2f2588c --- /dev/null +++ b/x-pack/plugins/observability_ai_assistant/public/components/insight/insight.stories.tsx @@ -0,0 +1,42 @@ +/* + * 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 { ComponentStory } from '@storybook/react'; + +import { Insight as Component, InsightProps } from './insight'; +import { KibanaReactStorybookDecorator } from '../../utils/storybook_decorator'; + +export default { + component: Component, + title: 'app/Molecules/Insight', + argTypes: { + debug: { + control: { + type: 'boolean', + }, + }, + }, + decorators: [KibanaReactStorybookDecorator], +}; + +const Template: ComponentStory = (props: InsightProps) => ( + +); + +const defaultProps = { + title: 'Elastic Assistant', + actions: [ + { id: 'foo', label: 'Put hands in pockets', handler: () => {} }, + { id: 'bar', label: 'Drop kick', handler: () => {} }, + ], + description: 'What is the root cause of performance degradation in my service?', + debug: true, +}; + +export const Insight = Template.bind({}); +Insight.args = defaultProps; diff --git a/x-pack/plugins/observability_ai_assistant/public/components/insight/insight.tsx b/x-pack/plugins/observability_ai_assistant/public/components/insight/insight.tsx new file mode 100644 index 0000000000000..56c0126907f68 --- /dev/null +++ b/x-pack/plugins/observability_ai_assistant/public/components/insight/insight.tsx @@ -0,0 +1,171 @@ +/* + * 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 { i18n } from '@kbn/i18n'; +import { + EuiAccordion, + EuiButtonIcon, + EuiContextMenuItem, + EuiContextMenuPanel, + EuiFilterButton, + EuiFilterGroup, + EuiFlexGroup, + EuiFlexItem, + EuiPanel, + EuiPopover, + EuiSpacer, + EuiText, + useEuiTheme, +} from '@elastic/eui'; +import moment from 'moment'; +import { useKibana } from '@kbn/kibana-react-plugin/public'; +import { AssistantAvatar } from '../assistant_avatar'; +import { InsightMissingCredentials } from './insight_missing_credentials'; +import { InsightError } from './insight_error'; +import { InsightGeneratedResponse } from './insight_generated_response'; +import { Feedback } from '../feedback_buttons'; + +export interface InsightProps { + title: string; + description?: string; + date?: Date; + debug?: boolean; + actions: Array<{ id: string; label: string; icon?: string; handler: () => void }>; +} + +export function Insight({ + title, + description, + date = new Date(), + debug, + actions = [], +}: InsightProps) { + const { euiTheme } = useEuiTheme(); + const { uiSettings } = useKibana().services; + + const dateFormat = uiSettings?.get('dateFormat'); + + const [isActionsPopoverOpen, setIsActionsPopover] = useState(false); + + const [state, setState] = useState<'missing' | 'error' | 'insightGenerated'>('insightGenerated'); + + const handleClickActions = () => { + setIsActionsPopover(!isActionsPopoverOpen); + }; + + const handleFeedback = (feedback: Feedback) => {}; + + const handleRegenerate = () => {}; + + const handleStartChat = () => {}; + + return ( + + + + + + + + +
{title}
+
+ + + {description} + + + + + + + {i18n.translate('xpack.observabilityAiAssistant.insight.generatedAt', { + defaultMessage: 'Generated at', + })}{' '} + {moment(date).format(dateFormat)} + + +
+ + } + extraAction={ + + } + panelPaddingSize="s" + closePopover={handleClickActions} + isOpen={isActionsPopoverOpen} + > + ( + + {label} + + ))} + /> + + } + > + + + {/* Debug controls. */} + {debug ? ( + + setState('insightGenerated')} + > + Normal + + setState('missing')} + > + Missing credentials + + setState('error')}> + Error + + + ) : null} + + {state === 'insightGenerated' ? ( + + ) : null} + + {state === 'error' ? : null} + + {state === 'missing' ? : null} +
+
+ ); +} diff --git a/x-pack/plugins/observability_ai_assistant/public/components/insight/insight_error.tsx b/x-pack/plugins/observability_ai_assistant/public/components/insight/insight_error.tsx new file mode 100644 index 0000000000000..c743185a5eb33 --- /dev/null +++ b/x-pack/plugins/observability_ai_assistant/public/components/insight/insight_error.tsx @@ -0,0 +1,34 @@ +/* + * 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 { EuiButton, EuiCallOut, EuiSpacer } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; + +export function InsightError() { + return ( + + {i18n.translate('xpack.observabilityAiAssistant.insight.error.description', { + defaultMessage: 'An error occured.', + })} + + + + + {i18n.translate('xpack.observabilityAiAssistant.insight.error.buttonLabel', { + defaultMessage: 'Regenerate', + })} + + + ); +} diff --git a/x-pack/plugins/observability_ai_assistant/public/components/insight/insight_generated_response.tsx b/x-pack/plugins/observability_ai_assistant/public/components/insight/insight_generated_response.tsx new file mode 100644 index 0000000000000..b396899849240 --- /dev/null +++ b/x-pack/plugins/observability_ai_assistant/public/components/insight/insight_generated_response.tsx @@ -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 from 'react'; +import { + EuiButton, + EuiButtonEmpty, + EuiFlexGroup, + EuiFlexItem, + EuiHorizontalRule, + EuiPanel, + EuiText, +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { Feedback, FeedbackButtons } from '../feedback_buttons'; +import { useStreamingText } from './use_streaming_words'; +interface InsightGeneratedResponseProps { + answer: string; + onClickFeedback: (feedback: Feedback) => void; + onClickRegenerate: () => void; + onClickStartChat: () => void; +} + +export function InsightGeneratedResponse({ + onClickFeedback, + onClickRegenerate, + onClickStartChat, +}: InsightGeneratedResponseProps) { + const answer = useStreamingText({ + message: `Lorem ipsum dolor sit amet, consectetur adipiscing elit. + +Aliquam commodo sollicitudin erat in ultrices. Vestibulum euismod ex ac lectus semper hendrerit. + +Morbi mattis odio justo, in ullamcorper metus aliquet eu. Praesent risus velit, rutrum ac magna non, vehicula vestibulum sapien. Quisque pulvinar eros eu finibus iaculis. + +Morbi dapibus sapien lacus, vitae suscipit ex egestas pharetra. In velit eros, fermentum sit amet augue ut, aliquam sodales nulla. Nunc mattis lobortis eros sit amet dapibus. + + Morbi non faucibus massa. Aliquam sed augue in eros ornare luctus sit amet cursus dolor. Pellentesque pellentesque lorem eu odio auctor convallis. Sed sodales felis at velit tempus tincidunt. Nulla sed ante cursus nibh mollis blandit. In mattis imperdiet tellus. Vestibulum nisl turpis, efficitur quis sollicitudin id, mollis in arcu. Vestibulum pulvinar tincidunt magna, vitae facilisis massa congue quis. Cras commodo efficitur tellus, et commodo risus rutrum at.`, + }); + return ( + + +

{answer}

+
+ + + + + + + + + + + + + {i18n.translate('xpack.observabilityAiAssistant.insight.response.regenerate', { + defaultMessage: 'Regenerate', + })} + + + + + + {i18n.translate('xpack.observabilityAiAssistant.insight.response.startChat', { + defaultMessage: 'Start chat', + })} + + + + + +
+ ); +} diff --git a/x-pack/plugins/observability_ai_assistant/public/components/insight/insight_missing_credentials.tsx b/x-pack/plugins/observability_ai_assistant/public/components/insight/insight_missing_credentials.tsx new file mode 100644 index 0000000000000..e074abe7830c5 --- /dev/null +++ b/x-pack/plugins/observability_ai_assistant/public/components/insight/insight_missing_credentials.tsx @@ -0,0 +1,35 @@ +/* + * 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 { EuiButton, EuiCallOut, EuiSpacer } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; + +export function InsightMissingCredentials() { + return ( + + {i18n.translate('xpack.observabilityAiAssistant.insight.missing.description', { + defaultMessage: + 'You haven’t authorised OpenAI in order to generate responses from the Elastic Assistant. Authorise the model in order to proceed.', + })} + + + + + {i18n.translate('xpack.observabilityAiAssistant.insight.missing.buttonLabel', { + defaultMessage: 'Connect Assistant', + })} + + + ); +} diff --git a/x-pack/plugins/observability_ai_assistant/public/components/insight/use_streaming_words.ts b/x-pack/plugins/observability_ai_assistant/public/components/insight/use_streaming_words.ts new file mode 100644 index 0000000000000..4bd4cedd1bff0 --- /dev/null +++ b/x-pack/plugins/observability_ai_assistant/public/components/insight/use_streaming_words.ts @@ -0,0 +1,28 @@ +/* + * 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 { useEffect, useState } from 'react'; + +interface UseStreamingTextProps { + message: string; +} + +export function useStreamingText({ message }: UseStreamingTextProps) { + const [chatMessages, setChatMessages] = useState(''); + + useEffect(() => { + const words = message.split(' '); + + for (let i = 0; i < words.length; i++) { + setTimeout(() => { + setChatMessages((prevState) => `${prevState} ${words[i]}`); + }, i * 50); // Adjust typing speed here (milliseconds per word) + } + }, [message]); + + return chatMessages; +} diff --git a/x-pack/plugins/observability_ai_assistant/public/components/insight_panel.stories.tsx b/x-pack/plugins/observability_ai_assistant/public/components/insight_panel.stories.tsx deleted file mode 100644 index a90ca3b2019b0..0000000000000 --- a/x-pack/plugins/observability_ai_assistant/public/components/insight_panel.stories.tsx +++ /dev/null @@ -1,28 +0,0 @@ -/* - * 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 { ComponentStory } from '@storybook/react'; - -import { InsightPanel as Component, InsightPanelProps } from './insight_panel'; - -export default { - component: Component, - title: 'app/Molecules/InsightPanel', - argTypes: {}, -}; - -const Template: ComponentStory = (props: InsightPanelProps) => ( - -); - -const defaultProps = { - title: 'Elastic Assistant', -}; - -export const InsightPanel = Template.bind({}); -InsightPanel.args = defaultProps; diff --git a/x-pack/plugins/observability_ai_assistant/public/components/insight_panel.tsx b/x-pack/plugins/observability_ai_assistant/public/components/insight_panel.tsx deleted file mode 100644 index 8d500ca19858f..0000000000000 --- a/x-pack/plugins/observability_ai_assistant/public/components/insight_panel.tsx +++ /dev/null @@ -1,50 +0,0 @@ -/* - * 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 { EuiButtonIcon, EuiFlexGroup, EuiFlexItem, EuiPanel, EuiText } from '@elastic/eui'; -import { AssistantAvatar } from './assistant_avatar'; - -export interface InsightPanelProps { - title: string; -} - -export function InsightPanel({ title }: InsightPanelProps) { - return ( - - - {/* expand / contract */} - - - - - {/* content */} - - - - - - - -
{title}
-
-
-
-
- - {/* actions */} - - - - - - - -
-
- ); -} diff --git a/x-pack/plugins/observability_ai_assistant/public/utils/storybook_decorator.tsx b/x-pack/plugins/observability_ai_assistant/public/utils/storybook_decorator.tsx new file mode 100644 index 0000000000000..0ef1cfa5ce358 --- /dev/null +++ b/x-pack/plugins/observability_ai_assistant/public/utils/storybook_decorator.tsx @@ -0,0 +1,27 @@ +/* + * 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, { ComponentType } from 'react'; +import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public'; + +export function KibanaReactStorybookDecorator(Story: ComponentType) { + return ( + { + if (setting === 'dateFormat') { + return 'MMM D, YYYY HH:mm'; + } + }, + }, + }} + > + + + ); +}