Skip to content

Commit

Permalink
[8.x] [Observability AI Assistant] duplicate conversations (#208044) (#…
Browse files Browse the repository at this point in the history
…213167)

# Backport

This will backport the following commits from `main` to `8.x`:
- [[Observability AI Assistant] duplicate conversations
(#208044)](#208044)

<!--- Backport version: 9.6.6 -->

### Questions ?
Please refer to the [Backport tool
documentation](https://github.com/sorenlouv/backport)

<!--BACKPORT [{"author":{"name":"Arturo
Lidueña","email":"arturo.liduena@elastic.co"},"sourceCommit":{"committedDate":"2025-03-04T22:15:37Z","message":"[Observability
AI Assistant] duplicate conversations (#208044)\n\nCloses #209382\n\n###
Summary:\n\n#### Duplicate Conversation \n- **Readonly** → Public
conversations can only be modified by the owner.\n- Duplicated
conversations are **owned** by the user who duplicates\nthem.\n-
Duplicated conversations are **private** by default `public: false`. \n
\n\nhttps://github.com/user-attachments/assets/9a2d1727-aa0d-4d8f-a886-727c0ce1578c\n\nUPDATE:\n\n\nhttps://github.com/user-attachments/assets/ee3282e8-5ae8-445d-9368-928dd59cfb75\n\n###
Checklist\n\nCheck the PR satisfies following conditions. \n\nReviewers
should verify this PR satisfies this list as well.\n\n- [ ] Any text
added follows [EUI's
writing\nguidelines](https://elastic.github.io/eui/#/guidelines/writing),
uses\nsentence case text and includes
[i18n\nsupport](https://github.com/elastic/kibana/blob/main/src/platform/packages/shared/kbn-i18n/README.md)\n-
[
]\n[Documentation](https://www.elastic.co/guide/en/kibana/master/development-documentation.html)\nwas
added for features that require explanation or tutorials\n- [ ] [Unit or
functional\ntests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)\nwere
updated or added to match the most common scenarios\n- [ ] If a plugin
configuration key changed, check if it needs to be\nallowlisted in the
cloud and added to the
[docker\nlist](https://github.com/elastic/kibana/blob/main/src/dev/build/tasks/os_packages/docker_generator/resources/base/bin/kibana-docker)\n-
[ ] This was checked for breaking HTTP API changes, and any
breaking\nchanges have been approved by the breaking-change committee.
The\n`release_note:breaking` label should be applied in these
situations.\n- [ ] [Flaky
Test\nRunner](https://ci-stats.kibana.dev/trigger_flaky_test_runner/1)
was\nused on any tests changed\n- [ ] The PR description includes the
appropriate Release Notes section,\nand the correct `release_note:*`
label is applied per
the\n[guidelines](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process)","sha":"b331fa1c53f817c0e9c372ab4e5939551550ab9c","branchLabelMapping":{"^v9.1.0$":"main","^v8.19.0$":"8.x","^v(\\d+).(\\d+).\\d+$":"$1.$2"}},"sourcePullRequest":{"labels":["release_note:feature","Team:Obs
AI
Assistant","ci:project-deploy-observability","backport:version","v9.1.0","v8.19.0"],"title":"[Observability
AI Assistant] duplicate
conversations","number":208044,"url":"https://github.com/elastic/kibana/pull/208044","mergeCommit":{"message":"[Observability
AI Assistant] duplicate conversations (#208044)\n\nCloses #209382\n\n###
Summary:\n\n#### Duplicate Conversation \n- **Readonly** → Public
conversations can only be modified by the owner.\n- Duplicated
conversations are **owned** by the user who duplicates\nthem.\n-
Duplicated conversations are **private** by default `public: false`. \n
\n\nhttps://github.com/user-attachments/assets/9a2d1727-aa0d-4d8f-a886-727c0ce1578c\n\nUPDATE:\n\n\nhttps://github.com/user-attachments/assets/ee3282e8-5ae8-445d-9368-928dd59cfb75\n\n###
Checklist\n\nCheck the PR satisfies following conditions. \n\nReviewers
should verify this PR satisfies this list as well.\n\n- [ ] Any text
added follows [EUI's
writing\nguidelines](https://elastic.github.io/eui/#/guidelines/writing),
uses\nsentence case text and includes
[i18n\nsupport](https://github.com/elastic/kibana/blob/main/src/platform/packages/shared/kbn-i18n/README.md)\n-
[
]\n[Documentation](https://www.elastic.co/guide/en/kibana/master/development-documentation.html)\nwas
added for features that require explanation or tutorials\n- [ ] [Unit or
functional\ntests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)\nwere
updated or added to match the most common scenarios\n- [ ] If a plugin
configuration key changed, check if it needs to be\nallowlisted in the
cloud and added to the
[docker\nlist](https://github.com/elastic/kibana/blob/main/src/dev/build/tasks/os_packages/docker_generator/resources/base/bin/kibana-docker)\n-
[ ] This was checked for breaking HTTP API changes, and any
breaking\nchanges have been approved by the breaking-change committee.
The\n`release_note:breaking` label should be applied in these
situations.\n- [ ] [Flaky
Test\nRunner](https://ci-stats.kibana.dev/trigger_flaky_test_runner/1)
was\nused on any tests changed\n- [ ] The PR description includes the
appropriate Release Notes section,\nand the correct `release_note:*`
label is applied per
the\n[guidelines](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process)","sha":"b331fa1c53f817c0e9c372ab4e5939551550ab9c"}},"sourceBranch":"main","suggestedTargetBranches":["8.x"],"targetPullRequestStates":[{"branch":"main","label":"v9.1.0","branchLabelMappingKey":"^v9.1.0$","isSourceBranch":true,"state":"MERGED","url":"https://github.com/elastic/kibana/pull/208044","number":208044,"mergeCommit":{"message":"[Observability
AI Assistant] duplicate conversations (#208044)\n\nCloses #209382\n\n###
Summary:\n\n#### Duplicate Conversation \n- **Readonly** → Public
conversations can only be modified by the owner.\n- Duplicated
conversations are **owned** by the user who duplicates\nthem.\n-
Duplicated conversations are **private** by default `public: false`. \n
\n\nhttps://github.com/user-attachments/assets/9a2d1727-aa0d-4d8f-a886-727c0ce1578c\n\nUPDATE:\n\n\nhttps://github.com/user-attachments/assets/ee3282e8-5ae8-445d-9368-928dd59cfb75\n\n###
Checklist\n\nCheck the PR satisfies following conditions. \n\nReviewers
should verify this PR satisfies this list as well.\n\n- [ ] Any text
added follows [EUI's
writing\nguidelines](https://elastic.github.io/eui/#/guidelines/writing),
uses\nsentence case text and includes
[i18n\nsupport](https://github.com/elastic/kibana/blob/main/src/platform/packages/shared/kbn-i18n/README.md)\n-
[
]\n[Documentation](https://www.elastic.co/guide/en/kibana/master/development-documentation.html)\nwas
added for features that require explanation or tutorials\n- [ ] [Unit or
functional\ntests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)\nwere
updated or added to match the most common scenarios\n- [ ] If a plugin
configuration key changed, check if it needs to be\nallowlisted in the
cloud and added to the
[docker\nlist](https://github.com/elastic/kibana/blob/main/src/dev/build/tasks/os_packages/docker_generator/resources/base/bin/kibana-docker)\n-
[ ] This was checked for breaking HTTP API changes, and any
breaking\nchanges have been approved by the breaking-change committee.
The\n`release_note:breaking` label should be applied in these
situations.\n- [ ] [Flaky
Test\nRunner](https://ci-stats.kibana.dev/trigger_flaky_test_runner/1)
was\nused on any tests changed\n- [ ] The PR description includes the
appropriate Release Notes section,\nand the correct `release_note:*`
label is applied per
the\n[guidelines](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process)","sha":"b331fa1c53f817c0e9c372ab4e5939551550ab9c"}},{"branch":"8.x","label":"v8.19.0","branchLabelMappingKey":"^v8.19.0$","isSourceBranch":false,"state":"NOT_CREATED"}]}]
BACKPORT-->

Co-authored-by: Arturo Lidueña <arturo.liduena@elastic.co>
  • Loading branch information
kibanamachine and arturoliduena authored Mar 5, 2025
1 parent 4850d36 commit f2f5660
Show file tree
Hide file tree
Showing 18 changed files with 707 additions and 132 deletions.
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

0 comments on commit f2f5660

Please sign in to comment.