diff --git a/x-pack/plugins/observability_ai_assistant/public/components/chat/chat_body.tsx b/x-pack/plugins/observability_ai_assistant/public/components/chat/chat_body.tsx
index 64375b0bd9a98..5b7e6b509e69f 100644
--- a/x-pack/plugins/observability_ai_assistant/public/components/chat/chat_body.tsx
+++ b/x-pack/plugins/observability_ai_assistant/public/components/chat/chat_body.tsx
@@ -28,7 +28,7 @@ import type { UseGenAIConnectorsResult } from '../../hooks/use_genai_connectors'
import type { UseKnowledgeBaseResult } from '../../hooks/use_knowledge_base';
import { type Conversation, type Message, MessageRole } from '../../../common/types';
import { ChatHeader } from './chat_header';
-import { ChatPromptEditor } from './chat_prompt_editor';
+import { PromptEditor } from '../prompt_editor/prompt_editor';
import { ChatTimeline } from './chat_timeline';
import { Feedback } from '../feedback_buttons';
import { IncorrectLicensePanel } from './incorrect_license_panel';
@@ -214,7 +214,7 @@ export function ChatBody({
- {
+ setStickToBottom(true);
const indexOf = messages.indexOf(editedMessage);
next(messages.slice(0, indexOf).concat(newMessage));
}}
@@ -313,14 +314,14 @@ export function ChatBody({
color="subdued"
className={promptEditorContainerClassName}
>
-
sendEvent(chatService.analytics, eventWithPayload)
}
- onChangeHeight={handleChangeHeight}
onSubmit={(message) => {
setStickToBottom(true);
return next(messages.concat(message));
diff --git a/x-pack/plugins/observability_ai_assistant/public/components/chat/chat_item.tsx b/x-pack/plugins/observability_ai_assistant/public/components/chat/chat_item.tsx
index 9f581b31795f1..ea8cc1eb17911 100644
--- a/x-pack/plugins/observability_ai_assistant/public/components/chat/chat_item.tsx
+++ b/x-pack/plugins/observability_ai_assistant/public/components/chat/chat_item.tsx
@@ -127,6 +127,7 @@ export function ChatItem({
const handleInlineEditSubmit = (newMessage: Message) => {
handleToggleEdit();
+
return onEditSubmit(newMessage);
};
diff --git a/x-pack/plugins/observability_ai_assistant/public/components/chat/chat_item_content_inline_prompt_editor.tsx b/x-pack/plugins/observability_ai_assistant/public/components/chat/chat_item_content_inline_prompt_editor.tsx
index a01032e8711c5..ea24804833e46 100644
--- a/x-pack/plugins/observability_ai_assistant/public/components/chat/chat_item_content_inline_prompt_editor.tsx
+++ b/x-pack/plugins/observability_ai_assistant/public/components/chat/chat_item_content_inline_prompt_editor.tsx
@@ -6,10 +6,11 @@
*/
import React from 'react';
-import { EuiPanel } from '@elastic/eui';
+import { noop } from 'lodash';
import { css } from '@emotion/css';
+import { EuiPanel } from '@elastic/eui';
import { MessageText } from '../message_panel/message_text';
-import { ChatPromptEditor } from './chat_prompt_editor';
+import { PromptEditor } from '../prompt_editor/prompt_editor';
import type { Message } from '../../../common';
import type { ChatActionClickHandler } from './types';
import type { TelemetryEventTypeWithPayload } from '../../analytics';
@@ -59,14 +60,14 @@ export function ChatItemContentInlinePromptEditor({
hasShadow={false}
className={editorContainerClassName}
>
- {}}
+ onChangeHeight={noop}
onSubmit={onSubmit}
onSendTelemetry={onSendTelemetry}
/>
diff --git a/x-pack/plugins/observability_ai_assistant/public/components/chat/chat_timeline.tsx b/x-pack/plugins/observability_ai_assistant/public/components/chat/chat_timeline.tsx
index afee49619fd24..a07c1f4f55586 100644
--- a/x-pack/plugins/observability_ai_assistant/public/components/chat/chat_timeline.tsx
+++ b/x-pack/plugins/observability_ai_assistant/public/components/chat/chat_timeline.tsx
@@ -120,9 +120,9 @@ export function ChatTimeline({
key={index}
consolidatedItem={item}
onActionClick={onActionClick}
+ onEditSubmit={onEdit}
onFeedback={onFeedback}
onRegenerate={onRegenerate}
- onEditSubmit={onEdit}
onSendTelemetry={onSendTelemetry}
onStopGenerating={onStopGenerating}
/>
diff --git a/x-pack/plugins/observability_ai_assistant/public/components/chat/function_list_popover.tsx b/x-pack/plugins/observability_ai_assistant/public/components/chat/function_list_popover.tsx
index e3c5f27e7bfe7..8ec32e35a66b9 100644
--- a/x-pack/plugins/observability_ai_assistant/public/components/chat/function_list_popover.tsx
+++ b/x-pack/plugins/observability_ai_assistant/public/components/chat/function_list_popover.tsx
@@ -5,7 +5,7 @@
* 2.0.
*/
-import React, { useEffect, useRef, useState } from 'react';
+import React, { useEffect, useState } from 'react';
import {
EuiBetaBadge,
EuiButtonIcon,
@@ -42,8 +42,6 @@ export function FunctionListPopover({
const { getFunctions } = useObservabilityAIAssistantChatService();
const functions = getFunctions();
- const filterRef = useRef(null);
-
const [functionOptions, setFunctionOptions] = useState<
Array>
>(mapFunctions({ functions, selectedFunctionName }));
@@ -65,26 +63,6 @@ export function FunctionListPopover({
onSelectFunction(func.label);
};
- useEffect(() => {
- const keyboardListener = (event: KeyboardEvent) => {
- if (event.shiftKey && event.code === 'Digit4') {
- setIsFunctionListOpen(true);
- }
- };
-
- window.addEventListener('keyup', keyboardListener);
-
- return () => {
- window.removeEventListener('keyup', keyboardListener);
- };
- }, []);
-
- useEffect(() => {
- if (isFunctionListOpen && filterRef.current) {
- filterRef.current.focus();
- }
- }, [isFunctionListOpen]);
-
useEffect(() => {
const options = mapFunctions({ functions, selectedFunctionName });
if (options.length !== functionOptions.length) {
@@ -92,42 +70,12 @@ export function FunctionListPopover({
}
}, [functionOptions.length, functions, selectedFunctionName]);
- const renderFunctionOption = (
- option: EuiSelectableOption,
- searchValue: string
- ) => {
- return (
-
-
-
-
-
-
- {option.label}{' '}
-
-
-
-
-
-
-
-
-
- {option.searchableLabel || ''}
-
-
-
- );
- };
-
return (
@@ -173,7 +126,7 @@ export function FunctionListPopover({
searchable
searchProps={{
'data-test-subj': 'searchFiltersList',
- inputRef: (node) => (filterRef.current = node),
+ id: 'searchFilterList',
placeholder: i18n.translate('xpack.observabilityAiAssistant.prompt.functionList.filter', {
defaultMessage: 'Filter',
}),
@@ -215,3 +168,34 @@ function mapFunctions({
: ('off' as EuiSelectableOptionCheckedType),
}));
}
+
+function renderFunctionOption(
+ option: EuiSelectableOption,
+ searchValue: string
+) {
+ return (
+
+
+
+
+
+
+ {option.label}{' '}
+
+
+
+
+
+
+
+
+
+ {option.searchableLabel || ''}
+
+
+
+ );
+}
diff --git a/x-pack/plugins/observability_ai_assistant/public/components/chat/chat_prompt_editor.stories.tsx b/x-pack/plugins/observability_ai_assistant/public/components/prompt_editor/prompt_editor.stories.tsx
similarity index 66%
rename from x-pack/plugins/observability_ai_assistant/public/components/chat/chat_prompt_editor.stories.tsx
rename to x-pack/plugins/observability_ai_assistant/public/components/prompt_editor/prompt_editor.stories.tsx
index eeb01da9b7d04..054d23b56e6de 100644
--- a/x-pack/plugins/observability_ai_assistant/public/components/chat/chat_prompt_editor.stories.tsx
+++ b/x-pack/plugins/observability_ai_assistant/public/components/prompt_editor/prompt_editor.stories.tsx
@@ -7,17 +7,17 @@
import React from 'react';
import { ComponentStory } from '@storybook/react';
-import { ChatPromptEditor as Component, ChatPromptEditorProps } from './chat_prompt_editor';
+import { PromptEditor as Component, PromptEditorProps } from './prompt_editor';
import { KibanaReactStorybookDecorator } from '../../utils/storybook_decorator';
/*
- JSON Schema validation in the ChatPromptEditor compponent does not work
+ JSON Schema validation in the PromptEditor compponent does not work
when rendering the component from within Storybook.
*/
export default {
component: Component,
- title: 'app/Molecules/ChatPromptEditor',
+ title: 'app/Molecules/PromptEditor',
argTypes: {},
parameters: {
backgrounds: {
@@ -28,11 +28,11 @@ export default {
decorators: [KibanaReactStorybookDecorator],
};
-const Template: ComponentStory = (props: ChatPromptEditorProps) => {
+const Template: ComponentStory = (props: PromptEditorProps) => {
return ;
};
const defaultProps = {};
-export const ChatPromptEditor = Template.bind({});
-ChatPromptEditor.args = defaultProps;
+export const PromptEditor = Template.bind({});
+PromptEditor.args = defaultProps;
diff --git a/x-pack/plugins/observability_ai_assistant/public/components/chat/chat_prompt_editor.tsx b/x-pack/plugins/observability_ai_assistant/public/components/prompt_editor/prompt_editor.tsx
similarity index 51%
rename from x-pack/plugins/observability_ai_assistant/public/components/chat/chat_prompt_editor.tsx
rename to x-pack/plugins/observability_ai_assistant/public/components/prompt_editor/prompt_editor.tsx
index 4b35d1ee11432..7d2c438daec6e 100644
--- a/x-pack/plugins/observability_ai_assistant/public/components/chat/chat_prompt_editor.tsx
+++ b/x-pack/plugins/observability_ai_assistant/public/components/prompt_editor/prompt_editor.tsx
@@ -5,18 +5,16 @@
* 2.0.
*/
-import React, { useCallback, useEffect, useRef, useState } from 'react';
+import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { i18n } from '@kbn/i18n';
-import { EuiButtonIcon, EuiFlexGroup, EuiFlexItem, EuiFocusTrap, keys } from '@elastic/eui';
-
+import { EuiButtonIcon, EuiFlexGroup, EuiFlexItem, keys } from '@elastic/eui';
import { MessageRole, type Message } from '../../../common';
-import { FunctionListPopover } from './function_list_popover';
-
+import { FunctionListPopover } from '../chat/function_list_popover';
import { TelemetryEventTypeWithPayload, TELEMETRY } from '../../analytics';
-import { ChatPromptEditorFunction } from './chat_prompt_editor_function';
-import { ChatPromptEditorPrompt } from './chat_prompt_editor_prompt';
+import { PromptEditorFunction } from './prompt_editor_function';
+import { PromptEditorNaturalLanguage } from './prompt_editor_natural_language';
-export interface ChatPromptEditorProps {
+export interface PromptEditorProps {
disabled: boolean;
hidden: boolean;
loading: boolean;
@@ -28,7 +26,7 @@ export interface ChatPromptEditorProps {
onSubmit: (message: Message) => void;
}
-export function ChatPromptEditor({
+export function PromptEditor({
disabled,
hidden,
loading,
@@ -38,15 +36,15 @@ export function ChatPromptEditor({
onChangeHeight,
onSendTelemetry,
onSubmit,
-}: ChatPromptEditorProps) {
+}: PromptEditorProps) {
const containerRef = useRef(null);
- const isFocusTrapEnabled = Boolean(initialContent || initialFunctionCall);
-
const [mode, setMode] = useState<'prompt' | 'function'>(
initialFunctionCall?.name ? 'function' : 'prompt'
);
+ const [hasFocus, setHasFocus] = useState(false);
+
const initialInnerMessage = initialRole
? {
role: initialRole,
@@ -59,6 +57,19 @@ export function ChatPromptEditor({
initialInnerMessage
);
+ const invalid = useMemo(() => {
+ let isInvalid = false;
+
+ if (innerMessage?.function_call?.name && innerMessage?.function_call?.arguments) {
+ try {
+ JSON.parse(innerMessage.function_call.arguments);
+ } catch (e) {
+ isInvalid = true;
+ }
+ return isInvalid;
+ }
+ }, [innerMessage?.function_call?.arguments, innerMessage?.function_call?.name]);
+
const handleChangeMessageInner = (newInnerMessage: Message['message']) => {
setInnerMessage(newInnerMessage);
};
@@ -113,9 +124,11 @@ export function ChatPromptEditor({
// Submit on Enter
useEffect(() => {
const keyboardListener = (event: KeyboardEvent) => {
- if (!event.shiftKey && event.key === keys.ENTER && innerMessage) {
- event.preventDefault();
- handleSubmit();
+ if (innerMessage && !disabled && !invalid && hasFocus) {
+ if (!event.shiftKey && event.key === keys.ENTER) {
+ event.preventDefault();
+ handleSubmit();
+ }
}
};
@@ -124,7 +137,7 @@ export function ChatPromptEditor({
return () => {
window.removeEventListener('keypress', keyboardListener);
};
- }, [handleSubmit, innerMessage]);
+ }, [disabled, handleSubmit, hasFocus, innerMessage, invalid]);
useEffect(() => {
if (hidden) {
@@ -133,57 +146,59 @@ export function ChatPromptEditor({
}, [hidden, onChangeHeight]);
return (
-
-
-
-
+
+
+
+
+ {mode === 'function' && innerMessage?.function_call?.name ? (
+ setHasFocus(true)}
+ onBlur={() => setHasFocus(false)}
/>
-
-
- {mode === 'function' && innerMessage?.function_call?.name ? (
-
- ) : (
-
+ ) : (
+ setHasFocus(true)}
+ onBlur={() => setHasFocus(false)}
+ />
+ )}
+
+
+
+
-
-
-
-
-
-
+ : innerMessage?.content
+ ? 'fill'
+ : 'base'
+ }
+ iconType="kqlFunction"
+ isLoading={loading}
+ size="m"
+ onClick={handleSubmit}
+ />
+
+
);
}
diff --git a/x-pack/plugins/observability_ai_assistant/public/components/chat/chat_prompt_editor_function.tsx b/x-pack/plugins/observability_ai_assistant/public/components/prompt_editor/prompt_editor_function.tsx
similarity index 82%
rename from x-pack/plugins/observability_ai_assistant/public/components/chat/chat_prompt_editor_function.tsx
rename to x-pack/plugins/observability_ai_assistant/public/components/prompt_editor/prompt_editor_function.tsx
index 74e5d63702c4d..f7cbb55782dd7 100644
--- a/x-pack/plugins/observability_ai_assistant/public/components/chat/chat_prompt_editor_function.tsx
+++ b/x-pack/plugins/observability_ai_assistant/public/components/prompt_editor/prompt_editor_function.tsx
@@ -4,12 +4,13 @@
* 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 React, { useCallback, useEffect, useRef, useState } from 'react';
+import usePrevious from 'react-use/lib/usePrevious';
+import { css } from '@emotion/css';
import { CodeEditor } from '@kbn/code-editor';
+import { monaco } from '@kbn/monaco';
import { i18n } from '@kbn/i18n';
-import usePrevious from 'react-use/lib/usePrevious';
import { EuiCode, EuiPanel } from '@elastic/eui';
-import { css } from '@emotion/css';
import { useJsonEditorModel } from '../../hooks/use_json_editor_model';
import { type Message, MessageRole } from '../../../common';
@@ -17,13 +18,22 @@ export interface Props {
functionName: string;
functionPayload?: string;
onChange: (message: Message['message']) => void;
+ onFocus: () => void;
+ onBlur: () => void;
}
const functionNameClassName = css`
display: inline-block;
`;
-export function ChatPromptEditorFunction({ functionName, functionPayload, onChange }: Props) {
+export function PromptEditorFunction({
+ functionName,
+ functionPayload,
+ onChange,
+ onFocus,
+ onBlur,
+}: Props) {
+ const editorRef = useRef(null);
const [functionEditorLineCount, setFunctionEditorLineCount] = useState(0);
const previousPayload = usePrevious(functionPayload);
@@ -33,8 +43,8 @@ export function ChatPromptEditorFunction({ functionName, functionPayload, onChan
initialJson: functionPayload,
});
- const handleChangeFunctionPayload = (params: string) => {
- recalculateFunctionEditorLineCount();
+ const handleChangePayload = (args: string) => {
+ recalculateLineCount();
onChange({
role: MessageRole.Assistant,
@@ -42,12 +52,12 @@ export function ChatPromptEditorFunction({ functionName, functionPayload, onChan
function_call: {
name: functionName,
trigger: MessageRole.User,
- arguments: params,
+ arguments: args,
},
});
};
- const recalculateFunctionEditorLineCount = useCallback(() => {
+ const recalculateLineCount = useCallback(() => {
const newLineCount = model?.getLineCount() || 0;
if (newLineCount !== functionEditorLineCount) {
setFunctionEditorLineCount(newLineCount + 1);
@@ -55,8 +65,8 @@ export function ChatPromptEditorFunction({ functionName, functionPayload, onChan
}, [functionEditorLineCount, model]);
useEffect(() => {
- recalculateFunctionEditorLineCount();
- }, [model, recalculateFunctionEditorLineCount]);
+ recalculateLineCount();
+ }, [model, recalculateLineCount]);
useEffect(() => {
if (previousPayload === undefined && initialJsonString) {
@@ -72,6 +82,10 @@ export function ChatPromptEditorFunction({ functionName, functionPayload, onChan
}
}, [functionName, functionPayload, initialJsonString, onChange, previousPayload]);
+ editorRef.current?.onDidBlurEditorWidget(() => {
+ onBlur();
+ });
+
return (
{functionName}
@@ -81,10 +95,15 @@ export function ChatPromptEditorFunction({ functionName, functionPayload, onChan
{ defaultMessage: 'payloadEditor' }
)}
data-test-subj="observabilityAiAssistantChatPromptEditorCodeEditor"
+ editorDidMount={(editor) => {
+ editorRef.current = editor;
+ editor.focus();
+ onFocus();
+ }}
fullWidth
height={'180px'}
- languageId="json"
isCopyable
+ languageId="json"
languageConfiguration={{
autoClosingPairs: [
{
@@ -93,9 +112,6 @@ export function ChatPromptEditorFunction({ functionName, functionPayload, onChan
},
],
}}
- editorDidMount={(editor) => {
- editor.focus();
- }}
options={{
accessibilitySupport: 'off',
acceptSuggestionOnEnter: 'on',
@@ -121,7 +137,7 @@ export function ChatPromptEditorFunction({ functionName, functionPayload, onChan
}}
transparentBackground
value={functionPayload || ''}
- onChange={handleChangeFunctionPayload}
+ onChange={handleChangePayload}
/>
);
diff --git a/x-pack/plugins/observability_ai_assistant/public/components/chat/chat_prompt_editor_prompt.tsx b/x-pack/plugins/observability_ai_assistant/public/components/prompt_editor/prompt_editor_natural_language.tsx
similarity index 88%
rename from x-pack/plugins/observability_ai_assistant/public/components/chat/chat_prompt_editor_prompt.tsx
rename to x-pack/plugins/observability_ai_assistant/public/components/prompt_editor/prompt_editor_natural_language.tsx
index 57c083d9704fd..e7bd833aee356 100644
--- a/x-pack/plugins/observability_ai_assistant/public/components/chat/chat_prompt_editor_prompt.tsx
+++ b/x-pack/plugins/observability_ai_assistant/public/components/prompt_editor/prompt_editor_natural_language.tsx
@@ -14,9 +14,18 @@ interface Props {
prompt: string | undefined;
onChange: (message: Message['message']) => void;
onChangeHeight: (height: number) => void;
+ onFocus: () => void;
+ onBlur: () => void;
}
-export function ChatPromptEditorPrompt({ disabled, prompt, onChange, onChangeHeight }: Props) {
+export function PromptEditorNaturalLanguage({
+ disabled,
+ prompt,
+ onChange,
+ onChangeHeight,
+ onFocus,
+ onBlur,
+}: Props) {
const textAreaRef = useRef(null);
const handleChange = (event: React.ChangeEvent) => {
@@ -44,6 +53,7 @@ export function ChatPromptEditorPrompt({ disabled, prompt, onChange, onChangeHei
if (textarea) {
textarea.focus();
+ textarea.select();
}
}, [handleResizeTextArea]);
@@ -65,6 +75,8 @@ export function ChatPromptEditorPrompt({ disabled, prompt, onChange, onChangeHei
rows={1}
value={prompt || ''}
onChange={handleChange}
+ onFocus={onFocus}
+ onBlur={onBlur}
/>
);
}
diff --git a/x-pack/plugins/observability_ai_assistant/public/hooks/use_json_editor_model.ts b/x-pack/plugins/observability_ai_assistant/public/hooks/use_json_editor_model.ts
index 230d0065e29c3..6f4535d84acef 100644
--- a/x-pack/plugins/observability_ai_assistant/public/hooks/use_json_editor_model.ts
+++ b/x-pack/plugins/observability_ai_assistant/public/hooks/use_json_editor_model.ts
@@ -12,9 +12,6 @@ import { safeJsonParse } from '../utils/safe_json_parse';
const { editor, languages, Uri } = monaco;
-const SCHEMA_URI = 'http://elastic.co/foo.json';
-const modelUri = Uri.parse(SCHEMA_URI);
-
export const useJsonEditorModel = ({
functionName,
initialJson,
@@ -28,13 +25,17 @@ export const useJsonEditorModel = ({
const [initialJsonValue, setInitialJsonValue] = useState(initialJson);
+ const SCHEMA_URI = `http://elastic.co/${functionName}.json`;
+
+ const modelUri = useMemo(() => Uri.parse(SCHEMA_URI), [SCHEMA_URI]);
+
useEffect(() => {
setInitialJsonValue(initialJson);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [functionName]);
return useMemo(() => {
- if (!functionDefinition) {
+ if (!functionDefinition || !modelUri) {
return {};
}
@@ -66,5 +67,5 @@ export const useJsonEditorModel = ({
}
return { model, initialJsonString };
- }, [functionDefinition, initialJsonValue]);
+ }, [SCHEMA_URI, functionDefinition, initialJsonValue, modelUri]);
};