diff --git a/x-pack/plugins/enterprise_search/public/applications/applications/components/playground/playground.tsx b/x-pack/plugins/enterprise_search/public/applications/applications/components/playground/playground.tsx index e6882acb0ac3b..b117518d3a6e0 100644 --- a/x-pack/plugins/enterprise_search/public/applications/applications/components/playground/playground.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/applications/components/playground/playground.tsx @@ -9,11 +9,8 @@ import React from 'react'; import { useValues } from 'kea'; -import { EuiBetaBadge, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { FormattedMessage } from '@kbn/i18n-react'; - import { KibanaLogic } from '../../../shared/kibana'; import { EnterpriseSearchApplicationsPageTemplate } from '../layout/page_template'; @@ -31,32 +28,9 @@ export const Playground: React.FC = () => { defaultMessage: 'Playground', }), ]} - pageHeader={{ - pageTitle: ( - - - - - - - - - ), - rightSideItems: [], - }} pageViewTelemetry="Playground" restrictWidth={false} + panelled={false} customPageSections bottomBorder="extended" docLink="playground" diff --git a/x-pack/plugins/search_playground/__mocks__/search_playground_mock.ts b/x-pack/plugins/search_playground/__mocks__/search_playground_mock.ts index d1bde7eaf53a5..1a73128730324 100644 --- a/x-pack/plugins/search_playground/__mocks__/search_playground_mock.ts +++ b/x-pack/plugins/search_playground/__mocks__/search_playground_mock.ts @@ -12,7 +12,6 @@ export type Start = jest.Mocked; const createStartMock = (): Start => { const startContract: Start = { PlaygroundProvider: jest.fn(), - PlaygroundToolbar: jest.fn(), Playground: jest.fn(), PlaygroundHeaderDocs: jest.fn(), }; diff --git a/x-pack/plugins/search_playground/public/analytics/constants.ts b/x-pack/plugins/search_playground/public/analytics/constants.ts index 30347e67bac82..055b5b74b201b 100644 --- a/x-pack/plugins/search_playground/public/analytics/constants.ts +++ b/x-pack/plugins/search_playground/public/analytics/constants.ts @@ -13,24 +13,21 @@ export enum AnalyticsEvents { chatRegenerateMessages = 'chat_regenerate_messages', citationDetailsExpanded = 'citation_details_expanded', citationDetailsCollapsed = 'citation_details_collapsed', - editContextFlyoutOpened = 'edit_context_flyout_opened', editContextFieldToggled = 'edit_context_field_toggled', editContextDocSizeChanged = 'edit_context_doc_size_changed', - editContextSaved = 'edit_context_saved', genAiConnectorAdded = 'gen_ai_connector_added', genAiConnectorCreated = 'gen_ai_connector_created', genAiConnectorExists = 'gen_ai_connector_exists', genAiConnectorSetup = 'gen_ai_connector_setup', includeCitations = 'include_citations', instructionsFieldChanged = 'instructions_field_changed', + queryFieldsUpdated = 'view_query_fields_updated', + queryModeLoaded = 'query_mode_loaded', modelSelected = 'model_selected', retrievalDocsFlyoutOpened = 'retrieval_docs_flyout_opened', sourceFieldsLoaded = 'source_fields_loaded', sourceIndexUpdated = 'source_index_updated', - startNewChatPageLoaded = 'start_new_chat_page_loaded', - viewQueryFlyoutOpened = 'view_query_flyout_opened', - viewQueryFieldsUpdated = 'view_query_fields_updated', - viewQuerySaved = 'view_query_saved', + setupChatPageLoaded = 'start_new_chat_page_loaded', viewCodeFlyoutOpened = 'view_code_flyout_opened', viewCodeLanguageChange = 'view_code_language_change', } diff --git a/x-pack/plugins/search_playground/public/chat_playground_overview.tsx b/x-pack/plugins/search_playground/public/chat_playground_overview.tsx index fc350356e873a..b5d753e6044f2 100644 --- a/x-pack/plugins/search_playground/public/chat_playground_overview.tsx +++ b/x-pack/plugins/search_playground/public/chat_playground_overview.tsx @@ -5,15 +5,13 @@ * 2.0. */ -import { i18n } from '@kbn/i18n'; import React, { useMemo } from 'react'; -import { EuiBetaBadge, EuiFlexGroup, EuiFlexItem, EuiPageTemplate, EuiTitle } from '@elastic/eui'; -import { FormattedMessage } from '@kbn/i18n-react'; +import { EuiPageTemplate } from '@elastic/eui'; +import { QueryClientProvider } from '@tanstack/react-query'; +import { queryClient } from './utils/query_client'; import { PlaygroundProvider } from './providers/playground_provider'; import { App } from './components/app'; -import { PlaygroundToolbar } from './embeddable'; -import { PlaygroundHeaderDocs } from './components/playground_header_docs'; import { useKibana } from './hooks/use_kibana'; export const ChatPlaygroundOverview: React.FC = () => { @@ -27,46 +25,19 @@ export const ChatPlaygroundOverview: React.FC = () => { ); return ( - - - .euiFlexGroup': { flexWrap: 'wrap' } }} - pageTitle={ - - - -

- -

-
-
- - - -
- } - data-test-subj="chat-playground-home-page" - rightSideItems={[, ]} - /> - - {embeddableConsole} -
-
+ + + + + {embeddableConsole} + + + ); }; diff --git a/x-pack/plugins/search_playground/public/components/app.tsx b/x-pack/plugins/search_playground/public/components/app.tsx index 854be8d3ac760..b779897dc4afa 100644 --- a/x-pack/plugins/search_playground/public/components/app.tsx +++ b/x-pack/plugins/search_playground/public/components/app.tsx @@ -5,28 +5,61 @@ * 2.0. */ -import React, { useState } from 'react'; +import React, { useEffect, useState } from 'react'; import { KibanaPageTemplate } from '@kbn/shared-ux-page-kibana-template'; -import { StartNewChat } from './start_new_chat'; +import { useFormContext } from 'react-hook-form'; +import { QueryMode } from './query_mode/query_mode'; +import { SetupPage } from './setup_page/setup_page'; +import { Header } from './header'; +import { useLoadConnectors } from '../hooks/use_load_connectors'; +import { ChatForm, ChatFormFields } from '../types'; import { Chat } from './chat'; -export const App: React.FC = () => { - const [showStartPage, setShowStartPage] = useState(true); +export interface AppProps { + showDocs?: boolean; +} + +export enum ViewMode { + chat = 'chat', + query = 'query', +} + +export const App: React.FC = ({ showDocs = false }) => { + const [showSetupPage, setShowSetupPage] = useState(true); + const [selectedMode, setSelectedMode] = useState(ViewMode.chat); + const { watch } = useFormContext(); + const { data: connectors } = useLoadConnectors(); + const hasSelectedIndices = watch(ChatFormFields.indices).length; + const handleModeChange = (id: string) => setSelectedMode(id as ViewMode); + + useEffect(() => { + if (showSetupPage && connectors?.length && hasSelectedIndices) { + setShowSetupPage(false); + } + }, [connectors, hasSelectedIndices, showSetupPage]); return ( - - {showStartPage ? setShowStartPage(false)} /> : } - + <> +
+ + {showSetupPage ? : selectedMode === ViewMode.chat ? : } + + ); }; diff --git a/x-pack/plugins/search_playground/public/components/chat.tsx b/x-pack/plugins/search_playground/public/components/chat.tsx index de0a296e162b0..cc4c0b1ccdff2 100644 --- a/x-pack/plugins/search_playground/public/components/chat.tsx +++ b/x-pack/plugins/search_playground/public/components/chat.tsx @@ -13,6 +13,7 @@ import { EuiFlexGroup, EuiFlexItem, EuiForm, + EuiHideFor, EuiHorizontalRule, EuiSpacer, useEuiTheme, @@ -50,14 +51,12 @@ export const Chat = () => { const { euiTheme } = useEuiTheme(); const { control, - watch, formState: { isValid, isSubmitting }, resetField, handleSubmit, getValues, } = useFormContext(); const { messages, append, stop: stopRequest, setMessages, reload, error } = useChat(); - const selectedIndicesCount = watch(ChatFormFields.indices, []).length; const messagesRef = useAutoBottomScroll(); const [isRegenerating, setIsRegenerating] = useState(false); const usageTracker = useUsageTracker(); @@ -123,7 +122,6 @@ export const Chat = () => { { > - + - + { iconType="refresh" disabled={isToolBarActionsDisabled} onClick={handleClearChat} + size="xs" data-test-subj="clearChatActionButton" > { - + { - - - + + + + + ); diff --git a/x-pack/plugins/search_playground/public/components/chat_sidebar.tsx b/x-pack/plugins/search_playground/public/components/chat_sidebar.tsx index 2172ee8831ead..5686a2d14b642 100644 --- a/x-pack/plugins/search_playground/public/components/chat_sidebar.tsx +++ b/x-pack/plugins/search_playground/public/components/chat_sidebar.tsx @@ -6,84 +6,91 @@ */ import { - EuiAccordion, + EuiButtonEmpty, EuiFlexGroup, EuiFlexItem, - EuiSpacer, - EuiText, + EuiLink, EuiTitle, useEuiTheme, - useGeneratedHtmlId, } from '@elastic/eui'; -import React, { useState } from 'react'; +import React from 'react'; import { i18n } from '@kbn/i18n'; -import { SourcesPanelSidebar } from './sources_panel/sources_panel_sidebar'; +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 { useManagementLink } from '../hooks/use_management_link'; import { SummarizationPanel } from './summarization_panel/summarization_panel'; -interface ChatSidebarProps { - selectedIndicesCount: number; -} - -export const ChatSidebar: React.FC = ({ selectedIndicesCount }) => { +export const ChatSidebar: React.FC = () => { const { euiTheme } = useEuiTheme(); - const accordions = [ + const selectedModel = useWatch({ + name: ChatFormFields.summarizationModel, + }); + const managementLink = useManagementLink(selectedModel?.connectorId); + const panels = [ { - id: useGeneratedHtmlId({ prefix: 'summarizationAccordion' }), title: i18n.translate('xpack.searchPlayground.sidebar.summarizationTitle', { defaultMessage: 'Model settings', }), children: , - dataTestId: 'summarizationAccordion', + extraAction: ( + + + + ), }, { - id: useGeneratedHtmlId({ prefix: 'sourcesAccordion' }), - title: i18n.translate('xpack.searchPlayground.sidebar.sourceTitle', { - defaultMessage: 'Indices', + title: i18n.translate('xpack.searchPlayground.sidebar.contextTitle', { + defaultMessage: 'Context', }), - extraAction: !!selectedIndicesCount && ( - -

- {i18n.translate('xpack.searchPlayground.sidebar.sourceIndicesCount', { - defaultMessage: '{count, number} {count, plural, one {Index} other {Indices}}', - values: { count: Number(selectedIndicesCount) }, - })} -

-
+ extraAction: ( + + + ), - children: , - dataTestId: 'sourcesAccordion', + children: , }, ]; - const [openAccordionId, setOpenAccordionId] = useState(accordions[0].id); return ( - {accordions.map(({ id, title, extraAction, children, dataTestId }, index) => ( - - -
{title}
- - } - extraAction={extraAction} - buttonProps={{ paddingSize: 'l' }} - forceState={openAccordionId === id ? 'open' : 'closed'} - onToggle={() => setOpenAccordionId(openAccordionId === id ? '' : id)} - data-test-subj={dataTestId} - > - {children} - -
+ {panels?.map(({ title, children, extraAction }) => ( + + + + + +

{title}

+
+ {extraAction && {extraAction}} +
+
+ {children} +
))}
diff --git a/x-pack/plugins/search_playground/public/components/data_action_button.tsx b/x-pack/plugins/search_playground/public/components/data_action_button.tsx new file mode 100644 index 0000000000000..abc2a0c265cf9 --- /dev/null +++ b/x-pack/plugins/search_playground/public/components/data_action_button.tsx @@ -0,0 +1,37 @@ +/* + * 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, { 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 { SelectIndicesFlyout } from './select_indices_flyout'; + +export const DataActionButton: React.FC = () => { + const selectedIndices = useWatch({ + name: ChatFormFields.indices, + }); + const [showFlyout, setShowFlyout] = useState(false); + const handleFlyoutClose = () => setShowFlyout(false); + const handleShowFlyout = () => setShowFlyout(true); + + return ( + <> + {showFlyout && } + + + + + ); +}; diff --git a/x-pack/plugins/search_playground/public/components/edit_context/edit_context_action.tsx b/x-pack/plugins/search_playground/public/components/edit_context/edit_context_action.tsx deleted file mode 100644 index 6e9a638248660..0000000000000 --- a/x-pack/plugins/search_playground/public/components/edit_context/edit_context_action.tsx +++ /dev/null @@ -1,37 +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, { useState } from 'react'; -import { EuiButtonEmpty } from '@elastic/eui'; -import { useFormContext } from 'react-hook-form'; -import { FormattedMessage } from '@kbn/i18n-react'; -import { ChatForm } from '../../types'; -import { EditContextFlyout } from './edit_context_flyout'; - -export const EditContextAction: React.FC = () => { - const { watch } = useFormContext(); - const [showFlyout, setShowFlyout] = useState(false); - const selectedIndices: string[] = watch('indices'); - - const closeFlyout = () => setShowFlyout(false); - - return ( - <> - {showFlyout && } - setShowFlyout(true)} - disabled={selectedIndices?.length === 0} - data-test-subj="editContextActionButton" - > - - - - ); -}; diff --git a/x-pack/plugins/search_playground/public/components/edit_context/edit_context_flyout.tsx b/x-pack/plugins/search_playground/public/components/edit_context/edit_context_flyout.tsx deleted file mode 100644 index cb6968dcce6e4..0000000000000 --- a/x-pack/plugins/search_playground/public/components/edit_context/edit_context_flyout.tsx +++ /dev/null @@ -1,211 +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, { useEffect, useState } from 'react'; -import { - EuiFlyout, - EuiFlyoutHeader, - EuiFlyoutBody, - EuiTitle, - EuiFlexItem, - EuiFlexGroup, - EuiButtonEmpty, - EuiButton, - EuiFlyoutFooter, - EuiLink, - EuiSpacer, - EuiText, - EuiSelect, - EuiSuperSelect, - EuiFormRow, -} from '@elastic/eui'; -import { useController, useFormContext } from 'react-hook-form'; -import { i18n } from '@kbn/i18n'; -import { FormattedMessage } from '@kbn/i18n-react'; -import { docLinks } from '../../../common/doc_links'; -import { useUsageTracker } from '../../hooks/use_usage_tracker'; -import { ChatForm, ChatFormFields } from '../../types'; -import { useIndicesFields } from '../../hooks/use_indices_fields'; -import { getDefaultSourceFields } from '../../utils/create_query'; -import { AnalyticsEvents } from '../../analytics/constants'; - -interface EditContextFlyoutProps { - onClose: () => void; -} - -export const EditContextFlyout: React.FC = ({ onClose }) => { - const usageTracker = useUsageTracker(); - const { getValues } = useFormContext(); - const selectedIndices: string[] = getValues(ChatFormFields.indices); - const { fields } = useIndicesFields(selectedIndices); - const defaultFields = getDefaultSourceFields(fields); - - const { - field: { onChange: onChangeSize, value: docSizeInitialValue }, - } = useController({ - name: ChatFormFields.docSize, - }); - - const [docSize, setDocSize] = useState(docSizeInitialValue); - - const { - field: { onChange: onChangeSourceFields, value: sourceFields }, - } = useController({ - name: ChatFormFields.sourceFields, - defaultValue: defaultFields, - }); - - const [tempSourceFields, setTempSourceFields] = useState(sourceFields); - - const updateSourceField = (index: string, field: string) => { - setTempSourceFields({ - ...tempSourceFields, - [index]: [field], - }); - usageTracker?.click(AnalyticsEvents.editContextFieldToggled); - }; - - const saveSourceFields = () => { - usageTracker?.click(AnalyticsEvents.editContextSaved); - onChangeSourceFields(tempSourceFields); - onChangeSize(docSize); - onClose(); - }; - - const handleDocSizeChange = (e: React.ChangeEvent) => { - usageTracker?.click(AnalyticsEvents.editContextDocSizeChanged); - setDocSize(Number(e.target.value)); - }; - - useEffect(() => { - usageTracker?.load(AnalyticsEvents.editContextFlyoutOpened); - }, [usageTracker]); - - return ( - - - -

- -

-
- - -

- - - - -

-
-
- - - - - - - - - - - - - -
- -
-
-
- {Object.entries(fields).map(([index, group]) => ( - - - ({ - value: field, - inputDisplay: field, - 'data-test-subj': 'contextField', - }))} - valueOfSelected={tempSourceFields[index]?.[0]} - onChange={(value) => updateSourceField(index, value)} - /> - - - ))} -
-
-
-
-
-
- - - - - - - - - - - - - - -
- ); -}; diff --git a/x-pack/plugins/search_playground/public/components/edit_context/edit_context_flyout.test.tsx b/x-pack/plugins/search_playground/public/components/edit_context/edit_context_panel.test.tsx similarity index 67% rename from x-pack/plugins/search_playground/public/components/edit_context/edit_context_flyout.test.tsx rename to x-pack/plugins/search_playground/public/components/edit_context/edit_context_panel.test.tsx index de82892b167be..ac86efd665b27 100644 --- a/x-pack/plugins/search_playground/public/components/edit_context/edit_context_flyout.test.tsx +++ b/x-pack/plugins/search_playground/public/components/edit_context/edit_context_panel.test.tsx @@ -7,12 +7,13 @@ import React from 'react'; import { render, fireEvent, screen } from '@testing-library/react'; -import { EditContextFlyout } from './edit_context_flyout'; +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'; -jest.mock('../../hooks/use_indices_fields', () => ({ - useIndicesFields: () => ({ +jest.mock('../../hooks/use_source_indices_field', () => ({ + useSourceIndicesFields: () => ({ fields: { index1: { elser_query_fields: [], @@ -43,9 +44,9 @@ jest.mock('../../hooks/use_usage_tracker', () => ({ const MockFormProvider = ({ children }: { children: React.ReactElement }) => { const methods = useForm({ values: { - indices: ['index1'], - docSize: 1, - sourceFields: { + [ChatFormFields.indices]: ['index1'], + [ChatFormFields.docSize]: 1, + [ChatFormFields.sourceFields]: { index1: ['context_field1'], index2: ['context_field2'], }, @@ -55,27 +56,20 @@ const MockFormProvider = ({ children }: { children: React.ReactElement }) => { }; describe('EditContextFlyout component tests', () => { - const onCloseMock = jest.fn(); - beforeEach(() => { render( - + ); }); - it('calls onClose when the close button is clicked', () => { - fireEvent.click(screen.getByRole('button', { name: 'Close' })); - expect(onCloseMock).toHaveBeenCalledTimes(1); - }); - it('should see the context fields', async () => { - expect(screen.getByTestId('contextFieldsSelectable_index1')).toBeInTheDocument(); - fireEvent.click(screen.getByTestId('contextFieldsSelectable_index1')); - expect(screen.getByRole('option', { name: 'context_field1' })).toBeInTheDocument(); - expect(screen.getByRole('option', { name: 'context_field2' })).toBeInTheDocument(); + expect(screen.getByTestId('contextFieldsSelectable-0')).toBeInTheDocument(); + fireEvent.click(screen.getByTestId('contextFieldsSelectable-0')); + const fields = await screen.findAllByTestId('contextField'); + expect(fields.length).toBe(2); }); }); diff --git a/x-pack/plugins/search_playground/public/components/edit_context/edit_context_panel.tsx b/x-pack/plugins/search_playground/public/components/edit_context/edit_context_panel.tsx new file mode 100644 index 0000000000000..1283730efa9cc --- /dev/null +++ b/x-pack/plugins/search_playground/public/components/edit_context/edit_context_panel.tsx @@ -0,0 +1,137 @@ +/* + * 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 { + EuiFlexGroup, + EuiFlexItem, + EuiFormRow, + EuiPanel, + EuiSelect, + EuiSuperSelect, + EuiText, + EuiCallOut, +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n-react'; +import React 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 { AnalyticsEvents } from '../../analytics/constants'; + +export const EditContextPanel: React.FC = () => { + const usageTracker = useUsageTracker(); + const { fields } = useSourceIndicesFields(); + + const { + field: { onChange: onChangeSize, value: docSize }, + } = useController({ + name: ChatFormFields.docSize, + }); + + const { + field: { onChange: onChangeSourceFields, value: sourceFields }, + } = useController({ + name: ChatFormFields.sourceFields, + }); + + const updateSourceField = (index: string, field: string) => { + onChangeSourceFields({ + ...sourceFields, + [index]: [field], + }); + usageTracker?.click(AnalyticsEvents.editContextFieldToggled); + }; + + const handleDocSizeChange = (e: React.ChangeEvent) => { + usageTracker?.click(AnalyticsEvents.editContextDocSizeChanged); + onChangeSize(Number(e.target.value)); + }; + + return ( + + + + + + + + + + + +
+ +
+
+
+ {Object.entries(fields).map(([index, group], indexNum) => ( + + + {!!group.source_fields?.length ? ( + ({ + value: field, + inputDisplay: field, + 'data-test-subj': 'contextField', + }))} + valueOfSelected={sourceFields[index]?.[0]} + onChange={(value) => updateSourceField(index, value)} + fullWidth + /> + ) : ( + + )} + + + ))} +
+
+
+
+ ); +}; diff --git a/x-pack/plugins/search_playground/public/components/header.tsx b/x-pack/plugins/search_playground/public/components/header.tsx new file mode 100644 index 0000000000000..bc468a0e4be87 --- /dev/null +++ b/x-pack/plugins/search_playground/public/components/header.tsx @@ -0,0 +1,103 @@ +/* + * 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 { + EuiBetaBadge, + EuiButtonGroup, + EuiFlexGroup, + EuiPageHeaderSection, + EuiPageTemplate, + EuiTitle, + useEuiTheme, +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n-react'; +import React from 'react'; +import { PlaygroundHeaderDocs } from './playground_header_docs'; +import { Toolbar } from './toolbar'; +import { ViewMode } from './app'; + +interface HeaderProps { + showDocs?: boolean; + selectedMode: string; + onModeChange: (mode: string) => void; + isActionsDisabled?: boolean; +} + +export const Header: React.FC = ({ + selectedMode, + onModeChange, + showDocs = false, + isActionsDisabled = false, +}) => { + const { euiTheme } = useEuiTheme(); + const options = [ + { + id: ViewMode.chat, + label: i18n.translate('xpack.searchPlayground.header.view.chat', { + defaultMessage: 'Chat', + }), + 'data-test-subj': 'chatMode', + }, + { + id: ViewMode.query, + label: i18n.translate('xpack.searchPlayground.header.view.query', { + defaultMessage: 'Query', + }), + 'data-test-subj': 'queryMode', + }, + ]; + + return ( + .euiFlexGroup': { flexWrap: 'wrap' }, + backgroundColor: euiTheme.colors.ghost, + }} + paddingSize="s" + data-test-subj="chat-playground-home-page" + > + + + +

+ +

+
+ +
+
+ + + + + + {showDocs && } + + + +
+ ); +}; diff --git a/x-pack/plugins/search_playground/public/components/message_list/assistant_message.tsx b/x-pack/plugins/search_playground/public/components/message_list/assistant_message.tsx index 3aa90f188a9cf..7e94059bdc272 100644 --- a/x-pack/plugins/search_playground/public/components/message_list/assistant_message.tsx +++ b/x-pack/plugins/search_playground/public/components/message_list/assistant_message.tsx @@ -17,6 +17,7 @@ import { EuiSpacer, EuiText, EuiTitle, + useEuiTheme, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; @@ -42,6 +43,7 @@ const AIMessageCSS = css` `; export const AssistantMessage: React.FC = ({ message }) => { + const { euiTheme } = useEuiTheme(); const [isDocsFlyoutOpen, setIsDocsFlyoutOpen] = useState(false); const { content, createdAt, citations, retrievalDocs, inputTokens } = message; const username = i18n.translate('xpack.searchPlayground.chat.message.assistant.username', { @@ -55,6 +57,14 @@ export const AssistantMessage: React.FC = ({ message }) = username={username} timelineAvatar="dot" data-test-subj="retrieval-docs-comment" + eventColor="subdued" + css={{ + '.euiAvatar': { backgroundColor: euiTheme.colors.ghost }, + '.euiCommentEvent': { + border: euiTheme.border.thin, + borderRadius: euiTheme.border.radius.medium, + }, + }} event={ <> @@ -96,6 +106,14 @@ export const AssistantMessage: React.FC = ({ message }) = username={username} timelineAvatar="dot" data-test-subj="retrieval-docs-comment-no-docs" + eventColor="subdued" + css={{ + '.euiAvatar': { backgroundColor: euiTheme.colors.ghost }, + '.euiCommentEvent': { + border: euiTheme.border.thin, + borderRadius: euiTheme.border.radius.medium, + }, + }} event={ <> @@ -144,6 +162,13 @@ export const AssistantMessage: React.FC = ({ message }) = }, }) } + css={{ + '.euiAvatar': { backgroundColor: euiTheme.colors.ghost }, + '.euiCommentEvent__body': { + backgroundColor: euiTheme.colors.ghost, + }, + }} + eventColor="subdued" timelineAvatar="compute" timelineAvatarAriaLabel={i18n.translate( 'xpack.searchPlayground.chat.message.assistant.avatarAriaLabel', diff --git a/x-pack/plugins/search_playground/public/components/message_list/system_message.tsx b/x-pack/plugins/search_playground/public/components/message_list/system_message.tsx index 16295299a914c..e973bea3c8a14 100644 --- a/x-pack/plugins/search_playground/public/components/message_list/system_message.tsx +++ b/x-pack/plugins/search_playground/public/components/message_list/system_message.tsx @@ -7,7 +7,7 @@ import React from 'react'; -import { EuiComment } from '@elastic/eui'; +import { EuiComment, useEuiTheme } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; interface SystemMessageProps { @@ -15,6 +15,8 @@ interface SystemMessageProps { } export const SystemMessage: React.FC = ({ content }) => { + const { euiTheme } = useEuiTheme(); + return ( = ({ content }) => { event={content} timelineAvatar="dot" eventColor="subdued" + css={{ + '.euiAvatar': { backgroundColor: euiTheme.colors.ghost }, + '.euiCommentEvent': { + border: euiTheme.border.thin, + borderRadius: euiTheme.border.radius.medium, + }, + }} /> ); }; diff --git a/x-pack/plugins/search_playground/public/components/message_list/user_message.tsx b/x-pack/plugins/search_playground/public/components/message_list/user_message.tsx index 12c1d86283404..8c15ca1b6b69e 100644 --- a/x-pack/plugins/search_playground/public/components/message_list/user_message.tsx +++ b/x-pack/plugins/search_playground/public/components/message_list/user_message.tsx @@ -9,7 +9,7 @@ import React from 'react'; import moment from 'moment'; -import { EuiComment, EuiText } from '@elastic/eui'; +import { EuiComment, EuiText, useEuiTheme } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { UserAvatar } from '@kbn/user-profile-components'; @@ -26,10 +26,17 @@ const UserMessageCSS = css` `; export const UserMessage: React.FC = ({ content, createdAt }) => { + const { euiTheme } = useEuiTheme(); const currentUserProfile = useUserProfile(); return ( ( href={docLinks.chatPlayground} target="_blank" iconType="documents" + size="s" > {i18n.translate('xpack.searchPlayground.pageTitle.header.docLink', { defaultMessage: 'Playground Docs', diff --git a/x-pack/plugins/search_playground/public/components/view_query/view_query_flyout.test.tsx b/x-pack/plugins/search_playground/public/components/query_mode/query_mode.test.tsx similarity index 72% rename from x-pack/plugins/search_playground/public/components/view_query/view_query_flyout.test.tsx rename to x-pack/plugins/search_playground/public/components/query_mode/query_mode.test.tsx index 39136e2557296..ca92bb229e38f 100644 --- a/x-pack/plugins/search_playground/public/components/view_query/view_query_flyout.test.tsx +++ b/x-pack/plugins/search_playground/public/components/query_mode/query_mode.test.tsx @@ -6,14 +6,14 @@ */ import React from 'react'; -import { render, fireEvent, screen } from '@testing-library/react'; -import { ViewQueryFlyout } from './view_query_flyout'; +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'; -jest.mock('../../hooks/use_indices_fields', () => ({ - useIndicesFields: () => ({ +jest.mock('../../hooks/use_source_indices_field', () => ({ + useSourceIndicesFields: () => ({ fields: { index1: { elser_query_fields: [], @@ -30,6 +30,7 @@ jest.mock('../../hooks/use_indices_fields', () => ({ semantic_fields: [], }, }, + isFieldsLoading: false, }), })); @@ -45,6 +46,7 @@ const MockFormProvider = ({ children }: { children: React.ReactElement }) => { const methods = useForm({ values: { [ChatFormFields.indices]: ['index1', 'index2'], + [ChatFormFields.queryFields]: { index1: ['field1'], index2: ['field1'] }, [ChatFormFields.sourceFields]: { index1: ['field1'], index2: ['field1'], @@ -54,24 +56,17 @@ const MockFormProvider = ({ children }: { children: React.ReactElement }) => { return {children}; }; -describe('ViewQueryFlyout component tests', () => { - const onCloseMock = jest.fn(); - +describe('QueryMode component tests', () => { beforeEach(() => { render( - + ); }); - it('calls onClose when the close button is clicked', () => { - fireEvent.click(screen.getByTestId('euiFlyoutCloseButton')); - expect(onCloseMock).toHaveBeenCalledTimes(1); - }); - it('should see the view elasticsearch query', async () => { expect(screen.getByTestId('ViewElasticsearchQueryResult')).toBeInTheDocument(); expect(screen.getByTestId('ViewElasticsearchQueryResult')).toHaveTextContent( @@ -80,14 +75,14 @@ describe('ViewQueryFlyout component tests', () => { }); it('displays query fields and indicates hidden fields', () => { - expect(screen.getByTestId('queryFieldsSelectable_index1')).toBeInTheDocument(); - expect(screen.getByTestId('queryFieldsSelectable_index2')).toBeInTheDocument(); + expect(screen.getByTestId('fieldsAccordion-0')).toBeInTheDocument(); + expect(screen.getByTestId('fieldsAccordion-1')).toBeInTheDocument(); // Check if hidden fields indicator is shown - expect(screen.getByTestId('skipped_fields_index1')).toBeInTheDocument(); - expect(screen.getByTestId('skipped_fields_index1')).toHaveTextContent('1 fields are hidden.'); + expect(screen.getByTestId('skippedFields-0')).toBeInTheDocument(); + expect(screen.getByTestId('skippedFields-0')).toHaveTextContent('1 fields are hidden.'); // Check if hidden fields indicator is shown - expect(screen.queryByTestId('skipped_fields_index2')).not.toBeInTheDocument(); + expect(screen.queryByTestId('skippedFields-1')).not.toBeInTheDocument(); }); }); diff --git a/x-pack/plugins/search_playground/public/components/query_mode/query_mode.tsx b/x-pack/plugins/search_playground/public/components/query_mode/query_mode.tsx new file mode 100644 index 0000000000000..5027f0ac432a2 --- /dev/null +++ b/x-pack/plugins/search_playground/public/components/query_mode/query_mode.tsx @@ -0,0 +1,210 @@ +/* + * 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 { + EuiAccordion, + EuiBasicTable, + EuiCodeBlock, + EuiFlexGroup, + EuiFlexItem, + EuiIcon, + EuiLink, + EuiPanel, + EuiSpacer, + EuiSwitch, + EuiText, + useEuiTheme, +} from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n-react'; +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 { AnalyticsEvents } from '../../analytics/constants'; +import { docLinks } from '../../../common/doc_links'; +import { createQuery } from '../../utils/create_query'; + +const isQueryFieldSelected = ( + queryFields: ChatForm[ChatFormFields.queryFields], + index: string, + field: string +): boolean => { + return Boolean(queryFields[index]?.includes(field)); +}; + +export const QueryMode: React.FC = () => { + const { euiTheme } = useEuiTheme(); + const usageTracker = useUsageTracker(); + const { fields, isFieldsLoading } = useSourceIndicesFields(); + const sourceFields = useWatch({ + name: ChatFormFields.sourceFields, + }); + const { + field: { onChange: queryFieldsOnChange, value: queryFields }, + } = useController({ + name: ChatFormFields.queryFields, + }); + + const { + field: { onChange: elasticsearchQueryChange }, + } = useController({ + name: ChatFormFields.elasticsearchQuery, + }); + + const updateFields = (index: string, fieldName: string, checked: boolean) => { + const currentIndexFields = checked + ? [...queryFields[index], fieldName] + : queryFields[index].filter((field) => fieldName !== field); + const updatedQueryFields = { ...queryFields, [index]: currentIndexFields }; + + queryFieldsOnChange(updatedQueryFields); + elasticsearchQueryChange(createQuery(updatedQueryFields, sourceFields, fields)); + usageTracker?.count(AnalyticsEvents.queryFieldsUpdated, currentIndexFields.length); + }; + + useEffect(() => { + usageTracker?.load(AnalyticsEvents.queryModeLoaded); + }, [usageTracker]); + + const query = useMemo( + () => + !isFieldsLoading && JSON.stringify(createQuery(queryFields, sourceFields, fields), null, 2), + [isFieldsLoading, queryFields, sourceFields, fields] + ); + + return ( + + + + {query} + + + + + +
+ +
+
+ {Object.entries(fields).map(([index, group], indexNum) => ( + + + +
{index}
+
+ } + initialIsOpen + data-test-subj={`fieldsAccordion-${indexNum}`} + > + + + ({ + name: typeof field === 'string' ? field : field.field, + checked: isQueryFieldSelected( + queryFields, + index, + typeof field === 'string' ? field : field.field + ), + }))} + rowHeader="name" + columns={[ + { + field: 'name', + name: 'Field', + 'data-test-subj': 'fieldName', + }, + { + field: 'checked', + name: 'Enabled', + align: 'right', + render: (checked, field) => { + return ( + updateFields(index, field.name, e.target.checked)} + compressed + /> + ); + }, + }, + ]} + /> + + {group.skipped_fields > 0 && ( + <> + + + + + + {` `} + + + + + + + + + + + )} + + + + ))} + + + + ); +}; diff --git a/x-pack/plugins/search_playground/public/components/select_indices_flyout.test.tsx b/x-pack/plugins/search_playground/public/components/select_indices_flyout.test.tsx new file mode 100644 index 0000000000000..94dc1ad037401 --- /dev/null +++ b/x-pack/plugins/search_playground/public/components/select_indices_flyout.test.tsx @@ -0,0 +1,112 @@ +/* + * 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, PropsWithChildren } from 'react'; +import { render, fireEvent, waitFor } from '@testing-library/react'; +import { SelectIndicesFlyout } from './select_indices_flyout'; +import { useSourceIndicesFields } from '../hooks/use_source_indices_field'; +import { useQueryIndices } from '../hooks/use_query_indices'; +import { __IntlProvider as IntlProvider } from '@kbn/i18n-react'; + +jest.mock('../hooks/use_source_indices_field'); +jest.mock('../hooks/use_query_indices'); +jest.mock('../hooks/use_indices_fields', () => ({ + useIndicesFields: () => ({ fields: {} }), +})); + +const Wrapper: FC> = ({ children }) => { + return ( + <> + {children} + + ); +}; + +const mockedUseSourceIndicesFields = useSourceIndicesFields as jest.MockedFunction< + typeof useSourceIndicesFields +>; +const mockedUseQueryIndices = useQueryIndices as jest.MockedFunction; + +describe('SelectIndicesFlyout', () => { + const onCloseMock = jest.fn(); + + beforeEach(() => { + jest.clearAllMocks(); + + mockedUseSourceIndicesFields.mockReturnValue({ + indices: ['index1', 'index2'], + setIndices: jest.fn(), + fields: {}, + loading: false, + addIndex: () => {}, + removeIndex: () => {}, + isFieldsLoading: false, + }); + + mockedUseQueryIndices.mockReturnValue({ + indices: ['index1', 'index2', 'index3'], + isLoading: false, + }); + }); + + it('renders correctly', () => { + const { getByTestId } = render(, { + wrapper: Wrapper, + }); + + expect(getByTestId('indicesTable')).toBeInTheDocument(); + expect(getByTestId('saveButton')).toBeInTheDocument(); + expect(getByTestId('closeButton')).toBeInTheDocument(); + }); + + it('selecting indices and saving', async () => { + const { getByTestId } = render(, { + wrapper: Wrapper, + }); + + fireEvent.click(getByTestId('sourceIndex-2')); + fireEvent.click(getByTestId('saveButton')); + + await waitFor(() => { + expect(mockedUseSourceIndicesFields().setIndices).toHaveBeenCalledWith([ + 'index1', + 'index2', + 'index3', + ]); + expect(onCloseMock).toHaveBeenCalled(); + }); + }); + + it('closing flyout without saving', () => { + const { getByTestId } = render(, { + wrapper: Wrapper, + }); + + fireEvent.click(getByTestId('closeButton')); + + expect(onCloseMock).toHaveBeenCalled(); + }); + + it('save button is disabled when no indices are selected', () => { + mockedUseSourceIndicesFields.mockReturnValueOnce({ + indices: [], + setIndices: jest.fn(), + fields: {}, + loading: false, + addIndex: () => {}, + removeIndex: () => {}, + isFieldsLoading: false, + }); + + const { getByTestId } = render(, { + wrapper: Wrapper, + }); + + const saveButton = getByTestId('saveButton'); + expect(saveButton).toBeDisabled(); + }); +}); diff --git a/x-pack/plugins/search_playground/public/components/select_indices_flyout.tsx b/x-pack/plugins/search_playground/public/components/select_indices_flyout.tsx new file mode 100644 index 0000000000000..d98a6fc9de8b7 --- /dev/null +++ b/x-pack/plugins/search_playground/public/components/select_indices_flyout.tsx @@ -0,0 +1,172 @@ +/* + * 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, { useState } from 'react'; +import { + EuiButton, + EuiButtonEmpty, + EuiCallOut, + EuiFlexGroup, + EuiFlexItem, + EuiFlyout, + EuiFlyoutBody, + EuiFlyoutFooter, + EuiFlyoutHeader, + EuiSelectable, + EuiSpacer, + EuiTabbedContent, + EuiTitle, +} from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n-react'; +import { i18n } from '@kbn/i18n'; +import { EuiSelectableOption } from '@elastic/eui/src/components/selectable/selectable_option'; +import { getIndicesWithNoSourceFields } from '../utils/create_query'; +import { useIndicesFields } from '../hooks/use_indices_fields'; +import { useSourceIndicesFields } from '../hooks/use_source_indices_field'; +import { useQueryIndices } from '../hooks/use_query_indices'; + +interface SelectIndicesFlyout { + onClose: () => void; +} + +export const SelectIndicesFlyout: React.FC = ({ onClose }) => { + const [query, setQuery] = useState(''); + const { indices, isLoading: isIndicesLoading } = useQueryIndices(query); + const { indices: selectedIndices, setIndices: setSelectedIndices } = useSourceIndicesFields(); + const [selectedTempIndices, setSelectedTempIndices] = useState(selectedIndices); + const handleSelectOptions = (options: EuiSelectableOption[]) => { + setSelectedTempIndices( + options.filter((option) => option.checked === 'on').map((option) => option.label) + ); + }; + const handleSearchChange = (searchValue: string) => { + setQuery(searchValue); + }; + + const handleSaveQuery = () => { + setSelectedIndices(selectedTempIndices); + onClose(); + }; + const tabs = [ + { + id: 'indices', + name: i18n.translate('xpack.searchPlayground.setupPage.addDataSource.flyout.tabName', { + defaultMessage: 'Indices', + }), + content: ( + <> + + + ({ + label: index, + checked: selectedTempIndices.includes(index) ? 'on' : '', + 'data-test-subj': `sourceIndex-${num}`, + } as EuiSelectableOption) + ), + ]} + onChange={handleSelectOptions} + listProps={{ + showIcons: true, + bordered: false, + }} + isLoading={isIndicesLoading} + renderOption={undefined} + > + {(list, search) => ( + <> + {search} + {list} + + )} + + + ), + }, + ]; + const { fields, isLoading: isFieldsLoading } = useIndicesFields(selectedTempIndices); + const noSourceFieldsWarning = getIndicesWithNoSourceFields(fields); + + return ( + + + +

+ +

+
+
+ + + {!isFieldsLoading && !!noSourceFieldsWarning && ( + +

+ +

+
+ )} +
+ + + + + + + + + + + + + + +
+ ); +}; diff --git a/x-pack/plugins/search_playground/public/components/set_up_connector_panel_for_start_chat.tsx b/x-pack/plugins/search_playground/public/components/set_up_connector_panel_for_start_chat.tsx deleted file mode 100644 index 2430cd4e6c7a0..0000000000000 --- a/x-pack/plugins/search_playground/public/components/set_up_connector_panel_for_start_chat.tsx +++ /dev/null @@ -1,111 +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, { useEffect, useState } from 'react'; -import { i18n } from '@kbn/i18n'; -import { FormattedMessage } from '@kbn/i18n-react'; -import { EuiButton, EuiCallOut, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; -import { GenerativeAIForSearchPlaygroundConnectorFeatureId } from '@kbn/actions-plugin/common'; -import { AnalyticsEvents } from '../analytics/constants'; -import { useUsageTracker } from '../hooks/use_usage_tracker'; -import { useKibana } from '../hooks/use_kibana'; -import { useLoadConnectors } from '../hooks/use_load_connectors'; -import { StartChatPanel } from './start_chat_panel'; - -export const SetUpConnectorPanelForStartChat: React.FC = () => { - const [connectorFlyoutOpen, setConnectorFlyoutOpen] = useState(false); - const [showCallout, setShowAddedCallout] = useState(false); - const { - services: { - triggersActionsUi: { getAddConnectorFlyout: ConnectorFlyout }, - }, - } = useKibana(); - const { - data: connectors, - refetch: refetchConnectors, - isLoading: isConnectorListLoading, - } = useLoadConnectors(); - const usageTracker = useUsageTracker(); - const handleConnectorCreated = () => { - refetchConnectors(); - setShowAddedCallout(true); - setConnectorFlyoutOpen(false); - }; - const handleSetupGenAiConnector = () => { - usageTracker?.click(AnalyticsEvents.genAiConnectorCreated); - setConnectorFlyoutOpen(true); - }; - - useEffect(() => { - if (connectors?.length) { - if (showCallout) { - usageTracker?.load(AnalyticsEvents.genAiConnectorAdded); - } else { - usageTracker?.load(AnalyticsEvents.genAiConnectorExists); - } - } else { - usageTracker?.load(AnalyticsEvents.genAiConnectorSetup); - } - }, [connectors?.length, showCallout, usageTracker]); - - if (isConnectorListLoading) { - return null; - } - - return connectors?.length ? ( - showCallout ? ( - - ) : null - ) : ( - <> - - } - dataTestSubj="connectToLLMChatPanel" - > - - - - - - - - - {connectorFlyoutOpen && ( - setConnectorFlyoutOpen(false)} - /> - )} - - ); -}; diff --git a/x-pack/plugins/search_playground/public/components/setup_page/add_data_sources.tsx b/x-pack/plugins/search_playground/public/components/setup_page/add_data_sources.tsx new file mode 100644 index 0000000000000..77d8114866592 --- /dev/null +++ b/x-pack/plugins/search_playground/public/components/setup_page/add_data_sources.tsx @@ -0,0 +1,53 @@ +/* + * 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, { 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 { SelectIndicesFlyout } from '../select_indices_flyout'; + +export const AddDataSources: React.FC = () => { + const [showFlyout, setShowFlyout] = useState(false); + const { getValues } = useFormContext(); + const hasSelectedIndices: boolean = !!getValues(ChatFormFields.indices)?.length; + const handleFlyoutClose = () => { + setShowFlyout(false); + }; + + return ( + <> + {showFlyout && } + {hasSelectedIndices ? ( + setShowFlyout(true)} + data-test-subj="dataSourcesSuccessButton" + > + + + ) : ( + setShowFlyout(true)} + data-test-subj="addDataSourcesButton" + > + + + )} + + ); +}; diff --git a/x-pack/plugins/search_playground/public/components/set_up_connector_panel_for_start_chat.test.tsx b/x-pack/plugins/search_playground/public/components/setup_page/connect_llm_button.test.tsx similarity index 66% rename from x-pack/plugins/search_playground/public/components/set_up_connector_panel_for_start_chat.test.tsx rename to x-pack/plugins/search_playground/public/components/setup_page/connect_llm_button.test.tsx index 7ed41e946a91f..65be856d77eb3 100644 --- a/x-pack/plugins/search_playground/public/components/set_up_connector_panel_for_start_chat.test.tsx +++ b/x-pack/plugins/search_playground/public/components/setup_page/connect_llm_button.test.tsx @@ -7,17 +7,17 @@ import React from 'react'; import { fireEvent, render as testingLibraryRender, waitFor } from '@testing-library/react'; -import { SetUpConnectorPanelForStartChat } from './set_up_connector_panel_for_start_chat'; -import { useKibana } from '../hooks/use_kibana'; -import { useLoadConnectors } from '../hooks/use_load_connectors'; +import { ConnectLLMButton } from './connect_llm_button'; +import { useKibana } from '../../hooks/use_kibana'; +import { useLoadConnectors } from '../../hooks/use_load_connectors'; import { __IntlProvider as IntlProvider } from '@kbn/i18n-react'; const render = (children: React.ReactNode) => testingLibraryRender({children}); -jest.mock('../hooks/use_kibana'); -jest.mock('../hooks/use_load_connectors'); -jest.mock('../hooks/use_usage_tracker', () => ({ +jest.mock('../../hooks/use_kibana'); +jest.mock('../../hooks/use_load_connectors'); +jest.mock('../../hooks/use_usage_tracker', () => ({ useUsageTracker: () => ({ count: jest.fn(), load: jest.fn(), @@ -30,7 +30,7 @@ const mockConnectors = { '2': { title: 'Connector 2' }, }; -describe('SetUpConnectorPanelForStartChat', () => { +describe('ConnectLLMButton', () => { beforeEach(() => { (useKibana as jest.Mock).mockReturnValue({ services: { @@ -59,8 +59,8 @@ describe('SetUpConnectorPanelForStartChat', () => { isLoading: false, isSuccess: true, }); - const { getByTestId } = render(); - expect(getByTestId('setupGenAIConnectorButton')).toBeInTheDocument(); + const { getByTestId } = render(); + expect(getByTestId('connectLLMButton')).toBeInTheDocument(); }); it('show the flyout when the button is clicked', async () => { @@ -69,11 +69,22 @@ describe('SetUpConnectorPanelForStartChat', () => { isLoading: false, isSuccess: true, }); - const { getByTestId, queryByTestId } = render(); + const { getByTestId, queryByTestId } = render(); expect(queryByTestId('addConnectorFlyout')).not.toBeInTheDocument(); - fireEvent.click(getByTestId('setupGenAIConnectorButton')); + fireEvent.click(getByTestId('connectLLMButton')); await waitFor(() => expect(getByTestId('addConnectorFlyout')).toBeInTheDocument()); }); + + it('show success button when connector exists', async () => { + (useLoadConnectors as jest.Mock).mockReturnValue({ + data: [{}], + isLoading: false, + isSuccess: true, + }); + const { queryByTestId } = render(); + + expect(queryByTestId('successConnectLLMButton')).toBeInTheDocument(); + }); }); diff --git a/x-pack/plugins/search_playground/public/components/setup_page/connect_llm_button.tsx b/x-pack/plugins/search_playground/public/components/setup_page/connect_llm_button.tsx new file mode 100644 index 0000000000000..579715e79ea55 --- /dev/null +++ b/x-pack/plugins/search_playground/public/components/setup_page/connect_llm_button.tsx @@ -0,0 +1,85 @@ +/* + * 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 { EuiButton, EuiButtonEmpty } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n-react'; +import React, { useEffect, useState } from 'react'; +import { GenerativeAIForSearchPlaygroundConnectorFeatureId } from '@kbn/actions-plugin/common'; +import { useKibana } from '../../hooks/use_kibana'; +import { useLoadConnectors } from '../../hooks/use_load_connectors'; +import { useUsageTracker } from '../../hooks/use_usage_tracker'; +import { AnalyticsEvents } from '../../analytics/constants'; + +export const ConnectLLMButton: React.FC = () => { + const [connectorFlyoutOpen, setConnectorFlyoutOpen] = useState(false); + const [showCallout, setShowAddedCallout] = useState(false); + const { + services: { + triggersActionsUi: { getAddConnectorFlyout: ConnectorFlyout }, + }, + } = useKibana(); + const { data: connectors, refetch: refetchConnectors } = useLoadConnectors(); + const usageTracker = useUsageTracker(); + const handleConnectorCreated = () => { + refetchConnectors(); + setShowAddedCallout(true); + setConnectorFlyoutOpen(false); + }; + const handleSetupGenAiConnector = () => { + usageTracker?.click(AnalyticsEvents.genAiConnectorCreated); + setConnectorFlyoutOpen(true); + }; + + useEffect(() => { + if (connectors?.length) { + if (showCallout) { + usageTracker?.load(AnalyticsEvents.genAiConnectorAdded); + } else { + usageTracker?.load(AnalyticsEvents.genAiConnectorExists); + } + } else { + usageTracker?.load(AnalyticsEvents.genAiConnectorSetup); + } + }, [connectors?.length, showCallout, usageTracker]); + + return ( + <> + {connectors?.length ? ( + + + + ) : ( + + + + )} + {connectorFlyoutOpen && ( + setConnectorFlyoutOpen(false)} + /> + )} + + ); +}; diff --git a/x-pack/plugins/search_playground/public/components/sources_panel/create_index_callout.test.tsx b/x-pack/plugins/search_playground/public/components/setup_page/create_index_button.test.tsx similarity index 87% rename from x-pack/plugins/search_playground/public/components/sources_panel/create_index_callout.test.tsx rename to x-pack/plugins/search_playground/public/components/setup_page/create_index_button.test.tsx index d75d5385cc3f8..485bb5eb5df55 100644 --- a/x-pack/plugins/search_playground/public/components/sources_panel/create_index_callout.test.tsx +++ b/x-pack/plugins/search_playground/public/components/setup_page/create_index_button.test.tsx @@ -7,9 +7,9 @@ import React, { FC, PropsWithChildren } from 'react'; import { render, fireEvent, waitFor } from '@testing-library/react'; -import { CreateIndexCallout } from './create_index_callout'; import { useKibana } from '../../hooks/use_kibana'; import { __IntlProvider as IntlProvider } from '@kbn/i18n-react'; +import { CreateIndexButton } from './create_index_button'; // Mocking the useKibana hook jest.mock('../../hooks/use_kibana', () => ({ @@ -37,9 +37,9 @@ const Wrapper: FC> = ({ children }) => { ); }; -describe('CreateIndexCallout', () => { +describe('CreateIndexButton', () => { it('renders correctly when there is no locator', async () => { - const { queryByTestId } = render(, { wrapper: Wrapper }); + const { queryByTestId } = render(, { wrapper: Wrapper }); expect(queryByTestId('createIndexButton')).not.toBeInTheDocument(); }); @@ -64,7 +64,7 @@ describe('CreateIndexCallout', () => { }, })); - const { getByTestId } = render(, { wrapper: Wrapper }); + const { getByTestId } = render(, { wrapper: Wrapper }); const createIndexButton = getByTestId('createIndexButton'); expect(createIndexButton).toBeInTheDocument(); diff --git a/x-pack/plugins/search_playground/public/components/setup_page/create_index_button.tsx b/x-pack/plugins/search_playground/public/components/setup_page/create_index_button.tsx new file mode 100644 index 0000000000000..d4a165f56266c --- /dev/null +++ b/x-pack/plugins/search_playground/public/components/setup_page/create_index_button.tsx @@ -0,0 +1,53 @@ +/* + * 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 { EuiButton, EuiCallOut } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n-react'; +import React, { useCallback, useMemo } from 'react'; +import { i18n } from '@kbn/i18n'; +import { useKibana } from '../../hooks/use_kibana'; + +export const CreateIndexButton: React.FC = () => { + const { + services: { application, share }, + } = useKibana(); + const createIndexLocator = useMemo( + () => share.url.locators.get('CREATE_INDEX_LOCATOR_ID'), + [share.url.locators] + ); + const handleNavigateToIndex = useCallback(async () => { + const createIndexUrl = await createIndexLocator?.getUrl({}); + + if (createIndexUrl) { + application?.navigateToUrl(createIndexUrl); + } + }, [application, createIndexLocator]); + + return createIndexLocator ? ( + + + + ) : ( + + ); +}; diff --git a/x-pack/plugins/search_playground/public/components/setup_page/setup_page.tsx b/x-pack/plugins/search_playground/public/components/setup_page/setup_page.tsx new file mode 100644 index 0000000000000..6dc4d80cc8b15 --- /dev/null +++ b/x-pack/plugins/search_playground/public/components/setup_page/setup_page.tsx @@ -0,0 +1,109 @@ +/* + * 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 { + EuiEmptyPrompt, + EuiFlexGroup, + EuiFlexItem, + EuiLink, + EuiLoadingSpinner, + EuiTitle, +} from '@elastic/eui'; +import React, { useEffect, useMemo } from 'react'; +import { FormattedMessage } from '@kbn/i18n-react'; +import { useSearchParams } from 'react-router-dom-v5-compat'; +import { CreateIndexButton } from './create_index_button'; +import { useQueryIndices } from '../../hooks/use_query_indices'; +import { docLinks } from '../../../common/doc_links'; +import { useSourceIndicesFields } from '../../hooks/use_source_indices_field'; +import { useUsageTracker } from '../../hooks/use_usage_tracker'; +import { AnalyticsEvents } from '../../analytics/constants'; +import { ConnectLLMButton } from './connect_llm_button'; +import { AddDataSources } from './add_data_sources'; + +export const SetupPage: React.FC = () => { + const usageTracker = useUsageTracker(); + const [searchParams] = useSearchParams(); + const { indices, isLoading: isIndicesLoading } = useQueryIndices(); + const index = useMemo(() => searchParams.get('default-index'), [searchParams]); + const { addIndex } = useSourceIndicesFields(); + + useEffect(() => { + if (index) { + addIndex(index); + } + }, [index, addIndex]); + + useEffect(() => { + usageTracker?.load(AnalyticsEvents.setupChatPageLoaded); + }, [usageTracker]); + + return ( + + + + } + body={ + <> +

+ +

+

+ +

+ + } + actions={ + + {isIndicesLoading ? ( + + ) : ( + <> + + + + + {indices.length ? : } + + + )} + + } + footer={ + <> + + + + + {' '} + + + + + } + /> + ); +}; diff --git a/x-pack/plugins/search_playground/public/components/sources_panel/add_indices_field.tsx b/x-pack/plugins/search_playground/public/components/sources_panel/add_indices_field.tsx deleted file mode 100644 index 786a9db0c8238..0000000000000 --- a/x-pack/plugins/search_playground/public/components/sources_panel/add_indices_field.tsx +++ /dev/null @@ -1,65 +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 { EuiComboBox, EuiFormRow } from '@elastic/eui'; -import React, { useState } from 'react'; -import { i18n } from '@kbn/i18n'; -import { EuiComboBoxOptionOption } from '@elastic/eui/src/components/combo_box/types'; -import { IndexName } from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; -import { useQueryIndices } from '../../hooks/use_query_indices'; - -interface AddIndicesFieldProps { - selectedIndices: IndexName[]; - onIndexSelect: (index: IndexName) => void; - loading: boolean; -} - -export const AddIndicesField: React.FC = ({ - selectedIndices, - onIndexSelect, - loading, -}) => { - const [query, setQuery] = useState(''); - const { indices, isLoading } = useQueryIndices(query); - const handleChange = (value: Array>) => { - if (value?.[0]?.label) { - onIndexSelect(value[0].label); - } - }; - const handleSearchChange = (searchValue: string) => { - setQuery(searchValue); - }; - - return ( - - ({ - label: index, - disabled: selectedIndices.includes(index), - }))} - isClearable={false} - data-test-subj="selectIndicesComboBox" - /> - - ); -}; diff --git a/x-pack/plugins/search_playground/public/components/sources_panel/create_index_callout.tsx b/x-pack/plugins/search_playground/public/components/sources_panel/create_index_callout.tsx deleted file mode 100644 index 4294272946f76..0000000000000 --- a/x-pack/plugins/search_playground/public/components/sources_panel/create_index_callout.tsx +++ /dev/null @@ -1,65 +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 { EuiButton, EuiCallOut, EuiSpacer, EuiText } from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; -import { FormattedMessage } from '@kbn/i18n-react'; -import React, { useCallback, useMemo } from 'react'; -import { useKibana } from '../../hooks/use_kibana'; - -export const CreateIndexCallout: React.FC = () => { - const { - services: { application, share }, - } = useKibana(); - const createIndexLocator = useMemo( - () => share.url.locators.get('CREATE_INDEX_LOCATOR_ID'), - [share.url.locators] - ); - const handleNavigateToIndex = useCallback(async () => { - const createIndexUrl = await createIndexLocator?.getUrl({}); - - if (createIndexUrl) { - application?.navigateToUrl(createIndexUrl); - } - }, [application, createIndexLocator]); - - return ( - - -

- -

-
- - {createIndexLocator && ( - - - - )} -
- ); -}; diff --git a/x-pack/plugins/search_playground/public/components/sources_panel/sources_panel_for_sidebar.test.tsx b/x-pack/plugins/search_playground/public/components/sources_panel/sources_panel_for_sidebar.test.tsx deleted file mode 100644 index 194b451ccd1fb..0000000000000 --- a/x-pack/plugins/search_playground/public/components/sources_panel/sources_panel_for_sidebar.test.tsx +++ /dev/null @@ -1,73 +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, PropsWithChildren } from 'react'; -import { render, screen } from '@testing-library/react'; -import { SourcesPanelSidebar } from './sources_panel_sidebar'; -import { useSourceIndicesFields } from '../../hooks/use_source_indices_field'; -import { useQueryIndices } from '../../hooks/use_query_indices'; -import { __IntlProvider as IntlProvider } from '@kbn/i18n-react'; - -jest.mock('../../hooks/use_source_indices_field', () => ({ - useSourceIndicesFields: jest.fn(), -})); -jest.mock('../../hooks/use_query_indices', () => ({ - useQueryIndices: jest.fn(), -})); - -const Wrapper: FC> = ({ children }) => { - return ( - <> - {children} - - ); -}; - -describe('SourcesPanelSidebar component', () => { - afterEach(jest.clearAllMocks); - - it('shows the "AddIndicesField" component when there are indices and not loading', () => { - (useQueryIndices as jest.Mock).mockReturnValue({ indices: ['index1'], isLoading: false }); - (useSourceIndicesFields as jest.Mock).mockReturnValue({ - indices: [], - removeIndex: jest.fn(), - addIndex: jest.fn(), - loading: false, - }); - - render(, { wrapper: Wrapper }); - expect(screen.queryByTestId('indicesLoading')).not.toBeInTheDocument(); - }); - - it('displays IndicesTable when there are selected indices', () => { - (useQueryIndices as jest.Mock).mockReturnValue({ indices: ['index1'], isLoading: false }); - (useSourceIndicesFields as jest.Mock).mockReturnValue({ - indices: ['index1', 'index2'], - removeIndex: jest.fn(), - addIndex: jest.fn(), - loading: false, - }); - - render(, { wrapper: Wrapper }); - expect(screen.getAllByTestId('removeIndexButton')).toHaveLength(2); - expect(screen.getAllByTestId('removeIndexButton')[0]).not.toBeDisabled(); - expect(screen.getAllByTestId('removeIndexButton')[1]).not.toBeDisabled(); - }); - - it('does not allow to remove all indices', () => { - (useQueryIndices as jest.Mock).mockReturnValue({ indices: ['index1'], isLoading: false }); - (useSourceIndicesFields as jest.Mock).mockReturnValue({ - indices: ['index1'], - removeIndex: jest.fn(), - addIndex: jest.fn(), - loading: false, - }); - - render(, { wrapper: Wrapper }); - expect(screen.getByTestId('removeIndexButton')).toBeDisabled(); - }); -}); diff --git a/x-pack/plugins/search_playground/public/components/sources_panel/sources_panel_for_start_chat.test.tsx b/x-pack/plugins/search_playground/public/components/sources_panel/sources_panel_for_start_chat.test.tsx deleted file mode 100644 index 72c0aadfdb708..0000000000000 --- a/x-pack/plugins/search_playground/public/components/sources_panel/sources_panel_for_start_chat.test.tsx +++ /dev/null @@ -1,108 +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, PropsWithChildren } from 'react'; -import { render, screen } from '@testing-library/react'; -import { SourcesPanelForStartChat } from './sources_panel_for_start_chat'; -import { useSourceIndicesFields } from '../../hooks/use_source_indices_field'; -import { useQueryIndices } from '../../hooks/use_query_indices'; -import { __IntlProvider as IntlProvider } from '@kbn/i18n-react'; - -jest.mock('../../hooks/use_source_indices_field', () => ({ - useSourceIndicesFields: jest.fn(), -})); -jest.mock('../../hooks/use_query_indices', () => ({ - useQueryIndices: jest.fn(), -})); - -jest.mock('../../hooks/use_kibana', () => ({ - useKibana: jest.fn(() => ({ - services: { - application: { navigateToUrl: jest.fn() }, - share: { url: { locators: { get: jest.fn() } } }, - }, - })), -})); - -const Wrapper: FC> = ({ children }) => { - return ( - <> - {children} - - ); -}; - -describe('SourcesPanelForStartChat component', () => { - afterEach(jest.clearAllMocks); - - it('shows a loading spinner when query is loading', () => { - (useQueryIndices as jest.Mock).mockReturnValue({ indices: [], isLoading: true }); - (useSourceIndicesFields as jest.Mock).mockReturnValue({ - indices: [], - removeIndex: jest.fn(), - addIndex: jest.fn(), - loading: false, - }); - - render(, { wrapper: Wrapper }); - expect(screen.getByTestId('indicesLoading')).toBeInTheDocument(); - }); - - it('shows the "AddIndicesField" component when there are indices and not loading', () => { - (useQueryIndices as jest.Mock).mockReturnValue({ indices: ['index1'], isLoading: false }); - (useSourceIndicesFields as jest.Mock).mockReturnValue({ - indices: [], - removeIndex: jest.fn(), - addIndex: jest.fn(), - loading: false, - }); - - render(, { wrapper: Wrapper }); - expect(screen.queryByTestId('indicesLoading')).not.toBeInTheDocument(); - }); - - it('displays IndicesTable when there are selected indices', () => { - (useQueryIndices as jest.Mock).mockReturnValue({ indices: ['index1'], isLoading: false }); - (useSourceIndicesFields as jest.Mock).mockReturnValue({ - indices: ['index1'], - removeIndex: jest.fn(), - addIndex: jest.fn(), - loading: false, - }); - - render(, { wrapper: Wrapper }); - expect(screen.getAllByText('index1')).toHaveLength(1); - expect(screen.getByTestId('removeIndexButton')).toBeInTheDocument(); - }); - - it('displays "CreateIndexCallout" when no indices are found and not loading', () => { - (useQueryIndices as jest.Mock).mockReturnValue({ indices: [], isLoading: false }); - (useSourceIndicesFields as jest.Mock).mockReturnValue({ - indices: [], - removeIndex: jest.fn(), - addIndex: jest.fn(), - loading: false, - }); - - render(, { wrapper: Wrapper }); - expect(screen.getByTestId('createIndexCallout')).toBeInTheDocument(); - }); - - it('renders warning callout', () => { - (useSourceIndicesFields as jest.Mock).mockReturnValue({ - indices: ['index1'], - removeIndex: jest.fn(), - addIndex: jest.fn(), - loading: false, - noFieldsIndicesWarning: 'index1', - }); - - render(, { wrapper: Wrapper }); - expect(screen.getByTestId('NoIndicesFieldsMessage')).toBeInTheDocument(); - expect(screen.getByTestId('NoIndicesFieldsMessage')).toHaveTextContent('index1'); - }); -}); diff --git a/x-pack/plugins/search_playground/public/components/sources_panel/sources_panel_for_start_chat.tsx b/x-pack/plugins/search_playground/public/components/sources_panel/sources_panel_for_start_chat.tsx deleted file mode 100644 index 992ae7e574658..0000000000000 --- a/x-pack/plugins/search_playground/public/components/sources_panel/sources_panel_for_start_chat.tsx +++ /dev/null @@ -1,79 +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 { EuiCallOut, EuiFlexGroup, EuiFlexItem, EuiLoadingSpinner } from '@elastic/eui'; -import React from 'react'; -import { i18n } from '@kbn/i18n'; -import { AddIndicesField } from './add_indices_field'; -import { IndicesTable } from './indices_table'; -import { StartChatPanel } from '../start_chat_panel'; -import { CreateIndexCallout } from './create_index_callout'; -import { useQueryIndices } from '../../hooks/use_query_indices'; -import { useSourceIndicesFields } from '../../hooks/use_source_indices_field'; - -export const SourcesPanelForStartChat: React.FC = () => { - const { - indices: selectedIndices, - removeIndex, - addIndex, - loading: fieldIndicesLoading, - noFieldsIndicesWarning, - } = useSourceIndicesFields(); - const { indices, isLoading } = useQueryIndices(); - - return ( - - {!!selectedIndices?.length && ( - - - - )} - - {noFieldsIndicesWarning && ( - -

- {i18n.translate('xpack.searchPlayground.emptyPrompts.sources.warningCallout', { - defaultMessage: - 'No fields found for {errorMessage}. Try adding data to these indices.', - values: { - errorMessage: noFieldsIndicesWarning, - }, - })} -

-
- )} - - {isLoading && ( - - - - )} - - {!isLoading && !!indices?.length && ( - - - - )} - - {!isLoading && !indices?.length && } -
- ); -}; diff --git a/x-pack/plugins/search_playground/public/components/sources_panel/sources_panel_sidebar.tsx b/x-pack/plugins/search_playground/public/components/sources_panel/sources_panel_sidebar.tsx deleted file mode 100644 index 813c98641ff29..0000000000000 --- a/x-pack/plugins/search_playground/public/components/sources_panel/sources_panel_sidebar.tsx +++ /dev/null @@ -1,43 +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 from 'react'; -import { i18n } from '@kbn/i18n'; -import { EuiCallOut, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; -import { useSourceIndicesFields } from '../../hooks/use_source_indices_field'; -import { AddIndicesField } from './add_indices_field'; -import { IndicesList } from './indices_list'; - -export const SourcesPanelSidebar: React.FC = () => { - const { indices: selectedIndices, removeIndex, addIndex, loading } = useSourceIndicesFields(); - - return ( - - - - - - - - - - - - - - ); -}; diff --git a/x-pack/plugins/search_playground/public/components/start_chat_panel.tsx b/x-pack/plugins/search_playground/public/components/start_chat_panel.tsx deleted file mode 100644 index 996f088090d29..0000000000000 --- a/x-pack/plugins/search_playground/public/components/start_chat_panel.tsx +++ /dev/null @@ -1,69 +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 { - EuiFlexGroup, - EuiFlexItem, - EuiIcon, - EuiPanel, - EuiSpacer, - EuiText, - EuiTitle, -} from '@elastic/eui'; -import React, { FC, PropsWithChildren } from 'react'; -import { FormattedMessage } from '@kbn/i18n-react'; - -interface StartChatPanelProps { - children: React.ReactNode; - title: string; - description: string | React.ReactNode; - isValid?: boolean; - dataTestSubj: string; -} - -export const StartChatPanel: FC> = ({ - title, - description, - children, - isValid, - dataTestSubj, -}) => ( - - - -
{title}
-
- - {isValid && ( - - - - - -

- -

-
-
-
- )} -
- - - - - -

{description}

-
- - {children} -
-
-); diff --git a/x-pack/plugins/search_playground/public/components/start_new_chat.test.tsx b/x-pack/plugins/search_playground/public/components/start_new_chat.test.tsx deleted file mode 100644 index ed380e00eb1da..0000000000000 --- a/x-pack/plugins/search_playground/public/components/start_new_chat.test.tsx +++ /dev/null @@ -1,123 +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 { __IntlProvider as IntlProvider } from '@kbn/i18n-react'; - -import React from 'react'; -import { render, screen } from '@testing-library/react'; -import { MemoryRouter, useSearchParams } from 'react-router-dom-v5-compat'; -import { StartNewChat } from './start_new_chat'; -import { useLoadConnectors } from '../hooks/use_load_connectors'; -import { useUsageTracker } from '../hooks/use_usage_tracker'; -import { AnalyticsEvents } from '../analytics/constants'; -import { useSourceIndicesFields } from '../hooks/use_source_indices_field'; -import { useKibana } from '../hooks/use_kibana'; -import { PlaygroundProvider } from '../providers/playground_provider'; - -jest.mock('../hooks/use_load_connectors'); -jest.mock('../hooks/use_usage_tracker'); -jest.mock('../hooks/use_source_indices_field', () => ({ - useSourceIndicesFields: jest.fn(() => ({ addIndex: jest.fn() })), -})); -jest.mock('react-router-dom-v5-compat', () => ({ - ...jest.requireActual('react-router-dom-v5-compat'), - useSearchParams: jest.fn(), -})); -jest.mock('../hooks/use_kibana'); - -const mockUseLoadConnectors = useLoadConnectors as jest.Mock; -const mockUseUsageTracker = useUsageTracker as jest.Mock; -const mockUseSearchParams = useSearchParams as jest.Mock; - -const renderWithForm = (ui: React.ReactElement) => { - const Wrapper: React.FC = ({ children }) => { - return ( - - {children} - - ); - }; - return render(ui, { wrapper: Wrapper }); -}; - -const mockConnectors = { - '1': { title: 'Connector 1' }, - '2': { title: 'Connector 2' }, -}; - -describe('StartNewChat', () => { - beforeEach(() => { - mockUseLoadConnectors.mockReturnValue({ data: [] }); - mockUseUsageTracker.mockReturnValue({ load: jest.fn() }); - mockUseSearchParams.mockReturnValue([new URLSearchParams()]); - - (useKibana as jest.Mock).mockReturnValue({ - services: { - triggersActionsUi: { - getAddConnectorFlyout: () => ( -
Add Connector Flyout
- ), - }, - }, - }); - (useLoadConnectors as jest.Mock).mockReturnValue({ - data: mockConnectors, - refetch: jest.fn(), - isLoading: false, - isSuccess: true, - }); - }); - - it('renders correctly', () => { - renderWithForm( - - - - ); - - expect(screen.getByTestId('startNewChatTitle')).toBeInTheDocument(); - }); - - it('disables the start button when form conditions are not met', () => { - renderWithForm( - - - - ); - - const startButton = screen.getByTestId('startChatButton'); - expect(startButton).toBeDisabled(); - }); - - it('tracks the page load event', () => { - const usageTracker = { load: jest.fn() }; - mockUseUsageTracker.mockReturnValue(usageTracker); - - renderWithForm( - - - - ); - - expect(usageTracker.load).toHaveBeenCalledWith(AnalyticsEvents.startNewChatPageLoaded); - }); - - it('calls addIndex when default-index is present in searchParams', () => { - const addIndex = jest.fn(); - (useSourceIndicesFields as jest.Mock).mockReturnValue({ addIndex }); - const searchParams = new URLSearchParams({ 'default-index': 'test-index' }); - mockUseSearchParams.mockReturnValue([searchParams]); - - renderWithForm( - - - - ); - - expect(addIndex).toHaveBeenCalledWith('test-index'); - }); -}); diff --git a/x-pack/plugins/search_playground/public/components/start_new_chat.tsx b/x-pack/plugins/search_playground/public/components/start_new_chat.tsx deleted file mode 100644 index 0df4485ec858c..0000000000000 --- a/x-pack/plugins/search_playground/public/components/start_new_chat.tsx +++ /dev/null @@ -1,123 +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 { - EuiButton, - EuiFlexGroup, - EuiFlexItem, - EuiIcon, - EuiText, - EuiTitle, - useEuiTheme, -} from '@elastic/eui'; -import { FormattedMessage } from '@kbn/i18n-react'; -import React, { useEffect, useMemo } from 'react'; -import { useFormContext } from 'react-hook-form'; -import { useSearchParams } from 'react-router-dom-v5-compat'; -import { useUsageTracker } from '../hooks/use_usage_tracker'; -import { useLoadConnectors } from '../hooks/use_load_connectors'; -import { SourcesPanelForStartChat } from './sources_panel/sources_panel_for_start_chat'; -import { SetUpConnectorPanelForStartChat } from './set_up_connector_panel_for_start_chat'; -import { ChatFormFields } from '../types'; -import { AnalyticsEvents } from '../analytics/constants'; -import { useSourceIndicesFields } from '../hooks/use_source_indices_field'; - -const maxWidthPage = 640; - -interface StartNewChatProps { - onStartClick: () => void; -} - -export const StartNewChat: React.FC = ({ onStartClick }) => { - const { euiTheme } = useEuiTheme(); - const { data: connectors } = useLoadConnectors(); - const { watch } = useFormContext(); - const usageTracker = useUsageTracker(); - - const [searchParams] = useSearchParams(); - const index = useMemo(() => searchParams.get('default-index'), [searchParams]); - const { addIndex } = useSourceIndicesFields(); - - useEffect(() => { - if (index) { - addIndex(index); - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [index]); - - useEffect(() => { - usageTracker?.load(AnalyticsEvents.startNewChatPageLoaded); - }, [usageTracker]); - - return ( - - - - - -

- -

-
- - -
-
- - -

- -

-
-
- - - - - - - - - - - - - - -
-
- ); -}; diff --git a/x-pack/plugins/search_playground/public/components/summarization_panel/include_citations_field.tsx b/x-pack/plugins/search_playground/public/components/summarization_panel/include_citations_field.tsx index 75212a2ba9cc3..6ab49bea16c3d 100644 --- a/x-pack/plugins/search_playground/public/components/summarization_panel/include_citations_field.tsx +++ b/x-pack/plugins/search_playground/public/components/summarization_panel/include_citations_field.tsx @@ -27,7 +27,7 @@ export const IncludeCitationsField: React.FC = ({ }; return ( - + = ({ value, onC } + fullWidth > { ); expect(getByTestId('summarizationModelSelect')).toBeInTheDocument(); - expect(getByTestId('manageConnectorsLink')).toHaveAttribute( - 'href', - 'http://example.com/manage-connectors' - ); }); }); diff --git a/x-pack/plugins/search_playground/public/components/summarization_panel/summarization_model.tsx b/x-pack/plugins/search_playground/public/components/summarization_panel/summarization_model.tsx index f60436025e0d1..e13823d87fd8d 100644 --- a/x-pack/plugins/search_playground/public/components/summarization_panel/summarization_model.tsx +++ b/x-pack/plugins/search_playground/public/components/summarization_panel/summarization_model.tsx @@ -8,7 +8,6 @@ import React, { useEffect, useMemo } from 'react'; import { - EuiButtonIcon, EuiFlexGroup, EuiFlexItem, EuiFormRow, @@ -16,18 +15,15 @@ import { EuiSuperSelect, type EuiSuperSelectOption, EuiText, - EuiToolTip, } from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n-react'; import { AnalyticsEvents } from '../../analytics/constants'; import { useUsageTracker } from '../../hooks/use_usage_tracker'; import type { LLMModel } from '../../types'; -import { useManagementLink } from '../../hooks/use_management_link'; interface SummarizationModelProps { - selectedModel: LLMModel; + selectedModel?: LLMModel; onSelect: (model: LLMModel) => void; models: LLMModel[]; } @@ -40,7 +36,6 @@ export const SummarizationModel: React.FC = ({ onSelect, }) => { const usageTracker = useUsageTracker(); - const managementLink = useManagementLink(selectedModel.connectorId); const onChange = (modelValue: string) => { const newSelectedModel = models.find((model) => getOptionValue(model) === modelValue); @@ -98,7 +93,7 @@ export const SummarizationModel: React.FC = ({ useEffect(() => { usageTracker?.click( - `${AnalyticsEvents.modelSelected}_${selectedModel.value || selectedModel.connectorType}` + `${AnalyticsEvents.modelSelected}_${selectedModel!.value || selectedModel!.connectorType}` ); }, [usageTracker, selectedModel]); @@ -113,36 +108,14 @@ export const SummarizationModel: React.FC = ({ />{' '} } - labelAppend={ - - - - } + fullWidth > ); diff --git a/x-pack/plugins/search_playground/public/components/summarization_panel/summarization_panel.tsx b/x-pack/plugins/search_playground/public/components/summarization_panel/summarization_panel.tsx index 77b152121f5b0..4a199f7d98b18 100644 --- a/x-pack/plugins/search_playground/public/components/summarization_panel/summarization_panel.tsx +++ b/x-pack/plugins/search_playground/public/components/summarization_panel/summarization_panel.tsx @@ -8,6 +8,7 @@ import React from 'react'; import { Controller, useFormContext } from 'react-hook-form'; +import { EuiPanel } from '@elastic/eui'; import { useLLMsModels } from '../../hooks/use_llms_models'; import { IncludeCitationsField } from './include_citations_field'; import { InstructionsField } from './instructions_field'; @@ -17,13 +18,11 @@ import { SummarizationModel } from './summarization_model'; export const SummarizationPanel: React.FC = () => { const { control } = useFormContext(); const models = useLLMsModels(); - const defaultModel = models.find((model) => !model.disabled); return ( - <> + ( @@ -51,6 +50,6 @@ export const SummarizationPanel: React.FC = () => { )} /> - + ); }; diff --git a/x-pack/plugins/search_playground/public/components/toolbar.tsx b/x-pack/plugins/search_playground/public/components/toolbar.tsx index e322c9c6bb3ff..31ea3345cdcbe 100644 --- a/x-pack/plugins/search_playground/public/components/toolbar.tsx +++ b/x-pack/plugins/search_playground/public/components/toolbar.tsx @@ -7,15 +7,13 @@ import { EuiFlexGroup } from '@elastic/eui'; import React from 'react'; +import { DataActionButton } from './data_action_button'; import { ViewCodeAction } from './view_code/view_code_action'; -import { ViewQueryAction } from './view_query/view_query_action'; -import { EditContextAction } from './edit_context/edit_context_action'; export const Toolbar: React.FC = () => { return ( - - - + + ); diff --git a/x-pack/plugins/search_playground/public/components/view_code/view_code_action.tsx b/x-pack/plugins/search_playground/public/components/view_code/view_code_action.tsx index 01ddc3d789dc5..13b6a2119757a 100644 --- a/x-pack/plugins/search_playground/public/components/view_code/view_code_action.tsx +++ b/x-pack/plugins/search_playground/public/components/view_code/view_code_action.tsx @@ -27,6 +27,7 @@ export const ViewCodeAction: React.FC = () => { onClick={() => setShowFlyout(true)} disabled={!selectedIndices || selectedIndices?.length === 0} data-test-subj="viewCodeActionButton" + size="s" > { - const [showFlyout, setShowFlyout] = useState(false); - const { watch } = useFormContext(); - const selectedIndices: string[] = watch(ChatFormFields.indices); - - return ( - <> - {showFlyout && setShowFlyout(false)} />} - setShowFlyout(true)} - disabled={selectedIndices?.length === 0} - data-test-subj="viewQueryActionButton" - > - - - - ); -}; diff --git a/x-pack/plugins/search_playground/public/components/view_query/view_query_flyout.tsx b/x-pack/plugins/search_playground/public/components/view_query/view_query_flyout.tsx deleted file mode 100644 index 2fd64f073eac7..0000000000000 --- a/x-pack/plugins/search_playground/public/components/view_query/view_query_flyout.tsx +++ /dev/null @@ -1,302 +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 { - EuiAccordion, - EuiSelectable, - EuiButton, - EuiButtonEmpty, - EuiCodeBlock, - EuiFlexGroup, - EuiFlexItem, - EuiFlyout, - EuiFlyoutBody, - EuiFlyoutFooter, - EuiFlyoutHeader, - EuiPanel, - EuiSpacer, - EuiSelectableOption, - EuiText, - EuiTitle, - EuiCheckbox, - EuiLink, - EuiIcon, -} from '@elastic/eui'; -import { FormattedMessage } from '@kbn/i18n-react'; -import React, { useEffect, useState } from 'react'; -import { useFormContext } from 'react-hook-form'; -import { useController } from 'react-hook-form'; -import { AnalyticsEvents } from '../../analytics/constants'; -import { docLinks } from '../../../common/doc_links'; -import { useIndicesFields } from '../../hooks/use_indices_fields'; -import { useUsageTracker } from '../../hooks/use_usage_tracker'; -import { ChatForm, ChatFormFields, IndicesQuerySourceFields } from '../../types'; -import { createQuery, getDefaultQueryFields, IndexFields } from '../../utils/create_query'; - -const groupTypeQueryFields = ( - fields: IndicesQuerySourceFields, - queryFields: IndexFields -): string[] => - Object.entries(queryFields).map(([index, selectedFields]) => { - const indexFields = fields[index]; - let typeQueryFields = ''; - - if (selectedFields.some((field) => indexFields.bm25_query_fields.includes(field))) { - typeQueryFields = 'BM25'; - } - - if ( - selectedFields.some((field) => - indexFields.dense_vector_query_fields.find((vectorField) => vectorField.field === field) - ) - ) { - typeQueryFields += (typeQueryFields ? '_' : '') + 'DENSE'; - } - - if ( - selectedFields.some((field) => - indexFields.elser_query_fields.find((elserField) => elserField.field === field) - ) - ) { - typeQueryFields += (typeQueryFields ? '_' : '') + 'SPARSE'; - } - - if ( - selectedFields.some((field) => indexFields.semantic_fields.find((f) => f.field === field)) - ) { - typeQueryFields += (typeQueryFields ? '_' : '') + 'SEMANTIC'; - } - - return typeQueryFields; - }); - -interface ViewQueryFlyoutProps { - onClose: () => void; -} - -export const ViewQueryFlyout: React.FC = ({ onClose }) => { - const usageTracker = useUsageTracker(); - const { getValues } = useFormContext(); - const selectedIndices: string[] = getValues(ChatFormFields.indices); - const sourceFields = getValues(ChatFormFields.sourceFields); - const { fields } = useIndicesFields(selectedIndices); - const defaultFields = getDefaultQueryFields(fields); - - const { - field: { onChange: queryFieldsOnChange, value: queryFields }, - } = useController({ - name: ChatFormFields.queryFields, - defaultValue: defaultFields, - }); - - const [tempQueryFields, setTempQueryFields] = useState(queryFields); - - const { - field: { onChange: elasticsearchQueryChange }, - } = useController({ - name: ChatFormFields.elasticsearchQuery, - }); - - const isQueryFieldSelected = (index: string, field: string) => { - return tempQueryFields[index].includes(field); - }; - - const updateFields = (index: string, options: EuiSelectableOption[]) => { - const newFields = options - .filter((option) => option.checked === 'on') - .map((option) => option.label); - setTempQueryFields({ - ...tempQueryFields, - [index]: newFields, - }); - usageTracker?.count(AnalyticsEvents.viewQueryFieldsUpdated, newFields.length); - }; - - const saveQuery = () => { - queryFieldsOnChange(tempQueryFields); - elasticsearchQueryChange(createQuery(tempQueryFields, sourceFields, fields)); - onClose(); - - const groupedQueryFields = groupTypeQueryFields(fields, tempQueryFields); - - groupedQueryFields.forEach((typeQueryFields) => - usageTracker?.click(`${AnalyticsEvents.viewQuerySaved}_${typeQueryFields}`) - ); - }; - - useEffect(() => { - usageTracker?.load(AnalyticsEvents.viewQueryFlyoutOpened); - }, [usageTracker]); - - return ( - - - -

- -

-
- - -

- - {` `} - - - -

-
-
- - - - - {JSON.stringify(createQuery(tempQueryFields, sourceFields, fields), null, 2)} - - - - - -
- -
-
- {Object.entries(fields).map(([index, group]) => ( - - - -
{index}
-
- } - > - - { - const checked = isQueryFieldSelected( - index, - typeof field === 'string' ? field : field.field - ); - return { - label: typeof field === 'string' ? field : field.field, - prepend: ( - {}} - /> - ), - checked: checked ? 'on' : undefined, - 'data-test-subj': 'queryField', - }; - })} - listProps={{ - bordered: false, - showIcons: false, - }} - onChange={(newOptions) => updateFields(index, newOptions)} - > - {(list) => list} - - {group.skipped_fields > 0 && ( - <> - - - - - - {` `} - - - - - - - - - - - )} - - - - ))} - - - - - - - - - - - - - - - - - - - - ); -}; diff --git a/x-pack/plugins/search_playground/public/embeddable.tsx b/x-pack/plugins/search_playground/public/embeddable.tsx index f21c0c0f667eb..10eaec93fab8a 100644 --- a/x-pack/plugins/search_playground/public/embeddable.tsx +++ b/x-pack/plugins/search_playground/public/embeddable.tsx @@ -9,16 +9,15 @@ import React from 'react'; import { dynamic } from '@kbn/shared-ux-utility'; import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public'; import { CoreStart } from '@kbn/core-lifecycle-browser'; +import { QueryClientProvider } from '@tanstack/react-query'; import { AppPluginStartDependencies } from './types'; +import { queryClient } from './utils/query_client'; +import { AppProps } from './components/app'; -export const Playground = dynamic(async () => ({ +export const Playground = dynamic>(async () => ({ default: (await import('./components/app')).App, })); -export const PlaygroundToolbar = dynamic(async () => ({ - default: (await import('./components/toolbar')).Toolbar, -})); - export const PlaygroundProvider = dynamic(async () => ({ default: (await import('./providers/playground_provider')).PlaygroundProvider, })); @@ -32,6 +31,8 @@ export const getPlaygroundProvider = (props: React.ComponentProps) => ( - + + + ); diff --git a/x-pack/plugins/search_playground/public/hooks/use_indices_fields.ts b/x-pack/plugins/search_playground/public/hooks/use_indices_fields.ts index f78ea3accf206..e2ca982f2dc3f 100644 --- a/x-pack/plugins/search_playground/public/hooks/use_indices_fields.ts +++ b/x-pack/plugins/search_playground/public/hooks/use_indices_fields.ts @@ -9,13 +9,15 @@ import { useQuery } from '@tanstack/react-query'; import { useKibana } from './use_kibana'; import { APIRoutes, IndicesQuerySourceFields } from '../types'; +const initialData = {}; + export const useIndicesFields = (indices: string[] = []) => { const { services } = useKibana(); - const { data, isLoading } = useQuery({ + const { data, isLoading, isFetching } = useQuery({ enabled: indices.length > 0, queryKey: ['fields', indices.toString()], - initialData: {}, + initialData, queryFn: async () => { const response = await services.http.post( APIRoutes.POST_QUERY_SOURCE_FIELDS, @@ -30,5 +32,5 @@ export const useIndicesFields = (indices: string[] = []) => { }, }); - return { fields: data!, isLoading }; + return { fields: data, isLoading: isLoading || isFetching }; }; diff --git a/x-pack/plugins/search_playground/public/hooks/use_query_indices.ts b/x-pack/plugins/search_playground/public/hooks/use_query_indices.ts index 6740928ac903c..d011b471a68d6 100644 --- a/x-pack/plugins/search_playground/public/hooks/use_query_indices.ts +++ b/x-pack/plugins/search_playground/public/hooks/use_query_indices.ts @@ -29,7 +29,8 @@ export const useQueryIndices = ( return response.indices; }, + initialData: [], }); - return { indices: data || [], isLoading }; + return { indices: data, isLoading }; }; diff --git a/x-pack/plugins/search_playground/public/hooks/use_source_indices_field.ts b/x-pack/plugins/search_playground/public/hooks/use_source_indices_field.ts index bc9a37060fb6f..2fe9fbc237ff3 100644 --- a/x-pack/plugins/search_playground/public/hooks/use_source_indices_field.ts +++ b/x-pack/plugins/search_playground/public/hooks/use_source_indices_field.ts @@ -5,12 +5,11 @@ * 2.0. */ -import { useQuery } from '@tanstack/react-query'; -import { useController, useFormContext } from 'react-hook-form'; +import { useController } from 'react-hook-form'; import { IndexName } from '@elastic/elasticsearch/lib/api/types'; -import { useEffect, useState } from 'react'; -import { useKibana } from './use_kibana'; -import { APIRoutes, IndicesQuerySourceFields } from '../types'; +import { useCallback, useEffect, useState } from 'react'; +import { merge } from 'lodash'; +import { useIndicesFields } from './use_indices_fields'; import { ChatForm, ChatFormFields } from '../types'; import { createQuery, @@ -36,10 +35,7 @@ export const getIndicesWithNoSourceFields = ( export const useSourceIndicesFields = () => { const usageTracker = useUsageTracker(); - const { services } = useKibana(); const [loading, setLoading] = useState(false); - const [noFieldsIndicesWarning, setNoFieldsIndicesWarning] = useState(null); - const { resetField } = useFormContext(); const { field: { value: selectedIndices, onChange: onIndicesChange }, @@ -55,73 +51,74 @@ export const useSourceIndicesFields = () => { }); const { - field: { onChange: onSourceFieldsChange }, - } = useController({ - name: ChatFormFields.sourceFields, + field: { onChange: onQueryFieldsOnChange, value: queryFields }, + } = useController({ + name: ChatFormFields.queryFields, }); - const { data: fields } = useQuery({ - enabled: selectedIndices.length > 0, - queryKey: ['fields', selectedIndices.toString()], - queryFn: async () => { - const response = await services.http.post( - APIRoutes.POST_QUERY_SOURCE_FIELDS, - { - body: JSON.stringify({ indices: selectedIndices }), - } - ); - return response; - }, + const { + field: { onChange: onSourceFieldsChange, value: sourceFields }, + } = useController({ + name: ChatFormFields.sourceFields, }); + const { fields, isLoading: isFieldsLoading } = useIndicesFields(selectedIndices); useEffect(() => { if (fields) { - resetField(ChatFormFields.queryFields); - const defaultFields = getDefaultQueryFields(fields); const defaultSourceFields = getDefaultSourceFields(fields); + const mergedQueryFields = merge(defaultFields, queryFields); + const mergedSourceFields = merge(defaultSourceFields, sourceFields); - const indicesWithNoSourceFields = getIndicesWithNoSourceFields(defaultSourceFields); + onElasticsearchQueryChange(createQuery(mergedQueryFields, mergedSourceFields, fields)); + onQueryFieldsOnChange(mergedQueryFields); - if (indicesWithNoSourceFields) { - setNoFieldsIndicesWarning(indicesWithNoSourceFields); - } else { - setNoFieldsIndicesWarning(null); - } - - onElasticsearchQueryChange(createQuery(defaultFields, defaultSourceFields, fields)); - onSourceFieldsChange(defaultSourceFields); + onSourceFieldsChange(mergedSourceFields); usageTracker?.count( AnalyticsEvents.sourceFieldsLoaded, Object.values(fields)?.flat()?.length ); - } else { - setNoFieldsIndicesWarning(null); } setLoading(false); // eslint-disable-next-line react-hooks/exhaustive-deps }, [fields]); - const addIndex = (newIndex: IndexName) => { - const newIndices = [...selectedIndices, newIndex]; - setLoading(true); - onIndicesChange(newIndices); - usageTracker?.count(AnalyticsEvents.sourceIndexUpdated, newIndices.length); - }; - - const removeIndex = (index: IndexName) => { - const newIndices = selectedIndices.filter((indexName: string) => indexName !== index); - setLoading(true); - onIndicesChange(newIndices); - usageTracker?.count(AnalyticsEvents.sourceIndexUpdated, newIndices.length); - }; + const addIndex = useCallback( + (newIndex: IndexName) => { + const newIndices = [...selectedIndices, newIndex]; + setLoading(true); + onIndicesChange(newIndices); + usageTracker?.count(AnalyticsEvents.sourceIndexUpdated, newIndices.length); + }, + [onIndicesChange, selectedIndices, usageTracker] + ); + + const removeIndex = useCallback( + (index: IndexName) => { + const newIndices = selectedIndices.filter((indexName: string) => indexName !== index); + setLoading(true); + onIndicesChange(newIndices); + usageTracker?.count(AnalyticsEvents.sourceIndexUpdated, newIndices.length); + }, + [onIndicesChange, selectedIndices, usageTracker] + ); + + const setIndices = useCallback( + (indices: IndexName[]) => { + setLoading(true); + onIndicesChange(indices); + usageTracker?.count(AnalyticsEvents.sourceIndexUpdated, indices.length); + }, + [onIndicesChange, usageTracker] + ); return { indices: selectedIndices, fields, loading, + isFieldsLoading, addIndex, removeIndex, - noFieldsIndicesWarning, + setIndices, }; }; diff --git a/x-pack/plugins/search_playground/public/hooks/use_source_indices_fields.test.tsx b/x-pack/plugins/search_playground/public/hooks/use_source_indices_fields.test.tsx index 7dd1a43d6fc01..431eab3cd943c 100644 --- a/x-pack/plugins/search_playground/public/hooks/use_source_indices_fields.test.tsx +++ b/x-pack/plugins/search_playground/public/hooks/use_source_indices_fields.test.tsx @@ -165,7 +165,6 @@ describe.skip('useSourceIndicesFields Hook', () => { expect(postMock).toHaveBeenCalled(); await act(async () => { - expect(result.current.noFieldsIndicesWarning).toEqual('missing_fields_index'); expect(result.current.loading).toBe(false); expect(getValues()).toMatchInlineSnapshot(` Object { @@ -219,7 +218,6 @@ describe.skip('useSourceIndicesFields Hook', () => { expect(postMock).toHaveBeenCalled(); await act(async () => { - expect(result.current.noFieldsIndicesWarning).toBeNull(); expect(result.current.loading).toBe(false); expect(getValues()).toMatchInlineSnapshot(` Object { diff --git a/x-pack/plugins/search_playground/public/plugin.ts b/x-pack/plugins/search_playground/public/plugin.ts index bb8403366a6ed..8b2976cab10c9 100644 --- a/x-pack/plugins/search_playground/public/plugin.ts +++ b/x-pack/plugins/search_playground/public/plugin.ts @@ -15,7 +15,7 @@ import { import { PLUGIN_ID, PLUGIN_NAME } from '../common'; import { docLinks } from '../common/doc_links'; import { PlaygroundHeaderDocs } from './components/playground_header_docs'; -import { PlaygroundToolbar, Playground, getPlaygroundProvider } from './embeddable'; +import { Playground, getPlaygroundProvider } from './embeddable'; import { AppPluginStartDependencies, SearchPlaygroundConfigType, @@ -60,7 +60,6 @@ export class SearchPlaygroundPlugin docLinks.setDocLinks(core.docLinks.links); return { PlaygroundProvider: getPlaygroundProvider(core, deps), - PlaygroundToolbar, Playground, PlaygroundHeaderDocs, }; diff --git a/x-pack/plugins/search_playground/public/providers/playground_provider.tsx b/x-pack/plugins/search_playground/public/providers/playground_provider.tsx index 562658b4b8bd5..6700bb4c66a57 100644 --- a/x-pack/plugins/search_playground/public/providers/playground_provider.tsx +++ b/x-pack/plugins/search_playground/public/providers/playground_provider.tsx @@ -5,32 +5,30 @@ * 2.0. */ -import React, { FC, PropsWithChildren } from 'react'; -import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; +import React, { FC, useEffect } from 'react'; import { FormProvider, useForm } from 'react-hook-form'; -import { ChatForm } from '../types'; +import { useLLMsModels } from '../hooks/use_llms_models'; +import { ChatForm, ChatFormFields } from '../types'; -const queryClient = new QueryClient({}); - -export interface PlaygroundProviderProps { - children: React.ReactNode; -} - -export const PlaygroundProvider: FC> = ({ - children, -}) => { +export const PlaygroundProvider: FC = ({ children }) => { + const models = useLLMsModels(); const form = useForm({ defaultValues: { prompt: 'You are an assistant for question-answering tasks.', doc_size: 3, source_fields: {}, indices: [], + summarization_model: {}, }, }); - return ( - - {children} - - ); + useEffect(() => { + const defaultModel = models.find((model) => !model.disabled); + + if (defaultModel) { + form.setValue(ChatFormFields.summarizationModel, defaultModel); + } + }, [form, models]); + + return {children}; }; diff --git a/x-pack/plugins/search_playground/public/types.ts b/x-pack/plugins/search_playground/public/types.ts index eea41aae96d59..2076ac2190b99 100644 --- a/x-pack/plugins/search_playground/public/types.ts +++ b/x-pack/plugins/search_playground/public/types.ts @@ -25,7 +25,6 @@ import type { ConsolePluginStart } from '@kbn/console-plugin/public'; import { ChatRequestData } from '../common/types'; import type { App } from './components/app'; import type { PlaygroundProvider as PlaygroundProviderComponent } from './providers/playground_provider'; -import type { Toolbar } from './components/toolbar'; import { PlaygroundHeaderDocs } from './components/playground_header_docs'; export * from '../common/types'; @@ -34,7 +33,6 @@ export * from '../common/types'; export interface SearchPlaygroundPluginSetup {} export interface SearchPlaygroundPluginStart { PlaygroundProvider: React.FC>; - PlaygroundToolbar: React.FC>; Playground: React.FC>; PlaygroundHeaderDocs: React.FC>; } diff --git a/x-pack/plugins/search_playground/public/utils/create_query.ts b/x-pack/plugins/search_playground/public/utils/create_query.ts index fadf15f291abd..1f3fee0e35104 100644 --- a/x-pack/plugins/search_playground/public/utils/create_query.ts +++ b/x-pack/plugins/search_playground/public/utils/create_query.ts @@ -55,6 +55,9 @@ export function createQuery( const indices = Object.keys(fieldDescriptors); const boolMatches = Object.keys(fields).reduce( (acc, index) => { + if (!fieldDescriptors[index]) { + return acc; + } const indexFields: string[] = fields[index]; const indexFieldDescriptors: QuerySourceFields = fieldDescriptors[index]; @@ -320,6 +323,21 @@ export function getDefaultSourceFields(fieldDescriptors: IndicesQuerySourceField return indexFields; } +export const getIndicesWithNoSourceFields = ( + fields: IndicesQuerySourceFields +): string | undefined => { + const defaultSourceFields = getDefaultSourceFields(fields); + const indices = Object.keys(defaultSourceFields).reduce((result, index: string) => { + if (defaultSourceFields[index].length === 0) { + result.push(index); + } + + return result; + }, []); + + return indices.length === 0 ? undefined : indices.join(); +}; + export function getDefaultQueryFields(fieldDescriptors: IndicesQuerySourceFields): IndexFields { const indexFields = Object.keys(fieldDescriptors).reduce( (acc: IndexFields, index: string) => { diff --git a/x-pack/plugins/search_playground/public/utils/query_client.ts b/x-pack/plugins/search_playground/public/utils/query_client.ts new file mode 100644 index 0000000000000..c5d9874fe71ef --- /dev/null +++ b/x-pack/plugins/search_playground/public/utils/query_client.ts @@ -0,0 +1,10 @@ +/* + * 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 { QueryClient } from '@tanstack/react-query'; + +export const queryClient = new QueryClient({}); diff --git a/x-pack/test/functional/apps/search_playground/playground_overview.ess.ts b/x-pack/test/functional/apps/search_playground/playground_overview.ess.ts index 0afa2979a6c15..0fb7675882016 100644 --- a/x-pack/test/functional/apps/search_playground/playground_overview.ess.ts +++ b/x-pack/test/functional/apps/search_playground/playground_overview.ess.ts @@ -15,7 +15,6 @@ import { LlmProxy, } from '../../../observability_ai_assistant_api_integration/common/create_llm_proxy'; -const indexName = 'basic_index'; const esArchiveIndex = 'test/api_integration/fixtures/es_archiver/index_patterns/basic_index'; export default function (ftrContext: FtrProviderContext) { @@ -56,6 +55,7 @@ export default function (ftrContext: FtrProviderContext) { await pageObjects.searchPlayground.PlaygroundStartChatPage.expectPlaygroundHeaderComponentsToExist(); await pageObjects.searchPlayground.PlaygroundStartChatPage.expectPlaygroundHeaderComponentsToDisabled(); await pageObjects.searchPlayground.PlaygroundStartChatPage.expectPlaygroundStartChatPageComponentsToExist(); + await pageObjects.searchPlayground.PlaygroundStartChatPage.expectPlaygroundStartChatPageIndexButtonExists(); }); describe('with gen ai connectors', () => { @@ -68,8 +68,8 @@ export default function (ftrContext: FtrProviderContext) { await removeOpenAIConnector?.(); await browser.refresh(); }); - it('hide gen ai panel', async () => { - await pageObjects.searchPlayground.PlaygroundStartChatPage.expectHideGenAIPanelConnector(); + it('show success llm button', async () => { + await pageObjects.searchPlayground.PlaygroundStartChatPage.expectShowSuccessLLMButton(); }); }); @@ -80,7 +80,7 @@ export default function (ftrContext: FtrProviderContext) { it('creates a connector successfully', async () => { await pageObjects.searchPlayground.PlaygroundStartChatPage.expectOpenConnectorPagePlayground(); - await pageObjects.searchPlayground.PlaygroundStartChatPage.expectHideGenAIPanelConnectorAfterCreatingConnector( + await pageObjects.searchPlayground.PlaygroundStartChatPage.expectSuccessButtonAfterCreatingConnector( createConnector ); }); @@ -92,18 +92,16 @@ export default function (ftrContext: FtrProviderContext) { }); describe('without any indices', () => { - it('show no index callout', async () => { - await pageObjects.searchPlayground.PlaygroundStartChatPage.expectNoIndexCalloutExists(); + it('show create index button', async () => { await pageObjects.searchPlayground.PlaygroundStartChatPage.expectCreateIndexButtonToExists(); }); - it('hide no index callout when index added', async () => { + it('show success button when index added', async () => { await createIndex(); - await pageObjects.searchPlayground.PlaygroundStartChatPage.expectSelectIndex(indexName); + await pageObjects.searchPlayground.PlaygroundStartChatPage.expectOpenFlyoutAndSelectIndex(); }); after(async () => { - await pageObjects.searchPlayground.PlaygroundStartChatPage.removeIndexFromComboBox(); await esArchiver.unload(esArchiveIndex); await browser.refresh(); }); @@ -116,14 +114,8 @@ export default function (ftrContext: FtrProviderContext) { await browser.refresh(); }); - it('dropdown shows up', async () => { - await pageObjects.searchPlayground.PlaygroundStartChatPage.expectIndicesInDropdown(); - }); - - it('can select index from dropdown and navigate to chat window', async () => { - await pageObjects.searchPlayground.PlaygroundStartChatPage.expectToSelectIndicesAndStartButtonEnabled( - indexName - ); + it('can select index from dropdown and load chat page', async () => { + await pageObjects.searchPlayground.PlaygroundStartChatPage.expectToSelectIndicesAndLoadChat(); }); after(async () => { @@ -139,7 +131,7 @@ export default function (ftrContext: FtrProviderContext) { await createConnector(); await createIndex(); await browser.refresh(); - await pageObjects.searchPlayground.PlaygroundChatPage.navigateToChatPage(indexName); + await pageObjects.searchPlayground.PlaygroundChatPage.navigateToChatPage(); }); it('loads successfully', async () => { await pageObjects.searchPlayground.PlaygroundChatPage.expectChatWindowLoaded(); diff --git a/x-pack/test/functional/page_objects/search_playground_page.ts b/x-pack/test/functional/page_objects/search_playground_page.ts index 89bffc8bbd841..01f50245a1a90 100644 --- a/x-pack/test/functional/page_objects/search_playground_page.ts +++ b/x-pack/test/functional/page_objects/search_playground_page.ts @@ -10,16 +10,28 @@ import { FtrProviderContext } from '../ftr_provider_context'; export function SearchPlaygroundPageProvider({ getService }: FtrProviderContext) { const testSubjects = getService('testSubjects'); - const comboBox = getService('comboBox'); const browser = getService('browser'); + const selectIndex = async () => { + await testSubjects.existOrFail('addDataSourcesButton'); + await testSubjects.click('addDataSourcesButton'); + await testSubjects.existOrFail('selectIndicesFlyout'); + await testSubjects.click('sourceIndex-0'); + await testSubjects.click('saveButton'); + }; return { PlaygroundStartChatPage: { async expectPlaygroundStartChatPageComponentsToExist() { - await testSubjects.existOrFail('startChatPage'); - await testSubjects.existOrFail('connectToLLMChatPanel'); - await testSubjects.existOrFail('selectIndicesChatPanel'); - await testSubjects.existOrFail('startChatButton'); + await testSubjects.existOrFail('setupPage'); + await testSubjects.existOrFail('connectLLMButton'); + }, + + async expectPlaygroundStartChatPageIndexButtonExists() { + await testSubjects.existOrFail('createIndexButton'); + }, + + async expectPlaygroundStartChatPageIndexCalloutExists() { + await testSubjects.existOrFail('createIndexCallout'); }, async expectPlaygroundHeaderComponentsToExist() { @@ -28,8 +40,8 @@ export function SearchPlaygroundPageProvider({ getService }: FtrProviderContext) }, async expectPlaygroundHeaderComponentsToDisabled() { - expect(await testSubjects.isEnabled('editContextActionButton')).to.be(false); - expect(await testSubjects.isEnabled('viewQueryActionButton')).to.be(false); + expect(await testSubjects.getAttribute('viewModeSelector', 'disabled')).to.be('true'); + expect(await testSubjects.isEnabled('dataSourceActionButton')).to.be(false); expect(await testSubjects.isEnabled('viewCodeActionButton')).to.be(false); }, @@ -37,66 +49,45 @@ export function SearchPlaygroundPageProvider({ getService }: FtrProviderContext) await testSubjects.existOrFail('createIndexButton'); }, - async expectNoIndexCalloutExists() { - await testSubjects.existOrFail('createIndexCallout'); - }, - - async expectSelectIndex(indexName: string) { + async expectOpenFlyoutAndSelectIndex() { await browser.refresh(); - await testSubjects.missingOrFail('createIndexCallout'); - await testSubjects.existOrFail('selectIndicesComboBox'); - await comboBox.setCustom('selectIndicesComboBox', indexName); + await selectIndex(); + await testSubjects.existOrFail('dataSourcesSuccessButton'); }, - async expectIndicesInDropdown() { - await testSubjects.existOrFail('selectIndicesComboBox'); - }, - - async removeIndexFromComboBox() { - await testSubjects.click('removeIndexButton'); - }, - - async expectToSelectIndicesAndStartButtonEnabled(indexName: string) { - await comboBox.setCustom('selectIndicesComboBox', indexName); - expect(await testSubjects.isEnabled('startChatButton')).to.be(true); - expect(await testSubjects.isEnabled('editContextActionButton')).to.be(true); - expect(await testSubjects.isEnabled('viewQueryActionButton')).to.be(true); - expect(await testSubjects.isEnabled('viewCodeActionButton')).to.be(true); - - await testSubjects.click('startChatButton'); + async expectToSelectIndicesAndLoadChat() { + await selectIndex(); await testSubjects.existOrFail('chatPage'); }, async expectAddConnectorButtonExists() { - await testSubjects.existOrFail('setupGenAIConnectorButton'); + await testSubjects.existOrFail('connectLLMButton'); }, async expectOpenConnectorPagePlayground() { - await testSubjects.click('setupGenAIConnectorButton'); + await testSubjects.click('connectLLMButton'); await testSubjects.existOrFail('create-connector-flyout'); }, - async expectHideGenAIPanelConnectorAfterCreatingConnector( - createConnector: () => Promise - ) { + async expectSuccessButtonAfterCreatingConnector(createConnector: () => Promise) { await createConnector(); await browser.refresh(); - await testSubjects.missingOrFail('connectToLLMChatPanel'); + await testSubjects.existOrFail('successConnectLLMButton'); }, - async expectHideGenAIPanelConnector() { - await testSubjects.missingOrFail('connectToLLMChatPanel'); + async expectShowSuccessLLMButton() { + await testSubjects.existOrFail('successConnectLLMButton'); }, }, PlaygroundChatPage: { - async navigateToChatPage(indexName: string) { - await comboBox.setCustom('selectIndicesComboBox', indexName); - await testSubjects.click('startChatButton'); + async navigateToChatPage() { + await selectIndex(); + await testSubjects.existOrFail('chatPage'); }, async expectChatWindowLoaded() { - expect(await testSubjects.isEnabled('editContextActionButton')).to.be(true); - expect(await testSubjects.isEnabled('viewQueryActionButton')).to.be(true); + expect(await testSubjects.getAttribute('viewModeSelector', 'disabled')).to.be(null); + expect(await testSubjects.isEnabled('dataSourceActionButton')).to.be(true); expect(await testSubjects.isEnabled('viewCodeActionButton')).to.be(true); expect(await testSubjects.isEnabled('regenerateActionButton')).to.be(false); @@ -114,9 +105,8 @@ export function SearchPlaygroundPageProvider({ getService }: FtrProviderContext) await (await testSubjects.find('manageConnectorsLink')).getAttribute('href') ).to.contain('/app/management/insightsAndAlerting/triggersActionsConnectors/connectors/'); - await testSubjects.click('sourcesAccordion'); - - expect(await testSubjects.findAll('indicesInAccordian')).to.have.length(1); + await testSubjects.existOrFail('editContextPanel'); + await testSubjects.existOrFail('summarizationPanel'); }, async sendQuestion() { @@ -145,9 +135,9 @@ export function SearchPlaygroundPageProvider({ getService }: FtrProviderContext) }, async expectViewQueryHasFields() { - await testSubjects.click('viewQueryActionButton'); - await testSubjects.existOrFail('viewQueryFlyout'); - const fields = await testSubjects.findAll('queryField'); + await testSubjects.existOrFail('queryMode'); + await testSubjects.click('queryMode'); + const fields = await testSubjects.findAll('fieldName'); expect(fields.length).to.be(1); @@ -156,18 +146,16 @@ export function SearchPlaygroundPageProvider({ getService }: FtrProviderContext) expect(code.replace(/ /g, '')).to.be( '{\n"retriever":{\n"standard":{\n"query":{\n"multi_match":{\n"query":"{query}",\n"fields":[\n"baz"\n]\n}\n}\n}\n}\n}' ); - await testSubjects.click('euiFlyoutCloseButton'); }, async expectEditContextOpens() { - await testSubjects.click('editContextActionButton'); - await testSubjects.existOrFail('editContextFlyout'); - await testSubjects.click('contextFieldsSelectable_basic_index'); + await testSubjects.click('chatMode'); + await testSubjects.existOrFail('contextFieldsSelectable-0'); + await testSubjects.click('contextFieldsSelectable-0'); await testSubjects.existOrFail('contextField'); const fields = await testSubjects.findAll('contextField'); expect(fields.length).to.be(1); - await testSubjects.click('euiFlyoutCloseButton'); }, }, }; diff --git a/x-pack/test_serverless/functional/test_suites/search/search_playground/playground_overview.ts b/x-pack/test_serverless/functional/test_suites/search/search_playground/playground_overview.ts index 33e2e9883b38b..b600da8ae4343 100644 --- a/x-pack/test_serverless/functional/test_suites/search/search_playground/playground_overview.ts +++ b/x-pack/test_serverless/functional/test_suites/search/search_playground/playground_overview.ts @@ -12,7 +12,6 @@ import { RoleCredentials } from '../../../../shared/services'; import { createOpenAIConnector } from './utils/create_openai_connector'; import { createLlmProxy, LlmProxy } from './utils/create_llm_proxy'; -const indexName = 'basic_index'; const esArchiveIndex = 'test/api_integration/fixtures/es_archiver/index_patterns/basic_index'; export default function ({ getPageObjects, getService }: FtrProviderContext) { @@ -66,6 +65,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { await pageObjects.searchPlayground.PlaygroundStartChatPage.expectPlaygroundHeaderComponentsToExist(); await pageObjects.searchPlayground.PlaygroundStartChatPage.expectPlaygroundHeaderComponentsToDisabled(); await pageObjects.searchPlayground.PlaygroundStartChatPage.expectPlaygroundStartChatPageComponentsToExist(); + await pageObjects.searchPlayground.PlaygroundStartChatPage.expectPlaygroundStartChatPageIndexCalloutExists(); }); describe('with gen ai connectors', () => { @@ -78,8 +78,8 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { await removeOpenAIConnector?.(); await browser.refresh(); }); - it('hide gen ai panel', async () => { - await pageObjects.searchPlayground.PlaygroundStartChatPage.expectHideGenAIPanelConnector(); + it('show success llm button', async () => { + await pageObjects.searchPlayground.PlaygroundStartChatPage.expectShowSuccessLLMButton(); }); }); @@ -90,7 +90,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { it('creates a connector successfully', async () => { await pageObjects.searchPlayground.PlaygroundStartChatPage.expectOpenConnectorPagePlayground(); - await pageObjects.searchPlayground.PlaygroundStartChatPage.expectHideGenAIPanelConnectorAfterCreatingConnector( + await pageObjects.searchPlayground.PlaygroundStartChatPage.expectSuccessButtonAfterCreatingConnector( createConnector ); }); @@ -102,17 +102,12 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { }); describe('without any indices', () => { - it('show no index callout', async () => { - await pageObjects.searchPlayground.PlaygroundStartChatPage.expectNoIndexCalloutExists(); - }); - it('hide no index callout when index added', async () => { await createIndex(); - await pageObjects.searchPlayground.PlaygroundStartChatPage.expectSelectIndex(indexName); + await pageObjects.searchPlayground.PlaygroundStartChatPage.expectOpenFlyoutAndSelectIndex(); }); after(async () => { - await pageObjects.searchPlayground.PlaygroundStartChatPage.removeIndexFromComboBox(); await esArchiver.unload(esArchiveIndex); await browser.refresh(); }); @@ -125,14 +120,8 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { await browser.refresh(); }); - it('dropdown shows up', async () => { - await pageObjects.searchPlayground.PlaygroundStartChatPage.expectIndicesInDropdown(); - }); - - it('can select index from dropdown and navigate to chat window', async () => { - await pageObjects.searchPlayground.PlaygroundStartChatPage.expectToSelectIndicesAndStartButtonEnabled( - indexName - ); + it('can select index from dropdown and load chat page', async () => { + await pageObjects.searchPlayground.PlaygroundStartChatPage.expectToSelectIndicesAndLoadChat(); }); after(async () => { @@ -148,7 +137,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { await createConnector(); await createIndex(); await browser.refresh(); - await pageObjects.searchPlayground.PlaygroundChatPage.navigateToChatPage(indexName); + await pageObjects.searchPlayground.PlaygroundChatPage.navigateToChatPage(); }); it('loads successfully', async () => { await pageObjects.searchPlayground.PlaygroundChatPage.expectChatWindowLoaded();