Skip to content

Commit

Permalink
[Obs AI Assistant] Conversation sharing route and other related permi…
Browse files Browse the repository at this point in the history
…ssion handling (#206590)
  • Loading branch information
viduni94 committed Feb 26, 2025
1 parent c0fa068 commit b487cf0
Show file tree
Hide file tree
Showing 12 changed files with 434 additions and 110 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
/*
* 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, { ReactNode } from 'react';
import { i18n } from '@kbn/i18n';
import { css } from '@emotion/css';
import { EuiText, EuiFlexGroup, EuiFlexItem, EuiIcon, EuiPanel, useEuiTheme } from '@elastic/eui';

export function ChatBanner({
title,
description,
button = null,
}: {
title: string;
description: string;
button?: ReactNode;
}) {
const { euiTheme } = useEuiTheme();

return (
<EuiPanel
paddingSize="m"
hasShadow={false}
color="subdued"
borderRadius="m"
grow={false}
className={css`
margin: ${euiTheme.size.m};
`}
>
<EuiFlexGroup>
<EuiFlexItem grow={false}>
<EuiIcon size="l" type="users" />
</EuiFlexItem>
<EuiFlexItem grow>
<EuiText size="xs">
<h3>
{i18n.translate('xpack.aiAssistant.shareBanner.title', {
defaultMessage: title,
})}
</h3>
<p>
{i18n.translate('xpack.aiAssistant.shareBanner.description', {
defaultMessage: description,
})}
</p>
{button}
</EuiText>
</EuiFlexItem>
</EuiFlexGroup>
</EuiPanel>
);
}
152 changes: 116 additions & 36 deletions x-pack/platform/packages/shared/kbn-ai-assistant/src/chat/chat_body.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
*/

import {
EuiButton,
EuiCallOut,
euiCanAnimate,
EuiFlexGroup,
Expand All @@ -19,7 +20,11 @@ import {
} from '@elastic/eui';
import { css, keyframes } from '@emotion/css';
import { i18n } from '@kbn/i18n';
import type { Conversation, Message } from '@kbn/observability-ai-assistant-plugin/common';
import type {
Conversation,
ConversationAccess,
Message,
} from '@kbn/observability-ai-assistant-plugin/common';
import {
ChatActionClickType,
ChatState,
Expand Down Expand Up @@ -47,6 +52,7 @@ import { WelcomeMessage } from './welcome_message';
import { useLicense } from '../hooks/use_license';
import { PromptEditor } from '../prompt_editor/prompt_editor';
import { useKibana } from '../hooks/use_kibana';
import { ChatBanner } from './chat_banner';

const fullHeightClassName = css`
height: 100%;
Expand All @@ -71,6 +77,7 @@ const incorrectLicenseContainer = (euiTheme: UseEuiTheme['euiTheme']) => css`

const chatBodyContainerClassNameWithError = css`
align-self: center;
margin: 12px;
`;

const promptEditorContainerClassName = css`
Expand Down Expand Up @@ -153,7 +160,19 @@ export function ChatBody({
false
);

const { conversation, messages, next, state, stop, saveTitle } = useConversation({
const {
conversation,
conversationId,
messages,
next,
state,
stop,
saveTitle,
isConversationOwnedByCurrentUser,
user: conversationUser,
updateConversationAccess,
} = useConversation({
currentUser,
initialConversationId,
initialMessages,
initialTitle,
Expand Down Expand Up @@ -323,6 +342,64 @@ export function ChatBody({
}
};

const handleConversationAccessUpdate = async (access: ConversationAccess) => {
await updateConversationAccess(access);
conversation.refresh();
refreshConversations();
};

let sharedBanner: React.ReactNode = null;
let showPromptEditor: boolean = false;

if (!conversation.value?.public) {
// Private conversation: Show only the prompt editor
showPromptEditor = true;
} else if (!!conversationUser) {
if (isConversationOwnedByCurrentUser) {
// Public, conversation has a user, and current user is the owner:
// Show both prompt editor and banner
showPromptEditor = true;
sharedBanner = (
<ChatBanner
title="This conversation is shared with your team."
description="Any further edits you do to this conversation will be shared with the rest of the team."
/>
);
} else {
// Public, conversation has a user, but current user is not the owner
// Don't show prompt editor, only show the banner with Duplicate button
sharedBanner = (
<ChatBanner
title="This conversation is shared with your team."
description="You can't edit or continue this conversation, but you can duplicate it into a new private conversation. The original conversation will remain unchanged."
button={
<EuiButton onClick={() => {}} iconType="copy" size="s">
{i18n.translate('xpack.aiAssistant.duplicateButton', {
defaultMessage: 'Duplicate',
})}
</EuiButton>
}
/>
);
}
} else {
// Public, but conversation doesn't have a user (for backwards compatibility with old conversations generated by the rule connector):
// Don't show prompt editor, only show the banner with Duplicate button
sharedBanner = (
<ChatBanner
title="This conversation is shared with your team."
description="You can't edit or continue this conversation, but you can duplicate it into a new private conversation. The original conversation will remain unchanged."
button={
<EuiButton onClick={() => {}} iconType="copy" size="s">
{i18n.translate('xpack.aiAssistant.duplicateButton', {
defaultMessage: 'Duplicate',
})}
</EuiButton>
}
/>
);
}

let footer: React.ReactNode;
if (!hasCorrectLicense && !initialConversationId) {
footer = (
Expand Down Expand Up @@ -415,35 +492,40 @@ export function ChatBody({
</EuiFlexItem>
) : null}

<EuiFlexItem
grow={false}
className={promptEditorClassname(euiTheme)}
style={{ height: promptEditorHeight }}
>
<EuiHorizontalRule margin="none" />
<EuiPanel
hasBorder={false}
hasShadow={false}
paddingSize="m"
color="subdued"
className={promptEditorContainerClassName}
>
<PromptEditor
disabled={!connectors.selectedConnector || !hasCorrectLicense}
hidden={connectors.loading || connectors.connectors?.length === 0}
loading={isLoading}
onChangeHeight={handleChangeHeight}
onSendTelemetry={(eventWithPayload) =>
chatService.sendAnalyticsEvent(eventWithPayload)
}
onSubmit={(message) => {
setStickToBottom(true);
return next(messages.concat(message));
}}
/>
<EuiSpacer size="s" />
</EuiPanel>
</EuiFlexItem>
<>
{conversationId ? sharedBanner : null}
{showPromptEditor ? (
<EuiFlexItem
grow={false}
className={promptEditorClassname(euiTheme)}
style={{ height: promptEditorHeight }}
>
<EuiHorizontalRule margin="none" />
<EuiPanel
hasBorder={false}
hasShadow={false}
paddingSize="m"
color="subdued"
className={promptEditorContainerClassName}
>
<PromptEditor
disabled={!connectors.selectedConnector || !hasCorrectLicense}
hidden={connectors.loading || connectors.connectors?.length === 0}
loading={isLoading}
onChangeHeight={handleChangeHeight}
onSendTelemetry={(eventWithPayload) =>
chatService.sendAnalyticsEvent(eventWithPayload)
}
onSubmit={(message) => {
setStickToBottom(true);
return next(messages.concat(message));
}}
/>
<EuiSpacer size="s" />
</EuiPanel>
</EuiFlexItem>
) : null}
</>
</>
);
}
Expand Down Expand Up @@ -506,11 +588,7 @@ 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}
conversation={conversation.value as Conversation}
flyoutPositionMode={flyoutPositionMode}
licenseInvalid={!hasCorrectLicense && !initialConversationId}
Expand All @@ -526,6 +604,8 @@ export function ChatBody({
setIsUpdatingConversationList={setIsUpdatingConversationList}
refreshConversations={refreshConversations}
updateDisplayedConversation={updateDisplayedConversation}
handleConversationAccessUpdate={handleConversationAccessUpdate}
isConversationOwnedByCurrentUser={isConversationOwnedByCurrentUser}
/>
</EuiFlexItem>
<EuiFlexItem grow={false}>
Expand Down
Loading

0 comments on commit b487cf0

Please sign in to comment.