From 3fa5f5390ee299d66d523238fe889437df8e23f7 Mon Sep 17 00:00:00 2001 From: Hailong Cui Date: Fri, 3 Nov 2023 13:14:53 +0800 Subject: [PATCH] Chat window header (#3) * Chat window header Signed-off-by: Hailong Cui * support chat title Signed-off-by: Hailong Cui * dock bottom and right icon update Signed-off-by: Hailong Cui * chat window style Signed-off-by: Hailong Cui * fix new conversation don't show when in trace page Signed-off-by: Hailong Cui --------- Signed-off-by: Hailong Cui --- public/chat_flyout.tsx | 13 +- public/chat_header_button.tsx | 8 +- public/contexts/chat_context.tsx | 5 +- public/hooks/use_chat_actions.tsx | 6 +- public/index.scss | 21 +--- public/tabs/chat/chat_page.tsx | 3 +- public/tabs/chat/chat_page_content.tsx | 2 +- public/tabs/chat_tab_bar.tsx | 65 ---------- public/tabs/chat_window_header.tsx | 145 ++++++++++++++++++++++ public/tabs/history/chat_history_page.tsx | 4 +- public/types.ts | 4 +- server/routes/chat_routes.ts | 15 ++- 12 files changed, 187 insertions(+), 104 deletions(-) delete mode 100644 public/tabs/chat_tab_bar.tsx create mode 100644 public/tabs/chat_window_header.tsx diff --git a/public/chat_flyout.tsx b/public/chat_flyout.tsx index 655aa40b..63ed427c 100644 --- a/public/chat_flyout.tsx +++ b/public/chat_flyout.tsx @@ -8,7 +8,7 @@ import cs from 'classnames'; import React from 'react'; import { useChatContext } from './contexts/chat_context'; import { ChatPage } from './tabs/chat/chat_page'; -import { ChatTabBar } from './tabs/chat_tab_bar'; +import { ChatWindowHeader } from './tabs/chat_window_header'; import { ChatHistoryPage } from './tabs/history/chat_history_page'; let chatHistoryPageLoaded = false; @@ -59,16 +59,17 @@ export const ChatFlyout: React.FC = (props) => { {...props.flyoutProps} > <> - {props.overrideComponent} - - + + + {props.overrideComponent} + + {chatHistoryPageLoaded && ( = (props) => { const [appId, setAppId] = useState(); const [sessionId, setSessionId] = useState(); + const [title, setTitle] = useState(); const [flyoutVisible, setFlyoutVisible] = useState(false); const [flyoutComponent, setFlyoutComponent] = useState(null); const [flyoutProps, setFlyoutProps] = useState>>( @@ -62,6 +62,8 @@ export const HeaderChatButton: React.FC = (props) => { contentRenderers: props.contentRenderers, actionExecutors: props.actionExecutors, currentAccount: props.currentAccount, + title, + setTitle, }), [ appId, @@ -72,6 +74,8 @@ export const HeaderChatButton: React.FC = (props) => { props.contentRenderers, props.actionExecutors, props.currentAccount, + title, + setTitle, ] ); diff --git a/public/contexts/chat_context.tsx b/public/contexts/chat_context.tsx index 240fe9c4..877e895a 100644 --- a/public/contexts/chat_context.tsx +++ b/public/contexts/chat_context.tsx @@ -4,8 +4,7 @@ */ import React, { useContext } from 'react'; -import { TabId } from '../tabs/chat_tab_bar'; -import { ActionExecutor, ContentRenderer, UserAccount } from '../types'; +import { ActionExecutor, ContentRenderer, UserAccount, TabId } from '../types'; export interface IChatContext { appId?: string; @@ -20,6 +19,8 @@ export interface IChatContext { contentRenderers: Record; actionExecutors: Record; currentAccount: UserAccount; + title?: string; + setTitle: React.Dispatch>; } export const ChatContext = React.createContext(null); diff --git a/public/hooks/use_chat_actions.tsx b/public/hooks/use_chat_actions.tsx index 902f5129..d1ccac35 100644 --- a/public/hooks/use_chat_actions.tsx +++ b/public/hooks/use_chat_actions.tsx @@ -12,6 +12,7 @@ import { useChatState } from './use_chat_state'; interface SendResponse { sessionId: string; + title: string; messages: IMessage[]; } @@ -37,6 +38,7 @@ export const useChatActions = (): AssistantActions => { }); if (abortController.signal.aborted) return; chatContext.setSessionId(response.sessionId); + chatContext.setTitle(response.title); chatStateDispatch({ type: 'receive', payload: response.messages }); } catch (error) { if (abortController.signal.aborted) return; @@ -44,10 +46,12 @@ export const useChatActions = (): AssistantActions => { } }; - const loadChat = (sessionId?: string) => { + const loadChat = (sessionId?: string, title?: string) => { abortControllerRef?.abort(); chatContext.setSessionId(sessionId); + chatContext.setTitle(title); chatContext.setSelectedTabId('chat'); + chatContext.setFlyoutComponent(null); if (!sessionId) chatStateDispatch({ type: 'reset' }); }; diff --git a/public/index.scss b/public/index.scss index 5c97b24a..9b7eeb29 100644 --- a/public/index.scss +++ b/public/index.scss @@ -28,24 +28,9 @@ } } -.llm-chat-flyout-header { - background: #e6f0f8; -} - -.llm-chat-tabs { - justify-content: left; - - .euiTab-isSelected { - font-weight: 700; - } - - .euiTab { - color: $ouiLinkColor; - &:hover, - &:focus { - text-decoration: none; - } - } +.llm-chat-flyout-body { + background-color: $euiPageBackgroundColor; + margin: 0px 20px; } .euiPanel { diff --git a/public/tabs/chat/chat_page.tsx b/public/tabs/chat/chat_page.tsx index a4c192f8..47439b67 100644 --- a/public/tabs/chat/chat_page.tsx +++ b/public/tabs/chat/chat_page.tsx @@ -5,6 +5,7 @@ import { EuiFlyoutBody, EuiFlyoutFooter, EuiPage, EuiPageBody, EuiSpacer } from '@elastic/eui'; import React, { useEffect, useState } from 'react'; +import cs from 'classnames'; import { useObservable } from 'react-use'; import { useChatContext } from '../../contexts/chat_context'; import { useChatState } from '../../hooks/use_chat_state'; @@ -41,7 +42,7 @@ export const ChatPage: React.FC = (props) => { return ( <> - + = React.memo((props - , + {props.showGreetings && props.setShowGreetings(false)} />} {chatState.messages .flatMap((message, i, array) => [ diff --git a/public/tabs/chat_tab_bar.tsx b/public/tabs/chat_tab_bar.tsx deleted file mode 100644 index 7e6f622f..00000000 --- a/public/tabs/chat_tab_bar.tsx +++ /dev/null @@ -1,65 +0,0 @@ -/* - * Copyright OpenSearch Contributors - * SPDX-License-Identifier: Apache-2.0 - */ - -import { - EuiButtonEmpty, - EuiButtonIcon, - EuiFlexGroup, - EuiFlexItem, - EuiTab, - EuiTabs, -} from '@elastic/eui'; -import React from 'react'; -import { useChatContext } from '../contexts/chat_context'; -import { useChatActions } from '../hooks/use_chat_actions'; - -export type TabId = 'chat' | 'compose' | 'insights' | 'history'; - -const tabs = [ - { id: 'chat', name: 'Chat' }, - { id: 'history', name: 'History' }, -] as const; - -interface ChatTabBarProps { - flyoutFullScreen: boolean; - toggleFlyoutFullScreen: () => void; -} - -export const ChatTabBar: React.FC = React.memo((props) => { - const chatContext = useChatContext(); - const { loadChat } = useChatActions(); - const tabsComponent = tabs.map((tab) => ( - chatContext.setSelectedTabId(tab.id)} - isSelected={tab.id === chatContext.selectedTabId} - key={tab.id} - disabled={!chatContext.chatEnabled} - > - {tab.name} - - )); - - return ( - - - {tabsComponent} - - - loadChat(undefined)}> - New chat - - - - - - - - ); -}); diff --git a/public/tabs/chat_window_header.tsx b/public/tabs/chat_window_header.tsx new file mode 100644 index 00000000..2d8b35b9 --- /dev/null +++ b/public/tabs/chat_window_header.tsx @@ -0,0 +1,145 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { + EuiButtonEmpty, + EuiButtonIcon, + EuiContextMenuItem, + EuiContextMenuPanel, + EuiFlexGroup, + EuiFlexItem, + EuiPopover, +} from '@elastic/eui'; +import React, { useState } from 'react'; +import { useChatContext } from '../contexts/chat_context'; +import { useChatActions } from '../hooks/use_chat_actions'; + +interface ChatWindowHeaderProps { + flyoutFullScreen: boolean; + toggleFlyoutFullScreen: () => void; +} + +export const ChatWindowHeader: React.FC = React.memo((props) => { + const chatContext = useChatContext(); + const { loadChat } = useChatActions(); + const [isPopoverOpen, setPopover] = useState(false); + + const onButtonClick = () => { + setPopover(!isPopoverOpen); + }; + + const closePopover = () => { + setPopover(false); + }; + + const dockBottom = () => ( + + + + + + + ); + + const dockRight = () => ( + + + + + + + ); + + const button = ( + + {chatContext.title || 'OpenSearch Assistant'} + + ); + + const items = [ + { + closePopover(); + }} + > + Rename conversation + , + { + closePopover(); + loadChat(undefined); + }} + > + New conversation + , + { + closePopover(); + }} + > + Save as notebook + , + ]; + + return ( + + + + + + + + + + { + chatContext.setSelectedTabId('history'); + }} + /> + + + + + + + + { + chatContext.setFlyoutVisible(false); + }} + /> + + + + ); +}); diff --git a/public/tabs/history/chat_history_page.tsx b/public/tabs/history/chat_history_page.tsx index a253be8f..212230dc 100644 --- a/public/tabs/history/chat_history_page.tsx +++ b/public/tabs/history/chat_history_page.tsx @@ -64,7 +64,9 @@ export const ChatHistoryPage: React.FC = (props) => { { field: 'id', name: 'Chat', - render: (id: string, item) => loadChat(id)}>{item.title}, + render: (id: string, item) => ( + loadChat(id, item.title)}>{item.title} + ), }, { field: 'updatedTimeMs', diff --git a/public/types.ts b/public/types.ts index a140952a..4e592a8d 100644 --- a/public/types.ts +++ b/public/types.ts @@ -12,7 +12,7 @@ export type ContentRenderer = (content: unknown) => React.ReactElement; export type ActionExecutor = (params: Record) => void; export interface AssistantActions { send: (input: IMessage) => void; - loadChat: (sessionId?: string) => void; + loadChat: (sessionId?: string, title?: string) => void; openChatUI: (sessionId?: string) => void; executeAction: (suggestedAction: ISuggestedAction, message: IMessage) => void; } @@ -43,3 +43,5 @@ export interface UserAccount { export interface ChatConfig { terms_accepted: boolean; } + +export type TabId = 'chat' | 'compose' | 'insights' | 'history'; diff --git a/server/routes/chat_routes.ts b/server/routes/chat_routes.ts index 079181c2..8382dd2a 100644 --- a/server/routes/chat_routes.ts +++ b/server/routes/chat_routes.ts @@ -86,12 +86,15 @@ export function registerChatRoutes(router: IRouter) { try { const outputs = await chatService.requestLLM(messages, context, request); - const saveMessagesResponse = await storageService.saveMessages( - input.content.substring(0, 50), - sessionId, - [...messages, input, ...outputs] - ); - return response.ok({ body: saveMessagesResponse }); + const title = input.content.substring(0, 50); + const saveMessagesResponse = await storageService.saveMessages(title, sessionId, [ + ...messages, + input, + ...outputs, + ]); + return response.ok({ + body: { ...saveMessagesResponse, title }, + }); } catch (error) { context.assistant_plugin.logger.warn(error); return response.custom({ statusCode: error.statusCode || 500, body: error.message });