From 3b2b78e9873545f7367cedd199b5c6b0ff51a42a Mon Sep 17 00:00:00 2001 From: Viduni Wickramarachchi Date: Tue, 18 Feb 2025 11:54:05 -0500 Subject: [PATCH] [Obs AI Assistant] Sharing conversations (new context menu for chat and other updates) (#206590) --- .../src/chat/chat_actions_menu.tsx | 10 - .../kbn-ai-assistant/src/chat/chat_body.tsx | 57 ++++- .../src/chat/chat_context_menu.tsx | 83 +++++++ .../kbn-ai-assistant/src/chat/chat_header.tsx | 209 ++++++++++-------- .../src/chat/chat_sharing_menu.tsx | 107 +++++++++ .../src/chat/conversation_list.tsx | 1 - .../src/hooks/use_conversations_by_date.ts | 3 +- 7 files changed, 361 insertions(+), 109 deletions(-) create mode 100644 x-pack/platform/packages/shared/kbn-ai-assistant/src/chat/chat_context_menu.tsx create mode 100644 x-pack/platform/packages/shared/kbn-ai-assistant/src/chat/chat_sharing_menu.tsx diff --git a/x-pack/platform/packages/shared/kbn-ai-assistant/src/chat/chat_actions_menu.tsx b/x-pack/platform/packages/shared/kbn-ai-assistant/src/chat/chat_actions_menu.tsx index 4a19272e8938b..dfe3fc8ff1b2f 100644 --- a/x-pack/platform/packages/shared/kbn-ai-assistant/src/chat/chat_actions_menu.tsx +++ b/x-pack/platform/packages/shared/kbn-ai-assistant/src/chat/chat_actions_menu.tsx @@ -131,16 +131,6 @@ export function ChatActionsMenu({ ), panel: 1, }, - { - name: i18n.translate('xpack.aiAssistant.chatHeader.actions.copyConversation', { - defaultMessage: 'Copy conversation', - }), - disabled: !conversationId, - onClick: () => { - toggleActionsMenu(); - onCopyConversationClick(); - }, - }, ], }, { diff --git a/x-pack/platform/packages/shared/kbn-ai-assistant/src/chat/chat_body.tsx b/x-pack/platform/packages/shared/kbn-ai-assistant/src/chat/chat_body.tsx index fa75e2dc475d8..8c10e7c096b85 100644 --- a/x-pack/platform/packages/shared/kbn-ai-assistant/src/chat/chat_body.tsx +++ b/x-pack/platform/packages/shared/kbn-ai-assistant/src/chat/chat_body.tsx @@ -140,7 +140,7 @@ export function ChatBody({ const chatService = useAIAssistantChatService(); const { - services: { uiSettings }, + services: { uiSettings, notifications }, } = useKibana(); const simulateFunctionCalling = uiSettings!.get( @@ -252,14 +252,57 @@ export function ChatBody({ }); const handleCopyConversation = () => { - const deserializedMessages = (conversation.value?.messages ?? messages).map(deserializeMessage); + try { + const deserializedMessages = (conversation.value?.messages ?? messages).map( + deserializeMessage + ); + + const content = JSON.stringify({ + title: initialTitle, + messages: deserializedMessages, + }); + + navigator.clipboard?.writeText(content || ''); + + notifications!.toasts.addSuccess({ + title: i18n.translate('xpack.aiAssistant.copyConversationSuccessToast', { + defaultMessage: 'Conversation copied to clipboard', + }), + }); + } catch (error) { + notifications!.toasts.addError(error, { + title: i18n.translate('xpack.aiAssistant.copyConversationErrorToast', { + defaultMessage: 'Could not copy conversation', + }), + }); + } + }; - const content = JSON.stringify({ - title: initialTitle, - messages: deserializedMessages, - }); + const handleCopyUrl = () => { + try { + const deserializedMessages = (conversation.value?.messages ?? messages).map( + deserializeMessage + ); - navigator.clipboard?.writeText(content || ''); + const content = JSON.stringify({ + title: initialTitle, + messages: deserializedMessages, + }); + + navigator.clipboard?.writeText(content || ''); + + notifications!.toasts.addSuccess({ + title: i18n.translate('xpack.aiAssistant.copyUrlSuccessToast', { + defaultMessage: 'URL copied to clipboard', + }), + }); + } catch (error) { + notifications!.toasts.addError(error, { + title: i18n.translate('xpack.aiAssistant.copyUrlErrorToast', { + defaultMessage: 'Could not copy URL', + }), + }); + } }; const handleActionClick = ({ diff --git a/x-pack/platform/packages/shared/kbn-ai-assistant/src/chat/chat_context_menu.tsx b/x-pack/platform/packages/shared/kbn-ai-assistant/src/chat/chat_context_menu.tsx new file mode 100644 index 0000000000000..3ebd3cab3cb8e --- /dev/null +++ b/x-pack/platform/packages/shared/kbn-ai-assistant/src/chat/chat_context_menu.tsx @@ -0,0 +1,83 @@ +/* + * 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 { + EuiButtonIcon, + EuiPopover, + EuiContextMenuItem, + EuiContextMenuPanel, + EuiToolTip, +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; + +export function ChatContextMenu({ + onCopyConversationClick, + disabled, +}: { + onCopyConversationClick: () => void; + disabled: boolean; +}) { + const [isPopoverOpen, setIsPopoverOpen] = useState(false); + + return ( + + setIsPopoverOpen(!isPopoverOpen)} + /> + + } + isOpen={isPopoverOpen} + closePopover={() => setIsPopoverOpen(false)} + anchorPosition="downCenter" + panelPaddingSize="xs" + > + { + onCopyConversationClick(); + setIsPopoverOpen(false); + }} + > + {i18n.translate('xpack.aiAssistant.chatHeader.contextMenu.copyConversation', { + defaultMessage: 'Copy conversation', + })} + , + { + navigator.clipboard.writeText(window.location.href); + setIsPopoverOpen(false); + }} + > + {i18n.translate('xpack.aiAssistant.chatHeader.contextMenu.copyURL', { + defaultMessage: 'Copy URL', + })} + , + ]} + /> + + ); +} diff --git a/x-pack/platform/packages/shared/kbn-ai-assistant/src/chat/chat_header.tsx b/x-pack/platform/packages/shared/kbn-ai-assistant/src/chat/chat_header.tsx index 2488323842a10..007c63341016e 100644 --- a/x-pack/platform/packages/shared/kbn-ai-assistant/src/chat/chat_header.tsx +++ b/x-pack/platform/packages/shared/kbn-ai-assistant/src/chat/chat_header.tsx @@ -4,6 +4,7 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ + import React, { useEffect, useState } from 'react'; import { EuiButtonIcon, @@ -23,6 +24,8 @@ import { AssistantIcon } from '@kbn/ai-assistant-icon'; import { ChatActionsMenu } from './chat_actions_menu'; import type { UseGenAIConnectorsResult } from '../hooks/use_genai_connectors'; import { FlyoutPositionMode } from './chat_flyout'; +import { ChatSharingMenu } from './chat_sharing_menu'; +import { ChatContextMenu } from './chat_context_menu'; // needed to prevent InlineTextEdit component from expanding container const minWidthClassName = css` @@ -87,7 +90,7 @@ export function ChatHeader({ className={breakpoint === 'xs' ? chatHeaderMobileClassName : chatHeaderClassName} hasBorder={false} hasShadow={false} - paddingSize={breakpoint === 'xs' ? 's' : 'm'} + paddingSize="s" > @@ -98,113 +101,139 @@ export function ChatHeader({ )} - - { - setNewTitle(e.currentTarget.nodeValue || ''); - }} - onSave={(e) => { - if (onSaveTitle) { - onSaveTitle(e); - } - }} - onCancel={() => { - setNewTitle(title); - }} - /> - + + + + { + setNewTitle(e.currentTarget.nodeValue || ''); + }} + onSave={onSaveTitle} + onCancel={() => { + setNewTitle(title); + }} + editModeProps={{ + formRowProps: { + fullWidth: true, + }, + }} + /> + - - - {flyoutPositionMode && onToggleFlyoutPositionMode ? ( - <> - - - - - } - /> - - {navigateToConversation ? ( + + + + + {conversationId ? ( + + + + ) : null} + + + + + {flyoutPositionMode && onToggleFlyoutPositionMode ? ( + <> navigateToConversation(conversationId)} + iconType={flyoutPositionMode === 'overlay' ? 'menuRight' : 'menuLeft'} + onClick={handleToggleFlyoutPositionMode} /> } /> - ) : null} - - ) : null} + {navigateToConversation ? ( + + + navigateToConversation(conversationId)} + /> + + } + /> + + ) : null} + + ) : null} - - - - - + + + + + + ); diff --git a/x-pack/platform/packages/shared/kbn-ai-assistant/src/chat/chat_sharing_menu.tsx b/x-pack/platform/packages/shared/kbn-ai-assistant/src/chat/chat_sharing_menu.tsx new file mode 100644 index 0000000000000..f71bf6195030f --- /dev/null +++ b/x-pack/platform/packages/shared/kbn-ai-assistant/src/chat/chat_sharing_menu.tsx @@ -0,0 +1,107 @@ +/* + * 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, useCallback } from 'react'; +import { + EuiPopover, + EuiBadge, + EuiSelectable, + EuiText, + EuiFlexGroup, + EuiFlexItem, + EuiSelectableOption, +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; + +interface OptionData { + description?: string; +} + +const privateLabel = i18n.translate('xpack.aiAssistant.chatHeader.shareOptions.private', { + defaultMessage: 'Private', +}); + +const sharedLabel = i18n.translate('xpack.aiAssistant.chatHeader.shareOptions.shared', { + defaultMessage: 'Shared', +}); + +export function ChatSharingMenu() { + const [selectedValue, setSelectedValue] = useState('private'); + const [isPopoverOpen, setIsPopoverOpen] = useState(false); + + const options: Array> = [ + { + key: 'private', + label: privateLabel, + checked: !selectedValue || selectedValue === 'private' ? 'on' : undefined, + description: i18n.translate('xpack.aiAssistant.chatHeader.shareOptions.privateDescription', { + defaultMessage: 'This conversation is only visible to you.', + }), + }, + { + key: 'shared', + label: sharedLabel, + checked: selectedValue === 'shared' ? 'on' : undefined, + description: i18n.translate('xpack.aiAssistant.chatHeader.shareOptions.sharedDescription', { + defaultMessage: 'Team members can view this conversation.', + }), + }, + ]; + + const renderOption = useCallback( + (option: EuiSelectableOption) => ( + + + + {option.label} + + + + {option.description} + + + ), + [] + ); + + return ( + setIsPopoverOpen(!isPopoverOpen)} + onClickAriaLabel="Toggle sharing options" + > + {selectedValue === 'shared' ? sharedLabel : privateLabel} + + } + isOpen={isPopoverOpen} + closePopover={() => setIsPopoverOpen(false)} + panelPaddingSize="none" + anchorPosition="downLeft" + > + { + const selectedOption = newOptions.find((option) => option.checked === 'on'); + if (selectedOption) setSelectedValue(selectedOption.key as string); + setIsPopoverOpen(false); + }} + listProps={{ + isVirtualized: false, + onFocusBadge: false, + textWrap: 'wrap', + }} + > + {(list) =>
{list}
} +
+
+ ); +} diff --git a/x-pack/platform/packages/shared/kbn-ai-assistant/src/chat/conversation_list.tsx b/x-pack/platform/packages/shared/kbn-ai-assistant/src/chat/conversation_list.tsx index 0b634f8ee9f7f..988f368b21e9d 100644 --- a/x-pack/platform/packages/shared/kbn-ai-assistant/src/chat/conversation_list.tsx +++ b/x-pack/platform/packages/shared/kbn-ai-assistant/src/chat/conversation_list.tsx @@ -156,7 +156,6 @@ export function ConversationList({ size="s" isActive={conversation.id === selectedConversationId} isDisabled={isLoading} - wrapText showToolTip href={conversation.href} onClick={(event) => onClickConversation(event, conversation.id)} diff --git a/x-pack/platform/packages/shared/kbn-ai-assistant/src/hooks/use_conversations_by_date.ts b/x-pack/platform/packages/shared/kbn-ai-assistant/src/hooks/use_conversations_by_date.ts index cc7e7927f381d..09ed264f7f10f 100644 --- a/x-pack/platform/packages/shared/kbn-ai-assistant/src/hooks/use_conversations_by_date.ts +++ b/x-pack/platform/packages/shared/kbn-ai-assistant/src/hooks/use_conversations_by_date.ts @@ -26,7 +26,7 @@ export function useConversationsByDate( const categorizedConversations: Record< string, - Array<{ id: string; label: string; lastUpdated: string; href?: string }> + Array<{ id: string; label: string; lastUpdated: string; href?: string; public: boolean }> > = { TODAY: [], YESTERDAY: [], @@ -49,6 +49,7 @@ export function useConversationsByDate( label: conversation.conversation.title, lastUpdated: conversation.conversation.last_updated, href: getConversationHref ? getConversationHref(conversation.conversation.id) : undefined, + public: conversation.public, }; if (lastUpdated >= startOfToday) {