refactor(search_playground): prep for saved playgrounds (#217251)

## Summary

Renaming types and files as well as moving providers around to make
implmentating saved playground routes more straightforward. Naming
updates to reduce future confusion from generic names that didn't fit
when there are multiple providers etc.
This commit is contained in:
Rodney Norris 2025-04-10 18:20:09 -05:00 committed by GitHub
parent 3485e52340
commit 0f79990912
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
42 changed files with 297 additions and 303 deletions

View file

@ -7,12 +7,14 @@
import React from 'react';
import ReactDOM from 'react-dom';
import { QueryClientProvider } from '@tanstack/react-query';
import { CoreStart } from '@kbn/core/public';
import { KibanaRenderContextProvider } from '@kbn/react-kibana-context-render';
import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public';
import { I18nProvider } from '@kbn/i18n-react';
import { Router } from '@kbn/shared-ux-router';
import { AppPluginStartDependencies } from './types';
import { queryClient } from './utils/query_client';
export const renderApp = async (
core: CoreStart,
@ -26,7 +28,9 @@ export const renderApp = async (
<KibanaContextProvider services={{ ...core, ...services }}>
<I18nProvider>
<Router history={services.history}>
<PlaygroundRouter />
<QueryClientProvider client={queryClient}>
<PlaygroundRouter />
</QueryClientProvider>
</Router>
</I18nProvider>
</KibanaContextProvider>

View file

@ -27,7 +27,7 @@ import { AnalyticsEvents } from '../analytics/constants';
import { useAutoBottomScroll } from '../hooks/use_auto_bottom_scroll';
import { ChatSidebar } from './chat_sidebar';
import { useChat } from '../hooks/use_chat';
import { ChatForm, ChatFormFields, ChatRequestData, MessageRole } from '../types';
import { PlaygroundForm, PlaygroundFormFields, ChatRequestData, MessageRole } from '../types';
import { MessageList } from './message_list/message_list';
import { QuestionInput } from './question_input';
@ -38,19 +38,19 @@ import { useUsageTracker } from '../hooks/use_usage_tracker';
import { PlaygroundBodySection } from './playground_body_section';
import { elasticsearchQueryString } from '../utils/user_query';
const buildFormData = (formData: ChatForm): ChatRequestData => ({
connector_id: formData[ChatFormFields.summarizationModel].connectorId!,
prompt: formData[ChatFormFields.prompt],
indices: formData[ChatFormFields.indices].join(),
citations: formData[ChatFormFields.citations],
const buildFormData = (formData: PlaygroundForm): ChatRequestData => ({
connector_id: formData[PlaygroundFormFields.summarizationModel].connectorId!,
prompt: formData[PlaygroundFormFields.prompt],
indices: formData[PlaygroundFormFields.indices].join(),
citations: formData[PlaygroundFormFields.citations],
elasticsearch_query: elasticsearchQueryString(
formData[ChatFormFields.elasticsearchQuery],
formData[ChatFormFields.userElasticsearchQuery],
formData[ChatFormFields.userElasticsearchQueryValidations]
formData[PlaygroundFormFields.elasticsearchQuery],
formData[PlaygroundFormFields.userElasticsearchQuery],
formData[PlaygroundFormFields.userElasticsearchQueryValidations]
),
summarization_model: formData[ChatFormFields.summarizationModel].value,
source_fields: JSON.stringify(formData[ChatFormFields.sourceFields]),
doc_size: formData[ChatFormFields.docSize],
summarization_model: formData[PlaygroundFormFields.summarizationModel].value,
source_fields: JSON.stringify(formData[PlaygroundFormFields.sourceFields]),
doc_size: formData[PlaygroundFormFields.docSize],
});
export const Chat = () => {
@ -61,12 +61,12 @@ export const Chat = () => {
resetField,
handleSubmit,
getValues,
} = useFormContext<ChatForm>();
} = useFormContext<PlaygroundForm>();
const { messages, append, stop: stopRequest, setMessages, reload } = useChat();
const messagesRef = useAutoBottomScroll();
const [isRegenerating, setIsRegenerating] = useState<boolean>(false);
const usageTracker = useUsageTracker();
const onSubmit = async (data: ChatForm) => {
const onSubmit = async (data: PlaygroundForm) => {
await append(
{ content: data.question, role: MessageRole.user, createdAt: new Date() },
{
@ -75,7 +75,7 @@ export const Chat = () => {
);
usageTracker?.click(AnalyticsEvents.chatQuestionSent);
resetField(ChatFormFields.question);
resetField(PlaygroundFormFields.question);
};
const handleStopRequest = () => {
stopRequest();
@ -190,7 +190,7 @@ export const Chat = () => {
<EuiSpacer size="s" />
<Controller
name={ChatFormFields.question}
name={PlaygroundFormFields.question}
control={control}
defaultValue=""
rules={{

View file

@ -19,14 +19,14 @@ import { FormattedMessage } from '@kbn/i18n-react';
import { useWatch } from 'react-hook-form';
import { docLinks } from '../../common/doc_links';
import { EditContextPanel } from './edit_context/edit_context_panel';
import { ChatForm, ChatFormFields } from '../types';
import { PlaygroundForm, PlaygroundFormFields } from '../types';
import { useManagementLink } from '../hooks/use_management_link';
import { SummarizationPanel } from './summarization_panel/summarization_panel';
export const ChatSidebar: React.FC = () => {
const { euiTheme } = useEuiTheme();
const selectedModel = useWatch<ChatForm, ChatFormFields.summarizationModel>({
name: ChatFormFields.summarizationModel,
const selectedModel = useWatch<PlaygroundForm, PlaygroundFormFields.summarizationModel>({
name: PlaygroundFormFields.summarizationModel,
});
const managementLink = useManagementLink(selectedModel?.connectorId);
const panels = [

View file

@ -9,12 +9,12 @@ import React, { useState } from 'react';
import { FormattedMessage } from '@kbn/i18n-react';
import { EuiButton } from '@elastic/eui';
import { useWatch } from 'react-hook-form';
import { ChatForm, ChatFormFields } from '../types';
import { PlaygroundForm, PlaygroundFormFields } from '../types';
import { SelectIndicesFlyout } from './select_indices_flyout';
export const DataActionButton: React.FC = () => {
const selectedIndices = useWatch<ChatForm, ChatFormFields.indices>({
name: ChatFormFields.indices,
const selectedIndices = useWatch<PlaygroundForm, PlaygroundFormFields.indices>({
name: PlaygroundFormFields.indices,
});
const [showFlyout, setShowFlyout] = useState(false);
const handleFlyoutClose = () => setShowFlyout(false);

View file

@ -10,7 +10,7 @@ import { render, fireEvent, screen } from '@testing-library/react';
import { EditContextPanel } from './edit_context_panel';
import { FormProvider, useForm } from 'react-hook-form';
import { __IntlProvider as IntlProvider } from '@kbn/i18n-react';
import { ChatFormFields } from '../../types';
import { PlaygroundFormFields } from '../../types';
jest.mock('../../hooks/use_source_indices_field', () => ({
useSourceIndicesFields: () => ({
@ -44,9 +44,9 @@ jest.mock('../../hooks/use_usage_tracker', () => ({
const MockFormProvider = ({ children }: { children: React.ReactElement }) => {
const methods = useForm({
values: {
[ChatFormFields.indices]: ['index1'],
[ChatFormFields.docSize]: 1,
[ChatFormFields.sourceFields]: {
[PlaygroundFormFields.indices]: ['index1'],
[PlaygroundFormFields.docSize]: 1,
[PlaygroundFormFields.sourceFields]: {
index1: ['title'],
index2: ['body'],
},

View file

@ -12,7 +12,7 @@ import React, { useCallback } from 'react';
import { useController } from 'react-hook-form';
import { useSourceIndicesFields } from '../../hooks/use_source_indices_field';
import { useUsageTracker } from '../../hooks/use_usage_tracker';
import { ChatForm, ChatFormFields } from '../../types';
import { PlaygroundForm, PlaygroundFormFields } from '../../types';
import { AnalyticsEvents } from '../../analytics/constants';
import { ContextFieldsSelect } from './context_fields_select';
@ -23,13 +23,13 @@ export const EditContextPanel: React.FC = () => {
const {
field: { onChange: onChangeSize, value: docSize },
} = useController({
name: ChatFormFields.docSize,
name: PlaygroundFormFields.docSize,
});
const {
field: { onChange: onChangeSourceFields, value: sourceFields },
} = useController<ChatForm, ChatFormFields.sourceFields>({
name: ChatFormFields.sourceFields,
} = useController<PlaygroundForm, PlaygroundFormFields.sourceFields>({
name: PlaygroundFormFields.sourceFields,
});
const updateSourceField = useCallback(

View file

@ -11,7 +11,7 @@
import { render, screen } from '@testing-library/react';
import React from 'react';
import { Header } from './header';
import { ChatFormFields, PlaygroundPageMode, PlaygroundViewMode } from '../types';
import { PlaygroundFormFields, PlaygroundPageMode, PlaygroundViewMode } from '../types';
import { __IntlProvider as IntlProvider } from '@kbn/i18n-react';
import { EuiForm } from '@elastic/eui';
import { FormProvider, useForm } from 'react-hook-form';
@ -28,13 +28,13 @@ jest.mock('../hooks/use_playground_parameters', () => ({
const MockFormProvider = ({ children }: { children: React.ReactElement }) => {
const methods = useForm({
values: {
[ChatFormFields.indices]: ['index1', 'index2'],
[ChatFormFields.queryFields]: { index1: ['field1'], index2: ['field1'] },
[ChatFormFields.sourceFields]: {
[PlaygroundFormFields.indices]: ['index1', 'index2'],
[PlaygroundFormFields.queryFields]: { index1: ['field1'], index2: ['field1'] },
[PlaygroundFormFields.sourceFields]: {
index1: ['field1'],
index2: ['field1'],
},
[ChatFormFields.elasticsearchQuery]: {
[PlaygroundFormFields.elasticsearchQuery]: {
retriever: {
rrf: {
retrievers: [
@ -48,7 +48,7 @@ const MockFormProvider = ({ children }: { children: React.ReactElement }) => {
});
return <FormProvider {...methods}>{children}</FormProvider>;
};
const MockChatForm = ({
const MockPlaygroundForm = ({
children,
handleSubmit,
}: {
@ -74,9 +74,9 @@ describe('Header', () => {
});
render(
<IntlProvider locale="en">
<MockChatForm handleSubmit={() => {}}>
<MockPlaygroundForm handleSubmit={() => {}}>
<Header onModeChange={() => {}} onSelectPageModeChange={() => {}} />
</MockChatForm>
</MockPlaygroundForm>
</IntlProvider>
);
@ -91,9 +91,9 @@ describe('Header', () => {
});
render(
<IntlProvider locale="en">
<MockChatForm handleSubmit={() => {}}>
<MockPlaygroundForm handleSubmit={() => {}}>
<Header onModeChange={() => {}} onSelectPageModeChange={() => {}} />
</MockChatForm>
</MockPlaygroundForm>
</IntlProvider>
);

View file

@ -22,7 +22,7 @@ import { FormattedMessage } from '@kbn/i18n-react';
import { useFormContext } from 'react-hook-form';
import { docLinks } from '../../../common/doc_links';
import { useLLMsModels } from '../../hooks/use_llms_models';
import { ChatForm, ChatFormFields } from '../../types';
import { PlaygroundForm, PlaygroundFormFields } from '../../types';
interface TokenEstimateTooltipProps {
context: number;
@ -37,8 +37,8 @@ export const TokenEstimateTooltip: React.FC<TokenEstimateTooltipProps> = ({
}) => {
const [showTooltip, setShowTooltip] = useState<boolean>(false);
const models = useLLMsModels();
const { getValues } = useFormContext<ChatForm>();
const formValues = getValues(ChatFormFields.summarizationModel);
const { getValues } = useFormContext<PlaygroundForm>();
const formValues = getValues(PlaygroundFormFields.summarizationModel);
const selectedModel = models.find((m) => m.value === formValues?.value);

View file

@ -15,7 +15,12 @@ import { SearchQueryMode } from './query_mode/search_query_mode';
import { ChatSetupPage } from './setup_page/chat_setup_page';
import { Header } from './header';
import { useLoadConnectors } from '../hooks/use_load_connectors';
import { ChatForm, ChatFormFields, PlaygroundPageMode, PlaygroundViewMode } from '../types';
import {
PlaygroundForm,
PlaygroundFormFields,
PlaygroundPageMode,
PlaygroundViewMode,
} from '../types';
import { Chat } from './chat';
import { SearchMode } from './search_mode/search_mode';
import { SearchPlaygroundSetupPage } from './setup_page/search_playground_setup_page';
@ -34,14 +39,14 @@ export interface AppProps {
showDocs?: boolean;
}
export const App: React.FC<AppProps> = ({ showDocs = false }) => {
export const Playground: React.FC<AppProps> = ({ showDocs = false }) => {
const isSearchModeEnabled = useSearchPlaygroundFeatureFlag();
const { pageMode, viewMode } = usePlaygroundParameters();
const { application } = useKibana().services;
const { data: connectors } = useLoadConnectors();
const hasSelectedIndices = Boolean(
useWatch<ChatForm, ChatFormFields.indices>({
name: ChatFormFields.indices,
useWatch<PlaygroundForm, PlaygroundFormFields.indices>({
name: PlaygroundFormFields.indices,
}).length
);
const navigateToView = useCallback(

View file

@ -10,7 +10,7 @@ import React from 'react';
import { EuiFieldText } from '@elastic/eui';
import { Controller, useFormContext } from 'react-hook-form';
import { i18n } from '@kbn/i18n';
import { ChatFormFields } from '../../types';
import { PlaygroundFormFields } from '../../types';
export const ChatPrompt = ({ isLoading }: { isLoading: boolean }) => {
const { control } = useFormContext();
@ -18,7 +18,7 @@ export const ChatPrompt = ({ isLoading }: { isLoading: boolean }) => {
return (
<Controller
control={control}
name={ChatFormFields.question}
name={PlaygroundFormFields.question}
render={({ field }) => (
<EuiFieldText
data-test-subj="searchPlaygroundChatQuestionFieldText"

View file

@ -23,10 +23,10 @@ import {
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n-react';
import { docLinks } from '../../../common/doc_links';
import type { ChatForm, ChatFormFields, QuerySourceFields } from '../../types';
import type { PlaygroundForm, PlaygroundFormFields, QuerySourceFields } from '../../types';
const isQueryFieldSelected = (
queryFields: ChatForm[ChatFormFields.queryFields],
queryFields: PlaygroundForm[PlaygroundFormFields.queryFields],
index: string,
field: string
): boolean => {
@ -38,7 +38,7 @@ export interface QueryFieldsPanelProps {
index: string;
indexFields: QuerySourceFields;
updateFields: (index: string, fieldName: string, checked: boolean) => void;
queryFields: ChatForm[ChatFormFields.queryFields];
queryFields: PlaygroundForm[PlaygroundFormFields.queryFields];
}
export const QueryFieldsPanel = ({

View file

@ -10,7 +10,7 @@ import { render, screen } from '@testing-library/react';
import { QueryMode } from './query_mode';
import { FormProvider, useForm } from 'react-hook-form';
import { __IntlProvider as IntlProvider } from '@kbn/i18n-react';
import { ChatFormFields } from '../../types';
import { PlaygroundFormFields } from '../../types';
jest.mock('../../hooks/use_source_indices_field', () => ({
useSourceIndicesFields: () => ({
@ -45,13 +45,13 @@ jest.mock('../../hooks/use_usage_tracker', () => ({
const MockFormProvider = ({ children }: { children: React.ReactElement }) => {
const methods = useForm({
values: {
[ChatFormFields.indices]: ['index1', 'index2'],
[ChatFormFields.queryFields]: { index1: ['field1'], index2: ['field1'] },
[ChatFormFields.sourceFields]: {
[PlaygroundFormFields.indices]: ['index1', 'index2'],
[PlaygroundFormFields.queryFields]: { index1: ['field1'], index2: ['field1'] },
[PlaygroundFormFields.sourceFields]: {
index1: ['field1'],
index2: ['field1'],
},
[ChatFormFields.elasticsearchQuery]: {
[PlaygroundFormFields.elasticsearchQuery]: {
retriever: {
rrf: {
retrievers: [

View file

@ -24,7 +24,7 @@ import React, { useEffect, useMemo } from 'react';
import { useController, useWatch } from 'react-hook-form';
import { useSourceIndicesFields } from '../../hooks/use_source_indices_field';
import { useUsageTracker } from '../../hooks/use_usage_tracker';
import { ChatForm, ChatFormFields } from '../../types';
import { PlaygroundForm, PlaygroundFormFields } from '../../types';
import { AnalyticsEvents } from '../../analytics/constants';
import { docLinks } from '../../../common/doc_links';
import { createQuery } from '../../utils/create_query';
@ -32,7 +32,7 @@ import { PlaygroundBodySection } from '../playground_body_section';
import { QueryViewSidebarContainer, QueryViewContainer } from './styles';
const isQueryFieldSelected = (
queryFields: ChatForm[ChatFormFields.queryFields],
queryFields: PlaygroundForm[PlaygroundFormFields.queryFields],
index: string,
field: string
): boolean => {
@ -43,18 +43,18 @@ export const QueryMode: React.FC = () => {
const { euiTheme } = useEuiTheme();
const usageTracker = useUsageTracker();
const { fields } = useSourceIndicesFields();
const sourceFields = useWatch<ChatForm, ChatFormFields.sourceFields>({
name: ChatFormFields.sourceFields,
const sourceFields = useWatch<PlaygroundForm, PlaygroundFormFields.sourceFields>({
name: PlaygroundFormFields.sourceFields,
});
const {
field: { onChange: queryFieldsOnChange, value: queryFields },
} = useController<ChatForm, ChatFormFields.queryFields>({
name: ChatFormFields.queryFields,
} = useController<PlaygroundForm, PlaygroundFormFields.queryFields>({
name: PlaygroundFormFields.queryFields,
});
const {
field: { onChange: elasticsearchQueryChange, value: elasticsearchQuery },
} = useController<ChatForm, ChatFormFields.elasticsearchQuery>({
name: ChatFormFields.elasticsearchQuery,
} = useController<PlaygroundForm, PlaygroundFormFields.elasticsearchQuery>({
name: PlaygroundFormFields.elasticsearchQuery,
});
const updateFields = (index: string, fieldName: string, checked: boolean) => {

View file

@ -12,7 +12,7 @@ import { FormattedMessage } from '@kbn/i18n-react';
import { useController, useWatch } from 'react-hook-form';
import { useSourceIndicesFields } from '../../hooks/use_source_indices_field';
import { useUsageTracker } from '../../hooks/use_usage_tracker';
import { ChatForm, ChatFormFields, PlaygroundPageMode } from '../../types';
import { PlaygroundForm, PlaygroundFormFields, PlaygroundPageMode } from '../../types';
import { AnalyticsEvents } from '../../analytics/constants';
import { createQuery } from '../../utils/create_query';
import { SearchQuery } from './search_query';
@ -35,28 +35,28 @@ export const QuerySidePanel = ({
}: QuerySidePanelProps) => {
const usageTracker = useUsageTracker();
const { fields } = useSourceIndicesFields();
const sourceFields = useWatch<ChatForm, ChatFormFields.sourceFields>({
name: ChatFormFields.sourceFields,
const sourceFields = useWatch<PlaygroundForm, PlaygroundFormFields.sourceFields>({
name: PlaygroundFormFields.sourceFields,
});
const {
field: { onChange: queryFieldsOnChange, value: queryFields },
} = useController<ChatForm, ChatFormFields.queryFields>({
name: ChatFormFields.queryFields,
} = useController<PlaygroundForm, PlaygroundFormFields.queryFields>({
name: PlaygroundFormFields.queryFields,
});
const {
field: { onChange: elasticsearchQueryChange },
} = useController<ChatForm, ChatFormFields.elasticsearchQuery>({
name: ChatFormFields.elasticsearchQuery,
} = useController<PlaygroundForm, PlaygroundFormFields.elasticsearchQuery>({
name: PlaygroundFormFields.elasticsearchQuery,
});
const {
field: { onChange: userElasticsearchQueryChange },
} = useController<ChatForm, ChatFormFields.userElasticsearchQuery>({
name: ChatFormFields.userElasticsearchQuery,
} = useController<PlaygroundForm, PlaygroundFormFields.userElasticsearchQuery>({
name: PlaygroundFormFields.userElasticsearchQuery,
});
const {
field: { value: userElasticsearchQueryValidations },
} = useController<ChatForm, ChatFormFields.userElasticsearchQueryValidations>({
name: ChatFormFields.userElasticsearchQueryValidations,
} = useController<PlaygroundForm, PlaygroundFormFields.userElasticsearchQueryValidations>({
name: PlaygroundFormFields.userElasticsearchQueryValidations,
});
const handleSearch = useCallback(

View file

@ -23,7 +23,7 @@ import { CodeEditor } from '@kbn/code-editor';
import { monaco as monacoEditor } from '@kbn/monaco';
import { Controller, useController, useFormContext } from 'react-hook-form';
import { ChatForm, ChatFormFields } from '../../types';
import { PlaygroundForm, PlaygroundFormFields } from '../../types';
import { FullHeight, QueryViewTitlePanel, PanelFillContainer } from './styles';
import { formatElasticsearchQueryString } from '../../utils/user_query';
@ -37,21 +37,21 @@ export const ElasticsearchQueryViewer = ({
isLoading: boolean;
}) => {
const { euiTheme } = useEuiTheme();
const { control } = useFormContext<ChatForm>();
const { control } = useFormContext<PlaygroundForm>();
const {
field: { value: elasticsearchQuery },
} = useController<ChatForm, ChatFormFields.elasticsearchQuery>({
name: ChatFormFields.elasticsearchQuery,
} = useController<PlaygroundForm, PlaygroundFormFields.elasticsearchQuery>({
name: PlaygroundFormFields.elasticsearchQuery,
});
const {
field: { value: userElasticsearchQuery, onChange: onChangeUserQuery },
} = useController<ChatForm, ChatFormFields.userElasticsearchQuery>({
name: ChatFormFields.userElasticsearchQuery,
} = useController<PlaygroundForm, PlaygroundFormFields.userElasticsearchQuery>({
name: PlaygroundFormFields.userElasticsearchQuery,
});
const {
field: { value: userElasticsearchQueryValidations },
} = useController<ChatForm, ChatFormFields.userElasticsearchQueryValidations>({
name: ChatFormFields.userElasticsearchQueryValidations,
} = useController<PlaygroundForm, PlaygroundFormFields.userElasticsearchQueryValidations>({
name: PlaygroundFormFields.userElasticsearchQueryValidations,
});
const generatedEsQuery = useMemo(
() => formatElasticsearchQueryString(elasticsearchQuery),
@ -154,7 +154,7 @@ export const ElasticsearchQueryViewer = ({
<EuiSplitPanel.Inner paddingSize="none" css={PanelFillContainer}>
<Controller
control={control}
name={ChatFormFields.userElasticsearchQuery}
name={PlaygroundFormFields.userElasticsearchQuery}
render={({ field }) => (
<CodeEditor
dataTestSubj="ViewElasticsearchQueryResult"

View file

@ -10,21 +10,21 @@ import React from 'react';
import { EuiFieldText } from '@elastic/eui';
import { Controller, useController, useFormContext } from 'react-hook-form';
import { i18n } from '@kbn/i18n';
import { ChatForm, ChatFormFields } from '../../types';
import { PlaygroundForm, PlaygroundFormFields } from '../../types';
export const SearchQuery = ({ isLoading }: { isLoading: boolean }) => {
const { control } = useFormContext();
const {
field: { value: searchBarValue },
formState: { isSubmitting },
} = useController<ChatForm, ChatFormFields.searchQuery>({
name: ChatFormFields.searchQuery,
} = useController<PlaygroundForm, PlaygroundFormFields.searchQuery>({
name: PlaygroundFormFields.searchQuery,
});
return (
<Controller
control={control}
name={ChatFormFields.searchQuery}
name={PlaygroundFormFields.searchQuery}
render={({ field }) => (
<EuiFieldText
data-test-subj="searchPlaygroundSearchModeFieldText"

View file

@ -22,7 +22,7 @@ import { ElasticsearchQueryViewer } from './query_viewer';
import { ElasticsearchQueryOutput } from './query_output';
import { QuerySidePanel } from './query_side_panel';
import { useElasticsearchQuery } from '../../hooks/use_elasticsearch_query';
import { ChatForm, ChatFormFields, PlaygroundPageMode } from '../../types';
import { PlaygroundForm, PlaygroundFormFields, PlaygroundPageMode } from '../../types';
import {
FullHeight,
QueryViewContainer,
@ -40,18 +40,18 @@ export const SearchQueryMode = ({ pageMode }: { pageMode: PlaygroundPageMode })
const { executeQuery, data, error, isError, fetchStatus } = useElasticsearchQuery(pageMode);
const {
field: { value: searchQuery },
} = useController<ChatForm, ChatFormFields.searchQuery>({
name: ChatFormFields.searchQuery,
} = useController<PlaygroundForm, PlaygroundFormFields.searchQuery>({
name: PlaygroundFormFields.searchQuery,
});
const {
field: { value: question },
} = useController<ChatForm, ChatFormFields.question>({
name: ChatFormFields.question,
} = useController<PlaygroundForm, PlaygroundFormFields.question>({
name: PlaygroundFormFields.question,
});
const {
field: { value: userElasticsearchQueryValidations },
} = useController<ChatForm, ChatFormFields.userElasticsearchQueryValidations>({
name: ChatFormFields.userElasticsearchQueryValidations,
} = useController<PlaygroundForm, PlaygroundFormFields.userElasticsearchQueryValidations>({
name: PlaygroundFormFields.userElasticsearchQueryValidations,
});
const executeQueryDisabled = disableExecuteQuery(
userElasticsearchQueryValidations,

View file

@ -12,14 +12,14 @@ import { __IntlProvider as IntlProvider } from '@kbn/i18n-react';
import { QuestionInput } from './question_input';
const mockButton = (
<EuiButton data-test="btn" className="btn" onClick={() => {}}>
<EuiButton data-test-subj="btn" className="btn" onClick={() => {}}>
Send
</EuiButton>
);
const handleOnSubmitMock = jest.fn();
const MockChatForm = ({
const MockPlaygroundForm = ({
children,
handleSubmit,
}: {
@ -40,9 +40,9 @@ describe('Question Input', () => {
it('correctly', () => {
render(
<IntlProvider locale="en">
<MockChatForm handleSubmit={handleOnSubmitMock}>
<MockPlaygroundForm handleSubmit={handleOnSubmitMock}>
<QuestionInput value="" onChange={() => {}} button={mockButton} isDisabled={false} />
</MockChatForm>
</MockPlaygroundForm>
</IntlProvider>
);
@ -52,14 +52,14 @@ describe('Question Input', () => {
it('disabled', () => {
render(
<IntlProvider locale="en">
<MockChatForm handleSubmit={handleOnSubmitMock}>
<MockPlaygroundForm handleSubmit={handleOnSubmitMock}>
<QuestionInput
value="my question"
onChange={() => {}}
button={mockButton}
isDisabled={true}
/>
</MockChatForm>
</MockPlaygroundForm>
</IntlProvider>
);
@ -69,14 +69,14 @@ describe('Question Input', () => {
it('with value', () => {
render(
<IntlProvider locale="en">
<MockChatForm handleSubmit={handleOnSubmitMock}>
<MockPlaygroundForm handleSubmit={handleOnSubmitMock}>
<QuestionInput
value="my question"
onChange={() => {}}
button={mockButton}
isDisabled={false}
/>
</MockChatForm>
</MockPlaygroundForm>
</IntlProvider>
);
@ -86,9 +86,9 @@ describe('Question Input', () => {
it('submits form', () => {
render(
<IntlProvider locale="en">
<MockChatForm handleSubmit={handleOnSubmitMock}>
<MockPlaygroundForm handleSubmit={handleOnSubmitMock}>
<QuestionInput value="" onChange={() => {}} button={mockButton} isDisabled={false} />
</MockChatForm>
</MockPlaygroundForm>
</IntlProvider>
);

View file

@ -20,19 +20,19 @@ import { KibanaPageTemplate } from '@kbn/shared-ux-page-kibana-template';
import { useQueryClient } from '@tanstack/react-query';
import { DEFAULT_PAGINATION } from '../../../common';
import { ResultList } from './result_list';
import { ChatForm, ChatFormFields, Pagination } from '../../types';
import { PlaygroundForm, PlaygroundFormFields, Pagination } from '../../types';
import { useSearchPreview } from '../../hooks/use_search_preview';
import { getPaginationFromPage } from '../../utils/pagination_helper';
import { useIndexMappings } from '../../hooks/use_index_mappings';
export const SearchMode: React.FC = () => {
const { euiTheme } = useEuiTheme();
const { control } = useFormContext<ChatForm>();
const { control } = useFormContext<PlaygroundForm>();
const {
field: { value: searchBarValue },
formState: { isSubmitting },
} = useController<ChatForm, ChatFormFields.searchQuery>({
name: ChatFormFields.searchQuery,
} = useController<PlaygroundForm, PlaygroundFormFields.searchQuery>({
name: PlaygroundFormFields.searchQuery,
});
const [searchQuery, setSearchQuery] = React.useState<{
@ -76,7 +76,7 @@ export const SearchMode: React.FC = () => {
<EuiFlexItem grow={false}>
<Controller
control={control}
name={ChatFormFields.searchQuery}
name={PlaygroundFormFields.searchQuery}
render={({ field }) => (
<EuiFieldSearch
data-test-subj="searchPlaygroundSearchModeFieldText"

View file

@ -9,13 +9,13 @@ import React, { useState } from 'react';
import { useFormContext } from 'react-hook-form';
import { FormattedMessage } from '@kbn/i18n-react';
import { EuiButton, EuiButtonEmpty } from '@elastic/eui';
import { ChatForm, ChatFormFields } from '../../types';
import { PlaygroundForm, PlaygroundFormFields } from '../../types';
import { SelectIndicesFlyout } from '../select_indices_flyout';
export const AddDataSources: React.FC = () => {
const [showFlyout, setShowFlyout] = useState(false);
const { getValues } = useFormContext<ChatForm>();
const hasSelectedIndices: boolean = !!getValues(ChatFormFields.indices)?.length;
const { getValues } = useFormContext<PlaygroundForm>();
const hasSelectedIndices: boolean = !!getValues(PlaygroundFormFields.indices)?.length;
const handleFlyoutClose = () => {
setShowFlyout(false);
};

View file

@ -12,17 +12,17 @@ import { EuiPanel } from '@elastic/eui';
import { useLLMsModels } from '../../hooks/use_llms_models';
import { IncludeCitationsField } from './include_citations_field';
import { InstructionsField } from './instructions_field';
import { ChatForm, ChatFormFields } from '../../types';
import { PlaygroundForm, PlaygroundFormFields } from '../../types';
import { SummarizationModel } from './summarization_model';
export const SummarizationPanel: React.FC = () => {
const { control } = useFormContext<ChatForm>();
const { control } = useFormContext<PlaygroundForm>();
const models = useLLMsModels();
return (
<EuiPanel data-test-subj="summarizationPanel">
<Controller
name={ChatFormFields.summarizationModel}
name={PlaygroundFormFields.summarizationModel}
rules={{ required: true }}
control={control}
render={({ field }) => (
@ -35,7 +35,7 @@ export const SummarizationPanel: React.FC = () => {
/>
<Controller
name={ChatFormFields.prompt}
name={PlaygroundFormFields.prompt}
control={control}
rules={{ required: true }}
defaultValue="You are an assistant for question-answering tasks."
@ -43,7 +43,7 @@ export const SummarizationPanel: React.FC = () => {
/>
<Controller
name={ChatFormFields.citations}
name={PlaygroundFormFields.citations}
control={control}
defaultValue={true}
render={({ field }) => (

View file

@ -9,17 +9,17 @@ import React from 'react';
import { EuiCodeBlock } from '@elastic/eui';
import { useFormContext } from 'react-hook-form';
import { ChatForm, ChatFormFields } from '../../../types';
import { PlaygroundForm, PlaygroundFormFields } from '../../../types';
import { elasticsearchQueryObject } from '../../../utils/user_query';
export const DevToolsCode: React.FC = () => {
const { getValues } = useFormContext<ChatForm>();
const { getValues } = useFormContext<PlaygroundForm>();
const {
[ChatFormFields.indices]: indices,
[ChatFormFields.elasticsearchQuery]: esQuery,
[ChatFormFields.searchQuery]: searchQuery,
[ChatFormFields.userElasticsearchQuery]: userElasticsearchQuery,
[ChatFormFields.userElasticsearchQueryValidations]: userElasticsearchQueryValidations,
[PlaygroundFormFields.indices]: indices,
[PlaygroundFormFields.elasticsearchQuery]: esQuery,
[PlaygroundFormFields.searchQuery]: searchQuery,
[PlaygroundFormFields.userElasticsearchQuery]: userElasticsearchQuery,
[PlaygroundFormFields.userElasticsearchQueryValidations]: userElasticsearchQueryValidations,
} = getValues();
const query = elasticsearchQueryObject(
esQuery,

View file

@ -8,7 +8,7 @@
import { render } from '@testing-library/react';
import { PY_LANG_CLIENT } from './py_lang_client'; // Adjust the import path according to your project structure
import { ES_CLIENT_DETAILS } from '../view_code_flyout';
import { ChatForm } from '../../../types';
import { PlaygroundForm } from '../../../types';
describe('PY_LANG_CLIENT function', () => {
test('renders with correct content', () => {
@ -21,7 +21,7 @@ describe('PY_LANG_CLIENT function', () => {
prompt: 'Your prompt',
citations: true,
summarization_model: 'Your-new-model',
} as unknown as ChatForm;
} as unknown as PlaygroundForm;
const clientDetails = ES_CLIENT_DETAILS('http://my-local-cloud-instance');
@ -39,7 +39,7 @@ describe('PY_LANG_CLIENT function', () => {
prompt: 'Your prompt',
citations: true,
summarization_model: 'Your-new-model',
} as unknown as ChatForm;
} as unknown as PlaygroundForm;
const clientDetails = ES_CLIENT_DETAILS('http://my-local-cloud-instance');

View file

@ -7,12 +7,12 @@
import { EuiCodeBlock } from '@elastic/eui';
import React from 'react';
import { ChatForm } from '../../../types';
import { PlaygroundForm } from '../../../types';
import { Prompt } from '../../../../common/prompt';
import { elasticsearchQueryObject } from '../../../utils/user_query';
import { getESQuery } from './utils';
export const PY_LANG_CLIENT = (formValues: ChatForm, clientDetails: string) => (
export const PY_LANG_CLIENT = (formValues: PlaygroundForm, clientDetails: string) => (
<EuiCodeBlock language="py" isCopyable overflowHeight="100%">
{`## Install the required packages
## pip install -qU elasticsearch openai

View file

@ -7,7 +7,7 @@
import React from 'react';
import { render } from '@testing-library/react';
import { ES_CLIENT_DETAILS } from '../view_code_flyout';
import { ChatForm } from '../../../types';
import { PlaygroundForm } from '../../../types';
import { LangchainPythonExmaple } from './py_langchain_python';
describe('LangchainPythonExmaple component', () => {
@ -21,7 +21,7 @@ describe('LangchainPythonExmaple component', () => {
prompt: 'Your prompt',
citations: true,
summarization_model: 'Your-new-model',
} as unknown as ChatForm;
} as unknown as PlaygroundForm;
const clientDetails = ES_CLIENT_DETAILS('http://my-local-cloud-instance');
@ -42,7 +42,7 @@ describe('LangchainPythonExmaple component', () => {
prompt: 'Your prompt',
citations: true,
summarization_model: 'Your-new-model',
} as unknown as ChatForm;
} as unknown as PlaygroundForm;
const clientDetails = ES_CLIENT_DETAILS('http://my-local-cloud-instance');

View file

@ -7,12 +7,12 @@
import { EuiCodeBlock } from '@elastic/eui';
import React, { useMemo } from 'react';
import { ChatForm } from '../../../types';
import { PlaygroundForm } from '../../../types';
import { Prompt } from '../../../../common/prompt';
import { elasticsearchQueryObject } from '../../../utils/user_query';
import { getESQuery } from './utils';
export const getSourceFields = (sourceFields: ChatForm['source_fields']) => {
export const getSourceFields = (sourceFields: PlaygroundForm['source_fields']) => {
let hasContentFieldsArray = false;
const fields: Record<string, string | string[]> = {};
for (const indexName of Object.keys(sourceFields)) {
@ -33,7 +33,7 @@ export const LangchainPythonExmaple = ({
formValues,
clientDetails,
}: {
formValues: ChatForm;
formValues: PlaygroundForm;
clientDetails: string;
}) => {
const { esQuery, hasContentFieldsArray, indices, prompt, sourceFields } = useMemo(() => {

View file

@ -9,15 +9,15 @@ import React, { useState } from 'react';
import { EuiButton } from '@elastic/eui';
import { useFormContext } from 'react-hook-form';
import { FormattedMessage } from '@kbn/i18n-react';
import { ChatForm, ChatFormFields, PlaygroundPageMode } from '../../types';
import { PlaygroundForm, PlaygroundFormFields, PlaygroundPageMode } from '../../types';
import { ViewCodeFlyout } from './view_code_flyout';
export const ViewCodeAction: React.FC<{ selectedPageMode: PlaygroundPageMode }> = ({
selectedPageMode = PlaygroundPageMode.chat,
}) => {
const { watch } = useFormContext<ChatForm>();
const { watch } = useFormContext<PlaygroundForm>();
const [showFlyout, setShowFlyout] = useState(false);
const selectedIndices = watch(ChatFormFields.indices);
const selectedIndices = watch(PlaygroundFormFields.indices);
return (
<>

View file

@ -22,7 +22,7 @@ import React, { useEffect, useState } from 'react';
import { useFormContext } from 'react-hook-form';
import { AnalyticsEvents } from '../../analytics/constants';
import { useUsageTracker } from '../../hooks/use_usage_tracker';
import { ChatForm, PlaygroundPageMode } from '../../types';
import { PlaygroundForm, PlaygroundPageMode } from '../../types';
import { useKibana } from '../../hooks/use_kibana';
import { MANAGEMENT_API_KEYS } from '../../../common/routes';
import { LangchainPythonExmaple } from './examples/py_langchain_python';
@ -46,7 +46,7 @@ es_client = Elasticsearch(
export const ViewCodeFlyout: React.FC<ViewCodeFlyoutProps> = ({ onClose, selectedPageMode }) => {
const usageTracker = useUsageTracker();
const [selectedLanguage, setSelectedLanguage] = useState('py-es-client');
const { getValues } = useFormContext<ChatForm>();
const { getValues } = useFormContext<PlaygroundForm>();
const formValues = getValues();
const {
services: { cloud, http },
@ -103,6 +103,7 @@ export const ViewCodeFlyout: React.FC<ViewCodeFlyoutProps> = ({ onClose, selecte
<EuiFlexGroup>
<EuiFlexItem>
<EuiSelect
data-test-subj="view-code-lang-select"
options={[
{ value: 'py-es-client', text: 'Python Elasticsearch Client with OpenAI' },
{ value: 'lc-py', text: 'LangChain Python with OpenAI' },

View file

@ -8,7 +8,7 @@
import { useMutation } from '@tanstack/react-query';
import { useFormContext } from 'react-hook-form';
import { useKibana } from './use_kibana';
import { APIRoutes, ChatFormFields } from '../types';
import { APIRoutes, PlaygroundFormFields } from '../types';
interface UseApiKeyQueryParams {
name: string;
@ -27,7 +27,7 @@ export const useCreateApiKeyQuery = () => {
body: JSON.stringify({
name,
expiresInDays,
indices: getValues(ChatFormFields.indices),
indices: getValues(PlaygroundFormFields.indices),
}),
});

View file

@ -9,8 +9,8 @@ import { useQuery } from '@tanstack/react-query';
import { useFormContext } from 'react-hook-form';
import {
APIRoutes,
ChatForm,
ChatFormFields,
PlaygroundForm,
PlaygroundFormFields,
PlaygroundPageMode,
QueryTestResponse,
} from '../types';
@ -19,29 +19,29 @@ import { elasticsearchQueryString } from '../utils/user_query';
export const useElasticsearchQuery = (pageMode: PlaygroundPageMode) => {
const { http } = useKibana().services;
const { getValues } = useFormContext<ChatForm>();
const { getValues } = useFormContext<PlaygroundForm>();
const executeEsQuery = () => {
const formValues = getValues();
const esQuery = elasticsearchQueryString(
formValues[ChatFormFields.elasticsearchQuery],
formValues[ChatFormFields.userElasticsearchQuery],
formValues[ChatFormFields.userElasticsearchQueryValidations]
formValues[PlaygroundFormFields.elasticsearchQuery],
formValues[PlaygroundFormFields.userElasticsearchQuery],
formValues[PlaygroundFormFields.userElasticsearchQueryValidations]
);
const body =
pageMode === PlaygroundPageMode.chat
? JSON.stringify({
elasticsearch_query: esQuery,
indices: formValues[ChatFormFields.indices],
query: formValues[ChatFormFields.question],
indices: formValues[PlaygroundFormFields.indices],
query: formValues[PlaygroundFormFields.question],
chat_context: {
source_fields: JSON.stringify(formValues[ChatFormFields.sourceFields]),
doc_size: formValues[ChatFormFields.docSize],
source_fields: JSON.stringify(formValues[PlaygroundFormFields.sourceFields]),
doc_size: formValues[PlaygroundFormFields.docSize],
},
})
: JSON.stringify({
elasticsearch_query: esQuery,
indices: formValues[ChatFormFields.indices],
query: formValues[ChatFormFields.searchQuery],
indices: formValues[PlaygroundFormFields.indices],
query: formValues[PlaygroundFormFields.searchQuery],
});
return http.post<QueryTestResponse>(APIRoutes.POST_QUERY_TEST, {

View file

@ -9,11 +9,11 @@ import { useQuery } from '@tanstack/react-query';
import type { HttpSetup } from '@kbn/core-http-browser';
import { IndicesGetMappingResponse } from '@elastic/elasticsearch/lib/api/types';
import { useFormContext } from 'react-hook-form';
import { APIRoutes, ChatForm, ChatFormFields } from '../types';
import { APIRoutes, PlaygroundForm, PlaygroundFormFields } from '../types';
import { useKibana } from './use_kibana';
export interface FetchIndexMappingsArgs {
indices: ChatForm[ChatFormFields.indices];
indices: PlaygroundForm[PlaygroundFormFields.indices];
http: HttpSetup;
}
@ -32,7 +32,7 @@ export const useIndexMappings = () => {
services: { http },
} = useKibana();
const { getValues } = useFormContext();
const indices = getValues(ChatFormFields.indices);
const indices = getValues(PlaygroundFormFields.indices);
const { data } = useQuery({
queryKey: ['search-playground-index-mappings'],
queryFn: () => fetchIndexMappings({ indices, http }),

View file

@ -11,7 +11,7 @@ import { useUsageTracker } from './use_usage_tracker';
import { useIndicesFields } from './use_indices_fields';
import { createQuery, getDefaultQueryFields, getDefaultSourceFields } from '../utils/create_query';
import { AnalyticsEvents } from '../analytics/constants';
import { ChatFormFields } from '../types';
import { PlaygroundFormFields } from '../types';
// Mock dependencies
jest.mock('./use_usage_tracker');
@ -49,18 +49,21 @@ describe('useLoadFieldsByIndices', () => {
isFetched: true,
});
mockGetValues.mockReturnValueOnce({
[ChatFormFields.queryFields]: {},
[ChatFormFields.sourceFields]: {},
[PlaygroundFormFields.queryFields]: {},
[PlaygroundFormFields.sourceFields]: {},
});
mockWatch.mockReturnValue(['index1']);
setup();
expect(mockSetValue).toHaveBeenCalledWith(ChatFormFields.elasticsearchQuery, 'mocked query');
expect(mockSetValue).toHaveBeenCalledWith(ChatFormFields.queryFields, {
expect(mockSetValue).toHaveBeenCalledWith(
PlaygroundFormFields.elasticsearchQuery,
'mocked query'
);
expect(mockSetValue).toHaveBeenCalledWith(PlaygroundFormFields.queryFields, {
newIndex: ['title', 'body'],
});
expect(mockSetValue).toHaveBeenCalledWith(ChatFormFields.sourceFields, {
expect(mockSetValue).toHaveBeenCalledWith(PlaygroundFormFields.sourceFields, {
testIndex: ['content'],
});
expect(mockUsageTracker.count).toHaveBeenCalledWith(AnalyticsEvents.sourceFieldsLoaded, 2);
@ -71,17 +74,17 @@ describe('useLoadFieldsByIndices', () => {
(getDefaultQueryFields as jest.Mock).mockReturnValue({ index: ['title', 'body'] });
(getDefaultSourceFields as jest.Mock).mockReturnValue({ index: ['title'] });
mockGetValues.mockReturnValueOnce({
[ChatFormFields.queryFields]: { index: [] },
[ChatFormFields.sourceFields]: { index: ['body'] },
[PlaygroundFormFields.queryFields]: { index: [] },
[PlaygroundFormFields.sourceFields]: { index: ['body'] },
});
setup();
expect(mockSetValue).toHaveBeenCalledTimes(3);
expect(mockSetValue).toHaveBeenNthCalledWith(2, ChatFormFields.queryFields, {
expect(mockSetValue).toHaveBeenNthCalledWith(2, PlaygroundFormFields.queryFields, {
index: [],
});
expect(mockSetValue).toHaveBeenNthCalledWith(3, ChatFormFields.sourceFields, {
expect(mockSetValue).toHaveBeenNthCalledWith(3, PlaygroundFormFields.sourceFields, {
index: ['body'],
});
});
@ -90,16 +93,16 @@ describe('useLoadFieldsByIndices', () => {
(getDefaultQueryFields as jest.Mock).mockReturnValue({ index: ['title', 'body'] });
(getDefaultSourceFields as jest.Mock).mockReturnValue({ index: ['title'] });
mockGetValues.mockReturnValueOnce({
[ChatFormFields.queryFields]: { index: [], oldIndex: ['title'] },
[ChatFormFields.sourceFields]: { index: ['body'], oldIndex: ['title'] },
[PlaygroundFormFields.queryFields]: { index: [], oldIndex: ['title'] },
[PlaygroundFormFields.sourceFields]: { index: ['body'], oldIndex: ['title'] },
});
setup();
expect(mockSetValue).toHaveBeenNthCalledWith(2, ChatFormFields.queryFields, {
expect(mockSetValue).toHaveBeenNthCalledWith(2, PlaygroundFormFields.queryFields, {
index: [],
});
expect(mockSetValue).toHaveBeenNthCalledWith(3, ChatFormFields.sourceFields, {
expect(mockSetValue).toHaveBeenNthCalledWith(3, PlaygroundFormFields.sourceFields, {
index: ['body'],
});
});
@ -114,17 +117,17 @@ describe('useLoadFieldsByIndices', () => {
newIndex: ['content'],
});
mockGetValues.mockReturnValueOnce({
[ChatFormFields.queryFields]: { index: [], oldIndex: ['title'] },
[ChatFormFields.sourceFields]: { index: ['body'], oldIndex: ['title'] },
[PlaygroundFormFields.queryFields]: { index: [], oldIndex: ['title'] },
[PlaygroundFormFields.sourceFields]: { index: ['body'], oldIndex: ['title'] },
});
setup();
expect(mockSetValue).toHaveBeenNthCalledWith(2, ChatFormFields.queryFields, {
expect(mockSetValue).toHaveBeenNthCalledWith(2, PlaygroundFormFields.queryFields, {
index: [],
newIndex: ['content'],
});
expect(mockSetValue).toHaveBeenNthCalledWith(3, ChatFormFields.sourceFields, {
expect(mockSetValue).toHaveBeenNthCalledWith(3, PlaygroundFormFields.sourceFields, {
index: ['body'],
newIndex: ['content'],
});

View file

@ -8,7 +8,7 @@
import { useEffect } from 'react';
import { UseFormReturn } from 'react-hook-form/dist/types';
import { useUsageTracker } from './use_usage_tracker';
import { ChatForm, ChatFormFields } from '../types';
import { PlaygroundForm, PlaygroundFormFields } from '../types';
import { useIndicesFields } from './use_indices_fields';
import {
createQuery,
@ -32,17 +32,17 @@ export const useLoadFieldsByIndices = ({
watch,
setValue,
getValues,
}: Pick<UseFormReturn<ChatForm>, 'watch' | 'getValues' | 'setValue'>) => {
}: Pick<UseFormReturn<PlaygroundForm>, 'watch' | 'getValues' | 'setValue'>) => {
const usageTracker = useUsageTracker();
const selectedIndices = watch(ChatFormFields.indices);
const selectedIndices = watch(PlaygroundFormFields.indices);
const { fields, isFetched } = useIndicesFields(selectedIndices);
useEffect(() => {
// Don't merge fields if we haven't fetched them from indices yet, otherwise we'll overwrite save values with a race condition
if (!isFetched) return;
const {
[ChatFormFields.queryFields]: queryFields,
[ChatFormFields.sourceFields]: sourceFields,
[PlaygroundFormFields.queryFields]: queryFields,
[PlaygroundFormFields.sourceFields]: sourceFields,
} = getValues();
const defaultFields = getDefaultQueryFields(fields);
@ -51,11 +51,11 @@ export const useLoadFieldsByIndices = ({
const mergedSourceFields = mergeDefaultAndCurrentValues(defaultSourceFields, sourceFields);
setValue(
ChatFormFields.elasticsearchQuery,
PlaygroundFormFields.elasticsearchQuery,
createQuery(mergedQueryFields, mergedSourceFields, fields)
);
setValue(ChatFormFields.queryFields, mergedQueryFields);
setValue(ChatFormFields.sourceFields, mergedSourceFields);
setValue(PlaygroundFormFields.queryFields, mergedQueryFields);
setValue(PlaygroundFormFields.sourceFields, mergedSourceFields);
usageTracker?.count(AnalyticsEvents.sourceFieldsLoaded, Object.values(fields)?.flat()?.length);
}, [fields, getValues, setValue, usageTracker, isFetched]);

View file

@ -9,7 +9,7 @@ import { SearchHit } from '@elastic/elasticsearch/lib/api/types';
import { useQuery } from '@tanstack/react-query';
import { useFormContext } from 'react-hook-form';
import type { HttpSetup } from '@kbn/core-http-browser';
import { APIRoutes, ChatForm, ChatFormFields, Pagination } from '../types';
import { APIRoutes, PlaygroundForm, PlaygroundFormFields, Pagination } from '../types';
import { useKibana } from './use_kibana';
import { DEFAULT_PAGINATION } from '../../common';
import { elasticsearchQueryObject } from '../utils/user_query';
@ -17,8 +17,8 @@ import { elasticsearchQueryObject } from '../utils/user_query';
export interface FetchSearchResultsArgs {
query: string;
pagination: Pagination;
indices: ChatForm[ChatFormFields.indices];
elasticsearchQuery: ChatForm[ChatFormFields.elasticsearchQuery];
indices: PlaygroundForm[PlaygroundFormFields.indices];
elasticsearchQuery: PlaygroundForm[PlaygroundFormFields.elasticsearchQuery];
http: HttpSetup;
}
@ -71,21 +71,21 @@ export const useSearchPreview = ({
const {
services: { http },
} = useKibana();
const { getValues } = useFormContext<ChatForm>();
const indices = getValues(ChatFormFields.indices);
const elasticsearchQuery = getValues(ChatFormFields.elasticsearchQuery);
const { getValues } = useFormContext<PlaygroundForm>();
const indices = getValues(PlaygroundFormFields.indices);
const elasticsearchQuery = getValues(PlaygroundFormFields.elasticsearchQuery);
const queryFn = () => {
const formData = getValues();
const elasticsearchQueryBody = elasticsearchQueryObject(
formData[ChatFormFields.elasticsearchQuery],
formData[ChatFormFields.userElasticsearchQuery],
formData[ChatFormFields.userElasticsearchQueryValidations]
formData[PlaygroundFormFields.elasticsearchQuery],
formData[PlaygroundFormFields.userElasticsearchQuery],
formData[PlaygroundFormFields.userElasticsearchQueryValidations]
);
return fetchSearchResults({
query,
pagination,
http,
indices: formData[ChatFormFields.indices],
indices: formData[PlaygroundFormFields.indices],
elasticsearchQuery: elasticsearchQueryBody,
});
};

View file

@ -9,7 +9,7 @@ import { useController } from 'react-hook-form';
import { IndexName } from '@elastic/elasticsearch/lib/api/types';
import { useCallback } from 'react';
import { useIndicesFields } from './use_indices_fields';
import { ChatFormFields } from '../types';
import { PlaygroundFormFields } from '../types';
import { useUsageTracker } from './use_usage_tracker';
import { AnalyticsEvents } from '../analytics/constants';
@ -18,12 +18,12 @@ export const useSourceIndicesFields = () => {
const {
field: { value: selectedIndices, onChange: onIndicesChange },
} = useController({
name: ChatFormFields.indices,
name: PlaygroundFormFields.indices,
});
const {
field: { onChange: onUserQueryChange },
} = useController({
name: ChatFormFields.userElasticsearchQuery,
name: PlaygroundFormFields.userElasticsearchQuery,
});
const { fields, isLoading: isFieldsLoading } = useIndicesFields(selectedIndices);

View file

@ -9,7 +9,7 @@ import { useEffect } from 'react';
import { UseFormReturn } from 'react-hook-form/dist/types';
import { useDebounceFn } from '@kbn/react-hooks';
import { ChatForm, ChatFormFields } from '../types';
import { PlaygroundForm, PlaygroundFormFields } from '../types';
import { validateUserElasticSearchQuery } from '../utils/user_query';
const DEBOUNCE_OPTIONS = { wait: 500 };
@ -17,17 +17,17 @@ export const useUserQueryValidations = ({
watch,
setValue,
getValues,
}: Pick<UseFormReturn<ChatForm>, 'watch' | 'getValues' | 'setValue'>) => {
const userElasticsearchQuery = watch(ChatFormFields.userElasticsearchQuery);
const elasticsearchQuery = watch(ChatFormFields.elasticsearchQuery);
}: Pick<UseFormReturn<PlaygroundForm>, 'watch' | 'getValues' | 'setValue'>) => {
const userElasticsearchQuery = watch(PlaygroundFormFields.userElasticsearchQuery);
const elasticsearchQuery = watch(PlaygroundFormFields.elasticsearchQuery);
const userQueryValidation = useDebounceFn(() => {
const [esQuery, userInputQuery] = getValues([
ChatFormFields.elasticsearchQuery,
ChatFormFields.userElasticsearchQuery,
PlaygroundFormFields.elasticsearchQuery,
PlaygroundFormFields.userElasticsearchQuery,
]);
const validations = validateUserElasticSearchQuery(userInputQuery, esQuery);
setValue(ChatFormFields.userElasticsearchQueryValidations, validations);
setValue(PlaygroundFormFields.userElasticsearchQueryValidations, validations);
}, DEBOUNCE_OPTIONS);
useEffect(() => {
userQueryValidation.run();

View file

@ -7,10 +7,10 @@
import React, { useMemo } from 'react';
import { KibanaPageTemplate } from '@kbn/shared-ux-page-kibana-template';
import { PlaygroundProvider } from './providers/playground_provider';
import { UnsavedFormProvider } from './providers/unsaved_form_provider';
import { useKibana } from './hooks/use_kibana';
import { App } from './components/app';
import { Playground } from './components/playgorund';
import { usePlaygroundBreadcrumbs } from './hooks/use_playground_breadcrumbs';
export const PlaygroundOverview = () => {
@ -25,18 +25,18 @@ export const PlaygroundOverview = () => {
);
return (
<PlaygroundProvider>
<KibanaPageTemplate
offset={0}
restrictWidth={false}
data-test-subj="svlPlaygroundPage"
grow={false}
panelled={false}
solutionNav={searchNavigation?.useClassicNavigation(history)}
>
<App showDocs />
{embeddableConsole}
</KibanaPageTemplate>
</PlaygroundProvider>
<KibanaPageTemplate
offset={0}
restrictWidth={false}
data-test-subj="svlPlaygroundPage"
grow={false}
panelled={false}
solutionNav={searchNavigation?.useClassicNavigation(history)}
>
<UnsavedFormProvider>
<Playground showDocs />
</UnsavedFormProvider>
{embeddableConsole}
</KibanaPageTemplate>
);
};

View file

@ -1,19 +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 React, { FC } from 'react';
import { QueryClientProvider } from '@tanstack/react-query';
import { queryClient } from '../utils/query_client';
import { FormProvider } from './form_provider';
export const PlaygroundProvider: FC<React.PropsWithChildren<{}>> = ({ children }) => {
return (
<QueryClientProvider client={queryClient}>
<FormProvider>{children}</FormProvider>
</QueryClientProvider>
);
};

View file

@ -7,11 +7,11 @@
import React from 'react';
import { render, waitFor, act } from '@testing-library/react';
import { FormProvider, LOCAL_STORAGE_KEY } from './form_provider';
import { UnsavedFormProvider, LOCAL_STORAGE_KEY } from './unsaved_form_provider';
import { useLoadFieldsByIndices } from '../hooks/use_load_fields_by_indices';
import { useLLMsModels } from '../hooks/use_llms_models';
import * as ReactHookForm from 'react-hook-form';
import { ChatForm, ChatFormFields } from '../types';
import { PlaygroundForm, PlaygroundFormFields } from '../types';
import { useSearchParams } from 'react-router-dom-v5-compat';
jest.mock('../hooks/use_load_fields_by_indices');
@ -46,7 +46,7 @@ const localStorageMock = (() => {
};
})() as Storage;
const DEFAULT_FORM_STATE: Partial<ChatForm> = {
const DEFAULT_FORM_STATE: Partial<PlaygroundForm> = {
doc_size: 3,
prompt: 'You are an assistant for question-answering tasks.',
source_fields: {},
@ -60,7 +60,7 @@ const DEFAULT_FORM_STATE: Partial<ChatForm> = {
},
};
describe('FormProvider', () => {
describe('UnsavedFormProvider', () => {
beforeEach(() => {
formHookSpy = jest.spyOn(ReactHookForm, 'useForm');
mockUseLLMsModels.mockReturnValue([]);
@ -74,9 +74,9 @@ describe('FormProvider', () => {
it('renders the form provider with initial values, no default model', async () => {
render(
<FormProvider storage={localStorageMock}>
<UnsavedFormProvider storage={localStorageMock}>
<div>Test Child Component</div>
</FormProvider>
</UnsavedFormProvider>
);
const { getValues } = formHookSpy.mock.results[0].value;
@ -95,9 +95,9 @@ describe('FormProvider', () => {
mockUseLLMsModels.mockReturnValueOnce(mockModels);
render(
<FormProvider storage={localStorageMock}>
<UnsavedFormProvider storage={localStorageMock}>
<div>Test Child Component</div>
</FormProvider>
</UnsavedFormProvider>
);
await waitFor(() => {
@ -105,7 +105,7 @@ describe('FormProvider', () => {
const defaultModel = mockModels.find((model) => !model.disabled);
const { getValues } = formHookSpy.mock.results[0].value;
expect(getValues(ChatFormFields.summarizationModel)).toEqual(defaultModel);
expect(getValues(PlaygroundFormFields.summarizationModel)).toEqual(defaultModel);
});
});
@ -118,9 +118,9 @@ describe('FormProvider', () => {
mockUseLLMsModels.mockReturnValueOnce(modelsWithAllDisabled);
render(
<FormProvider storage={localStorageMock}>
<UnsavedFormProvider storage={localStorageMock}>
<div>Test Child Component</div>
</FormProvider>
</UnsavedFormProvider>
);
await waitFor(() => {
@ -128,23 +128,23 @@ describe('FormProvider', () => {
});
expect(
formHookSpy.mock.results[0].value.getValues(ChatFormFields.summarizationModel)
formHookSpy.mock.results[0].value.getValues(PlaygroundFormFields.summarizationModel)
).toBeUndefined();
});
it('saves form state to localStorage', async () => {
render(
<FormProvider storage={localStorageMock}>
<UnsavedFormProvider storage={localStorageMock}>
<div>Test Child Component</div>
</FormProvider>
</UnsavedFormProvider>
);
const { setValue } = formHookSpy.mock.results[0].value;
act(() => {
setValue(ChatFormFields.prompt, 'New prompt');
setValue(PlaygroundFormFields.prompt, 'New prompt');
// omit question from the session state
setValue(ChatFormFields.question, 'dont save me');
setValue(PlaygroundFormFields.question, 'dont save me');
});
await waitFor(() => {
@ -174,9 +174,9 @@ describe('FormProvider', () => {
);
render(
<FormProvider storage={localStorageMock}>
<UnsavedFormProvider storage={localStorageMock}>
<div>Test Child Component</div>
</FormProvider>
</UnsavedFormProvider>
);
const { getValues } = formHookSpy.mock.results[0].value;
@ -209,9 +209,9 @@ describe('FormProvider', () => {
);
render(
<FormProvider storage={localStorageMock}>
<UnsavedFormProvider storage={localStorageMock}>
<div>Test Child Component</div>
</FormProvider>
</UnsavedFormProvider>
);
const { getValues } = formHookSpy.mock.results[0].value;
@ -243,16 +243,16 @@ describe('FormProvider', () => {
);
render(
<FormProvider storage={localStorageMock}>
<UnsavedFormProvider storage={localStorageMock}>
<div>Test Child Component</div>
</FormProvider>
</UnsavedFormProvider>
);
await act(async () => {
const { getValues } = formHookSpy.mock.results[0].value;
await waitFor(() => {
expect(getValues(ChatFormFields.indices)).toEqual(['new-index']);
expect(getValues(PlaygroundFormFields.indices)).toEqual(['new-index']);
});
});
});

View file

@ -11,24 +11,24 @@ import { useDebounceFn } from '@kbn/react-hooks';
import { useIndicesValidation } from '../hooks/use_indices_validation';
import { useLoadFieldsByIndices } from '../hooks/use_load_fields_by_indices';
import { useUserQueryValidations } from '../hooks/use_user_query_validations';
import { ChatForm, ChatFormFields } from '../types';
import { PlaygroundForm, PlaygroundFormFields } from '../types';
import { useLLMsModels } from '../hooks/use_llms_models';
type PartialChatForm = Partial<ChatForm>;
type PartialPlaygroundForm = Partial<PlaygroundForm>;
export const LOCAL_STORAGE_KEY = 'search_playground_session';
export const LOCAL_STORAGE_DEBOUNCE_OPTIONS = { wait: 100 };
const DEFAULT_FORM_VALUES: PartialChatForm = {
const DEFAULT_FORM_VALUES: PartialPlaygroundForm = {
prompt: 'You are an assistant for question-answering tasks.',
doc_size: 3,
source_fields: {},
indices: [],
summarization_model: undefined,
[ChatFormFields.userElasticsearchQuery]: null,
[ChatFormFields.userElasticsearchQueryValidations]: undefined,
[PlaygroundFormFields.userElasticsearchQuery]: null,
[PlaygroundFormFields.userElasticsearchQueryValidations]: undefined,
};
const getLocalSession = (storage: Storage): PartialChatForm => {
const getLocalSession = (storage: Storage): PartialPlaygroundForm => {
try {
const localSessionJSON = storage.getItem(LOCAL_STORAGE_KEY);
const sessionState = localSessionJSON ? JSON.parse(localSessionJSON) : {};
@ -42,23 +42,23 @@ const getLocalSession = (storage: Storage): PartialChatForm => {
}
};
const setLocalSession = (formState: PartialChatForm, storage: Storage) => {
const setLocalSession = (formState: PartialPlaygroundForm, storage: Storage) => {
// omit question and search_query from the session state
const {
question,
search_query: _searchQuery,
[ChatFormFields.userElasticsearchQueryValidations]: _queryValidations,
[PlaygroundFormFields.userElasticsearchQueryValidations]: _queryValidations,
...state
} = formState;
storage.setItem(LOCAL_STORAGE_KEY, JSON.stringify(state));
};
interface FormProviderProps {
interface UnsavedFormProviderProps {
storage?: Storage;
}
export const FormProvider: React.FC<React.PropsWithChildren<FormProviderProps>> = ({
export const UnsavedFormProvider: React.FC<React.PropsWithChildren<UnsavedFormProviderProps>> = ({
children,
storage = localStorage,
}) => {
@ -69,8 +69,8 @@ export const FormProvider: React.FC<React.PropsWithChildren<FormProviderProps>>
return index ? [index] : null;
}, [searchParams]);
const sessionState: PartialChatForm = useMemo(() => getLocalSession(storage), [storage]);
const form = useForm<ChatForm>({
const sessionState: PartialPlaygroundForm = useMemo(() => getLocalSession(storage), [storage]);
const form = useForm<PlaygroundForm>({
defaultValues: {
...sessionState,
indices: [],
@ -94,23 +94,23 @@ export const FormProvider: React.FC<React.PropsWithChildren<FormProviderProps>>
const setLocalSessionDebounce = useDebounceFn(setLocalSession, LOCAL_STORAGE_DEBOUNCE_OPTIONS);
useEffect(() => {
const subscription = form.watch((values) =>
setLocalSessionDebounce.run(values as PartialChatForm, storage)
setLocalSessionDebounce.run(values as PartialPlaygroundForm, storage)
);
return () => subscription.unsubscribe();
}, [form, storage, setLocalSessionDebounce]);
useEffect(() => {
const defaultModel = models.find((model) => !model.disabled);
const currentModel = form.getValues(ChatFormFields.summarizationModel);
const currentModel = form.getValues(PlaygroundFormFields.summarizationModel);
if (defaultModel && (!currentModel || !models.find((model) => currentModel.id === model.id))) {
form.setValue(ChatFormFields.summarizationModel, defaultModel);
form.setValue(PlaygroundFormFields.summarizationModel, defaultModel);
}
}, [form, models]);
useEffect(() => {
if (isValidatedIndices) {
form.setValue(ChatFormFields.indices, validIndices);
form.setValue(PlaygroundFormFields.indices, validIndices);
}
}, [form, isValidatedIndices, validIndices]);

View file

@ -71,7 +71,7 @@ export interface AppPluginStartDependencies {
export type AppServicesContext = CoreStart & AppPluginStartDependencies;
export enum ChatFormFields {
export enum PlaygroundFormFields {
question = 'question',
citations = 'citations',
prompt = 'prompt',
@ -86,19 +86,19 @@ export enum ChatFormFields {
searchQuery = 'search_query',
}
export interface ChatForm {
[ChatFormFields.question]: string;
[ChatFormFields.prompt]: string;
[ChatFormFields.citations]: boolean;
[ChatFormFields.indices]: string[];
[ChatFormFields.summarizationModel]: LLMModel;
[ChatFormFields.elasticsearchQuery]: { retriever: any }; // RetrieverContainer leads to "Type instantiation is excessively deep and possibly infinite" error
[ChatFormFields.sourceFields]: { [index: string]: string[] };
[ChatFormFields.docSize]: number;
[ChatFormFields.queryFields]: { [index: string]: string[] };
[ChatFormFields.searchQuery]: string;
[ChatFormFields.userElasticsearchQuery]: string | null | undefined;
[ChatFormFields.userElasticsearchQueryValidations]: UserQueryValidations | undefined;
export interface PlaygroundForm {
[PlaygroundFormFields.question]: string;
[PlaygroundFormFields.prompt]: string;
[PlaygroundFormFields.citations]: boolean;
[PlaygroundFormFields.indices]: string[];
[PlaygroundFormFields.summarizationModel]: LLMModel;
[PlaygroundFormFields.elasticsearchQuery]: { retriever: any }; // RetrieverContainer leads to "Type instantiation is excessively deep and possibly infinite" error
[PlaygroundFormFields.sourceFields]: { [index: string]: string[] };
[PlaygroundFormFields.docSize]: number;
[PlaygroundFormFields.queryFields]: { [index: string]: string[] };
[PlaygroundFormFields.searchQuery]: string;
[PlaygroundFormFields.userElasticsearchQuery]: string | null | undefined;
[PlaygroundFormFields.userElasticsearchQueryValidations]: UserQueryValidations | undefined;
}
export interface UserQueryValidations {

View file

@ -7,11 +7,11 @@
import deepEqual from 'fast-deep-equal';
import { i18n } from '@kbn/i18n';
import type { ChatForm, ChatFormFields, UserQueryValidations } from '../types';
import type { PlaygroundForm, PlaygroundFormFields, UserQueryValidations } from '../types';
export const validateUserElasticSearchQuery = (
userQuery: ChatForm[ChatFormFields.userElasticsearchQuery],
elasticsearchQuery: ChatForm[ChatFormFields.elasticsearchQuery]
userQuery: PlaygroundForm[PlaygroundFormFields.userElasticsearchQuery],
elasticsearchQuery: PlaygroundForm[PlaygroundFormFields.elasticsearchQuery]
): UserQueryValidations => {
if (userQuery === null || userQuery === undefined || typeof userQuery !== 'string') {
return { isValid: false, isUserCustomized: false };
@ -59,9 +59,9 @@ export const disableExecuteQuery = (
};
export const elasticsearchQueryString = (
elasticsearchQuery: ChatForm[ChatFormFields.elasticsearchQuery],
userElasticsearchQuery: ChatForm[ChatFormFields.userElasticsearchQuery],
userElasticsearchQueryValidations: ChatForm[ChatFormFields.userElasticsearchQueryValidations]
elasticsearchQuery: PlaygroundForm[PlaygroundFormFields.elasticsearchQuery],
userElasticsearchQuery: PlaygroundForm[PlaygroundFormFields.userElasticsearchQuery],
userElasticsearchQueryValidations: PlaygroundForm[PlaygroundFormFields.userElasticsearchQueryValidations]
) => {
if (!userElasticsearchQuery || userElasticsearchQueryValidations?.isUserCustomized === false) {
return JSON.stringify(elasticsearchQuery);
@ -73,9 +73,9 @@ export const elasticsearchQueryString = (
};
export const elasticsearchQueryObject = (
elasticsearchQuery: ChatForm[ChatFormFields.elasticsearchQuery],
userElasticsearchQuery: ChatForm[ChatFormFields.userElasticsearchQuery],
userElasticsearchQueryValidations: ChatForm[ChatFormFields.userElasticsearchQueryValidations]
elasticsearchQuery: PlaygroundForm[PlaygroundFormFields.elasticsearchQuery],
userElasticsearchQuery: PlaygroundForm[PlaygroundFormFields.userElasticsearchQuery],
userElasticsearchQueryValidations: PlaygroundForm[PlaygroundFormFields.userElasticsearchQueryValidations]
): { retriever: any } => {
if (!userElasticsearchQuery || userElasticsearchQueryValidations?.isUserCustomized === false) {
return elasticsearchQuery;
@ -87,5 +87,5 @@ export const elasticsearchQueryObject = (
};
export const formatElasticsearchQueryString = (
elasticsearchQuery: ChatForm[ChatFormFields.elasticsearchQuery]
elasticsearchQuery: PlaygroundForm[PlaygroundFormFields.elasticsearchQuery]
) => JSON.stringify(elasticsearchQuery, null, 2);