[Security solution] Assistant package assistant/index.tsx cleanup (#190151)

This commit is contained in:
Steph Milovic 2024-08-21 08:59:24 -06:00 committed by GitHub
parent e3d6cf69b4
commit 730f8eae87
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
79 changed files with 1351 additions and 2469 deletions

View file

@ -0,0 +1,79 @@
/*
* 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, { Dispatch, SetStateAction } from 'react';
import { EuiFlexGroup, EuiFlexItem, EuiPanel, EuiText } from '@elastic/eui';
import { css } from '@emotion/react';
import { PromptResponse } from '@kbn/elastic-assistant-common';
import { QueryObserverResult } from '@tanstack/react-query';
import { Conversation } from '../../..';
import { AssistantAnimatedIcon } from '../assistant_animated_icon';
import { SystemPrompt } from '../prompt_editor/system_prompt';
import { SetupKnowledgeBaseButton } from '../../knowledge_base/setup_knowledge_base_button';
import * as i18n from '../translations';
interface Props {
currentConversation: Conversation | undefined;
currentSystemPromptId: string | undefined;
isSettingsModalVisible: boolean;
refetchCurrentUserConversations: () => Promise<
QueryObserverResult<Record<string, Conversation>, unknown>
>;
setIsSettingsModalVisible: Dispatch<SetStateAction<boolean>>;
setCurrentSystemPromptId: Dispatch<SetStateAction<string | undefined>>;
allSystemPrompts: PromptResponse[];
}
export const EmptyConvo: React.FC<Props> = ({
allSystemPrompts,
currentConversation,
currentSystemPromptId,
isSettingsModalVisible,
refetchCurrentUserConversations,
setCurrentSystemPromptId,
setIsSettingsModalVisible,
}) => {
return (
<EuiFlexGroup alignItems="center" justifyContent="center">
<EuiFlexItem grow={false}>
<EuiPanel
hasShadow={false}
css={css`
max-width: 400px;
text-align: center;
`}
>
<EuiFlexGroup alignItems="center" justifyContent="center" direction="column">
<EuiFlexItem grow={false}>
<AssistantAnimatedIcon />
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiText>
<h3>{i18n.EMPTY_SCREEN_TITLE}</h3>
<p>{i18n.EMPTY_SCREEN_DESCRIPTION}</p>
</EuiText>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<SystemPrompt
conversation={currentConversation}
currentSystemPromptId={currentSystemPromptId}
onSystemPromptSelectionChange={setCurrentSystemPromptId}
isSettingsModalVisible={isSettingsModalVisible}
setIsSettingsModalVisible={setIsSettingsModalVisible}
allSystemPrompts={allSystemPrompts}
refetchConversations={refetchCurrentUserConversations}
/>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<SetupKnowledgeBaseButton />
</EuiFlexItem>
</EuiFlexGroup>
</EuiPanel>
</EuiFlexItem>
</EuiFlexGroup>
);
};

View file

@ -0,0 +1,140 @@
/*
* 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, {
Dispatch,
FunctionComponent,
SetStateAction,
useEffect,
useMemo,
useRef,
} from 'react';
import { EuiEmptyPrompt, EuiFlexGroup, EuiFlexItem, EuiPanel, EuiText } from '@elastic/eui';
import { HttpSetup } from '@kbn/core-http-browser';
import { euiThemeVars } from '@kbn/ui-theme';
import { css } from '@emotion/react';
import { PromptResponse } from '@kbn/elastic-assistant-common';
import { QueryObserverResult } from '@tanstack/react-query';
import { AssistantAnimatedIcon } from '../assistant_animated_icon';
import { EmptyConvo } from './empty_convo';
import { WelcomeSetup } from './welcome_setup';
import { Conversation } from '../../..';
import { UpgradeLicenseCallToAction } from '../upgrade_license_cta';
import * as i18n from '../translations';
interface Props {
allSystemPrompts: PromptResponse[];
comments: JSX.Element;
currentConversation: Conversation | undefined;
currentSystemPromptId: string | undefined;
handleOnConversationSelected: ({ cId, cTitle }: { cId: string; cTitle: string }) => Promise<void>;
isAssistantEnabled: boolean;
isSettingsModalVisible: boolean;
isWelcomeSetup: boolean;
isLoading: boolean;
refetchCurrentUserConversations: () => Promise<
QueryObserverResult<Record<string, Conversation>, unknown>
>;
http: HttpSetup;
setCurrentSystemPromptId: Dispatch<SetStateAction<string | undefined>>;
setIsSettingsModalVisible: Dispatch<SetStateAction<boolean>>;
}
export const AssistantBody: FunctionComponent<Props> = ({
allSystemPrompts,
comments,
currentConversation,
currentSystemPromptId,
handleOnConversationSelected,
setCurrentSystemPromptId,
http,
isAssistantEnabled,
isLoading,
isSettingsModalVisible,
isWelcomeSetup,
refetchCurrentUserConversations,
setIsSettingsModalVisible,
}) => {
const isNewConversation = useMemo(
() => currentConversation?.messages.length === 0,
[currentConversation?.messages.length]
);
const disclaimer = useMemo(
() =>
isNewConversation && (
<EuiText
data-test-subj="assistant-disclaimer"
textAlign="center"
color={euiThemeVars.euiColorMediumShade}
size="xs"
css={css`
margin: 0 ${euiThemeVars.euiSizeL} ${euiThemeVars.euiSizeM} ${euiThemeVars.euiSizeL};
`}
>
{i18n.DISCLAIMER}
</EuiText>
),
[isNewConversation]
);
// Start Scrolling
const commentsContainerRef = useRef<HTMLDivElement | null>(null);
useEffect(() => {
const parent = commentsContainerRef.current?.parentElement;
if (!parent) {
return;
}
// when scrollHeight changes, parent is scrolled to bottom
parent.scrollTop = parent.scrollHeight;
(
commentsContainerRef.current?.childNodes[0].childNodes[0] as HTMLElement
).lastElementChild?.scrollIntoView();
});
// End Scrolling
if (!isAssistantEnabled) {
return <UpgradeLicenseCallToAction http={http} />;
}
return (
<EuiFlexGroup direction="column" justifyContent="spaceBetween">
<EuiFlexItem grow={false}>
{isLoading ? (
<EuiEmptyPrompt data-test-subj="animatedLogo" icon={<AssistantAnimatedIcon />} />
) : isWelcomeSetup ? (
<WelcomeSetup
currentConversation={currentConversation}
handleOnConversationSelected={handleOnConversationSelected}
/>
) : currentConversation?.messages.length === 0 ? (
<EmptyConvo
allSystemPrompts={allSystemPrompts}
currentConversation={currentConversation}
currentSystemPromptId={currentSystemPromptId}
isSettingsModalVisible={isSettingsModalVisible}
refetchCurrentUserConversations={refetchCurrentUserConversations}
setCurrentSystemPromptId={setCurrentSystemPromptId}
setIsSettingsModalVisible={setIsSettingsModalVisible}
/>
) : (
<EuiPanel
hasShadow={false}
panelRef={(element) => {
commentsContainerRef.current = (element?.parentElement as HTMLDivElement) || null;
}}
>
{comments}
</EuiPanel>
)}
</EuiFlexItem>
<EuiFlexItem grow={false}>{disclaimer}</EuiFlexItem>
</EuiFlexGroup>
);
};

View file

@ -0,0 +1,64 @@
/*
* 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 from 'react';
import { EuiFlexGroup, EuiFlexItem, EuiPanel, EuiText } from '@elastic/eui';
import { css } from '@emotion/react';
import { Conversation } from '../../..';
import { AssistantAnimatedIcon } from '../assistant_animated_icon';
import { ConnectorSetup } from '../../connectorland/connector_setup';
import * as i18n from '../translations';
interface Props {
currentConversation: Conversation | undefined;
handleOnConversationSelected: ({ cId, cTitle }: { cId: string; cTitle: string }) => Promise<void>;
}
export const WelcomeSetup: React.FC<Props> = ({
handleOnConversationSelected,
currentConversation,
}) => {
return (
<EuiFlexGroup alignItems="center" justifyContent="center">
<EuiFlexItem grow={false}>
<EuiPanel
hasShadow={false}
css={css`
text-align: center;
`}
>
<EuiFlexGroup alignItems="center" justifyContent="center" direction="column">
<EuiFlexItem grow={false}>
<AssistantAnimatedIcon />
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiText>
<h3>{i18n.WELCOME_SCREEN_TITLE}</h3>
</EuiText>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiText
color="subdued"
css={css`
max-width: 400px;
`}
>
<p>{i18n.WELCOME_SCREEN_DESCRIPTION}</p>
</EuiText>
</EuiFlexItem>
<EuiFlexItem grow={false} data-test-subj="connector-prompt">
<ConnectorSetup
conversation={currentConversation}
onConversationUpdate={handleOnConversationSelected}
/>
</EuiFlexItem>
</EuiFlexGroup>
</EuiPanel>
</EuiFlexItem>
</EuiFlexGroup>
);
};

View file

@ -26,6 +26,7 @@ const testProps = {
ELASTIC_WEBSITE_URL: 'https://www.elastic.co/',
DOC_LINK_VERSION: 'master',
},
isLoading: false,
isDisabled: false,
isSettingsModalVisible: false,
onConversationSelected,
@ -35,7 +36,7 @@ const testProps = {
onChatCleared: jest.fn(),
showAnonymizedValues: false,
conversations: mockConversations,
refetchConversationsState: jest.fn(),
refetchCurrentUserConversations: jest.fn(),
isAssistantEnabled: true,
anonymizationFields: { total: 0, page: 1, perPage: 1000, data: [] },
refetchAnonymizationFieldsResults: jest.fn(),

View file

@ -16,10 +16,12 @@ import {
EuiPanel,
EuiConfirmModal,
EuiToolTip,
EuiSkeletonTitle,
} from '@elastic/eui';
import { css } from '@emotion/react';
import { euiThemeVars } from '@kbn/ui-theme';
import { isEmpty } from 'lodash';
import { DataStreamApis } from '../use_data_stream_apis';
import { Conversation } from '../../..';
import { AssistantTitle } from '../assistant_title';
import { ConnectorSelectorInline } from '../../connectorland/connector_selector_inline/connector_selector_inline';
@ -32,6 +34,7 @@ interface OwnProps {
selectedConversation: Conversation | undefined;
defaultConnector?: AIConnector;
isDisabled: boolean;
isLoading: boolean;
isSettingsModalVisible: boolean;
onToggleShowAnonymizedValues: () => void;
setIsSettingsModalVisible: React.Dispatch<React.SetStateAction<boolean>>;
@ -43,7 +46,7 @@ interface OwnProps {
onConversationSelected: ({ cId, cTitle }: { cId: string; cTitle: string }) => void;
conversations: Record<string, Conversation>;
conversationsLoaded: boolean;
refetchConversationsState: () => Promise<void>;
refetchCurrentUserConversations: DataStreamApis['refetchCurrentUserConversations'];
onConversationCreate: () => Promise<void>;
isAssistantEnabled: boolean;
refetchPrompts?: (
@ -61,6 +64,7 @@ export const AssistantHeader: React.FC<Props> = ({
selectedConversation,
defaultConnector,
isDisabled,
isLoading,
isSettingsModalVisible,
onToggleShowAnonymizedValues,
setIsSettingsModalVisible,
@ -72,7 +76,7 @@ export const AssistantHeader: React.FC<Props> = ({
onConversationSelected,
conversations,
conversationsLoaded,
refetchConversationsState,
refetchCurrentUserConversations,
onConversationCreate,
isAssistantEnabled,
refetchPrompts,
@ -144,6 +148,7 @@ export const AssistantHeader: React.FC<Props> = ({
return (
<>
<FlyoutNavigation
isLoading={isLoading}
isExpanded={!!chatHistoryVisible}
setIsExpanded={setChatHistoryVisible}
onConversationCreate={onConversationCreate}
@ -164,7 +169,7 @@ export const AssistantHeader: React.FC<Props> = ({
onConversationSelected={onConversationSelected}
conversations={conversations}
conversationsLoaded={conversationsLoaded}
refetchConversationsState={refetchConversationsState}
refetchCurrentUserConversations={refetchCurrentUserConversations}
refetchPrompts={refetchPrompts}
/>
</EuiFlexItem>
@ -196,11 +201,16 @@ export const AssistantHeader: React.FC<Props> = ({
overflow: hidden;
`}
>
<AssistantTitle
title={selectedConversation?.title}
selectedConversation={selectedConversation}
refetchConversationsState={refetchConversationsState}
/>
{isLoading ? (
<EuiSkeletonTitle data-test-subj="skeletonTitle" size="xs" />
) : (
<AssistantTitle
isDisabled={isDisabled}
title={selectedConversation?.title}
selectedConversation={selectedConversation}
refetchCurrentUserConversations={refetchCurrentUserConversations}
/>
)}
</EuiFlexItem>
<EuiFlexItem grow={false}>
@ -240,6 +250,7 @@ export const AssistantHeader: React.FC<Props> = ({
button={
<EuiButtonIcon
aria-label="test"
isDisabled={isDisabled}
iconType="boxesVertical"
onClick={onButtonClick}
data-test-subj="chat-context-menu"

View file

@ -7,13 +7,6 @@
import { i18n } from '@kbn/i18n';
export const ANONYMIZED_VALUES = i18n.translate(
'xpack.elasticAssistant.assistant.settings.anonymizedValues',
{
defaultMessage: 'Anonymized values',
}
);
export const RESET_CONVERSATION = i18n.translate(
'xpack.elasticAssistant.assistant.settings.resetConversation',
{
@ -21,13 +14,6 @@ export const RESET_CONVERSATION = i18n.translate(
}
);
export const CONNECTOR_TITLE = i18n.translate(
'xpack.elasticAssistant.assistant.settings.connectorTitle',
{
defaultMessage: 'Connector',
}
);
export const SHOW_ANONYMIZED = i18n.translate(
'xpack.elasticAssistant.assistant.settings.showAnonymizedToggleLabel',
{
@ -42,13 +28,6 @@ export const SHOW_REAL_VALUES = i18n.translate(
}
);
export const SHOW_ANONYMIZED_TOOLTIP = i18n.translate(
'xpack.elasticAssistant.assistant.settings.showAnonymizedTooltip',
{
defaultMessage: 'Show the anonymized values sent to and from the assistant',
}
);
export const CANCEL_BUTTON_TEXT = i18n.translate(
'xpack.elasticAssistant.assistant.resetConversationModal.cancelButtonText',
{

View file

@ -15,6 +15,7 @@ import { NEW_CHAT } from '../conversations/conversation_sidepanel/translations';
export interface FlyoutNavigationProps {
isExpanded: boolean;
isLoading: boolean;
setIsExpanded?: (value: boolean) => void;
children: React.ReactNode;
onConversationCreate?: () => Promise<void>;
@ -35,7 +36,14 @@ const VerticalSeparator = styled.div`
*/
export const FlyoutNavigation = memo<FlyoutNavigationProps>(
({ isExpanded, setIsExpanded, children, onConversationCreate, isAssistantEnabled }) => {
({
isLoading,
isExpanded,
setIsExpanded,
children,
onConversationCreate,
isAssistantEnabled,
}) => {
const onToggle = useCallback(
() => setIsExpanded && setIsExpanded(!isExpanded),
[isExpanded, setIsExpanded]
@ -44,7 +52,7 @@ export const FlyoutNavigation = memo<FlyoutNavigationProps>(
const toggleButton = useMemo(
() => (
<EuiButtonIcon
disabled={!isAssistantEnabled}
disabled={isLoading || !isAssistantEnabled}
onClick={onToggle}
iconType={isExpanded ? 'arrowEnd' : 'arrowStart'}
size="xs"
@ -66,7 +74,7 @@ export const FlyoutNavigation = memo<FlyoutNavigationProps>(
}
/>
),
[isAssistantEnabled, isExpanded, onToggle]
[isAssistantEnabled, isExpanded, isLoading, onToggle]
);
return (
@ -99,7 +107,7 @@ export const FlyoutNavigation = memo<FlyoutNavigationProps>(
color="primary"
iconType="newChat"
onClick={onConversationCreate}
disabled={!isAssistantEnabled}
disabled={isLoading || !isAssistantEnabled}
>
{NEW_CHAT}
</EuiButtonEmpty>

View file

@ -18,7 +18,6 @@ import {
UserAvatar,
} from '../../assistant_context';
import { Assistant, CONVERSATION_SIDE_PANEL_WIDTH } from '..';
import { WELCOME_CONVERSATION_TITLE } from '../use_conversation/translations';
const isMac = navigator.platform.toLowerCase().indexOf('mac') >= 0;
@ -38,9 +37,8 @@ export const UnifiedTimelineGlobalStyles = createGlobalStyle`
export const AssistantOverlay = React.memo<Props>(({ currentUserAvatar }) => {
const [isModalVisible, setIsModalVisible] = useState(false);
const [conversationTitle, setConversationTitle] = useState<string | undefined>(
WELCOME_CONVERSATION_TITLE
);
// Why is this named Title and not Id?
const [conversationTitle, setConversationTitle] = useState<string | undefined>(undefined);
const [promptContextId, setPromptContextId] = useState<string | undefined>();
const { assistantTelemetry, setShowAssistantOverlay, getLastConversationId } =
useAssistantContext();
@ -55,16 +53,12 @@ export const AssistantOverlay = React.memo<Props>(({ currentUserAvatar }) => {
promptContextId: pid,
conversationTitle: cTitle,
}: ShowAssistantOverlayProps) => {
const newConversationTitle = getLastConversationId(cTitle);
if (so)
assistantTelemetry?.reportAssistantInvoked({
conversationId: newConversationTitle,
invokedBy: 'click',
});
const conversationId = getLastConversationId(cTitle);
if (so) assistantTelemetry?.reportAssistantInvoked({ conversationId, invokedBy: 'click' });
setIsModalVisible(so);
setPromptContextId(pid);
setConversationTitle(newConversationTitle);
setConversationTitle(conversationId);
},
[assistantTelemetry, getLastConversationId]
);

View file

@ -15,7 +15,7 @@ const testProps = {
docLinks: { ELASTIC_WEBSITE_URL: 'https://www.elastic.co/', DOC_LINK_VERSION: '7.15' },
selectedConversation: undefined,
onChange: jest.fn(),
refetchConversationsState: jest.fn(),
refetchCurrentUserConversations: jest.fn(),
};
describe('AssistantTitle', () => {

View file

@ -8,6 +8,7 @@
import React, { useCallback, useEffect, useState } from 'react';
import { EuiFlexGroup, EuiFlexItem, EuiInlineEditTitle } from '@elastic/eui';
import { css } from '@emotion/react';
import { DataStreamApis } from '../use_data_stream_apis';
import type { Conversation } from '../../..';
import { AssistantAvatar } from '../assistant_avatar/assistant_avatar';
import { useConversation } from '../use_conversation';
@ -18,10 +19,11 @@ import { NEW_CHAT } from '../conversations/conversation_sidepanel/translations';
* information about the assistant feature and access to documentation.
*/
export const AssistantTitle: React.FC<{
isDisabled?: boolean;
title?: string;
selectedConversation: Conversation | undefined;
refetchConversationsState: () => Promise<void>;
}> = ({ title, selectedConversation, refetchConversationsState }) => {
refetchCurrentUserConversations: DataStreamApis['refetchCurrentUserConversations'];
}> = ({ title, selectedConversation, refetchCurrentUserConversations, isDisabled = false }) => {
const [newTitle, setNewTitle] = useState(title);
const [newTitleError, setNewTitleError] = useState(false);
const { updateConversationTitle } = useConversation();
@ -35,10 +37,10 @@ export const AssistantTitle: React.FC<{
conversationId: selectedConversation.id,
updatedTitle,
});
await refetchConversationsState();
await refetchCurrentUserConversations();
}
},
[refetchConversationsState, selectedConversation, updateConversationTitle]
[refetchCurrentUserConversations, selectedConversation, updateConversationTitle]
);
useEffect(() => {
@ -62,7 +64,7 @@ export const AssistantTitle: React.FC<{
value={newTitle ?? NEW_CHAT}
size="xs"
isInvalid={!!newTitleError}
isReadOnly={selectedConversation?.isDefault}
isReadOnly={isDisabled || selectedConversation?.isDefault}
onChange={(e) => setNewTitle(e.currentTarget.nodeValue || '')}
onCancel={() => setNewTitle(title)}
onSave={handleUpdateTitle}

View file

@ -1,48 +0,0 @@
/*
* 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 from 'react';
import { render } from '@testing-library/react';
import { BlockBotCallToAction } from './cta';
import { HttpSetup } from '@kbn/core-http-browser';
const testProps = {
connectorPrompt: <div>{'Connector Prompt'}</div>,
http: { basePath: { get: jest.fn(() => 'http://localhost:5601') } } as unknown as HttpSetup,
isAssistantEnabled: false,
isWelcomeSetup: false,
};
describe('BlockBotCallToAction', () => {
it('UpgradeButtons is rendered when isAssistantEnabled is false and isWelcomeSetup is false', () => {
const { getByTestId, queryByTestId } = render(<BlockBotCallToAction {...testProps} />);
expect(getByTestId('upgrade-buttons')).toBeInTheDocument();
expect(queryByTestId('connector-prompt')).not.toBeInTheDocument();
});
it('connectorPrompt is rendered when isAssistantEnabled is true and isWelcomeSetup is true', () => {
const props = {
...testProps,
isAssistantEnabled: true,
isWelcomeSetup: true,
};
const { getByTestId, queryByTestId } = render(<BlockBotCallToAction {...props} />);
expect(getByTestId('connector-prompt')).toBeInTheDocument();
expect(queryByTestId('upgrade-buttons')).not.toBeInTheDocument();
});
it('null is returned when isAssistantEnabled is true and isWelcomeSetup is false', () => {
const props = {
...testProps,
isAssistantEnabled: true,
isWelcomeSetup: false,
};
const { container, queryByTestId } = render(<BlockBotCallToAction {...props} />);
expect(container.firstChild).toBeNull();
expect(queryByTestId('connector-prompt')).not.toBeInTheDocument();
expect(queryByTestId('upgrade-buttons')).not.toBeInTheDocument();
});
});

View file

@ -12,12 +12,12 @@ import { TestProviders } from '../../mock/test_providers/test_providers';
jest.mock('./use_chat_send');
const handlePromptChange = jest.fn();
const handleSendMessage = jest.fn();
const setUserPrompt = jest.fn();
const handleChatSend = jest.fn();
const handleRegenerateResponse = jest.fn();
const testProps: Props = {
handlePromptChange,
handleSendMessage,
setUserPrompt,
handleChatSend,
handleRegenerateResponse,
isLoading: false,
isDisabled: false,
@ -35,7 +35,7 @@ describe('ChatSend', () => {
const promptTextArea = getByTestId('prompt-textarea');
const promptText = 'valid prompt text';
fireEvent.change(promptTextArea, { target: { value: promptText } });
expect(handlePromptChange).toHaveBeenCalledWith(promptText);
expect(setUserPrompt).toHaveBeenCalledWith(promptText);
});
it('a message is sent when send button is clicked', async () => {
@ -46,7 +46,7 @@ describe('ChatSend', () => {
expect(getByTestId('prompt-textarea')).toHaveTextContent(promptText);
fireEvent.click(getByTestId('submit-chat'));
await waitFor(() => {
expect(handleSendMessage).toHaveBeenCalledWith(promptText);
expect(handleChatSend).toHaveBeenCalledWith(promptText);
});
});

View file

@ -25,8 +25,8 @@ export interface Props extends Omit<UseChatSend, 'abortStream' | 'handleOnChatCl
* Allows the user to clear the chat and switch between different system prompts.
*/
export const ChatSend: React.FC<Props> = ({
handlePromptChange,
handleSendMessage,
setUserPrompt,
handleChatSend,
isDisabled,
isLoading,
shouldRefocusPrompt,
@ -42,15 +42,15 @@ export const ChatSend: React.FC<Props> = ({
const promptValue = useMemo(() => (isDisabled ? '' : userPrompt ?? ''), [isDisabled, userPrompt]);
const onSendMessage = useCallback(() => {
handleSendMessage(promptTextAreaRef.current?.value?.trim() ?? '');
handlePromptChange('');
}, [handleSendMessage, promptTextAreaRef, handlePromptChange]);
handleChatSend(promptTextAreaRef.current?.value?.trim() ?? '');
setUserPrompt('');
}, [handleChatSend, promptTextAreaRef, setUserPrompt]);
useAutosizeTextArea(promptTextAreaRef?.current, promptValue);
useEffect(() => {
handlePromptChange(promptValue);
}, [handlePromptChange, promptValue]);
setUserPrompt(promptValue);
}, [setUserPrompt, promptValue]);
return (
<EuiFlexGroup
@ -66,9 +66,9 @@ export const ChatSend: React.FC<Props> = ({
`}
>
<PromptTextArea
onPromptSubmit={handleSendMessage}
onPromptSubmit={handleChatSend}
ref={promptTextAreaRef}
handlePromptChange={handlePromptChange}
setUserPrompt={setUserPrompt}
value={promptValue}
isDisabled={isDisabled}
/>

View file

@ -11,7 +11,7 @@ import { useConversation } from '../use_conversation';
import { emptyWelcomeConvo, welcomeConvo } from '../../mock/conversation';
import { defaultSystemPrompt, mockSystemPrompt } from '../../mock/system_prompt';
import { useChatSend, UseChatSendProps } from './use_chat_send';
import { renderHook } from '@testing-library/react-hooks';
import { act, renderHook } from '@testing-library/react-hooks';
import { waitFor } from '@testing-library/react';
import { TestProviders } from '../../mock/test_providers/test_providers';
import { useAssistantContext } from '../../..';
@ -20,9 +20,7 @@ jest.mock('../use_send_message');
jest.mock('../use_conversation');
jest.mock('../../..');
const setEditingSystemPromptId = jest.fn();
const setSelectedPromptContexts = jest.fn();
const setUserPrompt = jest.fn();
const sendMessage = jest.fn();
const removeLastMessage = jest.fn();
const clearConversation = jest.fn();
@ -40,11 +38,10 @@ export const testProps: UseChatSendProps = {
anonymousPaths: {},
externalUrl: {},
} as unknown as HttpSetup,
editingSystemPromptId: defaultSystemPrompt.id,
setEditingSystemPromptId,
currentSystemPromptId: defaultSystemPrompt.id,
setSelectedPromptContexts,
setUserPrompt,
setCurrentConversation,
refetchCurrentUserConversations: jest.fn(),
};
const robotMessage = { response: 'Response message from the robot', isError: false };
const reportAssistantMessageSent = jest.fn();
@ -70,29 +67,23 @@ describe('use chat send', () => {
const { result } = renderHook(() => useChatSend(testProps), {
wrapper: TestProviders,
});
result.current.handleOnChatCleared();
await act(async () => {
result.current.handleOnChatCleared();
});
expect(clearConversation).toHaveBeenCalled();
expect(setUserPrompt).toHaveBeenCalledWith('');
expect(result.current.userPrompt).toEqual('');
expect(setSelectedPromptContexts).toHaveBeenCalledWith({});
await waitFor(() => {
expect(clearConversation).toHaveBeenCalledWith(testProps.currentConversation);
expect(setCurrentConversation).toHaveBeenCalled();
});
expect(setEditingSystemPromptId).toHaveBeenCalledWith(defaultSystemPrompt.id);
});
it('handlePromptChange updates prompt successfully', () => {
const { result } = renderHook(() => useChatSend(testProps), {
wrapper: TestProviders,
});
result.current.handlePromptChange('new prompt');
expect(setUserPrompt).toHaveBeenCalledWith('new prompt');
});
it('handleSendMessage sends message with context prompt when a valid prompt text is provided', async () => {
it('handleChatSend sends message with context prompt when a valid prompt text is provided', async () => {
const promptText = 'prompt text';
const { result } = renderHook(() => useChatSend(testProps), {
wrapper: TestProviders,
});
result.current.handleSendMessage(promptText);
result.current.handleChatSend(promptText);
await waitFor(() => {
expect(sendMessage).toHaveBeenCalled();
@ -102,7 +93,7 @@ describe('use chat send', () => {
);
});
});
it('handleSendMessage sends message with only provided prompt text and context already exists in convo history', async () => {
it('handleChatSend sends message with only provided prompt text and context already exists in convo history', async () => {
const promptText = 'prompt text';
const { result } = renderHook(
() =>
@ -112,7 +103,7 @@ describe('use chat send', () => {
}
);
result.current.handleSendMessage(promptText);
result.current.handleChatSend(promptText);
await waitFor(() => {
expect(sendMessage).toHaveBeenCalled();
@ -143,7 +134,7 @@ describe('use chat send', () => {
const { result } = renderHook(() => useChatSend(testProps), {
wrapper: TestProviders,
});
result.current.handleSendMessage(promptText);
result.current.handleChatSend(promptText);
await waitFor(() => {
expect(reportAssistantMessageSent).toHaveBeenNthCalledWith(1, {

View file

@ -5,10 +5,12 @@
* 2.0.
*/
import React, { useCallback } from 'react';
import React, { useCallback, useState } from 'react';
import { HttpSetup } from '@kbn/core-http-browser';
import { i18n } from '@kbn/i18n';
import { PromptResponse, Replacements } from '@kbn/elastic-assistant-common';
import { DataStreamApis } from '../use_data_stream_apis';
import { NEW_CHAT } from '../conversations/conversation_sidepanel/translations';
import type { ClientMessage } from '../../assistant_context/types';
import { SelectedPromptContext } from '../prompt_context/types';
import { useSendMessage } from '../use_send_message';
@ -16,56 +18,49 @@ import { useConversation } from '../use_conversation';
import { getCombinedMessage } from '../prompt/helpers';
import { Conversation, useAssistantContext } from '../../..';
import { getMessageFromRawResponse } from '../helpers';
import { getDefaultSystemPrompt, getDefaultNewSystemPrompt } from '../use_conversation/helpers';
export interface UseChatSendProps {
allSystemPrompts: PromptResponse[];
currentConversation?: Conversation;
editingSystemPromptId: string | undefined;
currentSystemPromptId: string | undefined;
http: HttpSetup;
refetchCurrentUserConversations: DataStreamApis['refetchCurrentUserConversations'];
selectedPromptContexts: Record<string, SelectedPromptContext>;
setEditingSystemPromptId: React.Dispatch<React.SetStateAction<string | undefined>>;
setSelectedPromptContexts: React.Dispatch<
React.SetStateAction<Record<string, SelectedPromptContext>>
>;
setUserPrompt: React.Dispatch<React.SetStateAction<string | null>>;
setCurrentConversation: React.Dispatch<React.SetStateAction<Conversation | undefined>>;
}
export interface UseChatSend {
abortStream: () => void;
handleOnChatCleared: () => Promise<void>;
handlePromptChange: (prompt: string) => void;
handleSendMessage: (promptText: string) => void;
handleRegenerateResponse: () => void;
handleChatSend: (promptText: string) => Promise<void>;
setUserPrompt: React.Dispatch<React.SetStateAction<string | null>>;
isLoading: boolean;
userPrompt: string | null;
}
/**
* handles sending messages to an API and updating the conversation state.
* Provides a set of functions that can be used to handle user input, send messages to an API,
* and update the conversation state based on the API response.
* Handles sending user messages to the API and updating the conversation state.
*/
export const useChatSend = ({
allSystemPrompts,
currentConversation,
editingSystemPromptId,
currentSystemPromptId,
http,
refetchCurrentUserConversations,
selectedPromptContexts,
setEditingSystemPromptId,
setSelectedPromptContexts,
setUserPrompt,
setCurrentConversation,
}: UseChatSendProps): UseChatSend => {
const { assistantTelemetry, toasts } = useAssistantContext();
const [userPrompt, setUserPrompt] = useState<string | null>(null);
const { isLoading, sendMessage, abortStream } = useSendMessage();
const { clearConversation, removeLastMessage } = useConversation();
const handlePromptChange = (prompt: string) => {
setUserPrompt(prompt);
};
// Handles sending latest user prompt to API
const handleSendMessage = useCallback(
async (promptText: string) => {
@ -80,7 +75,7 @@ export const useChatSend = ({
);
return;
}
const systemPrompt = allSystemPrompts.find((prompt) => prompt.id === editingSystemPromptId);
const systemPrompt = allSystemPrompts.find((prompt) => prompt.id === currentSystemPromptId);
const userMessage = getCombinedMessage({
isNewChat: currentConversation.messages.length === 0,
@ -149,7 +144,7 @@ export const useChatSend = ({
allSystemPrompts,
assistantTelemetry,
currentConversation,
editingSystemPromptId,
currentSystemPromptId,
http,
selectedPromptContexts,
sendMessage,
@ -193,13 +188,7 @@ export const useChatSend = ({
});
}, [currentConversation, http, removeLastMessage, sendMessage, setCurrentConversation, toasts]);
const handleOnChatCleared = useCallback(async () => {
const defaultSystemPromptId =
getDefaultSystemPrompt({
allSystemPrompts,
conversation: currentConversation,
})?.id ?? getDefaultNewSystemPrompt(allSystemPrompts)?.id;
const onChatCleared = useCallback(async () => {
setUserPrompt('');
setSelectedPromptContexts({});
if (currentConversation) {
@ -208,23 +197,36 @@ export const useChatSend = ({
setCurrentConversation(updatedConversation);
}
}
setEditingSystemPromptId(defaultSystemPromptId);
}, [
allSystemPrompts,
clearConversation,
currentConversation,
setCurrentConversation,
setEditingSystemPromptId,
setSelectedPromptContexts,
setUserPrompt,
]);
const handleOnChatCleared = useCallback(async () => {
await onChatCleared();
await refetchCurrentUserConversations();
}, [onChatCleared, refetchCurrentUserConversations]);
const handleChatSend = useCallback(
async (promptText: string) => {
await handleSendMessage(promptText);
if (currentConversation?.title === NEW_CHAT) {
await refetchCurrentUserConversations();
}
},
[currentConversation, handleSendMessage, refetchCurrentUserConversations]
);
return {
abortStream,
handleOnChatCleared,
handlePromptChange,
handleSendMessage,
handleChatSend,
abortStream,
handleRegenerateResponse,
isLoading,
userPrompt,
setUserPrompt,
};
};

View file

@ -1,209 +0,0 @@
/*
* 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 from 'react';
import { ConversationSelector } from '.';
import { render, fireEvent, within } from '@testing-library/react';
import { TestProviders } from '../../../mock/test_providers/test_providers';
import { alertConvo, customConvo, welcomeConvo } from '../../../mock/conversation';
import { CONVERSATION_SELECTOR_PLACE_HOLDER } from './translations';
import { OpenAiProviderType } from '@kbn/stack-connectors-plugin/common/openai/constants';
const setConversation = jest.fn();
const deleteConversation = jest.fn();
const mockConversation = {
appendMessage: jest.fn(),
appendReplacements: jest.fn(),
clearConversation: jest.fn(),
createConversation: jest.fn(),
deleteConversation,
setApiConfig: jest.fn(),
setConversation,
};
const mockConversations = {
[alertConvo.title]: alertConvo,
[welcomeConvo.title]: welcomeConvo,
};
const mockConversationsWithCustom = {
[alertConvo.title]: alertConvo,
[welcomeConvo.title]: welcomeConvo,
[customConvo.title]: customConvo,
};
jest.mock('../../use_conversation', () => ({
useConversation: () => mockConversation,
}));
const onConversationSelected = jest.fn();
const onConversationDeleted = jest.fn();
const defaultProps = {
isDisabled: false,
onConversationSelected,
selectedConversationId: 'Welcome',
defaultConnectorId: '123',
defaultProvider: OpenAiProviderType.OpenAi,
conversations: mockConversations,
onConversationDeleted,
allPrompts: [],
};
describe('Conversation selector', () => {
beforeAll(() => {
jest.useFakeTimers();
});
beforeEach(() => {
jest.clearAllMocks();
});
it('renders with correct selected conversation', () => {
const { getByTestId } = render(
<TestProviders>
<ConversationSelector {...defaultProps} />
</TestProviders>
);
expect(getByTestId('conversation-selector')).toBeInTheDocument();
expect(getByTestId('comboBoxSearchInput')).toHaveValue(welcomeConvo.title);
});
it('On change, selects new item', () => {
const { getByTestId } = render(
<TestProviders>
<ConversationSelector {...defaultProps} />
</TestProviders>
);
fireEvent.click(getByTestId('comboBoxSearchInput'));
fireEvent.click(getByTestId(`convo-option-${alertConvo.title}`));
expect(onConversationSelected).toHaveBeenCalledWith({
cId: '',
cTitle: alertConvo.title,
});
});
it('On clear input, clears selected options', () => {
const { getByPlaceholderText, queryByPlaceholderText, getByTestId, queryByTestId } = render(
<TestProviders>
<ConversationSelector {...defaultProps} />
</TestProviders>
);
expect(getByTestId('comboBoxSearchInput')).toBeInTheDocument();
expect(queryByPlaceholderText(CONVERSATION_SELECTOR_PLACE_HOLDER)).not.toBeInTheDocument();
fireEvent.click(getByTestId('comboBoxClearButton'));
expect(getByPlaceholderText(CONVERSATION_SELECTOR_PLACE_HOLDER)).toBeInTheDocument();
expect(queryByTestId('euiComboBoxPill')).not.toBeInTheDocument();
});
it('We can add a custom option', () => {
const { getByTestId } = render(
<TestProviders>
<ConversationSelector {...defaultProps} conversations={mockConversationsWithCustom} />
</TestProviders>
);
const customOption = 'Custom option';
fireEvent.change(getByTestId('comboBoxSearchInput'), { target: { value: customOption } });
fireEvent.keyDown(getByTestId('comboBoxSearchInput'), {
key: 'Enter',
code: 'Enter',
charCode: 13,
});
expect(onConversationSelected).toHaveBeenCalledWith({
cId: '',
cTitle: customOption,
});
});
it('Only custom options can be deleted', () => {
const { getByTestId } = render(
<TestProviders>
<ConversationSelector
{...{ ...defaultProps, conversations: mockConversationsWithCustom }}
/>
</TestProviders>
);
fireEvent.click(getByTestId('comboBoxSearchInput'));
expect(
within(getByTestId(`convo-option-${customConvo.title}`)).getByTestId('delete-option')
).toBeInTheDocument();
expect(
within(getByTestId(`convo-option-${alertConvo.title}`)).queryByTestId('delete-option')
).not.toBeInTheDocument();
});
it('Custom options can be deleted', () => {
const { getByTestId } = render(
<TestProviders>
<ConversationSelector
{...{ ...defaultProps, conversations: mockConversationsWithCustom }}
/>
</TestProviders>
);
fireEvent.click(getByTestId('comboBoxSearchInput'));
fireEvent.click(
within(getByTestId(`convo-option-${customConvo.title}`)).getByTestId('delete-option')
);
jest.runAllTimers();
expect(onConversationSelected).not.toHaveBeenCalled();
expect(onConversationDeleted).toHaveBeenCalledWith(customConvo.title);
});
it('Previous conversation is set to active when selected conversation is deleted', () => {
const { getByTestId } = render(
<TestProviders>
<ConversationSelector
{...{ ...defaultProps, conversations: mockConversationsWithCustom }}
selectedConversationId={customConvo.title}
/>
</TestProviders>
);
fireEvent.click(getByTestId('comboBoxSearchInput'));
fireEvent.click(
within(getByTestId(`convo-option-${customConvo.title}`)).getByTestId('delete-option')
);
expect(onConversationSelected).toHaveBeenCalledWith({
cId: '',
cTitle: welcomeConvo.title,
});
});
it('Right arrow does nothing when ctrlKey is false', () => {
const { getByTestId } = render(
<TestProviders>
<ConversationSelector {...defaultProps} conversations={mockConversationsWithCustom} />
</TestProviders>
);
fireEvent.keyDown(getByTestId('comboBoxSearchInput'), {
key: 'ArrowRight',
ctrlKey: false,
code: 'ArrowRight',
charCode: 26,
});
expect(onConversationSelected).not.toHaveBeenCalled();
});
it('Right arrow does nothing when conversation lenth is 1', () => {
const { getByTestId } = render(
<TestProviders>
<ConversationSelector
{...defaultProps}
conversations={{
[welcomeConvo.title]: welcomeConvo,
}}
/>
</TestProviders>
);
fireEvent.keyDown(getByTestId('comboBoxSearchInput'), {
key: 'ArrowRight',
ctrlKey: true,
code: 'ArrowRight',
charCode: 26,
});
expect(onConversationSelected).not.toHaveBeenCalled();
});
});

View file

@ -1,302 +0,0 @@
/*
* 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 {
EuiButtonIcon,
EuiComboBox,
EuiComboBoxOptionOption,
EuiFlexGroup,
EuiFlexItem,
EuiFormRow,
EuiHighlight,
EuiToolTip,
} from '@elastic/eui';
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { css } from '@emotion/react';
import {
PromptResponse,
PromptTypeEnum,
} from '@kbn/elastic-assistant-common/impl/schemas/prompts/bulk_crud_prompts_route.gen';
import { getGenAiConfig } from '../../../connectorland/helpers';
import { AIConnector } from '../../../connectorland/connector_selector';
import { Conversation } from '../../../..';
import * as i18n from './translations';
import { DEFAULT_CONVERSATION_TITLE } from '../../use_conversation/translations';
import { useConversation } from '../../use_conversation';
import { SystemPromptSelectorOption } from '../../prompt_editor/system_prompt/system_prompt_modal/system_prompt_selector/system_prompt_selector';
interface Props {
defaultConnector?: AIConnector;
selectedConversationId: string | undefined;
onConversationSelected: ({ cId, cTitle }: { cId: string; cTitle: string }) => void;
onConversationDeleted: (conversationId: string) => void;
isDisabled?: boolean;
conversations: Record<string, Conversation>;
allPrompts: PromptResponse[];
}
const getPreviousConversationId = (conversationIds: string[], selectedConversationId: string) => {
return conversationIds.indexOf(selectedConversationId) === 0
? conversationIds[conversationIds.length - 1]
: conversationIds[conversationIds.indexOf(selectedConversationId) - 1];
};
const getNextConversationId = (conversationIds: string[], selectedConversationId: string) => {
return conversationIds.indexOf(selectedConversationId) + 1 >= conversationIds.length
? conversationIds[0]
: conversationIds[conversationIds.indexOf(selectedConversationId) + 1];
};
const getConvoId = (cId: string, cTitle: string): string => (cId === cTitle ? '' : cId);
export type ConversationSelectorOption = EuiComboBoxOptionOption<{
isDefault: boolean;
}>;
export const ConversationSelector: React.FC<Props> = React.memo(
({
selectedConversationId = DEFAULT_CONVERSATION_TITLE,
defaultConnector,
onConversationSelected,
onConversationDeleted,
isDisabled = false,
conversations,
allPrompts,
}) => {
const { createConversation } = useConversation();
const allSystemPrompts = useMemo(
() => allPrompts.filter((p) => p.promptType === PromptTypeEnum.system),
[allPrompts]
);
const conversationIds = useMemo(() => Object.keys(conversations), [conversations]);
const conversationOptions = useMemo<ConversationSelectorOption[]>(() => {
return Object.values(conversations).map((conversation) => ({
value: { isDefault: conversation.isDefault ?? false },
id: conversation.id !== '' ? conversation.id : conversation.title,
label: conversation.title,
}));
}, [conversations]);
const [selectedOptions, setSelectedOptions] = useState<ConversationSelectorOption[]>(() => {
return conversationOptions.filter((c) => c.id === selectedConversationId) ?? [];
});
// Callback for when user types to create a new system prompt
const onCreateOption = useCallback(
async (searchValue, flattenedOptions = []) => {
if (!searchValue || !searchValue.trim().toLowerCase()) {
return;
}
const normalizedSearchValue = searchValue.trim().toLowerCase();
const defaultSystemPrompt = allSystemPrompts.find(
(systemPrompt) => systemPrompt.isNewConversationDefault
);
const optionExists =
flattenedOptions.findIndex(
(option: SystemPromptSelectorOption) =>
option.label.trim().toLowerCase() === normalizedSearchValue
) !== -1;
let createdConversation;
if (!optionExists) {
const config = getGenAiConfig(defaultConnector);
const newConversation: Conversation = {
id: '',
title: searchValue,
category: 'assistant',
messages: [],
replacements: {},
...(defaultConnector
? {
apiConfig: {
connectorId: defaultConnector.id,
actionTypeId: defaultConnector.actionTypeId,
provider: defaultConnector.apiProvider,
defaultSystemPromptId: defaultSystemPrompt?.id,
model: config?.defaultModel,
},
}
: {}),
};
createdConversation = await createConversation(newConversation);
}
onConversationSelected(
createdConversation
? { cId: createdConversation.id, cTitle: createdConversation.title }
: { cId: '', cTitle: DEFAULT_CONVERSATION_TITLE }
);
},
[allSystemPrompts, onConversationSelected, defaultConnector, createConversation]
);
// Callback for when user deletes a conversation
const onDelete = useCallback(
(conversationId: string) => {
onConversationDeleted(conversationId);
if (selectedConversationId === conversationId) {
const prevConversationId = getPreviousConversationId(
conversationIds,
selectedConversationId
);
onConversationSelected({
cId: getConvoId(conversations[prevConversationId].id, prevConversationId),
cTitle: prevConversationId,
});
}
},
[
selectedConversationId,
onConversationDeleted,
onConversationSelected,
conversationIds,
conversations,
]
);
const onChange = useCallback(
async (newOptions: ConversationSelectorOption[]) => {
if (newOptions.length === 0 || !newOptions?.[0].id) {
setSelectedOptions([]);
} else if (conversationOptions.findIndex((o) => o.id === newOptions?.[0].id) !== -1) {
const { id, label } = newOptions?.[0];
await onConversationSelected({ cId: getConvoId(id, label), cTitle: label });
}
},
[conversationOptions, onConversationSelected]
);
const onLeftArrowClick = useCallback(() => {
const prevId = getPreviousConversationId(conversationIds, selectedConversationId);
onConversationSelected({
cId: getConvoId(prevId, conversations[prevId]?.title),
cTitle: conversations[prevId]?.title,
});
}, [conversationIds, selectedConversationId, onConversationSelected, conversations]);
const onRightArrowClick = useCallback(() => {
const nextId = getNextConversationId(conversationIds, selectedConversationId);
onConversationSelected({
cId: getConvoId(nextId, conversations[nextId]?.title),
cTitle: conversations[nextId]?.title,
});
}, [conversationIds, selectedConversationId, onConversationSelected, conversations]);
useEffect(() => {
setSelectedOptions(conversationOptions.filter((c) => c.id === selectedConversationId));
}, [conversationOptions, selectedConversationId]);
const renderOption: (
option: ConversationSelectorOption,
searchValue: string
) => React.ReactNode = (option, searchValue) => {
const { label, id, value } = option;
return (
<EuiFlexGroup
alignItems="center"
className={'parentFlexGroup'}
component={'span'}
justifyContent="spaceBetween"
data-test-subj={`convo-option-${label}`}
>
<EuiFlexItem
component={'span'}
grow={false}
css={css`
width: calc(100% - 60px);
`}
>
<EuiHighlight
search={searchValue}
css={css`
overflow: hidden;
text-overflow: ellipsis;
`}
>
{label}
</EuiHighlight>
</EuiFlexItem>
{!value?.isDefault && id && (
<EuiFlexItem grow={false} component={'span'}>
<EuiToolTip position="right" content={i18n.DELETE_CONVERSATION}>
<EuiButtonIcon
iconType="cross"
aria-label={i18n.DELETE_CONVERSATION}
color="danger"
onClick={(e: React.MouseEvent) => {
e.stopPropagation();
onDelete(id);
}}
data-test-subj="delete-option"
css={css`
visibility: hidden;
.parentFlexGroup:hover & {
visibility: visible;
}
`}
/>
</EuiToolTip>
</EuiFlexItem>
)}
</EuiFlexGroup>
);
};
return (
<EuiFormRow
label={i18n.SELECTED_CONVERSATION_LABEL}
display="rowCompressed"
css={css`
min-width: 300px;
`}
>
<EuiComboBox
data-test-subj="conversation-selector"
aria-label={i18n.CONVERSATION_SELECTOR_ARIA_LABEL}
customOptionText={`${i18n.CONVERSATION_SELECTOR_CUSTOM_OPTION_TEXT} {searchValue}`}
placeholder={i18n.CONVERSATION_SELECTOR_PLACE_HOLDER}
singleSelection={{ asPlainText: true }}
options={conversationOptions}
selectedOptions={selectedOptions}
onChange={onChange}
onCreateOption={onCreateOption as unknown as () => void}
renderOption={renderOption}
compressed={true}
isDisabled={isDisabled}
prepend={
<EuiToolTip content={`${i18n.PREVIOUS_CONVERSATION_TITLE}`} display="block">
<EuiButtonIcon
iconType="arrowLeft"
aria-label={i18n.PREVIOUS_CONVERSATION_TITLE}
onClick={onLeftArrowClick}
disabled={isDisabled || conversationIds.length <= 1}
/>
</EuiToolTip>
}
append={
<EuiToolTip content={`${i18n.NEXT_CONVERSATION_TITLE}`} display="block">
<EuiButtonIcon
iconType="arrowRight"
aria-label={i18n.NEXT_CONVERSATION_TITLE}
onClick={onRightArrowClick}
disabled={isDisabled || conversationIds.length <= 1}
/>
</EuiToolTip>
}
/>
</EuiFormRow>
);
}
);
ConversationSelector.displayName = 'ConversationSelector';

View file

@ -18,7 +18,7 @@ import React, { useCallback, useMemo, useState } from 'react';
import { css } from '@emotion/react';
import { Conversation } from '../../../..';
import * as i18n from '../conversation_selector/translations';
import * as i18n from './translations';
import { SystemPromptSelectorOption } from '../../prompt_editor/system_prompt/system_prompt_modal/system_prompt_selector/system_prompt_selector';
import { ConversationSelectorSettingsOption } from './types';

View file

@ -56,13 +56,6 @@ export const CONVERSATIONS_TABLE_COLUMN_UPDATED_AT = i18n.translate(
}
);
export const CONVERSATIONS_TABLE_COLUMN_ACTIONS = i18n.translate(
'xpack.elasticAssistant.assistant.conversationSettings.column.actions',
{
defaultMessage: 'Actions',
}
);
export const CONVERSATIONS_FLYOUT_DEFAULT_TITLE = i18n.translate(
'xpack.elasticAssistant.assistant.conversationSettings.flyout.defaultTitle',
{

View file

@ -6,7 +6,6 @@
*/
import {
EuiComboBoxOptionOption,
EuiFlexGroup,
EuiFlexItem,
EuiButton,
@ -20,6 +19,7 @@ import useEvent from 'react-use/lib/useEvent';
import { css } from '@emotion/react';
import { isEmpty, findIndex, orderBy } from 'lodash';
import { DataStreamApis } from '../../use_data_stream_apis';
import { Conversation } from '../../../..';
import * as i18n from './translations';
@ -33,7 +33,7 @@ interface Props {
conversations: Record<string, Conversation>;
onConversationDeleted: (conversationId: string) => void;
onConversationCreate: () => void;
refetchConversationsState: () => Promise<void>;
refetchCurrentUserConversations: DataStreamApis['refetchCurrentUserConversations'];
}
const getCurrentConversationIndex = (
@ -69,11 +69,6 @@ const getNextConversation = (
? conversationList[0]
: conversationList[conversationIndex + 1];
};
export type ConversationSelectorOption = EuiComboBoxOptionOption<{
isDefault: boolean;
}>;
export const ConversationSidePanel = React.memo<Props>(
({
currentConversation,

View file

@ -1,72 +0,0 @@
/*
* 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, { useMemo } from 'react';
import { useController } from 'react-hook-form';
import { EuiFieldText, EuiFormRow } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import deepEqual from 'fast-deep-equal';
interface TitleFieldProps {
conversationIds?: string[];
euiFieldProps?: Record<string, unknown>;
}
const TitleFieldComponent = ({ conversationIds, euiFieldProps }: TitleFieldProps) => {
const {
field: { onChange, value, name: fieldName },
fieldState: { error },
} = useController({
name: 'title',
defaultValue: '',
rules: {
required: {
message: i18n.translate(
'xpack.elasticAssistant.conversationSidepanel.titleField.titleIsRequired',
{
defaultMessage: 'Title is required',
}
),
value: true,
},
validate: () => {
if (conversationIds?.includes(value)) {
return i18n.translate(
'xpack.elasticAssistant.conversationSidepanel.titleField.uniqueTitle',
{
defaultMessage: 'Title must be unique',
}
);
}
},
},
});
const hasError = useMemo(() => !!error?.message, [error?.message]);
return (
<EuiFormRow
label={i18n.translate('xpack.elasticAssistant.conversationSidepanel.titleFieldLabel', {
defaultMessage: 'Title',
})}
error={error?.message}
isInvalid={hasError}
fullWidth
>
<EuiFieldText
isInvalid={hasError}
onChange={onChange}
value={value}
name={fieldName}
fullWidth
data-test-subj="input"
{...euiFieldProps}
/>
</EuiFormRow>
);
};
export const TitleField = React.memo(TitleFieldComponent, deepEqual);

View file

@ -7,20 +7,6 @@
import { i18n } from '@kbn/i18n';
export const SELECTED_CONVERSATION_LABEL = i18n.translate(
'xpack.elasticAssistant.assistant.conversationSelector.defaultConversationTitle',
{
defaultMessage: 'Conversations',
}
);
export const NEXT_CONVERSATION_TITLE = i18n.translate(
'xpack.elasticAssistant.assistant.conversationSelector.nextConversationTitle',
{
defaultMessage: 'Next conversation',
}
);
export const DELETE_CONVERSATION_ARIA_LABEL = i18n.translate(
'xpack.elasticAssistant.assistant.sidePanel.deleteConversationAriaLabel',
{

View file

@ -6,107 +6,13 @@
*/
import {
getBlockBotConversation,
getDefaultConnector,
getOptionalRequestParams,
mergeBaseWithPersistedConversations,
} from './helpers';
import { enterpriseMessaging } from './use_conversation/sample_conversations';
import { AIConnector } from '../connectorland/connector_selector';
const defaultConversation = {
id: 'conversation_id',
category: 'assistant',
theme: {},
messages: [],
apiConfig: { actionTypeId: '.gen-ai', connectorId: '123' },
replacements: {},
title: 'conversation_id',
};
describe('helpers', () => {
describe('isAssistantEnabled = false', () => {
const isAssistantEnabled = false;
it('When no conversation history, return only enterprise messaging', () => {
const result = getBlockBotConversation(defaultConversation, isAssistantEnabled);
expect(result.messages).toEqual(enterpriseMessaging);
expect(result.messages.length).toEqual(1);
});
it('When conversation history and the last message is not enterprise messaging, appends enterprise messaging to conversation', () => {
const conversation = {
...defaultConversation,
messages: [
{
role: 'user' as const,
content: 'Hello',
timestamp: '',
presentation: {
delay: 0,
stream: false,
},
},
],
};
const result = getBlockBotConversation(conversation, isAssistantEnabled);
expect(result.messages.length).toEqual(2);
});
it('returns the conversation without changes when the last message is enterprise messaging', () => {
const conversation = {
...defaultConversation,
messages: enterpriseMessaging,
};
const result = getBlockBotConversation(conversation, isAssistantEnabled);
expect(result.messages.length).toEqual(1);
expect(result.messages).toEqual(enterpriseMessaging);
});
it('returns the conversation with new enterprise message when conversation has enterprise messaging, but not as the last message', () => {
const conversation = {
...defaultConversation,
messages: [
...enterpriseMessaging,
{
role: 'user' as const,
content: 'Hello',
timestamp: '',
presentation: {
delay: 0,
stream: false,
},
},
],
};
const result = getBlockBotConversation(conversation, isAssistantEnabled);
expect(result.messages.length).toEqual(3);
});
});
describe('isAssistantEnabled = true', () => {
const isAssistantEnabled = true;
it('when no conversation history, returns the welcome conversation', () => {
const result = getBlockBotConversation(defaultConversation, isAssistantEnabled);
expect(result.messages.length).toEqual(0);
});
it('returns a conversation history with the welcome conversation appended', () => {
const conversation = {
...defaultConversation,
messages: [
{
role: 'user' as const,
content: 'Hello',
timestamp: '',
presentation: {
delay: 0,
stream: false,
},
},
],
};
const result = getBlockBotConversation(conversation, isAssistantEnabled);
expect(result.messages.length).toEqual(1);
});
});
describe('getDefaultConnector', () => {
const defaultConnector: AIConnector = {
actionTypeId: '.gen-ai',

View file

@ -10,7 +10,6 @@ import { AIConnector } from '../connectorland/connector_selector';
import { FetchConnectorExecuteResponse, FetchConversationsResponse } from './api';
import { Conversation } from '../..';
import type { ClientMessage } from '../assistant_context/types';
import { enterpriseMessaging } from './use_conversation/sample_conversations';
export const getMessageFromRawResponse = (
rawResponse: FetchConnectorExecuteResponse
@ -54,31 +53,6 @@ export const mergeBaseWithPersistedConversations = (
return transformed;
}, {});
};
export const getBlockBotConversation = (
conversation: Conversation,
isAssistantEnabled: boolean
): Conversation => {
if (!isAssistantEnabled) {
if (
conversation.messages.length === 0 ||
conversation.messages[conversation.messages.length - 1].content !==
enterpriseMessaging[0].content
) {
return {
...conversation,
messages: [...conversation.messages, ...enterpriseMessaging],
};
}
return conversation;
}
return {
...conversation,
messages: conversation.messages,
};
};
/**
* Returns a default connector if there is only one connector
* @param connectors

View file

@ -16,7 +16,6 @@ import { useLoadConnectors } from '../connectorland/use_load_connectors';
import { DefinedUseQueryResult, UseQueryResult } from '@tanstack/react-query';
import { useLocalStorage, useSessionStorage } from 'react-use';
import { PromptEditor } from './prompt_editor';
import { QuickPrompts } from './quick_prompts/quick_prompts';
import { mockAssistantAvailability, TestProviders } from '../mock/test_providers/test_providers';
import { useFetchCurrentUserConversations } from './api';
@ -30,19 +29,11 @@ jest.mock('../connectorland/use_load_connectors');
jest.mock('../connectorland/connector_setup');
jest.mock('react-use');
jest.mock('./prompt_editor', () => ({ PromptEditor: jest.fn() }));
jest.mock('./quick_prompts/quick_prompts', () => ({ QuickPrompts: jest.fn() }));
jest.mock('./api/conversations/use_fetch_current_user_conversations');
jest.mock('./use_conversation');
const renderAssistant = (extraProps = {}, providerProps = {}) =>
render(
<TestProviders>
<Assistant chatHistoryVisible={true} setChatHistoryVisible={jest.fn()} {...extraProps} />
</TestProviders>
);
const mockData = {
welcome_id: {
id: 'welcome_id',
@ -61,6 +52,29 @@ const mockData = {
replacements: {},
},
};
const renderAssistant = async (extraProps = {}, providerProps = {}) => {
const chatSendSpy = jest.spyOn(all, 'useChatSend');
const assistant = render(
<TestProviders>
<Assistant
conversationTitle={'Welcome'}
chatHistoryVisible={true}
setChatHistoryVisible={jest.fn()}
{...extraProps}
/>
</TestProviders>
);
await waitFor(() => {
// wait for conversation to mount before performing any tests
expect(chatSendSpy).toHaveBeenLastCalledWith(
expect.objectContaining({
currentConversation: mockData.welcome_id,
})
);
});
return assistant;
};
const mockDeleteConvo = jest.fn();
const mockGetDefaultConversation = jest.fn().mockReturnValue(mockData.welcome_id);
const clearConversation = jest.fn();
@ -84,7 +98,6 @@ describe('Assistant', () => {
persistToSessionStorage = jest.fn();
(useConversation as jest.Mock).mockReturnValue(mockUseConversation);
jest.mocked(PromptEditor).mockReturnValue(null);
jest.mocked(QuickPrompts).mockReturnValue(null);
const connectors: unknown[] = [
{
@ -127,17 +140,9 @@ describe('Assistant', () => {
});
describe('persistent storage', () => {
it('should refetchConversationsState after settings save button click', async () => {
it('should refetchCurrentUserConversations after settings save button click', async () => {
const chatSendSpy = jest.spyOn(all, 'useChatSend');
const setConversationTitle = jest.fn();
renderAssistant({ setConversationTitle });
expect(chatSendSpy).toHaveBeenLastCalledWith(
expect.objectContaining({
currentConversation: mockData.welcome_id,
})
);
await renderAssistant();
fireEvent.click(screen.getByTestId('settings'));
@ -181,7 +186,7 @@ describe('Assistant', () => {
);
});
it('should refetchConversationsState after settings save button click, but do not update convos when refetch returns bad results', async () => {
it('should refetchCurrentUserConversations after settings save button click, but do not update convos when refetch returns bad results', async () => {
jest.mocked(useFetchCurrentUserConversations).mockReturnValue({
data: mockData,
isLoading: false,
@ -192,9 +197,7 @@ describe('Assistant', () => {
isFetched: true,
} as unknown as DefinedUseQueryResult<Record<string, Conversation>, unknown>);
const chatSendSpy = jest.spyOn(all, 'useChatSend');
const setConversationTitle = jest.fn();
renderAssistant({ setConversationTitle });
await renderAssistant();
fireEvent.click(screen.getByTestId('settings'));
await act(async () => {
@ -216,7 +219,7 @@ describe('Assistant', () => {
});
it('should delete conversation when delete button is clicked', async () => {
renderAssistant();
await renderAssistant();
const deleteButton = screen.getAllByTestId('delete-option')[0];
await act(async () => {
fireEvent.click(deleteButton);
@ -230,8 +233,8 @@ describe('Assistant', () => {
expect(mockDeleteConvo).toHaveBeenCalledWith(mockData.electric_sheep_id.id);
});
});
it('should refetchConversationsState after clear chat history button click', async () => {
renderAssistant();
it('should refetchCurrentUserConversations after clear chat history button click', async () => {
await renderAssistant();
fireEvent.click(screen.getByTestId('chat-context-menu'));
fireEvent.click(screen.getByTestId('clear-chat'));
fireEvent.click(screen.getByTestId('confirmModalConfirmButton'));
@ -248,14 +251,13 @@ describe('Assistant', () => {
...mockUseConversation,
getConversation,
});
renderAssistant();
await renderAssistant();
expect(persistToLocalStorage).toHaveBeenCalled();
expect(persistToLocalStorage).toHaveBeenLastCalledWith(mockData.welcome_id.id);
const previousConversationButton = await screen.findByText(mockData.electric_sheep_id.title);
expect(previousConversationButton).toBeInTheDocument();
await act(async () => {
fireEvent.click(previousConversationButton);
@ -290,7 +292,7 @@ describe('Assistant', () => {
isFetched: true,
} as unknown as DefinedUseQueryResult<Record<string, Conversation>, unknown>);
const { findByText } = renderAssistant();
const { findByText } = await renderAssistant();
expect(persistToLocalStorage).toHaveBeenCalled();
@ -305,37 +307,16 @@ describe('Assistant', () => {
});
expect(persistToLocalStorage).toHaveBeenLastCalledWith(mockData.welcome_id.id);
});
it('should call the setConversationTitle callback if it is defined and the conversation id changes', async () => {
const getConversation = jest.fn().mockResolvedValue(mockData.electric_sheep_id);
(useConversation as jest.Mock).mockReturnValue({
...mockUseConversation,
getConversation,
});
const setConversationTitle = jest.fn();
renderAssistant({ setConversationTitle });
await act(async () => {
fireEvent.click(await screen.findByText(mockData.electric_sheep_id.title));
});
expect(setConversationTitle).toHaveBeenLastCalledWith('electric sheep');
});
it('should fetch current conversation when id has value', async () => {
const getConversation = jest
.fn()
.mockResolvedValue({ ...mockData.electric_sheep_id, title: 'updated title' });
(useConversation as jest.Mock).mockReturnValue({
...mockUseConversation,
getConversation,
});
const refetch = jest.fn();
jest.mocked(useFetchCurrentUserConversations).mockReturnValue({
data: {
...mockData,
electric_sheep_id: { ...mockData.electric_sheep_id, title: 'updated title' },
},
isLoading: false,
refetch: jest.fn().mockResolvedValue({
refetch: refetch.mockResolvedValue({
isLoading: false,
data: {
...mockData,
@ -344,14 +325,14 @@ describe('Assistant', () => {
}),
isFetched: true,
} as unknown as DefinedUseQueryResult<Record<string, Conversation>, unknown>);
renderAssistant();
await renderAssistant();
const previousConversationButton = await screen.findByText('updated title');
await act(async () => {
fireEvent.click(previousConversationButton);
});
expect(getConversation).toHaveBeenCalledWith('electric_sheep_id');
expect(refetch).toHaveBeenCalled();
expect(persistToLocalStorage).toHaveBeenLastCalledWith('electric_sheep_id');
});
@ -376,7 +357,7 @@ describe('Assistant', () => {
}),
isFetched: true,
} as unknown as DefinedUseQueryResult<Record<string, Conversation>, unknown>);
renderAssistant();
await renderAssistant();
const previousConversationButton = screen.getByLabelText('Previous conversation');
await act(async () => {
@ -396,7 +377,7 @@ describe('Assistant', () => {
describe('when no connectors are loaded', () => {
it('should set welcome conversation id in local storage', async () => {
renderAssistant();
await renderAssistant();
expect(persistToLocalStorage).toHaveBeenCalled();
expect(persistToLocalStorage).toHaveBeenLastCalledWith(mockData.welcome_id.id);
@ -405,7 +386,7 @@ describe('Assistant', () => {
describe('when not authorized', () => {
it('should be disabled', async () => {
const { queryByTestId } = renderAssistant(
const { queryByTestId } = await renderAssistant(
{},
{
assistantAvailability: { ...mockAssistantAvailability, isAssistantEnabled: false },

View file

@ -12,7 +12,6 @@ import React, {
useEffect,
useLayoutEffect,
useMemo,
useRef,
useState,
} from 'react';
import {
@ -24,44 +23,32 @@ import {
EuiFlyoutFooter,
EuiFlyoutHeader,
EuiFlyoutBody,
EuiText,
} from '@elastic/eui';
import { euiThemeVars } from '@kbn/ui-theme';
import { createPortal } from 'react-dom';
import { css } from '@emotion/react';
import styled from '@emotion/styled';
import deepEqual from 'fast-deep-equal';
import { find, isEmpty, uniqBy } from 'lodash';
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { PromptTypeEnum } from '@kbn/elastic-assistant-common/impl/schemas/prompts/bulk_crud_prompts_route.gen';
import { isEmpty } from 'lodash';
import { AssistantBody } from './assistant_body';
import { useCurrentConversation } from './use_current_conversation';
import { useDataStreamApis } from './use_data_stream_apis';
import { useChatSend } from './chat_send/use_chat_send';
import { ChatSend } from './chat_send';
import { BlockBotCallToAction } from './block_bot/cta';
import { WELCOME_CONVERSATION_TITLE } from './use_conversation/translations';
import {
getDefaultConnector,
getBlockBotConversation,
mergeBaseWithPersistedConversations,
sleep,
} from './helpers';
import { getDefaultConnector } from './helpers';
import { useAssistantContext, UserAvatar } from '../assistant_context';
import { ContextPills } from './context_pills';
import { getNewSelectedPromptContext } from '../data_anonymization/get_new_selected_prompt_context';
import type { PromptContext, SelectedPromptContext } from './prompt_context/types';
import { useConversation } from './use_conversation';
import { CodeBlockDetails, getDefaultSystemPrompt } from './use_conversation/helpers';
import { CodeBlockDetails } from './use_conversation/helpers';
import { QuickPrompts } from './quick_prompts/quick_prompts';
import { useLoadConnectors } from '../connectorland/use_load_connectors';
import { ConnectorSetup } from '../connectorland/connector_setup';
import { ConnectorMissingCallout } from '../connectorland/connector_missing_callout';
import { ConversationSidePanel } from './conversations/conversation_sidepanel';
import { NEW_CHAT } from './conversations/conversation_sidepanel/translations';
import { SystemPrompt } from './prompt_editor/system_prompt';
import { SelectedPromptContexts } from './prompt_editor/selected_prompt_contexts';
import { AssistantHeader } from './assistant_header';
import * as i18n from './translations';
export const CONVERSATION_SIDE_PANEL_WIDTH = 220;
@ -71,29 +58,14 @@ const CommentContainer = styled('span')`
overflow: hidden;
`;
import {
FetchConversationsResponse,
useFetchCurrentUserConversations,
CONVERSATIONS_QUERY_KEYS,
} from './api/conversations/use_fetch_current_user_conversations';
import { Conversation } from '../assistant_context/types';
import { getGenAiConfig } from '../connectorland/helpers';
import { AssistantAnimatedIcon } from './assistant_animated_icon';
import { useFetchAnonymizationFields } from './api/anonymization_fields/use_fetch_anonymization_fields';
import { SetupKnowledgeBaseButton } from '../knowledge_base/setup_knowledge_base_button';
import { useFetchPrompts } from './api/prompts/use_fetch_prompts';
export interface Props {
conversationTitle?: string;
embeddedLayout?: boolean;
promptContextId?: string;
shouldRefocusPrompt?: boolean;
showTitle?: boolean;
setConversationTitle?: Dispatch<SetStateAction<string>>;
onCloseFlyout?: () => void;
chatHistoryVisible?: boolean;
setChatHistoryVisible?: Dispatch<SetStateAction<boolean>>;
conversationTitle?: string;
currentUserAvatar?: UserAvatar;
onCloseFlyout?: () => void;
promptContextId?: string;
setChatHistoryVisible?: Dispatch<SetStateAction<boolean>>;
shouldRefocusPrompt?: boolean;
}
/**
@ -101,37 +73,26 @@ export interface Props {
* quick prompts for common actions, settings, and prompt context providers.
*/
const AssistantComponent: React.FC<Props> = ({
conversationTitle,
embeddedLayout = false,
promptContextId = '',
shouldRefocusPrompt = false,
showTitle = true,
setConversationTitle,
onCloseFlyout,
chatHistoryVisible,
setChatHistoryVisible,
conversationTitle,
currentUserAvatar,
onCloseFlyout,
promptContextId = '',
setChatHistoryVisible,
shouldRefocusPrompt = false,
}) => {
const {
assistantAvailability: { isAssistantEnabled },
assistantTelemetry,
augmentMessageCodeBlocks,
assistantAvailability: { isAssistantEnabled },
baseConversations,
getComments,
getLastConversationId,
http,
promptContexts,
setLastConversationId,
getLastConversationId,
baseConversations,
} = useAssistantContext();
const {
getDefaultConversation,
getConversation,
deleteConversation,
setApiConfig,
createConversation,
} = useConversation();
const [selectedPromptContexts, setSelectedPromptContexts] = useState<
Record<string, SelectedPromptContext>
>({});
@ -141,172 +102,81 @@ const AssistantComponent: React.FC<Props> = ({
[selectedPromptContexts]
);
const onFetchedConversations = useCallback(
(conversationsData: FetchConversationsResponse): Record<string, Conversation> =>
mergeBaseWithPersistedConversations(baseConversations, conversationsData),
[baseConversations]
);
const [isStreaming, setIsStreaming] = useState(false);
const {
data: conversations,
isLoading,
refetch: refetchResults,
isFetched: conversationsLoaded,
} = useFetchCurrentUserConversations({
http,
onFetch: onFetchedConversations,
refetchOnWindowFocus: !isStreaming,
isAssistantEnabled,
});
const {
data: anonymizationFields,
isLoading: isLoadingAnonymizationFields,
isError: isErrorAnonymizationFields,
isFetched: isFetchedAnonymizationFields,
} = useFetchAnonymizationFields();
const {
data: { data: allPrompts },
refetch: refetchPrompts,
isLoading: isLoadingPrompts,
} = useFetchPrompts();
const allSystemPrompts = useMemo(() => {
if (!isLoadingPrompts) {
return allPrompts.filter((p) => p.promptType === PromptTypeEnum.system);
}
return [];
}, [allPrompts, isLoadingPrompts]);
allPrompts,
allSystemPrompts,
anonymizationFields,
conversations,
isErrorAnonymizationFields,
isFetchedAnonymizationFields,
isFetchedCurrentUserConversations,
isFetchedPrompts,
isLoadingAnonymizationFields,
isLoadingCurrentUserConversations,
refetchPrompts,
refetchCurrentUserConversations,
setIsStreaming,
} = useDataStreamApis({ http, baseConversations, isAssistantEnabled });
// Connector details
const { data: connectors, isFetchedAfterMount: areConnectorsFetched } = useLoadConnectors({
const { data: connectors, isFetchedAfterMount: isFetchedConnectors } = useLoadConnectors({
http,
});
const defaultConnector = useMemo(() => getDefaultConnector(connectors), [connectors]);
const [currentConversationId, setCurrentConversationId] = useState<string | undefined>();
const [currentConversation, setCurrentConversation] = useState<Conversation | undefined>();
useEffect(() => {
if (setConversationTitle && currentConversation?.title) {
setConversationTitle(currentConversation?.title);
}
}, [currentConversation?.title, setConversationTitle]);
const refetchCurrentConversation = useCallback(
async ({
cId,
cTitle,
isStreamRefetch = false,
}: { cId?: string; cTitle?: string; isStreamRefetch?: boolean } = {}) => {
if (cId === '' || (cTitle && !conversations[cTitle])) {
return;
}
const conversationId = cId ?? (cTitle && conversations[cTitle].id) ?? currentConversation?.id;
if (conversationId) {
let updatedConversation = await getConversation(conversationId);
let retries = 0;
const maxRetries = 5;
// this retry is a workaround for the stream not YET being persisted to the stored conversation
while (
isStreamRefetch &&
updatedConversation &&
updatedConversation.messages[updatedConversation.messages.length - 1].role !==
'assistant' &&
retries < maxRetries
) {
retries++;
await sleep(2000);
updatedConversation = await getConversation(conversationId);
}
if (updatedConversation) {
setCurrentConversation(updatedConversation);
}
return updatedConversation;
}
},
[conversations, currentConversation?.id, getConversation]
);
useEffect(() => {
if (areConnectorsFetched && conversationsLoaded && Object.keys(conversations).length > 0) {
setCurrentConversation((prev) => {
const nextConversation =
(currentConversationId && conversations[currentConversationId]) ||
(isAssistantEnabled &&
(conversations[getLastConversationId(conversationTitle)] ||
find(conversations, ['title', getLastConversationId(conversationTitle)]))) ||
find(conversations, ['title', getLastConversationId(WELCOME_CONVERSATION_TITLE)]);
if (deepEqual(prev, nextConversation)) return prev;
const conversationToReturn =
(nextConversation &&
conversations[
nextConversation?.id !== '' ? nextConversation?.id : nextConversation?.title
]) ??
conversations[WELCOME_CONVERSATION_TITLE] ??
getDefaultConversation({ cTitle: WELCOME_CONVERSATION_TITLE });
// updated selected system prompt
setEditingSystemPromptId(
getDefaultSystemPrompt({
allSystemPrompts,
conversation: conversationToReturn,
})?.id
);
if (
prev &&
prev.id === conversationToReturn.id &&
// if the conversation id has not changed and the previous conversation has more messages
// it is because the local conversation has a readable stream running
// and it has not yet been persisted to the stored conversation
prev.messages.length > conversationToReturn.messages.length
) {
return {
...conversationToReturn,
messages: prev.messages,
};
}
return conversationToReturn;
});
}
}, [
const {
currentConversation,
currentSystemPromptId,
handleCreateConversation,
handleOnConversationDeleted,
handleOnConversationSelected,
refetchCurrentConversation,
setCurrentConversation,
setCurrentSystemPromptId,
} = useCurrentConversation({
allSystemPrompts,
areConnectorsFetched,
conversationTitle,
conversations,
conversationsLoaded,
currentConversationId,
getDefaultConversation,
getLastConversationId,
defaultConnector,
refetchCurrentUserConversations,
conversationId: getLastConversationId(conversationTitle),
mayUpdateConversations:
isFetchedConnectors &&
isFetchedCurrentUserConversations &&
isFetchedPrompts &&
Object.keys(conversations).length > 0,
});
const isInitialLoad = useMemo(() => {
if (!isAssistantEnabled) {
return false;
}
return (
(!isFetchedAnonymizationFields && !isFetchedCurrentUserConversations && !isFetchedPrompts) ||
!(currentConversation && currentConversation?.id !== '')
);
}, [
currentConversation,
isAssistantEnabled,
isFetchedAnonymizationFields,
isFetchedCurrentUserConversations,
isFetchedPrompts,
]);
// Welcome setup state
const isWelcomeSetup = useMemo(() => {
// if any conversation has a connector id, we're not in welcome set up
return Object.keys(conversations).some(
(conversation) => conversations[conversation]?.apiConfig?.connectorId != null
)
? false
: (connectors?.length ?? 0) === 0;
}, [connectors?.length, conversations]);
const isDisabled = isWelcomeSetup || !isAssistantEnabled;
// Welcome conversation is a special 'setup' case when no connector exists, mostly extracted to `ConnectorSetup` component,
// but currently a bit of state is littered throughout the assistant component. TODO: clean up/isolate this state
const blockBotConversation = useMemo(
() => currentConversation && getBlockBotConversation(currentConversation, isAssistantEnabled),
[currentConversation, isAssistantEnabled]
const isWelcomeSetup = useMemo(
() =>
Object.keys(conversations).some(
(conversation) =>
// if any conversation has a non-empty connector id, we're not in welcome set up
conversations[conversation]?.apiConfig?.connectorId != null &&
conversations[conversation]?.apiConfig?.connectorId !== ''
)
? false
: (connectors?.length ?? 0) === 0,
[connectors?.length, conversations]
);
const isDisabled = useMemo(
() => isWelcomeSetup || !isAssistantEnabled || isInitialLoad,
[isWelcomeSetup, isAssistantEnabled, isInitialLoad]
);
// Settings modal state (so it isn't shared between assistant instances like Timeline)
@ -315,7 +185,7 @@ const AssistantComponent: React.FC<Props> = ({
// Remember last selection for reuse after keyboard shortcut is pressed.
// Clear it if there is no connectors
useEffect(() => {
if (areConnectorsFetched && !connectors?.length) {
if (isFetchedConnectors && !connectors?.length) {
return setLastConversationId(WELCOME_CONVERSATION_TITLE);
}
@ -325,17 +195,15 @@ const AssistantComponent: React.FC<Props> = ({
);
}
}, [
areConnectorsFetched,
isFetchedConnectors,
connectors?.length,
conversations,
currentConversation,
isLoading,
isLoadingCurrentUserConversations,
setLastConversationId,
]);
const [autoPopulatedOnce, setAutoPopulatedOnce] = useState<boolean>(false);
const [userPrompt, setUserPrompt] = useState<string | null>(null);
const [showAnonymizedValues, setShowAnonymizedValues] = useState<boolean>(false);
const [messageCodeBlocks, setMessageCodeBlocks] = useState<CodeBlockDetails[][]>();
@ -352,7 +220,12 @@ const AssistantComponent: React.FC<Props> = ({
// Show missing connector callout if no connectors are configured
const showMissingConnectorCallout = useMemo(() => {
if (!isLoading && areConnectorsFetched && currentConversation?.id !== '') {
if (
!isLoadingCurrentUserConversations &&
isFetchedConnectors &&
currentConversation &&
currentConversation.id !== ''
) {
if (!currentConversation?.apiConfig?.connectorId) {
return true;
}
@ -363,13 +236,7 @@ const AssistantComponent: React.FC<Props> = ({
}
return false;
}, [
areConnectorsFetched,
connectors,
currentConversation?.apiConfig?.connectorId,
currentConversation?.id,
isLoading,
]);
}, [isFetchedConnectors, connectors, currentConversation, isLoadingCurrentUserConversations]);
const isSendingDisabled = useMemo(() => {
return isDisabled || showMissingConnectorCallout;
@ -393,72 +260,6 @@ const AssistantComponent: React.FC<Props> = ({
}, []);
// End drill in `Add To Timeline` action
// Start Scrolling
const commentsContainerRef = useRef<HTMLDivElement | null>(null);
useEffect(() => {
const parent = commentsContainerRef.current?.parentElement;
if (!parent) {
return;
}
// when scrollHeight changes, parent is scrolled to bottom
parent.scrollTop = parent.scrollHeight;
(
commentsContainerRef.current?.childNodes[0].childNodes[0] as HTMLElement
).lastElementChild?.scrollIntoView();
});
// End Scrolling
const selectedSystemPrompt = useMemo(
() =>
getDefaultSystemPrompt({
allSystemPrompts,
conversation: currentConversation,
}),
[allSystemPrompts, currentConversation]
);
const [editingSystemPromptId, setEditingSystemPromptId] = useState<string | undefined>(
selectedSystemPrompt?.id
);
const handleOnConversationSelected = useCallback(
async ({ cId, cTitle }: { cId: string; cTitle: string }) => {
const updatedConv = await refetchResults();
let selectedConversation;
if (cId === '') {
setCurrentConversationId(cTitle);
selectedConversation = updatedConv?.data?.[cTitle];
setCurrentConversationId(cTitle);
} else {
selectedConversation = await refetchCurrentConversation({ cId });
setCurrentConversationId(cId);
}
setEditingSystemPromptId(
getDefaultSystemPrompt({
allSystemPrompts,
conversation: selectedConversation,
})?.id
);
},
[allSystemPrompts, refetchCurrentConversation, refetchResults]
);
const handleOnConversationDeleted = useCallback(
async (cTitle: string) => {
await deleteConversation(conversations[cTitle].id);
await refetchResults();
},
[conversations, deleteConversation, refetchResults]
);
const handleOnSystemPromptSelectionChange = useCallback((systemPromptId?: string) => {
setEditingSystemPromptId(systemPromptId);
}, []);
// Add min-height to all codeblocks so timeline icon doesn't overflow
const codeBlockContainers = [...document.getElementsByClassName('euiCodeBlock')];
// @ts-ignore-expect-error
@ -469,10 +270,36 @@ const AssistantComponent: React.FC<Props> = ({
setShowAnonymizedValues((prevValue) => !prevValue);
}, [setShowAnonymizedValues]);
const isNewConversation = useMemo(
() => currentConversation?.messages.length === 0,
[currentConversation?.messages.length]
);
const {
abortStream,
handleOnChatCleared: onChatCleared,
handleChatSend,
handleRegenerateResponse,
isLoading: isLoadingChatSend,
setUserPrompt,
userPrompt,
} = useChatSend({
allSystemPrompts,
currentConversation,
currentSystemPromptId,
http,
refetchCurrentUserConversations,
selectedPromptContexts,
setSelectedPromptContexts,
setCurrentConversation,
});
const handleOnChatCleared = useCallback(() => {
onChatCleared();
if (!currentSystemPromptId) {
setCurrentSystemPromptId(currentConversation?.apiConfig?.defaultSystemPromptId);
}
}, [
currentConversation?.apiConfig?.defaultSystemPromptId,
currentSystemPromptId,
onChatCleared,
setCurrentSystemPromptId,
]);
useEffect(() => {
// Adding `conversationTitle !== selectedConversationTitle` to prevent auto-run still executing after changing selected conversation
@ -526,6 +353,7 @@ const AssistantComponent: React.FC<Props> = ({
isErrorAnonymizationFields,
anonymizationFields,
isFetchedAnonymizationFields,
setUserPrompt,
]);
const createCodeBlockPortals = useCallback(
@ -548,40 +376,6 @@ const AssistantComponent: React.FC<Props> = ({
[messageCodeBlocks]
);
const {
abortStream,
handleOnChatCleared: onChatCleared,
handlePromptChange,
handleSendMessage,
handleRegenerateResponse,
isLoading: isLoadingChatSend,
} = useChatSend({
allSystemPrompts,
currentConversation,
setUserPrompt,
editingSystemPromptId,
http,
setEditingSystemPromptId,
selectedPromptContexts,
setSelectedPromptContexts,
setCurrentConversation,
});
const handleOnChatCleared = useCallback(async () => {
await onChatCleared();
await refetchResults();
}, [onChatCleared, refetchResults]);
const handleChatSend = useCallback(
async (promptText: string) => {
await handleSendMessage(promptText);
if (currentConversation?.title === NEW_CHAT) {
await refetchResults();
}
},
[currentConversation, handleSendMessage, refetchResults]
);
const comments = useMemo(
() => (
<>
@ -619,6 +413,7 @@ const AssistantComponent: React.FC<Props> = ({
refetchCurrentConversation,
handleRegenerateResponse,
isLoadingChatSend,
setIsStreaming,
currentUserAvatar,
selectedPromptContextsCount,
]
@ -636,206 +431,6 @@ const AssistantComponent: React.FC<Props> = ({
[assistantTelemetry, currentConversation?.title]
);
const refetchConversationsState = useCallback(async () => {
await refetchResults();
}, [refetchResults]);
const queryClient = useQueryClient();
const { mutateAsync } = useMutation<Conversation | undefined, unknown, Conversation>(
['SET_DEFAULT_CONNECTOR'],
{
mutationFn: async (payload) => {
const apiConfig = getGenAiConfig(defaultConnector);
return setApiConfig({
conversation: payload,
apiConfig: {
...payload?.apiConfig,
connectorId: (defaultConnector?.id as string) ?? '',
actionTypeId: (defaultConnector?.actionTypeId as string) ?? '.gen-ai',
provider: apiConfig?.apiProvider,
model: apiConfig?.defaultModel,
defaultSystemPromptId: allSystemPrompts.find((sp) => sp.isNewConversationDefault)?.id,
},
});
},
onSuccess: async (data) => {
await queryClient.cancelQueries({ queryKey: CONVERSATIONS_QUERY_KEYS });
if (data) {
queryClient.setQueryData<{ data: Conversation[] }>(CONVERSATIONS_QUERY_KEYS, (prev) => ({
...(prev ?? {}),
data: uniqBy([data, ...(prev?.data ?? [])], 'id'),
}));
}
return data;
},
}
);
useEffect(() => {
(async () => {
if (areConnectorsFetched && currentConversation?.id === '' && !isLoadingPrompts) {
const conversation = await mutateAsync(currentConversation);
if (currentConversation.id === '' && conversation) {
setCurrentConversationId(conversation.id);
}
}
})();
}, [areConnectorsFetched, currentConversation, isLoadingPrompts, mutateAsync]);
const handleCreateConversation = useCallback(async () => {
const newChatExists = find(conversations, ['title', NEW_CHAT]);
if (newChatExists && !newChatExists.messages.length) {
handleOnConversationSelected({
cId: newChatExists.id,
cTitle: newChatExists.title,
});
return;
}
const newConversation = await createConversation({
title: NEW_CHAT,
apiConfig: currentConversation?.apiConfig,
});
await refetchConversationsState();
if (newConversation) {
handleOnConversationSelected({
cId: newConversation.id,
cTitle: newConversation.title,
});
}
}, [
conversations,
createConversation,
currentConversation?.apiConfig,
handleOnConversationSelected,
refetchConversationsState,
]);
const disclaimer = useMemo(
() =>
isNewConversation && (
<EuiText
data-test-subj="assistant-disclaimer"
textAlign="center"
color={euiThemeVars.euiColorMediumShade}
size="xs"
css={css`
margin: 0 ${euiThemeVars.euiSizeL} ${euiThemeVars.euiSizeM} ${euiThemeVars.euiSizeL};
`}
>
{i18n.DISCLAIMER}
</EuiText>
),
[isNewConversation]
);
const flyoutBodyContent = useMemo(() => {
if (isWelcomeSetup) {
return (
<EuiFlexGroup alignItems="center" justifyContent="center">
<EuiFlexItem grow={false}>
<EuiPanel
hasShadow={false}
css={css`
max-width: 400px;
text-align: center;
`}
>
<EuiFlexGroup alignItems="center" justifyContent="center" direction="column">
<EuiFlexItem grow={false}>
<AssistantAnimatedIcon />
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiText>
<h3>{i18n.WELCOME_SCREEN_TITLE}</h3>
</EuiText>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiText color="subdued">
<p>{i18n.WELCOME_SCREEN_DESCRIPTION}</p>
</EuiText>
</EuiFlexItem>
<EuiFlexItem grow={false} data-test-subj="connector-prompt">
<ConnectorSetup
conversation={blockBotConversation}
onConversationUpdate={handleOnConversationSelected}
/>
</EuiFlexItem>
</EuiFlexGroup>
</EuiPanel>
</EuiFlexItem>
</EuiFlexGroup>
);
}
if (currentConversation?.messages.length === 0) {
return (
<EuiFlexGroup alignItems="center" justifyContent="center">
<EuiFlexItem grow={false}>
<EuiPanel
hasShadow={false}
css={css`
max-width: 400px;
text-align: center;
`}
>
<EuiFlexGroup alignItems="center" justifyContent="center" direction="column">
<EuiFlexItem grow={false}>
<AssistantAnimatedIcon />
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiText>
<h3>{i18n.EMPTY_SCREEN_TITLE}</h3>
<p>{i18n.EMPTY_SCREEN_DESCRIPTION}</p>
</EuiText>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<SystemPrompt
conversation={currentConversation}
editingSystemPromptId={editingSystemPromptId}
onSystemPromptSelectionChange={handleOnSystemPromptSelectionChange}
isSettingsModalVisible={isSettingsModalVisible}
setIsSettingsModalVisible={setIsSettingsModalVisible}
allSystemPrompts={allSystemPrompts}
refetchConversations={refetchResults}
/>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<SetupKnowledgeBaseButton />
</EuiFlexItem>
</EuiFlexGroup>
</EuiPanel>
</EuiFlexItem>
</EuiFlexGroup>
);
}
return (
<EuiPanel
hasShadow={false}
panelRef={(element) => {
commentsContainerRef.current = (element?.parentElement as HTMLDivElement) || null;
}}
>
{comments}
</EuiPanel>
);
}, [
allSystemPrompts,
blockBotConversation,
comments,
currentConversation,
editingSystemPromptId,
handleOnConversationSelected,
handleOnSystemPromptSelectionChange,
isSettingsModalVisible,
isWelcomeSetup,
refetchResults,
]);
return (
<EuiFlexGroup direction={'row'} wrap={false} gutterSize="none">
{chatHistoryVisible && (
@ -852,7 +447,7 @@ const AssistantComponent: React.FC<Props> = ({
conversations={conversations}
onConversationDeleted={handleOnConversationDeleted}
onConversationCreate={handleCreateConversation}
refetchConversationsState={refetchConversationsState}
refetchCurrentUserConversations={refetchCurrentUserConversations}
/>
</EuiFlexItem>
)}
@ -874,6 +469,7 @@ const AssistantComponent: React.FC<Props> = ({
>
<EuiFlyoutHeader hasBorder>
<AssistantHeader
isLoading={isInitialLoad}
selectedConversation={currentConversation}
defaultConnector={defaultConnector}
isDisabled={isDisabled || isLoadingChatSend}
@ -887,8 +483,8 @@ const AssistantComponent: React.FC<Props> = ({
setChatHistoryVisible={setChatHistoryVisible}
onConversationSelected={handleOnConversationSelected}
conversations={conversations}
conversationsLoaded={conversationsLoaded}
refetchConversationsState={refetchConversationsState}
conversationsLoaded={isFetchedCurrentUserConversations}
refetchCurrentUserConversations={refetchCurrentUserConversations}
onConversationCreate={handleCreateConversation}
isAssistantEnabled={isAssistantEnabled}
refetchPrompts={refetchPrompts}
@ -921,7 +517,7 @@ const AssistantComponent: React.FC<Props> = ({
banner={
!isDisabled &&
showMissingConnectorCallout &&
areConnectorsFetched && (
isFetchedConnectors && (
<ConnectorMissingCallout
isConnectorConfigured={(connectors?.length ?? 0) > 0}
isSettingsModalVisible={isSettingsModalVisible}
@ -930,24 +526,21 @@ const AssistantComponent: React.FC<Props> = ({
)
}
>
{!isAssistantEnabled ? (
<BlockBotCallToAction
connectorPrompt={
<ConnectorSetup
conversation={blockBotConversation}
onConversationUpdate={handleOnConversationSelected}
/>
}
http={http}
isAssistantEnabled={isAssistantEnabled}
isWelcomeSetup={isWelcomeSetup}
/>
) : (
<EuiFlexGroup direction="column" justifyContent="spaceBetween">
<EuiFlexItem grow={false}>{flyoutBodyContent}</EuiFlexItem>
<EuiFlexItem grow={false}>{disclaimer}</EuiFlexItem>
</EuiFlexGroup>
)}
<AssistantBody
allSystemPrompts={allSystemPrompts}
comments={comments}
currentConversation={currentConversation}
currentSystemPromptId={currentSystemPromptId}
handleOnConversationSelected={handleOnConversationSelected}
http={http}
isAssistantEnabled={isAssistantEnabled}
isLoading={isInitialLoad}
isSettingsModalVisible={isSettingsModalVisible}
isWelcomeSetup={isWelcomeSetup}
refetchCurrentUserConversations={refetchCurrentUserConversations}
setCurrentSystemPromptId={setCurrentSystemPromptId}
setIsSettingsModalVisible={setIsSettingsModalVisible}
/>
</EuiFlyoutBody>
<EuiFlyoutFooter
css={css`
@ -997,13 +590,13 @@ const AssistantComponent: React.FC<Props> = ({
<EuiFlexItem grow={false}>
<ChatSend
handleChatSend={handleChatSend}
setUserPrompt={setUserPrompt}
handleRegenerateResponse={handleRegenerateResponse}
isDisabled={isSendingDisabled}
isLoading={isLoadingChatSend}
shouldRefocusPrompt={shouldRefocusPrompt}
userPrompt={userPrompt}
handlePromptChange={handlePromptChange}
handleSendMessage={handleChatSend}
handleRegenerateResponse={handleRegenerateResponse}
isLoading={isLoadingChatSend}
/>
</EuiFlexItem>
</EuiFlexGroup>

View file

@ -1,28 +0,0 @@
/*
* 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 { getPromptById } from './helpers';
import { mockSystemPrompt, mockSuperheroSystemPrompt } from '../../mock/system_prompt';
import { PromptResponse } from '@kbn/elastic-assistant-common';
describe('helpers', () => {
describe('getPromptById', () => {
const prompts: PromptResponse[] = [mockSystemPrompt, mockSuperheroSystemPrompt];
it('returns the correct prompt by id', () => {
const result = getPromptById({ prompts, id: mockSuperheroSystemPrompt.id });
expect(result).toEqual(prompts[1]);
});
it('returns undefined if the prompt is not found', () => {
const result = getPromptById({ prompts, id: 'does-not-exist' });
expect(result).toBeUndefined();
});
});
});

View file

@ -1,126 +0,0 @@
/*
* 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 from 'react';
import { render, screen, waitFor } from '@testing-library/react';
import { mockAlertPromptContext, mockEventPromptContext } from '../../mock/prompt_context';
import { TestProviders } from '../../mock/test_providers/test_providers';
import { SelectedPromptContext } from '../prompt_context/types';
import { PromptEditor, Props } from '.';
const mockSelectedAlertPromptContext: SelectedPromptContext = {
contextAnonymizationFields: { total: 0, page: 1, perPage: 1000, data: [] },
promptContextId: mockAlertPromptContext.id,
rawData: 'alert data',
};
const mockSelectedEventPromptContext: SelectedPromptContext = {
contextAnonymizationFields: { total: 0, page: 1, perPage: 1000, data: [] },
promptContextId: mockEventPromptContext.id,
rawData: 'event data',
};
const defaultProps: Props = {
conversation: undefined,
editingSystemPromptId: undefined,
isNewConversation: true,
isSettingsModalVisible: false,
onSystemPromptSelectionChange: jest.fn(),
promptContexts: {
[mockAlertPromptContext.id]: mockAlertPromptContext,
[mockEventPromptContext.id]: mockEventPromptContext,
},
promptTextPreview: 'Preview text',
selectedPromptContexts: {},
setIsSettingsModalVisible: jest.fn(),
setSelectedPromptContexts: jest.fn(),
allSystemPrompts: [],
};
describe('PromptEditorComponent', () => {
beforeEach(() => jest.clearAllMocks());
it('renders the system prompt selector when isNewConversation is true', async () => {
render(
<TestProviders>
<PromptEditor {...defaultProps} />
</TestProviders>
);
await waitFor(() => {
expect(screen.getByTestId('selectSystemPrompt')).toBeInTheDocument();
});
});
it('does NOT render the system prompt selector when isNewConversation is false', async () => {
render(
<TestProviders>
<PromptEditor {...defaultProps} isNewConversation={false} />
</TestProviders>
);
await waitFor(() => {
expect(screen.queryByTestId('selectSystemPrompt')).not.toBeInTheDocument();
});
});
it('renders the selected prompt contexts', async () => {
const selectedPromptContexts = {
[mockAlertPromptContext.id]: mockSelectedAlertPromptContext,
[mockEventPromptContext.id]: mockSelectedEventPromptContext,
};
render(
<TestProviders>
<PromptEditor {...defaultProps} selectedPromptContexts={selectedPromptContexts} />
</TestProviders>
);
await waitFor(() => {
Object.keys(selectedPromptContexts).forEach((id) =>
expect(screen.queryByTestId(`selectedPromptContext-${id}`)).toBeInTheDocument()
);
});
});
it('renders the expected preview text', async () => {
render(
<TestProviders>
<PromptEditor {...defaultProps} />
</TestProviders>
);
await waitFor(() => {
expect(screen.getByTestId('previewText')).toHaveTextContent('Preview text');
});
});
it('renders an "editing prompt" `EuiComment` event', async () => {
render(
<TestProviders>
<PromptEditor {...defaultProps} />
</TestProviders>
);
await waitFor(() => {
expect(screen.getByTestId('eventText')).toHaveTextContent('editing prompt');
});
});
it('renders the user avatar', async () => {
render(
<TestProviders>
<PromptEditor {...defaultProps} />
</TestProviders>
);
await waitFor(() => {
expect(screen.getByTestId('userAvatar')).toBeInTheDocument();
});
});
});

View file

@ -1,125 +0,0 @@
/*
* 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 { EuiAvatar, EuiCommentList, EuiText } from '@elastic/eui';
import React, { useMemo } from 'react';
// eslint-disable-next-line @kbn/eslint/module_migration
import styled from 'styled-components';
import { PromptResponse } from '@kbn/elastic-assistant-common';
import { Conversation } from '../../..';
import type { PromptContext, SelectedPromptContext } from '../prompt_context/types';
import { SystemPrompt } from './system_prompt';
import * as i18n from './translations';
import { SelectedPromptContexts } from './selected_prompt_contexts';
export interface Props {
conversation: Conversation | undefined;
editingSystemPromptId: string | undefined;
isNewConversation: boolean;
isSettingsModalVisible: boolean;
promptContexts: Record<string, PromptContext>;
promptTextPreview: string;
onSystemPromptSelectionChange: (systemPromptId: string | undefined) => void;
selectedPromptContexts: Record<string, SelectedPromptContext>;
setIsSettingsModalVisible: React.Dispatch<React.SetStateAction<boolean>>;
setSelectedPromptContexts: React.Dispatch<
React.SetStateAction<Record<string, SelectedPromptContext>>
>;
allSystemPrompts: PromptResponse[];
}
const PreviewText = styled(EuiText)`
white-space: pre-line;
`;
const PromptEditorComponent: React.FC<Props> = ({
conversation,
editingSystemPromptId,
isNewConversation,
isSettingsModalVisible,
promptContexts,
promptTextPreview,
onSystemPromptSelectionChange,
selectedPromptContexts,
setIsSettingsModalVisible,
setSelectedPromptContexts,
allSystemPrompts,
}) => {
const commentBody = useMemo(
() => (
<>
{isNewConversation && (
<SystemPrompt
allSystemPrompts={allSystemPrompts}
conversation={conversation}
editingSystemPromptId={editingSystemPromptId}
onSystemPromptSelectionChange={onSystemPromptSelectionChange}
isSettingsModalVisible={isSettingsModalVisible}
setIsSettingsModalVisible={setIsSettingsModalVisible}
/>
)}
<SelectedPromptContexts
promptContexts={promptContexts}
selectedPromptContexts={selectedPromptContexts}
setSelectedPromptContexts={setSelectedPromptContexts}
currentReplacements={conversation?.replacements}
/>
<PreviewText color="subdued" data-test-subj="previewText">
{promptTextPreview}
</PreviewText>
</>
),
[
isNewConversation,
allSystemPrompts,
conversation,
editingSystemPromptId,
onSystemPromptSelectionChange,
isSettingsModalVisible,
setIsSettingsModalVisible,
promptContexts,
selectedPromptContexts,
setSelectedPromptContexts,
promptTextPreview,
]
);
const comments = useMemo(
() => [
{
children: commentBody,
event: (
<EuiText data-test-subj="eventText" size="xs">
<i>{i18n.EDITING_PROMPT}</i>
</EuiText>
),
timelineAvatar: (
<EuiAvatar
data-test-subj="userAvatar"
name="user"
size="l"
color="subdued"
iconType="userAvatar"
/>
),
timelineAvatarAriaLabel: i18n.YOU,
username: i18n.YOU,
},
],
[commentBody]
);
return <EuiCommentList aria-label={i18n.COMMENTS_LIST_ARIA_LABEL} comments={comments} />;
};
PromptEditorComponent.displayName = 'PromptEditorComponent';
export const PromptEditor = React.memo(PromptEditorComponent);

View file

@ -8,7 +8,6 @@
import { EuiAccordion, EuiButtonIcon, EuiFlexGroup, EuiFlexItem, EuiToolTip } from '@elastic/eui';
import { isEmpty, omit } from 'lodash/fp';
import React, { useCallback } from 'react';
import styled from '@emotion/styled';
import { css } from '@emotion/react';
import { euiThemeVars } from '@kbn/ui-theme';
import { Conversation } from '../../../assistant_context/types';
@ -25,14 +24,6 @@ export interface Props {
currentReplacements: Conversation['replacements'] | undefined;
}
export const EditorContainer = styled.div<{
$accordionState: 'closed' | 'open';
}>`
${({ $accordionState }) => ($accordionState === 'closed' ? 'height: 0px;' : '')}
${({ $accordionState }) => ($accordionState === 'closed' ? 'overflow: hidden;' : '')}
${({ $accordionState }) => ($accordionState === 'closed' ? 'position: absolute;' : '')}
`;
const SelectedPromptContextsComponent: React.FC<Props> = ({
promptContexts,
selectedPromptContexts,

View file

@ -62,7 +62,7 @@ jest.mock('../../use_conversation', () => {
});
describe('SystemPrompt', () => {
const editingSystemPromptId = undefined;
const currentSystemPromptId = undefined;
const isSettingsModalVisible = false;
const onSystemPromptSelectionChange = jest.fn();
const setIsSettingsModalVisible = jest.fn();
@ -86,7 +86,7 @@ describe('SystemPrompt', () => {
render(
<SystemPrompt
conversation={conversation}
editingSystemPromptId={editingSystemPromptId}
currentSystemPromptId={currentSystemPromptId}
isSettingsModalVisible={isSettingsModalVisible}
onSystemPromptSelectionChange={onSystemPromptSelectionChange}
setIsSettingsModalVisible={setIsSettingsModalVisible}
@ -113,7 +113,7 @@ describe('SystemPrompt', () => {
render(
<SystemPrompt
conversation={BASE_CONVERSATION}
editingSystemPromptId={mockSystemPrompt.id}
currentSystemPromptId={mockSystemPrompt.id}
isSettingsModalVisible={isSettingsModalVisible}
onSystemPromptSelectionChange={onSystemPromptSelectionChange}
setIsSettingsModalVisible={setIsSettingsModalVisible}
@ -144,7 +144,7 @@ describe('SystemPrompt', () => {
<TestProviders>
<SystemPrompt
conversation={BASE_CONVERSATION}
editingSystemPromptId={editingSystemPromptId}
currentSystemPromptId={currentSystemPromptId}
isSettingsModalVisible={isSettingsModalVisible}
onSystemPromptSelectionChange={onSystemPromptSelectionChange}
setIsSettingsModalVisible={setIsSettingsModalVisible}
@ -191,7 +191,7 @@ describe('SystemPrompt', () => {
<TestProviders>
<SystemPrompt
conversation={BASE_CONVERSATION}
editingSystemPromptId={editingSystemPromptId}
currentSystemPromptId={currentSystemPromptId}
isSettingsModalVisible={isSettingsModalVisible}
onSystemPromptSelectionChange={onSystemPromptSelectionChange}
setIsSettingsModalVisible={setIsSettingsModalVisible}
@ -252,7 +252,7 @@ describe('SystemPrompt', () => {
<TestProviders>
<SystemPrompt
conversation={BASE_CONVERSATION}
editingSystemPromptId={editingSystemPromptId}
currentSystemPromptId={currentSystemPromptId}
isSettingsModalVisible={isSettingsModalVisible}
onSystemPromptSelectionChange={onSystemPromptSelectionChange}
setIsSettingsModalVisible={setIsSettingsModalVisible}
@ -320,7 +320,7 @@ describe('SystemPrompt', () => {
<TestProviders>
<SystemPrompt
conversation={BASE_CONVERSATION}
editingSystemPromptId={editingSystemPromptId}
currentSystemPromptId={currentSystemPromptId}
isSettingsModalVisible={isSettingsModalVisible}
onSystemPromptSelectionChange={onSystemPromptSelectionChange}
setIsSettingsModalVisible={setIsSettingsModalVisible}
@ -403,7 +403,7 @@ describe('SystemPrompt', () => {
<TestProviders>
<SystemPrompt
conversation={BASE_CONVERSATION}
editingSystemPromptId={editingSystemPromptId}
currentSystemPromptId={currentSystemPromptId}
isSettingsModalVisible={isSettingsModalVisible}
onSystemPromptSelectionChange={onSystemPromptSelectionChange}
setIsSettingsModalVisible={setIsSettingsModalVisible}
@ -473,7 +473,7 @@ describe('SystemPrompt', () => {
<TestProviders>
<SystemPrompt
conversation={BASE_CONVERSATION}
editingSystemPromptId={mockSystemPrompt.id}
currentSystemPromptId={mockSystemPrompt.id}
isSettingsModalVisible={isSettingsModalVisible}
onSystemPromptSelectionChange={onSystemPromptSelectionChange}
setIsSettingsModalVisible={setIsSettingsModalVisible}

View file

@ -13,7 +13,7 @@ import { SelectSystemPrompt } from './select_system_prompt';
interface Props {
conversation: Conversation | undefined;
editingSystemPromptId: string | undefined;
currentSystemPromptId: string | undefined;
isSettingsModalVisible: boolean;
onSystemPromptSelectionChange: (systemPromptId: string | undefined) => void;
setIsSettingsModalVisible: React.Dispatch<React.SetStateAction<boolean>>;
@ -23,7 +23,7 @@ interface Props {
const SystemPromptComponent: React.FC<Props> = ({
conversation,
editingSystemPromptId,
currentSystemPromptId,
isSettingsModalVisible,
onSystemPromptSelectionChange,
setIsSettingsModalVisible,
@ -32,16 +32,16 @@ const SystemPromptComponent: React.FC<Props> = ({
}) => {
const [isCleared, setIsCleared] = useState(false);
const selectedPrompt = useMemo(() => {
if (editingSystemPromptId !== undefined) {
if (currentSystemPromptId !== undefined) {
setIsCleared(false);
return allSystemPrompts.find((p) => p.id === editingSystemPromptId);
return allSystemPrompts.find((p) => p.id === currentSystemPromptId);
} else {
return allSystemPrompts.find((p) => p.id === conversation?.apiConfig?.defaultSystemPromptId);
}
}, [allSystemPrompts, conversation?.apiConfig?.defaultSystemPromptId, editingSystemPromptId]);
}, [allSystemPrompts, conversation?.apiConfig?.defaultSystemPromptId, currentSystemPromptId]);
const handleClearSystemPrompt = useCallback(() => {
if (editingSystemPromptId === undefined) {
if (currentSystemPromptId === undefined) {
setIsCleared(false);
onSystemPromptSelectionChange(
allSystemPrompts.find((p) => p.id === conversation?.apiConfig?.defaultSystemPromptId)?.id
@ -53,7 +53,7 @@ const SystemPromptComponent: React.FC<Props> = ({
}, [
allSystemPrompts,
conversation?.apiConfig?.defaultSystemPromptId,
editingSystemPromptId,
currentSystemPromptId,
onSystemPromptSelectionChange,
]);

View file

@ -20,13 +20,6 @@ export const SETTINGS_DESCRIPTION = i18n.translate(
'Create and manage System Prompts. System Prompts are configurable chunks of context that are always sent for a given conversation.',
}
);
export const ADD_SYSTEM_PROMPT_MODAL_TITLE = i18n.translate(
'xpack.elasticAssistant.assistant.promptEditor.systemPrompt.settings.modalTitle',
{
defaultMessage: 'System Prompts',
}
);
export const SYSTEM_PROMPT_NAME = i18n.translate(
'xpack.elasticAssistant.assistant.promptEditor.systemPrompt.settings.nameLabel',
{

View file

@ -27,13 +27,6 @@ export const SYSTEM_PROMPTS_TABLE_COLUMN_DATE_UPDATED = i18n.translate(
}
);
export const SYSTEM_PROMPTS_TABLE_COLUMN_ACTIONS = i18n.translate(
'xpack.elasticAssistant.assistant.promptsTable.systemPromptsTableColumnActions',
{
defaultMessage: 'Actions',
}
);
export const SYSTEM_PROMPTS_TABLE_SETTINGS_DESCRIPTION = i18n.translate(
'xpack.elasticAssistant.assistant.promptsTable.settingsDescription',
{

View file

@ -7,13 +7,6 @@
import { i18n } from '@kbn/i18n';
export const ADD_SYSTEM_PROMPT_TOOLTIP = i18n.translate(
'xpack.elasticAssistant.assistant.firstPromptEditor.addSystemPromptTooltip',
{
defaultMessage: 'Add system prompt',
}
);
export const CLEAR_SYSTEM_PROMPT = i18n.translate(
'xpack.elasticAssistant.assistant.firstPromptEditor.clearSystemPrompt',
{

View file

@ -1,26 +0,0 @@
/*
* 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 { i18n } from '@kbn/i18n';
export const COMMENTS_LIST_ARIA_LABEL = i18n.translate(
'xpack.elasticAssistant.assistant.firstPromptEditor.commentsListAriaLabel',
{
defaultMessage: 'List of comments',
}
);
export const EDITING_PROMPT = i18n.translate(
'xpack.elasticAssistant.assistant.firstPromptEditor.editingPromptLabel',
{
defaultMessage: 'editing prompt',
}
);
export const YOU = i18n.translate('xpack.elasticAssistant.assistant.firstPromptEditor.youLabel', {
defaultMessage: 'You',
});

View file

@ -12,19 +12,19 @@ import { css } from '@emotion/react';
import * as i18n from './translations';
export interface Props extends React.TextareaHTMLAttributes<HTMLTextAreaElement> {
handlePromptChange: (value: string) => void;
setUserPrompt: (value: string) => void;
isDisabled?: boolean;
onPromptSubmit: (value: string) => void;
value: string;
}
export const PromptTextArea = forwardRef<HTMLTextAreaElement, Props>(
({ isDisabled = false, value, onPromptSubmit, handlePromptChange }, ref) => {
({ isDisabled = false, value, onPromptSubmit, setUserPrompt }, ref) => {
const onChangeCallback = useCallback(
(event: React.ChangeEvent<HTMLTextAreaElement>) => {
handlePromptChange(event.target.value);
setUserPrompt(event.target.value);
},
[handlePromptChange]
[setUserPrompt]
);
const onKeyDown = useCallback(
@ -35,13 +35,13 @@ export const PromptTextArea = forwardRef<HTMLTextAreaElement, Props>(
if (value.trim().length) {
onPromptSubmit(event.target.value?.trim());
handlePromptChange('');
setUserPrompt('');
} else {
event.stopPropagation();
}
}
},
[value, onPromptSubmit, handlePromptChange]
[value, onPromptSubmit, setUserPrompt]
);
return (

View file

@ -17,6 +17,7 @@ const testProps = {
selectedQuickPrompt: MOCK_QUICK_PROMPTS[0],
onQuickPromptDeleted,
onQuickPromptSelectionChange,
selectedColor: '#D36086',
};
describe('QuickPromptSelector', () => {
@ -28,7 +29,10 @@ describe('QuickPromptSelector', () => {
expect(getByTestId('euiComboBoxPill')).toHaveTextContent(MOCK_QUICK_PROMPTS[0].name);
fireEvent.click(getByTestId('comboBoxToggleListButton'));
fireEvent.click(getByTestId(MOCK_QUICK_PROMPTS[1].name));
expect(onQuickPromptSelectionChange).toHaveBeenCalledWith(MOCK_QUICK_PROMPTS[1]);
expect(onQuickPromptSelectionChange).toHaveBeenCalledWith(
MOCK_QUICK_PROMPTS[1],
testProps.selectedColor
);
});
it('Only custom option can be deleted', () => {
const { getByTestId } = render(<QuickPromptSelector {...testProps} />);
@ -46,14 +50,17 @@ describe('QuickPromptSelector', () => {
code: 'Enter',
charCode: 13,
});
expect(onQuickPromptSelectionChange).toHaveBeenCalledWith({
categories: [],
color: '#D36086',
content: 'quickly prompt please',
id: 'A_CUSTOM_OPTION',
name: 'A_CUSTOM_OPTION',
promptType: 'quick',
});
expect(onQuickPromptSelectionChange).toHaveBeenCalledWith(
{
categories: [],
color: '#D36086',
content: 'quickly prompt please',
id: 'A_CUSTOM_OPTION',
name: 'A_CUSTOM_OPTION',
promptType: 'quick',
},
testProps.selectedColor
);
});
it('Reset settings every time before selecting an system prompt from the input if resetSettings is provided', () => {
const mockResetSettings = jest.fn();
@ -80,6 +87,9 @@ describe('QuickPromptSelector', () => {
code: 'Enter',
charCode: 13,
});
expect(onQuickPromptSelectionChange).toHaveBeenCalledWith(customOption);
expect(onQuickPromptSelectionChange).toHaveBeenCalledWith(
customOption,
testProps.selectedColor
);
});
});

View file

@ -24,8 +24,12 @@ import * as i18n from './translations';
interface Props {
isDisabled?: boolean;
onQuickPromptDeleted: (quickPromptTitle: string) => void;
onQuickPromptSelectionChange: (quickPrompt?: PromptResponse | string) => void;
onQuickPromptSelectionChange: (
quickPrompt: PromptResponse | string,
selectedColor: string
) => void;
quickPrompts: PromptResponse[];
selectedColor: string;
selectedQuickPrompt?: PromptResponse;
resetSettings?: () => void;
}
@ -42,6 +46,7 @@ export const QuickPromptSelector: React.FC<Props> = React.memo(
onQuickPromptDeleted,
onQuickPromptSelectionChange,
resetSettings,
selectedColor,
selectedQuickPrompt,
}) => {
// Form options
@ -80,9 +85,11 @@ export const QuickPromptSelector: React.FC<Props> = React.memo(
? undefined
: quickPrompts.find((qp) => qp.name === quickPromptSelectorOption[0]?.label) ??
quickPromptSelectorOption[0]?.label;
onQuickPromptSelectionChange(newQuickPrompt);
if (newQuickPrompt) {
onQuickPromptSelectionChange(newQuickPrompt, selectedColor);
}
},
[onQuickPromptSelectionChange, resetSettings, quickPrompts]
[onQuickPromptSelectionChange, resetSettings, quickPrompts, selectedColor]
);
// Callback for when user types to create a new quick prompt

View file

@ -5,12 +5,10 @@
* 2.0.
*/
import { PromptResponse } from '@kbn/elastic-assistant-common';
import { euiPaletteColorBlind } from '@elastic/eui';
export const getPromptById = ({
prompts,
id,
}: {
prompts: PromptResponse[];
id: string;
}): PromptResponse | undefined => prompts.find((p) => p.id === id);
const euiVisPalette = euiPaletteColorBlind();
export const getRandomEuiColor = () => {
const randomIndex = Math.floor(Math.random() * euiVisPalette.length);
return euiVisPalette[randomIndex];
};

View file

@ -14,6 +14,7 @@ import {
PromptResponse,
PerformPromptsBulkActionRequestBody as PromptsPerformBulkActionRequestBody,
} from '@kbn/elastic-assistant-common/impl/schemas/prompts/bulk_crud_prompts_route.gen';
import { getRandomEuiColor } from './helpers';
import { PromptContextTemplate } from '../../../..';
import * as i18n from './translations';
import { QuickPromptSelector } from '../quick_prompt_selector/quick_prompt_selector';
@ -21,8 +22,6 @@ import { PromptContextSelector } from '../prompt_context_selector/prompt_context
import { useAssistantContext } from '../../../assistant_context';
import { useQuickPromptEditor } from './use_quick_prompt_editor';
const DEFAULT_COLOR = '#D36086';
interface Props {
onSelectedQuickPromptChange: (quickPrompt?: PromptResponse) => void;
quickPromptSettings: PromptResponse[];
@ -112,12 +111,6 @@ const QuickPromptSettingsEditorComponent = ({
]
);
// Color
const selectedColor = useMemo(
() => selectedQuickPrompt?.color ?? DEFAULT_COLOR,
[selectedQuickPrompt?.color]
);
const handleColorChange = useCallback<EuiSetColorMethod>(
(color, { hex, isValid }) => {
if (selectedQuickPrompt != null) {
@ -177,6 +170,17 @@ const QuickPromptSettingsEditorComponent = ({
]
);
const setDefaultPromptColor = useCallback((): string => {
const randomColor = getRandomEuiColor();
handleColorChange(randomColor, { hex: randomColor, isValid: true });
return randomColor;
}, [handleColorChange]);
// Color
const selectedColor = useMemo(
() => selectedQuickPrompt?.color ?? setDefaultPromptColor(),
[selectedQuickPrompt?.color, setDefaultPromptColor]
);
// Prompt Contexts
const selectedPromptContexts = useMemo(
() =>
@ -263,6 +267,7 @@ const QuickPromptSettingsEditorComponent = ({
quickPrompts={quickPromptSettings}
resetSettings={resetSettings}
selectedQuickPrompt={selectedQuickPrompt}
selectedColor={selectedColor}
/>
</EuiFormRow>

View file

@ -42,17 +42,17 @@ jest.mock('../quick_prompt_selector/quick_prompt_selector', () => ({
<button
type="button"
data-test-subj="delete-qp"
onClick={() => onQuickPromptDeleted('A_CUSTOM_OPTION')}
onClick={() => onQuickPromptDeleted('A_CUSTOM_OPTION', '#D36086')}
/>
<button
type="button"
data-test-subj="change-qp"
onClick={() => onQuickPromptSelectionChange(MOCK_QUICK_PROMPTS[3])}
onClick={() => onQuickPromptSelectionChange(MOCK_QUICK_PROMPTS[3], '#D36086')}
/>
<button
type="button"
data-test-subj="change-qp-custom"
onClick={() => onQuickPromptSelectionChange('sooper custom prompt')}
onClick={() => onQuickPromptSelectionChange('sooper custom prompt', '#D36086')}
/>
</>
),

View file

@ -21,12 +21,6 @@ export const SETTINGS_DESCRIPTION = i18n.translate(
'Create and manage Quick Prompts. Quick Prompts are shortcuts to common actions.',
}
);
export const ADD_QUICK_PROMPT_MODAL_TITLE = i18n.translate(
'xpack.elasticAssistant.assistant.quickPrompts.settings.modalTitle',
{
defaultMessage: 'Quick Prompts',
}
);
export const QUICK_PROMPT_NAME = i18n.translate(
'xpack.elasticAssistant.assistant.quickPrompts.settings.nameLabel',

View file

@ -6,12 +6,12 @@
*/
import { renderHook, act } from '@testing-library/react-hooks';
import { useQuickPromptEditor, DEFAULT_COLOR } from './use_quick_prompt_editor';
import { useQuickPromptEditor } from './use_quick_prompt_editor';
import { mockAlertPromptContext } from '../../../mock/prompt_context';
import { MOCK_QUICK_PROMPTS } from '../../../mock/quick_prompt';
import { PromptResponse } from '@kbn/elastic-assistant-common';
import { useAssistantContext } from '../../../assistant_context';
const DEFAULT_COLOR = '#D36086';
jest.mock('../../../assistant_context');
// Mock functions for the tests
const mockOnSelectedQuickPromptChange = jest.fn();
@ -58,7 +58,7 @@ describe('useQuickPromptEditor', () => {
);
act(() => {
result.current.onQuickPromptSelectionChange(newPromptTitle);
result.current.onQuickPromptSelectionChange(newPromptTitle, DEFAULT_COLOR);
});
const newPrompt: PromptResponse = {
@ -100,7 +100,7 @@ describe('useQuickPromptEditor', () => {
};
act(() => {
result.current.onQuickPromptSelectionChange(expectedPrompt);
result.current.onQuickPromptSelectionChange(expectedPrompt, DEFAULT_COLOR);
});
expect(mockOnSelectedQuickPromptChange).toHaveBeenCalledWith(expectedPrompt);

View file

@ -11,10 +11,9 @@ import {
PerformPromptsBulkActionRequestBody as PromptsPerformBulkActionRequestBody,
} from '@kbn/elastic-assistant-common/impl/schemas/prompts/bulk_crud_prompts_route.gen';
import { useCallback } from 'react';
import { getRandomEuiColor } from './helpers';
import { useAssistantContext } from '../../../..';
export const DEFAULT_COLOR = '#D36086';
export const useQuickPromptEditor = ({
onSelectedQuickPromptChange,
setUpdatedQuickPromptSettings,
@ -42,14 +41,16 @@ export const useQuickPromptEditor = ({
// When top level quick prompt selection changes
const onQuickPromptSelectionChange = useCallback(
(quickPrompt?: PromptResponse | string) => {
(quickPrompt: PromptResponse | string, color?: string) => {
const isNew = typeof quickPrompt === 'string';
const qpColor = color ? color : isNew ? getRandomEuiColor() : quickPrompt.color;
const newSelectedQuickPrompt: PromptResponse | undefined = isNew
? {
name: quickPrompt,
id: quickPrompt,
content: '',
color: DEFAULT_COLOR,
color: qpColor,
categories: [],
promptType: PromptTypeEnum.quick,
consumer: currentAppId,

View file

@ -20,13 +20,6 @@ export const QUICK_PROMPTS_TABLE_COLUMN_DATE_UPDATED = i18n.translate(
}
);
export const QUICK_PROMPTS_TABLE_COLUMN_ACTIONS = i18n.translate(
'xpack.elasticAssistant.assistant.quickPromptsTable.quickPromptsTableColumnActions',
{
defaultMessage: 'Actions',
}
);
export const QUICK_PROMPTS_DESCRIPTION = i18n.translate(
'xpack.elasticAssistant.assistant.quickPromptsTable.description',
{

View file

@ -25,7 +25,7 @@ export const useQuickPromptTable = () => {
}: {
isActionsDisabled: boolean;
basePromptContexts: PromptContextTemplate[];
onEditActionClicked: (prompt: PromptResponse) => void;
onEditActionClicked: (prompt: PromptResponse, color?: string) => void;
onDeleteActionClicked: (prompt: PromptResponse) => void;
}): Array<EuiBasicTableColumn<PromptResponse>> => [
{

View file

@ -175,6 +175,7 @@ export const AssistantSettings: React.FC<Props> = React.memo(
conversationSettings[defaultSelectedConversationId] == null;
const newSelectedConversation: Conversation | undefined =
Object.values(conversationSettings)[0];
if (isSelectedConversationDeleted && newSelectedConversation != null) {
onConversationSelected({
cId: newSelectedConversation.id,

View file

@ -25,7 +25,7 @@ const testProps = {
onConversationSelected,
conversations: {},
conversationsLoaded: true,
refetchConversationsState: jest.fn(),
refetchCurrentUserConversations: jest.fn(),
anonymizationFields: { total: 0, page: 1, perPage: 1000, data: [] },
refetchAnonymizationFieldsResults: jest.fn(),
};

View file

@ -9,6 +9,7 @@ import React, { useCallback } from 'react';
import { EuiButtonIcon, EuiToolTip } from '@elastic/eui';
import { QueryObserverResult, RefetchOptions, RefetchQueryFilters } from '@tanstack/react-query';
import { DataStreamApis } from '../use_data_stream_apis';
import { AIConnector } from '../../connectorland/connector_selector';
import { Conversation } from '../../..';
import { AssistantSettings } from './assistant_settings';
@ -25,7 +26,7 @@ interface Props {
isDisabled?: boolean;
conversations: Record<string, Conversation>;
conversationsLoaded: boolean;
refetchConversationsState: () => Promise<void>;
refetchCurrentUserConversations: DataStreamApis['refetchCurrentUserConversations'];
refetchPrompts?: (
options?: RefetchOptions & RefetchQueryFilters<unknown>
) => Promise<QueryObserverResult<unknown, unknown>>;
@ -44,7 +45,7 @@ export const AssistantSettingsButton: React.FC<Props> = React.memo(
onConversationSelected,
conversations,
conversationsLoaded,
refetchConversationsState,
refetchCurrentUserConversations,
refetchPrompts,
}) => {
const { toasts, setSelectedSettingsTab } = useAssistantContext();
@ -61,7 +62,7 @@ export const AssistantSettingsButton: React.FC<Props> = React.memo(
const handleSave = useCallback(
async (success: boolean) => {
cleanupAndCloseModal();
await refetchConversationsState();
await refetchCurrentUserConversations();
if (refetchPrompts) {
await refetchPrompts();
}
@ -72,7 +73,7 @@ export const AssistantSettingsButton: React.FC<Props> = React.memo(
});
}
},
[cleanupAndCloseModal, refetchConversationsState, refetchPrompts, toasts]
[cleanupAndCloseModal, refetchCurrentUserConversations, refetchPrompts, toasts]
);
const handleShowConversationSettings = useCallback(() => {

View file

@ -84,13 +84,6 @@ export const EVALUATION_MENU_ITEM = i18n.translate(
}
);
export const ADD_SYSTEM_PROMPT_MODAL_TITLE = i18n.translate(
'xpack.elasticAssistant.assistant.settings.modalTitle',
{
defaultMessage: 'System Prompts',
}
);
export const CANCEL = i18n.translate(
'xpack.elasticAssistant.assistant.promptEditor.systemPrompt.slCancelButtonTitle',
{

View file

@ -1,53 +0,0 @@
/*
* 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 { useCallback } from 'react';
import { IToasts } from '@kbn/core/public';
import { Conversation } from '../../..';
import { SETTINGS_UPDATED_TOAST_TITLE } from './translations';
interface Props {
conversationSettings: Record<string, Conversation>;
defaultSelectedConversation: Conversation;
setSelectedConversationId: React.Dispatch<React.SetStateAction<string>>;
saveSettings: () => void;
setHasPendingChanges: React.Dispatch<React.SetStateAction<boolean>>;
toasts: IToasts | undefined;
}
export const useHandleSave = ({
conversationSettings,
defaultSelectedConversation,
setSelectedConversationId,
saveSettings,
setHasPendingChanges,
toasts,
}: Props) => {
const handleSave = useCallback(() => {
// If the selected conversation is deleted, we need to select a new conversation to prevent a crash creating a conversation that already exists
const isSelectedConversationDeleted =
conversationSettings[defaultSelectedConversation.title] == null;
const newSelectedConversationId: string | undefined = Object.keys(conversationSettings)[0];
if (isSelectedConversationDeleted && newSelectedConversationId != null) {
setSelectedConversationId(conversationSettings[newSelectedConversationId].title);
}
saveSettings();
toasts?.addSuccess({
iconType: 'check',
title: SETTINGS_UPDATED_TOAST_TITLE,
});
setHasPendingChanges(false);
}, [
conversationSettings,
defaultSelectedConversation.title,
saveSettings,
setHasPendingChanges,
setSelectedConversationId,
toasts,
]);
return handleSave;
};

View file

@ -7,10 +7,6 @@
import { i18n } from '@kbn/i18n';
export const CLEAR_CHAT = i18n.translate('xpack.elasticAssistant.assistant.clearChat', {
defaultMessage: 'Clear chat',
});
export const DEFAULT_ASSISTANT_TITLE = i18n.translate(
'xpack.elasticAssistant.assistant.defaultAssistantTitle',
{
@ -26,13 +22,6 @@ export const API_ERROR = i18n.translate('xpack.elasticAssistant.assistant.apiErr
defaultMessage: 'An error occurred sending your message.',
});
export const TOOLTIP_ARIA_LABEL = i18n.translate(
'xpack.elasticAssistant.documentationLinks.ariaLabel',
{
defaultMessage: 'Click to open Elastic Assistant documentation in a new tab',
}
);
export const DOCUMENTATION = i18n.translate(
'xpack.elasticAssistant.documentationLinks.documentation',
{

View file

@ -0,0 +1,25 @@
/*
* 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 from 'react';
import { render } from '@testing-library/react';
import { UpgradeLicenseCallToAction } from '.';
import { HttpSetup } from '@kbn/core-http-browser';
const testProps = {
connectorPrompt: <div>{'Connector Prompt'}</div>,
http: { basePath: { get: jest.fn(() => 'http://localhost:5601') } } as unknown as HttpSetup,
isAssistantEnabled: false,
isWelcomeSetup: false,
};
describe('UpgradeLicenseCallToAction', () => {
it('UpgradeButtons is rendered ', () => {
const { getByTestId, queryByTestId } = render(<UpgradeLicenseCallToAction {...testProps} />);
expect(getByTestId('upgrade-buttons')).toBeInTheDocument();
expect(queryByTestId('connector-prompt')).not.toBeInTheDocument();
});
});

View file

@ -13,26 +13,17 @@ import { ENTERPRISE } from '../../content/prompts/welcome/translations';
import { UpgradeButtons } from '../../upgrade/upgrade_buttons';
interface OwnProps {
connectorPrompt: React.ReactElement;
http: HttpSetup;
isAssistantEnabled: boolean;
isWelcomeSetup: boolean;
}
type Props = OwnProps;
/**
* Provides a call-to-action for users to upgrade their subscription or set up a connector
* depending on the isAssistantEnabled and isWelcomeSetup props.
* Provides a call-to-action for users to upgrade their subscription
*/
export const BlockBotCallToAction: React.FC<Props> = ({
connectorPrompt,
http,
isAssistantEnabled,
isWelcomeSetup,
}) => {
export const UpgradeLicenseCallToAction: React.FC<Props> = ({ http }) => {
const basePath = http.basePath.get();
return !isAssistantEnabled ? (
return (
<EuiFlexGroup
justifyContent="center"
direction="column"
@ -54,13 +45,5 @@ export const BlockBotCallToAction: React.FC<Props> = ({
</EuiFlexItem>
<EuiFlexItem grow={false}>{<UpgradeButtons basePath={basePath} />}</EuiFlexItem>
</EuiFlexGroup>
) : isWelcomeSetup ? (
<EuiFlexGroup
css={css`
width: 100%;
`}
>
<EuiFlexItem data-test-subj="connector-prompt">{connectorPrompt}</EuiFlexItem>
</EuiFlexGroup>
) : null;
);
};

View file

@ -11,12 +11,6 @@ import { useCallback, useEffect, useMemo } from 'react';
import { useAssistantContext } from '../../assistant_context';
import { getUniquePromptContextId } from '../../assistant_context/helpers';
import type { PromptContext } from '../prompt_context/types';
import { useConversation } from '../use_conversation';
import { getDefaultConnector, mergeBaseWithPersistedConversations } from '../helpers';
import { getGenAiConfig } from '../../connectorland/helpers';
import { useLoadConnectors } from '../../connectorland/use_load_connectors';
import { FetchConversationsResponse, useFetchCurrentUserConversations } from '../api';
import { Conversation } from '../../assistant_context/types';
interface UseAssistantOverlay {
showAssistantOverlay: (show: boolean, silent?: boolean) => void;
@ -82,26 +76,6 @@ export const useAssistantOverlay = (
*/
replacements?: Replacements | null
): UseAssistantOverlay => {
const { http } = useAssistantContext();
const { data: connectors } = useLoadConnectors({
http,
});
const defaultConnector = useMemo(() => getDefaultConnector(connectors), [connectors]);
const apiConfig = useMemo(() => getGenAiConfig(defaultConnector), [defaultConnector]);
const { createConversation } = useConversation();
const onFetchedConversations = useCallback(
(conversationsData: FetchConversationsResponse): Record<string, Conversation> =>
mergeBaseWithPersistedConversations({}, conversationsData),
[]
);
const { data: conversations, isLoading } = useFetchCurrentUserConversations({
http,
onFetch: onFetchedConversations,
isAssistantEnabled,
});
// memoize the props so that we can use them in the effect below:
const _category: PromptContext['category'] = useMemo(() => category, [category]);
const _description: PromptContext['description'] = useMemo(() => description, [description]);
@ -131,29 +105,6 @@ export const useAssistantOverlay = (
// silent:boolean doesn't show the toast notification if the conversation is not found
const showAssistantOverlay = useCallback(
async (showOverlay: boolean) => {
let conversation;
if (!isLoading) {
conversation = conversationTitle
? Object.values(conversations).find((conv) => conv.title === conversationTitle)
: undefined;
}
if (isAssistantEnabled && !conversation && defaultConnector && !isLoading) {
try {
conversation = await createConversation({
apiConfig: {
...apiConfig,
actionTypeId: defaultConnector?.actionTypeId,
connectorId: defaultConnector?.id,
},
category: 'assistant',
title: conversationTitle ?? '',
});
} catch (e) {
/* empty */
}
}
if (promptContextId != null) {
assistantContextShowOverlay({
showOverlay,
@ -162,17 +113,7 @@ export const useAssistantOverlay = (
});
}
},
[
apiConfig,
assistantContextShowOverlay,
conversationTitle,
conversations,
createConversation,
defaultConnector,
isAssistantEnabled,
isLoading,
promptContextId,
]
[assistantContextShowOverlay, conversationTitle, promptContextId]
);
useEffect(() => {

View file

@ -45,7 +45,7 @@ interface UpdateConversationTitleProps {
updatedTitle: string;
}
interface UseConversation {
export interface UseConversation {
clearConversation: (conversation: Conversation) => Promise<Conversation | undefined>;
getDefaultConversation: ({ cTitle, messages }: CreateConversationProps) => Conversation;
deleteConversation: (conversationId: string) => void;
@ -135,6 +135,7 @@ export const useConversation = (): UseConversation => {
title: cTitle,
messages: messages != null ? messages : [],
};
return newConversation;
},
[]

View file

@ -5,8 +5,7 @@
* 2.0.
*/
import { Conversation, ClientMessage } from '../../assistant_context/types';
import * as i18n from '../../content/prompts/welcome/translations';
import { Conversation } from '../../assistant_context/types';
import { WELCOME_CONVERSATION_TITLE } from './translations';
export const WELCOME_CONVERSATION: Conversation = {
@ -17,15 +16,3 @@ export const WELCOME_CONVERSATION: Conversation = {
replacements: {},
excludeFromLastConversationStorage: true,
};
export const enterpriseMessaging: ClientMessage[] = [
{
role: 'assistant',
content: i18n.ENTERPRISE,
timestamp: '',
presentation: {
delay: 2 * 1000,
stream: true,
},
},
];

View file

@ -26,10 +26,3 @@ export const ELASTIC_AI_ASSISTANT_TITLE = i18n.translate(
defaultMessage: 'Elastic AI Assistant',
}
);
export const ELASTIC_AI_ASSISTANT = i18n.translate(
'xpack.elasticAssistant.assistant.useConversation.elasticAiAssistantName',
{
defaultMessage: 'Assistant',
}
);

View file

@ -0,0 +1,253 @@
/*
* 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 { renderHook, act } from '@testing-library/react-hooks';
import { useCurrentConversation, Props } from '.';
import { useConversation } from '../use_conversation';
import deepEqual from 'fast-deep-equal';
import { Conversation } from '../../..';
import { find } from 'lodash';
// Mock dependencies
jest.mock('../use_conversation');
jest.mock('../helpers');
jest.mock('fast-deep-equal');
jest.mock('lodash');
const mockData = {
welcome_id: {
id: 'welcome_id',
title: 'Welcome',
category: 'assistant',
messages: [],
apiConfig: {
connectorId: '123',
actionTypeId: '.gen-ai',
defaultSystemPromptId: 'system-prompt-id',
},
replacements: {},
},
electric_sheep_id: {
id: 'electric_sheep_id',
category: 'assistant',
title: 'electric sheep',
messages: [],
apiConfig: { connectorId: '123', actionTypeId: '.gen-ai' },
replacements: {},
},
};
describe('useCurrentConversation', () => {
const mockUseConversation = {
createConversation: jest.fn(),
deleteConversation: jest.fn(),
getConversation: jest.fn(),
getDefaultConversation: jest.fn(),
setApiConfig: jest.fn(),
};
beforeEach(() => {
jest.clearAllMocks();
(useConversation as jest.Mock).mockReturnValue(mockUseConversation);
(deepEqual as jest.Mock).mockReturnValue(false);
(find as jest.Mock).mockReturnValue(undefined);
});
const defaultProps: Props = {
// @ts-ignore not exact system prompt type, ok for test
allSystemPrompts: [{ id: 'system-prompt-id' }, { id: 'something-crazy' }],
conversationId: '',
conversations: {},
mayUpdateConversations: true,
refetchCurrentUserConversations: jest.fn().mockResolvedValue({ data: mockData }),
};
const setupHook = (props: Partial<Props> = {}) => {
return renderHook(() => useCurrentConversation({ ...defaultProps, ...props }));
};
it('should initialize with correct default values', () => {
const { result } = setupHook();
expect(result.current.currentConversation).toBeUndefined();
expect(result.current.currentSystemPromptId).toBeUndefined();
});
it('should set the current system prompt ID when the prompt selection changes', () => {
const { result } = setupHook();
act(() => {
result.current.setCurrentSystemPromptId('prompt-id');
});
expect(result.current.currentSystemPromptId).toBe('prompt-id');
});
it('should fetch and set the current conversation', async () => {
const conversationId = 'welcome_id';
const conversation = mockData.welcome_id;
mockUseConversation.getConversation.mockResolvedValue(conversation);
const { result } = setupHook({
conversationId,
conversations: { [conversationId]: conversation },
});
await act(async () => {
await result.current.refetchCurrentConversation({ cId: conversationId });
});
expect(result.current.currentConversation).toEqual(conversation);
});
it('should handle conversation selection', async () => {
const conversationId = 'test-id';
const conversationTitle = 'Test Conversation';
const conversation = {
...mockData.welcome_id,
id: conversationId,
title: conversationTitle,
apiConfig: {
...mockData.welcome_id.apiConfig,
defaultSystemPromptId: 'something-crazy',
},
} as Conversation;
const mockConversations = {
...mockData,
[conversationId]: conversation,
};
(find as jest.Mock).mockReturnValue(conversation);
const { result } = setupHook({
conversationId: mockData.welcome_id.id,
conversations: mockConversations,
refetchCurrentUserConversations: jest.fn().mockResolvedValue({
data: mockConversations,
}),
});
await act(async () => {
await result.current.handleOnConversationSelected({
cId: conversationId,
cTitle: conversationTitle,
});
});
expect(result.current.currentConversation).toEqual(conversation);
expect(result.current.currentSystemPromptId).toBe('something-crazy');
});
it('should non-existing handle conversation selection', async () => {
const conversationId = 'test-id';
const conversationTitle = 'Test Conversation';
const conversation = {
...mockData.welcome_id,
id: conversationId,
title: conversationTitle,
} as Conversation;
const mockConversations = {
...mockData,
[conversationId]: conversation,
};
(find as jest.Mock).mockReturnValue(conversation);
const { result } = setupHook({
conversationId: mockData.welcome_id.id,
conversations: mockConversations,
refetchCurrentUserConversations: jest.fn().mockResolvedValue({
data: mockConversations,
}),
});
await act(async () => {
await result.current.handleOnConversationSelected({
cId: 'bad',
cTitle: 'bad',
});
});
expect(result.current.currentConversation).toEqual(mockData.welcome_id);
expect(result.current.currentSystemPromptId).toBe('system-prompt-id');
});
it('should create a new conversation', async () => {
const newConversation = {
...mockData.welcome_id,
id: 'new-id',
title: 'NEW_CHAT',
messages: [],
} as Conversation;
mockUseConversation.createConversation.mockResolvedValue(newConversation);
const { result } = setupHook({
conversations: {
'old-id': {
...mockData.welcome_id,
id: 'old-id',
title: 'Old Chat',
messages: [],
} as Conversation,
},
refetchCurrentUserConversations: jest.fn().mockResolvedValue({
data: {
'old-id': {
...mockData.welcome_id,
id: 'old-id',
title: 'Old Chat',
messages: [],
} as Conversation,
[newConversation.id]: newConversation,
},
}),
});
await act(async () => {
await result.current.handleCreateConversation();
});
expect(mockUseConversation.createConversation).toHaveBeenCalled();
});
it('should delete a conversation', async () => {
const conversationTitle = 'Test Conversation';
const conversation = {
...mockData.welcome_id,
id: 'test-id',
title: conversationTitle,
messages: [],
} as Conversation;
const { result } = setupHook({
conversations: { ...mockData, 'test-id': conversation },
});
await act(async () => {
await result.current.handleOnConversationDeleted('test-id');
});
expect(mockUseConversation.deleteConversation).toHaveBeenCalledWith('test-id');
expect(result.current.currentConversation).toBeUndefined();
});
it('should refetch the conversation multiple times if isStreamRefetch is true', async () => {
const conversationId = 'test-id';
const conversation = { id: conversationId, messages: [{ role: 'user' }] } as Conversation;
mockUseConversation.getConversation.mockResolvedValue(conversation);
const { result } = setupHook({
conversationId,
conversations: { [conversationId]: conversation },
});
await act(async () => {
await result.current.refetchCurrentConversation({
cId: conversationId,
isStreamRefetch: true,
});
});
expect(mockUseConversation.getConversation).toHaveBeenCalledTimes(6); // initial + 5 retries
});
});

View file

@ -0,0 +1,283 @@
/*
* 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 { Dispatch, SetStateAction, useCallback, useEffect, useMemo, useState } from 'react';
import { QueryObserverResult } from '@tanstack/react-query';
import { PromptResponse } from '@kbn/elastic-assistant-common';
import { find } from 'lodash';
import deepEqual from 'fast-deep-equal';
import { AIConnector } from '../../connectorland/connector_selector';
import { getGenAiConfig } from '../../connectorland/helpers';
import { NEW_CHAT } from '../conversations/conversation_sidepanel/translations';
import { getDefaultSystemPrompt } from '../use_conversation/helpers';
import { useConversation } from '../use_conversation';
import { sleep } from '../helpers';
import { Conversation, WELCOME_CONVERSATION_TITLE } from '../../..';
export interface Props {
allSystemPrompts: PromptResponse[];
conversationId: string;
conversations: Record<string, Conversation>;
defaultConnector?: AIConnector;
mayUpdateConversations: boolean;
refetchCurrentUserConversations: () => Promise<
QueryObserverResult<Record<string, Conversation>, unknown>
>;
}
interface UseCurrentConversation {
currentConversation: Conversation | undefined;
currentSystemPromptId: string | undefined;
handleCreateConversation: () => Promise<void>;
handleOnConversationDeleted: (cTitle: string) => Promise<void>;
handleOnConversationSelected: ({ cId, cTitle }: { cId: string; cTitle: string }) => Promise<void>;
refetchCurrentConversation: (options?: {
cId?: string;
cTitle?: string;
isStreamRefetch?: boolean;
}) => Promise<Conversation | undefined>;
setCurrentConversation: Dispatch<SetStateAction<Conversation | undefined>>;
setCurrentSystemPromptId: Dispatch<SetStateAction<string | undefined>>;
}
/**
* Manages the current conversation state. Interacts with the conversation API and keeps local state up to date.
* Manages system prompt as that is per conversation
* Provides methods to handle conversation selection, creation, deletion, and system prompt selection.
*/
export const useCurrentConversation = ({
allSystemPrompts,
conversationId,
conversations,
defaultConnector,
mayUpdateConversations,
refetchCurrentUserConversations,
}: Props): UseCurrentConversation => {
const {
createConversation,
deleteConversation,
getConversation,
getDefaultConversation,
setApiConfig,
} = useConversation();
const [currentConversation, setCurrentConversation] = useState<Conversation | undefined>();
const [currentConversationId, setCurrentConversationId] = useState<string | undefined>(
conversationId
);
useEffect(() => {
setCurrentConversationId(conversationId);
}, [conversationId]);
/**
* START SYSTEM PROMPT
*/
const currentSystemPrompt = useMemo(
() =>
getDefaultSystemPrompt({
allSystemPrompts,
conversation: currentConversation,
}),
[allSystemPrompts, currentConversation]
);
const [currentSystemPromptId, setCurrentSystemPromptId] = useState<string | undefined>(
currentSystemPrompt?.id
);
useEffect(() => {
setCurrentSystemPromptId(currentSystemPrompt?.id);
}, [currentSystemPrompt?.id]);
/**
* END SYSTEM PROMPT
*/
/**
* Refetches the current conversation, optionally by conversation ID or title.
* @param cId - The conversation ID to refetch.
* @param cTitle - The conversation title to refetch.
* @param isStreamRefetch - Are we refetching because stream completed? If so retry several times to ensure the message has updated on the server
*/
const refetchCurrentConversation = useCallback(
async ({
cId,
cTitle,
isStreamRefetch = false,
}: { cId?: string; cTitle?: string; isStreamRefetch?: boolean } = {}) => {
if (cId === '' || (cTitle && !conversations[cTitle])) {
return;
}
const cConversationId =
cId ?? (cTitle && conversations[cTitle].id) ?? currentConversation?.id;
if (cConversationId) {
let updatedConversation = await getConversation(cConversationId);
let retries = 0;
const maxRetries = 5;
// this retry is a workaround for the stream not YET being persisted to the stored conversation
while (
isStreamRefetch &&
updatedConversation &&
updatedConversation.messages[updatedConversation.messages.length - 1].role !==
'assistant' &&
retries < maxRetries
) {
retries++;
await sleep(2000);
updatedConversation = await getConversation(cConversationId);
}
if (updatedConversation) {
setCurrentConversation(updatedConversation);
}
return updatedConversation;
}
},
[conversations, currentConversation?.id, getConversation]
);
const initializeDefaultConversationWithConnector = useCallback(
async (defaultConvo: Conversation): Promise<Conversation> => {
const apiConfig = getGenAiConfig(defaultConnector);
const updatedConvo =
(await setApiConfig({
conversation: defaultConvo,
apiConfig: {
...defaultConvo?.apiConfig,
connectorId: (defaultConnector?.id as string) ?? '',
actionTypeId: (defaultConnector?.actionTypeId as string) ?? '.gen-ai',
provider: apiConfig?.apiProvider,
model: apiConfig?.defaultModel,
defaultSystemPromptId: allSystemPrompts.find((sp) => sp.isNewConversationDefault)?.id,
},
})) ?? defaultConvo;
await refetchCurrentUserConversations();
return updatedConvo;
},
[allSystemPrompts, defaultConnector, refetchCurrentUserConversations, setApiConfig]
);
const handleOnConversationSelected = useCallback(
async ({ cId, cTitle }: { cId: string; cTitle: string }) => {
const allConversations = await refetchCurrentUserConversations();
// This is a default conversation that has not yet been initialized
// add the default connector config
if (cId === '' && allConversations?.data?.[cTitle]) {
const updatedConvo = await initializeDefaultConversationWithConnector(
allConversations.data[cTitle]
);
setCurrentConversationId(updatedConvo.id);
} else if (allConversations?.data?.[cId]) {
setCurrentConversationId(cId);
}
},
[
initializeDefaultConversationWithConnector,
refetchCurrentUserConversations,
setCurrentConversationId,
]
);
// update currentConversation when conversations or currentConversationId update
useEffect(() => {
if (!mayUpdateConversations) return;
const updateConversation = async () => {
const nextConversation: Conversation =
(currentConversationId && conversations[currentConversationId]) ||
// if currentConversationId is not an id, it should be a title from a
// default conversation that has not yet been initialized
find(conversations, ['title', currentConversationId]) ||
find(conversations, ['title', WELCOME_CONVERSATION_TITLE]) ||
// if no Welcome convo exists, create one
getDefaultConversation({ cTitle: WELCOME_CONVERSATION_TITLE });
if (nextConversation && nextConversation.id === '') {
// This is a default conversation that has not yet been initialized
const conversation = await initializeDefaultConversationWithConnector(nextConversation);
return setCurrentConversation(conversation);
}
setCurrentConversation((prev) => {
if (deepEqual(prev, nextConversation)) return prev;
if (
prev &&
prev.id === nextConversation.id &&
// if the conversation id has not changed and the previous conversation has more messages
// it is because the local conversation has a readable stream running
// and it has not yet been persisted to the stored conversation
prev.messages.length > nextConversation.messages.length
) {
return {
...nextConversation,
messages: prev.messages,
};
}
return nextConversation;
});
};
updateConversation();
}, [
currentConversationId,
conversations,
getDefaultConversation,
initializeDefaultConversationWithConnector,
mayUpdateConversations,
]);
const handleOnConversationDeleted = useCallback(
async (cTitle: string) => {
await deleteConversation(conversations[cTitle].id);
await refetchCurrentUserConversations();
},
[conversations, deleteConversation, refetchCurrentUserConversations]
);
const handleCreateConversation = useCallback(async () => {
const newChatExists = find(conversations, ['title', NEW_CHAT]);
if (newChatExists && !newChatExists.messages.length) {
handleOnConversationSelected({
cId: newChatExists.id,
cTitle: newChatExists.title,
});
return;
}
const newConversation = await createConversation({
title: NEW_CHAT,
apiConfig: currentConversation?.apiConfig,
});
if (newConversation) {
handleOnConversationSelected({
cId: newConversation.id,
cTitle: newConversation.title,
});
} else {
await refetchCurrentUserConversations();
}
}, [
conversations,
createConversation,
currentConversation?.apiConfig,
handleOnConversationSelected,
refetchCurrentUserConversations,
]);
return {
currentConversation,
currentSystemPromptId,
handleCreateConversation,
handleOnConversationDeleted,
handleOnConversationSelected,
refetchCurrentConversation,
setCurrentConversation,
setCurrentSystemPromptId,
};
};

View file

@ -0,0 +1,106 @@
/*
* 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 { useCallback, useMemo, useState } from 'react';
import type { HttpSetup } from '@kbn/core-http-browser';
import { PromptResponse, PromptTypeEnum } from '@kbn/elastic-assistant-common';
import type { FindAnonymizationFieldsResponse } from '@kbn/elastic-assistant-common/impl/schemas/anonymization_fields/find_anonymization_fields_route.gen';
import { QueryObserverResult, RefetchOptions, RefetchQueryFilters } from '@tanstack/react-query';
import { useFetchAnonymizationFields } from './api/anonymization_fields/use_fetch_anonymization_fields';
import { FetchConversationsResponse, useFetchPrompts } from './api';
import {
Conversation,
mergeBaseWithPersistedConversations,
useFetchCurrentUserConversations,
} from '../..';
interface Props {
baseConversations: Record<string, Conversation>;
http: HttpSetup;
isAssistantEnabled: boolean;
}
export interface DataStreamApis {
allPrompts: PromptResponse[];
allSystemPrompts: PromptResponse[];
anonymizationFields: FindAnonymizationFieldsResponse;
conversations: Record<string, Conversation>;
isErrorAnonymizationFields: boolean;
isFetchedAnonymizationFields: boolean;
isFetchedCurrentUserConversations: boolean;
isLoadingAnonymizationFields: boolean;
isLoadingCurrentUserConversations: boolean;
isLoadingPrompts: boolean;
isFetchedPrompts: boolean;
refetchPrompts: (
options?: RefetchOptions & RefetchQueryFilters<unknown>
) => Promise<QueryObserverResult<unknown, unknown>>;
refetchCurrentUserConversations: () => Promise<
QueryObserverResult<Record<string, Conversation>, unknown>
>;
setIsStreaming: (isStreaming: boolean) => void;
}
export const useDataStreamApis = ({
http,
baseConversations,
isAssistantEnabled,
}: Props): DataStreamApis => {
const [isStreaming, setIsStreaming] = useState(false);
const onFetchedConversations = useCallback(
(conversationsData: FetchConversationsResponse): Record<string, Conversation> =>
mergeBaseWithPersistedConversations(baseConversations, conversationsData),
[baseConversations]
);
const {
data: conversations,
isLoading: isLoadingCurrentUserConversations,
refetch: refetchCurrentUserConversations,
isFetched: isFetchedCurrentUserConversations,
} = useFetchCurrentUserConversations({
http,
onFetch: onFetchedConversations,
refetchOnWindowFocus: !isStreaming,
isAssistantEnabled,
});
const {
data: anonymizationFields,
isLoading: isLoadingAnonymizationFields,
isError: isErrorAnonymizationFields,
isFetched: isFetchedAnonymizationFields,
} = useFetchAnonymizationFields();
const {
data: { data: allPrompts },
refetch: refetchPrompts,
isLoading: isLoadingPrompts,
isFetched: isFetchedPrompts,
} = useFetchPrompts();
const allSystemPrompts = useMemo(() => {
if (!isLoadingPrompts) {
return allPrompts.filter((p) => p.promptType === PromptTypeEnum.system);
}
return [];
}, [allPrompts, isLoadingPrompts]);
return {
allPrompts,
allSystemPrompts,
anonymizationFields,
conversations,
isErrorAnonymizationFields,
isFetchedAnonymizationFields,
isFetchedCurrentUserConversations,
isLoadingAnonymizationFields,
isLoadingCurrentUserConversations,
isLoadingPrompts,
isFetchedPrompts,
refetchPrompts,
refetchCurrentUserConversations,
setIsStreaming,
};
};

View file

@ -9,8 +9,6 @@ import { KnowledgeBaseConfig } from '../assistant/types';
export const ATTACK_DISCOVERY_STORAGE_KEY = 'attackDiscovery';
export const DEFAULT_ASSISTANT_NAMESPACE = 'elasticAssistantDefault';
export const QUICK_PROMPT_LOCAL_STORAGE_KEY = 'quickPrompts';
export const SYSTEM_PROMPT_LOCAL_STORAGE_KEY = 'systemPrompts';
export const LAST_CONVERSATION_ID_LOCAL_STORAGE_KEY = 'lastConversationId';
export const KNOWLEDGE_BASE_LOCAL_STORAGE_KEY = 'knowledgeBase';
export const STREAMING_LOCAL_STORAGE_KEY = 'streaming';

View file

@ -19,23 +19,6 @@ export interface ClientMessage extends Omit<Message, 'content' | 'reader'> {
content?: string;
presentation?: MessagePresentation;
}
export interface ConversationTheme {
title?: string;
titleIcon?: string;
user?: {
name?: string;
icon?: string;
};
assistant?: {
name?: string;
icon?: string;
};
system?: {
name?: string;
icon?: string;
};
}
/**
* Complete state to reconstruct a conversation instance.
* Includes all messages, connector configured, and relevant UI state.

View file

@ -78,6 +78,7 @@ export const ConnectorSelector: React.FC<Props> = React.memo(
<EuiButtonEmpty
data-test-subj="addNewConnectorButton"
href="#"
isDisabled={localIsDisabled}
iconType="plus"
size="xs"
>
@ -91,7 +92,7 @@ export const ConnectorSelector: React.FC<Props> = React.memo(
</EuiFlexGroup>
),
};
}, []);
}, [localIsDisabled]);
const connectorOptions = useMemo(
() =>
@ -188,6 +189,7 @@ export const ConnectorSelector: React.FC<Props> = React.memo(
<EuiButtonEmpty
data-test-subj="addNewConnectorButton"
iconType="plusInCircle"
isDisabled={localIsDisabled}
size="xs"
onClick={() => setIsConnectorModalVisible(true)}
>

View file

@ -29,7 +29,7 @@ interface Props {
actionTypeSelectorInline: boolean;
}
const itemClassName = css`
inline-size: 240px;
inline-size: 220px;
.euiKeyPadMenuItem__label {
white-space: nowrap;

View file

@ -84,34 +84,6 @@ export const ADD_CONNECTOR_MISSING_PRIVILEGES_DESCRIPTION = i18n.translate(
}
);
export const CONNECTOR_SETUP_USER_YOU = i18n.translate(
'xpack.elasticAssistant.assistant.connectors.setup.userYouTitle',
{
defaultMessage: 'You',
}
);
export const CONNECTOR_SETUP_USER_ASSISTANT = i18n.translate(
'xpack.elasticAssistant.assistant.connectors.setup.userAssistantTitle',
{
defaultMessage: 'Assistant',
}
);
export const CONNECTOR_SETUP_TIMESTAMP_AT = i18n.translate(
'xpack.elasticAssistant.assistant.connectors.setup.timestampAtTitle',
{
defaultMessage: 'at',
}
);
export const CONNECTOR_SETUP_SKIP = i18n.translate(
'xpack.elasticAssistant.assistant.connectors.setup.skipTitle',
{
defaultMessage: 'Click to skip...',
}
);
export const MISSING_CONNECTOR_CALLOUT_TITLE = i18n.translate(
'xpack.elasticAssistant.assistant.connectors.connectorMissingCallout.calloutTitle',
{
@ -125,17 +97,3 @@ export const MISSING_CONNECTOR_CONVERSATION_SETTINGS_LINK = i18n.translate(
defaultMessage: 'Conversation Settings',
}
);
export const CREATE_CONNECTOR_BUTTON = i18n.translate(
'xpack.elasticAssistant.assistant.connectors.createConnectorButton',
{
defaultMessage: 'Connector',
}
);
export const REFRESH_CONNECTORS_BUTTON = i18n.translate(
'xpack.elasticAssistant.assistant.connectors.refreshConnectorsButton',
{
defaultMessage: 'Refresh',
}
);

View file

@ -7,7 +7,6 @@
import type { UseQueryResult } from '@tanstack/react-query';
import { useQuery } from '@tanstack/react-query';
import type { UserProfile } from '@kbn/security-plugin/common';
import type { ServerError } from '@kbn/cases-plugin/public/types';
import { loadActionTypes } from '@kbn/triggers-actions-ui-plugin/public/common/constants';
import type { IHttpFetchError } from '@kbn/core-http-browser';
@ -62,5 +61,3 @@ export const useLoadActionTypes = ({
}
);
};
export type UseSuggestUserProfiles = UseQueryResult<UserProfile[], ServerError>;

View file

@ -7,31 +7,6 @@
import { i18n } from '@kbn/i18n';
export const YOU_ARE_A_HELPFUL_EXPERT_ASSISTANT = i18n.translate(
'xpack.elasticAssistant.assistant.content.prompts.system.youAreAHelpfulExpertAssistant',
{
defaultMessage:
'You are a helpful, expert assistant who answers questions about Elastic Security.',
}
);
export const IF_YOU_DONT_KNOW_THE_ANSWER = i18n.translate(
'xpack.elasticAssistant.assistant.content.prompts.system.ifYouDontKnowTheAnswer',
{
defaultMessage: 'Do not answer questions unrelated to Elastic Security.',
}
);
export const SUPERHERO_PERSONALITY = i18n.translate(
'xpack.elasticAssistant.assistant.content.prompts.system.superheroPersonality',
{
defaultMessage:
'Provide the most detailed and relevant answer possible, as if you were relaying this information back to a cyber security expert.',
}
);
export const DEFAULT_SYSTEM_PROMPT_NON_I18N = `${YOU_ARE_A_HELPFUL_EXPERT_ASSISTANT} ${IF_YOU_DONT_KNOW_THE_ANSWER}`;
export const DEFAULT_SYSTEM_PROMPT_NAME = i18n.translate(
'xpack.elasticAssistant.assistant.content.prompts.system.defaultSystemPromptName',
{
@ -39,30 +14,6 @@ export const DEFAULT_SYSTEM_PROMPT_NAME = i18n.translate(
}
);
export const DEFAULT_SYSTEM_PROMPT_LABEL = i18n.translate(
'xpack.elasticAssistant.assistant.content.prompts.system.defaultSystemPromptLabel',
{
defaultMessage: 'Default',
}
);
export const SUPERHERO_SYSTEM_PROMPT_NON_I18N = `${YOU_ARE_A_HELPFUL_EXPERT_ASSISTANT} ${IF_YOU_DONT_KNOW_THE_ANSWER}
${SUPERHERO_PERSONALITY}`;
export const SUPERHERO_SYSTEM_PROMPT_NAME = i18n.translate(
'xpack.elasticAssistant.assistant.content.prompts.system.superheroSystemPromptName',
{
defaultMessage: 'Enhanced system prompt',
}
);
export const SUPERHERO_SYSTEM_PROMPT_LABEL = i18n.translate(
'xpack.elasticAssistant.assistant.content.prompts.system.superheroSystemPromptLabel',
{
defaultMessage: 'Enhanced',
}
);
export const SYSTEM_PROMPT_CONTEXT_NON_I18N = (context: string) => {
return `CONTEXT:\n"""\n${context}\n"""`;
};

View file

@ -7,27 +7,6 @@
import { i18n } from '@kbn/i18n';
export const CALLOUT_PARAGRAPH1 = i18n.translate(
'xpack.elasticAssistant.dataAnonymization.settings.anonymizationSettings.calloutParagraph1',
{
defaultMessage: 'The fields below are allowed by default',
}
);
export const CALLOUT_PARAGRAPH2 = i18n.translate(
'xpack.elasticAssistant.dataAnonymization.settings.anonymizationSettings.calloutParagraph2',
{
defaultMessage: 'Optionally enable anonymization for these fields',
}
);
export const CALLOUT_TITLE = i18n.translate(
'xpack.elasticAssistant.dataAnonymization.settings.anonymizationSettings.calloutTitle',
{
defaultMessage: 'Anonymization defaults',
}
);
export const SETTINGS_TITLE = i18n.translate(
'xpack.elasticAssistant.dataAnonymization.settings.anonymizationSettings.settingsTitle',
{

View file

@ -21,13 +21,6 @@ export const ALLOW = i18n.translate(
}
);
export const ALLOW_BY_DEFAULT = i18n.translate(
'xpack.elasticAssistant.assistant.dataAnonymizationEditor.contextEditor.allowByDefaultAction',
{
defaultMessage: 'Allow by default',
}
);
export const ALLOWED = i18n.translate(
'xpack.elasticAssistant.assistant.dataAnonymizationEditor.contextEditor.allowedColumnTitle',
{
@ -48,14 +41,6 @@ export const ANONYMIZE = i18n.translate(
defaultMessage: 'Anonymize',
}
);
export const ANONYMIZE_BY_DEFAULT = i18n.translate(
'xpack.elasticAssistant.assistant.dataAnonymizationEditor.contextEditor.anonymizeByDefaultAction',
{
defaultMessage: 'Anonymize by default',
}
);
export const ANONYMIZED = i18n.translate(
'xpack.elasticAssistant.assistant.dataAnonymizationEditor.contextEditor.anonymizedColumnTitle',
{
@ -83,14 +68,6 @@ export const DENY = i18n.translate(
defaultMessage: 'Deny',
}
);
export const DENY_BY_DEFAULT = i18n.translate(
'xpack.elasticAssistant.assistant.dataAnonymizationEditor.contextEditor.denyByDefaultAction',
{
defaultMessage: 'Deny by default',
}
);
export const FIELD = i18n.translate(
'xpack.elasticAssistant.assistant.dataAnonymizationEditor.contextEditor.fieldColumnTitle',
{
@ -130,13 +107,6 @@ export const UNANONYMIZE = i18n.translate(
}
);
export const UNANONYMIZE_BY_DEFAULT = i18n.translate(
'xpack.elasticAssistant.assistant.dataAnonymizationEditor.contextEditor.unanonymizeByDefaultAction',
{
defaultMessage: 'Unanonymize by default',
}
);
export const VALUES = i18n.translate(
'xpack.elasticAssistant.assistant.dataAnonymizationEditor.contextEditor.valuesColumnTitle',
{

View file

@ -6,4 +6,3 @@
*/
export const TITLE_SIZE = 'xs';
export const STAT_TITLE_SIZE = 'm';

View file

@ -13,14 +13,6 @@ export const ALERTS_LABEL = i18n.translate(
defaultMessage: 'Alerts',
}
);
export const ASK_QUESTIONS_ABOUT = i18n.translate(
'xpack.elasticAssistant.assistant.settings.knowledgeBaseSettings.askQuestionsAboutLabel',
{
defaultMessage: 'Ask questions about the',
}
);
export const LATEST_AND_RISKIEST_OPEN_ALERTS = (alertsCount: number) =>
i18n.translate(
'xpack.elasticAssistant.assistant.settings.knowledgeBaseSettings.latestAndRiskiestOpenAlertsLabel',
@ -93,14 +85,6 @@ export const SETUP_KNOWLEDGE_BASE_BUTTON_TOOLTIP = i18n.translate(
defaultMessage: 'Knowledge Base unavailable, please see documentation for more details.',
}
);
export const KNOWLEDGE_BASE_TOOLTIP = i18n.translate(
'xpack.elasticAssistant.assistant.settings.knowledgeBaseSettings.knowledgeBaseTooltip',
{
defaultMessage: 'ELSER must be configured to enable the Knowledge Base',
}
);
export const KNOWLEDGE_BASE_DESCRIPTION = i18n.translate(
'xpack.elasticAssistant.assistant.settings.knowledgeBaseSettings.knowledgeBaseDescription',
{
@ -117,20 +101,6 @@ export const KNOWLEDGE_BASE_DESCRIPTION_INSTALLED = (kbIndexPattern: string) =>
}
);
export const KNOWLEDGE_BASE_INIT_BUTTON = i18n.translate(
'xpack.elasticAssistant.assistant.settings.knowledgeBaseSettings.initializeKnowledgeBaseButton',
{
defaultMessage: 'Initialize',
}
);
export const KNOWLEDGE_BASE_DELETE_BUTTON = i18n.translate(
'xpack.elasticAssistant.assistant.settings.knowledgeBaseSettings.deleteKnowledgeBaseButton',
{
defaultMessage: 'Delete',
}
);
export const KNOWLEDGE_BASE_ELSER_LABEL = i18n.translate(
'xpack.elasticAssistant.assistant.settings.knowledgeBaseSettings.elserLabel',
{
@ -138,20 +108,6 @@ export const KNOWLEDGE_BASE_ELSER_LABEL = i18n.translate(
}
);
export const KNOWLEDGE_BASE_ELSER_MACHINE_LEARNING = i18n.translate(
'xpack.elasticAssistant.assistant.settings.knowledgeBaseSettings.elserMachineLearningDescription',
{
defaultMessage: 'Machine Learning',
}
);
export const KNOWLEDGE_BASE_ELSER_SEE_DOCS = i18n.translate(
'xpack.elasticAssistant.assistant.settings.knowledgeBaseSettings.elserSeeDocsDescription',
{
defaultMessage: 'See docs',
}
);
export const ESQL_LABEL = i18n.translate(
'xpack.elasticAssistant.assistant.settings.knowledgeBaseSettings.esqlLabel',
{

View file

@ -1,16 +0,0 @@
/*
* 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 { PromptResponse } from '@kbn/elastic-assistant-common';
export const mockUserPrompt: PromptResponse = {
id: 'mock-user-prompt-1',
content: `Explain the meaning from the context above, then summarize a list of suggested Elasticsearch KQL and EQL queries.
Finally, suggest an investigation guide, and format it as markdown.`,
name: 'Mock user prompt',
promptType: 'quick',
};

View file

@ -22,7 +22,6 @@
"@kbn/stack-connectors-plugin",
"@kbn/triggers-actions-ui-plugin",
"@kbn/core-http-browser-mocks",
"@kbn/security-plugin",
"@kbn/cases-plugin",
"@kbn/actions-plugin",
"@kbn/core-notifications-browser",

View file

@ -15084,7 +15084,6 @@
"xpack.ecsDataQualityDashboard.getIndexStats.dateRangeRequiredErrorMessage": "\"startDate\" et \"endDate\" sont requis",
"xpack.elasticAssistant.anonymizationFields.bulkActionsAnonymizationFieldsError": "Erreur lors de la mise à jour des champs d'anonymisation {error}",
"xpack.elasticAssistant.assistant.apiErrorTitle": "Une erreur sest produite lors de lenvoi de votre message.",
"xpack.elasticAssistant.assistant.clearChat": "Effacer le chat",
"xpack.elasticAssistant.assistant.connectors.addConnectorButton.description": "Configurez un connecteur pour continuer la conversation",
"xpack.elasticAssistant.assistant.connectors.addConnectorButton.missingPrivilegesDescription": "Veuillez contacter votre administrateur pour activer le connecteur d'intelligence artificielle générative.",
"xpack.elasticAssistant.assistant.connectors.addConnectorButton.missingPrivilegesTitle": "Connecteur d'intelligence artificielle générative requis",
@ -15096,20 +15095,8 @@
"xpack.elasticAssistant.assistant.connectors.connectorSelector.ariaLabel": "Sélecteur de connecteur",
"xpack.elasticAssistant.assistant.connectors.connectorSelector.newConnectorOptions": "Ajouter un nouveau connecteur...",
"xpack.elasticAssistant.assistant.connectors.connectorSelectorInline.connectorPlaceholder": "Sélectionner un connecteur",
"xpack.elasticAssistant.assistant.connectors.createConnectorButton": "Connecteur",
"xpack.elasticAssistant.assistant.connectors.preconfiguredTitle": "Préconfiguré",
"xpack.elasticAssistant.assistant.connectors.refreshConnectorsButton": "Actualiser",
"xpack.elasticAssistant.assistant.connectors.setup.skipTitle": "Cliquez pour ignorer...",
"xpack.elasticAssistant.assistant.connectors.setup.timestampAtTitle": "à",
"xpack.elasticAssistant.assistant.connectors.setup.userAssistantTitle": "Assistant",
"xpack.elasticAssistant.assistant.connectors.setup.userYouTitle": "Vous",
"xpack.elasticAssistant.assistant.content.prompts.system.defaultSystemPromptLabel": "Par défaut",
"xpack.elasticAssistant.assistant.content.prompts.system.defaultSystemPromptName": "Invite de système par défaut",
"xpack.elasticAssistant.assistant.content.prompts.system.ifYouDontKnowTheAnswer": "Ne répondez pas aux questions qui ne sont pas liées à Elastic Security.",
"xpack.elasticAssistant.assistant.content.prompts.system.superheroPersonality": "Donnez la réponse la plus pertinente et détaillée possible, comme si vous deviez communiquer ces informations à un expert en cybersécurité.",
"xpack.elasticAssistant.assistant.content.prompts.system.superheroSystemPromptLabel": "Amélioré",
"xpack.elasticAssistant.assistant.content.prompts.system.superheroSystemPromptName": "Invite système améliorée",
"xpack.elasticAssistant.assistant.content.prompts.system.youAreAHelpfulExpertAssistant": "Vous êtes un assistant expert et serviable qui répond à des questions au sujet dElastic Security.",
"xpack.elasticAssistant.assistant.conversations.settings.connectorTitle": "Connecteur",
"xpack.elasticAssistant.assistant.conversations.settings.promptHelpTextTitle": "Contexte fourni dans le cadre de chaque conversation.",
"xpack.elasticAssistant.assistant.conversations.settings.promptTitle": "Invite système",
@ -15127,7 +15114,6 @@
"xpack.elasticAssistant.assistant.conversationSelector.nextConversationTitle": "Conversation suivante",
"xpack.elasticAssistant.assistant.conversationSelector.placeholderTitle": "Sélectionnez ou saisissez pour créer une nouvelle...",
"xpack.elasticAssistant.assistant.conversationSelector.previousConversationTitle": "Conversation précédente",
"xpack.elasticAssistant.assistant.conversationSettings.column.actions": "Actions",
"xpack.elasticAssistant.assistant.conversationSettings.column.connector": "Connecteur",
"xpack.elasticAssistant.assistant.conversationSettings.column.systemPrompt": "Invite système",
"xpack.elasticAssistant.assistant.conversationSettings.column.Title": "Titre",
@ -15140,21 +15126,17 @@
"xpack.elasticAssistant.assistant.conversationSettings.title": "Paramètres",
"xpack.elasticAssistant.assistant.dataAnonymizationEditor.contextEditor.allActionsTooltip": "Toutes les actions",
"xpack.elasticAssistant.assistant.dataAnonymizationEditor.contextEditor.allowAction": "Autoriser",
"xpack.elasticAssistant.assistant.dataAnonymizationEditor.contextEditor.allowByDefaultAction": "Permettre par défaut",
"xpack.elasticAssistant.assistant.dataAnonymizationEditor.contextEditor.allowedColumnTitle": "Permis",
"xpack.elasticAssistant.assistant.dataAnonymizationEditor.contextEditor.alwaysSubmenu": "Toujours",
"xpack.elasticAssistant.assistant.dataAnonymizationEditor.contextEditor.anonymizeAction": "Anonymiser",
"xpack.elasticAssistant.assistant.dataAnonymizationEditor.contextEditor.anonymizeByDefaultAction": "Anonymiser par défaut",
"xpack.elasticAssistant.assistant.dataAnonymizationEditor.contextEditor.anonymizedColumnTitle": "Anonymisé",
"xpack.elasticAssistant.assistant.dataAnonymizationEditor.contextEditor.bulkActions": "Actions groupées",
"xpack.elasticAssistant.assistant.dataAnonymizationEditor.contextEditor.defaultsSubmenu": "Par défaut",
"xpack.elasticAssistant.assistant.dataAnonymizationEditor.contextEditor.denyAction": "Refuser",
"xpack.elasticAssistant.assistant.dataAnonymizationEditor.contextEditor.denyByDefaultAction": "Refuser par défaut",
"xpack.elasticAssistant.assistant.dataAnonymizationEditor.contextEditor.fieldColumnTitle": "Champ",
"xpack.elasticAssistant.assistant.dataAnonymizationEditor.contextEditor.noButtonLabel": "Non",
"xpack.elasticAssistant.assistant.dataAnonymizationEditor.contextEditor.resetButton": "Réinitialiser",
"xpack.elasticAssistant.assistant.dataAnonymizationEditor.contextEditor.unanonymizeAction": "Désanonymiser",
"xpack.elasticAssistant.assistant.dataAnonymizationEditor.contextEditor.unanonymizeByDefaultAction": "Désanonymiser par défaut",
"xpack.elasticAssistant.assistant.dataAnonymizationEditor.contextEditor.valuesColumnTitle": "Valeurs",
"xpack.elasticAssistant.assistant.dataAnonymizationEditor.contextEditor.yesButtonLabel": "Oui",
"xpack.elasticAssistant.assistant.defaultAssistantTitle": "Assistant dintelligence artificielle dElastic",
@ -15165,13 +15147,9 @@
"xpack.elasticAssistant.assistant.emptyScreen.description": "Demandez-moi tout ce que vous voulez, de \"Résumez cette alerte\" à \"Aidez-moi à construire une requête\" en utilisant l'invite suivante du système :",
"xpack.elasticAssistant.assistant.emptyScreen.title": "Comment puis-je vous aider ?",
"xpack.elasticAssistant.assistant.firstPromptEditor.addNewSystemPrompt": "Ajouter une nouvelle invite système...",
"xpack.elasticAssistant.assistant.firstPromptEditor.addSystemPromptTooltip": "Ajouter une invite système",
"xpack.elasticAssistant.assistant.firstPromptEditor.clearSystemPrompt": "Effacer une invite système",
"xpack.elasticAssistant.assistant.firstPromptEditor.commentsListAriaLabel": "Liste de commentaires",
"xpack.elasticAssistant.assistant.firstPromptEditor.editingPromptLabel": "modification dinvite",
"xpack.elasticAssistant.assistant.firstPromptEditor.emptyPrompt": "(invite vide)",
"xpack.elasticAssistant.assistant.firstPromptEditor.selectASystemPromptPlaceholder": "Sélectionner une invite système",
"xpack.elasticAssistant.assistant.firstPromptEditor.youLabel": "Vous",
"xpack.elasticAssistant.assistant.newChat.newChatButton": "Chat",
"xpack.elasticAssistant.assistant.newChatByTitle.newChatByTitleButton": "Chat",
"xpack.elasticAssistant.assistant.overlay.CancelButton": "Annuler",
@ -15183,7 +15161,6 @@
"xpack.elasticAssistant.assistant.promptEditor.systemPrompt.settings.defaultConversationsHelpText": "Les conversations devant utiliser cette invite système par défaut.",
"xpack.elasticAssistant.assistant.promptEditor.systemPrompt.settings.defaultConversationsLabel": "Conversations par défaut",
"xpack.elasticAssistant.assistant.promptEditor.systemPrompt.settings.defaultNewConversationTitle": "Utiliser par défaut pour toutes les nouvelles conversations",
"xpack.elasticAssistant.assistant.promptEditor.systemPrompt.settings.modalTitle": "Invites système",
"xpack.elasticAssistant.assistant.promptEditor.systemPrompt.settings.nameLabel": "Nom",
"xpack.elasticAssistant.assistant.promptEditor.systemPrompt.settings.promptLabel": "Invite",
"xpack.elasticAssistant.assistant.promptEditor.systemPrompt.settings.promptPlaceholder": "Saisir une invite système",
@ -15197,7 +15174,6 @@
"xpack.elasticAssistant.assistant.promptEditor.systemPrompt.systemPromptModal.systemPromptSelector.deletePromptTitle": "Supprimer une invite système",
"xpack.elasticAssistant.assistant.promptPlaceholder": "Demandez-moi tout ce que vous voulez, de \"résume cette alerte\" à \"aide-moi à créer une recherche...\"",
"xpack.elasticAssistant.assistant.promptsTable.settingsDescription": "Créer et gérer des invites système. Les invites système sont des morceaux de contexte configurables systématiquement envoyés pour une conversation donnée.",
"xpack.elasticAssistant.assistant.promptsTable.systemPromptsTableColumnActions": "Actions",
"xpack.elasticAssistant.assistant.promptsTable.systemPromptsTableColumnDateUpdated": "Date de mise à jour",
"xpack.elasticAssistant.assistant.promptsTable.systemPromptsTableColumnDefaultConversations": "Conversations par défaut",
"xpack.elasticAssistant.assistant.promptsTable.systemPromptsTableColumnName": "Nom",
@ -15215,7 +15191,6 @@
"xpack.elasticAssistant.assistant.quickPrompts.settings.badgeColorLabel": "Couleur de badge",
"xpack.elasticAssistant.assistant.quickPrompts.settings.contextsHelpText": "Sélectionnez l'emplacement où cette invite rapide apparaîtra. Cette invite apparaîtra partout si vous nen sélectionnez aucun.",
"xpack.elasticAssistant.assistant.quickPrompts.settings.contextsLabel": "Contextes",
"xpack.elasticAssistant.assistant.quickPrompts.settings.modalTitle": "Invites rapides",
"xpack.elasticAssistant.assistant.quickPrompts.settings.nameLabel": "Nom",
"xpack.elasticAssistant.assistant.quickPrompts.settings.promptLabel": "Invite",
"xpack.elasticAssistant.assistant.quickPrompts.settings.promptPlaceholder": "Saisir une invite rapide",
@ -15226,7 +15201,6 @@
"xpack.elasticAssistant.assistant.quickPromptsTable.modal.deleteQuickPromptConfirmationDefaultTitle": "Supprimer linvite rapide ?",
"xpack.elasticAssistant.assistant.quickPromptsTable.modal.deleteQuickPromptConfirmationMessage": "Vous ne pouvez pas récupérer une invite supprimée",
"xpack.elasticAssistant.assistant.quickPromptsTable.modal.deleteQuickPromptConfirmationTitle": "Supprimer \"{prompt}\" ?",
"xpack.elasticAssistant.assistant.quickPromptsTable.quickPromptsTableColumnActions": "Actions",
"xpack.elasticAssistant.assistant.quickPromptsTable.quickPromptsTableColumnContexts": "Contextes",
"xpack.elasticAssistant.assistant.quickPromptsTable.quickPromptsTableColumnDateUpdated": "Date de mise à jour",
"xpack.elasticAssistant.assistant.quickPromptsTable.quickPromptsTableColumnName": "Nom",
@ -15234,9 +15208,7 @@
"xpack.elasticAssistant.assistant.resetConversationModal.clearChatConfirmation": "Êtes-vous sûr de vouloir effacer le chat actuel ? Toutes les données de conversation seront perdues.",
"xpack.elasticAssistant.assistant.resetConversationModal.resetButtonText": "Réinitialiser",
"xpack.elasticAssistant.assistant.settings.actionsButtonTitle": "Actions",
"xpack.elasticAssistant.assistant.settings.anonymizedValues": "Valeurs anonymisées",
"xpack.elasticAssistant.assistant.settings.connectorHelpTextTitle": "Le connecteur LLM par défaut pour ce type de conversation.",
"xpack.elasticAssistant.assistant.settings.connectorTitle": "Connecteur",
"xpack.elasticAssistant.assistant.settings.deleteButtonTitle": "Supprimer",
"xpack.elasticAssistant.assistant.settings.editButtonTitle": "Modifier",
"xpack.elasticAssistant.assistant.settings.evaluationSettings.apmUrlDescription": "URL pour l'application APM de Kibana. Utilisé pour établir un lien avec les traces APM pour les résultats de l'évaluation. La valeur par défaut est \"{defaultUrlPath}\".",
@ -15269,26 +15241,19 @@
"xpack.elasticAssistant.assistant.settings.knowledgeBasedSettings.knowledgeBaseDescription": "Pour commencer, configurez ELSER dans {machineLearning}. {seeDocs}",
"xpack.elasticAssistant.assistant.settings.knowledgeBaseSettings.alertsLabel": "Alertes",
"xpack.elasticAssistant.assistant.settings.knowledgeBaseSettings.alertsRangeSliderLabel": "Plage d'alertes",
"xpack.elasticAssistant.assistant.settings.knowledgeBaseSettings.askQuestionsAboutLabel": "Poser une question à propos de",
"xpack.elasticAssistant.assistant.settings.knowledgeBaseSettings.deleteKnowledgeBaseButton": "Supprimer",
"xpack.elasticAssistant.assistant.settings.knowledgeBaseSettings.elserLabel": "ELSER configuré",
"xpack.elasticAssistant.assistant.settings.knowledgeBaseSettings.elserMachineLearningDescription": "Machine Learning",
"xpack.elasticAssistant.assistant.settings.knowledgeBaseSettings.elserSeeDocsDescription": "Voir les documents",
"xpack.elasticAssistant.assistant.settings.knowledgeBaseSettings.esqlDescription": "Documents de la base de connaissances pour générer des requêtes ES|QL",
"xpack.elasticAssistant.assistant.settings.knowledgeBaseSettings.esqlInstalledDescription": "Documents de la base de connaissances ES|QL chargés",
"xpack.elasticAssistant.assistant.settings.knowledgeBaseSettings.esqlLabel": "Documents de la base de connaissances ES|QL",
"xpack.elasticAssistant.assistant.settings.knowledgeBaseSettings.initializeKnowledgeBaseButton": "Initialiser",
"xpack.elasticAssistant.assistant.settings.knowledgeBaseSettings.knowledgeBaseDescription": "Index où sont stockés les documents de la base de connaissances",
"xpack.elasticAssistant.assistant.settings.knowledgeBaseSettings.knowledgeBaseInstalledDescription": "Initialisé sur `{kbIndexPattern}`",
"xpack.elasticAssistant.assistant.settings.knowledgeBaseSettings.knowledgeBaseLabel": "Base de connaissances",
"xpack.elasticAssistant.assistant.settings.knowledgeBaseSettings.knowledgeBaseTooltip": "ELSER doit être configuré pour activer la base de connaissances",
"xpack.elasticAssistant.assistant.settings.knowledgeBaseSettings.latestAndRiskiestOpenAlertsLabel": "Envoyez à l'Assistant d'IA des informations sur vos {alertsCount} alertes ouvertes ou confirmées les plus récentes et les plus risquées.",
"xpack.elasticAssistant.assistant.settings.knowledgeBaseSettings.selectFewerAlertsLabel": "Envoyez moins d'alertes si la fenêtre contextuelle du modèle est trop petite.",
"xpack.elasticAssistant.assistant.settings.knowledgeBaseSettings.settingsBadgeTitle": "Expérimental",
"xpack.elasticAssistant.assistant.settings.knowledgeBaseSettings.settingsDescription": "documentation",
"xpack.elasticAssistant.assistant.settings.knowledgeBaseSettings.settingsTitle": "Base de connaissances",
"xpack.elasticAssistant.assistant.settings.knowledgeBaseSettings.yourAnonymizationSettingsLabel": "Vos paramètres d'anonymisation seront appliqués à ces alertes.",
"xpack.elasticAssistant.assistant.settings.modalTitle": "Invites système",
"xpack.elasticAssistant.assistant.settings.resetConversation": "Réinitialiser la conversation",
"xpack.elasticAssistant.assistant.settings.securityAiSettingsTitle": "Paramètres dIA de sécurité",
"xpack.elasticAssistant.assistant.settings.settingsAnonymizationMenuItemTitle": "Anonymisation",
@ -15303,11 +15268,9 @@
"xpack.elasticAssistant.assistant.settings.settingsUpdatedToastTitle": "Paramètres mis à jour",
"xpack.elasticAssistant.assistant.settings.showAnonymizedToggleLabel": "Afficher les anonymisés",
"xpack.elasticAssistant.assistant.settings.showAnonymizedToggleRealValuesLabel": "Afficher les valeurs réelles",
"xpack.elasticAssistant.assistant.settings.showAnonymizedTooltip": "Afficher les valeurs anonymisées envoyées vers et depuis lassistant",
"xpack.elasticAssistant.assistant.sidePanel.deleteConversationAriaLabel": "Supprimer la conversation",
"xpack.elasticAssistant.assistant.submitMessage": "Envoyer un message",
"xpack.elasticAssistant.assistant.useConversation.defaultConversationTitle": "Par défaut",
"xpack.elasticAssistant.assistant.useConversation.elasticAiAssistantName": "Assistant",
"xpack.elasticAssistant.assistant.useConversation.elasticAiAssistantTitle": "Assistant dintelligence artificielle dElastic",
"xpack.elasticAssistant.assistant.useConversation.welcomeConversationTitle": "Bienvenue",
"xpack.elasticAssistant.assistant.welcomeScreen.description": "Avant toute chose, il faut configurer un Connecteur d'intelligence artificielle générative pour lancer cette expérience de chat !",
@ -15333,12 +15296,6 @@
"xpack.elasticAssistant.conversations.getConversationError": "Erreur lors de la recherche d'une conversation par l'identifiant {id}",
"xpack.elasticAssistant.conversations.getUserConversationsError": "Erreur lors de la recherche de conversations",
"xpack.elasticAssistant.conversations.updateConversationError": "Erreur lors de la mise à jour d'une conversation par l'identifiant {conversationId}",
"xpack.elasticAssistant.conversationSidepanel.titleField.titleIsRequired": "Un titre est requis",
"xpack.elasticAssistant.conversationSidepanel.titleField.uniqueTitle": "Le titre doit être unique",
"xpack.elasticAssistant.conversationSidepanel.titleFieldLabel": "Titre",
"xpack.elasticAssistant.dataAnonymization.settings.anonymizationSettings.calloutParagraph1": "Les champs ci-dessous sont permis par défaut",
"xpack.elasticAssistant.dataAnonymization.settings.anonymizationSettings.calloutParagraph2": "Activer optionnellement lanonymisation pour ces champs",
"xpack.elasticAssistant.dataAnonymization.settings.anonymizationSettings.calloutTitle": "Valeur par défaut de l'anonymisation",
"xpack.elasticAssistant.dataAnonymization.settings.anonymizationSettings.settingsDescription": "Définissez les paramètres de confidentialité pour les données d'événements envoyées à des fournisseurs de LLM tiers. Vous pouvez choisir les champs à inclure et ceux à anonymiser en remplaçant leurs valeurs par des chaînes aléatoires. Les valeurs par défaut sont fournies ci-dessous.",
"xpack.elasticAssistant.dataAnonymization.settings.anonymizationSettings.settingsTitle": "Anonymisation",
"xpack.elasticAssistant.dataAnonymizationEditor.contextEditor.selectAllFields": "Sélectionnez l'ensemble des {totalFields} champs",
@ -15350,15 +15307,12 @@
"xpack.elasticAssistant.dataAnonymizationEditor.stats.anonymizedStat.noneOfTheDataWillBeAnonymizedTooltip": "{isDataAnonymizable, select, true {Sélectionnez les champs à remplacer par des valeurs aléatoires. Les réponses sont automatiquement ramenées à leur valeur d'origine.} other {Ce contexte ne peut pas être anonymisé}}",
"xpack.elasticAssistant.dataAnonymizationEditor.stats.availableStat.availableDescription": "Disponible",
"xpack.elasticAssistant.dataAnonymizationEditor.stats.availableStat.availableTooltip": "{total} champs dans ce contexte sont disponibles pour être inclus dans la conversation",
"xpack.elasticAssistant.documentationLinks.ariaLabel": "Cliquez pour ouvrir la documentation de l'assistant d'Elastic dans un nouvel onglet",
"xpack.elasticAssistant.documentationLinks.documentation": "documentation",
"xpack.elasticAssistant.evaluation.evaluationError": "Erreur lors de la réalisation de l'évaluation...",
"xpack.elasticAssistant.evaluation.fetchEvaluationDataError": "Erreur lors de la récupération des données d'évaluation…",
"xpack.elasticAssistant.flyout.right.header.collapseDetailButtonAriaLabel": "Masquer les chats",
"xpack.elasticAssistant.flyout.right.header.expandDetailButtonAriaLabel": "Afficher les chats",
"xpack.elasticAssistant.knowledgeBase.deleteError": "Erreur lors de la suppression de la base de connaissances",
"xpack.elasticAssistant.knowledgeBase.entries.createErrorTitle": "Erreur lors de la création dune entrée de la base de connaissances",
"xpack.elasticAssistant.knowledgeBase.entries.deleteErrorTitle": "Erreur lors de la suppression dentrées de la base de connaissances",
"xpack.elasticAssistant.knowledgeBase.installKnowledgeBaseButton": "Installer la base de connaissances",
"xpack.elasticAssistant.knowledgeBase.setupError": "Erreur lors de la configuration de la base de connaissances",
"xpack.elasticAssistant.knowledgeBase.statusError": "Erreur lors de la récupération du statut de la base de connaissances",

View file

@ -15070,7 +15070,6 @@
"xpack.ecsDataQualityDashboard.getIndexStats.dateRangeRequiredErrorMessage": "startDateとendDateは必須です",
"xpack.elasticAssistant.anonymizationFields.bulkActionsAnonymizationFieldsError": "匿名化フィールドの更新エラー{error}",
"xpack.elasticAssistant.assistant.apiErrorTitle": "メッセージの送信中にエラーが発生しました。",
"xpack.elasticAssistant.assistant.clearChat": "チャットを消去",
"xpack.elasticAssistant.assistant.connectors.addConnectorButton.description": "会話を続行するようにコネクターを構成",
"xpack.elasticAssistant.assistant.connectors.addConnectorButton.missingPrivilegesDescription": "生成AIコネクターを有効にするには、管理者に連絡してください。",
"xpack.elasticAssistant.assistant.connectors.addConnectorButton.missingPrivilegesTitle": "生成AIコネクターが必要です",
@ -15082,20 +15081,8 @@
"xpack.elasticAssistant.assistant.connectors.connectorSelector.ariaLabel": "コネクターセレクター",
"xpack.elasticAssistant.assistant.connectors.connectorSelector.newConnectorOptions": "新しいコネクターを追加...",
"xpack.elasticAssistant.assistant.connectors.connectorSelectorInline.connectorPlaceholder": "コネクターを選択",
"xpack.elasticAssistant.assistant.connectors.createConnectorButton": "コネクター",
"xpack.elasticAssistant.assistant.connectors.preconfiguredTitle": "構成済み",
"xpack.elasticAssistant.assistant.connectors.refreshConnectorsButton": "更新",
"xpack.elasticAssistant.assistant.connectors.setup.skipTitle": "クリックしてスキップ....",
"xpack.elasticAssistant.assistant.connectors.setup.timestampAtTitle": "に",
"xpack.elasticAssistant.assistant.connectors.setup.userAssistantTitle": "アシスタント",
"xpack.elasticAssistant.assistant.connectors.setup.userYouTitle": "あなた",
"xpack.elasticAssistant.assistant.content.prompts.system.defaultSystemPromptLabel": "デフォルト",
"xpack.elasticAssistant.assistant.content.prompts.system.defaultSystemPromptName": "デフォルトシステムプロンプト",
"xpack.elasticAssistant.assistant.content.prompts.system.ifYouDontKnowTheAnswer": "Elasticセキュリティに関連していない質問には回答しないでください。",
"xpack.elasticAssistant.assistant.content.prompts.system.superheroPersonality": "サイバーセキュリティの専門家に情報を伝えるつもりで、できるだけ詳細で関連性のある回答を入力してください。",
"xpack.elasticAssistant.assistant.content.prompts.system.superheroSystemPromptLabel": "拡張",
"xpack.elasticAssistant.assistant.content.prompts.system.superheroSystemPromptName": "拡張システムプロンプト",
"xpack.elasticAssistant.assistant.content.prompts.system.youAreAHelpfulExpertAssistant": "あなたはElasticセキュリティに関する質問に答える、親切で専門的なアシスタントです。",
"xpack.elasticAssistant.assistant.conversations.settings.connectorTitle": "コネクター",
"xpack.elasticAssistant.assistant.conversations.settings.promptHelpTextTitle": "すべての会話の一部として提供されたコンテキスト。",
"xpack.elasticAssistant.assistant.conversations.settings.promptTitle": "システムプロンプト",
@ -15113,7 +15100,6 @@
"xpack.elasticAssistant.assistant.conversationSelector.nextConversationTitle": "次の会話",
"xpack.elasticAssistant.assistant.conversationSelector.placeholderTitle": "選択するか、入力して新規作成...",
"xpack.elasticAssistant.assistant.conversationSelector.previousConversationTitle": "前の会話",
"xpack.elasticAssistant.assistant.conversationSettings.column.actions": "アクション",
"xpack.elasticAssistant.assistant.conversationSettings.column.connector": "コネクター",
"xpack.elasticAssistant.assistant.conversationSettings.column.systemPrompt": "システムプロンプト",
"xpack.elasticAssistant.assistant.conversationSettings.column.Title": "タイトル",
@ -15126,21 +15112,17 @@
"xpack.elasticAssistant.assistant.conversationSettings.title": "設定",
"xpack.elasticAssistant.assistant.dataAnonymizationEditor.contextEditor.allActionsTooltip": "すべてのアクション",
"xpack.elasticAssistant.assistant.dataAnonymizationEditor.contextEditor.allowAction": "許可",
"xpack.elasticAssistant.assistant.dataAnonymizationEditor.contextEditor.allowByDefaultAction": "デフォルトで許可",
"xpack.elasticAssistant.assistant.dataAnonymizationEditor.contextEditor.allowedColumnTitle": "許可",
"xpack.elasticAssistant.assistant.dataAnonymizationEditor.contextEditor.alwaysSubmenu": "常に実行",
"xpack.elasticAssistant.assistant.dataAnonymizationEditor.contextEditor.anonymizeAction": "匿名化",
"xpack.elasticAssistant.assistant.dataAnonymizationEditor.contextEditor.anonymizeByDefaultAction": "デフォルトで匿名化",
"xpack.elasticAssistant.assistant.dataAnonymizationEditor.contextEditor.anonymizedColumnTitle": "匿名化",
"xpack.elasticAssistant.assistant.dataAnonymizationEditor.contextEditor.bulkActions": "一斉アクション",
"xpack.elasticAssistant.assistant.dataAnonymizationEditor.contextEditor.defaultsSubmenu": "デフォルト",
"xpack.elasticAssistant.assistant.dataAnonymizationEditor.contextEditor.denyAction": "拒否",
"xpack.elasticAssistant.assistant.dataAnonymizationEditor.contextEditor.denyByDefaultAction": "デフォルトで拒否",
"xpack.elasticAssistant.assistant.dataAnonymizationEditor.contextEditor.fieldColumnTitle": "フィールド",
"xpack.elasticAssistant.assistant.dataAnonymizationEditor.contextEditor.noButtonLabel": "いいえ",
"xpack.elasticAssistant.assistant.dataAnonymizationEditor.contextEditor.resetButton": "リセット",
"xpack.elasticAssistant.assistant.dataAnonymizationEditor.contextEditor.unanonymizeAction": "非匿名化",
"xpack.elasticAssistant.assistant.dataAnonymizationEditor.contextEditor.unanonymizeByDefaultAction": "デフォルトで非匿名化",
"xpack.elasticAssistant.assistant.dataAnonymizationEditor.contextEditor.valuesColumnTitle": "値",
"xpack.elasticAssistant.assistant.dataAnonymizationEditor.contextEditor.yesButtonLabel": "はい",
"xpack.elasticAssistant.assistant.defaultAssistantTitle": "Elastic AI Assistant",
@ -15151,13 +15133,9 @@
"xpack.elasticAssistant.assistant.emptyScreen.description": "次のシステムプロンプトを使用して、「このアラートを要約してください」から「クエリの作成を手伝ってください」まで、何でも依頼してください。",
"xpack.elasticAssistant.assistant.emptyScreen.title": "お手伝いできることはありますか?",
"xpack.elasticAssistant.assistant.firstPromptEditor.addNewSystemPrompt": "新しいシステムプロンプトを追加...",
"xpack.elasticAssistant.assistant.firstPromptEditor.addSystemPromptTooltip": "システムプロンプトを追加",
"xpack.elasticAssistant.assistant.firstPromptEditor.clearSystemPrompt": "システムプロンプトを消去",
"xpack.elasticAssistant.assistant.firstPromptEditor.commentsListAriaLabel": "コメントのリスト",
"xpack.elasticAssistant.assistant.firstPromptEditor.editingPromptLabel": "プロンプトを編集中",
"xpack.elasticAssistant.assistant.firstPromptEditor.emptyPrompt": "(空のプロンプト)",
"xpack.elasticAssistant.assistant.firstPromptEditor.selectASystemPromptPlaceholder": "システムプロンプトを選択",
"xpack.elasticAssistant.assistant.firstPromptEditor.youLabel": "あなた",
"xpack.elasticAssistant.assistant.newChat.newChatButton": "チャット",
"xpack.elasticAssistant.assistant.newChatByTitle.newChatByTitleButton": "チャット",
"xpack.elasticAssistant.assistant.overlay.CancelButton": "キャンセル",
@ -15169,7 +15147,6 @@
"xpack.elasticAssistant.assistant.promptEditor.systemPrompt.settings.defaultConversationsHelpText": "デフォルトでこのシステムプロンプトを使用する会話。",
"xpack.elasticAssistant.assistant.promptEditor.systemPrompt.settings.defaultConversationsLabel": "デフォルトの会話",
"xpack.elasticAssistant.assistant.promptEditor.systemPrompt.settings.defaultNewConversationTitle": "すべての新しい会話でデフォルトとして使用",
"xpack.elasticAssistant.assistant.promptEditor.systemPrompt.settings.modalTitle": "システムプロンプト",
"xpack.elasticAssistant.assistant.promptEditor.systemPrompt.settings.nameLabel": "名前",
"xpack.elasticAssistant.assistant.promptEditor.systemPrompt.settings.promptLabel": "プロンプト",
"xpack.elasticAssistant.assistant.promptEditor.systemPrompt.settings.promptPlaceholder": "システムプロンプトを入力",
@ -15183,7 +15160,6 @@
"xpack.elasticAssistant.assistant.promptEditor.systemPrompt.systemPromptModal.systemPromptSelector.deletePromptTitle": "システムプロンプトを削除",
"xpack.elasticAssistant.assistant.promptPlaceholder": "「このアラートを要約してください」から「クエリーの作成を手伝ってください」まで、何でも依頼してください。",
"xpack.elasticAssistant.assistant.promptsTable.settingsDescription": "システムプロンプトを作成して管理できます。システムプロンプトは、会話の一部に対して常に送信される、構成可能なコンテキストのチャンクです。",
"xpack.elasticAssistant.assistant.promptsTable.systemPromptsTableColumnActions": "アクション",
"xpack.elasticAssistant.assistant.promptsTable.systemPromptsTableColumnDateUpdated": "更新日",
"xpack.elasticAssistant.assistant.promptsTable.systemPromptsTableColumnDefaultConversations": "デフォルトの会話",
"xpack.elasticAssistant.assistant.promptsTable.systemPromptsTableColumnName": "名前",
@ -15201,7 +15177,6 @@
"xpack.elasticAssistant.assistant.quickPrompts.settings.badgeColorLabel": "バッジの色",
"xpack.elasticAssistant.assistant.quickPrompts.settings.contextsHelpText": "このクイックプロンプトが表示される場所を選択します。ノードを選択すると、このプロンプトがすべての場所に表示されます。",
"xpack.elasticAssistant.assistant.quickPrompts.settings.contextsLabel": "コンテキスト",
"xpack.elasticAssistant.assistant.quickPrompts.settings.modalTitle": "クイックプロンプト",
"xpack.elasticAssistant.assistant.quickPrompts.settings.nameLabel": "名前",
"xpack.elasticAssistant.assistant.quickPrompts.settings.promptLabel": "プロンプト",
"xpack.elasticAssistant.assistant.quickPrompts.settings.promptPlaceholder": "クイックプロンプトを入力",
@ -15212,7 +15187,6 @@
"xpack.elasticAssistant.assistant.quickPromptsTable.modal.deleteQuickPromptConfirmationDefaultTitle": "クイックプロンプトを削除しますか?",
"xpack.elasticAssistant.assistant.quickPromptsTable.modal.deleteQuickPromptConfirmationMessage": "一度削除したプロンプトは回復できません",
"xpack.elasticAssistant.assistant.quickPromptsTable.modal.deleteQuickPromptConfirmationTitle": "\"{prompt}\"を削除しますか?",
"xpack.elasticAssistant.assistant.quickPromptsTable.quickPromptsTableColumnActions": "アクション",
"xpack.elasticAssistant.assistant.quickPromptsTable.quickPromptsTableColumnContexts": "コンテキスト",
"xpack.elasticAssistant.assistant.quickPromptsTable.quickPromptsTableColumnDateUpdated": "更新日",
"xpack.elasticAssistant.assistant.quickPromptsTable.quickPromptsTableColumnName": "名前",
@ -15220,9 +15194,7 @@
"xpack.elasticAssistant.assistant.resetConversationModal.clearChatConfirmation": "現在のチャットを消去しますか?すべての会話データは失われます。",
"xpack.elasticAssistant.assistant.resetConversationModal.resetButtonText": "リセット",
"xpack.elasticAssistant.assistant.settings.actionsButtonTitle": "アクション",
"xpack.elasticAssistant.assistant.settings.anonymizedValues": "匿名化された値",
"xpack.elasticAssistant.assistant.settings.connectorHelpTextTitle": "この会話タイプのデフォルトLLMコネクター。",
"xpack.elasticAssistant.assistant.settings.connectorTitle": "コネクター",
"xpack.elasticAssistant.assistant.settings.deleteButtonTitle": "削除",
"xpack.elasticAssistant.assistant.settings.editButtonTitle": "編集",
"xpack.elasticAssistant.assistant.settings.evaluationSettings.apmUrlDescription": "Kibana APMアプリのURL。評価結果のAPMトレースにリンクするために使用されます。デフォルトは\"{defaultUrlPath}\"です。",
@ -15255,26 +15227,19 @@
"xpack.elasticAssistant.assistant.settings.knowledgeBasedSettings.knowledgeBaseDescription": "{machineLearning}内でELSERを構成して開始します。{seeDocs}",
"xpack.elasticAssistant.assistant.settings.knowledgeBaseSettings.alertsLabel": "アラート",
"xpack.elasticAssistant.assistant.settings.knowledgeBaseSettings.alertsRangeSliderLabel": "アラート範囲",
"xpack.elasticAssistant.assistant.settings.knowledgeBaseSettings.askQuestionsAboutLabel": "質問をする",
"xpack.elasticAssistant.assistant.settings.knowledgeBaseSettings.deleteKnowledgeBaseButton": "削除",
"xpack.elasticAssistant.assistant.settings.knowledgeBaseSettings.elserLabel": "ELSERが構成されました",
"xpack.elasticAssistant.assistant.settings.knowledgeBaseSettings.elserMachineLearningDescription": "機械学習",
"xpack.elasticAssistant.assistant.settings.knowledgeBaseSettings.elserSeeDocsDescription": "ドキュメントを参照",
"xpack.elasticAssistant.assistant.settings.knowledgeBaseSettings.esqlDescription": "ES|SQLクエリーを生成するためのナレッジベースドキュメント",
"xpack.elasticAssistant.assistant.settings.knowledgeBaseSettings.esqlInstalledDescription": "ES|QLナレッジベースドキュメントが読み込まれました",
"xpack.elasticAssistant.assistant.settings.knowledgeBaseSettings.esqlLabel": "ES|QLナレッジベースドキュメント",
"xpack.elasticAssistant.assistant.settings.knowledgeBaseSettings.initializeKnowledgeBaseButton": "初期化",
"xpack.elasticAssistant.assistant.settings.knowledgeBaseSettings.knowledgeBaseDescription": "ナレッジベースドキュメントが保存されているインデックス",
"xpack.elasticAssistant.assistant.settings.knowledgeBaseSettings.knowledgeBaseInstalledDescription": "`{kbIndexPattern}`に初期化されました",
"xpack.elasticAssistant.assistant.settings.knowledgeBaseSettings.knowledgeBaseLabel": "ナレッジベース",
"xpack.elasticAssistant.assistant.settings.knowledgeBaseSettings.knowledgeBaseTooltip": "ナレッジベースを有効にするには、ELSERを構成する必要があります",
"xpack.elasticAssistant.assistant.settings.knowledgeBaseSettings.latestAndRiskiestOpenAlertsLabel": "{alertsCount}件の最新の最もリスクが高い未解決または確認済みのアラートに関する情報をAI Assistantに送信します。",
"xpack.elasticAssistant.assistant.settings.knowledgeBaseSettings.selectFewerAlertsLabel": "モデルのコンテキストウィンドウが小さすぎるため、少ないアラートが送信されます。",
"xpack.elasticAssistant.assistant.settings.knowledgeBaseSettings.settingsBadgeTitle": "実験的",
"xpack.elasticAssistant.assistant.settings.knowledgeBaseSettings.settingsDescription": "ドキュメンテーション",
"xpack.elasticAssistant.assistant.settings.knowledgeBaseSettings.settingsTitle": "ナレッジベース",
"xpack.elasticAssistant.assistant.settings.knowledgeBaseSettings.yourAnonymizationSettingsLabel": "匿名化設定がこれらのアラートに適用されます。",
"xpack.elasticAssistant.assistant.settings.modalTitle": "システムプロンプト",
"xpack.elasticAssistant.assistant.settings.resetConversation": "会話をリセット",
"xpack.elasticAssistant.assistant.settings.securityAiSettingsTitle": "セキュリティAI設定",
"xpack.elasticAssistant.assistant.settings.settingsAnonymizationMenuItemTitle": "匿名化",
@ -15289,11 +15254,9 @@
"xpack.elasticAssistant.assistant.settings.settingsUpdatedToastTitle": "設定が更新されました",
"xpack.elasticAssistant.assistant.settings.showAnonymizedToggleLabel": "匿名化して表示",
"xpack.elasticAssistant.assistant.settings.showAnonymizedToggleRealValuesLabel": "実際の値を表示",
"xpack.elasticAssistant.assistant.settings.showAnonymizedTooltip": "アシスタントの間で送信された匿名化された値を表示",
"xpack.elasticAssistant.assistant.sidePanel.deleteConversationAriaLabel": "会話を削除",
"xpack.elasticAssistant.assistant.submitMessage": "メッセージを送信",
"xpack.elasticAssistant.assistant.useConversation.defaultConversationTitle": "デフォルト",
"xpack.elasticAssistant.assistant.useConversation.elasticAiAssistantName": "アシスタント",
"xpack.elasticAssistant.assistant.useConversation.elasticAiAssistantTitle": "Elastic AI Assistant",
"xpack.elasticAssistant.assistant.useConversation.welcomeConversationTitle": "ようこそ",
"xpack.elasticAssistant.assistant.welcomeScreen.description": "まず最初に、このチャットエクスペリエンスを開始するために生成AIコネクターを設定する必要があります。",
@ -15319,12 +15282,6 @@
"xpack.elasticAssistant.conversations.getConversationError": "id {id}による会話の取得エラー",
"xpack.elasticAssistant.conversations.getUserConversationsError": "会話の取得エラー",
"xpack.elasticAssistant.conversations.updateConversationError": "id {conversationId}による会話の更新エラー",
"xpack.elasticAssistant.conversationSidepanel.titleField.titleIsRequired": "タイトルは必須です",
"xpack.elasticAssistant.conversationSidepanel.titleField.uniqueTitle": "タイトルは一意でなければなりません",
"xpack.elasticAssistant.conversationSidepanel.titleFieldLabel": "タイトル",
"xpack.elasticAssistant.dataAnonymization.settings.anonymizationSettings.calloutParagraph1": "このフィールドはデフォルトで許可されています",
"xpack.elasticAssistant.dataAnonymization.settings.anonymizationSettings.calloutParagraph2": "任意で、これらのフィールドの匿名化を有効にします",
"xpack.elasticAssistant.dataAnonymization.settings.anonymizationSettings.calloutTitle": "匿名化デフォルト",
"xpack.elasticAssistant.dataAnonymization.settings.anonymizationSettings.settingsDescription": "サードパーティLLMプロバイダーに送信されたイベントデータのプライバシー設定を定義します。含めるフィールド、および値をランダム文字列で置換することで匿名化するフィールドを選択できます。参考になるデフォルト値を以下に示します。",
"xpack.elasticAssistant.dataAnonymization.settings.anonymizationSettings.settingsTitle": "匿名化",
"xpack.elasticAssistant.dataAnonymizationEditor.contextEditor.selectAllFields": "すべての{totalFields}フィールドを選択",
@ -15336,15 +15293,12 @@
"xpack.elasticAssistant.dataAnonymizationEditor.stats.anonymizedStat.noneOfTheDataWillBeAnonymizedTooltip": "{isDataAnonymizable, select, true {ランダムな値に置き換えるフィールドを選択します。応答は自動的に元の値に変換されます。} other {このコンテキストは匿名化できません}}",
"xpack.elasticAssistant.dataAnonymizationEditor.stats.availableStat.availableDescription": "利用可能",
"xpack.elasticAssistant.dataAnonymizationEditor.stats.availableStat.availableTooltip": "このコンテキストの{total}個のフィールドを会話に含めることができます",
"xpack.elasticAssistant.documentationLinks.ariaLabel": "クリックすると、新しいタブでElastic Assistantドキュメントを開きます",
"xpack.elasticAssistant.documentationLinks.documentation": "ドキュメンテーション",
"xpack.elasticAssistant.evaluation.evaluationError": "評価の実行エラー...",
"xpack.elasticAssistant.evaluation.fetchEvaluationDataError": "評価データの取得エラー...",
"xpack.elasticAssistant.flyout.right.header.collapseDetailButtonAriaLabel": "チャットを非表示",
"xpack.elasticAssistant.flyout.right.header.expandDetailButtonAriaLabel": "チャットを表示",
"xpack.elasticAssistant.knowledgeBase.deleteError": "ナレッジベースの削除エラー",
"xpack.elasticAssistant.knowledgeBase.entries.createErrorTitle": "ナレッジベースエントリの作成エラー",
"xpack.elasticAssistant.knowledgeBase.entries.deleteErrorTitle": "ナレッジベースエントリの削除エラー",
"xpack.elasticAssistant.knowledgeBase.installKnowledgeBaseButton": "ナレッジベースをインストール",
"xpack.elasticAssistant.knowledgeBase.setupError": "ナレッジベースの設定エラー",
"xpack.elasticAssistant.knowledgeBase.statusError": "ナレッジベースステータスの取得エラー",

View file

@ -15095,7 +15095,6 @@
"xpack.ecsDataQualityDashboard.getIndexStats.dateRangeRequiredErrorMessage": "“开始日期”和“结束日期”必填",
"xpack.elasticAssistant.anonymizationFields.bulkActionsAnonymizationFieldsError": "更新匿名处理字段时出错 {error}",
"xpack.elasticAssistant.assistant.apiErrorTitle": "发送消息时出错。",
"xpack.elasticAssistant.assistant.clearChat": "清除聊天",
"xpack.elasticAssistant.assistant.connectors.addConnectorButton.description": "配置连接器以继续对话",
"xpack.elasticAssistant.assistant.connectors.addConnectorButton.missingPrivilegesDescription": "请联系管理员启用生成式 AI 连接器。",
"xpack.elasticAssistant.assistant.connectors.addConnectorButton.missingPrivilegesTitle": "需要生成式 AI 连接器",
@ -15107,20 +15106,8 @@
"xpack.elasticAssistant.assistant.connectors.connectorSelector.ariaLabel": "连接器选择器",
"xpack.elasticAssistant.assistant.connectors.connectorSelector.newConnectorOptions": "添加新连接器……",
"xpack.elasticAssistant.assistant.connectors.connectorSelectorInline.connectorPlaceholder": "选择连接器",
"xpack.elasticAssistant.assistant.connectors.createConnectorButton": "连接器",
"xpack.elasticAssistant.assistant.connectors.preconfiguredTitle": "预配置",
"xpack.elasticAssistant.assistant.connectors.refreshConnectorsButton": "刷新",
"xpack.elasticAssistant.assistant.connectors.setup.skipTitle": "单击以跳过……",
"xpack.elasticAssistant.assistant.connectors.setup.timestampAtTitle": "处于",
"xpack.elasticAssistant.assistant.connectors.setup.userAssistantTitle": "助手",
"xpack.elasticAssistant.assistant.connectors.setup.userYouTitle": "您",
"xpack.elasticAssistant.assistant.content.prompts.system.defaultSystemPromptLabel": "默认",
"xpack.elasticAssistant.assistant.content.prompts.system.defaultSystemPromptName": "默认系统提示",
"xpack.elasticAssistant.assistant.content.prompts.system.ifYouDontKnowTheAnswer": "不回答与 Elastic Security 无关的问题。",
"xpack.elasticAssistant.assistant.content.prompts.system.superheroPersonality": "提供可能的最详细、最相关的答案,就好像您正将此信息转发给网络安全专家一样。",
"xpack.elasticAssistant.assistant.content.prompts.system.superheroSystemPromptLabel": "已增强",
"xpack.elasticAssistant.assistant.content.prompts.system.superheroSystemPromptName": "已增强系统提示",
"xpack.elasticAssistant.assistant.content.prompts.system.youAreAHelpfulExpertAssistant": "您是一位可帮助回答 Elastic Security 相关问题的专家助手。",
"xpack.elasticAssistant.assistant.conversations.settings.connectorTitle": "连接器",
"xpack.elasticAssistant.assistant.conversations.settings.promptHelpTextTitle": "已作为每个对话的一部分提供上下文。",
"xpack.elasticAssistant.assistant.conversations.settings.promptTitle": "系统提示",
@ -15138,7 +15125,6 @@
"xpack.elasticAssistant.assistant.conversationSelector.nextConversationTitle": "下一个对话",
"xpack.elasticAssistant.assistant.conversationSelector.placeholderTitle": "选择或键入以新建……",
"xpack.elasticAssistant.assistant.conversationSelector.previousConversationTitle": "上一个对话",
"xpack.elasticAssistant.assistant.conversationSettings.column.actions": "操作",
"xpack.elasticAssistant.assistant.conversationSettings.column.connector": "连接器",
"xpack.elasticAssistant.assistant.conversationSettings.column.systemPrompt": "系统提示",
"xpack.elasticAssistant.assistant.conversationSettings.column.Title": "标题",
@ -15151,21 +15137,17 @@
"xpack.elasticAssistant.assistant.conversationSettings.title": "设置",
"xpack.elasticAssistant.assistant.dataAnonymizationEditor.contextEditor.allActionsTooltip": "所有操作",
"xpack.elasticAssistant.assistant.dataAnonymizationEditor.contextEditor.allowAction": "允许",
"xpack.elasticAssistant.assistant.dataAnonymizationEditor.contextEditor.allowByDefaultAction": "默认允许",
"xpack.elasticAssistant.assistant.dataAnonymizationEditor.contextEditor.allowedColumnTitle": "已允许",
"xpack.elasticAssistant.assistant.dataAnonymizationEditor.contextEditor.alwaysSubmenu": "始终",
"xpack.elasticAssistant.assistant.dataAnonymizationEditor.contextEditor.anonymizeAction": "匿名处理",
"xpack.elasticAssistant.assistant.dataAnonymizationEditor.contextEditor.anonymizeByDefaultAction": "默认匿名处理",
"xpack.elasticAssistant.assistant.dataAnonymizationEditor.contextEditor.anonymizedColumnTitle": "已匿名处理",
"xpack.elasticAssistant.assistant.dataAnonymizationEditor.contextEditor.bulkActions": "批处理操作",
"xpack.elasticAssistant.assistant.dataAnonymizationEditor.contextEditor.defaultsSubmenu": "默认值",
"xpack.elasticAssistant.assistant.dataAnonymizationEditor.contextEditor.denyAction": "拒绝",
"xpack.elasticAssistant.assistant.dataAnonymizationEditor.contextEditor.denyByDefaultAction": "默认拒绝",
"xpack.elasticAssistant.assistant.dataAnonymizationEditor.contextEditor.fieldColumnTitle": "字段",
"xpack.elasticAssistant.assistant.dataAnonymizationEditor.contextEditor.noButtonLabel": "否",
"xpack.elasticAssistant.assistant.dataAnonymizationEditor.contextEditor.resetButton": "重置",
"xpack.elasticAssistant.assistant.dataAnonymizationEditor.contextEditor.unanonymizeAction": "取消匿名处理",
"xpack.elasticAssistant.assistant.dataAnonymizationEditor.contextEditor.unanonymizeByDefaultAction": "默认取消匿名处理",
"xpack.elasticAssistant.assistant.dataAnonymizationEditor.contextEditor.valuesColumnTitle": "值",
"xpack.elasticAssistant.assistant.dataAnonymizationEditor.contextEditor.yesButtonLabel": "是",
"xpack.elasticAssistant.assistant.defaultAssistantTitle": "Elastic AI 助手",
@ -15176,13 +15158,9 @@
"xpack.elasticAssistant.assistant.emptyScreen.description": "使用以下系统提示向我提出任何要求,从“汇总此告警”到“帮助我构建查询”:",
"xpack.elasticAssistant.assistant.emptyScreen.title": "我如何帮助您?",
"xpack.elasticAssistant.assistant.firstPromptEditor.addNewSystemPrompt": "添加新系统提示……",
"xpack.elasticAssistant.assistant.firstPromptEditor.addSystemPromptTooltip": "添加系统提示",
"xpack.elasticAssistant.assistant.firstPromptEditor.clearSystemPrompt": "清除系统提示",
"xpack.elasticAssistant.assistant.firstPromptEditor.commentsListAriaLabel": "注释列表",
"xpack.elasticAssistant.assistant.firstPromptEditor.editingPromptLabel": "正在编辑提示",
"xpack.elasticAssistant.assistant.firstPromptEditor.emptyPrompt": "(空提示)",
"xpack.elasticAssistant.assistant.firstPromptEditor.selectASystemPromptPlaceholder": "选择系统提示",
"xpack.elasticAssistant.assistant.firstPromptEditor.youLabel": "您",
"xpack.elasticAssistant.assistant.newChat.newChatButton": "聊天",
"xpack.elasticAssistant.assistant.newChatByTitle.newChatByTitleButton": "聊天",
"xpack.elasticAssistant.assistant.overlay.CancelButton": "取消",
@ -15194,7 +15172,6 @@
"xpack.elasticAssistant.assistant.promptEditor.systemPrompt.settings.defaultConversationsHelpText": "应默认使用此系统提示的对话。",
"xpack.elasticAssistant.assistant.promptEditor.systemPrompt.settings.defaultConversationsLabel": "默认对话",
"xpack.elasticAssistant.assistant.promptEditor.systemPrompt.settings.defaultNewConversationTitle": "用作所有新对话的默认设置",
"xpack.elasticAssistant.assistant.promptEditor.systemPrompt.settings.modalTitle": "系统提示",
"xpack.elasticAssistant.assistant.promptEditor.systemPrompt.settings.nameLabel": "名称",
"xpack.elasticAssistant.assistant.promptEditor.systemPrompt.settings.promptLabel": "提示",
"xpack.elasticAssistant.assistant.promptEditor.systemPrompt.settings.promptPlaceholder": "输入系统提示",
@ -15208,7 +15185,6 @@
"xpack.elasticAssistant.assistant.promptEditor.systemPrompt.systemPromptModal.systemPromptSelector.deletePromptTitle": "删除系统提示",
"xpack.elasticAssistant.assistant.promptPlaceholder": "向我提出任何要求,从“汇总此告警”到“帮助我构建查询……”",
"xpack.elasticAssistant.assistant.promptsTable.settingsDescription": "创建和管理系统提示。系统提示是始终作为对话的一部分发送的可配置上下文块。",
"xpack.elasticAssistant.assistant.promptsTable.systemPromptsTableColumnActions": "操作",
"xpack.elasticAssistant.assistant.promptsTable.systemPromptsTableColumnDateUpdated": "更新日期",
"xpack.elasticAssistant.assistant.promptsTable.systemPromptsTableColumnDefaultConversations": "默认对话",
"xpack.elasticAssistant.assistant.promptsTable.systemPromptsTableColumnName": "名称",
@ -15226,7 +15202,6 @@
"xpack.elasticAssistant.assistant.quickPrompts.settings.badgeColorLabel": "徽章颜色",
"xpack.elasticAssistant.assistant.quickPrompts.settings.contextsHelpText": "选择将在什么地方显示此快速提示。选择无将随处显示此提示。",
"xpack.elasticAssistant.assistant.quickPrompts.settings.contextsLabel": "上下文",
"xpack.elasticAssistant.assistant.quickPrompts.settings.modalTitle": "快速提示",
"xpack.elasticAssistant.assistant.quickPrompts.settings.nameLabel": "名称",
"xpack.elasticAssistant.assistant.quickPrompts.settings.promptLabel": "提示",
"xpack.elasticAssistant.assistant.quickPrompts.settings.promptPlaceholder": "输入快速提示",
@ -15237,7 +15212,6 @@
"xpack.elasticAssistant.assistant.quickPromptsTable.modal.deleteQuickPromptConfirmationDefaultTitle": "删除快速提示?",
"xpack.elasticAssistant.assistant.quickPromptsTable.modal.deleteQuickPromptConfirmationMessage": "提示一旦删除,将无法恢复",
"xpack.elasticAssistant.assistant.quickPromptsTable.modal.deleteQuickPromptConfirmationTitle": "删除“{prompt}”?",
"xpack.elasticAssistant.assistant.quickPromptsTable.quickPromptsTableColumnActions": "操作",
"xpack.elasticAssistant.assistant.quickPromptsTable.quickPromptsTableColumnContexts": "上下文",
"xpack.elasticAssistant.assistant.quickPromptsTable.quickPromptsTableColumnDateUpdated": "更新日期",
"xpack.elasticAssistant.assistant.quickPromptsTable.quickPromptsTableColumnName": "名称",
@ -15245,9 +15219,7 @@
"xpack.elasticAssistant.assistant.resetConversationModal.clearChatConfirmation": "是否确定要清除当前聊天?所有对话数据将会丢失。",
"xpack.elasticAssistant.assistant.resetConversationModal.resetButtonText": "重置",
"xpack.elasticAssistant.assistant.settings.actionsButtonTitle": "操作",
"xpack.elasticAssistant.assistant.settings.anonymizedValues": "已匿名处理值",
"xpack.elasticAssistant.assistant.settings.connectorHelpTextTitle": "此对话类型的默认 LLM 连接器。",
"xpack.elasticAssistant.assistant.settings.connectorTitle": "连接器",
"xpack.elasticAssistant.assistant.settings.deleteButtonTitle": "删除",
"xpack.elasticAssistant.assistant.settings.editButtonTitle": "编辑",
"xpack.elasticAssistant.assistant.settings.evaluationSettings.apmUrlDescription": "Kibana APM 应用的 URL。用于链接到 APM 跟踪以获取评估结果。默认为“{defaultUrlPath}”。",
@ -15280,26 +15252,19 @@
"xpack.elasticAssistant.assistant.settings.knowledgeBasedSettings.knowledgeBaseDescription": "在 {machineLearning} 中配置 ELSER 以开始。{seeDocs}",
"xpack.elasticAssistant.assistant.settings.knowledgeBaseSettings.alertsLabel": "告警",
"xpack.elasticAssistant.assistant.settings.knowledgeBaseSettings.alertsRangeSliderLabel": "告警范围",
"xpack.elasticAssistant.assistant.settings.knowledgeBaseSettings.askQuestionsAboutLabel": "提出有关以下项的问题",
"xpack.elasticAssistant.assistant.settings.knowledgeBaseSettings.deleteKnowledgeBaseButton": "删除",
"xpack.elasticAssistant.assistant.settings.knowledgeBaseSettings.elserLabel": "ELSER 已配置",
"xpack.elasticAssistant.assistant.settings.knowledgeBaseSettings.elserMachineLearningDescription": "Machine Learning",
"xpack.elasticAssistant.assistant.settings.knowledgeBaseSettings.elserSeeDocsDescription": "参阅文档",
"xpack.elasticAssistant.assistant.settings.knowledgeBaseSettings.esqlDescription": "用于生成 ES|QL 查询的知识库文档",
"xpack.elasticAssistant.assistant.settings.knowledgeBaseSettings.esqlInstalledDescription": "已加载 ES|QL 知识库文档",
"xpack.elasticAssistant.assistant.settings.knowledgeBaseSettings.esqlLabel": "ES|QL 知识库文档",
"xpack.elasticAssistant.assistant.settings.knowledgeBaseSettings.initializeKnowledgeBaseButton": "初始化",
"xpack.elasticAssistant.assistant.settings.knowledgeBaseSettings.knowledgeBaseDescription": "存储知识库文档的索引",
"xpack.elasticAssistant.assistant.settings.knowledgeBaseSettings.knowledgeBaseInstalledDescription": "已初始化为 `{kbIndexPattern}`",
"xpack.elasticAssistant.assistant.settings.knowledgeBaseSettings.knowledgeBaseLabel": "知识库",
"xpack.elasticAssistant.assistant.settings.knowledgeBaseSettings.knowledgeBaseTooltip": "必须配置 ELSER 才能启用知识库",
"xpack.elasticAssistant.assistant.settings.knowledgeBaseSettings.latestAndRiskiestOpenAlertsLabel": "发送有关 {alertsCount} 个最新和风险最高的未决或已确认告警的 AI 助手信息。",
"xpack.elasticAssistant.assistant.settings.knowledgeBaseSettings.selectFewerAlertsLabel": "如果此模型的上下文窗口太小,则发送更少告警。",
"xpack.elasticAssistant.assistant.settings.knowledgeBaseSettings.settingsBadgeTitle": "实验性",
"xpack.elasticAssistant.assistant.settings.knowledgeBaseSettings.settingsDescription": "文档",
"xpack.elasticAssistant.assistant.settings.knowledgeBaseSettings.settingsTitle": "知识库",
"xpack.elasticAssistant.assistant.settings.knowledgeBaseSettings.yourAnonymizationSettingsLabel": "您的匿名处理设置将应用于这些告警。",
"xpack.elasticAssistant.assistant.settings.modalTitle": "系统提示",
"xpack.elasticAssistant.assistant.settings.resetConversation": "重置对话",
"xpack.elasticAssistant.assistant.settings.securityAiSettingsTitle": "安全性 AI 设置",
"xpack.elasticAssistant.assistant.settings.settingsAnonymizationMenuItemTitle": "匿名处理",
@ -15314,11 +15279,9 @@
"xpack.elasticAssistant.assistant.settings.settingsUpdatedToastTitle": "设置已更新",
"xpack.elasticAssistant.assistant.settings.showAnonymizedToggleLabel": "显示已匿名处理项",
"xpack.elasticAssistant.assistant.settings.showAnonymizedToggleRealValuesLabel": "显示实际值",
"xpack.elasticAssistant.assistant.settings.showAnonymizedTooltip": "显示该助手接收和发送的已匿名处理值",
"xpack.elasticAssistant.assistant.sidePanel.deleteConversationAriaLabel": "删除对话",
"xpack.elasticAssistant.assistant.submitMessage": "提交消息",
"xpack.elasticAssistant.assistant.useConversation.defaultConversationTitle": "默认",
"xpack.elasticAssistant.assistant.useConversation.elasticAiAssistantName": "助手",
"xpack.elasticAssistant.assistant.useConversation.elasticAiAssistantTitle": "Elastic AI 助手",
"xpack.elasticAssistant.assistant.useConversation.welcomeConversationTitle": "欢迎",
"xpack.elasticAssistant.assistant.welcomeScreen.description": "首先,我们需要设置生成式 AI 连接器以继续这种聊天体验!",
@ -15344,12 +15307,6 @@
"xpack.elasticAssistant.conversations.getConversationError": "按 ID {id} 提取对话时出错",
"xpack.elasticAssistant.conversations.getUserConversationsError": "提取对话时出错",
"xpack.elasticAssistant.conversations.updateConversationError": "按 ID {conversationId} 更新对话时出错",
"xpack.elasticAssistant.conversationSidepanel.titleField.titleIsRequired": "“标题”必填",
"xpack.elasticAssistant.conversationSidepanel.titleField.uniqueTitle": "标题必须唯一",
"xpack.elasticAssistant.conversationSidepanel.titleFieldLabel": "标题",
"xpack.elasticAssistant.dataAnonymization.settings.anonymizationSettings.calloutParagraph1": "默认允许使用以下字段",
"xpack.elasticAssistant.dataAnonymization.settings.anonymizationSettings.calloutParagraph2": "(可选)对这些字段启用匿名处理",
"xpack.elasticAssistant.dataAnonymization.settings.anonymizationSettings.calloutTitle": "匿名处理默认设置",
"xpack.elasticAssistant.dataAnonymization.settings.anonymizationSettings.settingsDescription": "为发送到第三方 LLM 提供商的事件数据定义隐私设置。您可以选择要包括哪些字段,并通过将其值替换为随机字符串来选择要匿名处理的字段。下面提供了有用的默认值。",
"xpack.elasticAssistant.dataAnonymization.settings.anonymizationSettings.settingsTitle": "匿名处理",
"xpack.elasticAssistant.dataAnonymizationEditor.contextEditor.selectAllFields": "选择所有 {totalFields} 个字段",
@ -15361,15 +15318,12 @@
"xpack.elasticAssistant.dataAnonymizationEditor.stats.anonymizedStat.noneOfTheDataWillBeAnonymizedTooltip": "{isDataAnonymizable, select, true {选择要用随机值替换的字段。会自动将响应重新转换为原始值。} other {此上下文无法进行匿名处理}}",
"xpack.elasticAssistant.dataAnonymizationEditor.stats.availableStat.availableDescription": "可用",
"xpack.elasticAssistant.dataAnonymizationEditor.stats.availableStat.availableTooltip": "可在对话中包含此上下文中的 {total} 个字段",
"xpack.elasticAssistant.documentationLinks.ariaLabel": "单击以在新选项卡中打开 Elastic 助手文档",
"xpack.elasticAssistant.documentationLinks.documentation": "文档",
"xpack.elasticAssistant.evaluation.evaluationError": "执行评估时出错......",
"xpack.elasticAssistant.evaluation.fetchEvaluationDataError": "提取评估数据时出错......",
"xpack.elasticAssistant.flyout.right.header.collapseDetailButtonAriaLabel": "隐藏聊天",
"xpack.elasticAssistant.flyout.right.header.expandDetailButtonAriaLabel": "显示聊天",
"xpack.elasticAssistant.knowledgeBase.deleteError": "删除知识库时出错",
"xpack.elasticAssistant.knowledgeBase.entries.createErrorTitle": "创建知识库条目时出错",
"xpack.elasticAssistant.knowledgeBase.entries.deleteErrorTitle": "删除知识库条目时出错",
"xpack.elasticAssistant.knowledgeBase.installKnowledgeBaseButton": "安装知识库",
"xpack.elasticAssistant.knowledgeBase.setupError": "设置知识库时出错",
"xpack.elasticAssistant.knowledgeBase.statusError": "提取知识库状态时出错",