From 6928a29522b24c5335e18588ae338c55192ef1b0 Mon Sep 17 00:00:00 2001 From: Steph Milovic Date: Mon, 3 Mar 2025 19:31:54 -0700 Subject: [PATCH] [8.x] [Security Assistant] Conversation pagination refactor (#211831) (#212997) # Backport This will backport the following commits from `main` to `8.x`: - [[Security Assistant] Conversation pagination refactor (#211831)](https://github.com/elastic/kibana/pull/211831) ### Questions ? Please refer to the [Backport tool documentation](https://github.com/sorenlouv/backport) --- oas_docs/output/kibana.serverless.yaml | 9 +- oas_docs/output/kibana.yaml | 9 +- ...sistant_api_2023_10_31.bundled.schema.yaml | 9 +- ...sistant_api_2023_10_31.bundled.schema.yaml | 9 +- .../conversations/common_attributes.gen.ts | 10 +- .../common_attributes.schema.yaml | 8 +- .../find_conversations_route.gen.ts | 7 +- .../find_conversations_route.schema.yaml | 1 - .../api/conversations/conversations.ts | 19 +- ..._fetch_current_user_conversations.test.tsx | 11 +- .../use_fetch_current_user_conversations.ts | 181 ++++-- .../impl/assistant/api/index.tsx | 1 - .../impl/assistant/assistant_body/index.tsx | 2 +- .../assistant_body/welcome_setup.tsx | 2 +- .../assistant/assistant_header/index.test.tsx | 2 + .../impl/assistant/assistant_header/index.tsx | 47 +- .../assistant_header/translations.ts | 14 - .../assistant_overlay/index.test.tsx | 1 - .../assistant/assistant_overlay/index.tsx | 35 +- .../impl/assistant/assistant_title/index.tsx | 11 +- .../impl/assistant/chat_send/index.tsx | 4 - .../chat_send/use_chat_send.test.tsx | 2 - .../assistant/chat_send/use_chat_send.tsx | 61 +- .../badges/index.tsx | 4 +- .../inline_actions/index.tsx | 2 +- .../pagination/use_session_pagination.ts | 94 ++-- .../index.test.tsx | 7 - .../conversation_selector_settings/index.tsx | 73 ++- .../conversation_selector_settings/types.ts | 12 - .../conversation_settings.test.tsx | 343 ------------ .../conversation_settings.tsx | 150 ----- .../conversation_settings_editor.tsx | 239 +++----- .../conversation_settings/translations.ts | 21 - .../use_conveersation_changed.test.tsx | 126 ----- .../use_conversation_changed.tsx | 104 ---- .../use_conversation_deleted.tsx | 56 -- .../use_conversation_deletex.test.tsx | 95 ---- .../index.tsx | 170 +++--- .../use_conversations_table.tsx | 4 +- .../conversation_sidepanel/date_utils.ts | 23 + .../conversation_sidepanel/index.tsx | 136 +++-- .../conversation_sidepanel/translations.ts | 27 + .../use_conversations_by_date.ts | 67 +++ .../impl/assistant/helpers.test.ts | 108 +--- .../impl/assistant/helpers.ts | 21 +- .../impl/assistant/index.test.tsx | 213 +++---- .../impl/assistant/index.tsx | 109 ++-- .../system_prompt/index.test.tsx | 3 +- .../conversation_multi_selector.test.tsx | 1 + .../conversation_multi_selector.tsx | 39 +- .../system_prompt_editor.tsx | 374 ++----------- .../system_prompt_selector.test.tsx | 22 +- .../system_prompt_selector.tsx | 44 +- .../system_prompt_settings.test.tsx | 99 +--- .../system_prompt_settings.tsx | 36 +- .../system_prompt_modal/types.ts | 31 +- .../use_system_prompt_editor.test.tsx | 123 ---- .../use_system_prompt_editor.tsx | 95 ---- .../index.tsx | 169 +++--- .../use_system_prompt_table.test.tsx | 107 +--- .../use_system_prompt_table.tsx | 71 +-- .../utils.test.tsx | 4 +- .../quick_prompt_editor.tsx | 233 +------- .../quick_prompt_settings.test.tsx | 68 +-- .../quick_prompt_settings.tsx | 43 +- .../use_quick_prompt_editor.test.tsx | 111 ---- .../use_quick_prompt_editor.tsx | 96 ---- .../index.tsx | 97 ++-- .../alerts_settings/alerts_settings_modal.tsx | 23 +- .../settings/assistant_settings.test.tsx | 127 +++-- .../assistant/settings/assistant_settings.tsx | 265 ++++----- .../assistant_settings_management.test.tsx | 24 +- .../assistant_settings_management.tsx | 14 +- .../assistant_settings_modal.test.tsx | 2 + .../settings/assistant_settings_modal.tsx | 18 +- .../impl/assistant/settings/index.tsx | 2 - .../impl/assistant/settings/types.ts | 11 +- .../use_anonymization_updater.test.ts | 230 ++++++++ .../use_anonymization_updater.ts | 122 ++++ .../use_conversations_updater.test.ts | 203 +++++++ .../use_conversations_updater.ts | 138 +++++ .../use_knowledge_base_updater.test.ts | 133 +++++ .../use_knowledge_base_updater.ts | 52 ++ .../use_quick_prompt_updater.test.ts | 178 ++++++ .../use_quick_prompt_updater.ts | 321 +++++++++++ .../use_settings_updater.test.tsx | 306 ---------- .../use_settings_updater.tsx | 276 --------- .../use_system_prompt_updater.test.ts | 233 ++++++++ .../use_system_prompt_updater.ts | 526 ++++++++++++++++++ .../use_assistant_overlay/index.test.tsx | 72 +-- .../assistant/use_assistant_overlay/index.tsx | 90 +-- .../assistant/use_conversation/helpers.ts | 11 +- .../assistant/use_conversation/index.test.tsx | 10 +- .../impl/assistant/use_conversation/index.tsx | 55 +- .../use_conversation/translations.ts | 13 - .../use_current_conversation/index.test.tsx | 235 ++------ .../use_current_conversation/index.tsx | 282 ++++------ .../impl/assistant/use_data_stream_apis.tsx | 46 +- .../impl/assistant_context/constants.tsx | 1 + .../impl/assistant_context/index.test.tsx | 33 +- .../impl/assistant_context/index.tsx | 99 +++- .../impl/assistant_context/types.tsx | 15 +- .../connector_missing_callout/index.tsx | 87 ++- .../connector_selector_inline.tsx | 36 +- .../connectorland/connector_setup/index.tsx | 3 +- .../impl/connectorland/translations.ts | 7 - .../anonymization_settings/index.test.tsx | 142 ----- .../settings/anonymization_settings/index.tsx | 69 --- .../use_anonymization_list_update.tsx | 65 --- .../index.tsx | 93 +--- .../context_editor/index.tsx | 29 +- .../context_editor/types.ts | 10 - .../index.test.tsx | 123 ++-- .../index.tsx | 34 +- .../use_knowledge_base_table.tsx | 2 +- .../impl/mock/conversation.ts | 6 +- .../impl/mock/system_prompt/index.ts | 7 + .../mock/test_providers/test_providers.tsx | 1 - .../impl/new_chat/index.tsx | 6 +- .../impl/new_chat_by_title/index.test.tsx | 30 +- .../impl/new_chat_by_title/index.tsx | 21 +- .../shared/kbn-elastic-assistant/index.ts | 9 +- .../kbn-elastic-assistant/tsconfig.json | 1 + .../translations/translations/fr-FR.json | 11 - .../translations/translations/ja-JP.json | 11 - .../translations/translations/zh-CN.json | 11 - .../data_quality_panel/actions/chat/index.tsx | 13 +- .../impl/data_quality_panel/actions/index.tsx | 8 +- .../historical_check_fields/index.test.tsx | 2 + .../historical_check_fields/index.tsx | 9 +- .../historical_result/index.test.tsx | 13 +- .../historical_result/index.tsx | 19 +- .../index.test.tsx | 20 +- .../legacy_historical_check_fields/index.tsx | 10 +- .../historical_results_list/index.test.tsx | 10 +- .../historical_results_list/index.tsx | 9 +- .../incompatible_tab/index.tsx | 4 + .../pattern/index_check_flyout/index.tsx | 7 +- .../latest_results/index.test.tsx | 1 + .../latest_results/index.tsx | 3 + .../latest_check_fields/index.test.tsx | 1 + .../latest_check_fields/index.tsx | 4 + .../sticky_actions/index.tsx | 3 + .../utils/get_formatted_check_time.ts | 2 +- .../mock/test_providers/test_providers.tsx | 1 - .../scripts/create_conversations_script.ts | 106 ++-- .../__mocks__/conversations_schema.mock.ts | 39 +- .../server/__mocks__/response.ts | 1 - .../conversations/create_conversation.test.ts | 2 - .../conversations/create_conversation.ts | 2 - .../conversations/field_maps_configuration.ts | 5 - .../conversations/get_conversation.test.ts | 2 - .../conversations/index.test.ts | 2 - .../conversations/transforms.ts | 2 - .../conversations/types.ts | 2 - .../conversations/update_conversation.test.ts | 1 - .../conversations/update_conversation.ts | 74 +-- .../ai_assistant_data_clients/find.test.ts | 2 - .../server/ai_assistant_data_clients/find.ts | 24 +- .../ai_assistant_data_clients/index.test.ts | 2 - .../persistence/transforms/transforms.ts | 1 + .../routes/prompts/bulk_actions_route.ts | 4 +- .../user_conversations/find_route.test.ts | 2 +- .../routes/user_conversations/find_route.ts | 14 +- .../user_conversations/update_route.test.ts | 11 - .../assistant/content/conversations/index.tsx | 78 --- .../content/conversations/translations.ts | 15 - .../public/assistant/provider.test.tsx | 6 - .../public/assistant/provider.tsx | 9 +- .../management_settings.test.tsx | 21 +- .../stack_management/management_settings.tsx | 60 +- .../use_assistant_telemetry/index.test.tsx | 42 +- .../use_assistant_telemetry/index.tsx | 21 +- .../use_conversation_store/index.test.tsx | 74 --- .../use_conversation_store/index.tsx | 28 - .../use_view_in_ai_assistant.ts | 2 +- .../telemetry/events/ai_assistant/index.ts | 21 - .../telemetry/events/ai_assistant/types.ts | 3 - .../common/mock/mock_assistant_provider.tsx | 2 - .../components/ai_assistant/index.test.tsx | 13 +- .../components/ai_assistant/index.tsx | 9 +- .../pages/rule_details/index.tsx | 1 + .../rules_table/rules_table_toolbar.tsx | 8 +- .../rule_status_failed_callout.test.tsx | 16 +- .../rule_status_failed_callout.tsx | 12 +- .../right/components/header_actions.test.tsx | 12 +- .../right/components/header_actions.tsx | 14 +- .../right/hooks/use_assistant.ts | 28 +- .../cards/assistant/assistant_card.tsx | 9 +- .../e2e/ai_assistant/conversations.cy.ts | 70 ++- .../cypress/e2e/ai_assistant/messages.cy.ts | 94 ++-- .../cypress/e2e/ai_assistant/prompts.cy.ts | 37 +- .../cypress/tasks/assistant.ts | 31 +- 193 files changed, 4685 insertions(+), 5993 deletions(-) delete mode 100644 x-pack/platform/packages/shared/kbn-elastic-assistant/impl/assistant/conversations/conversation_selector_settings/types.ts delete mode 100644 x-pack/platform/packages/shared/kbn-elastic-assistant/impl/assistant/conversations/conversation_settings/conversation_settings.test.tsx delete mode 100644 x-pack/platform/packages/shared/kbn-elastic-assistant/impl/assistant/conversations/conversation_settings/conversation_settings.tsx delete mode 100644 x-pack/platform/packages/shared/kbn-elastic-assistant/impl/assistant/conversations/conversation_settings/use_conveersation_changed.test.tsx delete mode 100644 x-pack/platform/packages/shared/kbn-elastic-assistant/impl/assistant/conversations/conversation_settings/use_conversation_changed.tsx delete mode 100644 x-pack/platform/packages/shared/kbn-elastic-assistant/impl/assistant/conversations/conversation_settings/use_conversation_deleted.tsx delete mode 100644 x-pack/platform/packages/shared/kbn-elastic-assistant/impl/assistant/conversations/conversation_settings/use_conversation_deletex.test.tsx create mode 100644 x-pack/platform/packages/shared/kbn-elastic-assistant/impl/assistant/conversations/conversation_sidepanel/date_utils.ts create mode 100644 x-pack/platform/packages/shared/kbn-elastic-assistant/impl/assistant/conversations/conversation_sidepanel/use_conversations_by_date.ts delete mode 100644 x-pack/platform/packages/shared/kbn-elastic-assistant/impl/assistant/prompt_editor/system_prompt/system_prompt_modal/use_system_prompt_editor.test.tsx delete mode 100644 x-pack/platform/packages/shared/kbn-elastic-assistant/impl/assistant/prompt_editor/system_prompt/system_prompt_modal/use_system_prompt_editor.tsx delete mode 100644 x-pack/platform/packages/shared/kbn-elastic-assistant/impl/assistant/quick_prompts/quick_prompt_settings/use_quick_prompt_editor.test.tsx delete mode 100644 x-pack/platform/packages/shared/kbn-elastic-assistant/impl/assistant/quick_prompts/quick_prompt_settings/use_quick_prompt_editor.tsx create mode 100644 x-pack/platform/packages/shared/kbn-elastic-assistant/impl/assistant/settings/use_settings_updater/use_anonymization_updater.test.ts create mode 100644 x-pack/platform/packages/shared/kbn-elastic-assistant/impl/assistant/settings/use_settings_updater/use_anonymization_updater.ts create mode 100644 x-pack/platform/packages/shared/kbn-elastic-assistant/impl/assistant/settings/use_settings_updater/use_conversations_updater.test.ts create mode 100644 x-pack/platform/packages/shared/kbn-elastic-assistant/impl/assistant/settings/use_settings_updater/use_conversations_updater.ts create mode 100644 x-pack/platform/packages/shared/kbn-elastic-assistant/impl/assistant/settings/use_settings_updater/use_knowledge_base_updater.test.ts create mode 100644 x-pack/platform/packages/shared/kbn-elastic-assistant/impl/assistant/settings/use_settings_updater/use_knowledge_base_updater.ts create mode 100644 x-pack/platform/packages/shared/kbn-elastic-assistant/impl/assistant/settings/use_settings_updater/use_quick_prompt_updater.test.ts create mode 100644 x-pack/platform/packages/shared/kbn-elastic-assistant/impl/assistant/settings/use_settings_updater/use_quick_prompt_updater.ts delete mode 100644 x-pack/platform/packages/shared/kbn-elastic-assistant/impl/assistant/settings/use_settings_updater/use_settings_updater.test.tsx delete mode 100644 x-pack/platform/packages/shared/kbn-elastic-assistant/impl/assistant/settings/use_settings_updater/use_settings_updater.tsx create mode 100644 x-pack/platform/packages/shared/kbn-elastic-assistant/impl/assistant/settings/use_settings_updater/use_system_prompt_updater.test.ts create mode 100644 x-pack/platform/packages/shared/kbn-elastic-assistant/impl/assistant/settings/use_settings_updater/use_system_prompt_updater.ts delete mode 100644 x-pack/platform/packages/shared/kbn-elastic-assistant/impl/data_anonymization/settings/anonymization_settings/index.test.tsx delete mode 100644 x-pack/platform/packages/shared/kbn-elastic-assistant/impl/data_anonymization/settings/anonymization_settings/index.tsx delete mode 100644 x-pack/platform/packages/shared/kbn-elastic-assistant/impl/data_anonymization/settings/anonymization_settings/use_anonymization_list_update.tsx delete mode 100644 x-pack/solutions/security/plugins/security_solution/public/assistant/content/conversations/index.tsx delete mode 100644 x-pack/solutions/security/plugins/security_solution/public/assistant/content/conversations/translations.ts delete mode 100644 x-pack/solutions/security/plugins/security_solution/public/assistant/use_conversation_store/index.test.tsx delete mode 100644 x-pack/solutions/security/plugins/security_solution/public/assistant/use_conversation_store/index.tsx diff --git a/oas_docs/output/kibana.serverless.yaml b/oas_docs/output/kibana.serverless.yaml index 81668b68e07db..a014a66f798d4 100644 --- a/oas_docs/output/kibana.serverless.yaml +++ b/oas_docs/output/kibana.serverless.yaml @@ -40577,9 +40577,6 @@ components: id: description: The conversation id. type: string - isDefault: - description: Is default conversation. - type: boolean messages: description: The conversation messages. items: @@ -40602,16 +40599,13 @@ components: $ref: '#/components/schemas/Security_AI_Assistant_API_ConversationCategory' description: The conversation category. createdAt: - description: The last time conversation was updated. + description: The time conversation was created. type: string excludeFromLastConversationStorage: description: excludeFromLastConversationStorage. type: boolean id: $ref: '#/components/schemas/Security_AI_Assistant_API_NonEmptyString' - isDefault: - description: Is default conversation. - type: boolean messages: description: The conversation messages. items: @@ -40838,7 +40832,6 @@ components: Security_AI_Assistant_API_FindConversationsSortField: enum: - created_at - - is_default - title - updated_at type: string diff --git a/oas_docs/output/kibana.yaml b/oas_docs/output/kibana.yaml index b9b03fbdf359d..12fd2d78c0b1b 100644 --- a/oas_docs/output/kibana.yaml +++ b/oas_docs/output/kibana.yaml @@ -28763,9 +28763,6 @@ components: id: description: The conversation id. type: string - isDefault: - description: Is default conversation. - type: boolean messages: description: The conversation messages. items: @@ -28788,16 +28785,13 @@ components: $ref: '#/components/schemas/Security_AI_Assistant_API_ConversationCategory' description: The conversation category. createdAt: - description: The last time conversation was updated. + description: The time conversation was created. type: string excludeFromLastConversationStorage: description: excludeFromLastConversationStorage. type: boolean id: $ref: '#/components/schemas/Security_AI_Assistant_API_NonEmptyString' - isDefault: - description: Is default conversation. - type: boolean messages: description: The conversation messages. items: @@ -29024,7 +29018,6 @@ components: Security_AI_Assistant_API_FindConversationsSortField: enum: - created_at - - is_default - title - updated_at type: string diff --git a/x-pack/platform/packages/shared/kbn-elastic-assistant-common/docs/openapi/ess/elastic_assistant_api_2023_10_31.bundled.schema.yaml b/x-pack/platform/packages/shared/kbn-elastic-assistant-common/docs/openapi/ess/elastic_assistant_api_2023_10_31.bundled.schema.yaml index 13c15e5ca58d2..ab675c8d9dedf 100644 --- a/x-pack/platform/packages/shared/kbn-elastic-assistant-common/docs/openapi/ess/elastic_assistant_api_2023_10_31.bundled.schema.yaml +++ b/x-pack/platform/packages/shared/kbn-elastic-assistant-common/docs/openapi/ess/elastic_assistant_api_2023_10_31.bundled.schema.yaml @@ -1179,9 +1179,6 @@ components: id: description: The conversation id. type: string - isDefault: - description: Is default conversation. - type: boolean messages: description: The conversation messages. items: @@ -1204,16 +1201,13 @@ components: $ref: '#/components/schemas/ConversationCategory' description: The conversation category. createdAt: - description: The last time conversation was updated. + description: The time conversation was created. type: string excludeFromLastConversationStorage: description: excludeFromLastConversationStorage. type: boolean id: $ref: '#/components/schemas/NonEmptyString' - isDefault: - description: Is default conversation. - type: boolean messages: description: The conversation messages. items: @@ -1448,7 +1442,6 @@ components: FindConversationsSortField: enum: - created_at - - is_default - title - updated_at type: string diff --git a/x-pack/platform/packages/shared/kbn-elastic-assistant-common/docs/openapi/serverless/elastic_assistant_api_2023_10_31.bundled.schema.yaml b/x-pack/platform/packages/shared/kbn-elastic-assistant-common/docs/openapi/serverless/elastic_assistant_api_2023_10_31.bundled.schema.yaml index 650fc048d0948..239686e34950c 100644 --- a/x-pack/platform/packages/shared/kbn-elastic-assistant-common/docs/openapi/serverless/elastic_assistant_api_2023_10_31.bundled.schema.yaml +++ b/x-pack/platform/packages/shared/kbn-elastic-assistant-common/docs/openapi/serverless/elastic_assistant_api_2023_10_31.bundled.schema.yaml @@ -1179,9 +1179,6 @@ components: id: description: The conversation id. type: string - isDefault: - description: Is default conversation. - type: boolean messages: description: The conversation messages. items: @@ -1204,16 +1201,13 @@ components: $ref: '#/components/schemas/ConversationCategory' description: The conversation category. createdAt: - description: The last time conversation was updated. + description: The time conversation was created. type: string excludeFromLastConversationStorage: description: excludeFromLastConversationStorage. type: boolean id: $ref: '#/components/schemas/NonEmptyString' - isDefault: - description: Is default conversation. - type: boolean messages: description: The conversation messages. items: @@ -1448,7 +1442,6 @@ components: FindConversationsSortField: enum: - created_at - - is_default - title - updated_at type: string diff --git a/x-pack/platform/packages/shared/kbn-elastic-assistant-common/impl/schemas/conversations/common_attributes.gen.ts b/x-pack/platform/packages/shared/kbn-elastic-assistant-common/impl/schemas/conversations/common_attributes.gen.ts index b41211aca8eb2..5ab44916e0204 100644 --- a/x-pack/platform/packages/shared/kbn-elastic-assistant-common/impl/schemas/conversations/common_attributes.gen.ts +++ b/x-pack/platform/packages/shared/kbn-elastic-assistant-common/impl/schemas/conversations/common_attributes.gen.ts @@ -327,7 +327,7 @@ export const ConversationResponse = z.object({ */ updatedAt: z.string().optional(), /** - * The last time conversation was updated. + * The time conversation was created. */ createdAt: z.string(), replacements: Replacements.optional(), @@ -340,10 +340,6 @@ export const ConversationResponse = z.object({ * LLM API configuration. */ apiConfig: ApiConfig.optional(), - /** - * Is default conversation. - */ - isDefault: z.boolean().optional(), /** * excludeFromLastConversationStorage. */ @@ -403,10 +399,6 @@ export const ConversationCreateProps = z.object({ * LLM API configuration. */ apiConfig: ApiConfig.optional(), - /** - * Is default conversation. - */ - isDefault: z.boolean().optional(), /** * excludeFromLastConversationStorage. */ diff --git a/x-pack/platform/packages/shared/kbn-elastic-assistant-common/impl/schemas/conversations/common_attributes.schema.yaml b/x-pack/platform/packages/shared/kbn-elastic-assistant-common/impl/schemas/conversations/common_attributes.schema.yaml index 6df8ee8a19f3a..c0be31b3d48ec 100644 --- a/x-pack/platform/packages/shared/kbn-elastic-assistant-common/impl/schemas/conversations/common_attributes.schema.yaml +++ b/x-pack/platform/packages/shared/kbn-elastic-assistant-common/impl/schemas/conversations/common_attributes.schema.yaml @@ -316,7 +316,7 @@ components: description: The last time conversation was updated. type: string createdAt: - description: The last time conversation was updated. + description: The time conversation was created. type: string replacements: $ref: '#/components/schemas/Replacements' @@ -332,9 +332,6 @@ components: apiConfig: $ref: '#/components/schemas/ApiConfig' description: LLM API configuration. - isDefault: - description: Is default conversation. - type: boolean excludeFromLastConversationStorage: description: excludeFromLastConversationStorage. type: boolean @@ -393,9 +390,6 @@ components: apiConfig: $ref: '#/components/schemas/ApiConfig' description: LLM API configuration. - isDefault: - description: Is default conversation. - type: boolean excludeFromLastConversationStorage: description: excludeFromLastConversationStorage. type: boolean diff --git a/x-pack/platform/packages/shared/kbn-elastic-assistant-common/impl/schemas/conversations/find_conversations_route.gen.ts b/x-pack/platform/packages/shared/kbn-elastic-assistant-common/impl/schemas/conversations/find_conversations_route.gen.ts index 244a88fa1daff..7354722ba6e38 100644 --- a/x-pack/platform/packages/shared/kbn-elastic-assistant-common/impl/schemas/conversations/find_conversations_route.gen.ts +++ b/x-pack/platform/packages/shared/kbn-elastic-assistant-common/impl/schemas/conversations/find_conversations_route.gen.ts @@ -21,12 +21,7 @@ import { SortOrder } from '../common_attributes.gen'; import { ConversationResponse } from './common_attributes.gen'; export type FindConversationsSortField = z.infer; -export const FindConversationsSortField = z.enum([ - 'created_at', - 'is_default', - 'title', - 'updated_at', -]); +export const FindConversationsSortField = z.enum(['created_at', 'title', 'updated_at']); export type FindConversationsSortFieldEnum = typeof FindConversationsSortField.enum; export const FindConversationsSortFieldEnum = FindConversationsSortField.enum; diff --git a/x-pack/platform/packages/shared/kbn-elastic-assistant-common/impl/schemas/conversations/find_conversations_route.schema.yaml b/x-pack/platform/packages/shared/kbn-elastic-assistant-common/impl/schemas/conversations/find_conversations_route.schema.yaml index 4b532c2cc7303..ccf2bd8e9cafe 100644 --- a/x-pack/platform/packages/shared/kbn-elastic-assistant-common/impl/schemas/conversations/find_conversations_route.schema.yaml +++ b/x-pack/platform/packages/shared/kbn-elastic-assistant-common/impl/schemas/conversations/find_conversations_route.schema.yaml @@ -98,6 +98,5 @@ components: type: string enum: - 'created_at' - - 'is_default' - 'title' - 'updated_at' diff --git a/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/assistant/api/conversations/conversations.ts b/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/assistant/api/conversations/conversations.ts index 54bac7e563acc..d2a5c9d265cb1 100644 --- a/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/assistant/api/conversations/conversations.ts +++ b/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/assistant/api/conversations/conversations.ts @@ -61,16 +61,15 @@ export const getConversationById = async ({ }; /** - * API call for getting all user conversations. + * API call for determining whether any user conversations exist * - * @param {Object} options - The options object. * @param {HttpSetup} options.http - HttpSetup * @param {IToasts} [options.toasts] - IToasts * @param {AbortSignal} [options.signal] - AbortSignal * - * @returns {Promise} + * @returns {Promise} */ -export const getUserConversations = async ({ +export const getUserConversationsExist = async ({ http, signal, toasts, @@ -78,16 +77,24 @@ export const getUserConversations = async ({ http: HttpSetup; toasts?: IToasts; signal?: AbortSignal | undefined; -}) => { +}): Promise => { try { - return await http.fetch( + const conversation = await http.fetch( ELASTIC_AI_ASSISTANT_CONVERSATIONS_URL_FIND, { method: 'GET', version: API_VERSIONS.public.v1, signal, + query: { + per_page: 1, + page: 1, + // one field to keep request as small as possible + fields: ['title'], + }, } ); + + return conversation.total > 0; } catch (error) { toasts?.addError(error.body && error.body.message ? new Error(error.body.message) : error, { title: i18n.translate('xpack.elasticAssistant.conversations.getUserConversationsError', { diff --git a/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/assistant/api/conversations/use_fetch_current_user_conversations.test.tsx b/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/assistant/api/conversations/use_fetch_current_user_conversations.test.tsx index cfe67c60324cc..4927d5256db71 100644 --- a/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/assistant/api/conversations/use_fetch_current_user_conversations.test.tsx +++ b/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/assistant/api/conversations/use_fetch_current_user_conversations.test.tsx @@ -19,11 +19,8 @@ import { defaultAssistantFeatures } from '@kbn/elastic-assistant-common'; const http = { fetch: jest.fn().mockResolvedValue(defaultAssistantFeatures), }; -const onFetch = jest.fn(); - const defaultProps = { http, - onFetch, isAssistantEnabled: true, } as unknown as UseFetchCurrentUserConversationsParams; @@ -48,14 +45,16 @@ describe('useFetchCurrentUserConversations', () => { method: 'GET', query: { page: 1, - per_page: 99, + fields: ['id', 'title', 'apiConfig', 'updatedAt'], + filter: undefined, + per_page: 28, + sort_field: 'updated_at', + sort_order: 'desc', }, version: '2023-10-31', signal: undefined, } ); - - expect(onFetch).toHaveBeenCalled(); }); }); }); diff --git a/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/assistant/api/conversations/use_fetch_current_user_conversations.ts b/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/assistant/api/conversations/use_fetch_current_user_conversations.ts index 9006ca387e086..1b0e0f9bcc028 100644 --- a/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/assistant/api/conversations/use_fetch_current_user_conversations.ts +++ b/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/assistant/api/conversations/use_fetch_current_user_conversations.ts @@ -6,71 +6,182 @@ */ import { HttpSetup } from '@kbn/core/public'; -import { useQuery } from '@tanstack/react-query'; +import { + InfiniteData, + QueryObserverResult, + RefetchOptions, + RefetchQueryFilters, + useInfiniteQuery, +} from '@tanstack/react-query'; import { API_VERSIONS, ELASTIC_AI_ASSISTANT_CONVERSATIONS_URL_FIND, } from '@kbn/elastic-assistant-common'; +import { useCallback, useEffect, useMemo, useRef } from 'react'; import { Conversation } from '../../../assistant_context/types'; export interface FetchConversationsResponse { page: number; - per_page: number; + perPage: number; total: number; data: Conversation[]; } export interface UseFetchCurrentUserConversationsParams { http: HttpSetup; - onFetch: (result: FetchConversationsResponse) => Record; + fields?: string[]; + filter?: string; + page?: number; + perPage?: number; signal?: AbortSignal | undefined; + sortField?: string; + sortOrder?: string; refetchOnWindowFocus?: boolean; isAssistantEnabled: boolean; + setTotalItemCount?: (total: number) => void; +} + +export interface FetchCurrentUserConversations { + data: Record; + isLoading: boolean; + refetch: ( + options?: RefetchOptions & RefetchQueryFilters + ) => Promise, unknown>>; + isFetched: boolean; + isFetching: boolean; + setPaginationObserver: (ref: HTMLDivElement) => void; } -/** - * API call for fetching assistant conversations for the current user - * - * @param {Object} options - The options object. - * @param {HttpSetup} options.http - HttpSetup - * @param {Function} [options.onFetch] - transformation function for conversations fetch result - * @param {AbortSignal} [options.signal] - AbortSignal - * - * @returns {useQuery} hook for getting the status of the conversations - */ const query = { page: 1, - per_page: 99, + perPage: 28, + // keep request small returning a subset of fields + // un-requested required fields like the users array will now appear empty + // something to consider if/when we add support for global/shared conversations. + fields: ['id', 'title', 'apiConfig', 'updatedAt'], }; -export const CONVERSATIONS_QUERY_KEYS = [ - ELASTIC_AI_ASSISTANT_CONVERSATIONS_URL_FIND, - query.page, - query.per_page, - API_VERSIONS.public.v1, -]; - +const formatFetchedData = (data: InfiniteData | undefined) => + data?.pages.reduce((acc, curr) => { + return { + ...acc, + ...curr.data.reduce( + (conversationsArr, conversation) => ({ + ...conversationsArr, + [conversation.id]: conversation, + }), + {} + ), + }; + }, {}); +/** + * API call for fetching assistant conversations for the current user + */ export const useFetchCurrentUserConversations = ({ http, - onFetch, + fields = query.fields, + filter, + page = query.page, + perPage = query.perPage, signal, + sortField = 'updated_at', + sortOrder = 'desc', refetchOnWindowFocus = true, isAssistantEnabled, -}: UseFetchCurrentUserConversationsParams) => - useQuery( - CONVERSATIONS_QUERY_KEYS, - async () => - http.fetch(ELASTIC_AI_ASSISTANT_CONVERSATIONS_URL_FIND, { + setTotalItemCount, +}: UseFetchCurrentUserConversationsParams): FetchCurrentUserConversations => { + const queryFn = useCallback( + async ({ pageParam }: { pageParam?: UseFetchCurrentUserConversationsParams }) => { + return http.fetch(ELASTIC_AI_ASSISTANT_CONVERSATIONS_URL_FIND, { method: 'GET', version: API_VERSIONS.public.v1, - query, + query: { + fields, + filter, + page: pageParam?.page ?? page, + per_page: pageParam?.perPage ?? perPage, + sort_field: sortField, + sort_order: sortOrder, + }, signal, - }), - { - select: (data) => onFetch(data), - keepPreviousData: true, - initialData: { ...query, total: 0, data: [] }, - refetchOnWindowFocus, - enabled: isAssistantEnabled, + }); + }, + [fields, filter, http, page, perPage, signal, sortField, sortOrder] + ); + + const getNextPageParam = useCallback((lastPage: FetchConversationsResponse) => { + const totalPages = Math.max(1, Math.ceil(lastPage.total / lastPage.perPage)); + if (totalPages === lastPage.page) { + return; + } + return { + ...lastPage, + page: lastPage.page + 1, + }; + }, []); + + const { data, fetchNextPage, hasNextPage, isFetched, isFetching, isLoading, refetch } = + useInfiniteQuery( + [ + ELASTIC_AI_ASSISTANT_CONVERSATIONS_URL_FIND, + page, + perPage, + API_VERSIONS.public.v1, + filter, + sortField, + sortOrder, + ], + queryFn, + { + enabled: isAssistantEnabled, + getNextPageParam, + refetchOnWindowFocus, + } + ); + useEffect(() => { + if (setTotalItemCount && data?.pages?.length) { + setTotalItemCount(data?.pages[0].total ?? 0); } + }, [data?.pages, setTotalItemCount]); + + const formatted = useMemo(() => formatFetchedData(data), [data]); + + const observerRef = useRef(); + const fetchNext = useCallback( + async ([{ isIntersecting }]: IntersectionObserverEntry[]) => { + if (isIntersecting && hasNextPage && !isLoading && !isFetching) { + await fetchNextPage(); + observerRef.current?.disconnect(); + } + }, + [fetchNextPage, hasNextPage, isFetching, isLoading] ); + + useEffect(() => { + return () => observerRef.current?.disconnect(); + }, []); + + // Attaches an intersection observer to the last element + // to trigger a callback to paginate when the user scrolls to it + const setPaginationObserver = useCallback( + (ref: HTMLDivElement) => { + observerRef.current?.disconnect(); + observerRef.current = new IntersectionObserver(fetchNext, { + root: null, + rootMargin: '0px', + threshold: 0.1, + }); + observerRef.current?.observe(ref); + }, + [fetchNext] + ); + + return { + data: formatted ?? {}, + isLoading, + refetch, + isFetched, + isFetching, + setPaginationObserver, + }; +}; diff --git a/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/assistant/api/index.tsx b/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/assistant/api/index.tsx index f522c22549cdc..ac912d3570746 100644 --- a/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/assistant/api/index.tsx +++ b/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/assistant/api/index.tsx @@ -57,7 +57,6 @@ export const fetchConnectorExecuteAction = async ({ traceOptions, screenContext, }: FetchConnectorExecuteAction): Promise => { - // TODO add streaming support for gemini with langchain on const isStream = assistantStreamingEnabled; const optionalRequestParams = getOptionalRequestParams({ diff --git a/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/assistant/assistant_body/index.tsx b/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/assistant/assistant_body/index.tsx index 8ce728f339db9..856ac9c9af97c 100644 --- a/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/assistant/assistant_body/index.tsx +++ b/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/assistant/assistant_body/index.tsx @@ -29,7 +29,7 @@ interface Props { comments: JSX.Element; currentConversation: Conversation | undefined; currentSystemPromptId: string | undefined; - handleOnConversationSelected: ({ cId, cTitle }: { cId: string; cTitle: string }) => Promise; + handleOnConversationSelected: ({ cId }: { cId: string }) => Promise; isAssistantEnabled: boolean; isSettingsModalVisible: boolean; isWelcomeSetup: boolean; diff --git a/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/assistant/assistant_body/welcome_setup.tsx b/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/assistant/assistant_body/welcome_setup.tsx index 9446454ad94cd..3ba0dc1783afe 100644 --- a/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/assistant/assistant_body/welcome_setup.tsx +++ b/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/assistant/assistant_body/welcome_setup.tsx @@ -15,7 +15,7 @@ import * as i18n from '../translations'; interface Props { currentConversation: Conversation | undefined; - handleOnConversationSelected: ({ cId, cTitle }: { cId: string; cTitle: string }) => Promise; + handleOnConversationSelected: ({ cId }: { cId: string }) => Promise; } export const WelcomeSetup: React.FC = ({ diff --git a/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/assistant/assistant_header/index.test.tsx b/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/assistant/assistant_header/index.test.tsx index fe4b0de4d2149..1b47ea546e3cb 100644 --- a/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/assistant/assistant_header/index.test.tsx +++ b/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/assistant/assistant_header/index.test.tsx @@ -38,6 +38,7 @@ const testProps = { onChatCleared: jest.fn(), showAnonymizedValues: false, conversations: mockConversations, + refetchCurrentConversation: jest.fn(), refetchCurrentUserConversations: jest.fn(), isAssistantEnabled: true, anonymizationFields: { total: 0, page: 1, perPage: 1000, data: [] }, @@ -45,6 +46,7 @@ const testProps = { allPrompts: [], contentReferencesVisible: true, setContentReferencesVisible: jest.fn(), + setPaginationObserver: jest.fn(), }; jest.mock('../../connectorland/use_load_connectors', () => ({ diff --git a/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/assistant/assistant_header/index.tsx b/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/assistant/assistant_header/index.tsx index 669c0c6f327e1..37832504392a7 100644 --- a/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/assistant/assistant_header/index.tsx +++ b/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/assistant/assistant_header/index.tsx @@ -10,7 +10,8 @@ import { QueryObserverResult, RefetchOptions, RefetchQueryFilters } from '@tanst import { EuiFlexGroup, EuiFlexItem, EuiButtonIcon, EuiPanel, EuiSkeletonTitle } from '@elastic/eui'; import { css } from '@emotion/react'; import { euiThemeVars } from '@kbn/ui-theme'; -import { isEmpty } from 'lodash'; +import { ApiConfig } from '@kbn/elastic-assistant-common'; +import { NEW_CHAT } from '../conversations/conversation_sidepanel/translations'; import { DataStreamApis } from '../use_data_stream_apis'; import { Conversation } from '../../..'; import { AssistantTitle } from '../assistant_title'; @@ -32,15 +33,25 @@ interface OwnProps { onCloseFlyout?: () => void; chatHistoryVisible?: boolean; setChatHistoryVisible?: React.Dispatch>; - onConversationSelected: ({ cId, cTitle }: { cId: string; cTitle: string }) => void; + onConversationSelected: ({ + cId, + cTitle, + apiConfig, + }: { + apiConfig?: ApiConfig; + cId: string; + cTitle?: string; + }) => void; conversations: Record; conversationsLoaded: boolean; + refetchCurrentConversation: ({ isStreamRefetch }: { isStreamRefetch?: boolean }) => void; refetchCurrentUserConversations: DataStreamApis['refetchCurrentUserConversations']; onConversationCreate: () => Promise; isAssistantEnabled: boolean; refetchPrompts?: ( options?: RefetchOptions & RefetchQueryFilters ) => Promise>; + setPaginationObserver: (ref: HTMLDivElement) => void; } type Props = OwnProps; @@ -53,23 +64,25 @@ export const AI_ASSISTANT_SETTINGS_MENU_CONTAINER_ID = 'aiAssistantSettingsMenuC * toggling the display of anonymized values, and accessing the assistant settings. */ export const AssistantHeader: React.FC = ({ - selectedConversation, + chatHistoryVisible, + conversations, + conversationsLoaded, defaultConnector, + isAssistantEnabled, isDisabled, isLoading, isSettingsModalVisible, - setIsSettingsModalVisible, onChatCleared, - chatHistoryVisible, - setChatHistoryVisible, onCloseFlyout, + onConversationCreate, onConversationSelected, - conversations, - conversationsLoaded, + refetchCurrentConversation, refetchCurrentUserConversations, - onConversationCreate, - isAssistantEnabled, refetchPrompts, + selectedConversation, + setChatHistoryVisible, + setIsSettingsModalVisible, + setPaginationObserver, }) => { const selectedConnectorId = useMemo( () => selectedConversation?.apiConfig?.connectorId, @@ -77,10 +90,11 @@ export const AssistantHeader: React.FC = ({ ); const onConversationChange = useCallback( - (updatedConversation: Conversation) => { + (updatedConversation: Conversation, apiConfig?: ApiConfig) => { onConversationSelected({ cId: updatedConversation.id, cTitle: updatedConversation.title, + ...(apiConfig ? { apiConfig } : {}), }); }, [onConversationSelected] @@ -101,17 +115,14 @@ export const AssistantHeader: React.FC = ({ defaultConnector={defaultConnector} isDisabled={isDisabled} isSettingsModalVisible={isSettingsModalVisible} - selectedConversationId={ - !isEmpty(selectedConversation?.id) - ? selectedConversation?.id - : selectedConversation?.title - } setIsSettingsModalVisible={setIsSettingsModalVisible} onConversationSelected={onConversationSelected} conversations={conversations} conversationsLoaded={conversationsLoaded} + refetchCurrentConversation={refetchCurrentConversation} refetchCurrentUserConversations={refetchCurrentUserConversations} refetchPrompts={refetchPrompts} + setPaginationObserver={setPaginationObserver} /> @@ -147,8 +158,8 @@ export const AssistantHeader: React.FC = ({ ) : ( diff --git a/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/assistant/assistant_header/translations.ts b/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/assistant/assistant_header/translations.ts index 38d439848ee38..020afe039b52c 100644 --- a/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/assistant/assistant_header/translations.ts +++ b/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/assistant/assistant_header/translations.ts @@ -49,20 +49,6 @@ export const RESET_CONVERSATION = i18n.translate( } ); -export const SHOW_ANONYMIZED = i18n.translate( - 'xpack.elasticAssistant.assistant.settings.showAnonymizedToggleLabel', - { - defaultMessage: 'Show anonymized', - } -); - -export const SHOW_REAL_VALUES = i18n.translate( - 'xpack.elasticAssistant.assistant.settings.showAnonymizedToggleRealValuesLabel', - { - defaultMessage: 'Show real values', - } -); - export const ANONYMIZE_VALUES = i18n.translate( 'xpack.elasticAssistant.assistant.settings.anonymizeValues', { diff --git a/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/assistant/assistant_overlay/index.test.tsx b/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/assistant/assistant_overlay/index.test.tsx index 9e6a9164607a3..4cd7b0fce58cc 100644 --- a/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/assistant/assistant_overlay/index.test.tsx +++ b/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/assistant/assistant_overlay/index.test.tsx @@ -57,7 +57,6 @@ describe('AssistantOverlay', () => { expect(reportAssistantInvoked).toHaveBeenCalledTimes(1); expect(reportAssistantInvoked).toHaveBeenCalledWith({ invokedBy: 'shortcut', - conversationId: 'Welcome', }); fireEvent.keyDown(document, { key: ';', ctrlKey: true }); expect(reportAssistantInvoked).toHaveBeenCalledTimes(1); diff --git a/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/assistant/assistant_overlay/index.tsx b/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/assistant/assistant_overlay/index.tsx index c80145dcef5b9..5f8f9fa4c3809 100644 --- a/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/assistant/assistant_overlay/index.tsx +++ b/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/assistant/assistant_overlay/index.tsx @@ -12,7 +12,11 @@ import useEvent from 'react-use/lib/useEvent'; import { css } from '@emotion/react'; import { createGlobalStyle } from 'styled-components'; -import { ShowAssistantOverlayProps, useAssistantContext } from '../../assistant_context'; +import { + LastConversation, + ShowAssistantOverlayProps, + useAssistantContext, +} from '../../assistant_context'; import { Assistant, CONVERSATION_SIDE_PANEL_WIDTH } from '..'; const isMac = navigator.platform.toLowerCase().indexOf('mac') >= 0; @@ -30,10 +34,12 @@ export const UnifiedTimelineGlobalStyles = createGlobalStyle` export const AssistantOverlay = React.memo(() => { const [isModalVisible, setIsModalVisible] = useState(false); - // Why is this named Title and not Id? - const [conversationTitle, setConversationTitle] = useState(undefined); + // id if the conversation exists in the data stream, title if it's a new conversation + const [lastConversation, setSelectedConversation] = useState( + undefined + ); const [promptContextId, setPromptContextId] = useState(); - const { assistantTelemetry, setShowAssistantOverlay, getLastConversationId } = + const { assistantTelemetry, setShowAssistantOverlay, getLastConversation } = useAssistantContext(); const [chatHistoryVisible, setChatHistoryVisible] = useState(false); @@ -44,16 +50,16 @@ export const AssistantOverlay = React.memo(() => { ({ showOverlay: so, promptContextId: pid, - conversationTitle: cTitle, + selectedConversation, }: ShowAssistantOverlayProps) => { - const conversationId = getLastConversationId(cTitle); - if (so) assistantTelemetry?.reportAssistantInvoked({ conversationId, invokedBy: 'click' }); + const nextConversation = getLastConversation(selectedConversation); + if (so) assistantTelemetry?.reportAssistantInvoked({ invokedBy: 'click' }); setIsModalVisible(so); setPromptContextId(pid); - setConversationTitle(conversationId); + setSelectedConversation(nextConversation); }, - [assistantTelemetry, getLastConversationId] + [assistantTelemetry, getLastConversation] ); useEffect(() => { setShowAssistantOverlay(showOverlay); @@ -63,15 +69,14 @@ export const AssistantOverlay = React.memo(() => { const handleShortcutPress = useCallback(() => { // Try to restore the last conversation on shortcut pressed if (!isModalVisible) { - setConversationTitle(getLastConversationId()); + setSelectedConversation(getLastConversation()); assistantTelemetry?.reportAssistantInvoked({ invokedBy: 'shortcut', - conversationId: getLastConversationId(), }); } setIsModalVisible(!isModalVisible); - }, [isModalVisible, getLastConversationId, assistantTelemetry]); + }, [isModalVisible, getLastConversation, assistantTelemetry]); // Register keyboard listener to show the modal when cmd + ; is pressed const onKeyDown = useCallback( @@ -89,8 +94,8 @@ export const AssistantOverlay = React.memo(() => { const cleanupAndCloseModal = useCallback(() => { setIsModalVisible(false); setPromptContextId(undefined); - setConversationTitle(conversationTitle); - }, [conversationTitle]); + setSelectedConversation(lastConversation); + }, [lastConversation]); const handleCloseModal = useCallback(() => { cleanupAndCloseModal(); @@ -132,7 +137,7 @@ export const AssistantOverlay = React.memo(() => { hideCloseButton > = ({ title, selectedConversation, refetchCurrentUserConversations, isDisabled = false }) => { +}> = ({ + title = NEW_CHAT, + selectedConversation, + refetchCurrentUserConversations, + isDisabled = false, +}) => { const [newTitle, setNewTitle] = useState(title); const [newTitleError, setNewTitleError] = useState(false); const { updateConversationTitle } = useConversation(); @@ -62,10 +67,10 @@ export const AssistantTitle: React.FC<{ data-test-subj="conversationTitle" heading="h2" inputAriaLabel="Edit text inline" - value={newTitle ?? NEW_CHAT} + value={newTitle} size="xs" isInvalid={!!newTitleError} - isReadOnly={isDisabled || selectedConversation?.isDefault} + isReadOnly={isDisabled} onChange={(e) => setNewTitle(e.currentTarget.nodeValue || '')} onCancel={() => setNewTitle(title)} onSave={handleUpdateTitle} diff --git a/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/assistant/chat_send/index.tsx b/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/assistant/chat_send/index.tsx index cdfa12e3507f0..6351b496422e4 100644 --- a/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/assistant/chat_send/index.tsx +++ b/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/assistant/chat_send/index.tsx @@ -48,10 +48,6 @@ export const ChatSend: React.FC = ({ useAutosizeTextArea(promptTextAreaRef?.current, promptValue); - useEffect(() => { - setUserPrompt(promptValue); - }, [setUserPrompt, promptValue]); - return ( { await waitFor(() => { expect(reportAssistantMessageSent).toHaveBeenNthCalledWith(1, { - conversationId: testProps.currentConversation?.title, role: 'user', actionTypeId: '.gen-ai', model: undefined, @@ -138,7 +137,6 @@ describe('use chat send', () => { isEnabledKnowledgeBase: false, }); expect(reportAssistantMessageSent).toHaveBeenNthCalledWith(2, { - conversationId: testProps.currentConversation?.title, role: 'assistant', actionTypeId: '.gen-ai', model: undefined, diff --git a/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/assistant/chat_send/use_chat_send.tsx b/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/assistant/chat_send/use_chat_send.tsx index c240d5ac6b60b..f2d9adc6bf81b 100644 --- a/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/assistant/chat_send/use_chat_send.tsx +++ b/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/assistant/chat_send/use_chat_send.tsx @@ -11,7 +11,6 @@ import { i18n } from '@kbn/i18n'; import { Replacements } from '@kbn/elastic-assistant-common'; import { useKnowledgeBaseStatus } from '../api/knowledge_base/use_knowledge_base_status'; import { DataStreamApis } from '../use_data_stream_apis'; -import { NEW_CHAT } from '../conversations/conversation_sidepanel/translations'; import type { ClientMessage } from '../../assistant_context/types'; import { SelectedPromptContext } from '../prompt_context/types'; import { useSendMessage } from '../use_send_message'; @@ -56,11 +55,13 @@ export const useChatSend = ({ assistantTelemetry, toasts, assistantAvailability: { isAssistantEnabled }, + setLastConversation, } = useAssistantContext(); const [userPrompt, setUserPrompt] = useState(null); const { isLoading, sendMessage, abortStream } = useSendMessage(); - const { clearConversation, removeLastMessage } = useConversation(); + const { clearConversation, createConversation, getConversation, removeLastMessage } = + useConversation(); const { data: kbStatus } = useKnowledgeBaseStatus({ http, enabled: isAssistantEnabled }); const isSetupComplete = kbStatus?.elser_exists && @@ -82,15 +83,25 @@ export const useChatSend = ({ ); return; } - + const apiConfig = currentConversation.apiConfig; + let newConvo; + if (currentConversation.id === '') { + // create conversation with empty title, GENERATE_CHAT_TITLE graph step will properly title + newConvo = await createConversation(currentConversation); + if (newConvo?.id) { + setLastConversation({ + id: newConvo.id, + }); + } + } + const convo: Conversation = { ...currentConversation, ...(newConvo ?? {}) }; const userMessage = getCombinedMessage({ - currentReplacements: currentConversation.replacements, + currentReplacements: convo.replacements, promptText, selectedPromptContexts, }); - const baseReplacements: Replacements = - userMessage.replacements ?? currentConversation.replacements; + const baseReplacements: Replacements = userMessage.replacements ?? convo.replacements; const selectedPromptContextsReplacements = Object.values( selectedPromptContexts @@ -100,12 +111,12 @@ export const useChatSend = ({ ...baseReplacements, ...selectedPromptContextsReplacements, }; - const updatedMessages = [...currentConversation.messages, userMessage].map((m) => ({ + const updatedMessages = [...convo.messages, userMessage].map((m) => ({ ...m, content: m.content ?? '', })); setCurrentConversation({ - ...currentConversation, + ...convo, replacements, messages: updatedMessages, }); @@ -114,46 +125,49 @@ export const useChatSend = ({ setSelectedPromptContexts({}); const rawResponse = await sendMessage({ - apiConfig: currentConversation.apiConfig, + apiConfig, http, message: userMessage.content ?? '', - conversationId: currentConversation.id, + conversationId: convo.id, replacements, }); assistantTelemetry?.reportAssistantMessageSent({ - conversationId: currentConversation.title, role: userMessage.role, - actionTypeId: currentConversation.apiConfig.actionTypeId, - model: currentConversation.apiConfig.model, - provider: currentConversation.apiConfig.provider, + actionTypeId: apiConfig.actionTypeId, + model: apiConfig.model, + provider: apiConfig.provider, isEnabledKnowledgeBase: isSetupComplete ?? false, }); const responseMessage: ClientMessage = getMessageFromRawResponse(rawResponse); - + if (convo.title === '') { + convo.title = (await getConversation(convo.id))?.title ?? ''; + } setCurrentConversation({ - ...currentConversation, + ...convo, replacements, messages: [...updatedMessages, responseMessage], }); assistantTelemetry?.reportAssistantMessageSent({ - conversationId: currentConversation.title, role: responseMessage.role, - actionTypeId: currentConversation.apiConfig.actionTypeId, - model: currentConversation.apiConfig.model, - provider: currentConversation.apiConfig.provider, + actionTypeId: apiConfig.actionTypeId, + model: apiConfig.model, + provider: apiConfig.provider, isEnabledKnowledgeBase: isSetupComplete ?? false, }); }, [ assistantTelemetry, + createConversation, currentConversation, + getConversation, http, isSetupComplete, selectedPromptContexts, sendMessage, setCurrentConversation, + setLastConversation, setSelectedPromptContexts, toasts, ] @@ -218,11 +232,10 @@ export const useChatSend = ({ const handleChatSend = useCallback( async (promptText: string) => { await handleSendMessage(promptText); - if (currentConversation?.title === NEW_CHAT) { - await refetchCurrentUserConversations(); - } + // refetch to update the conversation list + await refetchCurrentUserConversations(); }, - [currentConversation, handleSendMessage, refetchCurrentUserConversations] + [handleSendMessage, refetchCurrentUserConversations] ); return { diff --git a/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/assistant/common/components/assistant_settings_management/badges/index.tsx b/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/assistant/common/components/assistant_settings_management/badges/index.tsx index 0a44664bd5d34..b84e5225c4d6f 100644 --- a/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/assistant/common/components/assistant_settings_management/badges/index.tsx +++ b/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/assistant/common/components/assistant_settings_management/badges/index.tsx @@ -15,9 +15,9 @@ export const BadgesColumn: React.FC<{ }> = React.memo(({ items, prefix, color = 'hollow' }) => items && items.length > 0 ? (
- {items.map((c, idx) => ( + {items.map((title, idx) => ( - {c} + {title} ))}
diff --git a/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/assistant/common/components/assistant_settings_management/inline_actions/index.tsx b/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/assistant/common/components/assistant_settings_management/inline_actions/index.tsx index 7a2da0d22fc3e..8748d1bbe8e7d 100644 --- a/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/assistant/common/components/assistant_settings_management/inline_actions/index.tsx +++ b/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/assistant/common/components/assistant_settings_management/inline_actions/index.tsx @@ -16,7 +16,7 @@ interface Props { onEdit?: (rowItem: T) => void; } -export const useInlineActions = () => { +export const useInlineActions = () => { const getInlineActions = useCallback( ({ isEditEnabled = () => false, diff --git a/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/assistant/common/components/assistant_settings_management/pagination/use_session_pagination.ts b/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/assistant/common/components/assistant_settings_management/pagination/use_session_pagination.ts index 3bf0a5e792089..0386156197945 100644 --- a/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/assistant/common/components/assistant_settings_management/pagination/use_session_pagination.ts +++ b/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/assistant/common/components/assistant_settings_management/pagination/use_session_pagination.ts @@ -5,57 +5,89 @@ * 2.0. */ -import { Direction } from '@elastic/eui'; +import { CriteriaWithPagination, EuiInMemoryTableProps, EuiTableSortingType } from '@elastic/eui'; import { useCallback, useMemo } from 'react'; import useSessionStorage from 'react-use/lib/useSessionStorage'; import { DEFAULT_ASSISTANT_NAMESPACE } from '../../../../../assistant_context/constants'; import { DEFAULT_PAGE_SIZE } from '../../../../settings/const'; -export const DEFAULT_TABLE_OPTIONS = { - page: { size: DEFAULT_PAGE_SIZE, index: 0 }, - sort: { field: '', direction: 'asc' as const }, -}; +export const getDefaultTableOptions = ({ + pageSize, + sortDirection, + sortField, +}: { + sortField: keyof T; + sortDirection?: 'asc' | 'desc'; + pageSize?: number; +}) => ({ + page: { size: pageSize ?? DEFAULT_PAGE_SIZE, index: 0 }, + sort: { field: sortField, direction: sortDirection ?? ('desc' as const) }, +}); + +interface InMemoryPagination { + initialPageSize: number; + pageSizeOptions: number[]; + pageIndex: number; +} + +interface ServerSidePagination { + totalItemCount: number; + pageSize: number; + pageSizeOptions: number[]; + pageIndex: number; +} -export const useSessionPagination = ({ +interface UseSessionPaginationReturn { + onTableChange: (criteria: CriteriaWithPagination) => void; + pagination: B extends true ? InMemoryPagination : ServerSidePagination; + sorting: B extends true ? EuiInMemoryTableProps['sorting'] : EuiTableSortingType; +} + +export const useSessionPagination = ({ defaultTableOptions, nameSpace = DEFAULT_ASSISTANT_NAMESPACE, + inMemory, storageKey, + totalItemCount = 0, }: { - defaultTableOptions: { - page: { size: number; index: number }; - sort: { field: string; direction: Direction }; - }; + defaultTableOptions: CriteriaWithPagination; + inMemory?: B; nameSpace?: string; storageKey: string; -}) => { + totalItemCount?: number; +}): UseSessionPaginationReturn => { const [sessionStorageTableOptions = defaultTableOptions, setSessionStorageTableOptions] = - useSessionStorage(`${nameSpace}.${storageKey}`, defaultTableOptions); + useSessionStorage>(`${nameSpace}.${storageKey}`, defaultTableOptions); const pagination = useMemo( - () => ({ - initialPageSize: sessionStorageTableOptions.page.size, - pageSizeOptions: [5, 10, DEFAULT_PAGE_SIZE, 50], - pageIndex: sessionStorageTableOptions.page.index, - }), - [sessionStorageTableOptions] + () => + (inMemory + ? { + initialPageSize: sessionStorageTableOptions.page.size, + pageSizeOptions: [5, 10, DEFAULT_PAGE_SIZE, 50], + pageIndex: sessionStorageTableOptions.page.index, + } + : { + totalItemCount, + pageSize: sessionStorageTableOptions.page.size ?? DEFAULT_PAGE_SIZE, + pageSizeOptions: [5, 10, DEFAULT_PAGE_SIZE, 50], + pageIndex: sessionStorageTableOptions.page.index, + }) as UseSessionPaginationReturn['pagination'], + [inMemory, sessionStorageTableOptions, totalItemCount] ); - const sorting = useMemo( - () => ({ - sort: sessionStorageTableOptions.sort, - }), - [sessionStorageTableOptions.sort] - ); + const sorting = useMemo(() => { + return { + sort: sessionStorageTableOptions.sort ?? defaultTableOptions.sort, + } as UseSessionPaginationReturn['sorting']; + }, [defaultTableOptions.sort, sessionStorageTableOptions.sort]); - const onTableChange = useCallback( - ({ - page, - sort, - }: // eslint-disable-next-line @typescript-eslint/no-explicit-any - any) => { + const onTableChange: UseSessionPaginationReturn['onTableChange'] = useCallback( + (args: CriteriaWithPagination) => { + const { page, sort } = args; setSessionStorageTableOptions({ page, - sort, + ...(sort ? { sort } : {}), }); }, [setSessionStorageTableOptions] diff --git a/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/assistant/conversations/conversation_selector_settings/index.test.tsx b/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/assistant/conversations/conversation_selector_settings/index.test.tsx index e2c630bbedd14..1e9e5d1c9c3ca 100644 --- a/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/assistant/conversations/conversation_selector_settings/index.test.tsx +++ b/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/assistant/conversations/conversation_selector_settings/index.test.tsx @@ -35,13 +35,6 @@ describe('ConversationSelectorSettings', () => { fireEvent.click(getByTestId(alertConvo.title)); expect(onConversationSelectionChange).toHaveBeenCalledWith(alertConvo); }); - it('Only custom option can be deleted', () => { - const { getByTestId } = render(); - fireEvent.click(getByTestId('comboBoxToggleListButton')); - // there is only one delete conversation because there is only one custom convo - fireEvent.click(getByTestId('delete-conversation')); - expect(onConversationDeleted).toHaveBeenCalledWith(customConvo.title); - }); it('Selects existing conversation from the search input', () => { const { getByTestId } = render(); fireEvent.change(getByTestId('comboBoxSearchInput'), { target: { value: alertConvo.title } }); diff --git a/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/assistant/conversations/conversation_selector_settings/index.tsx b/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/assistant/conversations/conversation_selector_settings/index.tsx index cf30f5ea935e9..dacbc079e6691 100644 --- a/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/assistant/conversations/conversation_selector_settings/index.tsx +++ b/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/assistant/conversations/conversation_selector_settings/index.tsx @@ -8,6 +8,7 @@ import { EuiButtonIcon, EuiComboBox, + EuiComboBoxOptionOption, EuiFlexGroup, EuiFlexItem, EuiFormRow, @@ -20,7 +21,6 @@ import { css } from '@emotion/react'; import { Conversation } from '../../../..'; import * as i18n from './translations'; import { SystemPromptSelectorOption } from '../../prompt_editor/system_prompt/system_prompt_modal/system_prompt_selector/system_prompt_selector'; -import { ConversationSelectorSettingsOption } from './types'; interface Props { conversations: Record; @@ -67,25 +67,24 @@ export const ConversationSelectorSettings: React.FC = React.memo( [conversations] ); - const [conversationOptions, setConversationOptions] = useState< - ConversationSelectorSettingsOption[] - >(() => { - return Object.values(conversations).map((conversation) => ({ - value: { isDefault: conversation.isDefault ?? false }, - label: conversation.title, - id: conversation.id, - 'data-test-subj': conversation.title, - })); - }); + const [conversationOptions, setConversationOptions] = useState( + () => { + return Object.values(conversations).map((conversation) => ({ + label: conversation.title, + id: conversation.id, + 'data-test-subj': conversation.title, + })); + } + ); - const selectedOptions = useMemo(() => { + const selectedOptions = useMemo(() => { return selectedConversationTitle ? conversationOptions.filter((c) => c.label === selectedConversationTitle) ?? [] : []; }, [conversationOptions, selectedConversationTitle]); const handleSelectionChange = useCallback( - (conversationSelectorSettingsOption: ConversationSelectorSettingsOption[]) => { + (conversationSelectorSettingsOption: EuiComboBoxOptionOption[]) => { const newConversation = conversationSelectorSettingsOption.length === 0 ? undefined @@ -129,7 +128,7 @@ export const ConversationSelectorSettings: React.FC = React.memo( // Callback for when a user selects a conversation const onChange = useCallback( - (newOptions: ConversationSelectorSettingsOption[]) => { + (newOptions: EuiComboBoxOptionOption[]) => { if (newOptions.length === 0) { handleSelectionChange([]); } else if (conversationOptions.findIndex((o) => o.label === newOptions?.[0].label) !== -1) { @@ -163,11 +162,11 @@ export const ConversationSelectorSettings: React.FC = React.memo( }, [conversationTitles, selectedConversationTitle, conversationOptions, handleSelectionChange]); const renderOption: ( - option: ConversationSelectorSettingsOption, + option: EuiComboBoxOptionOption, searchValue: string, OPTION_CONTENT_CLASSNAME: string ) => React.ReactNode = (option, searchValue) => { - const { label, value } = option; + const { label } = option; return ( = React.memo( {label} - {!value?.isDefault && ( - - - { - e.stopPropagation(); - onDelete(label); - }} - css={css` - visibility: hidden; - .parentFlexGroup:hover & { - visibility: visible; - } - `} - /> - - - )} + + + { + e.stopPropagation(); + onDelete(label); + }} + css={css` + visibility: hidden; + .parentFlexGroup:hover & { + visibility: visible; + } + `} + /> + + ); }; diff --git a/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/assistant/conversations/conversation_selector_settings/types.ts b/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/assistant/conversations/conversation_selector_settings/types.ts deleted file mode 100644 index 548149ffe0c7b..0000000000000 --- a/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/assistant/conversations/conversation_selector_settings/types.ts +++ /dev/null @@ -1,12 +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 { EuiComboBoxOptionOption } from '@elastic/eui'; - -export type ConversationSelectorSettingsOption = EuiComboBoxOptionOption<{ - isDefault: boolean; -}>; diff --git a/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/assistant/conversations/conversation_settings/conversation_settings.test.tsx b/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/assistant/conversations/conversation_settings/conversation_settings.test.tsx deleted file mode 100644 index 9897fc9dc3d97..0000000000000 --- a/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/assistant/conversations/conversation_settings/conversation_settings.test.tsx +++ /dev/null @@ -1,343 +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 { fireEvent, render } from '@testing-library/react'; -import { ConversationSettings, ConversationSettingsProps } from './conversation_settings'; -import { TestProviders } from '../../../mock/test_providers/test_providers'; -import { alertConvo, customConvo, welcomeConvo } from '../../../mock/conversation'; -import { mockSystemPrompts } from '../../../mock/system_prompt'; -import { mockConnectors } from '../../../mock/connectors'; -import { HttpSetup } from '@kbn/core/public'; - -const mockConvos = { - '1234': { ...welcomeConvo, id: '1234' }, - '12345': { ...alertConvo, id: '12345' }, - '123': { ...customConvo, id: '123' }, -}; -const onSelectedConversationChange = jest.fn(); - -const setConversationSettings = jest.fn(); -const setConversationsSettingsBulkActions = jest.fn(); - -const testProps: ConversationSettingsProps = { - allSystemPrompts: mockSystemPrompts, - assistantStreamingEnabled: false, - connectors: mockConnectors, - conversationSettings: mockConvos, - conversationsSettingsBulkActions: {}, - http: { basePath: { get: jest.fn() } } as unknown as HttpSetup, - onSelectedConversationChange, - selectedConversation: mockConvos['1234'], - setAssistantStreamingEnabled: jest.fn(), - setConversationSettings, - setConversationsSettingsBulkActions, -}; - -jest.mock('../../../connectorland/use_load_connectors', () => ({ - useLoadConnectors: () => ({ - data: mockConnectors, - error: null, - isSuccess: true, - }), -})); - -const mockConvo = mockConvos['12345']; -jest.mock('../conversation_selector_settings', () => ({ - // @ts-ignore - ConversationSelectorSettings: ({ onConversationDeleted, onConversationSelectionChange }) => ( - <> -