mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 09:48:58 -04:00
[Security Solution] [Elastic AI Assistant] Fixes System Prompt not sending as part of the conversation (#161920)
## Summary Resolves System Prompt not sending issues: https://github.com/elastic/kibana/issues/161809 Also resolves: - [X] Not being able to delete really long Conversation, System Prompt, and Quick Prompt names - [X] Fix user/all System Prompts being overridden on refresh - [X] Conversation without default System Prompt not healed if it is initial conversation when Assistant opens (Timeline) - [X] New conversation created from Conversations Settings not getting a connector by default - [X] Current conversation not selected by default when settings gear is clicked (and other assistant instances exist) - [X] Sent to Timeline action sends anonymized values instead of actual plaintext - [X] Clicking Submit does not clear the text area - [X] Remove System Prompt Tooltip - [X] Fixes confusion when System or Quick Prompt is empty by adding a placeholder value - [X] Shows (empty prompt) in System Prompt selector when the Prompt content is empty - [X] Fixes connector error callout flashing on initial load - [X] Shows `(empty prompt)` text within Prompt Editor when prompt content is empty to prevent confusion ### Checklist Delete any items that are not applicable to this PR. - [X] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios
This commit is contained in:
parent
8a56a2bbaa
commit
83a31bfcc0
32 changed files with 830 additions and 331 deletions
|
@ -33,7 +33,7 @@ interface Props {
|
|||
defaultConnectorId?: string;
|
||||
defaultProvider?: OpenAiProviderType;
|
||||
selectedConversationId: string | undefined;
|
||||
setSelectedConversationId: React.Dispatch<React.SetStateAction<string>>;
|
||||
onConversationSelected: (conversationId: string) => void;
|
||||
shouldDisableKeyboardShortcut?: () => boolean;
|
||||
isDisabled?: boolean;
|
||||
}
|
||||
|
@ -59,7 +59,7 @@ export const ConversationSelector: React.FC<Props> = React.memo(
|
|||
selectedConversationId = DEFAULT_CONVERSATION_TITLE,
|
||||
defaultConnectorId,
|
||||
defaultProvider,
|
||||
setSelectedConversationId,
|
||||
onConversationSelected,
|
||||
shouldDisableKeyboardShortcut = () => false,
|
||||
isDisabled = false,
|
||||
}) => {
|
||||
|
@ -109,14 +109,14 @@ export const ConversationSelector: React.FC<Props> = React.memo(
|
|||
};
|
||||
setConversation({ conversation: newConversation });
|
||||
}
|
||||
setSelectedConversationId(searchValue);
|
||||
onConversationSelected(searchValue);
|
||||
},
|
||||
[
|
||||
allSystemPrompts,
|
||||
defaultConnectorId,
|
||||
defaultProvider,
|
||||
setConversation,
|
||||
setSelectedConversationId,
|
||||
onConversationSelected,
|
||||
]
|
||||
);
|
||||
|
||||
|
@ -124,13 +124,13 @@ export const ConversationSelector: React.FC<Props> = React.memo(
|
|||
const onDelete = useCallback(
|
||||
(cId: string) => {
|
||||
if (selectedConversationId === cId) {
|
||||
setSelectedConversationId(getPreviousConversationId(conversationIds, cId));
|
||||
onConversationSelected(getPreviousConversationId(conversationIds, cId));
|
||||
}
|
||||
setTimeout(() => {
|
||||
deleteConversation(cId);
|
||||
}, 0);
|
||||
},
|
||||
[conversationIds, deleteConversation, selectedConversationId, setSelectedConversationId]
|
||||
[conversationIds, deleteConversation, selectedConversationId, onConversationSelected]
|
||||
);
|
||||
|
||||
const onChange = useCallback(
|
||||
|
@ -138,20 +138,20 @@ export const ConversationSelector: React.FC<Props> = React.memo(
|
|||
if (newOptions.length === 0) {
|
||||
setSelectedOptions([]);
|
||||
} else if (conversationOptions.findIndex((o) => o.label === newOptions?.[0].label) !== -1) {
|
||||
setSelectedConversationId(newOptions?.[0].label);
|
||||
onConversationSelected(newOptions?.[0].label);
|
||||
}
|
||||
},
|
||||
[conversationOptions, setSelectedConversationId]
|
||||
[conversationOptions, onConversationSelected]
|
||||
);
|
||||
|
||||
const onLeftArrowClick = useCallback(() => {
|
||||
const prevId = getPreviousConversationId(conversationIds, selectedConversationId);
|
||||
setSelectedConversationId(prevId);
|
||||
}, [conversationIds, selectedConversationId, setSelectedConversationId]);
|
||||
onConversationSelected(prevId);
|
||||
}, [conversationIds, selectedConversationId, onConversationSelected]);
|
||||
const onRightArrowClick = useCallback(() => {
|
||||
const nextId = getNextConversationId(conversationIds, selectedConversationId);
|
||||
setSelectedConversationId(nextId);
|
||||
}, [conversationIds, selectedConversationId, setSelectedConversationId]);
|
||||
onConversationSelected(nextId);
|
||||
}, [conversationIds, selectedConversationId, onConversationSelected]);
|
||||
|
||||
// Register keyboard listener for quick conversation switching
|
||||
const onKeyDown = useCallback(
|
||||
|
@ -200,11 +200,17 @@ export const ConversationSelector: React.FC<Props> = React.memo(
|
|||
return (
|
||||
<EuiFlexGroup
|
||||
alignItems="center"
|
||||
justifyContent="spaceBetween"
|
||||
component={'span'}
|
||||
className={'parentFlexGroup'}
|
||||
component={'span'}
|
||||
justifyContent="spaceBetween"
|
||||
>
|
||||
<EuiFlexItem grow={false} component={'span'} css={css``}>
|
||||
<EuiFlexItem
|
||||
component={'span'}
|
||||
grow={false}
|
||||
css={css`
|
||||
width: calc(100% - 60px);
|
||||
`}
|
||||
>
|
||||
<EuiHighlight
|
||||
search={searchValue}
|
||||
css={css`
|
||||
|
|
|
@ -167,11 +167,17 @@ export const ConversationSelectorSettings: React.FC<Props> = React.memo(
|
|||
return (
|
||||
<EuiFlexGroup
|
||||
alignItems="center"
|
||||
justifyContent="spaceBetween"
|
||||
component={'span'}
|
||||
className={'parentFlexGroup'}
|
||||
component={'span'}
|
||||
justifyContent="spaceBetween"
|
||||
>
|
||||
<EuiFlexItem grow={false} component={'span'} css={css``}>
|
||||
<EuiFlexItem
|
||||
component={'span'}
|
||||
grow={false}
|
||||
css={css`
|
||||
width: calc(100% - 60px);
|
||||
`}
|
||||
>
|
||||
<EuiHighlight
|
||||
search={searchValue}
|
||||
css={css`
|
||||
|
|
|
@ -12,6 +12,7 @@ import { ActionTypeRegistryContract } from '@kbn/triggers-actions-ui-plugin/publ
|
|||
import { HttpSetup } from '@kbn/core-http-browser';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import { OpenAiProviderType } from '@kbn/stack-connectors-plugin/public/common';
|
||||
import { noop } from 'lodash/fp';
|
||||
import { Conversation, Prompt } from '../../../..';
|
||||
import * as i18n from './translations';
|
||||
import * as i18nModel from '../../../connectorland/models/model_selector/translations';
|
||||
|
@ -21,12 +22,14 @@ import { SelectSystemPrompt } from '../../prompt_editor/system_prompt/select_sys
|
|||
import { ModelSelector } from '../../../connectorland/models/model_selector/model_selector';
|
||||
import { UseAssistantContext } from '../../../assistant_context';
|
||||
import { ConversationSelectorSettings } from '../conversation_selector_settings';
|
||||
import { getDefaultSystemPromptFromConversation } from './helpers';
|
||||
import { getDefaultSystemPrompt } from '../../use_conversation/helpers';
|
||||
|
||||
export interface ConversationSettingsProps {
|
||||
actionTypeRegistry: ActionTypeRegistryContract;
|
||||
allSystemPrompts: Prompt[];
|
||||
conversationSettings: UseAssistantContext['conversations'];
|
||||
defaultConnectorId?: string;
|
||||
defaultProvider?: OpenAiProviderType;
|
||||
http: HttpSetup;
|
||||
onSelectedConversationChange: (conversation?: Conversation) => void;
|
||||
selectedConversation: Conversation | undefined;
|
||||
|
@ -43,6 +46,8 @@ export const ConversationSettings: React.FC<ConversationSettingsProps> = React.m
|
|||
({
|
||||
actionTypeRegistry,
|
||||
allSystemPrompts,
|
||||
defaultConnectorId,
|
||||
defaultProvider,
|
||||
selectedConversation,
|
||||
onSelectedConversationChange,
|
||||
conversationSettings,
|
||||
|
@ -50,14 +55,14 @@ export const ConversationSettings: React.FC<ConversationSettingsProps> = React.m
|
|||
setUpdatedConversationSettings,
|
||||
isDisabled = false,
|
||||
}) => {
|
||||
// Defaults
|
||||
const defaultSystemPrompt = useMemo(() => {
|
||||
return (
|
||||
allSystemPrompts.find((systemPrompt) => systemPrompt.isNewConversationDefault) ??
|
||||
allSystemPrompts[0]
|
||||
);
|
||||
return getDefaultSystemPrompt({ allSystemPrompts, conversation: undefined });
|
||||
}, [allSystemPrompts]);
|
||||
|
||||
const selectedSystemPrompt = useMemo(() => {
|
||||
return getDefaultSystemPrompt({ allSystemPrompts, conversation: selectedConversation });
|
||||
}, [allSystemPrompts, selectedConversation]);
|
||||
|
||||
// Conversation callbacks
|
||||
// When top level conversation selection changes
|
||||
const onConversationSelectionChange = useCallback(
|
||||
|
@ -68,8 +73,8 @@ export const ConversationSettings: React.FC<ConversationSettingsProps> = React.m
|
|||
id: c ?? '',
|
||||
messages: [],
|
||||
apiConfig: {
|
||||
connectorId: undefined,
|
||||
provider: undefined,
|
||||
connectorId: defaultConnectorId,
|
||||
provider: defaultProvider,
|
||||
defaultSystemPromptId: defaultSystemPrompt?.id,
|
||||
},
|
||||
}
|
||||
|
@ -86,7 +91,13 @@ export const ConversationSettings: React.FC<ConversationSettingsProps> = React.m
|
|||
|
||||
onSelectedConversationChange(newSelectedConversation);
|
||||
},
|
||||
[defaultSystemPrompt?.id, onSelectedConversationChange, setUpdatedConversationSettings]
|
||||
[
|
||||
defaultConnectorId,
|
||||
defaultProvider,
|
||||
defaultSystemPrompt?.id,
|
||||
onSelectedConversationChange,
|
||||
setUpdatedConversationSettings,
|
||||
]
|
||||
);
|
||||
|
||||
const onConversationDeleted = useCallback(
|
||||
|
@ -102,18 +113,8 @@ export const ConversationSettings: React.FC<ConversationSettingsProps> = React.m
|
|||
[setUpdatedConversationSettings]
|
||||
);
|
||||
|
||||
const selectedSystemPrompt = useMemo(
|
||||
() =>
|
||||
getDefaultSystemPromptFromConversation({
|
||||
allSystemPrompts,
|
||||
conversation: selectedConversation,
|
||||
defaultSystemPrompt,
|
||||
}),
|
||||
[allSystemPrompts, defaultSystemPrompt, selectedConversation]
|
||||
);
|
||||
|
||||
const handleOnSystemPromptSelectionChange = useCallback(
|
||||
(systemPromptId?: string) => {
|
||||
(systemPromptId?: string | undefined) => {
|
||||
if (selectedConversation != null) {
|
||||
setUpdatedConversationSettings((prev) => ({
|
||||
...prev,
|
||||
|
@ -215,6 +216,8 @@ export const ConversationSettings: React.FC<ConversationSettingsProps> = React.m
|
|||
onSystemPromptSelectionChange={handleOnSystemPromptSelectionChange}
|
||||
selectedPrompt={selectedSystemPrompt}
|
||||
showTitles={true}
|
||||
isSettingsModalVisible={true}
|
||||
setIsSettingsModalVisible={noop} // noop, already in settings
|
||||
/>
|
||||
</EuiFormRow>
|
||||
|
||||
|
|
|
@ -1,33 +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 { Conversation, Prompt } from '../../../..';
|
||||
|
||||
/**
|
||||
* Returns a conversation's default system prompt, or the default system prompt if the conversation does not have one.
|
||||
* @param allSystemPrompts
|
||||
* @param conversation
|
||||
* @param defaultSystemPrompt
|
||||
*/
|
||||
export const getDefaultSystemPromptFromConversation = ({
|
||||
allSystemPrompts,
|
||||
conversation,
|
||||
defaultSystemPrompt,
|
||||
}: {
|
||||
conversation: Conversation | undefined;
|
||||
allSystemPrompts: Prompt[];
|
||||
defaultSystemPrompt: Prompt;
|
||||
}) => {
|
||||
const convoDefaultSystemPromptId = conversation?.apiConfig.defaultSystemPromptId;
|
||||
if (convoDefaultSystemPromptId && allSystemPrompts) {
|
||||
return (
|
||||
allSystemPrompts.find((prompt) => prompt.id === convoDefaultSystemPromptId) ??
|
||||
defaultSystemPrompt
|
||||
);
|
||||
}
|
||||
return allSystemPrompts.find((prompt) => prompt.isNewConversationDefault) ?? defaultSystemPrompt;
|
||||
};
|
|
@ -5,8 +5,9 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { getWelcomeConversation } from './helpers';
|
||||
import { getDefaultConnector, getWelcomeConversation } from './helpers';
|
||||
import { enterpriseMessaging } from './use_conversation/sample_conversations';
|
||||
import { ActionConnector } from '@kbn/triggers-actions-ui-plugin/public';
|
||||
|
||||
describe('getWelcomeConversation', () => {
|
||||
describe('isAssistantEnabled = false', () => {
|
||||
|
@ -112,4 +113,81 @@ describe('getWelcomeConversation', () => {
|
|||
expect(result.messages.length).toEqual(4);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getDefaultConnector', () => {
|
||||
it('should return undefined if connectors array is undefined', () => {
|
||||
const connectors = undefined;
|
||||
const result = getDefaultConnector(connectors);
|
||||
|
||||
expect(result).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should return undefined if connectors array is empty', () => {
|
||||
const connectors: Array<ActionConnector<Record<string, unknown>, Record<string, unknown>>> =
|
||||
[];
|
||||
const result = getDefaultConnector(connectors);
|
||||
|
||||
expect(result).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should return the connector id if there is only one connector', () => {
|
||||
const connectors: Array<ActionConnector<Record<string, unknown>, Record<string, unknown>>> = [
|
||||
{
|
||||
actionTypeId: '.gen-ai',
|
||||
isPreconfigured: false,
|
||||
isDeprecated: false,
|
||||
referencedByCount: 0,
|
||||
isMissingSecrets: false,
|
||||
isSystemAction: false,
|
||||
secrets: {},
|
||||
id: 'c5f91dc0-2197-11ee-aded-897192c5d6f5',
|
||||
name: 'OpenAI',
|
||||
config: {
|
||||
apiProvider: 'OpenAI',
|
||||
apiUrl: 'https://api.openai.com/v1/chat/completions',
|
||||
},
|
||||
},
|
||||
];
|
||||
const result = getDefaultConnector(connectors);
|
||||
|
||||
expect(result).toBe(connectors[0]);
|
||||
});
|
||||
|
||||
it('should return undefined if there are multiple connectors', () => {
|
||||
const connectors: Array<ActionConnector<Record<string, unknown>, Record<string, unknown>>> = [
|
||||
{
|
||||
actionTypeId: '.gen-ai',
|
||||
isPreconfigured: false,
|
||||
isDeprecated: false,
|
||||
referencedByCount: 0,
|
||||
isMissingSecrets: false,
|
||||
isSystemAction: false,
|
||||
secrets: {},
|
||||
id: 'c5f91dc0-2197-11ee-aded-897192c5d6f5',
|
||||
name: 'OpenAI',
|
||||
config: {
|
||||
apiProvider: 'OpenAI 1',
|
||||
apiUrl: 'https://api.openai.com/v1/chat/completions',
|
||||
},
|
||||
},
|
||||
{
|
||||
actionTypeId: '.gen-ai',
|
||||
isPreconfigured: false,
|
||||
isDeprecated: false,
|
||||
referencedByCount: 0,
|
||||
isMissingSecrets: false,
|
||||
isSystemAction: false,
|
||||
secrets: {},
|
||||
id: 'c7f91dc0-2197-11ee-aded-897192c5d633',
|
||||
name: 'OpenAI',
|
||||
config: {
|
||||
apiProvider: 'OpenAI 2',
|
||||
apiUrl: 'https://api.openai.com/v1/chat/completions',
|
||||
},
|
||||
},
|
||||
];
|
||||
const result = getDefaultConnector(connectors);
|
||||
expect(result).toBeUndefined();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { ActionConnector } from '@kbn/triggers-actions-ui-plugin/public';
|
||||
import { Conversation } from '../..';
|
||||
import type { Message } from '../assistant_context/types';
|
||||
import { enterpriseMessaging, WELCOME_CONVERSATION } from './use_conversation/sample_conversations';
|
||||
|
@ -49,3 +50,12 @@ export const getWelcomeConversation = (
|
|||
messages: [...conversation.messages, ...WELCOME_CONVERSATION.messages],
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns a default connector if there is only one connector
|
||||
* @param connectors
|
||||
*/
|
||||
export const getDefaultConnector = (
|
||||
connectors: Array<ActionConnector<Record<string, unknown>, Record<string, unknown>>> | undefined
|
||||
): ActionConnector<Record<string, unknown>, Record<string, unknown>> | undefined =>
|
||||
connectors?.length === 1 ? connectors[0] : undefined;
|
||||
|
|
|
@ -29,7 +29,7 @@ import { ActionConnectorProps } from '@kbn/triggers-actions-ui-plugin/public/typ
|
|||
import { WELCOME_CONVERSATION_TITLE } from './use_conversation/translations';
|
||||
import { AssistantTitle } from './assistant_title';
|
||||
import { UpgradeButtons } from '../upgrade/upgrade_buttons';
|
||||
import { getMessageFromRawResponse, getWelcomeConversation } from './helpers';
|
||||
import { getDefaultConnector, getMessageFromRawResponse, getWelcomeConversation } from './helpers';
|
||||
|
||||
import { useAssistantContext } from '../assistant_context';
|
||||
import { ContextPills } from './context_pills';
|
||||
|
@ -37,7 +37,7 @@ import { getNewSelectedPromptContext } from '../data_anonymization/get_new_selec
|
|||
import { PromptTextArea } from './prompt_textarea';
|
||||
import type { PromptContext, SelectedPromptContext } from './prompt_context/types';
|
||||
import { useConversation } from './use_conversation';
|
||||
import { CodeBlockDetails } from './use_conversation/helpers';
|
||||
import { CodeBlockDetails, getDefaultSystemPrompt } from './use_conversation/helpers';
|
||||
import { useSendMessages } from './use_send_messages';
|
||||
import type { Message } from '../assistant_context/types';
|
||||
import { ConversationSelector } from './conversations/conversation_selector';
|
||||
|
@ -103,11 +103,15 @@ const AssistantComponent: React.FC<Props> = ({
|
|||
isSuccess: areConnectorsFetched,
|
||||
refetch: refetchConnectors,
|
||||
} = useLoadConnectors({ http });
|
||||
const defaultConnectorId = useMemo(() => connectors?.[0]?.id, [connectors]);
|
||||
const defaultConnectorId = useMemo(() => getDefaultConnector(connectors)?.id, [connectors]);
|
||||
const defaultProvider = useMemo(
|
||||
() =>
|
||||
(connectors?.[0] as ActionConnectorProps<{ apiProvider: OpenAiProviderType }, unknown>)
|
||||
?.config?.apiProvider,
|
||||
(
|
||||
getDefaultConnector(connectors) as ActionConnectorProps<
|
||||
{ apiProvider: OpenAiProviderType },
|
||||
unknown
|
||||
>
|
||||
)?.config?.apiProvider,
|
||||
[connectors]
|
||||
);
|
||||
|
||||
|
@ -145,6 +149,9 @@ const AssistantComponent: React.FC<Props> = ({
|
|||
[currentConversation, isAssistantEnabled]
|
||||
);
|
||||
|
||||
// Settings modal state (so it isn't shared between assistant instances like Timeline)
|
||||
const [isSettingsModalVisible, setIsSettingsModalVisible] = useState(false);
|
||||
|
||||
// Remember last selection for reuse after keyboard shortcut is pressed.
|
||||
// Clear it if there is no connectors
|
||||
useEffect(() => {
|
||||
|
@ -178,7 +185,7 @@ const AssistantComponent: React.FC<Props> = ({
|
|||
|
||||
const [promptTextPreview, setPromptTextPreview] = useState<string>('');
|
||||
const [autoPopulatedOnce, setAutoPopulatedOnce] = useState<boolean>(false);
|
||||
const [suggestedUserPrompt, setSuggestedUserPrompt] = useState<string | null>(null);
|
||||
const [userPrompt, setUserPrompt] = useState<string | null>(null);
|
||||
|
||||
const [showMissingConnectorCallout, setShowMissingConnectorCallout] = useState<boolean>(false);
|
||||
|
||||
|
@ -234,13 +241,29 @@ const AssistantComponent: React.FC<Props> = ({
|
|||
////
|
||||
//
|
||||
|
||||
const selectedSystemPrompt = useMemo(() => {
|
||||
if (currentConversation.apiConfig.defaultSystemPromptId) {
|
||||
return allSystemPrompts.find(
|
||||
(prompt) => prompt.id === currentConversation.apiConfig.defaultSystemPromptId
|
||||
const selectedSystemPrompt = useMemo(
|
||||
() => getDefaultSystemPrompt({ allSystemPrompts, conversation: currentConversation }),
|
||||
[allSystemPrompts, currentConversation]
|
||||
);
|
||||
|
||||
const [editingSystemPromptId, setEditingSystemPromptId] = useState<string | undefined>(
|
||||
selectedSystemPrompt?.id
|
||||
);
|
||||
|
||||
const handleOnConversationSelected = useCallback(
|
||||
(cId: string) => {
|
||||
setSelectedConversationId(cId);
|
||||
setEditingSystemPromptId(
|
||||
getDefaultSystemPrompt({ allSystemPrompts, conversation: conversations[cId] })?.id
|
||||
);
|
||||
}
|
||||
}, [allSystemPrompts, currentConversation.apiConfig.defaultSystemPromptId]);
|
||||
},
|
||||
[allSystemPrompts, conversations]
|
||||
);
|
||||
|
||||
const handlePromptChange = useCallback((prompt: string) => {
|
||||
setPromptTextPreview(prompt);
|
||||
setUserPrompt(prompt);
|
||||
}, []);
|
||||
|
||||
// Handles sending latest user prompt to API
|
||||
const handleSendMessage = useCallback(
|
||||
|
@ -251,13 +274,15 @@ const AssistantComponent: React.FC<Props> = ({
|
|||
replacements: newReplacements,
|
||||
});
|
||||
|
||||
const systemPrompt = allSystemPrompts.find((prompt) => prompt.id === editingSystemPromptId);
|
||||
|
||||
const message = await getCombinedMessage({
|
||||
isNewChat: currentConversation.messages.length === 0,
|
||||
currentReplacements: currentConversation.replacements,
|
||||
onNewReplacements,
|
||||
promptText,
|
||||
selectedPromptContexts,
|
||||
selectedSystemPrompt,
|
||||
selectedSystemPrompt: systemPrompt,
|
||||
});
|
||||
|
||||
const updatedMessages = appendMessage({
|
||||
|
@ -278,23 +303,42 @@ const AssistantComponent: React.FC<Props> = ({
|
|||
appendMessage({ conversationId: selectedConversationId, message: responseMessage });
|
||||
},
|
||||
[
|
||||
selectedSystemPrompt,
|
||||
appendMessage,
|
||||
appendReplacements,
|
||||
currentConversation.apiConfig,
|
||||
allSystemPrompts,
|
||||
currentConversation.messages.length,
|
||||
currentConversation.replacements,
|
||||
http,
|
||||
selectedConversationId,
|
||||
currentConversation.apiConfig,
|
||||
selectedPromptContexts,
|
||||
appendMessage,
|
||||
selectedConversationId,
|
||||
sendMessages,
|
||||
http,
|
||||
appendReplacements,
|
||||
editingSystemPromptId,
|
||||
]
|
||||
);
|
||||
|
||||
const handleButtonSendMessage = useCallback(() => {
|
||||
handleSendMessage(promptTextAreaRef.current?.value?.trim() ?? '');
|
||||
setUserPrompt('');
|
||||
}, [handleSendMessage, promptTextAreaRef]);
|
||||
|
||||
const handleOnSystemPromptSelectionChange = useCallback((systemPromptId?: string) => {
|
||||
setEditingSystemPromptId(systemPromptId);
|
||||
}, []);
|
||||
|
||||
const handleOnChatCleared = useCallback(() => {
|
||||
const defaultSystemPromptId = getDefaultSystemPrompt({
|
||||
allSystemPrompts,
|
||||
conversation: conversations[selectedConversationId],
|
||||
})?.id;
|
||||
|
||||
setPromptTextPreview('');
|
||||
setUserPrompt('');
|
||||
setSelectedPromptContexts({});
|
||||
clearConversation(selectedConversationId);
|
||||
setEditingSystemPromptId(defaultSystemPromptId);
|
||||
}, [allSystemPrompts, clearConversation, conversations, selectedConversationId]);
|
||||
|
||||
const shouldDisableConversationSelectorHotkeys = useCallback(() => {
|
||||
const promptTextAreaHasFocus = document.activeElement === promptTextAreaRef.current;
|
||||
return promptTextAreaHasFocus;
|
||||
|
@ -347,7 +391,7 @@ const AssistantComponent: React.FC<Props> = ({
|
|||
}
|
||||
|
||||
if (promptContext.suggestedUserPrompt != null) {
|
||||
setSuggestedUserPrompt(promptContext.suggestedUserPrompt);
|
||||
setUserPrompt(promptContext.suggestedUserPrompt);
|
||||
}
|
||||
}
|
||||
}, [
|
||||
|
@ -398,16 +442,21 @@ const AssistantComponent: React.FC<Props> = ({
|
|||
`}
|
||||
/>
|
||||
|
||||
<EuiSpacer size={'m'} />
|
||||
{currentConversation.messages.length !== 0 &&
|
||||
Object.keys(selectedPromptContexts).length > 0 && <EuiSpacer size={'m'} />}
|
||||
|
||||
{(currentConversation.messages.length === 0 ||
|
||||
Object.keys(selectedPromptContexts).length > 0) && (
|
||||
<PromptEditor
|
||||
conversation={currentConversation}
|
||||
editingSystemPromptId={editingSystemPromptId}
|
||||
isNewConversation={currentConversation.messages.length === 0}
|
||||
isSettingsModalVisible={isSettingsModalVisible}
|
||||
promptContexts={promptContexts}
|
||||
promptTextPreview={promptTextPreview}
|
||||
onSystemPromptSelectionChange={handleOnSystemPromptSelectionChange}
|
||||
selectedPromptContexts={selectedPromptContexts}
|
||||
setIsSettingsModalVisible={setIsSettingsModalVisible}
|
||||
setSelectedPromptContexts={setSelectedPromptContexts}
|
||||
/>
|
||||
)}
|
||||
|
@ -417,7 +466,10 @@ const AssistantComponent: React.FC<Props> = ({
|
|||
),
|
||||
[
|
||||
currentConversation,
|
||||
editingSystemPromptId,
|
||||
getComments,
|
||||
handleOnSystemPromptSelectionChange,
|
||||
isSettingsModalVisible,
|
||||
promptContexts,
|
||||
promptTextPreview,
|
||||
selectedPromptContexts,
|
||||
|
@ -474,7 +526,7 @@ const AssistantComponent: React.FC<Props> = ({
|
|||
defaultConnectorId={defaultConnectorId}
|
||||
defaultProvider={defaultProvider}
|
||||
selectedConversationId={selectedConversationId}
|
||||
setSelectedConversationId={setSelectedConversationId}
|
||||
onConversationSelected={handleOnConversationSelected}
|
||||
shouldDisableKeyboardShortcut={shouldDisableConversationSelectorHotkeys}
|
||||
isDisabled={isDisabled}
|
||||
/>
|
||||
|
@ -504,8 +556,12 @@ const AssistantComponent: React.FC<Props> = ({
|
|||
|
||||
<EuiFlexItem grow={false}>
|
||||
<AssistantSettingsButton
|
||||
defaultConnectorId={defaultConnectorId}
|
||||
defaultProvider={defaultProvider}
|
||||
isDisabled={isDisabled}
|
||||
isSettingsModalVisible={isSettingsModalVisible}
|
||||
selectedConversation={currentConversation}
|
||||
setIsSettingsModalVisible={setIsSettingsModalVisible}
|
||||
setSelectedConversationId={setSelectedConversationId}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
|
@ -536,12 +592,15 @@ const AssistantComponent: React.FC<Props> = ({
|
|||
<EuiModalBody>
|
||||
{comments}
|
||||
|
||||
{!isDisabled && showMissingConnectorCallout && (
|
||||
{!isDisabled && showMissingConnectorCallout && areConnectorsFetched && (
|
||||
<>
|
||||
<EuiSpacer />
|
||||
<EuiFlexGroup justifyContent="spaceAround">
|
||||
<EuiFlexItem grow={false}>
|
||||
<ConnectorMissingCallout />
|
||||
<ConnectorMissingCallout
|
||||
isSettingsModalVisible={isSettingsModalVisible}
|
||||
setIsSettingsModalVisible={setIsSettingsModalVisible}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</>
|
||||
|
@ -585,8 +644,8 @@ const AssistantComponent: React.FC<Props> = ({
|
|||
<PromptTextArea
|
||||
onPromptSubmit={handleSendMessage}
|
||||
ref={promptTextAreaRef}
|
||||
handlePromptChange={setPromptTextPreview}
|
||||
value={isSendingDisabled ? '' : suggestedUserPrompt ?? ''}
|
||||
handlePromptChange={handlePromptChange}
|
||||
value={isSendingDisabled ? '' : userPrompt ?? ''}
|
||||
isDisabled={isSendingDisabled}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
|
@ -614,12 +673,7 @@ const AssistantComponent: React.FC<Props> = ({
|
|||
isDisabled={isSendingDisabled}
|
||||
aria-label={i18n.CLEAR_CHAT}
|
||||
color="danger"
|
||||
onClick={() => {
|
||||
setPromptTextPreview('');
|
||||
clearConversation(selectedConversationId);
|
||||
setSelectedPromptContexts({});
|
||||
setSuggestedUserPrompt('');
|
||||
}}
|
||||
onClick={handleOnChatCleared}
|
||||
/>
|
||||
</EuiToolTip>
|
||||
</EuiFlexItem>
|
||||
|
@ -639,7 +693,12 @@ const AssistantComponent: React.FC<Props> = ({
|
|||
</EuiFlexGroup>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
{!isDisabled && <QuickPrompts setInput={setSuggestedUserPrompt} />}
|
||||
{!isDisabled && (
|
||||
<QuickPrompts
|
||||
setInput={setUserPrompt}
|
||||
setIsSettingsModalVisible={setIsSettingsModalVisible}
|
||||
/>
|
||||
)}
|
||||
</EuiModalFooter>
|
||||
</>
|
||||
);
|
||||
|
|
|
@ -29,20 +29,24 @@ const mockSelectedEventPromptContext: SelectedPromptContext = {
|
|||
|
||||
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(),
|
||||
};
|
||||
|
||||
describe('PromptEditorComponent', () => {
|
||||
beforeEach(() => jest.clearAllMocks());
|
||||
|
||||
it('renders the system prompt viewer when isNewConversation is true', async () => {
|
||||
it('renders the system prompt selector when isNewConversation is true', async () => {
|
||||
render(
|
||||
<TestProviders>
|
||||
<PromptEditor {...defaultProps} />
|
||||
|
@ -50,7 +54,7 @@ describe('PromptEditorComponent', () => {
|
|||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('systemPromptText')).toBeInTheDocument();
|
||||
expect(screen.getByTestId('selectSystemPrompt')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -19,10 +19,14 @@ 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>>
|
||||
>;
|
||||
|
@ -34,16 +38,28 @@ const PreviewText = styled(EuiText)`
|
|||
|
||||
const PromptEditorComponent: React.FC<Props> = ({
|
||||
conversation,
|
||||
editingSystemPromptId,
|
||||
isNewConversation,
|
||||
isSettingsModalVisible,
|
||||
promptContexts,
|
||||
promptTextPreview,
|
||||
onSystemPromptSelectionChange,
|
||||
selectedPromptContexts,
|
||||
setIsSettingsModalVisible,
|
||||
setSelectedPromptContexts,
|
||||
}) => {
|
||||
const commentBody = useMemo(
|
||||
() => (
|
||||
<>
|
||||
{isNewConversation && <SystemPrompt conversation={conversation} />}
|
||||
{isNewConversation && (
|
||||
<SystemPrompt
|
||||
conversation={conversation}
|
||||
editingSystemPromptId={editingSystemPromptId}
|
||||
onSystemPromptSelectionChange={onSystemPromptSelectionChange}
|
||||
isSettingsModalVisible={isSettingsModalVisible}
|
||||
setIsSettingsModalVisible={setIsSettingsModalVisible}
|
||||
/>
|
||||
)}
|
||||
|
||||
<SelectedPromptContexts
|
||||
isNewConversation={isNewConversation}
|
||||
|
@ -59,10 +75,14 @@ const PromptEditorComponent: React.FC<Props> = ({
|
|||
),
|
||||
[
|
||||
conversation,
|
||||
editingSystemPromptId,
|
||||
isNewConversation,
|
||||
isSettingsModalVisible,
|
||||
onSystemPromptSelectionChange,
|
||||
promptContexts,
|
||||
promptTextPreview,
|
||||
selectedPromptContexts,
|
||||
setIsSettingsModalVisible,
|
||||
setSelectedPromptContexts,
|
||||
]
|
||||
);
|
||||
|
|
|
@ -12,7 +12,9 @@ import React from 'react';
|
|||
import styled from 'styled-components';
|
||||
|
||||
import { css } from '@emotion/react';
|
||||
import { isEmpty } from 'lodash/fp';
|
||||
import type { Prompt } from '../../types';
|
||||
import { EMPTY_PROMPT } from './translations';
|
||||
|
||||
const Strong = styled.strong`
|
||||
margin-right: ${({ theme }) => theme.eui.euiSizeS};
|
||||
|
@ -44,9 +46,10 @@ export const getOptionFromPrompt = ({
|
|||
<>
|
||||
<Strong data-test-subj="name">{name}</Strong>
|
||||
|
||||
<EuiToolTip content={content}>
|
||||
{/* Empty content tooltip gets around :hover styles from SuperSelectOptionButton */}
|
||||
<EuiToolTip content={undefined}>
|
||||
<EuiText color="subdued" data-test-subj="content" size="s">
|
||||
<p>{content}</p>
|
||||
{isEmpty(content) ? <p>{EMPTY_PROMPT}</p> : <p>{content}</p>}
|
||||
</EuiText>
|
||||
</EuiToolTip>
|
||||
</>
|
||||
|
|
|
@ -60,6 +60,11 @@ jest.mock('../../use_conversation', () => {
|
|||
});
|
||||
|
||||
describe('SystemPrompt', () => {
|
||||
const editingSystemPromptId = undefined;
|
||||
const isSettingsModalVisible = false;
|
||||
const onSystemPromptSelectionChange = jest.fn();
|
||||
const setIsSettingsModalVisible = jest.fn();
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
|
||||
|
@ -72,33 +77,49 @@ describe('SystemPrompt', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('when conversation is undefined and default prompt is used', () => {
|
||||
describe('when conversation is undefined', () => {
|
||||
const conversation = undefined;
|
||||
|
||||
beforeEach(() => {
|
||||
render(<SystemPrompt conversation={conversation} />);
|
||||
render(
|
||||
<SystemPrompt
|
||||
conversation={conversation}
|
||||
editingSystemPromptId={editingSystemPromptId}
|
||||
isSettingsModalVisible={isSettingsModalVisible}
|
||||
onSystemPromptSelectionChange={onSystemPromptSelectionChange}
|
||||
setIsSettingsModalVisible={setIsSettingsModalVisible}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
it('does render the system prompt fallback text', () => {
|
||||
expect(screen.getByTestId('systemPromptText')).toBeInTheDocument();
|
||||
it('renders the system prompt select', () => {
|
||||
expect(screen.getByTestId('selectSystemPrompt')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('does NOT render the system prompt select', () => {
|
||||
expect(screen.queryByTestId('selectSystemPrompt')).not.toBeInTheDocument();
|
||||
it('does NOT render the system prompt text', () => {
|
||||
expect(screen.queryByTestId('systemPromptText')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('does render the edit button', () => {
|
||||
expect(screen.getByTestId('edit')).toBeInTheDocument();
|
||||
it('does NOT render the edit button', () => {
|
||||
expect(screen.queryByTestId('edit')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('does render the clear button', () => {
|
||||
expect(screen.getByTestId('clear')).toBeInTheDocument();
|
||||
it('does NOT render the clear button', () => {
|
||||
expect(screen.queryByTestId('clear')).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('when conversation is NOT null', () => {
|
||||
beforeEach(() => {
|
||||
render(<SystemPrompt conversation={BASE_CONVERSATION} />);
|
||||
render(
|
||||
<SystemPrompt
|
||||
conversation={BASE_CONVERSATION}
|
||||
editingSystemPromptId={BASE_CONVERSATION.id}
|
||||
isSettingsModalVisible={isSettingsModalVisible}
|
||||
onSystemPromptSelectionChange={onSystemPromptSelectionChange}
|
||||
setIsSettingsModalVisible={setIsSettingsModalVisible}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
it('does NOT render the system prompt select', () => {
|
||||
|
@ -125,7 +146,13 @@ describe('SystemPrompt', () => {
|
|||
const customPromptText = 'custom prompt text';
|
||||
render(
|
||||
<TestProviders>
|
||||
<SystemPrompt conversation={BASE_CONVERSATION} />
|
||||
<SystemPrompt
|
||||
conversation={BASE_CONVERSATION}
|
||||
editingSystemPromptId={editingSystemPromptId}
|
||||
isSettingsModalVisible={isSettingsModalVisible}
|
||||
onSystemPromptSelectionChange={onSystemPromptSelectionChange}
|
||||
setIsSettingsModalVisible={setIsSettingsModalVisible}
|
||||
/>
|
||||
</TestProviders>
|
||||
);
|
||||
userEvent.click(screen.getByTestId('edit'));
|
||||
|
@ -165,7 +192,13 @@ describe('SystemPrompt', () => {
|
|||
const customPromptText = 'custom prompt text';
|
||||
render(
|
||||
<TestProviders>
|
||||
<SystemPrompt conversation={BASE_CONVERSATION} />
|
||||
<SystemPrompt
|
||||
conversation={BASE_CONVERSATION}
|
||||
editingSystemPromptId={editingSystemPromptId}
|
||||
isSettingsModalVisible={isSettingsModalVisible}
|
||||
onSystemPromptSelectionChange={onSystemPromptSelectionChange}
|
||||
setIsSettingsModalVisible={setIsSettingsModalVisible}
|
||||
/>
|
||||
</TestProviders>
|
||||
);
|
||||
userEvent.click(screen.getByTestId('edit'));
|
||||
|
@ -219,7 +252,13 @@ describe('SystemPrompt', () => {
|
|||
const customPromptText = 'custom prompt text';
|
||||
render(
|
||||
<TestProviders>
|
||||
<SystemPrompt conversation={BASE_CONVERSATION} />
|
||||
<SystemPrompt
|
||||
conversation={BASE_CONVERSATION}
|
||||
editingSystemPromptId={editingSystemPromptId}
|
||||
isSettingsModalVisible={isSettingsModalVisible}
|
||||
onSystemPromptSelectionChange={onSystemPromptSelectionChange}
|
||||
setIsSettingsModalVisible={setIsSettingsModalVisible}
|
||||
/>
|
||||
</TestProviders>
|
||||
);
|
||||
userEvent.click(screen.getByTestId('edit'));
|
||||
|
@ -280,7 +319,13 @@ describe('SystemPrompt', () => {
|
|||
it('should save new prompt correctly when prompt is removed from selected conversation', async () => {
|
||||
render(
|
||||
<TestProviders>
|
||||
<SystemPrompt conversation={BASE_CONVERSATION} />
|
||||
<SystemPrompt
|
||||
conversation={BASE_CONVERSATION}
|
||||
editingSystemPromptId={editingSystemPromptId}
|
||||
isSettingsModalVisible={isSettingsModalVisible}
|
||||
onSystemPromptSelectionChange={onSystemPromptSelectionChange}
|
||||
setIsSettingsModalVisible={setIsSettingsModalVisible}
|
||||
/>
|
||||
</TestProviders>
|
||||
);
|
||||
userEvent.click(screen.getByTestId('edit'));
|
||||
|
@ -351,7 +396,13 @@ describe('SystemPrompt', () => {
|
|||
|
||||
render(
|
||||
<TestProviders>
|
||||
<SystemPrompt conversation={BASE_CONVERSATION} />
|
||||
<SystemPrompt
|
||||
conversation={BASE_CONVERSATION}
|
||||
editingSystemPromptId={editingSystemPromptId}
|
||||
isSettingsModalVisible={isSettingsModalVisible}
|
||||
onSystemPromptSelectionChange={onSystemPromptSelectionChange}
|
||||
setIsSettingsModalVisible={setIsSettingsModalVisible}
|
||||
/>
|
||||
</TestProviders>
|
||||
);
|
||||
userEvent.click(screen.getByTestId('edit'));
|
||||
|
@ -413,7 +464,13 @@ describe('SystemPrompt', () => {
|
|||
it('shows the system prompt select when the edit button is clicked', () => {
|
||||
render(
|
||||
<TestProviders>
|
||||
<SystemPrompt conversation={BASE_CONVERSATION} />
|
||||
<SystemPrompt
|
||||
conversation={BASE_CONVERSATION}
|
||||
editingSystemPromptId={BASE_CONVERSATION.id}
|
||||
isSettingsModalVisible={isSettingsModalVisible}
|
||||
onSystemPromptSelectionChange={onSystemPromptSelectionChange}
|
||||
setIsSettingsModalVisible={setIsSettingsModalVisible}
|
||||
/>
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
|
@ -425,7 +482,13 @@ describe('SystemPrompt', () => {
|
|||
it('shows the system prompt select when system prompt text is clicked', () => {
|
||||
render(
|
||||
<TestProviders>
|
||||
<SystemPrompt conversation={BASE_CONVERSATION} />
|
||||
<SystemPrompt
|
||||
conversation={BASE_CONVERSATION}
|
||||
editingSystemPromptId={BASE_CONVERSATION.id}
|
||||
isSettingsModalVisible={isSettingsModalVisible}
|
||||
onSystemPromptSelectionChange={onSystemPromptSelectionChange}
|
||||
setIsSettingsModalVisible={setIsSettingsModalVisible}
|
||||
/>
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
|
|
|
@ -6,38 +6,50 @@
|
|||
*/
|
||||
|
||||
import { EuiButtonIcon, EuiFlexGroup, EuiFlexItem, EuiText, EuiToolTip } from '@elastic/eui';
|
||||
import React, { useCallback, useEffect, useState } from 'react';
|
||||
import React, { useCallback, useMemo } from 'react';
|
||||
|
||||
import { css } from '@emotion/react';
|
||||
import { isEmpty } from 'lodash/fp';
|
||||
import { useAssistantContext } from '../../../assistant_context';
|
||||
import { Conversation, Prompt } from '../../../..';
|
||||
import { Conversation } from '../../../..';
|
||||
import * as i18n from './translations';
|
||||
import { SelectSystemPrompt } from './select_system_prompt';
|
||||
|
||||
interface Props {
|
||||
conversation: Conversation | undefined;
|
||||
editingSystemPromptId: string | undefined;
|
||||
isSettingsModalVisible: boolean;
|
||||
onSystemPromptSelectionChange: (systemPromptId: string | undefined) => void;
|
||||
setIsSettingsModalVisible: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
}
|
||||
|
||||
const SystemPromptComponent: React.FC<Props> = ({ conversation }) => {
|
||||
const SystemPromptComponent: React.FC<Props> = ({
|
||||
conversation,
|
||||
editingSystemPromptId,
|
||||
isSettingsModalVisible,
|
||||
onSystemPromptSelectionChange,
|
||||
setIsSettingsModalVisible,
|
||||
}) => {
|
||||
const { allSystemPrompts } = useAssistantContext();
|
||||
|
||||
const [selectedPrompt, setSelectedPrompt] = useState<Prompt | undefined>(
|
||||
allSystemPrompts?.find((p) => p.id === conversation?.apiConfig.defaultSystemPromptId) ??
|
||||
allSystemPrompts?.[0]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
setSelectedPrompt(
|
||||
allSystemPrompts?.find((p) => p.id === conversation?.apiConfig.defaultSystemPromptId) ??
|
||||
allSystemPrompts?.[0]
|
||||
);
|
||||
}, [allSystemPrompts, conversation]);
|
||||
const selectedPrompt = useMemo(() => {
|
||||
if (editingSystemPromptId !== undefined) {
|
||||
return (
|
||||
allSystemPrompts?.find((p) => p.id === editingSystemPromptId) ??
|
||||
allSystemPrompts?.find((p) => p.id === conversation?.apiConfig.defaultSystemPromptId)
|
||||
);
|
||||
} else {
|
||||
return undefined;
|
||||
}
|
||||
}, [allSystemPrompts, conversation?.apiConfig.defaultSystemPromptId, editingSystemPromptId]);
|
||||
|
||||
const [isEditing, setIsEditing] = React.useState<boolean>(false);
|
||||
|
||||
const handleClearSystemPrompt = useCallback(() => {
|
||||
setSelectedPrompt(undefined);
|
||||
}, []);
|
||||
if (conversation) {
|
||||
onSystemPromptSelectionChange(undefined);
|
||||
}
|
||||
}, [conversation, onSystemPromptSelectionChange]);
|
||||
|
||||
const handleEditSystemPrompt = useCallback(() => setIsEditing(true), []);
|
||||
|
||||
|
@ -52,8 +64,11 @@ const SystemPromptComponent: React.FC<Props> = ({ conversation }) => {
|
|||
isClearable={true}
|
||||
isEditing={isEditing}
|
||||
isOpen={isEditing}
|
||||
isSettingsModalVisible={isSettingsModalVisible}
|
||||
onSystemPromptSelectionChange={onSystemPromptSelectionChange}
|
||||
selectedPrompt={selectedPrompt}
|
||||
setIsEditing={setIsEditing}
|
||||
setIsSettingsModalVisible={setIsSettingsModalVisible}
|
||||
/>
|
||||
) : (
|
||||
<EuiFlexGroup alignItems="flexStart" gutterSize="none">
|
||||
|
@ -70,7 +85,7 @@ const SystemPromptComponent: React.FC<Props> = ({ conversation }) => {
|
|||
}
|
||||
`}
|
||||
>
|
||||
{selectedPrompt?.content ?? ''}
|
||||
{isEmpty(selectedPrompt?.content) ? i18n.EMPTY_PROMPT : selectedPrompt?.content}
|
||||
</EuiText>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
|
|
|
@ -24,7 +24,9 @@ const props: Props = {
|
|||
},
|
||||
],
|
||||
conversation: undefined,
|
||||
isSettingsModalVisible: false,
|
||||
selectedPrompt: undefined,
|
||||
setIsSettingsModalVisible: jest.fn(),
|
||||
};
|
||||
|
||||
const mockUseAssistantContext = {
|
||||
|
|
|
@ -37,9 +37,11 @@ export interface Props {
|
|||
isEditing?: boolean;
|
||||
isDisabled?: boolean;
|
||||
isOpen?: boolean;
|
||||
isSettingsModalVisible: boolean;
|
||||
setIsEditing?: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
setIsSettingsModalVisible: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
showTitles?: boolean;
|
||||
onSystemPromptSelectionChange?: (promptId: string) => void;
|
||||
onSystemPromptSelectionChange?: (promptId: string | undefined) => void;
|
||||
}
|
||||
|
||||
const ADD_NEW_SYSTEM_PROMPT = 'ADD_NEW_SYSTEM_PROMPT';
|
||||
|
@ -54,12 +56,13 @@ const SelectSystemPromptComponent: React.FC<Props> = ({
|
|||
isEditing = false,
|
||||
isDisabled = false,
|
||||
isOpen = false,
|
||||
isSettingsModalVisible,
|
||||
onSystemPromptSelectionChange,
|
||||
setIsEditing,
|
||||
setIsSettingsModalVisible,
|
||||
showTitles = false,
|
||||
}) => {
|
||||
const { isSettingsModalVisible, setIsSettingsModalVisible, setSelectedSettingsTab } =
|
||||
useAssistantContext();
|
||||
const { setSelectedSettingsTab } = useAssistantContext();
|
||||
const { setApiConfig } = useConversation();
|
||||
|
||||
const [isOpenLocal, setIsOpenLocal] = useState<boolean>(isOpen);
|
||||
|
|
|
@ -148,25 +148,33 @@ export const SystemPromptSelector: React.FC<Props> = React.memo(
|
|||
alignItems="center"
|
||||
className={'parentFlexGroup'}
|
||||
component={'span'}
|
||||
gutterSize={'none'}
|
||||
justifyContent="spaceBetween"
|
||||
data-test-subj="systemPromptOptionSelector"
|
||||
>
|
||||
<EuiFlexItem grow={1} component={'span'}>
|
||||
<EuiFlexItem
|
||||
grow={false}
|
||||
component={'span'}
|
||||
css={css`
|
||||
width: calc(100% - 60px);
|
||||
`}
|
||||
>
|
||||
<EuiFlexGroup alignItems="center" component={'span'} gutterSize={'s'}>
|
||||
<EuiFlexItem grow={false} component={'span'}>
|
||||
<span className={contentClassName}>
|
||||
<EuiHighlight
|
||||
search={searchValue}
|
||||
css={css`
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
max-width: 70%;
|
||||
`}
|
||||
>
|
||||
{label}
|
||||
</EuiHighlight>
|
||||
</span>
|
||||
<EuiFlexItem
|
||||
component={'span'}
|
||||
grow={false}
|
||||
css={css`
|
||||
max-width: 100%;
|
||||
`}
|
||||
>
|
||||
<EuiHighlight
|
||||
search={searchValue}
|
||||
css={css`
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
`}
|
||||
>
|
||||
{label}
|
||||
</EuiHighlight>
|
||||
</EuiFlexItem>
|
||||
{value?.isNewConversationDefault && (
|
||||
<EuiFlexItem grow={false} component={'span'}>
|
||||
|
@ -179,7 +187,7 @@ export const SystemPromptSelector: React.FC<Props> = React.memo(
|
|||
</EuiFlexItem>
|
||||
|
||||
{!value?.isDefault && (
|
||||
<EuiFlexItem grow={2} component={'span'}>
|
||||
<EuiFlexItem grow={false} component={'span'}>
|
||||
<EuiToolTip position="right" content={i18n.DELETE_SYSTEM_PROMPT}>
|
||||
<EuiButtonIcon
|
||||
iconType="cross"
|
||||
|
|
|
@ -216,6 +216,7 @@ export const SystemPromptSettings: React.FC<Props> = React.memo(
|
|||
data-test-subj={TEST_IDS.SYSTEM_PROMPT_MODAL.PROMPT_TEXT}
|
||||
disabled={selectedSystemPrompt == null}
|
||||
onChange={handlePromptContentChange}
|
||||
placeholder={i18n.SYSTEM_PROMPT_PROMPT_PLACEHOLDER}
|
||||
value={promptContent}
|
||||
compressed
|
||||
fullWidth
|
||||
|
|
|
@ -41,6 +41,13 @@ export const SYSTEM_PROMPT_PROMPT = i18n.translate(
|
|||
}
|
||||
);
|
||||
|
||||
export const SYSTEM_PROMPT_PROMPT_PLACEHOLDER = i18n.translate(
|
||||
'xpack.elasticAssistant.assistant.promptEditor.systemPrompt.settings.promptPlaceholder',
|
||||
{
|
||||
defaultMessage: 'Enter a System Prompt',
|
||||
}
|
||||
);
|
||||
|
||||
export const SYSTEM_PROMPT_DEFAULT_CONVERSATIONS = i18n.translate(
|
||||
'xpack.elasticAssistant.assistant.promptEditor.systemPrompt.settings.defaultConversationsLabel',
|
||||
{
|
||||
|
|
|
@ -34,3 +34,10 @@ export const ADD_NEW_SYSTEM_PROMPT = i18n.translate(
|
|||
defaultMessage: 'Add new system prompt...',
|
||||
}
|
||||
);
|
||||
|
||||
export const EMPTY_PROMPT = i18n.translate(
|
||||
'xpack.elasticAssistant.assistant.firstPromptEditor.emptyPrompt',
|
||||
{
|
||||
defaultMessage: '(empty prompt)',
|
||||
}
|
||||
);
|
||||
|
|
|
@ -13,7 +13,7 @@ import styled from 'styled-components';
|
|||
import * as i18n from './translations';
|
||||
|
||||
export interface Props extends React.TextareaHTMLAttributes<HTMLTextAreaElement> {
|
||||
handlePromptChange?: (value: string) => void;
|
||||
handlePromptChange: (value: string) => void;
|
||||
isDisabled?: boolean;
|
||||
onPromptSubmit: (value: string) => void;
|
||||
value: string;
|
||||
|
@ -26,35 +26,30 @@ const StyledTextArea = styled(EuiTextArea)`
|
|||
|
||||
export const PromptTextArea = forwardRef<HTMLTextAreaElement, Props>(
|
||||
({ isDisabled = false, value, onPromptSubmit, handlePromptChange, ...props }, ref) => {
|
||||
const [currentValue, setCurrentValue] = React.useState(value);
|
||||
|
||||
const onChangeCallback = useCallback(
|
||||
(event: React.ChangeEvent<HTMLTextAreaElement>) => {
|
||||
setCurrentValue(event.target.value);
|
||||
if (handlePromptChange) {
|
||||
handlePromptChange(event.target.value);
|
||||
}
|
||||
handlePromptChange(event.target.value);
|
||||
},
|
||||
[handlePromptChange]
|
||||
);
|
||||
|
||||
const onKeyDown = useCallback(
|
||||
(event) => {
|
||||
if (event.key === 'Enter' && !event.shiftKey && currentValue.trim().length > 0) {
|
||||
if (event.key === 'Enter' && !event.shiftKey && value.trim().length > 0) {
|
||||
event.preventDefault();
|
||||
onPromptSubmit(event.target.value?.trim());
|
||||
setCurrentValue('');
|
||||
} else if (event.key === 'Enter' && !event.shiftKey && currentValue.trim().length === 0) {
|
||||
handlePromptChange('');
|
||||
} else if (event.key === 'Enter' && !event.shiftKey && value.trim().length === 0) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
}
|
||||
},
|
||||
[currentValue, onPromptSubmit]
|
||||
[value, onPromptSubmit, handlePromptChange]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
setCurrentValue(value);
|
||||
}, [value]);
|
||||
handlePromptChange(value);
|
||||
}, [handlePromptChange, value]);
|
||||
|
||||
return (
|
||||
<StyledTextArea
|
||||
|
@ -66,7 +61,7 @@ export const PromptTextArea = forwardRef<HTMLTextAreaElement, Props>(
|
|||
autoFocus
|
||||
disabled={isDisabled}
|
||||
placeholder={i18n.PROMPT_PLACEHOLDER}
|
||||
value={currentValue}
|
||||
value={value}
|
||||
onChange={onChangeCallback}
|
||||
onKeyDown={onKeyDown}
|
||||
/>
|
||||
|
|
|
@ -138,14 +138,24 @@ export const QuickPromptSelector: React.FC<Props> = React.memo(
|
|||
return (
|
||||
<EuiFlexGroup
|
||||
alignItems="center"
|
||||
justifyContent="spaceBetween"
|
||||
className={'parentFlexGroup'}
|
||||
component={'span'}
|
||||
justifyContent="spaceBetween"
|
||||
>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiHealth color={color}>
|
||||
<span className={contentClassName}>
|
||||
<EuiHighlight search={searchValue}>{label}</EuiHighlight>
|
||||
</span>
|
||||
<EuiFlexItem
|
||||
component={'span'}
|
||||
grow={false}
|
||||
css={css`
|
||||
width: calc(100% - 60px);
|
||||
`}
|
||||
>
|
||||
<EuiHealth
|
||||
color={color}
|
||||
css={css`
|
||||
overflow: hidden;
|
||||
`}
|
||||
>
|
||||
<EuiHighlight search={searchValue}>{label}</EuiHighlight>
|
||||
</EuiHealth>
|
||||
</EuiFlexItem>
|
||||
{!value?.isDefault && (
|
||||
|
|
|
@ -198,6 +198,7 @@ export const QuickPromptSettings: React.FC<Props> = React.memo<Props>(
|
|||
disabled={selectedQuickPrompt == null}
|
||||
fullWidth
|
||||
onChange={handlePromptChange}
|
||||
placeholder={i18n.QUICK_PROMPT_PROMPT_PLACEHOLDER}
|
||||
value={prompt}
|
||||
css={css`
|
||||
min-height: 150px;
|
||||
|
|
|
@ -42,6 +42,13 @@ export const QUICK_PROMPT_PROMPT = i18n.translate(
|
|||
}
|
||||
);
|
||||
|
||||
export const QUICK_PROMPT_PROMPT_PLACEHOLDER = i18n.translate(
|
||||
'xpack.elasticAssistant.assistant.quickPrompts.settings.promptPlaceholder',
|
||||
{
|
||||
defaultMessage: 'Enter a Quick Prompt',
|
||||
}
|
||||
);
|
||||
|
||||
export const QUICK_PROMPT_BADGE_COLOR = i18n.translate(
|
||||
'xpack.elasticAssistant.assistant.quickPrompts.settings.badgeColorLabel',
|
||||
{
|
||||
|
|
|
@ -21,6 +21,7 @@ const QuickPromptsFlexGroup = styled(EuiFlexGroup)`
|
|||
const COUNT_BEFORE_OVERFLOW = 5;
|
||||
interface QuickPromptsProps {
|
||||
setInput: (input: string) => void;
|
||||
setIsSettingsModalVisible: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -28,95 +29,96 @@ interface QuickPromptsProps {
|
|||
* text, and support for adding new quick prompts and editing existing. Also supports overflow of quick prompts,
|
||||
* and localstorage for storing new and edited prompts.
|
||||
*/
|
||||
export const QuickPrompts: React.FC<QuickPromptsProps> = React.memo(({ setInput }) => {
|
||||
const { allQuickPrompts, promptContexts, setIsSettingsModalVisible, setSelectedSettingsTab } =
|
||||
useAssistantContext();
|
||||
export const QuickPrompts: React.FC<QuickPromptsProps> = React.memo(
|
||||
({ setInput, setIsSettingsModalVisible }) => {
|
||||
const { allQuickPrompts, promptContexts, setSelectedSettingsTab } = useAssistantContext();
|
||||
|
||||
const contextFilteredQuickPrompts = useMemo(() => {
|
||||
const registeredPromptContextTitles = Object.values(promptContexts).map((pc) => pc.category);
|
||||
return allQuickPrompts.filter((quickPrompt) => {
|
||||
// Return quick prompt as match if it has no categories, otherwise ensure category exists in registered prompt contexts
|
||||
if (quickPrompt.categories == null || quickPrompt.categories.length === 0) {
|
||||
return true;
|
||||
} else {
|
||||
return quickPrompt.categories.some((category) => {
|
||||
return registeredPromptContextTitles.includes(category);
|
||||
});
|
||||
}
|
||||
});
|
||||
}, [allQuickPrompts, promptContexts]);
|
||||
const contextFilteredQuickPrompts = useMemo(() => {
|
||||
const registeredPromptContextTitles = Object.values(promptContexts).map((pc) => pc.category);
|
||||
return allQuickPrompts.filter((quickPrompt) => {
|
||||
// Return quick prompt as match if it has no categories, otherwise ensure category exists in registered prompt contexts
|
||||
if (quickPrompt.categories == null || quickPrompt.categories.length === 0) {
|
||||
return true;
|
||||
} else {
|
||||
return quickPrompt.categories.some((category) => {
|
||||
return registeredPromptContextTitles.includes(category);
|
||||
});
|
||||
}
|
||||
});
|
||||
}, [allQuickPrompts, promptContexts]);
|
||||
|
||||
// Overflow state
|
||||
const [isOverflowPopoverOpen, setIsOverflowPopoverOpen] = useState(false);
|
||||
const toggleOverflowPopover = useCallback(
|
||||
() => setIsOverflowPopoverOpen(!isOverflowPopoverOpen),
|
||||
[isOverflowPopoverOpen]
|
||||
);
|
||||
const closeOverflowPopover = useCallback(() => setIsOverflowPopoverOpen(false), []);
|
||||
// Overflow state
|
||||
const [isOverflowPopoverOpen, setIsOverflowPopoverOpen] = useState(false);
|
||||
const toggleOverflowPopover = useCallback(
|
||||
() => setIsOverflowPopoverOpen(!isOverflowPopoverOpen),
|
||||
[isOverflowPopoverOpen]
|
||||
);
|
||||
const closeOverflowPopover = useCallback(() => setIsOverflowPopoverOpen(false), []);
|
||||
|
||||
const onClickOverflowQuickPrompt = useCallback(
|
||||
(prompt: string) => {
|
||||
setInput(prompt);
|
||||
closeOverflowPopover();
|
||||
},
|
||||
[closeOverflowPopover, setInput]
|
||||
);
|
||||
const onClickOverflowQuickPrompt = useCallback(
|
||||
(prompt: string) => {
|
||||
setInput(prompt);
|
||||
closeOverflowPopover();
|
||||
},
|
||||
[closeOverflowPopover, setInput]
|
||||
);
|
||||
|
||||
const showQuickPromptSettings = useCallback(() => {
|
||||
setIsSettingsModalVisible(true);
|
||||
setSelectedSettingsTab(QUICK_PROMPTS_TAB);
|
||||
}, [setIsSettingsModalVisible, setSelectedSettingsTab]);
|
||||
const showQuickPromptSettings = useCallback(() => {
|
||||
setIsSettingsModalVisible(true);
|
||||
setSelectedSettingsTab(QUICK_PROMPTS_TAB);
|
||||
}, [setIsSettingsModalVisible, setSelectedSettingsTab]);
|
||||
|
||||
return (
|
||||
<QuickPromptsFlexGroup gutterSize="s" alignItems="center">
|
||||
{contextFilteredQuickPrompts.slice(0, COUNT_BEFORE_OVERFLOW).map((badge, index) => (
|
||||
<EuiFlexItem key={index} grow={false}>
|
||||
<EuiBadge
|
||||
color={badge.color}
|
||||
onClick={() => setInput(badge.prompt)}
|
||||
onClickAriaLabel={badge.title}
|
||||
>
|
||||
{badge.title}
|
||||
</EuiBadge>
|
||||
</EuiFlexItem>
|
||||
))}
|
||||
{contextFilteredQuickPrompts.length > COUNT_BEFORE_OVERFLOW && (
|
||||
return (
|
||||
<QuickPromptsFlexGroup gutterSize="s" alignItems="center">
|
||||
{contextFilteredQuickPrompts.slice(0, COUNT_BEFORE_OVERFLOW).map((badge, index) => (
|
||||
<EuiFlexItem key={index} grow={false}>
|
||||
<EuiBadge
|
||||
color={badge.color}
|
||||
onClick={() => setInput(badge.prompt)}
|
||||
onClickAriaLabel={badge.title}
|
||||
>
|
||||
{badge.title}
|
||||
</EuiBadge>
|
||||
</EuiFlexItem>
|
||||
))}
|
||||
{contextFilteredQuickPrompts.length > COUNT_BEFORE_OVERFLOW && (
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiPopover
|
||||
button={
|
||||
<EuiBadge
|
||||
color={'hollow'}
|
||||
iconType={'boxesHorizontal'}
|
||||
onClick={toggleOverflowPopover}
|
||||
onClickAriaLabel={i18n.QUICK_PROMPT_OVERFLOW_ARIA}
|
||||
/>
|
||||
}
|
||||
isOpen={isOverflowPopoverOpen}
|
||||
closePopover={closeOverflowPopover}
|
||||
anchorPosition="rightUp"
|
||||
>
|
||||
<EuiFlexGroup direction="column" gutterSize="s">
|
||||
{contextFilteredQuickPrompts.slice(COUNT_BEFORE_OVERFLOW).map((badge, index) => (
|
||||
<EuiFlexItem key={index} grow={false}>
|
||||
<EuiBadge
|
||||
color={badge.color}
|
||||
onClick={() => onClickOverflowQuickPrompt(badge.prompt)}
|
||||
onClickAriaLabel={badge.title}
|
||||
>
|
||||
{badge.title}
|
||||
</EuiBadge>
|
||||
</EuiFlexItem>
|
||||
))}
|
||||
</EuiFlexGroup>
|
||||
</EuiPopover>
|
||||
</EuiFlexItem>
|
||||
)}
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiPopover
|
||||
button={
|
||||
<EuiBadge
|
||||
color={'hollow'}
|
||||
iconType={'boxesHorizontal'}
|
||||
onClick={toggleOverflowPopover}
|
||||
onClickAriaLabel={i18n.QUICK_PROMPT_OVERFLOW_ARIA}
|
||||
/>
|
||||
}
|
||||
isOpen={isOverflowPopoverOpen}
|
||||
closePopover={closeOverflowPopover}
|
||||
anchorPosition="rightUp"
|
||||
>
|
||||
<EuiFlexGroup direction="column" gutterSize="s">
|
||||
{contextFilteredQuickPrompts.slice(COUNT_BEFORE_OVERFLOW).map((badge, index) => (
|
||||
<EuiFlexItem key={index} grow={false}>
|
||||
<EuiBadge
|
||||
color={badge.color}
|
||||
onClick={() => onClickOverflowQuickPrompt(badge.prompt)}
|
||||
onClickAriaLabel={badge.title}
|
||||
>
|
||||
{badge.title}
|
||||
</EuiBadge>
|
||||
</EuiFlexItem>
|
||||
))}
|
||||
</EuiFlexGroup>
|
||||
</EuiPopover>
|
||||
<EuiButtonEmpty onClick={showQuickPromptSettings} iconType="plus" size="xs">
|
||||
{i18n.ADD_QUICK_PROMPT}
|
||||
</EuiButtonEmpty>
|
||||
</EuiFlexItem>
|
||||
)}
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButtonEmpty onClick={showQuickPromptSettings} iconType="plus" size="xs">
|
||||
{i18n.ADD_QUICK_PROMPT}
|
||||
</EuiButtonEmpty>
|
||||
</EuiFlexItem>
|
||||
</QuickPromptsFlexGroup>
|
||||
);
|
||||
});
|
||||
</QuickPromptsFlexGroup>
|
||||
);
|
||||
}
|
||||
);
|
||||
QuickPrompts.displayName = 'QuickPrompts';
|
||||
|
|
|
@ -23,6 +23,7 @@ import {
|
|||
// eslint-disable-next-line @kbn/eslint/module_migration
|
||||
import styled from 'styled-components';
|
||||
import { css } from '@emotion/react';
|
||||
import { OpenAiProviderType } from '@kbn/stack-connectors-plugin/common/gen_ai/constants';
|
||||
import { Conversation, Prompt, QuickPrompt } from '../../..';
|
||||
import * as i18n from './translations';
|
||||
import { useAssistantContext } from '../../assistant_context';
|
||||
|
@ -54,6 +55,8 @@ export type SettingsTabs =
|
|||
| typeof FUNCTIONS_TAB
|
||||
| typeof ADVANCED_TAB;
|
||||
interface Props {
|
||||
defaultConnectorId?: string;
|
||||
defaultProvider?: OpenAiProviderType;
|
||||
onClose: (
|
||||
event?: React.KeyboardEvent<HTMLDivElement> | React.MouseEvent<HTMLButtonElement>
|
||||
) => void;
|
||||
|
@ -68,6 +71,8 @@ interface Props {
|
|||
*/
|
||||
export const AssistantSettings: React.FC<Props> = React.memo(
|
||||
({
|
||||
defaultConnectorId,
|
||||
defaultProvider,
|
||||
onClose,
|
||||
onSave,
|
||||
selectedConversation: defaultSelectedConversation,
|
||||
|
@ -244,6 +249,8 @@ export const AssistantSettings: React.FC<Props> = React.memo(
|
|||
>
|
||||
{selectedSettingsTab === CONVERSATIONS_TAB && (
|
||||
<ConversationSettings
|
||||
defaultConnectorId={defaultConnectorId}
|
||||
defaultProvider={defaultProvider}
|
||||
conversationSettings={conversationSettings}
|
||||
setUpdatedConversationSettings={setUpdatedConversationSettings}
|
||||
allSystemPrompts={systemPromptSettings}
|
||||
|
|
|
@ -8,13 +8,18 @@
|
|||
import React, { useCallback } from 'react';
|
||||
import { EuiButtonIcon, EuiToolTip } from '@elastic/eui';
|
||||
|
||||
import { OpenAiProviderType } from '@kbn/stack-connectors-plugin/common/gen_ai/constants';
|
||||
import { Conversation } from '../../..';
|
||||
import { AssistantSettings, CONVERSATIONS_TAB } from './assistant_settings';
|
||||
import * as i18n from './translations';
|
||||
import { useAssistantContext } from '../../assistant_context';
|
||||
|
||||
interface Props {
|
||||
defaultConnectorId?: string;
|
||||
defaultProvider?: OpenAiProviderType;
|
||||
isSettingsModalVisible: boolean;
|
||||
selectedConversation: Conversation;
|
||||
setIsSettingsModalVisible: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
setSelectedConversationId: React.Dispatch<React.SetStateAction<string>>;
|
||||
isDisabled?: boolean;
|
||||
}
|
||||
|
@ -23,9 +28,16 @@ interface Props {
|
|||
* Gear button that opens the assistant settings modal
|
||||
*/
|
||||
export const AssistantSettingsButton: React.FC<Props> = React.memo(
|
||||
({ isDisabled = false, selectedConversation, setSelectedConversationId }) => {
|
||||
const { isSettingsModalVisible, setIsSettingsModalVisible, setSelectedSettingsTab } =
|
||||
useAssistantContext();
|
||||
({
|
||||
defaultConnectorId,
|
||||
defaultProvider,
|
||||
isDisabled = false,
|
||||
isSettingsModalVisible,
|
||||
setIsSettingsModalVisible,
|
||||
selectedConversation,
|
||||
setSelectedConversationId,
|
||||
}) => {
|
||||
const { setSelectedSettingsTab } = useAssistantContext();
|
||||
|
||||
// Modal control functions
|
||||
const cleanupAndCloseModal = useCallback(() => {
|
||||
|
@ -60,6 +72,8 @@ export const AssistantSettingsButton: React.FC<Props> = React.memo(
|
|||
|
||||
{isSettingsModalVisible && (
|
||||
<AssistantSettings
|
||||
defaultConnectorId={defaultConnectorId}
|
||||
defaultProvider={defaultProvider}
|
||||
selectedConversation={selectedConversation}
|
||||
setSelectedConversationId={setSelectedConversationId}
|
||||
onClose={handleCloseModal}
|
||||
|
|
|
@ -12,6 +12,6 @@ export interface Prompt {
|
|||
content: string;
|
||||
name: string;
|
||||
promptType: PromptType;
|
||||
isDefault?: boolean;
|
||||
isDefault?: boolean; // TODO: Should be renamed to isImmutable as this flag is used to prevent users from deleting prompts
|
||||
isNewConversationDefault?: boolean;
|
||||
}
|
||||
|
|
|
@ -5,7 +5,8 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { analyzeMarkdown } from './helpers';
|
||||
import { analyzeMarkdown, getDefaultSystemPrompt } from './helpers';
|
||||
import { Conversation, Prompt } from '../../..';
|
||||
|
||||
const tilde = '`';
|
||||
const codeDelimiter = '```';
|
||||
|
@ -52,13 +53,152 @@ ${codeDelimiter}
|
|||
|
||||
This query will filter the events based on the condition that the ${tilde}user.name${tilde} field should exactly match the value \"9dcc9960-78cf-4ef6-9a2e-dbd5816daa60\".`;
|
||||
|
||||
describe('analyzeMarkdown', () => {
|
||||
it('should identify dsl Query successfully.', () => {
|
||||
const result = analyzeMarkdown(markDownWithDSLQuery);
|
||||
expect(result[0].type).toBe('dsl');
|
||||
describe('useConversation helpers', () => {
|
||||
describe('analyzeMarkdown', () => {
|
||||
it('should identify dsl Query successfully.', () => {
|
||||
const result = analyzeMarkdown(markDownWithDSLQuery);
|
||||
expect(result[0].type).toBe('dsl');
|
||||
});
|
||||
it('should identify kql Query successfully.', () => {
|
||||
const result = analyzeMarkdown(markDownWithKQLQuery);
|
||||
expect(result[0].type).toBe('kql');
|
||||
});
|
||||
});
|
||||
it('should identify kql Query successfully.', () => {
|
||||
const result = analyzeMarkdown(markDownWithKQLQuery);
|
||||
expect(result[0].type).toBe('kql');
|
||||
|
||||
describe('getDefaultSystemPrompt', () => {
|
||||
const allSystemPrompts: Prompt[] = [
|
||||
{
|
||||
id: '1',
|
||||
content: 'Prompt 1',
|
||||
name: 'Prompt 1',
|
||||
promptType: 'user',
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
content: 'Prompt 2',
|
||||
name: 'Prompt 2',
|
||||
promptType: 'user',
|
||||
isNewConversationDefault: true,
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
content: 'Prompt 3',
|
||||
name: 'Prompt 3',
|
||||
promptType: 'user',
|
||||
},
|
||||
];
|
||||
const allSystemPromptsNoDefault: Prompt[] = allSystemPrompts.filter(
|
||||
({ isNewConversationDefault }) => isNewConversationDefault !== true
|
||||
);
|
||||
const conversation: Conversation = {
|
||||
apiConfig: {
|
||||
defaultSystemPromptId: '3',
|
||||
},
|
||||
id: '1',
|
||||
messages: [],
|
||||
};
|
||||
|
||||
test('should return the conversation system prompt if it exists', () => {
|
||||
const result = getDefaultSystemPrompt({ allSystemPrompts, conversation });
|
||||
|
||||
expect(result).toEqual(allSystemPrompts[2]);
|
||||
});
|
||||
|
||||
test('should return the default (starred) isNewConversationDefault system prompt if conversation system prompt does not exist', () => {
|
||||
const conversationWithoutSystemPrompt: Conversation = {
|
||||
apiConfig: {},
|
||||
id: '1',
|
||||
messages: [],
|
||||
};
|
||||
const result = getDefaultSystemPrompt({
|
||||
allSystemPrompts,
|
||||
conversation: conversationWithoutSystemPrompt,
|
||||
});
|
||||
|
||||
expect(result).toEqual(allSystemPrompts[1]);
|
||||
});
|
||||
|
||||
test('should return the default (starred) isNewConversationDefault system prompt if conversation system prompt does not exist within all system prompts', () => {
|
||||
const conversationWithoutSystemPrompt: Conversation = {
|
||||
apiConfig: {},
|
||||
id: '4', // this id does not exist within allSystemPrompts
|
||||
messages: [],
|
||||
};
|
||||
const result = getDefaultSystemPrompt({
|
||||
allSystemPrompts,
|
||||
conversation: conversationWithoutSystemPrompt,
|
||||
});
|
||||
|
||||
expect(result).toEqual(allSystemPrompts[1]);
|
||||
});
|
||||
|
||||
test('should return the first prompt if both conversation system prompt and default new system prompt do not exist', () => {
|
||||
const conversationWithoutSystemPrompt: Conversation = {
|
||||
apiConfig: {},
|
||||
id: '1',
|
||||
messages: [],
|
||||
};
|
||||
const result = getDefaultSystemPrompt({
|
||||
allSystemPrompts: allSystemPromptsNoDefault,
|
||||
conversation: conversationWithoutSystemPrompt,
|
||||
});
|
||||
|
||||
expect(result).toEqual(allSystemPromptsNoDefault[0]);
|
||||
});
|
||||
|
||||
test('should return undefined if conversation system prompt does not exist and there are no system prompts', () => {
|
||||
const conversationWithoutSystemPrompt: Conversation = {
|
||||
apiConfig: {},
|
||||
id: '1',
|
||||
messages: [],
|
||||
};
|
||||
const result = getDefaultSystemPrompt({
|
||||
allSystemPrompts: [],
|
||||
conversation: conversationWithoutSystemPrompt,
|
||||
});
|
||||
|
||||
expect(result).toEqual(undefined);
|
||||
});
|
||||
|
||||
test('should return undefined if conversation system prompt does not exist within all system prompts', () => {
|
||||
const conversationWithoutSystemPrompt: Conversation = {
|
||||
apiConfig: {},
|
||||
id: '4', // this id does not exist within allSystemPrompts
|
||||
messages: [],
|
||||
};
|
||||
const result = getDefaultSystemPrompt({
|
||||
allSystemPrompts: allSystemPromptsNoDefault,
|
||||
conversation: conversationWithoutSystemPrompt,
|
||||
});
|
||||
|
||||
expect(result).toEqual(allSystemPromptsNoDefault[0]);
|
||||
});
|
||||
|
||||
test('should return (starred) isNewConversationDefault system prompt if conversation is undefined', () => {
|
||||
const result = getDefaultSystemPrompt({
|
||||
allSystemPrompts,
|
||||
conversation: undefined,
|
||||
});
|
||||
|
||||
expect(result).toEqual(allSystemPrompts[1]);
|
||||
});
|
||||
|
||||
test('should return the first system prompt if the conversation is undefined and isNewConversationDefault is not present in system prompts', () => {
|
||||
const result = getDefaultSystemPrompt({
|
||||
allSystemPrompts: allSystemPromptsNoDefault,
|
||||
conversation: undefined,
|
||||
});
|
||||
|
||||
expect(result).toEqual(allSystemPromptsNoDefault[0]);
|
||||
});
|
||||
|
||||
test('should return undefined if conversation is undefined and no system prompts are provided', () => {
|
||||
const result = getDefaultSystemPrompt({
|
||||
allSystemPrompts: [],
|
||||
conversation: undefined,
|
||||
});
|
||||
|
||||
expect(result).toEqual(undefined);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -6,6 +6,8 @@
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { Prompt } from '../types';
|
||||
import { Conversation } from '../../assistant_context/types';
|
||||
|
||||
export interface CodeBlockDetails {
|
||||
type: QueryType;
|
||||
|
@ -64,3 +66,24 @@ export const analyzeMarkdown = (markdown: string): CodeBlockDetails[] => {
|
|||
|
||||
return result;
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns the default system prompt for a given conversation
|
||||
*
|
||||
* @param allSystemPrompts All available System Prompts
|
||||
* @param conversation Conversation to get the default system prompt for
|
||||
*/
|
||||
export const getDefaultSystemPrompt = ({
|
||||
allSystemPrompts,
|
||||
conversation,
|
||||
}: {
|
||||
allSystemPrompts: Prompt[];
|
||||
conversation: Conversation | undefined;
|
||||
}): Prompt | undefined => {
|
||||
const conversationSystemPrompt = allSystemPrompts.find(
|
||||
(prompt) => prompt.id === conversation?.apiConfig?.defaultSystemPromptId
|
||||
);
|
||||
const defaultNewSystemPrompt = allSystemPrompts.find((prompt) => prompt.isNewConversationDefault);
|
||||
|
||||
return conversationSystemPrompt ?? defaultNewSystemPrompt ?? allSystemPrompts?.[0];
|
||||
};
|
||||
|
|
|
@ -11,6 +11,7 @@ import { useAssistantContext } from '../../assistant_context';
|
|||
import { Conversation, Message } from '../../assistant_context/types';
|
||||
import * as i18n from './translations';
|
||||
import { ELASTIC_AI_ASSISTANT, ELASTIC_AI_ASSISTANT_TITLE } from './translations';
|
||||
import { getDefaultSystemPrompt } from './helpers';
|
||||
|
||||
export const DEFAULT_CONVERSATION_STATE: Conversation = {
|
||||
id: i18n.DEFAULT_CONVERSATION_TITLE,
|
||||
|
@ -68,7 +69,7 @@ interface UseConversation {
|
|||
}
|
||||
|
||||
export const useConversation = (): UseConversation => {
|
||||
const { setConversations } = useAssistantContext();
|
||||
const { allSystemPrompts, setConversations } = useAssistantContext();
|
||||
|
||||
/**
|
||||
* Append a message to the conversation[] for a given conversationId
|
||||
|
@ -135,10 +136,18 @@ export const useConversation = (): UseConversation => {
|
|||
(conversationId: string) => {
|
||||
setConversations((prev: Record<string, Conversation>) => {
|
||||
const prevConversation: Conversation | undefined = prev[conversationId];
|
||||
const defaultSystemPromptId = getDefaultSystemPrompt({
|
||||
allSystemPrompts,
|
||||
conversation: prevConversation,
|
||||
})?.id;
|
||||
|
||||
if (prevConversation != null) {
|
||||
const newConversation = {
|
||||
const newConversation: Conversation = {
|
||||
...prevConversation,
|
||||
apiConfig: {
|
||||
...prevConversation.apiConfig,
|
||||
defaultSystemPromptId,
|
||||
},
|
||||
messages: [],
|
||||
replacements: undefined,
|
||||
};
|
||||
|
@ -152,7 +161,7 @@ export const useConversation = (): UseConversation => {
|
|||
}
|
||||
});
|
||||
},
|
||||
[setConversations]
|
||||
[allSystemPrompts, setConversations]
|
||||
);
|
||||
|
||||
/**
|
||||
|
@ -160,8 +169,17 @@ export const useConversation = (): UseConversation => {
|
|||
*/
|
||||
const createConversation = useCallback(
|
||||
({ conversationId, messages }: CreateConversationProps): Conversation => {
|
||||
const defaultSystemPromptId = getDefaultSystemPrompt({
|
||||
allSystemPrompts,
|
||||
conversation: undefined,
|
||||
})?.id;
|
||||
|
||||
const newConversation: Conversation = {
|
||||
...DEFAULT_CONVERSATION_STATE,
|
||||
apiConfig: {
|
||||
...DEFAULT_CONVERSATION_STATE.apiConfig,
|
||||
defaultSystemPromptId,
|
||||
},
|
||||
id: conversationId,
|
||||
messages: messages != null ? messages : [],
|
||||
};
|
||||
|
@ -180,7 +198,7 @@ export const useConversation = (): UseConversation => {
|
|||
});
|
||||
return newConversation;
|
||||
},
|
||||
[setConversations]
|
||||
[allSystemPrompts, setConversations]
|
||||
);
|
||||
|
||||
/**
|
||||
|
|
|
@ -101,7 +101,6 @@ export interface UseAssistantContext {
|
|||
showAnonymizedValues: boolean;
|
||||
}) => EuiCommentProps[];
|
||||
http: HttpSetup;
|
||||
isSettingsModalVisible: boolean;
|
||||
localStorageLastConversationId: string | undefined;
|
||||
promptContexts: Record<string, PromptContext>;
|
||||
nameSpace: string;
|
||||
|
@ -112,7 +111,6 @@ export interface UseAssistantContext {
|
|||
setConversations: React.Dispatch<React.SetStateAction<Record<string, Conversation>>>;
|
||||
setDefaultAllow: React.Dispatch<React.SetStateAction<string[]>>;
|
||||
setDefaultAllowReplacement: React.Dispatch<React.SetStateAction<string[]>>;
|
||||
setIsSettingsModalVisible: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
setLastConversationId: React.Dispatch<React.SetStateAction<string | undefined>>;
|
||||
setSelectedSettingsTab: React.Dispatch<React.SetStateAction<SettingsTabs>>;
|
||||
setShowAssistantOverlay: (showAssistantOverlay: ShowAssistantOverlay) => void;
|
||||
|
@ -160,12 +158,6 @@ export const AssistantProvider: React.FC<AssistantProviderProps> = ({
|
|||
baseSystemPrompts
|
||||
);
|
||||
|
||||
// if basePrompt has been updated, the localstorage should be accordingly updated
|
||||
// if it exists
|
||||
useEffect(() => {
|
||||
setLocalStorageSystemPrompts(baseSystemPrompts);
|
||||
}, [baseSystemPrompts, setLocalStorageSystemPrompts]);
|
||||
|
||||
const [localStorageLastConversationId, setLocalStorageLastConversationId] =
|
||||
useLocalStorage<string>(`${nameSpace}.${LAST_CONVERSATION_ID_LOCAL_STORAGE_KEY}`);
|
||||
|
||||
|
@ -212,7 +204,6 @@ export const AssistantProvider: React.FC<AssistantProviderProps> = ({
|
|||
/**
|
||||
* Settings State
|
||||
*/
|
||||
const [isSettingsModalVisible, setIsSettingsModalVisible] = useState(false);
|
||||
const [selectedSettingsTab, setSelectedSettingsTab] = useState<SettingsTabs>(CONVERSATIONS_TAB);
|
||||
|
||||
const [conversations, setConversationsInternal] = useState(getInitialConversations());
|
||||
|
@ -264,7 +255,6 @@ export const AssistantProvider: React.FC<AssistantProviderProps> = ({
|
|||
docLinks,
|
||||
getComments,
|
||||
http,
|
||||
isSettingsModalVisible,
|
||||
promptContexts,
|
||||
nameSpace,
|
||||
registerPromptContext,
|
||||
|
@ -274,7 +264,6 @@ export const AssistantProvider: React.FC<AssistantProviderProps> = ({
|
|||
setConversations: onConversationsUpdated,
|
||||
setDefaultAllow,
|
||||
setDefaultAllowReplacement,
|
||||
setIsSettingsModalVisible,
|
||||
setSelectedSettingsTab,
|
||||
setShowAssistantOverlay,
|
||||
showAssistantOverlay,
|
||||
|
@ -298,7 +287,6 @@ export const AssistantProvider: React.FC<AssistantProviderProps> = ({
|
|||
docLinks,
|
||||
getComments,
|
||||
http,
|
||||
isSettingsModalVisible,
|
||||
localStorageLastConversationId,
|
||||
localStorageQuickPrompts,
|
||||
localStorageSystemPrompts,
|
||||
|
@ -309,7 +297,6 @@ export const AssistantProvider: React.FC<AssistantProviderProps> = ({
|
|||
selectedSettingsTab,
|
||||
setDefaultAllow,
|
||||
setDefaultAllowReplacement,
|
||||
setIsSettingsModalVisible,
|
||||
setLocalStorageLastConversationId,
|
||||
setLocalStorageQuickPrompts,
|
||||
setLocalStorageSystemPrompts,
|
||||
|
|
|
@ -13,6 +13,11 @@ import * as i18n from '../translations';
|
|||
import { useAssistantContext } from '../../assistant_context';
|
||||
import { CONVERSATIONS_TAB } from '../../assistant/settings/assistant_settings';
|
||||
|
||||
interface Props {
|
||||
isSettingsModalVisible: boolean;
|
||||
setIsSettingsModalVisible: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Error callout to be displayed when there is no connector configured for a conversation. Includes deep-link
|
||||
* to conversation settings to quickly resolve.
|
||||
|
@ -20,39 +25,40 @@ import { CONVERSATIONS_TAB } from '../../assistant/settings/assistant_settings';
|
|||
* TODO: Add 'quick fix' button to just pick a connector
|
||||
* TODO: Add setting for 'default connector' so we can auto-resolve and not even show this
|
||||
*/
|
||||
export const ConnectorMissingCallout: React.FC = React.memo(() => {
|
||||
const { isSettingsModalVisible, setIsSettingsModalVisible, setSelectedSettingsTab } =
|
||||
useAssistantContext();
|
||||
export const ConnectorMissingCallout: React.FC<Props> = React.memo(
|
||||
({ isSettingsModalVisible, setIsSettingsModalVisible }) => {
|
||||
const { setSelectedSettingsTab } = useAssistantContext();
|
||||
|
||||
const onConversationSettingsClicked = useCallback(() => {
|
||||
if (!isSettingsModalVisible) {
|
||||
setIsSettingsModalVisible(true);
|
||||
setSelectedSettingsTab(CONVERSATIONS_TAB);
|
||||
}
|
||||
}, [isSettingsModalVisible, setIsSettingsModalVisible, setSelectedSettingsTab]);
|
||||
const onConversationSettingsClicked = useCallback(() => {
|
||||
if (!isSettingsModalVisible) {
|
||||
setIsSettingsModalVisible(true);
|
||||
setSelectedSettingsTab(CONVERSATIONS_TAB);
|
||||
}
|
||||
}, [isSettingsModalVisible, setIsSettingsModalVisible, setSelectedSettingsTab]);
|
||||
|
||||
return (
|
||||
<EuiCallOut
|
||||
color="danger"
|
||||
iconType="controlsVertical"
|
||||
size="m"
|
||||
title={i18n.MISSING_CONNECTOR_CALLOUT_TITLE}
|
||||
>
|
||||
<p>
|
||||
{' '}
|
||||
<FormattedMessage
|
||||
defaultMessage="Select a connector from the {link} to continue"
|
||||
id="xpack.elasticAssistant.assistant.connectors.connectorMissingCallout.calloutDescription"
|
||||
values={{
|
||||
link: (
|
||||
<EuiLink onClick={onConversationSettingsClicked}>
|
||||
{i18n.MISSING_CONNECTOR_CONVERSATION_SETTINGS_LINK}
|
||||
</EuiLink>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</p>
|
||||
</EuiCallOut>
|
||||
);
|
||||
});
|
||||
return (
|
||||
<EuiCallOut
|
||||
color="danger"
|
||||
iconType="controlsVertical"
|
||||
size="m"
|
||||
title={i18n.MISSING_CONNECTOR_CALLOUT_TITLE}
|
||||
>
|
||||
<p>
|
||||
{' '}
|
||||
<FormattedMessage
|
||||
defaultMessage="Select a connector from the {link} to continue"
|
||||
id="xpack.elasticAssistant.assistant.connectors.connectorMissingCallout.calloutDescription"
|
||||
values={{
|
||||
link: (
|
||||
<EuiLink onClick={onConversationSettingsClicked}>
|
||||
{i18n.MISSING_CONNECTOR_CONVERSATION_SETTINGS_LINK}
|
||||
</EuiLink>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</p>
|
||||
</EuiCallOut>
|
||||
);
|
||||
}
|
||||
);
|
||||
ConnectorMissingCallout.displayName = 'ConnectorMissingCallout';
|
||||
|
|
|
@ -51,6 +51,26 @@ export const getPromptContextFromEventDetailsItem = (data: TimelineEventsDetails
|
|||
|
||||
const sendToTimelineEligibleQueryTypes: Array<CodeBlockDetails['type']> = ['kql', 'dsl', 'eql'];
|
||||
|
||||
/**
|
||||
* Returns message contents with replacements applied.
|
||||
*
|
||||
* @param message
|
||||
* @param replacements
|
||||
*/
|
||||
export const getMessageContentWithReplacements = ({
|
||||
messageContent,
|
||||
replacements,
|
||||
}: {
|
||||
messageContent: string;
|
||||
replacements: Record<string, string> | undefined;
|
||||
}): string =>
|
||||
replacements != null
|
||||
? Object.keys(replacements).reduce(
|
||||
(acc, replacement) => acc.replaceAll(replacement, replacements[replacement]),
|
||||
messageContent
|
||||
)
|
||||
: messageContent;
|
||||
|
||||
/**
|
||||
* Augments the messages in a conversation with code block details, including
|
||||
* the start and end indices of the code block in the message, the type of the
|
||||
|
@ -61,7 +81,14 @@ const sendToTimelineEligibleQueryTypes: Array<CodeBlockDetails['type']> = ['kql'
|
|||
export const augmentMessageCodeBlocks = (
|
||||
currentConversation: Conversation
|
||||
): CodeBlockDetails[][] => {
|
||||
const cbd = currentConversation.messages.map(({ content }) => analyzeMarkdown(content));
|
||||
const cbd = currentConversation.messages.map(({ content }) =>
|
||||
analyzeMarkdown(
|
||||
getMessageContentWithReplacements({
|
||||
messageContent: content,
|
||||
replacements: currentConversation.replacements,
|
||||
})
|
||||
)
|
||||
);
|
||||
|
||||
const output = cbd.map((codeBlocks, messageIndex) =>
|
||||
codeBlocks.map((codeBlock, codeBlockIndex) => {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue