[8.12] Update design of Prompt Editor (#173571) (#173636)

# Backport

This will backport the following commits from `main` to `8.12`:
- [Update design of Prompt Editor
(#173571)](https://github.com/elastic/kibana/pull/173571)

<!--- Backport version: 8.9.7 -->

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

<!--BACKPORT [{"author":{"name":"Coen
Warmer","email":"coen.warmer@gmail.com"},"sourceCommit":{"committedDate":"2023-12-19T14:14:16Z","message":"Update
design of Prompt Editor
(#173571)","sha":"3ea5865a0ab485782ddd62562890e7ac618768bf","branchLabelMapping":{"^v8.13.0$":"main","^v(\\d+).(\\d+).\\d+$":"$1.$2"}},"sourcePullRequest":{"labels":["release_note:skip","backport:prev-minor","v8.12.0","v8.13.0"],"number":173571,"url":"https://github.com/elastic/kibana/pull/173571","mergeCommit":{"message":"Update
design of Prompt Editor
(#173571)","sha":"3ea5865a0ab485782ddd62562890e7ac618768bf"}},"sourceBranch":"main","suggestedTargetBranches":["8.12"],"targetPullRequestStates":[{"branch":"8.12","label":"v8.12.0","labelRegex":"^v(\\d+).(\\d+).\\d+$","isSourceBranch":false,"state":"NOT_CREATED"},{"branch":"main","label":"v8.13.0","labelRegex":"^v8.13.0$","isSourceBranch":true,"state":"MERGED","url":"https://github.com/elastic/kibana/pull/173571","number":173571,"mergeCommit":{"message":"Update
design of Prompt Editor
(#173571)","sha":"3ea5865a0ab485782ddd62562890e7ac618768bf"}}]}]
BACKPORT-->

Co-authored-by: Coen Warmer <coen.warmer@gmail.com>
This commit is contained in:
Kibana Machine 2023-12-19 11:17:51 -05:00 committed by GitHub
parent 3e1732087e
commit ba8fc0ad2f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 365 additions and 286 deletions

View file

@ -81,6 +81,8 @@ const animClassName = css`
${euiThemeVars.euiAnimSlightBounce} ${euiThemeVars.euiAnimSpeedNormal} forwards;
`;
const PADDING_AND_BORDER = 32;
export function ChatBody({
initialTitle,
initialMessages,
@ -139,6 +141,8 @@ export function ChatBody({
const isAtBottom = (parent: HTMLElement) =>
parent.scrollTop + parent.clientHeight >= parent.scrollHeight;
const [promptEditorHeight, setPromptEditorHeight] = useState<number>(0);
const handleFeedback = (message: Message, feedback: Feedback) => {
if (conversation.value?.conversation && 'user' in conversation.value) {
sendEvent(chatService.analytics, {
@ -151,6 +155,14 @@ export function ChatBody({
}
};
const handleChangeHeight = (editorHeight: number) => {
if (editorHeight === 0) {
setPromptEditorHeight(0);
} else {
setPromptEditorHeight(editorHeight + PADDING_AND_BORDER);
}
};
useEffect(() => {
const parent = timelineContainerRef.current?.parentElement;
if (!parent) {
@ -198,8 +210,10 @@ export function ChatBody({
<EuiFlexItem grow={false}>
<EuiPanel hasBorder={false} hasShadow={false} paddingSize="m">
<ChatPromptEditor
hidden={connectors.loading || connectors.connectors?.length === 0}
loading={isLoading}
disabled
onChangeHeight={setPromptEditorHeight}
onSubmit={(message) => {
next(messages.concat(message));
}}
@ -284,23 +298,24 @@ export function ChatBody({
<EuiFlexItem
grow={false}
className={promptEditorClassname}
style={{
height: !connectors.loading && connectors.connectors?.length !== 0 ? 110 : 0,
}}
style={{ height: promptEditorHeight }}
>
<EuiHorizontalRule margin="none" />
<EuiPanel
hasBorder={false}
hasShadow={false}
paddingSize="m"
color="subdued"
className={promptEditorContainerClassName}
>
<ChatPromptEditor
disabled={!connectors.selectedConnector || !hasCorrectLicense}
hidden={connectors.loading || connectors.connectors?.length === 0}
loading={isLoading}
onSendTelemetry={(eventWithPayload) =>
sendEvent(chatService.analytics, eventWithPayload)
}
onChangeHeight={handleChangeHeight}
onSubmit={(message) => {
setStickToBottom(true);
return next(messages.concat(message));

View file

@ -41,6 +41,11 @@ const normalMessageClassName = css`
padding: 0;
}
.euiCommentEvent__header > .euiPanel {
padding-top: 4px;
padding-bottom: 4px;
}
/* targets .*euiTimelineItemEvent-top, makes sure text properly wraps and doesn't overflow */
> :last-child {
overflow-x: hidden;
@ -74,9 +79,7 @@ export function ChatItem({
element,
error,
loading,
message: {
message: { function_call: functionCall, role },
},
message,
title,
onActionClick,
onEditSubmit,
@ -115,9 +118,9 @@ export function ChatItem({
setEditing(!editing);
};
const handleInlineEditSubmit = (message: Message) => {
const handleInlineEditSubmit = (newMessage: Message) => {
handleToggleEdit();
return onEditSubmit(message);
return onEditSubmit(newMessage);
};
const handleCopyToClipboard = () => {
@ -127,10 +130,9 @@ export function ChatItem({
let contentElement: React.ReactNode =
content || loading || error ? (
<ChatItemContentInlinePromptEditor
content={content}
editing={editing}
functionCall={functionCall}
loading={loading}
message={message}
onSubmit={handleInlineEditSubmit}
onActionClick={onActionClick}
onSendTelemetry={onSendTelemetry}
@ -153,8 +155,10 @@ export function ChatItem({
return (
<EuiComment
timelineAvatar={<ChatItemAvatar loading={loading} currentUser={currentUser} role={role} />}
username={getRoleTranslation(role)}
timelineAvatar={
<ChatItemAvatar loading={loading} currentUser={currentUser} role={message.message.role} />
}
username={getRoleTranslation(message.message.role)}
event={title}
actions={
<ChatItemActions

View file

@ -8,44 +8,39 @@
import React from 'react';
import { MessageText } from '../message_panel/message_text';
import { ChatPromptEditor } from './chat_prompt_editor';
import { type Message, MessageRole } from '../../../common';
import type { Message } from '../../../common';
import type { ChatActionClickHandler } from './types';
import type { TelemetryEventTypeWithPayload } from '../../analytics';
interface Props {
content: string | undefined;
functionCall:
| {
name: string;
arguments?: string | undefined;
trigger: MessageRole;
}
| undefined;
loading: boolean;
editing: boolean;
loading: boolean;
message: Message;
onActionClick: ChatActionClickHandler;
onSendTelemetry: (eventWithPayload: TelemetryEventTypeWithPayload) => void;
onSubmit: (message: Message) => void;
}
export function ChatItemContentInlinePromptEditor({
content,
functionCall,
editing,
loading,
message,
onActionClick,
onSendTelemetry,
onSubmit,
}: Props) {
return !editing ? (
<MessageText content={content || ''} loading={loading} onActionClick={onActionClick} />
<MessageText
content={message.message.content || ''}
loading={loading}
onActionClick={onActionClick}
/>
) : (
<ChatPromptEditor
disabled={false}
hidden={false}
loading={false}
initialPrompt={content}
initialFunctionPayload={functionCall?.arguments}
initialSelectedFunctionName={functionCall?.name}
trigger={functionCall?.trigger}
initialMessage={message}
onChangeHeight={() => {}}
onSubmit={onSubmit}
onSendTelemetry={onSendTelemetry}
/>

View file

@ -7,162 +7,101 @@
import React, { useCallback, useEffect, useRef, useState } from 'react';
import { i18n } from '@kbn/i18n';
import {
EuiButtonEmpty,
EuiButtonIcon,
EuiFlexGroup,
EuiFlexItem,
EuiFocusTrap,
EuiPanel,
EuiSpacer,
EuiTextArea,
keys,
} from '@elastic/eui';
import { CodeEditor } from '@kbn/kibana-react-plugin/public';
import { EuiButtonIcon, EuiFlexGroup, EuiFlexItem, EuiFocusTrap, keys } from '@elastic/eui';
import { MessageRole, type Message } from '../../../common';
import { FunctionListPopover } from './function_list_popover';
import { useJsonEditorModel } from '../../hooks/use_json_editor_model';
import { TelemetryEventTypeWithPayload, TELEMETRY } from '../../analytics';
import { ChatPromptEditorFunction } from './chat_prompt_editor_function';
import { ChatPromptEditorPrompt } from './chat_prompt_editor_prompt';
export interface ChatPromptEditorProps {
disabled: boolean;
hidden: boolean;
loading: boolean;
initialPrompt?: string;
initialSelectedFunctionName?: string;
initialFunctionPayload?: string;
trigger?: MessageRole;
onSubmit: (message: Message) => void;
initialMessage?: Message;
onChangeHeight: (height: number) => void;
onSendTelemetry: (eventWithPayload: TelemetryEventTypeWithPayload) => void;
onSubmit: (message: Message) => void;
}
export function ChatPromptEditor({
disabled,
hidden,
loading,
initialPrompt,
initialSelectedFunctionName,
initialFunctionPayload,
onSubmit,
initialMessage,
onChangeHeight,
onSendTelemetry,
onSubmit,
}: ChatPromptEditorProps) {
const isFocusTrapEnabled = Boolean(initialPrompt);
const containerRef = useRef<HTMLDivElement>(null);
const [prompt, setPrompt] = useState(initialPrompt);
const isFocusTrapEnabled = Boolean(initialMessage?.message);
const [selectedFunctionName, setSelectedFunctionName] = useState<string | undefined>(
initialSelectedFunctionName
const [innerMessage, setInnerMessage] = useState<Message['message'] | undefined>(
initialMessage?.message
);
const [functionPayload, setFunctionPayload] = useState<string | undefined>(
initialFunctionPayload
const [mode, setMode] = useState<'prompt' | 'function'>(
initialMessage?.message.function_call?.name ? 'function' : 'prompt'
);
const [functionEditorLineCount, setFunctionEditorLineCount] = useState<number>(0);
const { model, initialJsonString } = useJsonEditorModel({
functionName: selectedFunctionName,
initialJson: functionPayload,
});
const textAreaRef = useRef<HTMLTextAreaElement>(null);
const handleChange = (event: React.ChangeEvent<HTMLTextAreaElement>) => {
setPrompt(event.currentTarget.value);
const handleChangeMessageInner = (newInnerMessage: Message['message']) => {
setInnerMessage(newInnerMessage);
};
const handleChangeFunctionPayload = (params: string) => {
setFunctionPayload(params);
recalculateFunctionEditorLineCount();
};
const handleClearSelection = () => {
setSelectedFunctionName(undefined);
setFunctionPayload('');
setPrompt('');
};
const handleSelectFunction = (functionName: string) => {
setPrompt('');
setFunctionPayload('');
setSelectedFunctionName(functionName);
};
const handleResizeTextArea = () => {
if (textAreaRef.current) {
textAreaRef.current.style.minHeight = 'auto';
textAreaRef.current.style.minHeight = textAreaRef.current?.scrollHeight + 'px';
}
};
const handleResetTextArea = () => {
if (textAreaRef.current) {
textAreaRef.current.style.minHeight = 'auto';
}
};
const recalculateFunctionEditorLineCount = useCallback(() => {
const newLineCount = model?.getLineCount() || 0;
if (newLineCount !== functionEditorLineCount) {
setFunctionEditorLineCount(newLineCount);
}
}, [functionEditorLineCount, model]);
const handleSubmit = useCallback(async () => {
if (loading || (!prompt?.trim() && !selectedFunctionName)) {
const handleSelectFunction = (func: string | undefined) => {
if (func) {
setMode('function');
setInnerMessage({
function_call: { name: func, trigger: MessageRole.Assistant },
role: MessageRole.User,
});
onChangeHeight(200);
return;
}
const currentPrompt = prompt;
const currentPayload = functionPayload;
setPrompt('');
setFunctionPayload(undefined);
handleResetTextArea();
setMode('prompt');
setInnerMessage(undefined);
let message: Message;
if (containerRef.current) {
onChangeHeight(containerRef.current.clientHeight);
}
};
const handleSubmit = useCallback(async () => {
if (loading || !innerMessage) {
return;
}
const oldMessage = innerMessage;
try {
if (selectedFunctionName) {
message = {
'@timestamp': new Date().toISOString(),
message: {
role: MessageRole.Assistant,
content: '',
function_call: {
name: selectedFunctionName,
trigger: MessageRole.User,
arguments: currentPayload,
},
},
};
onSubmit(message);
const message = {
'@timestamp': new Date().toISOString(),
message: innerMessage,
};
setFunctionPayload(undefined);
setSelectedFunctionName(undefined);
} else {
message = {
'@timestamp': new Date().toISOString(),
message: { role: MessageRole.User, content: currentPrompt },
};
onSubmit(message);
}
onSubmit(message);
setInnerMessage(undefined);
setMode('prompt');
onSendTelemetry({
type: TELEMETRY.observability_ai_assistant_user_sent_prompt_in_chat,
payload: message,
});
} catch (_) {
setPrompt(currentPrompt);
setInnerMessage(oldMessage);
setMode(oldMessage.function_call?.name ? 'function' : 'prompt');
}
}, [functionPayload, loading, onSendTelemetry, onSubmit, prompt, selectedFunctionName]);
useEffect(() => {
setFunctionPayload(initialJsonString);
}, [initialJsonString, selectedFunctionName]);
useEffect(() => {
recalculateFunctionEditorLineCount();
}, [model, recalculateFunctionEditorLineCount]);
}, [innerMessage, loading, onSendTelemetry, onSubmit]);
// Submit on Enter
useEffect(() => {
const keyboardListener = (event: KeyboardEvent) => {
if (!event.shiftKey && event.key === keys.ENTER && (prompt || selectedFunctionName)) {
if (!event.shiftKey && event.key === keys.ENTER && innerMessage) {
event.preventDefault();
handleSubmit();
}
@ -173,139 +112,58 @@ export function ChatPromptEditor({
return () => {
window.removeEventListener('keypress', keyboardListener);
};
}, [handleSubmit, prompt, selectedFunctionName]);
}, [handleSubmit, innerMessage]);
useEffect(() => {
const textarea = textAreaRef.current;
if (textarea) {
textarea.focus();
textarea.addEventListener('input', handleResizeTextArea, false);
if (hidden) {
onChangeHeight(0);
}
return () => {
textarea?.removeEventListener('input', handleResizeTextArea, false);
};
});
useEffect(() => {
handleResizeTextArea();
}, []);
}, [hidden, onChangeHeight]);
return (
<EuiFocusTrap disabled={!isFocusTrapEnabled}>
<EuiFlexGroup gutterSize="s" responsive={false}>
<EuiFlexItem grow>
<EuiFlexGroup direction="column" gutterSize="s">
<EuiFlexItem>
<EuiFlexGroup responsive={false}>
<EuiFlexItem grow>
<FunctionListPopover
selectedFunctionName={selectedFunctionName}
onSelectFunction={handleSelectFunction}
disabled={loading || disabled}
/>
</EuiFlexItem>
<EuiFlexItem grow={false}>
{selectedFunctionName ? (
<EuiButtonEmpty
data-test-subj="observabilityAiAssistantChatPromptEditorEmptySelectionButton"
iconType="cross"
iconSide="right"
size="xs"
disabled={loading || disabled}
onClick={handleClearSelection}
>
{i18n.translate('xpack.observabilityAiAssistant.prompt.emptySelection', {
defaultMessage: 'Empty selection',
})}
</EuiButtonEmpty>
) : null}
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlexItem>
<EuiFlexItem>
{selectedFunctionName ? (
<EuiPanel borderRadius="none" color="subdued" hasShadow={false} paddingSize="xs">
<CodeEditor
aria-label={i18n.translate(
'xpack.observabilityAiAssistant.chatPromptEditor.codeEditor.payloadEditorLabel',
{ defaultMessage: 'payloadEditor' }
)}
data-test-subj="observabilityAiAssistantChatPromptEditorCodeEditor"
fullWidth
height={functionEditorLineCount > 8 ? '200px' : '120px'}
languageId="json"
isCopyable
languageConfiguration={{
autoClosingPairs: [
{
open: '{',
close: '}',
},
],
}}
editorDidMount={(editor) => {
editor.focus();
}}
options={{
accessibilitySupport: 'off',
acceptSuggestionOnEnter: 'on',
automaticLayout: true,
autoClosingQuotes: 'always',
autoIndent: 'full',
contextmenu: true,
fontSize: 12,
formatOnPaste: true,
formatOnType: true,
inlineHints: { enabled: true },
lineNumbers: 'on',
minimap: { enabled: false },
model,
overviewRulerBorder: false,
quickSuggestions: true,
scrollbar: { alwaysConsumeMouseWheel: false },
scrollBeyondLastLine: false,
suggestOnTriggerCharacters: true,
tabSize: 2,
wordWrap: 'on',
wrappingIndent: 'indent',
}}
transparentBackground
value={functionPayload || ''}
onChange={handleChangeFunctionPayload}
/>
</EuiPanel>
) : (
<EuiTextArea
data-test-subj="observabilityAiAssistantChatPromptEditorTextArea"
css={{ maxHeight: 200 }}
disabled={disabled}
fullWidth
inputRef={textAreaRef}
placeholder={i18n.translate('xpack.observabilityAiAssistant.prompt.placeholder', {
defaultMessage: 'Send a message to the Assistant',
})}
resize="vertical"
rows={1}
value={prompt}
onChange={handleChange}
/>
)}
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlexItem>
<EuiFlexGroup gutterSize="s" responsive={false} alignItems="center" ref={containerRef}>
<EuiFlexItem grow={false}>
<FunctionListPopover
mode={mode}
selectedFunctionName={innerMessage?.function_call?.name}
onSelectFunction={handleSelectFunction}
disabled={loading || disabled}
/>
</EuiFlexItem>
<EuiFlexItem>
{mode === 'function' && innerMessage?.function_call?.name ? (
<ChatPromptEditorFunction
functionName={innerMessage.function_call.name}
functionPayload={innerMessage.function_call.arguments}
onChange={handleChangeMessageInner}
/>
) : (
<ChatPromptEditorPrompt
disabled={disabled}
prompt={innerMessage?.content}
onChange={handleChangeMessageInner}
onChangeHeight={onChangeHeight}
/>
)}
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiSpacer size="xl" />
<EuiButtonIcon
data-test-subj="observabilityAiAssistantChatPromptEditorButtonIcon"
aria-label={i18n.translate(
'xpack.observabilityAiAssistant.chatPromptEditor.euiButtonIcon.submitLabel',
{ defaultMessage: 'Submit' }
)}
disabled={selectedFunctionName ? false : !prompt?.trim() || loading || disabled}
disabled={loading || disabled}
display={
selectedFunctionName ? (functionPayload ? 'fill' : 'base') : prompt ? 'fill' : 'base'
mode === 'function'
? innerMessage?.function_call?.name
? 'fill'
: 'base'
: innerMessage?.content
? 'fill'
: 'base'
}
iconType="kqlFunction"
isLoading={loading}

View file

@ -0,0 +1,122 @@
/*
* 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, { useCallback, useEffect, useState } from 'react';
import { CodeEditor } from '@kbn/kibana-react-plugin/public';
import { i18n } from '@kbn/i18n';
import usePrevious from 'react-use/lib/usePrevious';
import { EuiCode, EuiPanel } from '@elastic/eui';
import { useJsonEditorModel } from '../../hooks/use_json_editor_model';
import { type Message, MessageRole } from '../../../common';
export interface Props {
functionName: string;
functionPayload?: string;
onChange: (message: Message['message']) => void;
}
export function ChatPromptEditorFunction({ functionName, functionPayload, onChange }: Props) {
const [functionEditorLineCount, setFunctionEditorLineCount] = useState<number>(0);
const previousPayload = usePrevious(functionPayload);
const { model, initialJsonString } = useJsonEditorModel({
functionName,
initialJson: functionPayload,
});
const handleChangeFunctionPayload = (params: string) => {
recalculateFunctionEditorLineCount();
onChange({
role: MessageRole.Assistant,
content: '',
function_call: {
name: functionName,
trigger: MessageRole.User,
arguments: params,
},
});
};
const recalculateFunctionEditorLineCount = useCallback(() => {
const newLineCount = model?.getLineCount() || 0;
if (newLineCount !== functionEditorLineCount) {
setFunctionEditorLineCount(newLineCount + 1);
}
}, [functionEditorLineCount, model]);
useEffect(() => {
recalculateFunctionEditorLineCount();
}, [model, recalculateFunctionEditorLineCount]);
useEffect(() => {
if (previousPayload === undefined && initialJsonString) {
onChange({
role: MessageRole.Assistant,
content: '',
function_call: {
name: functionName,
trigger: MessageRole.User,
arguments: initialJsonString,
},
});
}
}, [functionName, functionPayload, initialJsonString, onChange, previousPayload]);
return (
<EuiPanel paddingSize="none">
<EuiCode>{functionName}</EuiCode>
<CodeEditor
aria-label={i18n.translate(
'xpack.observabilityAiAssistant.chatPromptEditor.codeEditor.payloadEditorLabel',
{ defaultMessage: 'payloadEditor' }
)}
data-test-subj="observabilityAiAssistantChatPromptEditorCodeEditor"
fullWidth
height={'180px'}
languageId="json"
isCopyable
languageConfiguration={{
autoClosingPairs: [
{
open: '{',
close: '}',
},
],
}}
editorDidMount={(editor) => {
editor.focus();
}}
options={{
accessibilitySupport: 'off',
acceptSuggestionOnEnter: 'on',
automaticLayout: true,
autoClosingQuotes: 'always',
autoIndent: 'full',
contextmenu: true,
fontSize: 12,
formatOnPaste: true,
formatOnType: true,
inlineHints: { enabled: true },
lineNumbers: 'on',
minimap: { enabled: false },
model,
overviewRulerBorder: false,
quickSuggestions: true,
scrollbar: { alwaysConsumeMouseWheel: false },
scrollBeyondLastLine: false,
suggestOnTriggerCharacters: true,
tabSize: 2,
wordWrap: 'on',
wrappingIndent: 'indent',
}}
transparentBackground
value={functionPayload || ''}
onChange={handleChangeFunctionPayload}
/>
</EuiPanel>
);
}

View file

@ -0,0 +1,70 @@
/*
* 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, { useCallback, useEffect, useRef } from 'react';
import { EuiTextArea } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { type Message, MessageRole } from '../../../common';
interface Props {
disabled: boolean;
prompt: string | undefined;
onChange: (message: Message['message']) => void;
onChangeHeight: (height: number) => void;
}
export function ChatPromptEditorPrompt({ disabled, prompt, onChange, onChangeHeight }: Props) {
const textAreaRef = useRef<HTMLTextAreaElement>(null);
const handleChange = (event: React.ChangeEvent<HTMLTextAreaElement>) => {
handleResizeTextArea();
onChange({
role: MessageRole.User,
content: event.currentTarget.value,
});
};
const handleResizeTextArea = useCallback(() => {
if (textAreaRef.current) {
textAreaRef.current.style.minHeight = 'auto';
textAreaRef.current.style.minHeight = textAreaRef.current?.scrollHeight + 'px';
}
if (textAreaRef.current?.scrollHeight) {
onChangeHeight(textAreaRef.current.scrollHeight);
}
}, [onChangeHeight]);
useEffect(() => {
const textarea = textAreaRef.current;
if (textarea) {
textarea.focus();
}
}, [handleResizeTextArea]);
useEffect(() => {
handleResizeTextArea();
}, [handleResizeTextArea]);
return (
<EuiTextArea
data-test-subj="observabilityAiAssistantChatPromptEditorTextArea"
css={{ maxHeight: 200 }}
disabled={disabled}
fullWidth
inputRef={textAreaRef}
placeholder={i18n.translate('xpack.observabilityAiAssistant.prompt.placeholder', {
defaultMessage: 'Send a message to the Assistant',
})}
resize="vertical"
rows={1}
value={prompt || ''}
onChange={handleChange}
/>
);
}

View file

@ -25,6 +25,8 @@ const Template: ComponentStory<typeof Component> = (props: FunctionListPopover)
const defaultProps: FunctionListPopover = {
onSelectFunction: () => {},
disabled: false,
mode: 'prompt',
selectedFunctionName: 'foo',
};
export const ConversationList = Template.bind({});

View file

@ -8,7 +8,7 @@
import React, { useEffect, useRef, useState } from 'react';
import {
EuiBetaBadge,
EuiButtonEmpty,
EuiButtonIcon,
EuiFlexGroup,
EuiFlexItem,
EuiHighlight,
@ -16,6 +16,7 @@ import {
EuiSelectable,
EuiSelectableOption,
EuiText,
EuiToolTip,
} from '@elastic/eui';
import type { EuiSelectableOptionCheckedType } from '@elastic/eui/src/components/selectable/selectable_option';
import { i18n } from '@kbn/i18n';
@ -28,12 +29,14 @@ interface FunctionListOption {
}
export function FunctionListPopover({
mode,
selectedFunctionName,
onSelectFunction,
disabled,
}: {
mode: 'prompt' | 'function';
selectedFunctionName?: string;
onSelectFunction: (func: string) => void;
onSelectFunction: (func: string | undefined) => void;
disabled: boolean;
}) {
const { getFunctions } = useObservabilityAIAssistantChatService();
@ -48,6 +51,12 @@ export function FunctionListPopover({
const [isFunctionListOpen, setIsFunctionListOpen] = useState(false);
const handleClickFunctionList = () => {
if (selectedFunctionName) {
onSelectFunction(undefined);
setIsFunctionListOpen(false);
return;
}
setIsFunctionListOpen(!isFunctionListOpen);
};
@ -118,20 +127,30 @@ export function FunctionListPopover({
<EuiPopover
anchorPosition="downLeft"
button={
<EuiButtonEmpty
data-test-subj="observabilityAiAssistantFunctionListPopoverButton"
iconType="arrowRight"
iconSide="right"
size="xs"
onClick={handleClickFunctionList}
disabled={disabled}
<EuiToolTip
content={
mode === 'prompt'
? i18n.translate(
'xpack.observabilityAiAssistant.functionListPopover.euiToolTip.selectAFunctionLabel',
{ defaultMessage: 'Select a function' }
)
: i18n.translate(
'xpack.observabilityAiAssistant.functionListPopover.euiToolTip.clearFunction',
{
defaultMessage: 'Clear function',
}
)
}
display="block"
>
{selectedFunctionName
? selectedFunctionName
: i18n.translate('xpack.observabilityAiAssistant.prompt.functionList.callFunction', {
defaultMessage: 'Call function',
})}
</EuiButtonEmpty>
<EuiButtonIcon
data-test-subj="observabilityAiAssistantFunctionListPopoverButton"
disabled={disabled}
iconType={selectedFunctionName ? 'cross' : 'plusInCircle'}
size="xs"
onClick={handleClickFunctionList}
/>
</EuiToolTip>
}
closePopover={handleClickFunctionList}
css={{ maxWidth: 400 }}

View file

@ -29673,8 +29673,6 @@
"xpack.observabilityAiAssistant.missingCredentialsCallout.description": "Vous n'avez pas autorisé OpenAI de manière à ce que l'assistant d'Elastic puisse formuler des réponses. Autoriser le modèle afin de continuer.",
"xpack.observabilityAiAssistant.missingCredentialsCallout.title": "Informations d'identification manquantes",
"xpack.observabilityAiAssistant.newChatButton": "Nouveau chat",
"xpack.observabilityAiAssistant.prompt.emptySelection": "Sélection vide",
"xpack.observabilityAiAssistant.prompt.functionList.callFunction": "Fonction d'appel",
"xpack.observabilityAiAssistant.prompt.functionList.filter": "Filtre",
"xpack.observabilityAiAssistant.prompt.functionList.functionList": "Liste de fonctions",
"xpack.observabilityAiAssistant.prompt.placeholder": "Envoyer un message à l'assistant",

View file

@ -29673,8 +29673,6 @@
"xpack.observabilityAiAssistant.missingCredentialsCallout.description": "Elastic Assistantからの応答を生成するために、OpenAIを許可していません。続行するには、モデルを許可してください。",
"xpack.observabilityAiAssistant.missingCredentialsCallout.title": "資格情報がありません",
"xpack.observabilityAiAssistant.newChatButton": "新しいチャット",
"xpack.observabilityAiAssistant.prompt.emptySelection": "空の選択",
"xpack.observabilityAiAssistant.prompt.functionList.callFunction": "関数を呼び出し",
"xpack.observabilityAiAssistant.prompt.functionList.filter": "フィルター",
"xpack.observabilityAiAssistant.prompt.functionList.functionList": "関数リスト",
"xpack.observabilityAiAssistant.prompt.placeholder": "アシスタントにメッセージを送信",

View file

@ -29670,8 +29670,6 @@
"xpack.observabilityAiAssistant.missingCredentialsCallout.description": "您尚未授权 OpenAI 以从 Elastic 助手生成响应。授权模型以继续。",
"xpack.observabilityAiAssistant.missingCredentialsCallout.title": "凭据缺失",
"xpack.observabilityAiAssistant.newChatButton": "新聊天",
"xpack.observabilityAiAssistant.prompt.emptySelection": "选择为空",
"xpack.observabilityAiAssistant.prompt.functionList.callFunction": "调用函数",
"xpack.observabilityAiAssistant.prompt.functionList.filter": "筛选",
"xpack.observabilityAiAssistant.prompt.functionList.functionList": "函数列表",
"xpack.observabilityAiAssistant.prompt.placeholder": "向助手发送消息",