Skip to content

Commit b331fa1

Browse files
[Observability AI Assistant] duplicate conversations (elastic#208044)
Closes elastic#209382 ### Summary: #### Duplicate Conversation - **Readonly** → Public conversations can only be modified by the owner. - Duplicated conversations are **owned** by the user who duplicates them. - Duplicated conversations are **private** by default `public: false`. https://github.com/user-attachments/assets/9a2d1727-aa0d-4d8f-a886-727c0ce1578c UPDATE: https://github.com/user-attachments/assets/ee3282e8-5ae8-445d-9368-928dd59cfb75 ### Checklist Check the PR satisfies following conditions. Reviewers should verify this PR satisfies this list as well. - [ ] Any text added follows [EUI's writing guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses sentence case text and includes [i18n support](https://github.com/elastic/kibana/blob/main/src/platform/packages/shared/kbn-i18n/README.md) - [ ] [Documentation](https://www.elastic.co/guide/en/kibana/master/development-documentation.html) was added for features that require explanation or tutorials - [ ] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios - [ ] If a plugin configuration key changed, check if it needs to be allowlisted in the cloud and added to the [docker list](https://github.com/elastic/kibana/blob/main/src/dev/build/tasks/os_packages/docker_generator/resources/base/bin/kibana-docker) - [ ] This was checked for breaking HTTP API changes, and any breaking changes have been approved by the breaking-change committee. The `release_note:breaking` label should be applied in these situations. - [ ] [Flaky Test Runner](https://ci-stats.kibana.dev/trigger_flaky_test_runner/1) was used on any tests changed - [ ] The PR description includes the appropriate Release Notes section, and the correct `release_note:*` label is applied per the [guidelines](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process)
1 parent df59c26 commit b331fa1

File tree

18 files changed

+707
-132
lines changed

18 files changed

+707
-132
lines changed

x-pack/platform/packages/shared/kbn-ai-assistant/src/chat/chat_actions_menu.tsx

+12
Original file line numberDiff line numberDiff line change
@@ -25,11 +25,13 @@ export function ChatActionsMenu({
2525
conversationId,
2626
disabled,
2727
onCopyConversationClick,
28+
onDuplicateConversationClick,
2829
}: {
2930
connectors: UseGenAIConnectorsResult;
3031
conversationId?: string;
3132
disabled: boolean;
3233
onCopyConversationClick: () => void;
34+
onDuplicateConversationClick: () => void;
3335
}) {
3436
const { application, http } = useKibana().services;
3537
const knowledgeBase = useKnowledgeBase();
@@ -141,6 +143,16 @@ export function ChatActionsMenu({
141143
onCopyConversationClick();
142144
},
143145
},
146+
{
147+
name: i18n.translate('xpack.aiAssistant.chatHeader.actions.duplicateConversation', {
148+
defaultMessage: 'Duplicate',
149+
}),
150+
disabled: !conversationId,
151+
onClick: () => {
152+
toggleActionsMenu();
153+
onDuplicateConversationClick();
154+
},
155+
},
144156
],
145157
},
146158
{

x-pack/platform/packages/shared/kbn-ai-assistant/src/chat/chat_body.tsx

+88-31
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,17 @@
66
*/
77

88
import {
9+
EuiButton,
910
EuiCallOut,
1011
euiCanAnimate,
1112
EuiFlexGroup,
1213
EuiFlexItem,
1314
EuiHorizontalRule,
15+
EuiIcon,
1416
EuiPanel,
1517
euiScrollBarStyles,
1618
EuiSpacer,
19+
EuiText,
1720
useEuiTheme,
1821
UseEuiTheme,
1922
} from '@elastic/eui';
@@ -46,8 +49,8 @@ import { SimulatedFunctionCallingCallout } from './simulated_function_calling_ca
4649
import { WelcomeMessage } from './welcome_message';
4750
import { useLicense } from '../hooks/use_license';
4851
import { PromptEditor } from '../prompt_editor/prompt_editor';
49-
import { deserializeMessage } from '../utils/deserialize_message';
5052
import { useKibana } from '../hooks/use_kibana';
53+
import { deserializeMessage } from '../utils/deserialize_message';
5154

5255
const fullHeightClassName = css`
5356
height: 100%;
@@ -118,16 +121,18 @@ export function ChatBody({
118121
onConversationUpdate,
119122
onToggleFlyoutPositionMode,
120123
navigateToConversation,
124+
onConversationDuplicate,
121125
}: {
122126
connectors: ReturnType<typeof useGenAIConnectors>;
123-
currentUser?: Pick<AuthenticatedUser, 'full_name' | 'username'>;
127+
currentUser?: Pick<AuthenticatedUser, 'full_name' | 'username' | 'profile_uid'>;
124128
flyoutPositionMode?: FlyoutPositionMode;
125129
initialTitle?: string;
126130
initialMessages?: Message[];
127131
initialConversationId?: string;
128132
knowledgeBase: UseKnowledgeBaseResult;
129133
showLinkToConversationsApp: boolean;
130134
onConversationUpdate: (conversation: { conversation: Conversation['conversation'] }) => void;
135+
onConversationDuplicate: (conversation: Conversation) => void;
131136
onToggleFlyoutPositionMode?: (flyoutPositionMode: FlyoutPositionMode) => void;
132137
navigateToConversation?: (conversationId?: string) => void;
133138
}) {
@@ -148,13 +153,26 @@ export function ChatBody({
148153
false
149154
);
150155

151-
const { conversation, messages, next, state, stop, saveTitle } = useConversation({
156+
const {
157+
conversation,
158+
conversationId,
159+
messages,
160+
next,
161+
state,
162+
stop,
163+
saveTitle,
164+
duplicateConversation,
165+
isConversationOwnedByCurrentUser,
166+
user: conversationUser,
167+
} = useConversation({
168+
currentUser,
152169
initialConversationId,
153170
initialMessages,
154171
initialTitle,
155172
chatService,
156173
connectorId: connectors.selectedConnector,
157174
onConversationUpdate,
175+
onConversationDuplicate,
158176
});
159177

160178
const timelineContainerRef = useRef<HTMLDivElement | null>(null);
@@ -391,28 +409,65 @@ export function ChatBody({
391409
}
392410
/>
393411
) : (
394-
<ChatTimeline
395-
messages={messages}
396-
knowledgeBase={knowledgeBase}
397-
chatService={chatService}
398-
currentUser={currentUser}
399-
chatState={state}
400-
hasConnector={!!connectors.connectors?.length}
401-
onEdit={(editedMessage, newMessage) => {
402-
setStickToBottom(true);
403-
const indexOf = messages.indexOf(editedMessage);
404-
next(messages.slice(0, indexOf).concat(newMessage));
405-
}}
406-
onFeedback={handleFeedback}
407-
onRegenerate={(message) => {
408-
next(reverseToLastUserMessage(messages, message));
409-
}}
410-
onSendTelemetry={(eventWithPayload) =>
411-
chatService.sendAnalyticsEvent(eventWithPayload)
412-
}
413-
onStopGenerating={stop}
414-
onActionClick={handleActionClick}
415-
/>
412+
<>
413+
<ChatTimeline
414+
conversationId={conversationId}
415+
messages={messages}
416+
knowledgeBase={knowledgeBase}
417+
chatService={chatService}
418+
currentUser={conversationUser}
419+
isConversationOwnedByCurrentUser={isConversationOwnedByCurrentUser}
420+
chatState={state}
421+
hasConnector={!!connectors.connectors?.length}
422+
onEdit={(editedMessage, newMessage) => {
423+
setStickToBottom(true);
424+
const indexOf = messages.indexOf(editedMessage);
425+
next(messages.slice(0, indexOf).concat(newMessage));
426+
}}
427+
onFeedback={handleFeedback}
428+
onRegenerate={(message) => {
429+
next(reverseToLastUserMessage(messages, message));
430+
}}
431+
onSendTelemetry={(eventWithPayload) =>
432+
chatService.sendAnalyticsEvent(eventWithPayload)
433+
}
434+
onStopGenerating={stop}
435+
onActionClick={handleActionClick}
436+
/>
437+
{conversationId && !isConversationOwnedByCurrentUser ? (
438+
<>
439+
<EuiPanel paddingSize="m" hasShadow={false} color="subdued">
440+
<EuiFlexGroup>
441+
<EuiFlexItem grow={false}>
442+
<EuiIcon size="l" type="users" />
443+
</EuiFlexItem>
444+
<EuiFlexItem grow>
445+
<EuiText size="xs">
446+
<h3>
447+
{i18n.translate('xpack.aiAssistant.sharedBanner.title', {
448+
defaultMessage: 'This conversation is shared with your team.',
449+
})}
450+
</h3>
451+
<p>
452+
{i18n.translate('xpack.aiAssistant.sharedBanner.description', {
453+
defaultMessage: `You can’t edit or continue this conversation, but you can duplicate
454+
it into a new private conversation. The original conversation will
455+
remain unchanged.`,
456+
})}
457+
</p>
458+
<EuiButton onClick={duplicateConversation} iconType="copy" size="s">
459+
{i18n.translate('xpack.aiAssistant.duplicateButton', {
460+
defaultMessage: 'Duplicate',
461+
})}
462+
</EuiButton>
463+
</EuiText>
464+
</EuiFlexItem>
465+
</EuiFlexGroup>
466+
</EuiPanel>
467+
<EuiSpacer size="m" />
468+
</>
469+
) : null}
470+
</>
416471
)}
417472
</EuiPanel>
418473
</div>
@@ -438,7 +493,11 @@ export function ChatBody({
438493
className={promptEditorContainerClassName}
439494
>
440495
<PromptEditor
441-
disabled={!connectors.selectedConnector || !hasCorrectLicense}
496+
disabled={
497+
!connectors.selectedConnector ||
498+
!hasCorrectLicense ||
499+
(!!conversationId && !isConversationOwnedByCurrentUser)
500+
}
442501
hidden={connectors.loading || connectors.connectors?.length === 0}
443502
loading={isLoading}
444503
onChangeHeight={handleChangeHeight}
@@ -515,23 +574,21 @@ export function ChatBody({
515574
<EuiFlexItem grow={false} className={headerContainerClassName}>
516575
<ChatHeader
517576
connectors={connectors}
518-
conversationId={
519-
conversation.value?.conversation && 'id' in conversation.value.conversation
520-
? conversation.value.conversation.id
521-
: undefined
522-
}
577+
conversationId={conversationId}
523578
flyoutPositionMode={flyoutPositionMode}
524579
licenseInvalid={!hasCorrectLicense && !initialConversationId}
525580
loading={isLoading}
526581
title={title}
527582
onCopyConversation={handleCopyConversation}
583+
onDuplicateConversation={duplicateConversation}
528584
onSaveTitle={(newTitle) => {
529585
saveTitle(newTitle);
530586
}}
531587
onToggleFlyoutPositionMode={onToggleFlyoutPositionMode}
532588
navigateToConversation={
533589
initialMessages?.length && !initialConversationId ? undefined : navigateToConversation
534590
}
591+
isConversationOwnedByCurrentUser={isConversationOwnedByCurrentUser}
535592
/>
536593
</EuiFlexItem>
537594
<EuiFlexItem grow={false}>

x-pack/platform/packages/shared/kbn-ai-assistant/src/chat/chat_consolidated_items.tsx

+3
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ const noPanelStyle = css`
4949

5050
export function ChatConsolidatedItems({
5151
consolidatedItem,
52+
isConversationOwnedByCurrentUser,
5253
onActionClick,
5354
onEditSubmit,
5455
onFeedback,
@@ -57,6 +58,7 @@ export function ChatConsolidatedItems({
5758
onStopGenerating,
5859
}: {
5960
consolidatedItem: ChatTimelineItem[];
61+
isConversationOwnedByCurrentUser: ChatTimelineProps['isConversationOwnedByCurrentUser'];
6062
onActionClick: ChatTimelineProps['onActionClick'];
6163
onEditSubmit: ChatTimelineProps['onEdit'];
6264
onFeedback: ChatTimelineProps['onFeedback'];
@@ -134,6 +136,7 @@ export function ChatConsolidatedItems({
134136
}}
135137
onSendTelemetry={onSendTelemetry}
136138
onStopGeneratingClick={onStopGenerating}
139+
isConversationOwnedByCurrentUser={isConversationOwnedByCurrentUser}
137140
/>
138141
))
139142
: null}

x-pack/platform/packages/shared/kbn-ai-assistant/src/chat/chat_flyout.tsx

+10-2
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ import {
1616
} from '@elastic/eui';
1717
import { css } from '@emotion/css';
1818
import { i18n } from '@kbn/i18n';
19-
import { Message } from '@kbn/observability-ai-assistant-plugin/common';
19+
import { Conversation, Message } from '@kbn/observability-ai-assistant-plugin/common';
2020
import React, { useState } from 'react';
2121
import ReactDOM from 'react-dom';
2222
import { useConversationKey } from '../hooks/use_conversation_key';
@@ -42,7 +42,7 @@ export enum FlyoutPositionMode {
4242

4343
export function ChatFlyout({
4444
initialTitle,
45-
initialMessages,
45+
initialMessages: initialMessagesFromProps,
4646
initialFlyoutPositionMode,
4747
onFlyoutPositionModeChange,
4848
isOpen,
@@ -69,6 +69,7 @@ export function ChatFlyout({
6969
const knowledgeBase = useKnowledgeBase();
7070

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

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

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

92+
const onConversationDuplicate = (conversation: Conversation) => {
93+
conversationList.conversations.refresh();
94+
setInitialMessages([]);
95+
setConversationId(conversation.conversation.id);
96+
};
97+
9198
const flyoutClassName = css`
9299
max-inline-size: 100% !important;
93100
`;
@@ -287,6 +294,7 @@ export function ChatFlyout({
287294
}
288295
: undefined
289296
}
297+
onConversationDuplicate={onConversationDuplicate}
290298
/>
291299
</EuiFlexItem>
292300

x-pack/platform/packages/shared/kbn-ai-assistant/src/chat/chat_header.tsx

+7-1
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,9 @@ export function ChatHeader({
4646
licenseInvalid,
4747
loading,
4848
title,
49+
isConversationOwnedByCurrentUser,
4950
onCopyConversation,
51+
onDuplicateConversation,
5052
onSaveTitle,
5153
onToggleFlyoutPositionMode,
5254
navigateToConversation,
@@ -57,7 +59,9 @@ export function ChatHeader({
5759
licenseInvalid: boolean;
5860
loading: boolean;
5961
title: string;
62+
isConversationOwnedByCurrentUser: boolean;
6063
onCopyConversation: () => void;
64+
onDuplicateConversation: () => void;
6165
onSaveTitle: (title: string) => void;
6266
onToggleFlyoutPositionMode?: (newFlyoutPositionMode: FlyoutPositionMode) => void;
6367
navigateToConversation?: (nextConversationId?: string) => void;
@@ -115,7 +119,8 @@ export function ChatHeader({
115119
!conversationId ||
116120
!connectors.selectedConnector ||
117121
licenseInvalid ||
118-
!Boolean(onSaveTitle)
122+
!Boolean(onSaveTitle) ||
123+
!isConversationOwnedByCurrentUser
119124
}
120125
onChange={(e) => {
121126
setNewTitle(e.currentTarget.nodeValue || '');
@@ -201,6 +206,7 @@ export function ChatHeader({
201206
conversationId={conversationId}
202207
disabled={licenseInvalid}
203208
onCopyConversationClick={onCopyConversation}
209+
onDuplicateConversationClick={onDuplicateConversation}
204210
/>
205211
</EuiFlexItem>
206212
</EuiFlexGroup>

x-pack/platform/packages/shared/kbn-ai-assistant/src/chat/chat_item.tsx

+7-1
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ export interface ChatItemProps extends Omit<ChatTimelineItem, 'message'> {
3535
onRegenerateClick: () => void;
3636
onSendTelemetry: (eventWithPayload: TelemetryEventTypeWithPayload) => void;
3737
onStopGeneratingClick: () => void;
38+
isConversationOwnedByCurrentUser: boolean;
3839
}
3940

4041
const moreCompactHeaderClassName = css`
@@ -87,6 +88,7 @@ export function ChatItem({
8788
error,
8889
loading,
8990
title,
91+
isConversationOwnedByCurrentUser,
9092
onActionClick,
9193
onEditSubmit,
9294
onFeedbackClick,
@@ -167,7 +169,11 @@ export function ChatItem({
167169
return (
168170
<EuiComment
169171
timelineAvatar={<ChatItemAvatar loading={loading} currentUser={currentUser} role={role} />}
170-
username={getRoleTranslation(role)}
172+
username={getRoleTranslation({
173+
role,
174+
isCurrentUser: isConversationOwnedByCurrentUser,
175+
username: currentUser?.username,
176+
})}
171177
event={title}
172178
actions={
173179
<ChatItemActions

0 commit comments

Comments
 (0)