Skip to content

Commit

Permalink
Add JSON schema validation for Prompt Editor
Browse files Browse the repository at this point in the history
  • Loading branch information
CoenWarmer committed Aug 2, 2023
1 parent f1426b6 commit 280f886
Show file tree
Hide file tree
Showing 12 changed files with 413 additions and 156 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

import { EuiFlexGroup, EuiFlexItem, EuiHorizontalRule, EuiPanel, useEuiTheme } from '@elastic/eui';
import { css } from '@emotion/css';
import { i18n } from '@kbn/i18n';
import type { AuthenticatedUser } from '@kbn/security-plugin/common';
import React from 'react';
import { type ConversationCreateRequest } from '../../../common/types';
Expand Down Expand Up @@ -68,7 +69,12 @@ export function ChatBody({
<EuiFlexItem grow={false}>
<EuiPanel hasBorder={false} hasShadow={false} paddingSize="m">
<ChatHeader
title={initialConversation?.conversation.title ?? ''}
title={
initialConversation?.conversation.title ??
i18n.translate('xpack.observabilityAiAssistant.chatHeader.newConversation', {
defaultMessage: 'New conversation',
})
}
connectors={connectors}
/>
</EuiPanel>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { EuiFlexGroup, EuiFlexItem, EuiTitle } from '@elastic/eui';
import React from 'react';
import { EuiFlexGroup, EuiFlexItem, EuiTitle } from '@elastic/eui';
import { UseGenAIConnectorsResult } from '../../hooks/use_genai_connectors';
import { AssistantAvatar } from '../assistant_avatar';
import { ConnectorSelectorBase } from '../connector_selector/connector_selector_base';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,24 @@
import React from 'react';
import { ComponentStory } from '@storybook/react';
import { ChatPromptEditor as Component, ChatPromptEditorProps } from './chat_prompt_editor';
import { KibanaReactStorybookDecorator } from '../../utils/storybook_decorator';

/*
JSON Schema validation in the ChatPromptEditor compponent does not work
when rendering the component from within Storybook.
*/
export default {
component: Component,
title: 'app/Molecules/ChatPromptEditor',
argTypes: {},
parameters: {
backgrounds: {
default: 'white',
values: [{ name: 'white', value: '#fff' }],
},
},
decorators: [KibanaReactStorybookDecorator],
};

const Template: ComponentStory<typeof Component> = (props: ChatPromptEditorProps) => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,19 +5,23 @@
* 2.0.
*/

import React, { useState } from 'react';
import React, { useEffect, useState } from 'react';
import {
EuiButtonIcon,
EuiContextMenuItem,
EuiContextMenuPanel,
EuiButtonEmpty,
EuiFieldText,
EuiFlexGroup,
EuiFlexItem,
EuiPopover,
EuiSpacer,
EuiPanel,
} from '@elastic/eui';
import { CodeEditor } from '@kbn/kibana-react-plugin/public';
import { i18n } from '@kbn/i18n';
import { useFunctions, type Func } from '../../hooks/use_functions';
import { useObservabilityAIAssistant } from '../../hooks/use_observability_ai_assistant';
import { useJsonEditorModel } from '../../hooks/use_json_editor_model';
import { type Message, MessageRole } from '../../../common';
import type { FunctionDefinition } from '../../../common/types';
import { FunctionListPopover } from './function_list_popover';

export interface ChatPromptEditorProps {
disabled: boolean;
Expand All @@ -26,80 +30,158 @@ export interface ChatPromptEditorProps {
}

export function ChatPromptEditor({ onSubmit, disabled, loading }: ChatPromptEditorProps) {
const functions = useFunctions();
const { getFunctions } = useObservabilityAIAssistant();
const functions = getFunctions();

const [prompt, setPrompt] = useState('');
const [isFunctionListOpen, setIsFunctionListOpen] = useState(false);
const [functionPayload, setFunctionPayload] = useState<string | undefined>('');
const [selectedFunction, setSelectedFunction] = useState<FunctionDefinition | undefined>();

const { model, initialJsonString } = useJsonEditorModel(selectedFunction);

useEffect(() => {
setFunctionPayload(initialJsonString);
}, [initialJsonString, selectedFunction]);

const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
setPrompt(event.currentTarget.value);
};

const handleSubmit = () => {
const currentPrompt = prompt;
setPrompt('');
onSubmit({
'@timestamp': new Date().toISOString(),
message: { role: MessageRole.User, content: currentPrompt },
})
.then(() => {
setPrompt('');
})
.catch(() => {
setPrompt(currentPrompt);
});
const handleChangeFunctionPayload = (params: string) => {
setFunctionPayload(params);
};

const handleClickFunctionList = () => {
setIsFunctionListOpen(!isFunctionListOpen);
const handleClearSelection = () => {
setSelectedFunction(undefined);
setFunctionPayload('');
};

const handleSelectFunction = (func: Func) => {
setIsFunctionListOpen(false);
const handleSubmit = async () => {
const currentPrompt = prompt;
const currentPayload = functionPayload;

setPrompt('');
setFunctionPayload(undefined);

try {
if (selectedFunction) {
await onSubmit({
'@timestamp': new Date().toISOString(),
message: {
role: MessageRole.Function,
function_call: {
name: selectedFunction.options.name,
trigger: MessageRole.User,
arguments: currentPayload,
},
},
});
} else {
await onSubmit({
'@timestamp': new Date().toISOString(),
message: { role: MessageRole.User, content: currentPrompt },
});
setPrompt('');
}
} catch (_) {
setPrompt(currentPrompt);
}
};

return (
<EuiFlexGroup gutterSize="m">
<EuiFlexItem grow={false}>
<EuiPopover
anchorPosition="downLeft"
button={
<EuiButtonIcon
display="base"
iconType="function"
size="m"
onClick={handleClickFunctionList}
/>
}
closePopover={handleClickFunctionList}
panelPaddingSize="s"
isOpen={isFunctionListOpen}
>
<EuiContextMenuPanel
size="s"
items={functions.map((func) => (
<EuiContextMenuItem key={func.id} onClick={() => handleSelectFunction(func)}>
{func.function_name}
</EuiContextMenuItem>
))}
/>
</EuiPopover>
</EuiFlexItem>
<EuiFlexGroup gutterSize="s" responsive={false}>
<EuiFlexItem grow>
<EuiFieldText
fullWidth
value={prompt}
placeholder={i18n.translate('xpack.observabilityAiAssistant.prompt.placeholder', {
defaultMessage: 'Press ‘space’ or ‘$’ for function recommendations',
})}
onChange={handleChange}
onSubmit={handleSubmit}
/>
<EuiFlexGroup direction="column" gutterSize="s">
<EuiFlexItem>
<EuiFlexGroup responsive={false}>
<EuiFlexItem grow>
<FunctionListPopover
functions={functions}
selectedFunction={selectedFunction}
onSelectFunction={setSelectedFunction}
/>
</EuiFlexItem>
<EuiFlexItem grow={false}>
{selectedFunction ? (
<EuiButtonEmpty
iconType="cross"
iconSide="right"
size="xs"
onClick={handleClearSelection}
>
{i18n.translate('xpack.observabilityAiAssistant.prompt.emptySelection', {
defaultMessage: 'Empty selection',
})}
</EuiButtonEmpty>
) : null}
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlexItem>
<EuiFlexItem>
{selectedFunction ? (
<EuiPanel borderRadius="none" color="subdued" hasShadow={false} paddingSize="xs">
<CodeEditor
aria-label="payloadEditor"
fullWidth
height="120px"
languageId="json"
value={functionPayload || ''}
onChange={handleChangeFunctionPayload}
isCopyable
languageConfiguration={{
autoClosingPairs: [
{
open: '{',
close: '}',
},
],
}}
options={{
automaticLayout: true,
autoIndent: 'full',
autoClosingQuotes: 'always',
accessibilitySupport: 'off',
acceptSuggestionOnEnter: 'on',
contextmenu: true,
fontSize: 12,
formatOnType: true,
formatOnPaste: 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
/>
</EuiPanel>
) : (
<EuiFieldText
fullWidth
value={prompt}
placeholder={i18n.translate('xpack.observabilityAiAssistant.prompt.placeholder', {
defaultMessage: 'Press ‘$’ for function recommendations',
})}
onChange={handleChange}
onSubmit={handleSubmit}
/>
)}
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiSpacer size="xl" />
<EuiButtonIcon
aria-label="Submit"
isLoading={loading}
disabled={!prompt || loading || disabled}
disabled={selectedFunction ? !functionPayload : !prompt || loading || disabled}
display={prompt ? 'fill' : 'base'}
iconType="kqlFunction"
size="m"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
/*
* 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 { ComponentStory } from '@storybook/react';
import React from 'react';
import { KibanaReactStorybookDecorator } from '../../utils/storybook_decorator';
import { FunctionListPopover as Component } from './function_list_popover';

export default {
component: Component,
title: 'app/Organisms/FunctionListPopover',
decorators: [KibanaReactStorybookDecorator],
};

type FunctionListPopover = React.ComponentProps<typeof Component>;

const Template: ComponentStory<typeof Component> = (props: FunctionListPopover) => {
return <Component {...props} />;
};

const defaultProps: FunctionListPopover = {
functions: [],
onSelectFunction: () => {},
};

export const ConversationList = Template.bind({});
ConversationList.args = defaultProps;
Loading

0 comments on commit 280f886

Please sign in to comment.