mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 01:13:23 -04:00
# Backport This will backport the following commits from `main` to `8.15`: - [[Search][Playground] Update UI (#187608)](https://github.com/elastic/kibana/pull/187608) <!--- Backport version: 9.4.3 --> ### Questions ? Please refer to the [Backport tool documentation](https://github.com/sqren/backport) <!--BACKPORT [{"author":{"name":"Yan Savitski","email":"yan.savitski@elastic.co"},"sourceCommit":{"committedDate":"2024-07-10T13:05:59Z","message":"[Search][Playground] Update UI (#187608)\n\n## Summary\r\n\r\nSummarize your PR. If it involves visual changes include a screenshot or\r\ngif.\r\n\r\n\r\n### Checklist\r\n\r\nDelete any items that are not applicable to this PR.\r\n\r\n- [ ] Any text added follows [EUI's writing\r\nguidelines](https://elastic.github.io/eui/#/guidelines/writing), uses\r\nsentence case text and includes [i18n\r\nsupport](https://github.com/elastic/kibana/blob/main/packages/kbn-i18n/README.md)\r\n- [ ]\r\n[Documentation](https://www.elastic.co/guide/en/kibana/master/development-documentation.html)\r\nwas added for features that require explanation or tutorials\r\n- [ ] [Unit or functional\r\ntests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)\r\nwere updated or added to match the most common scenarios\r\n- [ ] [Flaky Test\r\nRunner](https://ci-stats.kibana.dev/trigger_flaky_test_runner/1) was\r\nused on any tests changed\r\n- [ ] Any UI touched in this PR is usable by keyboard only (learn more\r\nabout [keyboard accessibility](https://webaim.org/techniques/keyboard/))\r\n- [ ] Any UI touched in this PR does not create any new axe failures\r\n(run axe in browser:\r\n[FF](https://addons.mozilla.org/en-US/firefox/addon/axe-devtools/),\r\n[Chrome](https://chrome.google.com/webstore/detail/axe-web-accessibility-tes/lhdoppojpmngadmnindnejefpokejbdd?hl=en-US))\r\n- [ ] If a plugin configuration key changed, check if it needs to be\r\nallowlisted in the cloud and added to the [docker\r\nlist](https://github.com/elastic/kibana/blob/main/src/dev/build/tasks/os_packages/docker_generator/resources/base/bin/kibana-docker)\r\n- [ ] This renders correctly on smaller devices using a responsive\r\nlayout. (You can test this [in your\r\nbrowser](https://www.browserstack.com/guide/responsive-testing-on-local-server))\r\n- [ ] This was checked for [cross-browser\r\ncompatibility](https://www.elastic.co/support/matrix#matrix_browsers)\r\n\r\n\r\n### Risk Matrix\r\n\r\nDelete this section if it is not applicable to this PR.\r\n\r\nBefore closing this PR, invite QA, stakeholders, and other developers to\r\nidentify risks that should be tested prior to the change/feature\r\nrelease.\r\n\r\nWhen forming the risk matrix, consider some of the following examples\r\nand how they may potentially impact the change:\r\n\r\n| Risk | Probability | Severity | Mitigation/Notes |\r\n\r\n|---------------------------|-------------|----------|-------------------------|\r\n| Multiple Spaces—unexpected behavior in non-default Kibana Space.\r\n| Low | High | Integration tests will verify that all features are still\r\nsupported in non-default Kibana Space and when user switches between\r\nspaces. |\r\n| Multiple nodes—Elasticsearch polling might have race conditions\r\nwhen multiple Kibana nodes are polling for the same tasks. | High | Low\r\n| Tasks are idempotent, so executing them multiple times will not result\r\nin logical error, but will degrade performance. To test for this case we\r\nadd plenty of unit tests around this logic and document manual testing\r\nprocedure. |\r\n| Code should gracefully handle cases when feature X or plugin Y are\r\ndisabled. | Medium | High | Unit tests will verify that any feature flag\r\nor plugin combination still results in our service operational. |\r\n| [See more potential risk\r\nexamples](https://github.com/elastic/kibana/blob/main/RISK_MATRIX.mdx) |\r\n\r\n\r\n### For maintainers\r\n\r\n- [ ] This was checked for breaking API changes and was [labeled\r\nappropriately](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process)\r\n\r\n---------\r\n\r\nCo-authored-by: Joseph McElroy <joseph.mcelroy@elastic.co>","sha":"77267b28ba6be1a85c7a2ee1169db48864b52ef0","branchLabelMapping":{"^v8.16.0$":"main","^v(\\d+).(\\d+).\\d+$":"$1.$2"}},"sourcePullRequest":{"labels":["release_note:skip","Team:EnterpriseSearch","v8.15.0","v8.16.0"],"title":"[Search][Playground] Update UI","number":187608,"url":"https://github.com/elastic/kibana/pull/187608","mergeCommit":{"message":"[Search][Playground] Update UI (#187608)\n\n## Summary\r\n\r\nSummarize your PR. If it involves visual changes include a screenshot or\r\ngif.\r\n\r\n\r\n### Checklist\r\n\r\nDelete any items that are not applicable to this PR.\r\n\r\n- [ ] Any text added follows [EUI's writing\r\nguidelines](https://elastic.github.io/eui/#/guidelines/writing), uses\r\nsentence case text and includes [i18n\r\nsupport](https://github.com/elastic/kibana/blob/main/packages/kbn-i18n/README.md)\r\n- [ ]\r\n[Documentation](https://www.elastic.co/guide/en/kibana/master/development-documentation.html)\r\nwas added for features that require explanation or tutorials\r\n- [ ] [Unit or functional\r\ntests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)\r\nwere updated or added to match the most common scenarios\r\n- [ ] [Flaky Test\r\nRunner](https://ci-stats.kibana.dev/trigger_flaky_test_runner/1) was\r\nused on any tests changed\r\n- [ ] Any UI touched in this PR is usable by keyboard only (learn more\r\nabout [keyboard accessibility](https://webaim.org/techniques/keyboard/))\r\n- [ ] Any UI touched in this PR does not create any new axe failures\r\n(run axe in browser:\r\n[FF](https://addons.mozilla.org/en-US/firefox/addon/axe-devtools/),\r\n[Chrome](https://chrome.google.com/webstore/detail/axe-web-accessibility-tes/lhdoppojpmngadmnindnejefpokejbdd?hl=en-US))\r\n- [ ] If a plugin configuration key changed, check if it needs to be\r\nallowlisted in the cloud and added to the [docker\r\nlist](https://github.com/elastic/kibana/blob/main/src/dev/build/tasks/os_packages/docker_generator/resources/base/bin/kibana-docker)\r\n- [ ] This renders correctly on smaller devices using a responsive\r\nlayout. (You can test this [in your\r\nbrowser](https://www.browserstack.com/guide/responsive-testing-on-local-server))\r\n- [ ] This was checked for [cross-browser\r\ncompatibility](https://www.elastic.co/support/matrix#matrix_browsers)\r\n\r\n\r\n### Risk Matrix\r\n\r\nDelete this section if it is not applicable to this PR.\r\n\r\nBefore closing this PR, invite QA, stakeholders, and other developers to\r\nidentify risks that should be tested prior to the change/feature\r\nrelease.\r\n\r\nWhen forming the risk matrix, consider some of the following examples\r\nand how they may potentially impact the change:\r\n\r\n| Risk | Probability | Severity | Mitigation/Notes |\r\n\r\n|---------------------------|-------------|----------|-------------------------|\r\n| Multiple Spaces—unexpected behavior in non-default Kibana Space.\r\n| Low | High | Integration tests will verify that all features are still\r\nsupported in non-default Kibana Space and when user switches between\r\nspaces. |\r\n| Multiple nodes—Elasticsearch polling might have race conditions\r\nwhen multiple Kibana nodes are polling for the same tasks. | High | Low\r\n| Tasks are idempotent, so executing them multiple times will not result\r\nin logical error, but will degrade performance. To test for this case we\r\nadd plenty of unit tests around this logic and document manual testing\r\nprocedure. |\r\n| Code should gracefully handle cases when feature X or plugin Y are\r\ndisabled. | Medium | High | Unit tests will verify that any feature flag\r\nor plugin combination still results in our service operational. |\r\n| [See more potential risk\r\nexamples](https://github.com/elastic/kibana/blob/main/RISK_MATRIX.mdx) |\r\n\r\n\r\n### For maintainers\r\n\r\n- [ ] This was checked for breaking API changes and was [labeled\r\nappropriately](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process)\r\n\r\n---------\r\n\r\nCo-authored-by: Joseph McElroy <joseph.mcelroy@elastic.co>","sha":"77267b28ba6be1a85c7a2ee1169db48864b52ef0"}},"sourceBranch":"main","suggestedTargetBranches":["8.15"],"targetPullRequestStates":[{"branch":"8.15","label":"v8.15.0","branchLabelMappingKey":"^v(\\d+).(\\d+).\\d+$","isSourceBranch":false,"state":"NOT_CREATED"},{"branch":"main","label":"v8.16.0","branchLabelMappingKey":"^v8.16.0$","isSourceBranch":true,"state":"MERGED","url":"https://github.com/elastic/kibana/pull/187608","number":187608,"mergeCommit":{"message":"[Search][Playground] Update UI (#187608)\n\n## Summary\r\n\r\nSummarize your PR. If it involves visual changes include a screenshot or\r\ngif.\r\n\r\n\r\n### Checklist\r\n\r\nDelete any items that are not applicable to this PR.\r\n\r\n- [ ] Any text added follows [EUI's writing\r\nguidelines](https://elastic.github.io/eui/#/guidelines/writing), uses\r\nsentence case text and includes [i18n\r\nsupport](https://github.com/elastic/kibana/blob/main/packages/kbn-i18n/README.md)\r\n- [ ]\r\n[Documentation](https://www.elastic.co/guide/en/kibana/master/development-documentation.html)\r\nwas added for features that require explanation or tutorials\r\n- [ ] [Unit or functional\r\ntests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)\r\nwere updated or added to match the most common scenarios\r\n- [ ] [Flaky Test\r\nRunner](https://ci-stats.kibana.dev/trigger_flaky_test_runner/1) was\r\nused on any tests changed\r\n- [ ] Any UI touched in this PR is usable by keyboard only (learn more\r\nabout [keyboard accessibility](https://webaim.org/techniques/keyboard/))\r\n- [ ] Any UI touched in this PR does not create any new axe failures\r\n(run axe in browser:\r\n[FF](https://addons.mozilla.org/en-US/firefox/addon/axe-devtools/),\r\n[Chrome](https://chrome.google.com/webstore/detail/axe-web-accessibility-tes/lhdoppojpmngadmnindnejefpokejbdd?hl=en-US))\r\n- [ ] If a plugin configuration key changed, check if it needs to be\r\nallowlisted in the cloud and added to the [docker\r\nlist](https://github.com/elastic/kibana/blob/main/src/dev/build/tasks/os_packages/docker_generator/resources/base/bin/kibana-docker)\r\n- [ ] This renders correctly on smaller devices using a responsive\r\nlayout. (You can test this [in your\r\nbrowser](https://www.browserstack.com/guide/responsive-testing-on-local-server))\r\n- [ ] This was checked for [cross-browser\r\ncompatibility](https://www.elastic.co/support/matrix#matrix_browsers)\r\n\r\n\r\n### Risk Matrix\r\n\r\nDelete this section if it is not applicable to this PR.\r\n\r\nBefore closing this PR, invite QA, stakeholders, and other developers to\r\nidentify risks that should be tested prior to the change/feature\r\nrelease.\r\n\r\nWhen forming the risk matrix, consider some of the following examples\r\nand how they may potentially impact the change:\r\n\r\n| Risk | Probability | Severity | Mitigation/Notes |\r\n\r\n|---------------------------|-------------|----------|-------------------------|\r\n| Multiple Spaces—unexpected behavior in non-default Kibana Space.\r\n| Low | High | Integration tests will verify that all features are still\r\nsupported in non-default Kibana Space and when user switches between\r\nspaces. |\r\n| Multiple nodes—Elasticsearch polling might have race conditions\r\nwhen multiple Kibana nodes are polling for the same tasks. | High | Low\r\n| Tasks are idempotent, so executing them multiple times will not result\r\nin logical error, but will degrade performance. To test for this case we\r\nadd plenty of unit tests around this logic and document manual testing\r\nprocedure. |\r\n| Code should gracefully handle cases when feature X or plugin Y are\r\ndisabled. | Medium | High | Unit tests will verify that any feature flag\r\nor plugin combination still results in our service operational. |\r\n| [See more potential risk\r\nexamples](https://github.com/elastic/kibana/blob/main/RISK_MATRIX.mdx) |\r\n\r\n\r\n### For maintainers\r\n\r\n- [ ] This was checked for breaking API changes and was [labeled\r\nappropriately](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process)\r\n\r\n---------\r\n\r\nCo-authored-by: Joseph McElroy <joseph.mcelroy@elastic.co>","sha":"77267b28ba6be1a85c7a2ee1169db48864b52ef0"}}]}] BACKPORT--> Co-authored-by: Yan Savitski <yan.savitski@elastic.co>
This commit is contained in:
parent
fc2898c058
commit
0cc8062cee
59 changed files with 1489 additions and 1878 deletions
|
@ -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: (
|
||||
<EuiFlexGroup>
|
||||
<EuiFlexItem grow={false}>
|
||||
<FormattedMessage
|
||||
id="xpack.enterpriseSearch.content.playground.headerTitle"
|
||||
defaultMessage="Playground"
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiBetaBadge
|
||||
label={i18n.translate(
|
||||
'xpack.enterpriseSearch.content.playground.headerTitle.techPreview',
|
||||
{
|
||||
defaultMessage: 'TECH PREVIEW',
|
||||
}
|
||||
)}
|
||||
color="hollow"
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
),
|
||||
rightSideItems: [<searchPlayground.PlaygroundToolbar />],
|
||||
}}
|
||||
pageViewTelemetry="Playground"
|
||||
restrictWidth={false}
|
||||
panelled={false}
|
||||
customPageSections
|
||||
bottomBorder="extended"
|
||||
docLink="playground"
|
||||
|
|
|
@ -12,7 +12,6 @@ export type Start = jest.Mocked<SearchPlaygroundPluginStart>;
|
|||
const createStartMock = (): Start => {
|
||||
const startContract: Start = {
|
||||
PlaygroundProvider: jest.fn(),
|
||||
PlaygroundToolbar: jest.fn(),
|
||||
Playground: jest.fn(),
|
||||
PlaygroundHeaderDocs: jest.fn(),
|
||||
};
|
||||
|
|
|
@ -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',
|
||||
}
|
||||
|
|
|
@ -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 (
|
||||
<PlaygroundProvider>
|
||||
<EuiPageTemplate
|
||||
offset={0}
|
||||
restrictWidth={false}
|
||||
data-test-subj="svlPlaygroundPage"
|
||||
grow={false}
|
||||
>
|
||||
<EuiPageTemplate.Header
|
||||
css={{ '.euiPageHeaderContent > .euiFlexGroup': { flexWrap: 'wrap' } }}
|
||||
pageTitle={
|
||||
<EuiFlexGroup>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiTitle
|
||||
css={{ whiteSpace: 'nowrap' }}
|
||||
data-test-subj="chat-playground-home-page-title"
|
||||
>
|
||||
<h2>
|
||||
<FormattedMessage
|
||||
id="xpack.searchPlayground.pageTitle"
|
||||
defaultMessage="Playground"
|
||||
/>
|
||||
</h2>
|
||||
</EuiTitle>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiBetaBadge
|
||||
label={i18n.translate('xpack.searchPlayground.pageTitle.techPreview', {
|
||||
defaultMessage: 'TECH PREVIEW',
|
||||
})}
|
||||
color="hollow"
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
}
|
||||
data-test-subj="chat-playground-home-page"
|
||||
rightSideItems={[<PlaygroundHeaderDocs />, <PlaygroundToolbar />]}
|
||||
/>
|
||||
<App />
|
||||
{embeddableConsole}
|
||||
</EuiPageTemplate>
|
||||
</PlaygroundProvider>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<PlaygroundProvider>
|
||||
<EuiPageTemplate
|
||||
offset={0}
|
||||
restrictWidth={false}
|
||||
data-test-subj="svlPlaygroundPage"
|
||||
grow={false}
|
||||
panelled={false}
|
||||
>
|
||||
<App showDocs />
|
||||
{embeddableConsole}
|
||||
</EuiPageTemplate>
|
||||
</PlaygroundProvider>
|
||||
</QueryClientProvider>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -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<AppProps> = ({ showDocs = false }) => {
|
||||
const [showSetupPage, setShowSetupPage] = useState(true);
|
||||
const [selectedMode, setSelectedMode] = useState<ViewMode>(ViewMode.chat);
|
||||
const { watch } = useFormContext<ChatForm>();
|
||||
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 (
|
||||
<KibanaPageTemplate.Section
|
||||
alignment="top"
|
||||
restrictWidth={false}
|
||||
grow
|
||||
css={{
|
||||
position: 'relative',
|
||||
}}
|
||||
contentProps={{ css: { display: 'flex', flexGrow: 1, position: 'absolute', inset: 0 } }}
|
||||
paddingSize="none"
|
||||
className="eui-fullHeight"
|
||||
>
|
||||
{showStartPage ? <StartNewChat onStartClick={() => setShowStartPage(false)} /> : <Chat />}
|
||||
</KibanaPageTemplate.Section>
|
||||
<>
|
||||
<Header
|
||||
showDocs={showDocs}
|
||||
onModeChange={handleModeChange}
|
||||
selectedMode={selectedMode}
|
||||
isActionsDisabled={showSetupPage}
|
||||
/>
|
||||
<KibanaPageTemplate.Section
|
||||
alignment="top"
|
||||
restrictWidth={false}
|
||||
grow
|
||||
css={{
|
||||
position: 'relative',
|
||||
}}
|
||||
contentProps={{ css: { display: 'flex', flexGrow: 1, position: 'absolute', inset: 0 } }}
|
||||
paddingSize="none"
|
||||
className="eui-fullHeight"
|
||||
>
|
||||
{showSetupPage ? <SetupPage /> : selectedMode === ViewMode.chat ? <Chat /> : <QueryMode />}
|
||||
</KibanaPageTemplate.Section>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -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<ChatForm>();
|
||||
const { messages, append, stop: stopRequest, setMessages, reload, error } = useChat();
|
||||
const selectedIndicesCount = watch(ChatFormFields.indices, []).length;
|
||||
const messagesRef = useAutoBottomScroll();
|
||||
const [isRegenerating, setIsRegenerating] = useState<boolean>(false);
|
||||
const usageTracker = useUsageTracker();
|
||||
|
@ -123,7 +122,6 @@ export const Chat = () => {
|
|||
<EuiFlexItem
|
||||
grow={2}
|
||||
css={{
|
||||
borderRight: euiTheme.border.thin,
|
||||
paddingTop: euiTheme.size.l,
|
||||
paddingBottom: euiTheme.size.l,
|
||||
// don't allow the chat to shrink below 66.6% of the screen
|
||||
|
@ -149,14 +147,15 @@ export const Chat = () => {
|
|||
>
|
||||
<EuiHorizontalRule margin="none" />
|
||||
|
||||
<EuiSpacer size="m" />
|
||||
<EuiSpacer size="s" />
|
||||
|
||||
<EuiFlexGroup>
|
||||
<EuiFlexGroup responsive={false}>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButtonEmpty
|
||||
iconType="sparkles"
|
||||
disabled={isToolBarActionsDisabled}
|
||||
onClick={regenerateMessages}
|
||||
size="xs"
|
||||
data-test-subj="regenerateActionButton"
|
||||
>
|
||||
<FormattedMessage
|
||||
|
@ -170,6 +169,7 @@ export const Chat = () => {
|
|||
iconType="refresh"
|
||||
disabled={isToolBarActionsDisabled}
|
||||
onClick={handleClearChat}
|
||||
size="xs"
|
||||
data-test-subj="clearChatActionButton"
|
||||
>
|
||||
<FormattedMessage
|
||||
|
@ -180,7 +180,7 @@ export const Chat = () => {
|
|||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
|
||||
<EuiSpacer size="m" />
|
||||
<EuiSpacer size="s" />
|
||||
|
||||
<Controller
|
||||
name={ChatFormFields.question}
|
||||
|
@ -234,9 +234,11 @@ export const Chat = () => {
|
|||
</EuiFlexGroup>
|
||||
</EuiFlexItem>
|
||||
|
||||
<EuiFlexItem grow={1} css={{ flexBasis: 0, minWidth: '33.3%' }}>
|
||||
<ChatSidebar selectedIndicesCount={selectedIndicesCount} />
|
||||
</EuiFlexItem>
|
||||
<EuiHideFor sizes={['xs', 's']}>
|
||||
<EuiFlexItem grow={1} css={{ flexBasis: 0, minWidth: '33.3%' }}>
|
||||
<ChatSidebar />
|
||||
</EuiFlexItem>
|
||||
</EuiHideFor>
|
||||
</EuiFlexGroup>
|
||||
</EuiForm>
|
||||
);
|
||||
|
|
|
@ -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<ChatSidebarProps> = ({ selectedIndicesCount }) => {
|
||||
export const ChatSidebar: React.FC = () => {
|
||||
const { euiTheme } = useEuiTheme();
|
||||
const accordions = [
|
||||
const selectedModel = useWatch<ChatForm, ChatFormFields.summarizationModel>({
|
||||
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: <SummarizationPanel />,
|
||||
dataTestId: 'summarizationAccordion',
|
||||
extraAction: (
|
||||
<EuiButtonEmpty
|
||||
target="_blank"
|
||||
href={managementLink}
|
||||
data-test-subj="manageConnectorsLink"
|
||||
iconType="wrench"
|
||||
size="s"
|
||||
aria-label={i18n.translate(
|
||||
'xpack.searchPlayground.sidebar.summarizationModel.manageConnectorLink',
|
||||
{
|
||||
defaultMessage: 'Manage connector',
|
||||
}
|
||||
)}
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.searchPlayground.sidebar.summarizationModel.manageConnectorTooltip"
|
||||
defaultMessage="Manage"
|
||||
/>
|
||||
</EuiButtonEmpty>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: useGeneratedHtmlId({ prefix: 'sourcesAccordion' }),
|
||||
title: i18n.translate('xpack.searchPlayground.sidebar.sourceTitle', {
|
||||
defaultMessage: 'Indices',
|
||||
title: i18n.translate('xpack.searchPlayground.sidebar.contextTitle', {
|
||||
defaultMessage: 'Context',
|
||||
}),
|
||||
extraAction: !!selectedIndicesCount && (
|
||||
<EuiText size="xs">
|
||||
<p>
|
||||
{i18n.translate('xpack.searchPlayground.sidebar.sourceIndicesCount', {
|
||||
defaultMessage: '{count, number} {count, plural, one {Index} other {Indices}}',
|
||||
values: { count: Number(selectedIndicesCount) },
|
||||
})}
|
||||
</p>
|
||||
</EuiText>
|
||||
extraAction: (
|
||||
<EuiLink
|
||||
href={docLinks.context}
|
||||
target="_blank"
|
||||
data-test-subj="hidden-fields-documentation-link"
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.searchPlayground.sidebar.contextLearnMore"
|
||||
defaultMessage="Learn more"
|
||||
/>
|
||||
</EuiLink>
|
||||
),
|
||||
children: <SourcesPanelSidebar />,
|
||||
dataTestId: 'sourcesAccordion',
|
||||
children: <EditContextPanel />,
|
||||
},
|
||||
];
|
||||
const [openAccordionId, setOpenAccordionId] = useState(accordions[0].id);
|
||||
|
||||
return (
|
||||
<EuiFlexGroup direction="column" className="eui-yScroll" gutterSize="none">
|
||||
{accordions.map(({ id, title, extraAction, children, dataTestId }, index) => (
|
||||
<EuiFlexItem
|
||||
key={id}
|
||||
css={{
|
||||
borderBottom: index === accordions.length - 1 ? 'none' : euiTheme.border.thin,
|
||||
padding: `0 ${euiTheme.size.l}`,
|
||||
flexGrow: openAccordionId === id ? 1 : 0,
|
||||
transition: `${euiTheme.animation.normal} ease-in-out`,
|
||||
}}
|
||||
>
|
||||
<EuiAccordion
|
||||
id={id}
|
||||
buttonContent={
|
||||
<EuiTitle size="xs">
|
||||
<h5>{title}</h5>
|
||||
</EuiTitle>
|
||||
}
|
||||
extraAction={extraAction}
|
||||
buttonProps={{ paddingSize: 'l' }}
|
||||
forceState={openAccordionId === id ? 'open' : 'closed'}
|
||||
onToggle={() => setOpenAccordionId(openAccordionId === id ? '' : id)}
|
||||
data-test-subj={dataTestId}
|
||||
>
|
||||
{children}
|
||||
<EuiSpacer size="l" />
|
||||
</EuiAccordion>
|
||||
{panels?.map(({ title, children, extraAction }) => (
|
||||
<EuiFlexItem key={title} grow={false}>
|
||||
<EuiFlexGroup direction="column" css={{ padding: euiTheme.size.l }} gutterSize="s">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiFlexGroup justifyContent="spaceBetween" gutterSize="none" alignItems="center">
|
||||
<EuiTitle size="xs">
|
||||
<h4>{title}</h4>
|
||||
</EuiTitle>
|
||||
{extraAction && <EuiFlexItem grow={false}>{extraAction}</EuiFlexItem>}
|
||||
</EuiFlexGroup>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>{children}</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiFlexItem>
|
||||
))}
|
||||
</EuiFlexGroup>
|
||||
|
|
|
@ -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<ChatForm, ChatFormFields.indices>({
|
||||
name: ChatFormFields.indices,
|
||||
});
|
||||
const [showFlyout, setShowFlyout] = useState(false);
|
||||
const handleFlyoutClose = () => setShowFlyout(false);
|
||||
const handleShowFlyout = () => setShowFlyout(true);
|
||||
|
||||
return (
|
||||
<>
|
||||
{showFlyout && <SelectIndicesFlyout onClose={handleFlyoutClose} />}
|
||||
<EuiButton
|
||||
size="s"
|
||||
iconType="database"
|
||||
onClick={handleShowFlyout}
|
||||
disabled={!selectedIndices?.length}
|
||||
data-test-subj="dataSourceActionButton"
|
||||
>
|
||||
<FormattedMessage id="xpack.searchPlayground.dataActionButton" defaultMessage="Data" />
|
||||
</EuiButton>
|
||||
</>
|
||||
);
|
||||
};
|
|
@ -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<ChatForm>();
|
||||
const [showFlyout, setShowFlyout] = useState(false);
|
||||
const selectedIndices: string[] = watch('indices');
|
||||
|
||||
const closeFlyout = () => setShowFlyout(false);
|
||||
|
||||
return (
|
||||
<>
|
||||
{showFlyout && <EditContextFlyout onClose={closeFlyout} />}
|
||||
<EuiButtonEmpty
|
||||
onClick={() => setShowFlyout(true)}
|
||||
disabled={selectedIndices?.length === 0}
|
||||
data-test-subj="editContextActionButton"
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.searchPlayground.editContext.actionButtonLabel"
|
||||
defaultMessage="Edit context"
|
||||
/>
|
||||
</EuiButtonEmpty>
|
||||
</>
|
||||
);
|
||||
};
|
|
@ -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<EditContextFlyoutProps> = ({ onClose }) => {
|
||||
const usageTracker = useUsageTracker();
|
||||
const { getValues } = useFormContext<ChatForm>();
|
||||
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<HTMLSelectElement>) => {
|
||||
usageTracker?.click(AnalyticsEvents.editContextDocSizeChanged);
|
||||
setDocSize(Number(e.target.value));
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
usageTracker?.load(AnalyticsEvents.editContextFlyoutOpened);
|
||||
}, [usageTracker]);
|
||||
|
||||
return (
|
||||
<EuiFlyout ownFocus onClose={onClose} size="s" data-test-subj="editContextFlyout">
|
||||
<EuiFlyoutHeader hasBorder>
|
||||
<EuiTitle size="m">
|
||||
<h2>
|
||||
<FormattedMessage
|
||||
id="xpack.searchPlayground.editContext.flyout.title"
|
||||
defaultMessage="Edit context"
|
||||
/>
|
||||
</h2>
|
||||
</EuiTitle>
|
||||
<EuiSpacer size="s" />
|
||||
<EuiText color="subdued">
|
||||
<p>
|
||||
<FormattedMessage
|
||||
id="xpack.searchPlayground.editContext.flyout.description"
|
||||
defaultMessage="Context is the information you provide to the LLM, by selecting fields from your Elasticsearch documents. Optimize context for better results."
|
||||
/>
|
||||
<EuiLink
|
||||
href={docLinks.context}
|
||||
target="_blank"
|
||||
data-test-subj="context-optimization-documentation-link"
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.searchPlayground.editContext.flyout.learnMoreLink"
|
||||
defaultMessage=" Learn more."
|
||||
/>
|
||||
</EuiLink>
|
||||
</p>
|
||||
</EuiText>
|
||||
</EuiFlyoutHeader>
|
||||
<EuiFlyoutBody>
|
||||
<EuiFlexGroup>
|
||||
<EuiFlexItem grow={3}>
|
||||
<EuiFlexGroup direction="column" gutterSize="l">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiFormRow
|
||||
label={i18n.translate(
|
||||
'xpack.searchPlayground.editContext.flyout.docsRetrievedCount',
|
||||
{
|
||||
defaultMessage: 'Number of documents sent to LLM',
|
||||
}
|
||||
)}
|
||||
>
|
||||
<EuiSelect
|
||||
options={[
|
||||
{
|
||||
value: 1,
|
||||
text: '1',
|
||||
},
|
||||
{
|
||||
value: 3,
|
||||
text: '3',
|
||||
},
|
||||
{
|
||||
value: 5,
|
||||
text: '5',
|
||||
},
|
||||
{
|
||||
value: 10,
|
||||
text: '10',
|
||||
},
|
||||
]}
|
||||
value={docSize}
|
||||
onChange={handleDocSizeChange}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<EuiFlexGroup direction="column" gutterSize="m">
|
||||
<EuiFlexItem>
|
||||
<EuiText>
|
||||
<h5>
|
||||
<FormattedMessage
|
||||
id="xpack.searchPlayground.editContext.flyout.table.title"
|
||||
defaultMessage="Select fields"
|
||||
/>
|
||||
</h5>
|
||||
</EuiText>
|
||||
</EuiFlexItem>
|
||||
{Object.entries(fields).map(([index, group]) => (
|
||||
<EuiFlexItem grow={false} key={index}>
|
||||
<EuiFormRow label={index}>
|
||||
<EuiSuperSelect
|
||||
data-test-subj={`contextFieldsSelectable_${index}`}
|
||||
options={group.source_fields.map((field) => ({
|
||||
value: field,
|
||||
inputDisplay: field,
|
||||
'data-test-subj': 'contextField',
|
||||
}))}
|
||||
valueOfSelected={tempSourceFields[index]?.[0]}
|
||||
onChange={(value) => updateSourceField(index, value)}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
</EuiFlexItem>
|
||||
))}
|
||||
</EuiFlexGroup>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiFlyoutBody>
|
||||
<EuiFlyoutFooter>
|
||||
<EuiFlexGroup justifyContent="spaceBetween">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButtonEmpty iconType="cross" onClick={onClose} flush="left">
|
||||
<FormattedMessage
|
||||
id="xpack.searchPlayground.editContext.flyout.closeButton"
|
||||
defaultMessage="Close"
|
||||
/>
|
||||
</EuiButtonEmpty>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButton onClick={saveSourceFields} fill>
|
||||
<FormattedMessage
|
||||
id="xpack.searchPlayground.editContext.flyout.saveButton"
|
||||
defaultMessage="Save changes"
|
||||
/>
|
||||
</EuiButton>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiFlyoutFooter>
|
||||
</EuiFlyout>
|
||||
);
|
||||
};
|
|
@ -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(
|
||||
<IntlProvider locale="en">
|
||||
<MockFormProvider>
|
||||
<EditContextFlyout onClose={onCloseMock} />
|
||||
<EditContextPanel />
|
||||
</MockFormProvider>
|
||||
</IntlProvider>
|
||||
);
|
||||
});
|
||||
|
||||
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);
|
||||
});
|
||||
});
|
|
@ -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<ChatForm, ChatFormFields.sourceFields>({
|
||||
name: ChatFormFields.sourceFields,
|
||||
});
|
||||
|
||||
const updateSourceField = (index: string, field: string) => {
|
||||
onChangeSourceFields({
|
||||
...sourceFields,
|
||||
[index]: [field],
|
||||
});
|
||||
usageTracker?.click(AnalyticsEvents.editContextFieldToggled);
|
||||
};
|
||||
|
||||
const handleDocSizeChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
|
||||
usageTracker?.click(AnalyticsEvents.editContextDocSizeChanged);
|
||||
onChangeSize(Number(e.target.value));
|
||||
};
|
||||
|
||||
return (
|
||||
<EuiPanel data-test-subj="editContextPanel">
|
||||
<EuiFlexGroup direction="column" gutterSize="l">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiFormRow
|
||||
label={i18n.translate('xpack.searchPlayground.editContext.docsRetrievedCount', {
|
||||
defaultMessage: 'Number of documents sent to LLM',
|
||||
})}
|
||||
fullWidth
|
||||
>
|
||||
<EuiSelect
|
||||
options={[
|
||||
{
|
||||
value: 1,
|
||||
text: '1',
|
||||
},
|
||||
{
|
||||
value: 3,
|
||||
text: '3',
|
||||
},
|
||||
{
|
||||
value: 5,
|
||||
text: '5',
|
||||
},
|
||||
{
|
||||
value: 10,
|
||||
text: '10',
|
||||
},
|
||||
]}
|
||||
value={docSize}
|
||||
onChange={handleDocSizeChange}
|
||||
fullWidth
|
||||
/>
|
||||
</EuiFormRow>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<EuiFlexGroup direction="column" gutterSize="m">
|
||||
<EuiFlexItem>
|
||||
<EuiText>
|
||||
<h5>
|
||||
<FormattedMessage
|
||||
id="xpack.searchPlayground.editContext.table.title"
|
||||
defaultMessage="Context fields"
|
||||
/>
|
||||
</h5>
|
||||
</EuiText>
|
||||
</EuiFlexItem>
|
||||
{Object.entries(fields).map(([index, group], indexNum) => (
|
||||
<EuiFlexItem grow={false} key={index}>
|
||||
<EuiFormRow label={index} fullWidth>
|
||||
{!!group.source_fields?.length ? (
|
||||
<EuiSuperSelect
|
||||
data-test-subj={`contextFieldsSelectable-${indexNum}`}
|
||||
options={group.source_fields.map((field) => ({
|
||||
value: field,
|
||||
inputDisplay: field,
|
||||
'data-test-subj': 'contextField',
|
||||
}))}
|
||||
valueOfSelected={sourceFields[index]?.[0]}
|
||||
onChange={(value) => updateSourceField(index, value)}
|
||||
fullWidth
|
||||
/>
|
||||
) : (
|
||||
<EuiCallOut
|
||||
title={i18n.translate(
|
||||
'xpack.searchPlayground.editContext.noSourceFieldWarning',
|
||||
{ defaultMessage: 'No source fields found' }
|
||||
)}
|
||||
color="warning"
|
||||
iconType="warning"
|
||||
size="s"
|
||||
/>
|
||||
)}
|
||||
</EuiFormRow>
|
||||
</EuiFlexItem>
|
||||
))}
|
||||
</EuiFlexGroup>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiPanel>
|
||||
);
|
||||
};
|
103
x-pack/plugins/search_playground/public/components/header.tsx
Normal file
103
x-pack/plugins/search_playground/public/components/header.tsx
Normal file
|
@ -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<HeaderProps> = ({
|
||||
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 (
|
||||
<EuiPageTemplate.Header
|
||||
css={{
|
||||
'.euiPageHeaderContent > .euiFlexGroup': { flexWrap: 'wrap' },
|
||||
backgroundColor: euiTheme.colors.ghost,
|
||||
}}
|
||||
paddingSize="s"
|
||||
data-test-subj="chat-playground-home-page"
|
||||
>
|
||||
<EuiPageHeaderSection>
|
||||
<EuiFlexGroup gutterSize="s" alignItems="center">
|
||||
<EuiTitle
|
||||
css={{ whiteSpace: 'nowrap' }}
|
||||
data-test-subj="chat-playground-home-page-title"
|
||||
size="xs"
|
||||
>
|
||||
<h2>
|
||||
<FormattedMessage id="xpack.searchPlayground.pageTitle" defaultMessage="Playground" />
|
||||
</h2>
|
||||
</EuiTitle>
|
||||
<EuiBetaBadge
|
||||
label={i18n.translate('xpack.searchPlayground.pageTitle.techPreview', {
|
||||
defaultMessage: 'TECH PREVIEW',
|
||||
})}
|
||||
color="hollow"
|
||||
alignment="middle"
|
||||
/>
|
||||
</EuiFlexGroup>
|
||||
</EuiPageHeaderSection>
|
||||
<EuiPageHeaderSection>
|
||||
<EuiButtonGroup
|
||||
legend="viewMode"
|
||||
options={options}
|
||||
idSelected={selectedMode}
|
||||
onChange={onModeChange}
|
||||
buttonSize="compressed"
|
||||
isDisabled={isActionsDisabled}
|
||||
data-test-subj="viewModeSelector"
|
||||
/>
|
||||
</EuiPageHeaderSection>
|
||||
<EuiPageHeaderSection>
|
||||
<EuiFlexGroup alignItems="center">
|
||||
{showDocs && <PlaygroundHeaderDocs />}
|
||||
<Toolbar />
|
||||
</EuiFlexGroup>
|
||||
</EuiPageHeaderSection>
|
||||
</EuiPageTemplate.Header>
|
||||
);
|
||||
};
|
|
@ -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<AssistantMessageProps> = ({ 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<AssistantMessageProps> = ({ 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={
|
||||
<>
|
||||
<EuiText size="s">
|
||||
|
@ -96,6 +106,14 @@ export const AssistantMessage: React.FC<AssistantMessageProps> = ({ 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={
|
||||
<>
|
||||
<EuiText size="s">
|
||||
|
@ -144,6 +162,13 @@ export const AssistantMessage: React.FC<AssistantMessageProps> = ({ 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',
|
||||
|
|
|
@ -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<SystemMessageProps> = ({ content }) => {
|
||||
const { euiTheme } = useEuiTheme();
|
||||
|
||||
return (
|
||||
<EuiComment
|
||||
username={i18n.translate('xpack.searchPlayground.chat.message.system.username', {
|
||||
|
@ -29,6 +31,13 @@ export const SystemMessage: React.FC<SystemMessageProps> = ({ content }) => {
|
|||
event={content}
|
||||
timelineAvatar="dot"
|
||||
eventColor="subdued"
|
||||
css={{
|
||||
'.euiAvatar': { backgroundColor: euiTheme.colors.ghost },
|
||||
'.euiCommentEvent': {
|
||||
border: euiTheme.border.thin,
|
||||
borderRadius: euiTheme.border.radius.medium,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -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<UserMessageProps> = ({ content, createdAt }) => {
|
||||
const { euiTheme } = useEuiTheme();
|
||||
const currentUserProfile = useUserProfile();
|
||||
|
||||
return (
|
||||
<EuiComment
|
||||
eventColor="subdued"
|
||||
css={{
|
||||
'.euiCommentEvent__body': {
|
||||
backgroundColor: euiTheme.colors.ghost,
|
||||
},
|
||||
}}
|
||||
username={i18n.translate('xpack.searchPlayground.chat.message.user.name', {
|
||||
defaultMessage: 'You',
|
||||
})}
|
||||
|
|
|
@ -20,6 +20,7 @@ export const PlaygroundHeaderDocs: React.FC = () => (
|
|||
href={docLinks.chatPlayground}
|
||||
target="_blank"
|
||||
iconType="documents"
|
||||
size="s"
|
||||
>
|
||||
{i18n.translate('xpack.searchPlayground.pageTitle.header.docLink', {
|
||||
defaultMessage: 'Playground Docs',
|
||||
|
|
|
@ -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 <FormProvider {...methods}>{children}</FormProvider>;
|
||||
};
|
||||
|
||||
describe('ViewQueryFlyout component tests', () => {
|
||||
const onCloseMock = jest.fn();
|
||||
|
||||
describe('QueryMode component tests', () => {
|
||||
beforeEach(() => {
|
||||
render(
|
||||
<IntlProvider locale="en">
|
||||
<MockFormProvider>
|
||||
<ViewQueryFlyout onClose={onCloseMock} />
|
||||
<QueryMode />
|
||||
</MockFormProvider>
|
||||
</IntlProvider>
|
||||
);
|
||||
});
|
||||
|
||||
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();
|
||||
});
|
||||
});
|
|
@ -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<ChatForm, ChatFormFields.sourceFields>({
|
||||
name: ChatFormFields.sourceFields,
|
||||
});
|
||||
const {
|
||||
field: { onChange: queryFieldsOnChange, value: queryFields },
|
||||
} = useController<ChatForm, ChatFormFields.queryFields>({
|
||||
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 (
|
||||
<EuiFlexGroup>
|
||||
<EuiFlexItem
|
||||
grow={6}
|
||||
className="eui-yScroll"
|
||||
css={{ padding: euiTheme.size.l, paddingRight: 0 }}
|
||||
>
|
||||
<EuiCodeBlock
|
||||
language="json"
|
||||
fontSize="m"
|
||||
paddingSize="none"
|
||||
lineNumbers
|
||||
transparentBackground
|
||||
data-test-subj="ViewElasticsearchQueryResult"
|
||||
>
|
||||
{query}
|
||||
</EuiCodeBlock>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem
|
||||
grow={3}
|
||||
className="eui-yScroll"
|
||||
css={{ padding: euiTheme.size.l, paddingLeft: 0 }}
|
||||
>
|
||||
<EuiFlexGroup direction="column" gutterSize="s">
|
||||
<EuiText>
|
||||
<h5>
|
||||
<FormattedMessage
|
||||
id="xpack.searchPlayground.viewQuery.flyout.table.title"
|
||||
defaultMessage="Fields to search (per index)"
|
||||
/>
|
||||
</h5>
|
||||
</EuiText>
|
||||
{Object.entries(fields).map(([index, group], indexNum) => (
|
||||
<EuiFlexItem grow={false} key={index}>
|
||||
<EuiPanel grow={false} hasShadow={false} hasBorder>
|
||||
<EuiAccordion
|
||||
id={index}
|
||||
buttonContent={
|
||||
<EuiText>
|
||||
<h5>{index}</h5>
|
||||
</EuiText>
|
||||
}
|
||||
initialIsOpen
|
||||
data-test-subj={`fieldsAccordion-${indexNum}`}
|
||||
>
|
||||
<EuiSpacer size="s" />
|
||||
|
||||
<EuiBasicTable
|
||||
tableCaption="Query Model table"
|
||||
items={[
|
||||
...group.semantic_fields,
|
||||
...group.elser_query_fields,
|
||||
...group.dense_vector_query_fields,
|
||||
...group.bm25_query_fields,
|
||||
].map((field) => ({
|
||||
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 (
|
||||
<EuiSwitch
|
||||
showLabel={false}
|
||||
label={field.name}
|
||||
checked={checked}
|
||||
onChange={(e) => updateFields(index, field.name, e.target.checked)}
|
||||
compressed
|
||||
/>
|
||||
);
|
||||
},
|
||||
},
|
||||
]}
|
||||
/>
|
||||
|
||||
{group.skipped_fields > 0 && (
|
||||
<>
|
||||
<EuiSpacer size="m" />
|
||||
<EuiFlexGroup>
|
||||
<EuiFlexItem>
|
||||
<EuiText
|
||||
size="s"
|
||||
color="subdued"
|
||||
data-test-subj={`skippedFields-${indexNum}`}
|
||||
>
|
||||
<EuiIcon type="eyeClosed" />
|
||||
{` `}
|
||||
<FormattedMessage
|
||||
id="xpack.searchPlayground.viewQuery.flyout.hiddenFields"
|
||||
defaultMessage="{skippedFields} fields are hidden."
|
||||
values={{ skippedFields: group.skipped_fields }}
|
||||
/>
|
||||
</EuiText>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiLink
|
||||
href={docLinks.hiddenFields}
|
||||
target="_blank"
|
||||
data-test-subj="hidden-fields-documentation-link"
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.searchPlayground.viewQuery.flyout.learnMoreLink"
|
||||
defaultMessage="Learn more."
|
||||
/>
|
||||
</EuiLink>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</>
|
||||
)}
|
||||
</EuiAccordion>
|
||||
</EuiPanel>
|
||||
</EuiFlexItem>
|
||||
))}
|
||||
</EuiFlexGroup>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
};
|
|
@ -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<PropsWithChildren<unknown>> = ({ children }) => {
|
||||
return (
|
||||
<>
|
||||
<IntlProvider locale="en">{children}</IntlProvider>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const mockedUseSourceIndicesFields = useSourceIndicesFields as jest.MockedFunction<
|
||||
typeof useSourceIndicesFields
|
||||
>;
|
||||
const mockedUseQueryIndices = useQueryIndices as jest.MockedFunction<typeof useQueryIndices>;
|
||||
|
||||
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(<SelectIndicesFlyout onClose={onCloseMock} />, {
|
||||
wrapper: Wrapper,
|
||||
});
|
||||
|
||||
expect(getByTestId('indicesTable')).toBeInTheDocument();
|
||||
expect(getByTestId('saveButton')).toBeInTheDocument();
|
||||
expect(getByTestId('closeButton')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('selecting indices and saving', async () => {
|
||||
const { getByTestId } = render(<SelectIndicesFlyout onClose={onCloseMock} />, {
|
||||
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(<SelectIndicesFlyout onClose={onCloseMock} />, {
|
||||
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(<SelectIndicesFlyout onClose={onCloseMock} />, {
|
||||
wrapper: Wrapper,
|
||||
});
|
||||
|
||||
const saveButton = getByTestId('saveButton');
|
||||
expect(saveButton).toBeDisabled();
|
||||
});
|
||||
});
|
|
@ -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<SelectIndicesFlyout> = ({ onClose }) => {
|
||||
const [query, setQuery] = useState<string>('');
|
||||
const { indices, isLoading: isIndicesLoading } = useQueryIndices(query);
|
||||
const { indices: selectedIndices, setIndices: setSelectedIndices } = useSourceIndicesFields();
|
||||
const [selectedTempIndices, setSelectedTempIndices] = useState<string[]>(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: (
|
||||
<>
|
||||
<EuiSpacer />
|
||||
<EuiSelectable
|
||||
searchable
|
||||
searchProps={{
|
||||
onChange: handleSearchChange,
|
||||
}}
|
||||
options={[
|
||||
{
|
||||
label: i18n.translate(
|
||||
'xpack.searchPlayground.setupPage.addDataSource.flyout.groupOption',
|
||||
{
|
||||
defaultMessage: 'Available indices',
|
||||
}
|
||||
),
|
||||
isGroupLabel: true,
|
||||
},
|
||||
...indices.map(
|
||||
(index, num) =>
|
||||
({
|
||||
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}
|
||||
</>
|
||||
)}
|
||||
</EuiSelectable>
|
||||
</>
|
||||
),
|
||||
},
|
||||
];
|
||||
const { fields, isLoading: isFieldsLoading } = useIndicesFields(selectedTempIndices);
|
||||
const noSourceFieldsWarning = getIndicesWithNoSourceFields(fields);
|
||||
|
||||
return (
|
||||
<EuiFlyout size="s" ownFocus onClose={onClose} data-test-subj="selectIndicesFlyout">
|
||||
<EuiFlyoutHeader>
|
||||
<EuiTitle size="m">
|
||||
<h2>
|
||||
<FormattedMessage
|
||||
id="xpack.searchPlayground.addDataSource.flyout.title"
|
||||
defaultMessage="Add data to query"
|
||||
/>
|
||||
</h2>
|
||||
</EuiTitle>
|
||||
</EuiFlyoutHeader>
|
||||
<EuiFlyoutBody>
|
||||
<EuiTabbedContent
|
||||
tabs={tabs}
|
||||
initialSelectedTab={tabs[0]}
|
||||
autoFocus="selected"
|
||||
data-test-subj="indicesTable"
|
||||
/>
|
||||
{!isFieldsLoading && !!noSourceFieldsWarning && (
|
||||
<EuiCallOut color="warning" iconType="warning" data-test-subj="NoIndicesFieldsMessage">
|
||||
<p>
|
||||
<FormattedMessage
|
||||
id="xpack.searchPlayground.addDataSource.flyout.warningCallout"
|
||||
defaultMessage="No fields found for {errorMessage}. Try adding data to these indices."
|
||||
values={{ errorMessage: noSourceFieldsWarning }}
|
||||
/>
|
||||
</p>
|
||||
</EuiCallOut>
|
||||
)}
|
||||
</EuiFlyoutBody>
|
||||
<EuiFlyoutFooter>
|
||||
<EuiFlexGroup justifyContent="spaceBetween">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButtonEmpty
|
||||
iconType="cross"
|
||||
onClick={onClose}
|
||||
flush="left"
|
||||
data-test-subj="closeButton"
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.searchPlayground.addDataSource.flyout.closeButton"
|
||||
defaultMessage="Close"
|
||||
/>
|
||||
</EuiButtonEmpty>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButton
|
||||
onClick={handleSaveQuery}
|
||||
data-test-subj="saveButton"
|
||||
fill
|
||||
disabled={!selectedTempIndices.length}
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.searchPlayground.setupPage.addDataSource.flyout.saveButton"
|
||||
defaultMessage="Save and continue"
|
||||
/>
|
||||
</EuiButton>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiFlyoutFooter>
|
||||
</EuiFlyout>
|
||||
);
|
||||
};
|
|
@ -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 ? (
|
||||
<EuiCallOut
|
||||
title={i18n.translate('xpack.searchPlayground.emptyPrompts.setUpConnector.settled', {
|
||||
defaultMessage: '{connectorName} connector added',
|
||||
values: {
|
||||
connectorName: connectors![0].title,
|
||||
},
|
||||
})}
|
||||
iconType="check"
|
||||
color="success"
|
||||
data-test-subj="addedConnectorCallout"
|
||||
/>
|
||||
) : null
|
||||
) : (
|
||||
<>
|
||||
<StartChatPanel
|
||||
title={i18n.translate('xpack.searchPlayground.emptyPrompts.setUpConnector.title', {
|
||||
defaultMessage: 'Connect to LLM',
|
||||
})}
|
||||
description={
|
||||
<FormattedMessage
|
||||
id="xpack.searchPlayground.emptyPrompts.setUpConnector.description"
|
||||
defaultMessage="You need to connect to a large-language model to use this feature. Start by adding connection details for your LLM provider."
|
||||
/>
|
||||
}
|
||||
dataTestSubj="connectToLLMChatPanel"
|
||||
>
|
||||
<EuiFlexGroup>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButton
|
||||
color="primary"
|
||||
data-test-subj="setupGenAIConnectorButton"
|
||||
onClick={handleSetupGenAiConnector}
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.searchPlayground.emptyPrompts.setUpConnector.btn"
|
||||
defaultMessage="Connect"
|
||||
/>
|
||||
</EuiButton>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</StartChatPanel>
|
||||
{connectorFlyoutOpen && (
|
||||
<ConnectorFlyout
|
||||
featureId={GenerativeAIForSearchPlaygroundConnectorFeatureId}
|
||||
onConnectorCreated={handleConnectorCreated}
|
||||
onClose={() => setConnectorFlyoutOpen(false)}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
|
@ -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<ChatForm>();
|
||||
const hasSelectedIndices: boolean = !!getValues(ChatFormFields.indices)?.length;
|
||||
const handleFlyoutClose = () => {
|
||||
setShowFlyout(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{showFlyout && <SelectIndicesFlyout onClose={handleFlyoutClose} />}
|
||||
{hasSelectedIndices ? (
|
||||
<EuiButtonEmpty
|
||||
iconType="check"
|
||||
color="success"
|
||||
onClick={() => setShowFlyout(true)}
|
||||
data-test-subj="dataSourcesSuccessButton"
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.searchPlayground.setupPage.addedDataSourcesLabel"
|
||||
defaultMessage="Data sources added"
|
||||
/>
|
||||
</EuiButtonEmpty>
|
||||
) : (
|
||||
<EuiButton
|
||||
fill
|
||||
iconType="plusInCircle"
|
||||
onClick={() => setShowFlyout(true)}
|
||||
data-test-subj="addDataSourcesButton"
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.searchPlayground.setupPage.addDataSourcesLabel"
|
||||
defaultMessage="Add data sources"
|
||||
/>
|
||||
</EuiButton>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
|
@ -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(<IntlProvider locale="en">{children}</IntlProvider>);
|
||||
|
||||
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(<SetUpConnectorPanelForStartChat />);
|
||||
expect(getByTestId('setupGenAIConnectorButton')).toBeInTheDocument();
|
||||
const { getByTestId } = render(<ConnectLLMButton />);
|
||||
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(<SetUpConnectorPanelForStartChat />);
|
||||
const { getByTestId, queryByTestId } = render(<ConnectLLMButton />);
|
||||
|
||||
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(<ConnectLLMButton />);
|
||||
|
||||
expect(queryByTestId('successConnectLLMButton')).toBeInTheDocument();
|
||||
});
|
||||
});
|
|
@ -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 ? (
|
||||
<EuiButtonEmpty
|
||||
iconType="check"
|
||||
color="success"
|
||||
onClick={handleSetupGenAiConnector}
|
||||
data-test-subj="successConnectLLMButton"
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.searchPlayground.setupPage.llmConnectedButtonLabel"
|
||||
defaultMessage="LLM connected"
|
||||
/>
|
||||
</EuiButtonEmpty>
|
||||
) : (
|
||||
<EuiButton
|
||||
fill
|
||||
iconType="link"
|
||||
data-test-subj="connectLLMButton"
|
||||
onClick={handleSetupGenAiConnector}
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.searchPlayground.setupPage.connectLLMButtonLabel"
|
||||
defaultMessage="Connect to an LLM"
|
||||
/>
|
||||
</EuiButton>
|
||||
)}
|
||||
{connectorFlyoutOpen && (
|
||||
<ConnectorFlyout
|
||||
featureId={GenerativeAIForSearchPlaygroundConnectorFeatureId}
|
||||
onConnectorCreated={handleConnectorCreated}
|
||||
onClose={() => setConnectorFlyoutOpen(false)}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
|
@ -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<PropsWithChildren<unknown>> = ({ children }) => {
|
|||
);
|
||||
};
|
||||
|
||||
describe('CreateIndexCallout', () => {
|
||||
describe('CreateIndexButton', () => {
|
||||
it('renders correctly when there is no locator', async () => {
|
||||
const { queryByTestId } = render(<CreateIndexCallout />, { wrapper: Wrapper });
|
||||
const { queryByTestId } = render(<CreateIndexButton />, { wrapper: Wrapper });
|
||||
|
||||
expect(queryByTestId('createIndexButton')).not.toBeInTheDocument();
|
||||
});
|
||||
|
@ -64,7 +64,7 @@ describe('CreateIndexCallout', () => {
|
|||
},
|
||||
}));
|
||||
|
||||
const { getByTestId } = render(<CreateIndexCallout />, { wrapper: Wrapper });
|
||||
const { getByTestId } = render(<CreateIndexButton />, { wrapper: Wrapper });
|
||||
|
||||
const createIndexButton = getByTestId('createIndexButton');
|
||||
expect(createIndexButton).toBeInTheDocument();
|
|
@ -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 ? (
|
||||
<EuiButton
|
||||
color="primary"
|
||||
iconType="plusInCircle"
|
||||
fill
|
||||
onClick={handleNavigateToIndex}
|
||||
data-test-subj="createIndexButton"
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.searchPlayground.createIndexButton"
|
||||
defaultMessage="Create an index"
|
||||
/>
|
||||
</EuiButton>
|
||||
) : (
|
||||
<EuiCallOut
|
||||
title={i18n.translate('xpack.searchPlayground.createIndexCallout', {
|
||||
defaultMessage: 'You need to create an index first',
|
||||
})}
|
||||
size="s"
|
||||
color="warning"
|
||||
data-test-subj="createIndexCallout"
|
||||
/>
|
||||
);
|
||||
};
|
|
@ -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 (
|
||||
<EuiEmptyPrompt
|
||||
iconType="discuss"
|
||||
data-test-subj="setupPage"
|
||||
title={
|
||||
<h2>
|
||||
<FormattedMessage
|
||||
id="xpack.searchPlayground.setupPage.title"
|
||||
defaultMessage="Setup a chat experience"
|
||||
/>
|
||||
</h2>
|
||||
}
|
||||
body={
|
||||
<>
|
||||
<p>
|
||||
<FormattedMessage
|
||||
id="xpack.searchPlayground.setupPage.description"
|
||||
defaultMessage="Experiment with combining your Elasticsearch data with powerful large language models (LLMs) using Playground for retrieval augmented generation (RAG)."
|
||||
/>
|
||||
</p>
|
||||
<p>
|
||||
<FormattedMessage
|
||||
id="xpack.searchPlayground.setupPage.descriptionLLM"
|
||||
defaultMessage="Connect to your LLM provider and select your data sources to get started."
|
||||
/>
|
||||
</p>
|
||||
</>
|
||||
}
|
||||
actions={
|
||||
<EuiFlexGroup justifyContent="center">
|
||||
{isIndicesLoading ? (
|
||||
<EuiLoadingSpinner />
|
||||
) : (
|
||||
<>
|
||||
<EuiFlexItem grow={false}>
|
||||
<ConnectLLMButton />
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
{indices.length ? <AddDataSources /> : <CreateIndexButton />}
|
||||
</EuiFlexItem>
|
||||
</>
|
||||
)}
|
||||
</EuiFlexGroup>
|
||||
}
|
||||
footer={
|
||||
<>
|
||||
<EuiTitle size="xxs">
|
||||
<span>
|
||||
<FormattedMessage
|
||||
id="xpack.searchPlayground.setupPage.learnMore"
|
||||
defaultMessage="Want to learn more?"
|
||||
/>
|
||||
</span>
|
||||
</EuiTitle>{' '}
|
||||
<EuiLink href={docLinks.chatPlayground} target="_blank" external>
|
||||
<FormattedMessage
|
||||
id="xpack.searchPlayground.setupPage.documentationLink"
|
||||
defaultMessage="Read documentation"
|
||||
/>
|
||||
</EuiLink>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
);
|
||||
};
|
|
@ -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<AddIndicesFieldProps> = ({
|
||||
selectedIndices,
|
||||
onIndexSelect,
|
||||
loading,
|
||||
}) => {
|
||||
const [query, setQuery] = useState<string>('');
|
||||
const { indices, isLoading } = useQueryIndices(query);
|
||||
const handleChange = (value: Array<EuiComboBoxOptionOption<IndexName>>) => {
|
||||
if (value?.[0]?.label) {
|
||||
onIndexSelect(value[0].label);
|
||||
}
|
||||
};
|
||||
const handleSearchChange = (searchValue: string) => {
|
||||
setQuery(searchValue);
|
||||
};
|
||||
|
||||
return (
|
||||
<EuiFormRow
|
||||
fullWidth
|
||||
label={i18n.translate('xpack.searchPlayground.sources.addIndex.label', {
|
||||
defaultMessage: 'Add index',
|
||||
})}
|
||||
labelType="legend"
|
||||
>
|
||||
<EuiComboBox
|
||||
singleSelection={{ asPlainText: true }}
|
||||
placeholder={i18n.translate('xpack.searchPlayground.sources.addIndex.placeholder', {
|
||||
defaultMessage: 'Search for index',
|
||||
})}
|
||||
async
|
||||
isLoading={isLoading}
|
||||
onChange={handleChange}
|
||||
onSearchChange={handleSearchChange}
|
||||
fullWidth
|
||||
isDisabled={loading}
|
||||
options={indices.map((index) => ({
|
||||
label: index,
|
||||
disabled: selectedIndices.includes(index),
|
||||
}))}
|
||||
isClearable={false}
|
||||
data-test-subj="selectIndicesComboBox"
|
||||
/>
|
||||
</EuiFormRow>
|
||||
);
|
||||
};
|
|
@ -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 (
|
||||
<EuiCallOut
|
||||
title={i18n.translate('xpack.searchPlayground.sources.createIndexCallout.headerText', {
|
||||
defaultMessage: 'Create an index',
|
||||
})}
|
||||
color="primary"
|
||||
iconType="iInCircle"
|
||||
data-test-subj="createIndexCallout"
|
||||
>
|
||||
<EuiText size="s">
|
||||
<p>
|
||||
<FormattedMessage
|
||||
id="xpack.searchPlayground.sources.createIndexCallout.description"
|
||||
defaultMessage="You need at least one index with data to search. "
|
||||
/>
|
||||
</p>
|
||||
</EuiText>
|
||||
<EuiSpacer size="l" />
|
||||
{createIndexLocator && (
|
||||
<EuiButton
|
||||
color="primary"
|
||||
iconType="plusInCircle"
|
||||
fill
|
||||
size="s"
|
||||
onClick={handleNavigateToIndex}
|
||||
data-test-subj="createIndexButton"
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.searchPlayground.sources.createIndexCallout."
|
||||
defaultMessage="Create an index"
|
||||
/>
|
||||
</EuiButton>
|
||||
)}
|
||||
</EuiCallOut>
|
||||
);
|
||||
};
|
|
@ -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<PropsWithChildren<unknown>> = ({ children }) => {
|
||||
return (
|
||||
<>
|
||||
<IntlProvider locale="en">{children}</IntlProvider>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
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(<SourcesPanelSidebar />, { 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(<SourcesPanelSidebar />, { 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(<SourcesPanelSidebar />, { wrapper: Wrapper });
|
||||
expect(screen.getByTestId('removeIndexButton')).toBeDisabled();
|
||||
});
|
||||
});
|
|
@ -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<PropsWithChildren<unknown>> = ({ children }) => {
|
||||
return (
|
||||
<>
|
||||
<IntlProvider locale="en">{children}</IntlProvider>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
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(<SourcesPanelForStartChat />, { 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(<SourcesPanelForStartChat />, { 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(<SourcesPanelForStartChat />, { 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(<SourcesPanelForStartChat />, { 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(<SourcesPanelForStartChat />, { wrapper: Wrapper });
|
||||
expect(screen.getByTestId('NoIndicesFieldsMessage')).toBeInTheDocument();
|
||||
expect(screen.getByTestId('NoIndicesFieldsMessage')).toHaveTextContent('index1');
|
||||
});
|
||||
});
|
|
@ -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 (
|
||||
<StartChatPanel
|
||||
title={i18n.translate('xpack.searchPlayground.emptyPrompts.sources.title', {
|
||||
defaultMessage: 'Select indices',
|
||||
})}
|
||||
description={i18n.translate('xpack.searchPlayground.emptyPrompts.sources.description', {
|
||||
defaultMessage:
|
||||
"Select the Elasticsearch indices you'd like to query, providing additional context for the LLM.",
|
||||
})}
|
||||
isValid={!!selectedIndices?.length}
|
||||
dataTestSubj="selectIndicesChatPanel"
|
||||
>
|
||||
{!!selectedIndices?.length && (
|
||||
<EuiFlexItem>
|
||||
<IndicesTable indices={selectedIndices} onRemoveClick={removeIndex} />
|
||||
</EuiFlexItem>
|
||||
)}
|
||||
|
||||
{noFieldsIndicesWarning && (
|
||||
<EuiCallOut color="warning" iconType="warning" data-test-subj="NoIndicesFieldsMessage">
|
||||
<p>
|
||||
{i18n.translate('xpack.searchPlayground.emptyPrompts.sources.warningCallout', {
|
||||
defaultMessage:
|
||||
'No fields found for {errorMessage}. Try adding data to these indices.',
|
||||
values: {
|
||||
errorMessage: noFieldsIndicesWarning,
|
||||
},
|
||||
})}
|
||||
</p>
|
||||
</EuiCallOut>
|
||||
)}
|
||||
|
||||
{isLoading && (
|
||||
<EuiFlexGroup justifyContent="center" alignItems="center">
|
||||
<EuiLoadingSpinner size="l" data-test-subj="indicesLoading" />
|
||||
</EuiFlexGroup>
|
||||
)}
|
||||
|
||||
{!isLoading && !!indices?.length && (
|
||||
<EuiFlexItem>
|
||||
<AddIndicesField
|
||||
selectedIndices={selectedIndices}
|
||||
onIndexSelect={addIndex}
|
||||
loading={fieldIndicesLoading}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
)}
|
||||
|
||||
{!isLoading && !indices?.length && <CreateIndexCallout />}
|
||||
</StartChatPanel>
|
||||
);
|
||||
};
|
|
@ -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 (
|
||||
<EuiFlexGroup direction="column">
|
||||
<EuiFlexItem>
|
||||
<EuiCallOut
|
||||
title={i18n.translate('xpack.searchPlayground.sources.callout', {
|
||||
defaultMessage: 'Changes update the query used to search your data',
|
||||
})}
|
||||
iconType="warning"
|
||||
size="s"
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
|
||||
<EuiFlexItem>
|
||||
<IndicesList indices={selectedIndices} onRemoveClick={removeIndex} hasBorder />
|
||||
</EuiFlexItem>
|
||||
|
||||
<EuiFlexItem>
|
||||
<AddIndicesField
|
||||
selectedIndices={selectedIndices}
|
||||
onIndexSelect={addIndex}
|
||||
loading={loading}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
};
|
|
@ -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<PropsWithChildren<StartChatPanelProps>> = ({
|
||||
title,
|
||||
description,
|
||||
children,
|
||||
isValid,
|
||||
dataTestSubj,
|
||||
}) => (
|
||||
<EuiPanel hasBorder paddingSize="l" data-test-subj={dataTestSubj}>
|
||||
<EuiFlexGroup alignItems="center" justifyContent="spaceBetween">
|
||||
<EuiTitle size="xs">
|
||||
<h5>{title}</h5>
|
||||
</EuiTitle>
|
||||
|
||||
{isValid && (
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiFlexGroup alignItems="center" gutterSize="s">
|
||||
<EuiIcon type="check" color="success" />
|
||||
|
||||
<EuiText size="xs" color="success">
|
||||
<p>
|
||||
<FormattedMessage
|
||||
id="xpack.searchPlayground.startChatPanel.verified"
|
||||
defaultMessage="Completed"
|
||||
/>
|
||||
</p>
|
||||
</EuiText>
|
||||
</EuiFlexGroup>
|
||||
</EuiFlexItem>
|
||||
)}
|
||||
</EuiFlexGroup>
|
||||
|
||||
<EuiSpacer size="s" />
|
||||
|
||||
<EuiFlexGroup direction="column" gutterSize="l">
|
||||
<EuiText size="s">
|
||||
<p>{description}</p>
|
||||
</EuiText>
|
||||
|
||||
{children}
|
||||
</EuiFlexGroup>
|
||||
</EuiPanel>
|
||||
);
|
|
@ -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 (
|
||||
<IntlProvider locale="en">
|
||||
<PlaygroundProvider>{children}</PlaygroundProvider>
|
||||
</IntlProvider>
|
||||
);
|
||||
};
|
||||
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: () => (
|
||||
<div data-test-subj="addConnectorFlyout">Add Connector Flyout</div>
|
||||
),
|
||||
},
|
||||
},
|
||||
});
|
||||
(useLoadConnectors as jest.Mock).mockReturnValue({
|
||||
data: mockConnectors,
|
||||
refetch: jest.fn(),
|
||||
isLoading: false,
|
||||
isSuccess: true,
|
||||
});
|
||||
});
|
||||
|
||||
it('renders correctly', () => {
|
||||
renderWithForm(
|
||||
<MemoryRouter>
|
||||
<StartNewChat onStartClick={jest.fn()} />
|
||||
</MemoryRouter>
|
||||
);
|
||||
|
||||
expect(screen.getByTestId('startNewChatTitle')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('disables the start button when form conditions are not met', () => {
|
||||
renderWithForm(
|
||||
<MemoryRouter>
|
||||
<StartNewChat onStartClick={jest.fn()} />
|
||||
</MemoryRouter>
|
||||
);
|
||||
|
||||
const startButton = screen.getByTestId('startChatButton');
|
||||
expect(startButton).toBeDisabled();
|
||||
});
|
||||
|
||||
it('tracks the page load event', () => {
|
||||
const usageTracker = { load: jest.fn() };
|
||||
mockUseUsageTracker.mockReturnValue(usageTracker);
|
||||
|
||||
renderWithForm(
|
||||
<MemoryRouter>
|
||||
<StartNewChat onStartClick={jest.fn()} />
|
||||
</MemoryRouter>
|
||||
);
|
||||
|
||||
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(
|
||||
<MemoryRouter>
|
||||
<StartNewChat onStartClick={jest.fn()} />
|
||||
</MemoryRouter>
|
||||
);
|
||||
|
||||
expect(addIndex).toHaveBeenCalledWith('test-index');
|
||||
});
|
||||
});
|
|
@ -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<StartNewChatProps> = ({ 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 (
|
||||
<EuiFlexGroup justifyContent="center" className="eui-yScroll" data-test-subj="startChatPage">
|
||||
<EuiFlexGroup
|
||||
css={{
|
||||
height: 'fit-content',
|
||||
padding: `${euiTheme.size.xxl} ${euiTheme.size.l}`,
|
||||
maxWidth: maxWidthPage,
|
||||
boxSizing: 'content-box',
|
||||
}}
|
||||
direction="column"
|
||||
gutterSize="xl"
|
||||
>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiFlexGroup alignItems="center" justifyContent="center" gutterSize="m">
|
||||
<EuiTitle>
|
||||
<h2 data-test-subj="startNewChatTitle">
|
||||
<FormattedMessage
|
||||
id="xpack.searchPlayground.startNewChat.title"
|
||||
defaultMessage="Start a new chat"
|
||||
/>
|
||||
</h2>
|
||||
</EuiTitle>
|
||||
|
||||
<EuiIcon type="discuss" size="xl" />
|
||||
</EuiFlexGroup>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiText>
|
||||
<p>
|
||||
<FormattedMessage
|
||||
id="xpack.searchPlayground.startNewChat.description"
|
||||
defaultMessage="Combine your Elasticsearch data with the power of large language models for retrieval augmented generation (RAG). Use the UI to view and edit the Elasticsearch queries used to search your data, then download the code to integrate into your own application."
|
||||
/>
|
||||
</p>
|
||||
</EuiText>
|
||||
</EuiFlexItem>
|
||||
|
||||
<EuiFlexItem grow={false}>
|
||||
<SetUpConnectorPanelForStartChat />
|
||||
</EuiFlexItem>
|
||||
|
||||
<EuiFlexItem grow={false}>
|
||||
<SourcesPanelForStartChat />
|
||||
</EuiFlexItem>
|
||||
|
||||
<EuiFlexGroup justifyContent="flexEnd">
|
||||
<EuiButton
|
||||
fill
|
||||
iconType="arrowRight"
|
||||
iconSide="right"
|
||||
data-test-subj="startChatButton"
|
||||
disabled={
|
||||
!watch(ChatFormFields.indices, [])?.length ||
|
||||
!Object.keys(connectors || {}).length ||
|
||||
!watch(ChatFormFields.elasticsearchQuery, '')
|
||||
}
|
||||
onClick={onStartClick}
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.searchPlayground.startNewChat.startBtn"
|
||||
defaultMessage="Start"
|
||||
/>
|
||||
</EuiButton>
|
||||
</EuiFlexGroup>
|
||||
</EuiFlexGroup>
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
};
|
|
@ -27,7 +27,7 @@ export const IncludeCitationsField: React.FC<IncludeCitationsFieldProps> = ({
|
|||
};
|
||||
|
||||
return (
|
||||
<EuiFormRow>
|
||||
<EuiFormRow fullWidth>
|
||||
<EuiSwitch
|
||||
label={i18n.translate('xpack.searchPlayground.sidebar.citationsField.label', {
|
||||
defaultMessage: 'Include citations',
|
||||
|
|
|
@ -53,6 +53,7 @@ export const InstructionsField: React.FC<InstructionsFieldProps> = ({ value, onC
|
|||
</>
|
||||
</EuiToolTip>
|
||||
}
|
||||
fullWidth
|
||||
>
|
||||
<EuiTextArea
|
||||
placeholder={i18n.translate(
|
||||
|
|
|
@ -60,9 +60,5 @@ describe('SummarizationModel', () => {
|
|||
);
|
||||
|
||||
expect(getByTestId('summarizationModelSelect')).toBeInTheDocument();
|
||||
expect(getByTestId('manageConnectorsLink')).toHaveAttribute(
|
||||
'href',
|
||||
'http://example.com/manage-connectors'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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<SummarizationModelProps> = ({
|
|||
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<SummarizationModelProps> = ({
|
|||
|
||||
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<SummarizationModelProps> = ({
|
|||
/>{' '}
|
||||
</>
|
||||
}
|
||||
labelAppend={
|
||||
<EuiToolTip
|
||||
delay="long"
|
||||
content={i18n.translate(
|
||||
'xpack.searchPlayground.sidebar.summarizationModel.manageConnectorTooltip',
|
||||
{
|
||||
defaultMessage: 'Manage',
|
||||
}
|
||||
)}
|
||||
>
|
||||
<EuiButtonIcon
|
||||
target="_blank"
|
||||
href={managementLink}
|
||||
data-test-subj="manageConnectorsLink"
|
||||
iconType="wrench"
|
||||
aria-label={i18n.translate(
|
||||
'xpack.searchPlayground.sidebar.summarizationModel.manageConnectorLink',
|
||||
{
|
||||
defaultMessage: 'Manage connector',
|
||||
}
|
||||
)}
|
||||
/>
|
||||
</EuiToolTip>
|
||||
}
|
||||
fullWidth
|
||||
>
|
||||
<EuiSuperSelect
|
||||
data-test-subj="summarizationModelSelect"
|
||||
options={modelsOption}
|
||||
valueOfSelected={getOptionValue(selectedModel)}
|
||||
valueOfSelected={selectedModel && getOptionValue(selectedModel)}
|
||||
onChange={onChange}
|
||||
fullWidth
|
||||
/>
|
||||
</EuiFormRow>
|
||||
);
|
||||
|
|
|
@ -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<ChatForm>();
|
||||
const models = useLLMsModels();
|
||||
const defaultModel = models.find((model) => !model.disabled);
|
||||
|
||||
return (
|
||||
<>
|
||||
<EuiPanel data-test-subj="summarizationPanel">
|
||||
<Controller
|
||||
name={ChatFormFields.summarizationModel}
|
||||
defaultValue={defaultModel}
|
||||
rules={{ required: true }}
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
|
@ -51,6 +50,6 @@ export const SummarizationPanel: React.FC = () => {
|
|||
<IncludeCitationsField checked={field.value} onChange={field.onChange} />
|
||||
)}
|
||||
/>
|
||||
</>
|
||||
</EuiPanel>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -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 (
|
||||
<EuiFlexGroup gutterSize="s" data-test-subj="playground-header-actions">
|
||||
<EditContextAction />
|
||||
<ViewQueryAction />
|
||||
<EuiFlexGroup gutterSize="s" alignItems="center" data-test-subj="playground-header-actions">
|
||||
<DataActionButton />
|
||||
<ViewCodeAction />
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
|
|
|
@ -27,6 +27,7 @@ export const ViewCodeAction: React.FC = () => {
|
|||
onClick={() => setShowFlyout(true)}
|
||||
disabled={!selectedIndices || selectedIndices?.length === 0}
|
||||
data-test-subj="viewCodeActionButton"
|
||||
size="s"
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.searchPlayground.viewCode.actionButtonLabel"
|
||||
|
|
|
@ -1,35 +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 { ViewQueryFlyout } from './view_query_flyout';
|
||||
import { ChatForm, ChatFormFields } from '../../types';
|
||||
|
||||
export const ViewQueryAction: React.FC = () => {
|
||||
const [showFlyout, setShowFlyout] = useState(false);
|
||||
const { watch } = useFormContext<ChatForm>();
|
||||
const selectedIndices: string[] = watch(ChatFormFields.indices);
|
||||
|
||||
return (
|
||||
<>
|
||||
{showFlyout && <ViewQueryFlyout onClose={() => setShowFlyout(false)} />}
|
||||
<EuiButtonEmpty
|
||||
onClick={() => setShowFlyout(true)}
|
||||
disabled={selectedIndices?.length === 0}
|
||||
data-test-subj="viewQueryActionButton"
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.searchPlayground.viewQuery.actionButtonLabel"
|
||||
defaultMessage="View query"
|
||||
/>
|
||||
</EuiButtonEmpty>
|
||||
</>
|
||||
);
|
||||
};
|
|
@ -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<ViewQueryFlyoutProps> = ({ onClose }) => {
|
||||
const usageTracker = useUsageTracker();
|
||||
const { getValues } = useFormContext<ChatForm>();
|
||||
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<IndexFields>(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 (
|
||||
<EuiFlyout ownFocus onClose={onClose} size="l" data-test-subj="viewQueryFlyout">
|
||||
<EuiFlyoutHeader hasBorder>
|
||||
<EuiTitle size="m">
|
||||
<h2>
|
||||
<FormattedMessage
|
||||
id="xpack.searchPlayground.viewQuery.flyout.title"
|
||||
defaultMessage="Customize your Elasticsearch query"
|
||||
/>
|
||||
</h2>
|
||||
</EuiTitle>
|
||||
<EuiSpacer size="s" />
|
||||
<EuiText color="subdued">
|
||||
<p>
|
||||
<FormattedMessage
|
||||
id="xpack.searchPlayground.viewQuery.flyout.description"
|
||||
defaultMessage="This query will be used to search your indices. Customize by choosing which
|
||||
fields in your Elasticsearch documents to search."
|
||||
/>
|
||||
{` `}
|
||||
<EuiLink
|
||||
href={docLinks.retrievalOptimize}
|
||||
target="_blank"
|
||||
data-test-subj="query-optimize-documentation-link"
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.searchPlayground.viewQuery.flyout.learnMoreQueryOptimizeLink"
|
||||
defaultMessage="Learn more."
|
||||
/>
|
||||
</EuiLink>
|
||||
</p>
|
||||
</EuiText>
|
||||
</EuiFlyoutHeader>
|
||||
<EuiFlyoutBody>
|
||||
<EuiFlexGroup>
|
||||
<EuiFlexItem grow={6}>
|
||||
<EuiCodeBlock
|
||||
language="json"
|
||||
fontSize="m"
|
||||
paddingSize="m"
|
||||
lineNumbers
|
||||
data-test-subj="ViewElasticsearchQueryResult"
|
||||
>
|
||||
{JSON.stringify(createQuery(tempQueryFields, sourceFields, fields), null, 2)}
|
||||
</EuiCodeBlock>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={3}>
|
||||
<EuiFlexGroup direction="column" gutterSize="s">
|
||||
<EuiText>
|
||||
<h5>
|
||||
<FormattedMessage
|
||||
id="xpack.searchPlayground.viewQuery.flyout.table.title"
|
||||
defaultMessage="Fields to search (per index)"
|
||||
/>
|
||||
</h5>
|
||||
</EuiText>
|
||||
{Object.entries(fields).map(([index, group]) => (
|
||||
<EuiFlexItem grow={false} key={index}>
|
||||
<EuiPanel grow={false} hasShadow={false} hasBorder>
|
||||
<EuiAccordion
|
||||
id={index}
|
||||
initialIsOpen
|
||||
buttonContent={
|
||||
<EuiText>
|
||||
<h5>{index}</h5>
|
||||
</EuiText>
|
||||
}
|
||||
>
|
||||
<EuiSpacer size="s" />
|
||||
<EuiSelectable
|
||||
aria-label="Select query fields"
|
||||
data-test-subj={`queryFieldsSelectable_${index}`}
|
||||
options={[
|
||||
...group.semantic_fields,
|
||||
...group.elser_query_fields,
|
||||
...group.dense_vector_query_fields,
|
||||
...group.bm25_query_fields,
|
||||
].map((field, idx) => {
|
||||
const checked = isQueryFieldSelected(
|
||||
index,
|
||||
typeof field === 'string' ? field : field.field
|
||||
);
|
||||
return {
|
||||
label: typeof field === 'string' ? field : field.field,
|
||||
prepend: (
|
||||
<EuiCheckbox
|
||||
id={`checkbox_${idx}`}
|
||||
checked={checked}
|
||||
onChange={() => {}}
|
||||
/>
|
||||
),
|
||||
checked: checked ? 'on' : undefined,
|
||||
'data-test-subj': 'queryField',
|
||||
};
|
||||
})}
|
||||
listProps={{
|
||||
bordered: false,
|
||||
showIcons: false,
|
||||
}}
|
||||
onChange={(newOptions) => updateFields(index, newOptions)}
|
||||
>
|
||||
{(list) => list}
|
||||
</EuiSelectable>
|
||||
{group.skipped_fields > 0 && (
|
||||
<>
|
||||
<EuiSpacer size="m" />
|
||||
<EuiFlexGroup>
|
||||
<EuiFlexItem>
|
||||
<EuiText
|
||||
size="s"
|
||||
color="subdued"
|
||||
data-test-subj={`skipped_fields_${index}`}
|
||||
>
|
||||
<EuiIcon type="eyeClosed" />
|
||||
{` `}
|
||||
<FormattedMessage
|
||||
id="xpack.searchPlayground.viewQuery.flyout.hiddenFields"
|
||||
defaultMessage="{skippedFields} fields are hidden."
|
||||
values={{ skippedFields: group.skipped_fields }}
|
||||
/>
|
||||
</EuiText>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiLink
|
||||
href={docLinks.hiddenFields}
|
||||
target="_blank"
|
||||
data-test-subj="hidden-fields-documentation-link"
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.searchPlayground.viewQuery.flyout.learnMoreLink"
|
||||
defaultMessage="Learn more."
|
||||
/>
|
||||
</EuiLink>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</>
|
||||
)}
|
||||
</EuiAccordion>
|
||||
</EuiPanel>
|
||||
</EuiFlexItem>
|
||||
))}
|
||||
</EuiFlexGroup>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiFlyoutBody>
|
||||
<EuiFlyoutFooter>
|
||||
<EuiFlexGroup justifyContent="spaceBetween">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButtonEmpty iconType="cross" onClick={onClose} flush="left">
|
||||
<FormattedMessage
|
||||
id="xpack.searchPlayground.viewQuery.flyout.closeButton"
|
||||
defaultMessage="Close"
|
||||
/>
|
||||
</EuiButtonEmpty>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButton onClick={saveQuery} fill>
|
||||
<FormattedMessage
|
||||
id="xpack.searchPlayground.viewQuery.flyout.saveButton"
|
||||
defaultMessage="Save changes"
|
||||
/>
|
||||
</EuiButton>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiFlyoutFooter>
|
||||
</EuiFlyout>
|
||||
);
|
||||
};
|
|
@ -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<React.FC<AppProps>>(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<typeof PlaygroundProvider>) =>
|
||||
(
|
||||
<KibanaContextProvider services={{ ...core, ...services }}>
|
||||
<PlaygroundProvider {...props} />
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<PlaygroundProvider {...props} />
|
||||
</QueryClientProvider>
|
||||
</KibanaContextProvider>
|
||||
);
|
||||
|
|
|
@ -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<IndicesQuerySourceFields>({
|
||||
enabled: indices.length > 0,
|
||||
queryKey: ['fields', indices.toString()],
|
||||
initialData: {},
|
||||
initialData,
|
||||
queryFn: async () => {
|
||||
const response = await services.http.post<IndicesQuerySourceFields>(
|
||||
APIRoutes.POST_QUERY_SOURCE_FIELDS,
|
||||
|
@ -30,5 +32,5 @@ export const useIndicesFields = (indices: string[] = []) => {
|
|||
},
|
||||
});
|
||||
|
||||
return { fields: data!, isLoading };
|
||||
return { fields: data, isLoading: isLoading || isFetching };
|
||||
};
|
||||
|
|
|
@ -29,7 +29,8 @@ export const useQueryIndices = (
|
|||
|
||||
return response.indices;
|
||||
},
|
||||
initialData: [],
|
||||
});
|
||||
|
||||
return { indices: data || [], isLoading };
|
||||
return { indices: data, isLoading };
|
||||
};
|
||||
|
|
|
@ -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<boolean>(false);
|
||||
const [noFieldsIndicesWarning, setNoFieldsIndicesWarning] = useState<string | null>(null);
|
||||
const { resetField } = useFormContext<ChatForm>();
|
||||
|
||||
const {
|
||||
field: { value: selectedIndices, onChange: onIndicesChange },
|
||||
|
@ -55,73 +51,74 @@ export const useSourceIndicesFields = () => {
|
|||
});
|
||||
|
||||
const {
|
||||
field: { onChange: onSourceFieldsChange },
|
||||
field: { onChange: onQueryFieldsOnChange, value: queryFields },
|
||||
} = useController<ChatForm, ChatFormFields.queryFields>({
|
||||
name: ChatFormFields.queryFields,
|
||||
});
|
||||
|
||||
const {
|
||||
field: { onChange: onSourceFieldsChange, value: sourceFields },
|
||||
} = useController({
|
||||
name: ChatFormFields.sourceFields,
|
||||
});
|
||||
|
||||
const { data: fields } = useQuery({
|
||||
enabled: selectedIndices.length > 0,
|
||||
queryKey: ['fields', selectedIndices.toString()],
|
||||
queryFn: async () => {
|
||||
const response = await services.http.post<IndicesQuerySourceFields>(
|
||||
APIRoutes.POST_QUERY_SOURCE_FIELDS,
|
||||
{
|
||||
body: JSON.stringify({ indices: selectedIndices }),
|
||||
}
|
||||
);
|
||||
return response;
|
||||
},
|
||||
});
|
||||
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 addIndex = useCallback(
|
||||
(newIndex: IndexName) => {
|
||||
const newIndices = [...selectedIndices, newIndex];
|
||||
setLoading(true);
|
||||
onIndicesChange(newIndices);
|
||||
usageTracker?.count(AnalyticsEvents.sourceIndexUpdated, newIndices.length);
|
||||
},
|
||||
[onIndicesChange, selectedIndices, usageTracker]
|
||||
);
|
||||
|
||||
const removeIndex = (index: IndexName) => {
|
||||
const newIndices = selectedIndices.filter((indexName: string) => indexName !== index);
|
||||
setLoading(true);
|
||||
onIndicesChange(newIndices);
|
||||
usageTracker?.count(AnalyticsEvents.sourceIndexUpdated, newIndices.length);
|
||||
};
|
||||
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,
|
||||
};
|
||||
};
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
|
|
|
@ -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<PropsWithChildren<PlaygroundProviderProps>> = ({
|
||||
children,
|
||||
}) => {
|
||||
export const PlaygroundProvider: FC = ({ children }) => {
|
||||
const models = useLLMsModels();
|
||||
const form = useForm<ChatForm>({
|
||||
defaultValues: {
|
||||
prompt: 'You are an assistant for question-answering tasks.',
|
||||
doc_size: 3,
|
||||
source_fields: {},
|
||||
indices: [],
|
||||
summarization_model: {},
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<FormProvider {...form}>{children}</FormProvider>
|
||||
</QueryClientProvider>
|
||||
);
|
||||
useEffect(() => {
|
||||
const defaultModel = models.find((model) => !model.disabled);
|
||||
|
||||
if (defaultModel) {
|
||||
form.setValue(ChatFormFields.summarizationModel, defaultModel);
|
||||
}
|
||||
}, [form, models]);
|
||||
|
||||
return <FormProvider {...form}>{children}</FormProvider>;
|
||||
};
|
||||
|
|
|
@ -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<React.ComponentProps<typeof PlaygroundProviderComponent>>;
|
||||
PlaygroundToolbar: React.FC<React.ComponentProps<typeof Toolbar>>;
|
||||
Playground: React.FC<React.ComponentProps<typeof App>>;
|
||||
PlaygroundHeaderDocs: React.FC<React.ComponentProps<typeof PlaygroundHeaderDocs>>;
|
||||
}
|
||||
|
|
|
@ -55,6 +55,9 @@ export function createQuery(
|
|||
const indices = Object.keys(fieldDescriptors);
|
||||
const boolMatches = Object.keys(fields).reduce<Matches>(
|
||||
(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<string[]>((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<IndexFields>(
|
||||
(acc: IndexFields, index: string) => {
|
||||
|
|
|
@ -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({});
|
|
@ -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();
|
||||
|
|
|
@ -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<void>
|
||||
) {
|
||||
async expectSuccessButtonAfterCreatingConnector(createConnector: () => Promise<void>) {
|
||||
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');
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
|
@ -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();
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue