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

[8.x] [Observability AI Assistant] duplicate conversations (#208044) #213167

Merged
merged 1 commit into from
Mar 5, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -25,11 +25,13 @@ export function ChatActionsMenu({
conversationId,
disabled,
onCopyConversationClick,
onDuplicateConversationClick,
}: {
connectors: UseGenAIConnectorsResult;
conversationId?: string;
disabled: boolean;
onCopyConversationClick: () => void;
onDuplicateConversationClick: () => void;
}) {
const { application, http } = useKibana().services;
const knowledgeBase = useKnowledgeBase();
Expand Down Expand Up @@ -141,6 +143,16 @@ export function ChatActionsMenu({
onCopyConversationClick();
},
},
{
name: i18n.translate('xpack.aiAssistant.chatHeader.actions.duplicateConversation', {
defaultMessage: 'Duplicate',
}),
disabled: !conversationId,
onClick: () => {
toggleActionsMenu();
onDuplicateConversationClick();
},
},
],
},
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,16 @@
*/

import {
EuiButton,
EuiCallOut,
EuiFlexGroup,
EuiFlexItem,
EuiHorizontalRule,
EuiIcon,
EuiPanel,
euiScrollBarStyles,
EuiSpacer,
EuiText,
useEuiTheme,
} from '@elastic/eui';
import { css, keyframes } from '@emotion/css';
Expand Down Expand Up @@ -45,8 +48,8 @@ import { SimulatedFunctionCallingCallout } from './simulated_function_calling_ca
import { WelcomeMessage } from './welcome_message';
import { useLicense } from '../hooks/use_license';
import { PromptEditor } from '../prompt_editor/prompt_editor';
import { deserializeMessage } from '../utils/deserialize_message';
import { useKibana } from '../hooks/use_kibana';
import { deserializeMessage } from '../utils/deserialize_message';

const fullHeightClassName = css`
height: 100%;
Expand Down Expand Up @@ -113,16 +116,18 @@ export function ChatBody({
onConversationUpdate,
onToggleFlyoutPositionMode,
navigateToConversation,
onConversationDuplicate,
}: {
connectors: ReturnType<typeof useGenAIConnectors>;
currentUser?: Pick<AuthenticatedUser, 'full_name' | 'username'>;
currentUser?: Pick<AuthenticatedUser, 'full_name' | 'username' | 'profile_uid'>;
flyoutPositionMode?: FlyoutPositionMode;
initialTitle?: string;
initialMessages?: Message[];
initialConversationId?: string;
knowledgeBase: UseKnowledgeBaseResult;
showLinkToConversationsApp: boolean;
onConversationUpdate: (conversation: { conversation: Conversation['conversation'] }) => void;
onConversationDuplicate: (conversation: Conversation) => void;
onToggleFlyoutPositionMode?: (flyoutPositionMode: FlyoutPositionMode) => void;
navigateToConversation?: (conversationId?: string) => void;
}) {
Expand All @@ -142,13 +147,26 @@ export function ChatBody({
false
);

const { conversation, messages, next, state, stop, saveTitle } = useConversation({
const {
conversation,
conversationId,
messages,
next,
state,
stop,
saveTitle,
duplicateConversation,
isConversationOwnedByCurrentUser,
user: conversationUser,
} = useConversation({
currentUser,
initialConversationId,
initialMessages,
initialTitle,
chatService,
connectorId: connectors.selectedConnector,
onConversationUpdate,
onConversationDuplicate,
});

const timelineContainerRef = useRef<HTMLDivElement | null>(null);
Expand Down Expand Up @@ -385,28 +403,65 @@ export function ChatBody({
}
/>
) : (
<ChatTimeline
messages={messages}
knowledgeBase={knowledgeBase}
chatService={chatService}
currentUser={currentUser}
chatState={state}
hasConnector={!!connectors.connectors?.length}
onEdit={(editedMessage, newMessage) => {
setStickToBottom(true);
const indexOf = messages.indexOf(editedMessage);
next(messages.slice(0, indexOf).concat(newMessage));
}}
onFeedback={handleFeedback}
onRegenerate={(message) => {
next(reverseToLastUserMessage(messages, message));
}}
onSendTelemetry={(eventWithPayload) =>
chatService.sendAnalyticsEvent(eventWithPayload)
}
onStopGenerating={stop}
onActionClick={handleActionClick}
/>
<>
<ChatTimeline
conversationId={conversationId}
messages={messages}
knowledgeBase={knowledgeBase}
chatService={chatService}
currentUser={conversationUser}
isConversationOwnedByCurrentUser={isConversationOwnedByCurrentUser}
chatState={state}
hasConnector={!!connectors.connectors?.length}
onEdit={(editedMessage, newMessage) => {
setStickToBottom(true);
const indexOf = messages.indexOf(editedMessage);
next(messages.slice(0, indexOf).concat(newMessage));
}}
onFeedback={handleFeedback}
onRegenerate={(message) => {
next(reverseToLastUserMessage(messages, message));
}}
onSendTelemetry={(eventWithPayload) =>
chatService.sendAnalyticsEvent(eventWithPayload)
}
onStopGenerating={stop}
onActionClick={handleActionClick}
/>
{conversationId && !isConversationOwnedByCurrentUser ? (
<>
<EuiPanel paddingSize="m" hasShadow={false} color="subdued">
<EuiFlexGroup>
<EuiFlexItem grow={false}>
<EuiIcon size="l" type="users" />
</EuiFlexItem>
<EuiFlexItem grow>
<EuiText size="xs">
<h3>
{i18n.translate('xpack.aiAssistant.sharedBanner.title', {
defaultMessage: 'This conversation is shared with your team.',
})}
</h3>
<p>
{i18n.translate('xpack.aiAssistant.sharedBanner.description', {
defaultMessage: `You can’t edit or continue this conversation, but you can duplicate
it into a new private conversation. The original conversation will
remain unchanged.`,
})}
</p>
<EuiButton onClick={duplicateConversation} iconType="copy" size="s">
{i18n.translate('xpack.aiAssistant.duplicateButton', {
defaultMessage: 'Duplicate',
})}
</EuiButton>
</EuiText>
</EuiFlexItem>
</EuiFlexGroup>
</EuiPanel>
<EuiSpacer size="m" />
</>
) : null}
</>
)}
</EuiPanel>
</div>
Expand All @@ -432,7 +487,11 @@ export function ChatBody({
className={promptEditorContainerClassName}
>
<PromptEditor
disabled={!connectors.selectedConnector || !hasCorrectLicense}
disabled={
!connectors.selectedConnector ||
!hasCorrectLicense ||
(!!conversationId && !isConversationOwnedByCurrentUser)
}
hidden={connectors.loading || connectors.connectors?.length === 0}
loading={isLoading}
onChangeHeight={handleChangeHeight}
Expand Down Expand Up @@ -509,23 +568,21 @@ export function ChatBody({
<EuiFlexItem grow={false} className={headerContainerClassName}>
<ChatHeader
connectors={connectors}
conversationId={
conversation.value?.conversation && 'id' in conversation.value.conversation
? conversation.value.conversation.id
: undefined
}
conversationId={conversationId}
flyoutPositionMode={flyoutPositionMode}
licenseInvalid={!hasCorrectLicense && !initialConversationId}
loading={isLoading}
title={title}
onCopyConversation={handleCopyConversation}
onDuplicateConversation={duplicateConversation}
onSaveTitle={(newTitle) => {
saveTitle(newTitle);
}}
onToggleFlyoutPositionMode={onToggleFlyoutPositionMode}
navigateToConversation={
initialMessages?.length && !initialConversationId ? undefined : navigateToConversation
}
isConversationOwnedByCurrentUser={isConversationOwnedByCurrentUser}
/>
</EuiFlexItem>
<EuiFlexItem grow={false}>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ const noPanelStyle = css`

export function ChatConsolidatedItems({
consolidatedItem,
isConversationOwnedByCurrentUser,
onActionClick,
onEditSubmit,
onFeedback,
Expand All @@ -57,6 +58,7 @@ export function ChatConsolidatedItems({
onStopGenerating,
}: {
consolidatedItem: ChatTimelineItem[];
isConversationOwnedByCurrentUser: ChatTimelineProps['isConversationOwnedByCurrentUser'];
onActionClick: ChatTimelineProps['onActionClick'];
onEditSubmit: ChatTimelineProps['onEdit'];
onFeedback: ChatTimelineProps['onFeedback'];
Expand Down Expand Up @@ -134,6 +136,7 @@ export function ChatConsolidatedItems({
}}
onSendTelemetry={onSendTelemetry}
onStopGeneratingClick={onStopGenerating}
isConversationOwnedByCurrentUser={isConversationOwnedByCurrentUser}
/>
))
: null}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import {
} from '@elastic/eui';
import { css } from '@emotion/css';
import { i18n } from '@kbn/i18n';
import { Message } from '@kbn/observability-ai-assistant-plugin/common';
import { Conversation, Message } from '@kbn/observability-ai-assistant-plugin/common';
import React, { useState } from 'react';
import ReactDOM from 'react-dom';
import { useConversationKey } from '../hooks/use_conversation_key';
Expand All @@ -42,7 +42,7 @@ export enum FlyoutPositionMode {

export function ChatFlyout({
initialTitle,
initialMessages,
initialMessages: initialMessagesFromProps,
initialFlyoutPositionMode,
onFlyoutPositionModeChange,
isOpen,
Expand All @@ -69,6 +69,7 @@ export function ChatFlyout({
const knowledgeBase = useKnowledgeBase();

const [conversationId, setConversationId] = useState<string | undefined>(undefined);
const [initialMessages, setInitialMessages] = useState(initialMessagesFromProps);

const [flyoutPositionMode, setFlyoutPositionMode] = useState<FlyoutPositionMode>(
initialFlyoutPositionMode || FlyoutPositionMode.OVERLAY
Expand All @@ -88,6 +89,12 @@ export function ChatFlyout({

const { key: bodyKey, updateConversationIdInPlace } = useConversationKey(conversationId);

const onConversationDuplicate = (conversation: Conversation) => {
conversationList.conversations.refresh();
setInitialMessages([]);
setConversationId(conversation.conversation.id);
};

const flyoutClassName = css`
max-inline-size: 100% !important;
`;
Expand Down Expand Up @@ -287,6 +294,7 @@ export function ChatFlyout({
}
: undefined
}
onConversationDuplicate={onConversationDuplicate}
/>
</EuiFlexItem>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,9 @@ export function ChatHeader({
licenseInvalid,
loading,
title,
isConversationOwnedByCurrentUser,
onCopyConversation,
onDuplicateConversation,
onSaveTitle,
onToggleFlyoutPositionMode,
navigateToConversation,
Expand All @@ -57,7 +59,9 @@ export function ChatHeader({
licenseInvalid: boolean;
loading: boolean;
title: string;
isConversationOwnedByCurrentUser: boolean;
onCopyConversation: () => void;
onDuplicateConversation: () => void;
onSaveTitle: (title: string) => void;
onToggleFlyoutPositionMode?: (newFlyoutPositionMode: FlyoutPositionMode) => void;
navigateToConversation?: (nextConversationId?: string) => void;
Expand Down Expand Up @@ -113,7 +117,8 @@ export function ChatHeader({
!conversationId ||
!connectors.selectedConnector ||
licenseInvalid ||
!Boolean(onSaveTitle)
!Boolean(onSaveTitle) ||
!isConversationOwnedByCurrentUser
}
onChange={(e) => {
setNewTitle(e.currentTarget.nodeValue || '');
Expand Down Expand Up @@ -199,6 +204,7 @@ export function ChatHeader({
conversationId={conversationId}
disabled={licenseInvalid}
onCopyConversationClick={onCopyConversation}
onDuplicateConversationClick={onDuplicateConversation}
/>
</EuiFlexItem>
</EuiFlexGroup>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ export interface ChatItemProps extends Omit<ChatTimelineItem, 'message'> {
onRegenerateClick: () => void;
onSendTelemetry: (eventWithPayload: TelemetryEventTypeWithPayload) => void;
onStopGeneratingClick: () => void;
isConversationOwnedByCurrentUser: boolean;
}

const moreCompactHeaderClassName = css`
Expand Down Expand Up @@ -87,6 +88,7 @@ export function ChatItem({
error,
loading,
title,
isConversationOwnedByCurrentUser,
onActionClick,
onEditSubmit,
onFeedbackClick,
Expand Down Expand Up @@ -167,7 +169,11 @@ export function ChatItem({
return (
<EuiComment
timelineAvatar={<ChatItemAvatar loading={loading} currentUser={currentUser} role={role} />}
username={getRoleTranslation(role)}
username={getRoleTranslation({
role,
isCurrentUser: isConversationOwnedByCurrentUser,
username: currentUser?.username,
})}
event={title}
actions={
<ChatItemActions
Expand Down
Loading