From d22ca947959a4f2891d58a09bf9f2bcba55d43e2 Mon Sep 17 00:00:00 2001 From: Igor Krupenja Date: Fri, 13 Dec 2024 15:51:46 +0200 Subject: [PATCH] Remove excess data fetching on /training/common-intents page (#755) * Merge relevant from old branch * Fix guard * Fix response logic * Fix deps * Remove deupe code * TODOs * Revert unrelated changes * Clean up * Clean up * Clean up * Fix deletion * Clean up * Move entities * Clean up * Clean up * Fix adding example * Fix examples * Clean up * Clean up * Clean up * Fix rule * Fix editing example * gsa * Clean up * Clean up * Clean up * Fix the issue * PoC without data mapping * PoC with poor format * PoC with better format * Clean up * Cle * PoC component * Clean up * Fix name * Fix query * Fix fetch * Revert unrelated changes * Clean up * Clean up * Clean up * Clean up * PoC request * Finish * PoC common component * Working PoC * Extract hook * Use hok in common * Clean up * Clean up * Clean up * Remove dupe list code * Remove unused files --- DSL/DMapper/hbs/get_intents_full.handlebars | 20 - .../hbs/get_intents_with_examples.handlebars | 15 - ...get_intents_with_examples_count.handlebars | 2 +- .../intents-with-examples-count.json | 18 +- .../GET/rasa/intents/common.yml | 88 ---- DSL/Ruuter.private/GET/rasa/intents/full.yml | 96 ---- .../GET/rasa/intents/with-examples-count.yml | 10 + .../pages/Training/Intents/CommonIntents.tsx | 415 ++---------------- .../pages/Training/Intents/IntentDetails.tsx | 4 +- GUI/src/pages/Training/Intents/IntentList.tsx | 2 +- .../pages/Training/Intents/IntentTabList.tsx | 2 +- GUI/src/pages/Training/Intents/index.tsx | 71 +-- .../pages/Training/Intents/useIntentsData.ts | 70 +++ GUI/src/types/intentWithExamplesCount.ts | 3 + GUI/src/utils/compare.ts | 2 +- 15 files changed, 131 insertions(+), 687 deletions(-) delete mode 100644 DSL/DMapper/hbs/get_intents_full.handlebars delete mode 100644 DSL/DMapper/hbs/get_intents_with_examples.handlebars delete mode 100644 DSL/Ruuter.private/GET/rasa/intents/common.yml delete mode 100644 DSL/Ruuter.private/GET/rasa/intents/full.yml create mode 100644 GUI/src/pages/Training/Intents/useIntentsData.ts create mode 100644 GUI/src/types/intentWithExamplesCount.ts diff --git a/DSL/DMapper/hbs/get_intents_full.handlebars b/DSL/DMapper/hbs/get_intents_full.handlebars deleted file mode 100644 index 2d64860d..00000000 --- a/DSL/DMapper/hbs/get_intents_full.handlebars +++ /dev/null @@ -1,20 +0,0 @@ -{ -"intents": [ -{{#each intents.intents}} - { - "id": "{{id}}", - "title": "{{title}}", - "examples": [ - {{#each examples}} - "{{{this}}}"{{#unless @last}},{{/unless}} - {{/each}} - ], - "inmodel": {{isInModel title ../intents}}, - "serviceId": "{{findConnectedServiceId title ../intents}}", - "isForService": "{{getObjectKeyFromObjectArray ../intents/intentsModifiedAt 'intent' title 'isforservice'}}", - "modifiedAt": "{{findModifiedAt title ../intents/intentsModifiedAt}}", - "count": {{getCount title ../intents}} - }{{#unless @last}},{{/unless}} -{{/each}} -] -} diff --git a/DSL/DMapper/hbs/get_intents_with_examples.handlebars b/DSL/DMapper/hbs/get_intents_with_examples.handlebars deleted file mode 100644 index f44a888d..00000000 --- a/DSL/DMapper/hbs/get_intents_with_examples.handlebars +++ /dev/null @@ -1,15 +0,0 @@ -{ -"intents": [ -{{#each hits}} - { - "id": "{{_id}}", - "title": "{{_source.intent}}", - "examples": [ - {{#each _source.examples}} - "{{{ this }}}"{{#unless @last}},{{/unless}} - {{/each}} - ] - }{{#unless @last}},{{/unless}} -{{/each}} -] -} diff --git a/DSL/DMapper/hbs/get_intents_with_examples_count.handlebars b/DSL/DMapper/hbs/get_intents_with_examples_count.handlebars index fc0e9a62..9212e9b9 100644 --- a/DSL/DMapper/hbs/get_intents_with_examples_count.handlebars +++ b/DSL/DMapper/hbs/get_intents_with_examples_count.handlebars @@ -3,7 +3,7 @@ {{#each buckets}} { "id": "{{key}}", - "examples_count": {{examples_counts.value}}, + "examplesCount": {{examples_counts.value}}, "inModel": {{isInModel key ../intents}}, "modifiedAt": "{{findModifiedAt key ../intents/intentsModifiedAt}}" }{{#unless @last}},{{/unless}} diff --git a/DSL/OpenSearch/templates/intents-with-examples-count.json b/DSL/OpenSearch/templates/intents-with-examples-count.json index b05cb200..b4c86efc 100644 --- a/DSL/OpenSearch/templates/intents-with-examples-count.json +++ b/DSL/OpenSearch/templates/intents-with-examples-count.json @@ -19,18 +19,18 @@ } }, "query": { - {{#intent}} - "wildcard": { - "intent": "*{{intent}}*" + "prefix": { + "intent": { + "value": "{{prefix}}" + } } - {{/intent}} - {{^intent}} - "match_all": {} - {{/intent}} } }, "params": { - "intent": "" + "prefix": { + "type": "string", + "description": "Optional prefix to filter intents" + } } } -} \ No newline at end of file +} diff --git a/DSL/Ruuter.private/GET/rasa/intents/common.yml b/DSL/Ruuter.private/GET/rasa/intents/common.yml deleted file mode 100644 index 725c8079..00000000 --- a/DSL/Ruuter.private/GET/rasa/intents/common.yml +++ /dev/null @@ -1,88 +0,0 @@ -declaration: - call: declare - version: 0.1 - description: "Decription placeholder for 'COMMON'" - method: get - accepts: json - returns: json - namespace: training - allowlist: - headers: - - field: cookie - type: string - description: "Cookie field" - -getIntents: - call: http.post - args: - url: "[#TRAINING_OPENSEARCH]/intents/_search?size=10000" - body: - query: - prefix: - id: "common_" - result: getIntentsResult - -getIntentsData: - call: http.post - args: - url: "[#TRAINING_DMAPPER]/hbs/training/get_intents_with_examples" - headers: - type: 'json' - body: - hits: ${getIntentsResult.response.body.hits.hits} - result: getIntentDataResult - -getDomainFile: - call: http.get - args: - url: "[#TRAINING_PUBLIC_RUUTER]/internal/domain-file" - headers: - cookie: ${incoming.headers.cookie} - result: getDomainDataResult - -getIntentsExampleCount: - call: http.post - args: - url: "[#TRAINING_OPENSEARCH]/intents/_search/template" - body: - id: "intents-with-examples-count" - result: getIntentCountResult - -checkIfIntentsExists: - switch: - - condition: ${getIntentDataResult.response.body.intents != null} - next: get_service_intent_connections - next: returnNoIntentsFound - -get_service_intent_connections: - call: http.post - args: - url: "[#TRAINING_RESQL]/get-service-intent-connections" - result: connection_res - -assignResults: - assign: - intents: - intents: ${getIntentDataResult.response.body.intents} - inmodel: ${getDomainDataResult.response.body.response.intents} - count: ${getIntentCountResult.response.body.aggregations.hot.buckets} - connections: ${connection_res.response.body} - -mapIntentsData: - call: http.post - args: - url: "[#TRAINING_DMAPPER]/hbs/training/get_intents_full" - headers: - type: 'json' - body: - intents: ${intents} - result: getIntentDataResult - next: returnSuccess - -returnSuccess: - return: ${getIntentDataResult.response.body} - next: end - -returnNoIntentsFound: - return: "Error: no intents found" - next: end diff --git a/DSL/Ruuter.private/GET/rasa/intents/full.yml b/DSL/Ruuter.private/GET/rasa/intents/full.yml deleted file mode 100644 index 922033ef..00000000 --- a/DSL/Ruuter.private/GET/rasa/intents/full.yml +++ /dev/null @@ -1,96 +0,0 @@ -declaration: - call: declare - version: 0.1 - description: "Decription placeholder for 'FULL'" - method: get - accepts: json - returns: json - namespace: training - allowlist: - headers: - - field: cookie - type: string - description: "Cookie field" - -getIntents: - call: http.get - args: - url: "[#TRAINING_OPENSEARCH]/intents/_search?size=10000" - result: getIntentsResult - -getIntentsData: - call: http.post - args: - url: "[#TRAINING_DMAPPER]/hbs/training/get_intents_with_examples" - headers: - type: 'json' - body: - hits: ${getIntentsResult.response.body.hits.hits} - result: getIntentDataResult - -getDomainFile: - call: http.get - args: - url: "[#TRAINING_PUBLIC_RUUTER]/internal/domain-file" - headers: - cookie: ${incoming.headers.cookie} - result: getDomainDataResult - -checkIfIntentsExists: - switch: - - condition: ${getIntentDataResult.response.body.intents != null} - next: get_service_intent_connections - next: returnNoIntentsFound - -get_service_intent_connections: - call: http.post - args: - url: "[#TRAINING_RESQL]/get-service-intent-connections" - result: connection_res - -getIntentsNames: - call: http.post - args: - url: "[#TRAINING_DMAPPER]/hbs/training/get_intents" - headers: - type: 'json' - body: - hits: ${getIntentsResult.response.body.hits.hits} - result: getIntentsNamesResult - -getIntentsListLastChanged: - call: http.post - args: - url: "[#TRAINING_RESQL]/get-intents-list-last-changed" - body: - intentsList: ${getIntentsNamesResult.response.body.intents} - result: getIntentsListLastChangedResult - -assignResults: - assign: - intents: - intents: ${getIntentDataResult.response.body.intents} - inmodel: ${getDomainDataResult.response.body.response.intents} - connections: ${connection_res.response.body} - intentsModifiedAt: ${getIntentsListLastChangedResult.response.body} - -mapIntentsData: - call: http.post - args: - url: "[#TRAINING_DMAPPER]/hbs/training/get_intents_full" - headers: - type: 'json' - body: - intents: ${intents} - result: getIntentDataResult - next: returnSuccess - -returnSuccess: - return: ${getIntentDataResult.response.body} - next: end - -returnNoIntentsFound: - return: "Error: no intents found" - wrapper: false - status: 409 - next: end diff --git a/DSL/Ruuter.private/GET/rasa/intents/with-examples-count.yml b/DSL/Ruuter.private/GET/rasa/intents/with-examples-count.yml index 4cb27c3f..ea458c1a 100644 --- a/DSL/Ruuter.private/GET/rasa/intents/with-examples-count.yml +++ b/DSL/Ruuter.private/GET/rasa/intents/with-examples-count.yml @@ -11,6 +11,14 @@ declaration: - field: cookie type: string description: "Cookie field" + params: + - field: prefix + type: string + description: "Prefix to filter intents" + +assignValues: + assign: + prefix: ${incoming.params.prefix} getIntentsExampleCount: call: http.post @@ -18,6 +26,8 @@ getIntentsExampleCount: url: "[#TRAINING_OPENSEARCH]/intents/_search/template" body: id: "intents-with-examples-count" + params: + prefix: ${prefix || ''} result: getIntentsResult getDomainFile: diff --git a/GUI/src/pages/Training/Intents/CommonIntents.tsx b/GUI/src/pages/Training/Intents/CommonIntents.tsx index 3fcabc15..6ed94bf4 100644 --- a/GUI/src/pages/Training/Intents/CommonIntents.tsx +++ b/GUI/src/pages/Training/Intents/CommonIntents.tsx @@ -1,185 +1,31 @@ -import { FC, useCallback, useEffect, useMemo, useState } from 'react'; -import { useSearchParams } from 'react-router-dom'; +import { FC, useCallback, useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; -import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; +import { useMutation } from '@tanstack/react-query'; import * as Tabs from '@radix-ui/react-tabs'; -import { format } from 'date-fns'; -import { AxiosError } from 'axios'; -import { MdCheckCircleOutline } from 'react-icons/md'; -import { Button, Card, Dialog, FormInput, Icon, Switch, Tooltip, Track } from 'components'; -import { useToast } from 'hooks/useToast'; -import { Intent } from 'types/intent'; -import { Entity } from 'types/entity'; -import { - addRemoveIntentModel, - deleteIntent, - downloadExamples, - getLastModified, - uploadExamples, -} from 'services/intents'; -import IntentExamplesTable from './IntentExamplesTable'; -import LoadingDialog from '../../../components/LoadingDialog'; -import ConnectServiceToIntentModal from 'pages/ConnectServiceToIntentModal'; +import { Card, FormInput, Switch, Track } from 'components'; +import { getLastModified } from 'services/intents'; import withAuthorization, { ROLES } from 'hoc/with-authorization'; +import IntentDetails from './IntentDetails'; + +import { useIntentsData } from './useIntentsData'; +import IntentList from './IntentList'; const CommonIntents: FC = () => { const { t } = useTranslation(); - const queryClient = useQueryClient(); - const toast = useToast(); - const [searchParams] = useSearchParams(); - const [commonIntentsEnabled, setCommonIntentsEnabled] = useState(true); - const [selectedIntent, setSelectedIntent] = useState(null); - const [deletableIntent, setDeletableIntent] = useState(null); - const [connectableIntent, setConnectableIntent] = useState(null); - const [filter, setFilter] = useState(''); - const [refreshing, setRefreshing] = useState(false); - - let intentParam; - const { data: intentsFullResponse, isLoading } = useQuery({ - queryKey: ['intents/common'], + const { intents, selectedIntent, setSelectedIntent, queryRefresh, isLoading } = useIntentsData({ + queryKey: 'intents/with-examples-count?prefix=common_', }); - const { data: entities } = useQuery({ - queryKey: ['entities'], - }); - - let intentsFullList = intentsFullResponse?.response?.intents; - let commonIntents: Intent[] = []; - - if (intentsFullList) { - intentsFullList.forEach((intent: any) => { - const countExamples = intent.examples.length; - const newIntent: Intent = { - id: intent.title, - description: null, - inModel: intent.inmodel, - modifiedAt: '', - examplesCount: countExamples, - examples: intent.examples, - serviceId: intent.serviceId, - }; - commonIntents.push(newIntent); - }); - intentParam = searchParams.get('intent'); - } - - useEffect(() => { - if (!intentParam || intentsFullList?.length !== commonIntents?.length) return; - - const queryIntent = commonIntents.find((intent) => intent.id === intentParam); - - if (queryIntent) { - setSelectedIntent(queryIntent); - } - }, [intentParam]); - - function isValidDate(dateString: string | number | Date) { - const date = new Date(dateString); - return !isNaN(date.getTime()); - } - - const updateSelectedIntent = (updatedIntent: Intent) => { - setSelectedIntent(null); - setTimeout(() => setSelectedIntent(updatedIntent), 20); - }; - - const queryRefresh = useCallback( - function queryRefresh(selectIntent: string | null) { - setSelectedIntent(null); - - queryClient.fetchQuery(['intents/common']).then((res: any) => { - setRefreshing(false); - if (commonIntents.length > 0) { - const newSelectedIntent = res.response.intents.find((intent) => intent.title === selectIntent) || null; - if (newSelectedIntent) { - setSelectedIntent({ - id: newSelectedIntent.title, - description: null, - inModel: newSelectedIntent.inmodel, - modifiedAt: '', - examplesCount: newSelectedIntent.examples.length, - examples: newSelectedIntent.examples, - serviceId: newSelectedIntent.serviceId, - }); - } - } - }); - }, - [commonIntents, queryClient] - ); - - const intentModelMutation = useMutation({ - mutationFn: (intentModelData: { name: string; inModel: boolean }) => addRemoveIntentModel(intentModelData), - onMutate: () => { - setRefreshing(true); - }, - onSuccess: async () => { - if (selectedIntent?.inModel === true) { - toast.open({ - type: 'success', - title: t('global.notification'), - message: t('toast.intentRemovedFromModel'), - }); - } else { - toast.open({ - type: 'success', - title: t('global.notification'), - message: t('toast.intentAddedToModel'), - }); - } - queryRefresh(selectedIntent?.id || ''); - }, - onError: (error: AxiosError) => { - toast.open({ - type: 'error', - title: t('global.notificationError'), - message: error.message, - }); - }, - onSettled: () => { - setRefreshing(false); - }, - }); - - const deleteIntentMutation = useMutation({ - mutationFn: (data: { name: string }) => deleteIntent(data), - onMutate: () => { - setRefreshing(true); - setDeletableIntent(null); - setConnectableIntent(null); - }, - onSuccess: async () => { - setSelectedIntent(null); - queryRefresh(null); - setRefreshing(false); - toast.open({ - type: 'success', - title: t('global.notification'), - message: t('toast.intentDeleted'), - }); - }, - onError: (error: AxiosError) => { - toast.open({ - type: 'error', - title: t('global.notificationError'), - message: error.message, - }); - }, - onSettled: () => { - commonIntents = commonIntents.filter((intent) => intent.id !== selectedIntent?.id); - setRefreshing(false); - }, - }); + const [commonIntentsEnabled, setCommonIntentsEnabled] = useState(true); + const [filter, setFilter] = useState(''); const filteredIntents = useMemo(() => { - if (!commonIntents) return []; + if (!intents) return []; const formattedFilter = filter.trim().replace(/\s+/g, '_'); - return commonIntents - .filter((intent) => intent.id?.includes(formattedFilter)) - .sort((a, b) => a.id.localeCompare(b.id)); - }, [commonIntents, filter]); + return intents.filter((intent) => intent.id?.includes(formattedFilter)).sort((a, b) => a.id.localeCompare(b.id)); + }, [intents, filter]); const intentModifiedMutation = useMutation({ mutationFn: (data: { intentName: string }) => getLastModified(data), @@ -188,8 +34,8 @@ const CommonIntents: FC = () => { const handleTabsValueChange = useCallback( (value: string) => { setSelectedIntent(null); - if (!commonIntents) return; - const selectedIntent = commonIntents.find((intent) => intent.id === value); + if (!intents) return; + const selectedIntent = intents.find((intent) => intent.id === value); if (selectedIntent) { queryRefresh(selectedIntent.id || ''); intentModifiedMutation.mutate( @@ -207,92 +53,9 @@ const CommonIntents: FC = () => { ); } }, - [intentModifiedMutation, commonIntents, queryRefresh] + [intentModifiedMutation, intents, queryRefresh, setSelectedIntent] ); - const intentDownloadMutation = useMutation({ - mutationFn: (intentModelData: { intentName: string }) => downloadExamples(intentModelData), - onSuccess: async (data) => { - // @ts-ignore - const blob = new Blob([data], { type: 'text/csv' }); - const fileName = selectedIntent?.id + '.csv'; - - if (window.showSaveFilePicker) { - const handle = await window.showSaveFilePicker({ suggestedName: fileName }); - const writable = await handle.createWritable(); - await writable.write(blob); - writable.close(); - } else { - const url = window.URL.createObjectURL(blob); - const a = document.createElement('a'); - a.href = url; - a.download = fileName; - a.click(); - window.URL.revokeObjectURL(url); - } - - toast.open({ - type: 'success', - title: t('global.notification'), - message: t('toast.examplesSentForDownloading'), - }); - }, - onError: (error: AxiosError) => { - if (error.name !== 'AbortError') { - toast.open({ - type: 'error', - title: t('global.notificationError'), - message: error.message, - }); - } - }, - }); - - const handleIntentExamplesUpload = () => { - const input = document.createElement('input'); - input.type = 'file'; - input.accept = '.csv'; - - input.addEventListener('change', async (event) => { - const fileInput = event.target as HTMLInputElement; - const files = fileInput.files; - - if (!files || files.length === 0) { - return; - } - - const file = files[0]; - - try { - await intentUploadMutation.mutateAsync({ - intentName: selectedIntent?.id || '', - formData: file, - }); - } catch (error) {} - }); - - input.click(); - }; - - const intentUploadMutation = useMutation({ - mutationFn: ({ intentName, formData }: { intentName: string; formData: File }) => - uploadExamples(intentName, formData), - onSuccess: () => { - toast.open({ - type: 'success', - title: t('global.notification'), - message: t('toast.fileUploadedSuccessfully'), - }); - }, - onError: (error: AxiosError) => { - toast.open({ - type: 'error', - title: t('global.notificationError'), - message: error.message, - }); - }, - }); - if (isLoading) return <>Loading...; return ( @@ -323,7 +86,7 @@ const CommonIntents: FC = () => { - {commonIntentsEnabled && commonIntents && ( + {commonIntentsEnabled && intents && ( {
- {filteredIntents.map((intent, index) => ( - - - {intent.id.replace(/^common_/, '').replace(/_/g, ' ')} - - {intent.examplesCount} - - {intent.inModel ? ( - - - - } - /> - - - ) : ( - - )} - - - ))} +
{selectedIntent && ( - -
- - - -

{selectedIntent.id.replace(/^common_/, '').replace(/_/g, ' ')}

- -

- {t('global.modifiedAt')}: - {isValidDate(selectedIntent.modifiedAt) - ? ` ${format(new Date(selectedIntent.modifiedAt), 'dd.MM.yyyy')}` - : ` ${t('global.missing')}`} -

- - - - - {selectedIntent.inModel ? ( - - ) : ( - - )} - - - - - - - - - -
-
- {selectedIntent?.examples && ( - - )} -
-
+ )}
)} - - {connectableIntent !== null && ( - setConnectableIntent(null)} /> - )} - - {deletableIntent !== null && ( - setDeletableIntent(null)} - footer={ - <> - - - - } - > -

{t('global.removeValidation')}

-
- )} - {refreshing && ( - -

{t('global.updatingDataBody')}

-
- )} ); }; diff --git a/GUI/src/pages/Training/Intents/IntentDetails.tsx b/GUI/src/pages/Training/Intents/IntentDetails.tsx index 41fbf20f..ee7142a0 100644 --- a/GUI/src/pages/Training/Intents/IntentDetails.tsx +++ b/GUI/src/pages/Training/Intents/IntentDetails.tsx @@ -22,11 +22,11 @@ import { ROLES } from 'hoc/with-authorization'; import useStore from '../../../store/store'; import { editResponse } from 'services/responses'; import { addStoryOrRule, deleteStoryOrRule } from 'services/stories'; -import { Rule, RuleDTO } from 'types/rule'; +import { RuleDTO } from 'types/rule'; import ConnectServiceToIntentModal from 'pages/ConnectServiceToIntentModal'; import LoadingDialog from 'components/LoadingDialog'; import useDocumentEscapeListener from 'hooks/useDocumentEscapeListener'; -import { IntentWithExamplesCount } from 'types/intentWithExampleCounts'; +import { IntentWithExamplesCount } from 'types/intentWithExamplesCount'; interface Response { name: string; diff --git a/GUI/src/pages/Training/Intents/IntentList.tsx b/GUI/src/pages/Training/Intents/IntentList.tsx index 5d85cde0..c1804a84 100644 --- a/GUI/src/pages/Training/Intents/IntentList.tsx +++ b/GUI/src/pages/Training/Intents/IntentList.tsx @@ -4,7 +4,7 @@ import { Icon, Tooltip, Track } from 'components'; import { MdCheckCircleOutline } from 'react-icons/md'; import { useTranslation } from 'react-i18next'; import './IntentTabList.scss'; -import { IntentWithExamplesCount } from 'types/intentWithExampleCounts'; +import { IntentWithExamplesCount } from 'types/intentWithExamplesCount'; interface IntentListProps { intents: IntentWithExamplesCount[]; diff --git a/GUI/src/pages/Training/Intents/IntentTabList.tsx b/GUI/src/pages/Training/Intents/IntentTabList.tsx index bdbbd3d4..2a44b32c 100644 --- a/GUI/src/pages/Training/Intents/IntentTabList.tsx +++ b/GUI/src/pages/Training/Intents/IntentTabList.tsx @@ -4,7 +4,7 @@ import { useTranslation } from 'react-i18next'; import { compareInModel, compareInModelReversed } from 'utils/compare'; import IntentList from './IntentList'; import './IntentTabList.scss'; -import { IntentWithExamplesCount } from 'types/intentWithExampleCounts'; +import { IntentWithExamplesCount } from 'types/intentWithExamplesCount'; interface IntentTabListProps { filter: string; diff --git a/GUI/src/pages/Training/Intents/index.tsx b/GUI/src/pages/Training/Intents/index.tsx index 23376345..f263cb46 100644 --- a/GUI/src/pages/Training/Intents/index.tsx +++ b/GUI/src/pages/Training/Intents/index.tsx @@ -1,90 +1,35 @@ -import { FC, useCallback, useEffect, useState } from 'react'; -import { useSearchParams } from 'react-router-dom'; +import { FC, useCallback, useState } from 'react'; import { useTranslation } from 'react-i18next'; -import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; +import { useMutation } from '@tanstack/react-query'; import * as Tabs from '@radix-ui/react-tabs'; import { AxiosError } from 'axios'; import { Button, FormInput, Track } from 'components'; import { useToast } from 'hooks/useToast'; -import { Intent } from 'types/intent'; import { addIntent } from 'services/intents'; import LoadingDialog from '../../../components/LoadingDialog'; import withAuthorization, { ROLES } from 'hoc/with-authorization'; import IntentTabList from './IntentTabList'; import IntentDetails from './IntentDetails'; -import { IntentWithExamplesCount } from 'types/intentWithExampleCounts'; - -// TODO: rename examples_count to examplesCount when possible with changes in CommonIntents -type IntentWithExamplesCountResponse = Pick & { examples_count: number }; - -type IntentsWithExamplesCountResponse = { - response: { - intents: IntentWithExamplesCountResponse[]; - }; -}; - -const intentResponseToIntent = (intent: IntentWithExamplesCountResponse): IntentWithExamplesCount => ({ - ...intent, - examplesCount: intent.examples_count, - isCommon: intent.id.startsWith('common_'), -}); +import { useIntentsData } from './useIntentsData'; const Intents: FC = () => { const { t } = useTranslation(); - const queryClient = useQueryClient(); const toast = useToast(); - const [searchParams] = useSearchParams(); - const [intents, setIntents] = useState([]); - const [selectedIntent, setSelectedIntent] = useState(null); - const [refreshing, setRefreshing] = useState(false); - const [filter, setFilter] = useState(''); - - const { data: intentsResponse, isLoading } = useQuery({ - queryKey: ['intents/with-examples-count'], + const { intents, selectedIntent, setSelectedIntent, queryRefresh, isLoading } = useIntentsData({ + queryKey: 'intents/with-examples-count', }); - useEffect(() => { - if (intentsResponse) { - setIntents(intentsResponse.response.intents.map((intent) => intentResponseToIntent(intent))); - } - }, [intentsResponse]); - - const queryRefresh = useCallback( - async (newIntent?: string) => { - const response = await queryClient.fetchQuery(['intents/with-examples-count']); - - if (response) { - setIntents(response.response.intents.map((intent) => intentResponseToIntent(intent))); - - const selectedIntent = response.response.intents.find( - (intent) => intent.id === newIntent - ) as IntentWithExamplesCountResponse; - - setSelectedIntent(intentResponseToIntent(selectedIntent)); - } - }, - [queryClient] - ); - - useEffect(() => { - let intentParam = searchParams.get('intent'); - if (!intentParam) return; - - const queryIntent = intents.find((intent) => intent.id === intentParam); - - if (queryIntent) { - setSelectedIntent(queryIntent); - } - }, [intents, searchParams]); + const [refreshing, setRefreshing] = useState(false); + const [filter, setFilter] = useState(''); const handleTabsValueChange = useCallback( (value: string) => { const selectedIntent = intents.find((intent) => intent.id === value); setSelectedIntent(selectedIntent!); }, - [intents] + [intents, setSelectedIntent] ); const newIntentMutation = useMutation({ diff --git a/GUI/src/pages/Training/Intents/useIntentsData.ts b/GUI/src/pages/Training/Intents/useIntentsData.ts new file mode 100644 index 00000000..8552c7fa --- /dev/null +++ b/GUI/src/pages/Training/Intents/useIntentsData.ts @@ -0,0 +1,70 @@ +import { useState, useEffect, useCallback } from 'react'; +import { useSearchParams } from 'react-router-dom'; +import { useQueryClient, useQuery } from '@tanstack/react-query'; +import { IntentWithExamplesCount } from 'types/intentWithExamplesCount'; + +type IntentsWithExamplesCountResponse = { + response: { + intents: IntentWithExamplesCount[]; + }; +}; + +const setIsCommon = (intent: IntentWithExamplesCount): IntentWithExamplesCount => ({ + ...intent, + isCommon: intent.id.startsWith('common_'), +}); + +interface UseIntentsDataProps { + queryKey: string; +} + +export const useIntentsData = ({ queryKey }: UseIntentsDataProps) => { + const queryClient = useQueryClient(); + const [searchParams] = useSearchParams(); + const [intents, setIntents] = useState([]); + const [selectedIntent, setSelectedIntent] = useState(null); + + const { data: intentsResponse, isLoading } = useQuery({ + queryKey: [queryKey], + }); + + useEffect(() => { + if (intentsResponse) setIntents(intentsResponse.response.intents.map((intent) => setIsCommon(intent))); + }, [intentsResponse]); + + const queryRefresh = useCallback( + async (newIntent?: string) => { + const response = await queryClient.fetchQuery([queryKey]); + + if (response) { + const mappedIntents = response.response.intents.map((intent) => setIsCommon(intent)); + setIntents(mappedIntents); + + if (newIntent) { + const selectedIntent = mappedIntents.find((intent) => intent.id === newIntent); + if (selectedIntent) setSelectedIntent(selectedIntent); + } + } + }, + [queryClient, queryKey] + ); + + useEffect(() => { + let intentParam = searchParams.get('intent'); + if (!intentParam) return; + + const queryIntent = intents.find((intent) => intent.id === intentParam); + + if (queryIntent) { + setSelectedIntent(queryIntent); + } + }, [intents, searchParams]); + + return { + intents, + selectedIntent, + setSelectedIntent, + queryRefresh, + isLoading, + }; +}; diff --git a/GUI/src/types/intentWithExamplesCount.ts b/GUI/src/types/intentWithExamplesCount.ts new file mode 100644 index 00000000..bf181bfb --- /dev/null +++ b/GUI/src/types/intentWithExamplesCount.ts @@ -0,0 +1,3 @@ +import { Intent } from './intent'; + +export type IntentWithExamplesCount = Pick; diff --git a/GUI/src/utils/compare.ts b/GUI/src/utils/compare.ts index 0c877b79..e0ea1525 100644 --- a/GUI/src/utils/compare.ts +++ b/GUI/src/utils/compare.ts @@ -1,4 +1,4 @@ -import { IntentWithExamplesCount } from 'types/intentWithExampleCounts'; +import { IntentWithExamplesCount } from 'types/intentWithExamplesCount'; export const compareInModel = (a: IntentWithExamplesCount, b: IntentWithExamplesCount) => { if (a.inModel === b.inModel) return 0;