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

Support creating alert in the chat window #217

Open
wants to merge 9 commits into
base: main
Choose a base branch
from
3 changes: 2 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,4 +25,5 @@ Inspired from [Keep a Changelog](https://keepachangelog.com/en/1.0.0/)
- Add experimental feature to support text to visualization ([#218](https://github.com/opensearch-project/dashboards-assistant/pull/218))
- Be compatible with ML configuration index mapping change ([#239](https://github.com/opensearch-project/dashboards-assistant/pull/239))
- Support context aware alert analysis by reusing incontext insight component([#215](https://github.com/opensearch-project/dashboards-assistant/pull/215))
Use smaller and compressed variants of buttons and form components ([#250](https://github.com/opensearch-project/dashboards-assistant/pull/250))
Use smaller and compressed variants of buttons and form components ([#250](https://github.com/opensearch-project/dashboards-assistant/pull/250))
- Add experimental feature to support create alert by chatbot ([#217](https://github.com/opensearch-project/dashboards-assistant/pull/217))
15 changes: 12 additions & 3 deletions common/types/chat_saved_object_attributes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,19 +55,22 @@ export interface IOutput {
toolsUsed?: string[];
contentType: 'error' | 'markdown' | 'visualization' | string;
content: string;
additionalActions?: IAdditionalAction[];
suggestedActions?: ISuggestedAction[];
messageId?: string;
fullWidth?: boolean;
}
export type IMessage = IInput | IOutput;

interface ISuggestedActionBase {
interface IActionBase {
actionType: string;
message: string;
}
export type ISuggestedAction = ISuggestedActionBase &
export type ISuggestedAction = IActionBase &
(
| { actionType: 'send_as_input' | 'copy' | 'view_in_dashboards' }
| {
actionType: 'send_as_input' | 'copy' | 'view_in_dashboards' | 'create_monitor_in_dashboard';
}
| {
actionType: 'view_ppl_visualization';
metadata: { query: string; question: string };
Expand All @@ -77,6 +80,12 @@ export type ISuggestedAction = ISuggestedActionBase &
metadata: { interactionId: string };
}
);

export type IAdditionalAction = IActionBase & {
actionType: 'create_monitor_grid' | 'create_alert_button';
content: string;
};

export interface SendFeedbackBody {
satisfaction: boolean;
}
Expand Down
74 changes: 39 additions & 35 deletions public/chat_flyout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { ChatWindowHeader } from './tabs/chat_window_header';
import { ChatHistoryPage } from './tabs/history/chat_history_page';
import { AgentFrameworkTracesFlyoutBody } from './components/agent_framework_traces_flyout_body';
import { TAB_ID } from './utils/constants';
import { ChatOverrideHeader } from './tabs/chat_override_header';

interface ChatFlyoutProps {
flyoutVisible: boolean;
Expand Down Expand Up @@ -86,52 +87,55 @@ export const ChatFlyout = (props: ChatFlyoutProps) => {
>
<>
<div className={cs('llm-chat-flyout-header')}>
<ChatWindowHeader />
{props.overrideComponent ? <ChatOverrideHeader /> : <ChatWindowHeader />}
Copy link
Collaborator

Choose a reason for hiding this comment

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

Is this ChatOverrideHeader suitable for all override component header? Shall we just hide the ChatWindowHeader and let overrideComponent take over the whole screen?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes, I think this is a standard header for all override component. As UX required, we should change the title, add a back button and hide the history button. There is a mock up for AD, which also applies to alert.

image

</div>

{props.overrideComponent}
<EuiResizableContainer style={{ height: '100%', overflow: 'hidden' }}>
{(Panel, Resizer) => (
<>
<Panel
aria-label="chat panel"
className={cs('llm-chat-horizontal-resize-panel', {
'llm-chat-hidden': leftPanelSize === 0,
})}
scrollable={false}
size={leftPanelSize}
initialSize={resizable ? 70 : undefined}
paddingSize="none"
>
<ChatPage />
</Panel>
{props.overrideComponent ? (
props.overrideComponent
) : (
<EuiResizableContainer style={{ height: '100%', overflow: 'hidden' }}>
{(Panel, Resizer) => (
<>
{resizable && <Resizer />}
<Panel
aria-label="history panel"
aria-label="chat panel"
className={cs('llm-chat-horizontal-resize-panel', {
'llm-chat-hidden': leftPanelSize === 100,
'llm-chat-hidden': leftPanelSize === 0,
})}
scrollable={false}
size={rightPanelSize}
initialSize={resizable ? 30 : undefined}
size={leftPanelSize}
initialSize={resizable ? 70 : undefined}
paddingSize="none"
>
{chatHistoryPageLoadedRef.current && (
<ChatHistoryPage
// refresh data when user switched to table from another tab
shouldRefresh={chatHistoryPageVisible}
className={cs({ 'llm-chat-hidden': !chatHistoryPageVisible })}
/>
)}
{chatTraceVisible && chatContext.interactionId && (
<AgentFrameworkTracesFlyoutBody />
)}
{<ChatPage />}
</Panel>
<>
{resizable && <Resizer />}
<Panel
aria-label="history panel"
className={cs('llm-chat-horizontal-resize-panel', {
'llm-chat-hidden': leftPanelSize === 100,
})}
scrollable={false}
size={rightPanelSize}
initialSize={resizable ? 30 : undefined}
paddingSize="none"
>
{chatHistoryPageLoadedRef.current && (
<ChatHistoryPage
// refresh data when user switched to table from another tab
shouldRefresh={chatHistoryPageVisible}
className={cs({ 'llm-chat-hidden': !chatHistoryPageVisible })}
/>
)}
{chatTraceVisible && chatContext.interactionId && (
<AgentFrameworkTracesFlyoutBody />
)}
</Panel>
</>
</>
</>
)}
</EuiResizableContainer>
)}
</EuiResizableContainer>
)}
</>
</div>
);
Expand Down
17 changes: 16 additions & 1 deletion public/chat_header_button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,10 @@ import { ChatStateProvider } from './hooks';
import './index.scss';
import { ActionExecutor, AssistantActions, MessageRenderer, TabId, UserAccount } from './types';
import {
TAB_ID,
DEFAULT_SIDECAR_DOCKED_MODE,
DEFAULT_SIDECAR_LEFT_OR_RIGHT_SIZE,
OVERRIDE_SIDECAR_LEFT_OR_RIGHT_SIZE,
TAB_ID,
} from './utils/constants';
import { useCore } from './contexts/core_context';
import { MountPointPortal } from '../../../src/plugins/opensearch_dashboards_react/public';
Expand All @@ -42,6 +43,7 @@ export const HeaderChatButton = (props: HeaderChatButtonProps) => {
const [conversationId, setConversationId] = useState<string>();
const [title, setTitle] = useState<string>();
const [flyoutVisible, setFlyoutVisible] = useState(false);
const [overrideName, setOverrideName] = useState<string>();
const [flyoutComponent, setFlyoutComponent] = useState<React.ReactNode | null>(null);
const [selectedTabId, setSelectedTabId] = useState<TabId>(TAB_ID.CHAT);
const [preSelectedTabId, setPreSelectedTabId] = useState<TabId | undefined>(undefined);
Expand Down Expand Up @@ -76,6 +78,8 @@ export const HeaderChatButton = (props: HeaderChatButtonProps) => {
flyoutFullScreen,
setFlyoutVisible,
setFlyoutComponent,
overrideName,
setOverrideName,
messageRenderers: props.messageRenderers,
actionExecutors: props.actionExecutors,
currentAccount: props.currentAccount,
Expand Down Expand Up @@ -156,6 +160,17 @@ export const HeaderChatButton = (props: HeaderChatButtonProps) => {
flyoutMountPoint.current = mountPoint;
}, []);

useEffect(() => {
if (flyoutLoaded && flyoutVisible) {
core.overlays.sidecar().setSidecarConfig({
paddingSize:
selectedTabId === TAB_ID.OVERRIDE
? OVERRIDE_SIDECAR_LEFT_OR_RIGHT_SIZE
: DEFAULT_SIDECAR_LEFT_OR_RIGHT_SIZE,
});
}
}, [selectedTabId === TAB_ID.OVERRIDE]);

useEffect(() => {
const onGlobalMouseUp = (e: KeyboardEvent) => {
if (e.ctrlKey && e.key === '/') {
Expand Down
2 changes: 2 additions & 0 deletions public/contexts/chat_context.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ export interface IChatContext {
flyoutFullScreen: boolean;
setFlyoutVisible: React.Dispatch<React.SetStateAction<boolean>>;
setFlyoutComponent: React.Dispatch<React.SetStateAction<React.ReactNode | null>>;
overrideName?: string;
setOverrideName: React.Dispatch<React.SetStateAction<string | undefined>>;
messageRenderers: Record<string, MessageRenderer>;
actionExecutors: Record<string, ActionExecutor>;
currentAccount?: UserAccount;
Expand Down
3 changes: 2 additions & 1 deletion public/index.scss
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,8 @@

.llm-chat-flyout {
height: 100%;
display: flex;
flex-direction: column;
.euiFlyoutFooter {
background: transparent;
}
Expand All @@ -93,7 +95,6 @@
}

.llm-chat-flyout-footer {
padding-bottom: 24px;
}

.llm-chat-bubble-wrapper {
Expand Down
1 change: 1 addition & 0 deletions public/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,3 +23,4 @@ export function plugin(initializerContext: PluginInitializerContext) {

export { AssistantSetup, RenderProps } from './types';
export { IMessage } from '../common/types/chat_saved_object_attributes';
export { TAB_ID } from './utils/constants';
44 changes: 44 additions & 0 deletions public/tabs/chat/messages/message_content.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,50 @@ describe('<MessageContent />', () => {
expect(screen.queryAllByText('title')).toHaveLength(1);
});

it('should display message(markdown + additionalAction)', () => {
render(
<MessageContent
message={{
type: 'output',
contentType: 'markdown',
additionalActions: [
{
actionType: 'customized_content_type',
message: 'customized title',
content: 'mock customized content',
},
],
content: '# title',
}}
/>
);
expect(screen.queryAllByText('title')).toHaveLength(1);
expect(customizedRenderMock.mock.calls[0][0]).toMatchInlineSnapshot(`
Object {
"content": "mock customized content",
"contentType": "customized_content_type",
"type": "output",
}
`);
expect(customizedRenderMock.mock.calls[0][1].props).toMatchInlineSnapshot(`
Object {
"message": Object {
"additionalActions": Array [
Object {
"actionType": "customized_content_type",
"content": "mock customized content",
"message": "customized title",
},
],
"content": "# title",
"contentType": "markdown",
"type": "output",
},
}
`);
expect(customizedRenderMock.mock.calls[0][1].chatContext).not.toBeUndefined();
});

it('should render customized render content', () => {
render(
<MessageContent
Expand Down
20 changes: 18 additions & 2 deletions public/tabs/chat/messages/message_content.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,12 @@
* SPDX-License-Identifier: Apache-2.0
*/

import { EuiMarkdownFormat, EuiText } from '@elastic/eui';
import { EuiButton, EuiFlexGroup, EuiFlexItem, EuiMarkdownFormat, EuiText } from '@elastic/eui';
import React from 'react';
import { IMessage } from '../../../../common/types/chat_saved_object_attributes';
import { CoreVisualization } from '../../../components/core_visualization';
import { useChatContext } from '../../../contexts/chat_context';
import { TAB_ID } from '../../../utils/constants';

export interface MessageContentProps {
message: IMessage;
Expand All @@ -28,7 +29,22 @@ export const MessageContent: React.FC<MessageContentProps> = React.memo((props)
);

case 'markdown':
return <EuiMarkdownFormat>{props.message.content}</EuiMarkdownFormat>;
return (
<>
<EuiMarkdownFormat>{props.message.content}</EuiMarkdownFormat>
{props.message.additionalActions &&
props.message.additionalActions.map((action, index) => (
<div key={'action-' + index}>
{chatContext.messageRenderers[action.actionType]?.(
{
...{ type: 'output', contentType: action.actionType, content: action.content },
},
{ props, chatContext }
) ?? null}
</div>
))}
</>
);

case 'visualization':
return (
Expand Down
53 changes: 53 additions & 0 deletions public/tabs/chat_override_header.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
/*
* Copyright OpenSearch Contributors
* SPDX-License-Identifier: Apache-2.0
*/

import { EuiButtonEmpty, EuiButtonIcon, EuiFlexGroup, EuiFlexItem, EuiIcon } from '@elastic/eui';
import React, { useCallback } from 'react';
import { IChatContext, useChatContext } from '../contexts/chat_context';
import { TAB_ID } from '../utils/constants';
import { SidecarIconMenu } from '../components/sidecar_icon_menu';

export const ChatOverrideHeader = React.memo(() => {
const chatContext = useChatContext() as IChatContext;
const { setSelectedTabId, setFlyoutComponent, setOverrideName } = chatContext;

const handleBack = useCallback(() => {
setSelectedTabId(TAB_ID.CHAT);
setFlyoutComponent(null);
setOverrideName(undefined);
}, [setSelectedTabId]);

return (
<>
<EuiFlexGroup
gutterSize="s"
justifyContent="spaceAround"
alignItems="center"
responsive={false}
>
<EuiFlexItem>
<EuiFlexGroup gutterSize="none" alignItems="center" responsive={false}>
<EuiButtonEmpty flush="left" size="xs" onClick={handleBack} iconType="arrowLeft">
{chatContext?.overrideName || 'Back'}
</EuiButtonEmpty>
</EuiFlexGroup>
</EuiFlexItem>
<SidecarIconMenu />
<EuiFlexItem grow={false}>
<EuiButtonIcon
aria-label="close"
size="xs"
color="text"
iconType="cross"
onClick={() => {
chatContext.setFlyoutVisible(false);
}}
/>
</EuiFlexItem>
<EuiFlexItem grow={false} />
</EuiFlexGroup>
</>
);
});
3 changes: 2 additions & 1 deletion public/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { IChatContext } from './contexts/chat_context';
import { MessageContentProps } from './tabs/chat/messages/message_content';
import { DataSourceServiceContract, IncontextInsightRegistry } from './services';
import { DataSourceManagementPluginSetup } from '../../../src/plugins/data_source_management/public';
import { TAB_ID } from './utils/constants';
import {
VisualizationsSetup,
VisualizationsStart,
Expand Down Expand Up @@ -103,4 +104,4 @@ export type IncontextInsightType =
| 'chatWithSuggestions'
| 'error';

export type TabId = 'chat' | 'compose' | 'insights' | 'history' | 'trace';
export type TabId = TAB_ID;
2 changes: 2 additions & 0 deletions public/utils/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,12 @@ export enum TAB_ID {
INSIGHTS = 'insights',
HISTORY = 'history',
TRACE = 'trace',
OVERRIDE = 'override',
}

export const DEFAULT_SIDECAR_DOCKED_MODE = SIDECAR_DOCKED_MODE.RIGHT;
export const DEFAULT_SIDECAR_LEFT_OR_RIGHT_SIZE = 460;
export const OVERRIDE_SIDECAR_LEFT_OR_RIGHT_SIZE = 570;
// this is a default padding top size for sidecar when switching to takeover
export const DEFAULT_SIDECAR_TAKEOVER_PADDING_TOP_SIZE = 136;

Expand Down
Loading
Loading