mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 09:19:04 -04:00
# 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:
parent
3e1732087e
commit
ba8fc0ad2f
11 changed files with 365 additions and 286 deletions
|
@ -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));
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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}
|
||||
/>
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
|
@ -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}
|
||||
/>
|
||||
);
|
||||
}
|
|
@ -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({});
|
||||
|
|
|
@ -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 }}
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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": "アシスタントにメッセージを送信",
|
||||
|
|
|
@ -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": "向助手发送消息",
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue