diff --git a/.vscode/settings.json b/.vscode/settings.json index 7a73a41b..738ac3f1 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,2 +1,5 @@ { -} \ No newline at end of file + "[typescript][typescriptreact][json]": { + "editor.defaultFormatter": "esbenp.prettier-vscode" + } +} diff --git a/DSL/DMapper/hbs/get_intent_by_id.handlebars b/DSL/DMapper/hbs/get_intent_by_id.handlebars new file mode 100644 index 00000000..bdfd5bf7 --- /dev/null +++ b/DSL/DMapper/hbs/get_intent_by_id.handlebars @@ -0,0 +1,12 @@ +{ + "id": "{{intents.intents.0._source.id}}", + "examples": [ + {{#each intents.intents.0._source.examples}} + "{{{this}}}"{{#unless @last}},{{/unless}} + {{/each}} + ], + "inModel": {{isInModel intents.intents.0._source.intent intents}}, + "serviceId": "{{findConnectedServiceId intents.intents.0._source.intent intents}}", + "isForService": "{{getObjectKeyFromObjectArray intents/intentsModifiedAt 'intent' intents.intents.0._source.intent 'isforservice'}}", + "modifiedAt": "{{intents.intentsModifiedAt.0.created}}" +} diff --git a/DSL/DMapper/hbs/get_intent_names_from_example_counts.handlebars b/DSL/DMapper/hbs/get_intent_names_from_example_counts.handlebars new file mode 100644 index 00000000..4469bde2 --- /dev/null +++ b/DSL/DMapper/hbs/get_intent_names_from_example_counts.handlebars @@ -0,0 +1,7 @@ +{ +"intents": [ +{{#each hits}} + "{{key}}"{{#unless @last}},{{/unless}} +{{/each}} +] +} diff --git a/DSL/DMapper/hbs/get_intents_with_examples.handlebars b/DSL/DMapper/hbs/get_intents_with_examples.handlebars index d9396188..f44a888d 100644 --- a/DSL/DMapper/hbs/get_intents_with_examples.handlebars +++ b/DSL/DMapper/hbs/get_intents_with_examples.handlebars @@ -2,7 +2,7 @@ "intents": [ {{#each hits}} { - "id": "{{_source._id}}", + "id": "{{_id}}", "title": "{{_source.intent}}", "examples": [ {{#each _source.examples}} diff --git a/DSL/DMapper/hbs/get_intents_with_examples_count.handlebars b/DSL/DMapper/hbs/get_intents_with_examples_count.handlebars index f6f1316d..fc0e9a62 100644 --- a/DSL/DMapper/hbs/get_intents_with_examples_count.handlebars +++ b/DSL/DMapper/hbs/get_intents_with_examples_count.handlebars @@ -2,8 +2,10 @@ "intents": [ {{#each buckets}} { - "intent": "{{key}}", - "examples_count": {{examples_counts.value}} + "id": "{{key}}", + "examples_count": {{examples_counts.value}}, + "inModel": {{isInModel key ../intents}}, + "modifiedAt": "{{findModifiedAt key ../intents/intentsModifiedAt}}" }{{#unless @last}},{{/unless}} {{/each}} ] diff --git a/DSL/OpenSearch/templates/intents-with-examples-count.json b/DSL/OpenSearch/templates/intents-with-examples-count.json index ebef7f19..b05cb200 100644 --- a/DSL/OpenSearch/templates/intents-with-examples-count.json +++ b/DSL/OpenSearch/templates/intents-with-examples-count.json @@ -6,32 +6,31 @@ "aggs": { "hot": { "terms": { - "field": "intent.keyword" + "field": "intent", + "size": 10000 }, "aggs": { "examples_counts": { "value_count": { - "field": "examples.raw" + "field": "examples" } } } } }, "query": { - "bool": { - "must": [ - { - "query_string": { - "query": "*{{intent}}*", - "default_field": "intent" - } - } - ] + {{#intent}} + "wildcard": { + "intent": "*{{intent}}*" } + {{/intent}} + {{^intent}} + "match_all": {} + {{/intent}} } }, "params": { "intent": "" } } -} +} \ No newline at end of file diff --git a/DSL/Ruuter.private/GET/.guard b/DSL/Ruuter.private/GET/.guard index 18f2da91..b1bde4ee 100644 --- a/DSL/Ruuter.private/GET/.guard +++ b/DSL/Ruuter.private/GET/.guard @@ -9,7 +9,7 @@ process_request: verify_header_nonce: call: http.post args: - url: [#TRAINING_RESQL]/use-nonce + url: "[#TRAINING_RESQL]/use-nonce" body: updated_nonce: ${incoming.headers['x-ruuter-nonce']} result: nonce_response @@ -18,7 +18,7 @@ verify_header_nonce: verify_param_nonce: call: http.post args: - url: [#TRAINING_RESQL]/use-nonce + url: "[#TRAINING_RESQL]/use-nonce" body: updated_nonce: ${incoming.params['ruuter-nonce']} result: nonce_response diff --git a/DSL/Ruuter.private/GET/rasa/intents/by-id.yml b/DSL/Ruuter.private/GET/rasa/intents/by-id.yml new file mode 100644 index 00000000..77a31594 --- /dev/null +++ b/DSL/Ruuter.private/GET/rasa/intents/by-id.yml @@ -0,0 +1,90 @@ +declaration: + call: declare + version: 0.1 + description: "Get intent with details by ID" + method: get + accepts: json + returns: json + namespace: training + allowlist: + headers: + - field: cookie + type: string + description: "Cookie field" + params: + - field: intent + type: string + description: "Intent ID" + +assignValues: + assign: + intent: ${incoming.params.intent} + +getIntent: + call: http.post + args: + url: "[#TRAINING_OPENSEARCH]/intents/_search/template" + body: + id: "intent-with-name" + params: + intent: ${intent} + result: getIntentResult + +getDomainFile: + call: http.get + args: + url: "[#TRAINING_PUBLIC_RUUTER]/internal/domain-file" + headers: + cookie: ${incoming.headers.cookie} + result: getDomainDataResult + +checkIfIntentExists: + switch: + - condition: ${getIntentResult.response.body.hits.hits != null} + next: getServiceIntentConnections + next: returnNoIntentFound + +getServiceIntentConnections: + call: http.post + args: + url: "[#TRAINING_RESQL]/get-service-intent-connections" + result: getServiceIntentConnectionsResult + +# todo if not using full, maybe modify to return one value +getIntentListLastChanged: + call: http.post + args: + url: "[#TRAINING_RESQL]/get-intents-list-last-changed" + body: + intentsList: ${getIntentResult.response.body.hits.hits[0]._id} + result: getIntentListLastChangedResult + +assignResults: + assign: + # TODO: Ideally, should use a single object, not array + intents: + intents: ${getIntentResult.response.body.hits.hits} + inmodel: ${getDomainDataResult.response.body.response.intents} + connections: ${getServiceIntentConnectionsResult.response.body} + intentsModifiedAt: ${getIntentListLastChangedResult.response.body} + +mapIntentData: + call: http.post + args: + url: "[#TRAINING_DMAPPER]/hbs/training/get_intent_by_id" + headers: + type: "json" + body: + intents: ${intents} + result: getIntentDataResult + next: returnSuccess + +returnSuccess: + return: ${getIntentDataResult.response.body} + next: end + +returnNoIntentFound: + return: "Error: no intent found" + wrapper: false + status: 404 + next: end diff --git a/DSL/Ruuter.private/GET/rasa/intents/examples/count.yml b/DSL/Ruuter.private/GET/rasa/intents/examples/count.yml deleted file mode 100644 index 6ac2e1da..00000000 --- a/DSL/Ruuter.private/GET/rasa/intents/examples/count.yml +++ /dev/null @@ -1,33 +0,0 @@ -declaration: - call: declare - version: 0.1 - description: "Decription placeholder for 'COUNT'" - method: get - accepts: json - returns: json - namespace: training - -getIntentsExampleCount: - call: http.post - args: - url: "[#TRAINING_OPENSEARCH]/intents/_search/template" - body: - id: "intents-with-examples-count" - params: - intent: '' - result: getIntentsResult - -mapIntentsData: - call: http.post - args: - url: "[#TRAINING_DMAPPER]/hbs/training/get_intents_with_examples_count" - headers: - type: 'json' - body: - buckets: ${getIntentsResult.response.body.aggregations.hot.buckets} - result: intentsData - next: returnSuccess - -returnSuccess: - return: ${intentsData.response.body} - 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 new file mode 100644 index 00000000..4cb27c3f --- /dev/null +++ b/DSL/Ruuter.private/GET/rasa/intents/with-examples-count.yml @@ -0,0 +1,65 @@ +declaration: + call: declare + version: 0.1 + description: "Decription placeholder for 'COUNT'" + method: get + accepts: json + returns: json + namespace: training + allowlist: + headers: + - field: cookie + type: string + description: "Cookie field" + +getIntentsExampleCount: + call: http.post + args: + url: "[#TRAINING_OPENSEARCH]/intents/_search/template" + body: + id: "intents-with-examples-count" + result: getIntentsResult + +getDomainFile: + call: http.get + args: + url: "[#TRAINING_PUBLIC_RUUTER]/internal/domain-file" + headers: + cookie: ${incoming.headers.cookie} + result: getDomainDataResult + +getIntentsNames: + call: http.post + args: + url: "[#TRAINING_DMAPPER]/hbs/training/get_intent_names_from_example_counts" + headers: + type: "json" + body: + hits: ${getIntentsResult.response.body.aggregations.hot.buckets} + result: getIntentsNamesResult + +getIntentListLastChanged: + call: http.post + args: + url: "[#TRAINING_RESQL]/get-intents-list-last-changed" + body: + intentsList: ${getIntentsNamesResult.response.body.intents} + result: getIntentsListLastChangedResult + +mapIntentsData: + call: http.post + args: + url: "[#TRAINING_DMAPPER]/hbs/training/get_intents_with_examples_count" + headers: + type: "json" + body: + buckets: ${getIntentsResult.response.body.aggregations.hot.buckets} + intents: + inmodel: ${getDomainDataResult.response.body.response.intents} + intentsModifiedAt: ${getIntentsListLastChangedResult.response.body} + result: intentsData + next: returnSuccess + +returnSuccess: + return: ${intentsData.response.body} + next: end diff --git a/DSL/Ruuter.private/POST/rasa/intents/examples/count.yml b/DSL/Ruuter.private/POST/rasa/intents/examples/count.yml deleted file mode 100644 index 2b99f01b..00000000 --- a/DSL/Ruuter.private/POST/rasa/intents/examples/count.yml +++ /dev/null @@ -1,36 +0,0 @@ -declaration: - call: declare - version: 0.1 - description: "Decription placeholder for 'COUNT'" - method: post - accepts: json - returns: json - namespace: training - -assign_values: - assign: - params: ${incoming.params} - -getIntentsExampleCount: - call: http.post - args: - url: "[#TRAINING_OPENSEARCH]/intents/_search/template" - body: - id: "intents-with-examples-count" - params: ${params} - result: getIntentsResult - -mapIntentsData: - call: http.post - args: - url: "[#TRAINING_DMAPPER]/hbs/training/get_intents_with_examples_count" - headers: - type: 'json' - body: - buckets: ${getIntentsResult.response.body.aggregations.hot.buckets} - result: intentsData - next: returnSuccess - -returnSuccess: - return: ${intentsData.response.body} - next: end diff --git a/GUI/src/pages/Training/Intents/CommonIntents.tsx b/GUI/src/pages/Training/Intents/CommonIntents.tsx index 6ee956a4..3fcabc15 100644 --- a/GUI/src/pages/Training/Intents/CommonIntents.tsx +++ b/GUI/src/pages/Training/Intents/CommonIntents.tsx @@ -7,21 +7,11 @@ 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 { 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 { - addExample, addRemoveIntentModel, deleteIntent, downloadExamples, @@ -40,12 +30,8 @@ const CommonIntents: FC = () => { const [searchParams] = useSearchParams(); const [commonIntentsEnabled, setCommonIntentsEnabled] = useState(true); const [selectedIntent, setSelectedIntent] = useState(null); - const [deletableIntent, setDeletableIntent] = useState< - string | number | null - >(null); - const [connectableIntent, setConnectableIntent] = useState( - null - ); + const [deletableIntent, setDeletableIntent] = useState(null); + const [connectableIntent, setConnectableIntent] = useState(null); const [filter, setFilter] = useState(''); const [refreshing, setRefreshing] = useState(false); @@ -82,9 +68,7 @@ const CommonIntents: FC = () => { useEffect(() => { if (!intentParam || intentsFullList?.length !== commonIntents?.length) return; - const queryIntent = commonIntents.find( - (intent) => intent.id === intentParam - ); + const queryIntent = commonIntents.find((intent) => intent.id === intentParam); if (queryIntent) { setSelectedIntent(queryIntent); @@ -126,37 +110,8 @@ const CommonIntents: FC = () => { [commonIntents, queryClient] ); - const addExamplesMutation = useMutation({ - mutationFn: (addExamplesData: { - intentName: string; - intentExamples: string[]; - newExamples: string; - }) => addExample(addExamplesData), - onMutate: () => { - setRefreshing(true); - }, - onSuccess: () => { - toast.open({ - type: 'success', - title: t('global.notification'), - message: t('toast.newExampleAdded'), - }); - }, - onError: (error: AxiosError) => { - toast.open({ - type: 'error', - title: t('global.notificationError'), - message: error.message, - }); - }, - onSettled: () => { - queryRefresh(selectedIntent?.id || ''); - }, - }); - const intentModelMutation = useMutation({ - mutationFn: (intentModelData: { name: string; inModel: boolean }) => - addRemoveIntentModel(intentModelData), + mutationFn: (intentModelData: { name: string; inModel: boolean }) => addRemoveIntentModel(intentModelData), onMutate: () => { setRefreshing(true); }, @@ -213,9 +168,7 @@ const CommonIntents: FC = () => { }); }, onSettled: () => { - commonIntents = commonIntents.filter( - (intent) => intent.id !== selectedIntent?.id - ); + commonIntents = commonIntents.filter((intent) => intent.id !== selectedIntent?.id); setRefreshing(false); }, }); @@ -223,7 +176,8 @@ const CommonIntents: FC = () => { const filteredIntents = useMemo(() => { if (!commonIntents) return []; const formattedFilter = filter.trim().replace(/\s+/g, '_'); - return commonIntents.filter((intent) => intent.id?.includes(formattedFilter)) + return commonIntents + .filter((intent) => intent.id?.includes(formattedFilter)) .sort((a, b) => a.id.localeCompare(b.id)); }, [commonIntents, filter]); @@ -231,18 +185,11 @@ const CommonIntents: FC = () => { mutationFn: (data: { intentName: string }) => getLastModified(data), }); - const examplesData = useMemo( - () => selectedIntent?.examples.map((example, index) => ({id: index, value: example})), - [selectedIntent?.examples] - ); - const handleTabsValueChange = useCallback( (value: string) => { setSelectedIntent(null); if (!commonIntents) return; - const selectedIntent = commonIntents.find( - (intent) => intent.id === value - ); + const selectedIntent = commonIntents.find((intent) => intent.id === value); if (selectedIntent) { queryRefresh(selectedIntent.id || ''); intentModifiedMutation.mutate( @@ -263,18 +210,8 @@ const CommonIntents: FC = () => { [intentModifiedMutation, commonIntents, queryRefresh] ); - const handleNewExample = (example: string) => { - if (!selectedIntent) return; - addExamplesMutation.mutate({ - intentName: selectedIntent.id, - intentExamples: selectedIntent.examples, - newExamples: example, - }); - }; - const intentDownloadMutation = useMutation({ - mutationFn: (intentModelData: { intentName: string }) => - downloadExamples(intentModelData), + mutationFn: (intentModelData: { intentName: string }) => downloadExamples(intentModelData), onSuccess: async (data) => { // @ts-ignore const blob = new Blob([data], { type: 'text/csv' }); @@ -306,7 +243,7 @@ const CommonIntents: FC = () => { type: 'error', title: t('global.notificationError'), message: error.message, - }); + }); } }, }); @@ -338,13 +275,8 @@ const CommonIntents: FC = () => { }; const intentUploadMutation = useMutation({ - mutationFn: ({ - intentName, - formData, - }: { - intentName: string; - formData: File; - }) => uploadExamples(intentName, formData), + mutationFn: ({ intentName, formData }: { intentName: string; formData: File }) => + uploadExamples(intentName, formData), onSuccess: () => { toast.open({ type: 'success', @@ -399,18 +331,13 @@ const CommonIntents: FC = () => { value={selectedIntent?.id ?? undefined} onValueChange={handleTabsValueChange} > - +
setFilter(e.target.value)} hideLabel @@ -420,39 +347,29 @@ const CommonIntents: FC = () => {
{filteredIntents.map((intent, index) => ( - + - - {intent.id.replace(/^common_/, '').replace(/_/g, ' ')} - + {intent.id.replace(/^common_/, '').replace(/_/g, ' ')} - - {intent.examplesCount} - + {intent.examplesCount} {intent.inModel ? ( - - - } - /> - + + + } + /> + ) : ( )} - ))}
+ ))} +
{selectedIntent && ( @@ -471,18 +388,12 @@ const CommonIntents: FC = () => {

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

- )} - + - @@ -551,15 +451,8 @@ const CommonIntents: FC = () => {
- {selectedIntent?.examples && examplesData && ( - + {selectedIntent?.examples && ( + )}
@@ -568,10 +461,7 @@ const CommonIntents: FC = () => { )} {connectableIntent !== null && ( - setConnectableIntent(null)} - /> + setConnectableIntent(null)} /> )} {deletableIntent !== null && ( @@ -580,18 +470,10 @@ const CommonIntents: FC = () => { onClose={() => setDeletableIntent(null)} footer={ <> - - diff --git a/GUI/src/pages/Training/Intents/IntentDetails.tsx b/GUI/src/pages/Training/Intents/IntentDetails.tsx new file mode 100644 index 00000000..962a351d --- /dev/null +++ b/GUI/src/pages/Training/Intents/IntentDetails.tsx @@ -0,0 +1,685 @@ +import { Track, FormInput, Button, Icon, Switch, Tooltip, FormTextarea, Dialog } from 'components'; +import { isHiddenFeaturesEnabled, RESPONSE_TEXT_LENGTH } from 'constants/config'; +import { format } from 'date-fns'; +import { t } from 'i18next'; +import { Dispatch, FC, SetStateAction, useCallback, useEffect, useState } from 'react'; +import { MdOutlineSave, MdOutlineModeEditOutline } from 'react-icons/md'; +import { Intent } from 'types/intent'; +import IntentExamplesTable from './IntentExamplesTable'; +import * as Tabs from '@radix-ui/react-tabs'; +import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; +import { AxiosError } from 'axios'; +import { + addRemoveIntentModel, + deleteIntent, + downloadExamples, + editIntent, + markForService, + uploadExamples, +} from 'services/intents'; +import { useToast } from 'hooks/useToast'; +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 ConnectServiceToIntentModal from 'pages/ConnectServiceToIntentModal'; +import LoadingDialog from 'components/LoadingDialog'; +import useDocumentEscapeListener from 'hooks/useDocumentEscapeListener'; +import { IntentWithExamplesCount } from 'types/intentWithExampleCounts'; + +interface ResponsesResponse + extends Array<{ + name: string; + response: { + name: string; + text: string; + }[]; + }> {} + +interface IntentResponse { + response: Intent; +} + +interface RulesResponse { + response: Rule[]; +} + +interface IntentDetailsProps { + intentId: string; + setSelectedIntent: Dispatch>; + listRefresh: (newIntent?: string) => Promise; +} + +const IntentDetails: FC = ({ intentId, setSelectedIntent, listRefresh }) => { + const [intent, setIntent] = useState(null); + + const [editingIntentTitle, setEditingIntentTitle] = useState(null); + const [refreshing, setRefreshing] = useState(false); + const [showConnectToServiceModal, setShowConnectToServiceModal] = useState(false); + const [showDeleteModal, setShowDeleteModal] = useState(false); + const [intentResponseText, setIntentResponseText] = useState(''); + const [intentResponseName, setIntentResponseName] = useState(''); + const [intentRule, setIntentRule] = useState(''); + + const queryClient = useQueryClient(); + const toast = useToast(); + + const { data: intentResponse } = useQuery({ + queryKey: [`intents/by-id?intent=${intentId}`], + }); + + useEffect(() => { + if (intentResponse) { + setIntent(intentResponse.response); + // Also reset form states on choosing another intent from list + setEditingIntentTitle(null); + setIntentResponseText(''); + } + }, [intentResponse]); + + const { data: isPossibleToUpdateMark, refetch } = useQuery({ + queryKey: [`intents/is-marked-for-service?intent=${intentId}`], + }); + + const setIntentResponse = useCallback( + (responsesResponse: ResponsesResponse | undefined) => { + if (!responsesResponse) return; + + const intentExistingResponse = responsesResponse[0].response.find( + (response: any) => `utter_${intentId}` === response.name + ); + if (intentExistingResponse) { + setIntentResponseText(intentExistingResponse.text); + setIntentResponseName(intentExistingResponse.name); + } + }, + [intentId] + ); + + const addIntentRule = useCallback( + (rulesResponse: RulesResponse | undefined) => { + if (!rulesResponse) return; + + const intentRule = rulesResponse.response.find((rule: Rule) => rule.id === `rule_${intentId}`); + if (intentRule) { + setIntentRule(intentRule.id); + } + }, + [intentId] + ); + + const queryRefresh = useCallback( + async (intent?: string) => { + const intentsResponse = await queryClient.fetchQuery([ + `intents/by-id?intent=${intent ?? intentId}`, + ]); + setIntent(intentsResponse.response); + setSelectedIntent(intentsResponse.response); + + const resonsesResponse = await queryClient.fetchQuery(['responses-list']); + setIntentResponse(resonsesResponse); + + const rulesResponse = await queryClient.fetchQuery(['rules']); + addIntentRule(rulesResponse); + }, + [addIntentRule, intentId, queryClient, setIntentResponse, setSelectedIntent] + ); + + // TODO: need to fetch response for the selected intent only + const { data: responsesResponse } = useQuery({ + queryKey: ['responses-list'], + }); + + useEffect(() => { + setIntentResponse(responsesResponse); + }, [responsesResponse, setIntentResponse]); + + // TODO: need to fetch rules for the selected intent only + const { data: rulesResponse } = useQuery({ + queryKey: ['rules'], + }); + + useEffect(() => { + addIntentRule(rulesResponse); + }, [rulesResponse, addIntentRule]); + + const markIntentServiceMutation = useMutation({ + mutationFn: (data: { name: string; isForService: boolean }) => markForService(data), + onMutate: () => { + setRefreshing(true); + }, + onSuccess: () => { + toast.open({ + type: 'success', + title: t('global.notification'), + message: t('toast.intentUpdated'), + }); + setIntent((prev) => { + if (!prev) return null; + return { ...prev, isForService: !prev.isForService }; + }); + }, + onError: (error: AxiosError) => { + toast.open({ + type: 'error', + title: t('global.notificationError'), + message: error.message, + }); + }, + onSettled: () => { + setRefreshing(false); + }, + }); + + const updateMarkForService = (value: boolean) => { + refetch().then((r) => { + if (!r.data) { + markIntentServiceMutation.mutate({ name: intent?.id ?? '', isForService: value }); + } + }); + }; + + const intentEditMutation = useMutation({ + mutationFn: (editIntentData: { oldName: string; newName: string }) => editIntent(editIntentData), + onMutate: () => { + setRefreshing(true); + }, + onSuccess: async () => { + await queryClient.invalidateQueries(['intents/with-examples-count']); + toast.open({ + type: 'success', + title: t('global.notification'), + message: t('toast.intentTitleSaved'), + }); + }, + onError: (error: AxiosError) => { + toast.open({ + type: 'error', + title: t('global.notificationError'), + message: error.message, + }); + }, + onSettled: () => { + setEditingIntentTitle(null); + setRefreshing(false); + }, + }); + + const editIntentName = async () => { + if (!intent || !editingIntentTitle) return; + + const newName = editingIntentTitle.replace(/\s+/g, '_'); + + await intentEditMutation.mutateAsync({ + oldName: intent.id, + newName, + }); + + queryRefresh(newName); + }; + + const isValidDate = (dateString: string | number | Date) => { + const date = new Date(dateString); + return !isNaN(date.getTime()); + }; + + const serviceEligible = () => { + const roles = useStore.getState().userInfo?.authorities; + if (roles && roles.length > 0) { + return ( + roles?.includes(ROLES.ROLE_ADMINISTRATOR) || + (roles?.includes(ROLES.ROLE_SERVICE_MANAGER) && roles?.includes(ROLES.ROLE_CHATBOT_TRAINER)) + ); + } + return false; + }; + + const intentUploadMutation = useMutation({ + mutationFn: ({ intentName, formData }: { intentName: string; formData: File }) => + uploadExamples(intentName, formData), + onMutate: () => { + setRefreshing(true); + }, + 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, + }); + }, + onSettled: () => { + setRefreshing(false); + queryRefresh(); + }, + }); + + 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: intent?.id || '', + formData: file, + }); + } catch (error) {} + }); + + input.click(); + }; + + const intentDownloadMutation = useMutation({ + mutationFn: (intentModelData: { intentName: string }) => downloadExamples(intentModelData), + onSuccess: async (data) => { + const blob = new Blob([data], { type: 'text/csv' }); + const fileName = intent?.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 intentModelMutation = useMutation({ + mutationFn: (intentModelData: { name: string; inModel: boolean }) => addRemoveIntentModel(intentModelData), + onMutate: () => { + setRefreshing(true); + }, + onSuccess: () => { + if (intent?.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'), + }); + } + }, + onError: (error: AxiosError) => { + toast.open({ + type: 'error', + title: t('global.notificationError'), + message: error.message, + }); + }, + onSettled: () => { + setEditingIntentTitle(null); + setRefreshing(false); + queryRefresh(); + }, + }); + + const addOrEditResponseMutation = useMutation({ + mutationFn: (intentResponseData: { id: string; responseText: string; update: boolean }) => + editResponse(intentResponseData.id, intentResponseData.responseText, intentResponseData.update), + onMutate: () => { + setRefreshing(true); + }, + onSuccess: async () => { + await queryClient.invalidateQueries({ queryKey: ['response-list'], refetchType: 'all' }); + toast.open({ + type: 'success', + title: t('global.notification'), + message: t('toast.newResponseAdded'), + }); + }, + onError: (error: AxiosError) => { + toast.open({ + type: 'error', + title: t('global.notificationError'), + message: error.message, + }); + }, + onSettled: () => { + setRefreshing(false); + }, + }); + + const addRuleMutation = useMutation({ + mutationFn: ({ data }: { data: RuleDTO }) => addStoryOrRule(data as RuleDTO, 'rules'), + onMutate: () => { + setRefreshing(true); + }, + onSuccess: () => { + toast.open({ + type: 'success', + title: t('global.notification'), + message: t('toast.storyAdded'), + }); + }, + onError: (error: AxiosError) => { + toast.open({ + type: 'error', + title: t('global.notificationError'), + message: error.message, + }); + }, + onSettled: () => { + setRefreshing(false); + }, + }); + + const handleIntentResponseSubmit = async () => { + if (intentResponseText === '' || !intent) return; + + const intentId = intent.id; + + addOrEditResponseMutation.mutate({ + id: `utter_${intentId}`, + responseText: intentResponseText, + update: !!intentResponseName, + }); + + if (!intentResponseName) { + addRuleMutation.mutate({ + data: { + rule: `rule_${intentId}`, + steps: [ + { + intent: intentId, + }, + { + action: `utter_${intentId}`, + }, + ], + }, + }); + } + + queryRefresh(); + }; + + const deleteIntentMutation = useMutation({ + mutationFn: (name: string) => deleteIntent({ name }), + onMutate: () => { + setRefreshing(true); + setShowDeleteModal(false); + setShowConnectToServiceModal(false); + }, + onSuccess: async () => { + setSelectedIntent(null); + await queryClient.invalidateQueries(['intents/with-examples-count']); + // Without the delay, back end still returns the deleted intent. Perhaps BE is deleting asynchronously? + setTimeout(() => listRefresh(), 300); + 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, + }); + }, + }); + + const deleteRuleWithIntentMutation = useMutation({ + mutationFn: (id: string | number) => deleteStoryOrRule(id, 'rules'), + onMutate: () => { + setRefreshing(true); + }, + onSuccess: async () => { + await queryClient.invalidateQueries(['response-list']); + await queryClient.invalidateQueries(['rules']); + toast.open({ + type: 'success', + title: t('global.notification'), + message: t('toast.storyDeleted'), + }); + }, + onError: (error: AxiosError) => { + toast.open({ + type: 'error', + title: t('global.notificationError'), + message: error.message, + }); + }, + onSettled: () => { + deleteIntentMutation.mutate(intentId); + setRefreshing(false); + }, + }); + + const handleDeleteIntent = async () => { + if (intentRule) { + await deleteRuleWithIntentMutation.mutateAsync(intentRule); + } else { + await deleteIntentMutation.mutateAsync(intentId); + } + }; + + const updateSelectedIntent = (updatedIntent: Intent) => { + setSelectedIntent(null); + setTimeout(() => setSelectedIntent(updatedIntent), 20); + }; + + useDocumentEscapeListener(() => setEditingIntentTitle(null)); + + if (!intent) return <>Loading...; + + return ( + +
+ + + + {editingIntentTitle ? ( + setEditingIntentTitle(e.target.value)} + hideLabel + /> + ) : ( +

{intent.id.replace(/_/g, ' ')}

+ )} + {editingIntentTitle ? ( + + ) : ( + + )} + +

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

+ + {serviceEligible() && ( + + updateMarkForService(value)} + checked={intent.isForService} + disabled={isPossibleToUpdateMark} + /> + + )} + + + + {intent.inModel ? ( + + ) : ( + + )} + {isHiddenFeaturesEnabled && serviceEligible() && ( + + + + + + )} + + + +
+ +
+ {intent?.examples && ( + +
+ +
+
+ + +

{t('training.intents.responseTitle')}

+ setIntentResponseText(e.target.value)} + disableHeightResize + /> + + + +
+ + )} +
+ + {showDeleteModal && ( + setShowDeleteModal(false)} + footer={ + <> + + + + } + > +

{t('global.removeValidation')}

+
+ )} + + {showConnectToServiceModal && ( + setShowConnectToServiceModal(false)} /> + )} + + {refreshing && ( + +

{t('global.updatingDataBody')}

+
+ )} +
+ ); +}; + +export default IntentDetails; diff --git a/GUI/src/pages/Training/Intents/IntentExamplesTable.tsx b/GUI/src/pages/Training/Intents/IntentExamplesTable.tsx index 377fbce6..c7c98ae4 100644 --- a/GUI/src/pages/Training/Intents/IntentExamplesTable.tsx +++ b/GUI/src/pages/Training/Intents/IntentExamplesTable.tsx @@ -1,43 +1,28 @@ import { FC, useMemo, useRef, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { createColumnHelper } from '@tanstack/react-table'; -import { useMutation, useQueryClient } from '@tanstack/react-query'; +import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; import { AxiosError, HttpStatusCode } from 'axios'; -import { - MdDeleteOutline, - MdOutlineModeEditOutline, - MdOutlineSave, - MdAddCircle, -} from 'react-icons/md'; +import { MdDeleteOutline, MdOutlineModeEditOutline, MdOutlineSave, MdAddCircle } from 'react-icons/md'; import { Button, DataTable, Dialog, FormTextarea, Icon } from 'components'; import useDocumentEscapeListener from 'hooks/useDocumentEscapeListener'; import { INTENT_EXAMPLE_LENGTH } from 'constants/config'; import type { Entity } from 'types/entity'; -import { turnExampleIntoIntent, deleteExample, editExample } from 'services/intents'; +import { turnExampleIntoIntent, deleteExample, editExample, addExample } from 'services/intents'; import { useToast } from 'hooks/useToast'; import IntentExamplesEntry from './IntentExamplesEntry'; import { Intent } from '../../../types/intent'; -import LoadingDialog from "../../../components/LoadingDialog"; +import LoadingDialog from '../../../components/LoadingDialog'; import i18n from '../../../../i18n'; +import { t } from 'i18next'; type IntentExamplesTableProps = { - examples: { id: number, value: string }[]; - onAddNewExample: (example: string) => void; - entities: Entity[]; - selectedIntent: Intent; - queryRefresh: (selectIntent: string) => void; + intent: Intent; updateSelectedIntent: (intent: Intent) => void; }; -const IntentExamplesTable: FC = ({ - examples, - onAddNewExample, - entities, - selectedIntent, - queryRefresh, - updateSelectedIntent -}) => { +const IntentExamplesTable: FC = ({ intent, updateSelectedIntent }) => { let updatedExampleTitle = ''; const { t } = useTranslation(); const toast = useToast(); @@ -52,19 +37,21 @@ const IntentExamplesTable: FC = ({ const [deletableRow, setDeletableRow] = useState<{ intentName: string; value: string; - } | null>( - null - ); + } | null>(null); const [exampleToIntent, setExampleToIntent] = useState<{ - intentName: string, + intentName: string; value: string; } | null>(null); const columnHelper = createColumnHelper<{ id: string; value: string }>(); - const queryClient = useQueryClient(); - const handleRefresh = (selectIntent: string) => { - queryRefresh(selectIntent); - }; + const { data: entitiesResponse } = useQuery<{ response: Entity[] }>({ + queryKey: ['entities'], + }); + + const examples = useMemo( + () => intent?.examples.map((example, index) => ({ id: index, value: example })) ?? [], + [intent?.examples] + ); useDocumentEscapeListener(() => { updatedExampleTitle = ''; @@ -76,24 +63,24 @@ const IntentExamplesTable: FC = ({ }; const updateExampleOnList = (oldExample: string, newExample: string): void => { - const updatedIntent = selectedIntent; + const updatedIntent = intent; updatedIntent.examples[updatedIntent.examples.indexOf(oldExample)] = newExample; updateSelectedIntent(updatedIntent); - } + }; const deleteExampleFromList = (example: string): void => { - const updatedIntent = selectedIntent; + const updatedIntent = intent; const examplesArray = updatedIntent.examples; - const index = examplesArray.findIndex(item => item === example); + const index = examplesArray.findIndex((item) => item === example); examplesArray.splice(index, 1); updatedIntent.examples = examplesArray; updateSelectedIntent(updatedIntent); - } + }; const exampleToIntentMutation = useMutation({ - mutationFn: ({ exampleName }: {intentName: string, exampleName: string} ) => + mutationFn: ({ exampleName }: { intentName: string; exampleName: string }) => turnExampleIntoIntent({ - intentName: selectedIntent.id, + intentName: intent.id, exampleName: exampleName, }), onSuccess: () => { @@ -117,13 +104,10 @@ const IntentExamplesTable: FC = ({ }); const exampleEditMutation = useMutation({ - mutationFn: (addExamplesData: { - intentName: string, - oldExample: string, - newExample: string}) => editExample(addExamplesData), + mutationFn: (editExampleData: { intentName: string; oldExample: string; newExample: string }) => + editExample(editExampleData), onMutate: async () => { - setRefreshing(true) - await queryClient.invalidateQueries(['intents/full']); + setRefreshing(true); }, onSuccess: () => { toast.open({ @@ -143,11 +127,11 @@ const IntentExamplesTable: FC = ({ onSettled: () => { setEditableRow(null); setRefreshing(false); - } + }, }); const exampleDeleteMutation = useMutation({ - mutationFn: (deleteExampleData: { intentName: string, example: string}) => deleteExample(deleteExampleData), + mutationFn: (deleteExampleData: { intentName: string; example: string }) => deleteExample(deleteExampleData), onMutate: () => setRefreshing(true), onSuccess: () => { toast.open({ @@ -155,7 +139,7 @@ const IntentExamplesTable: FC = ({ title: t('global.notification'), message: t('toast.exampleDeleted'), }); - handleRefresh(selectedIntent.id); + updateSelectedIntent(intent); deleteExampleFromList(oldExampleText); }, onError: (error: AxiosError) => { @@ -167,26 +151,67 @@ const IntentExamplesTable: FC = ({ }, onSettled: () => { setDeletableRow(null); - setRefreshing(false) - } + setRefreshing(false); + }, }); + const addExamplesMutation = useMutation({ + mutationFn: (addExamplesData: { intentName: string; intentExamples: string[]; newExamples: string }) => + addExample(addExamplesData), + onMutate: () => { + setRefreshing(true); + }, + onSuccess: () => { + toast.open({ + type: 'success', + title: t('global.notification'), + message: t('toast.newExampleAdded'), + }); + }, + onError: (error: AxiosError) => { + toast.open({ + type: 'error', + title: t('global.notificationError'), + message: error.message, + }); + }, + onSettled: () => { + setRefreshing(false); + updateSelectedIntent(intent); + }, + }); + + const handleNewExample = (example: string) => { + addExamplesMutation.mutate({ + intentName: intent.id, + intentExamples: intent.examples, + newExamples: example.replace(/(\t|\n)+/g, ' ').trim(), + }); + }; + const handleNewExampleSubmit = () => { if (!newExampleRef.current) return; - onAddNewExample(newExampleRef.current.value || ''); + handleNewExample(newExampleRef.current.value || ''); newExampleRef.current.value = ''; setExampleText(''); }; const updateEditingExampleTitle = (newName: string) => { updatedExampleTitle = newName; - } + }; const examplesColumns = useMemo( () => [ columnHelper.accessor('value', { header: t('training.intents.examples') || '', - cell: (props) => buildValueCell(editableRow, updateEditingExampleTitle, entities, props.row.original.id, props.getValue()), + cell: (props) => + buildValueCell( + editableRow, + updateEditingExampleTitle, + entitiesResponse?.response ?? [], + props.row.original.id, + props.getValue() + ), }), columnHelper.display({ header: '', @@ -194,7 +219,7 @@ const IntentExamplesTable: FC = ({ row: { original: { id, value: name }, }, - }) => buildTurnExampleToIntentCell(() => setExampleToIntent({ intentName: id, value: name})), + }) => buildTurnExampleToIntentCell(() => setExampleToIntent({ intentName: id, value: name })), id: 'turnExampleIntoIntent', meta: { size: '1%', @@ -202,24 +227,31 @@ const IntentExamplesTable: FC = ({ }), columnHelper.display({ header: '', - cell: (props) => buildEditCell( - editableRow?.intentName === props.row.original.id, - () => { - if(!editableRow) - return; - setOldExampleText(editableRow.value); - setExampleText(updatedExampleTitle.trim()); + cell: (props) => + buildEditCell( + editableRow?.intentName === props.row.original.id, + () => { + if (!editableRow) return; + setOldExampleText(editableRow.value); + setExampleText(updatedExampleTitle.trim()); + + const updatedTrimmedExample = updatedExampleTitle.trim(); + if (updatedTrimmedExample === '') { + setEditableRow(null); + return; + } exampleEditMutation.mutate({ - intentName: selectedIntent.id, + intentName: intent.id, oldExample: editableRow.value, - newExample: updatedExampleTitle.trim(), + newExample: updatedTrimmedExample, + }); + }, + () => + handleEditableRow({ + intentName: props.row.original.id, + value: props.row.original.value, }) - }, - () => handleEditableRow({ - intentName: props.row.original.id, - value: props.row.original.value - }), - ), + ), id: 'edit', meta: { size: '1%', @@ -227,18 +259,29 @@ const IntentExamplesTable: FC = ({ }), columnHelper.display({ header: '', - cell: (props) => buildDeleteCell(() => setDeletableRow({ - intentName: props.row.original.id, - value: props.row.original.value - })), + cell: (props) => + buildDeleteCell(() => + setDeletableRow({ + intentName: props.row.original.id, + value: props.row.original.value, + }) + ), id: 'delete', meta: { size: '1%', }, }), ], - [columnHelper, t, editableRow, entities, updateEditingExampleTitle, - exampleEditMutation, selectedIntent.id, updatedExampleTitle] + [ + columnHelper, + t, + editableRow, + updateEditingExampleTitle, + entitiesResponse?.response, + updatedExampleTitle, + exampleEditMutation, + intent.id, + ] ); return ( @@ -263,12 +306,8 @@ const IntentExamplesTable: FC = ({ /> - @@ -282,21 +321,18 @@ const IntentExamplesTable: FC = ({ onClose={() => setDeletableRow(null)} footer={ <> - @@ -313,21 +349,17 @@ const IntentExamplesTable: FC = ({ onClose={() => setExampleToIntent(null)} footer={ <> - @@ -338,22 +370,22 @@ const IntentExamplesTable: FC = ({ )} {refreshing && ( - -

{t('global.updatingDataBody')}

-
+ +

{t('global.updatingDataBody')}

+
)} ); }; const buildValueCell = ( - editableRow: { intentName: string; value: string; } | null, + editableRow: { intentName: string; value: string } | null, updateEditingExampleTitle: (newName: string) => void, entities: Entity[], id: string, - value: string, + value: string ): any => { - if(editableRow?.intentName === id) { + if (editableRow?.intentName === id) { return ( updateEditingExampleTitle(e.target.value)} - showMaxLength /> + showMaxLength + /> ); } - return ( - - ); -} + return ; +}; const buildTurnExampleToIntentCell = (onClick: () => void) => { return ( - - ) -} + ); +}; -const buildEditCell = ( - isSave: boolean, - onSaveClick: () => void, - onEditClick: () => void, -) => { - if(isSave) { +const buildEditCell = (isSave: boolean, onSaveClick: () => void, onEditClick: () => void) => { + if (isSave) { return ( - - ) + ); } return ( - - ) -} + + ); +}; const buildDeleteCell = (onClick: () => void) => { return ( - - ) -} + ); +}; export default IntentExamplesTable; diff --git a/GUI/src/pages/Training/Intents/IntentList.tsx b/GUI/src/pages/Training/Intents/IntentList.tsx index 2d624504..5d85cde0 100644 --- a/GUI/src/pages/Training/Intents/IntentList.tsx +++ b/GUI/src/pages/Training/Intents/IntentList.tsx @@ -1,13 +1,13 @@ -import { FC } from "react"; +import { FC } from 'react'; import * as Tabs from '@radix-ui/react-tabs'; import { Icon, Tooltip, Track } from 'components'; import { MdCheckCircleOutline } from 'react-icons/md'; -import { useTranslation } from "react-i18next"; -import { Intent } from "types/intent"; -import "./IntentTabList.scss"; +import { useTranslation } from 'react-i18next'; +import './IntentTabList.scss'; +import { IntentWithExamplesCount } from 'types/intentWithExampleCounts'; interface IntentListProps { - intents: Intent[]; + intents: IntentWithExamplesCount[]; } const IntentList: FC = ({ intents }) => { @@ -16,41 +16,28 @@ const IntentList: FC = ({ intents }) => { return ( <> {intents.map((intent, index) => ( - + - - {intent.id.replace(/_/g, ' ')} - + {intent.id.replace(/_/g, ' ')} - - {intent.examplesCount} - + {intent.examplesCount} - {!intent.inModel - ? - : ( - + {!intent.inModel ? ( + + ) : ( + - } + icon={} /> - - )} + + )} ))} ); -} +}; export default IntentList; diff --git a/GUI/src/pages/Training/Intents/IntentTabList.tsx b/GUI/src/pages/Training/Intents/IntentTabList.tsx index 137494d2..bdbbd3d4 100644 --- a/GUI/src/pages/Training/Intents/IntentTabList.tsx +++ b/GUI/src/pages/Training/Intents/IntentTabList.tsx @@ -1,14 +1,14 @@ -import { FC, useMemo, useState } from "react"; +import { FC, useMemo, useState } from 'react'; import { SwitchBox } from 'components'; -import { useTranslation } from "react-i18next"; -import { Intent } from "types/intent"; -import { compareInModel, compareInModelReversed } from "utils/compare"; -import IntentList from "./IntentList"; -import "./IntentTabList.scss"; +import { useTranslation } from 'react-i18next'; +import { compareInModel, compareInModelReversed } from 'utils/compare'; +import IntentList from './IntentList'; +import './IntentTabList.scss'; +import { IntentWithExamplesCount } from 'types/intentWithExampleCounts'; interface IntentTabListProps { filter: string; - intents: Intent[]; + intents: IntentWithExamplesCount[]; onDismiss: () => void; } @@ -45,26 +45,23 @@ const IntentTabList: FC = ({ filter, intents, onDismiss }) = return (
-
-
- +
+
+ { onDismiss(); setShowCommons(!showCommons); - }} + }} />
-
- - setOrder(e.target.value)}> @@ -79,7 +76,7 @@ const IntentTabList: FC = ({ filter, intents, onDismiss }) = {showCommons &&
} {showCommons && }
- ) -} + ); +}; export default IntentTabList; diff --git a/GUI/src/pages/Training/Intents/index.tsx b/GUI/src/pages/Training/Intents/index.tsx index e2b20415..40d7976b 100644 --- a/GUI/src/pages/Training/Intents/index.tsx +++ b/GUI/src/pages/Training/Intents/index.tsx @@ -1,286 +1,90 @@ -import { FC, useCallback, useEffect, useMemo, useState } from 'react'; +import { FC, useCallback, useEffect, useState } from 'react'; import { useSearchParams } from 'react-router-dom'; import { useTranslation } from 'react-i18next'; import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; import * as Tabs from '@radix-ui/react-tabs'; -import { format } from 'date-fns'; import { AxiosError } from 'axios'; -import {MdOutlineModeEditOutline, MdOutlineSave,} from 'react-icons/md'; -import {Button, Dialog, FormInput, FormTextarea, Icon, Switch, Tooltip, Track} from 'components'; -import useDocumentEscapeListener from 'hooks/useDocumentEscapeListener'; +import { Button, Dialog, FormInput, Track } from 'components'; import { useToast } from 'hooks/useToast'; import { Intent } from 'types/intent'; -import { Entity } from 'types/entity'; -import { - addExample, - addIntent, - addRemoveIntentModel, - deleteIntent, - downloadExamples, - editIntent, - getLastModified, markForService, - turnIntentIntoService, - uploadExamples, -} from 'services/intents'; -import IntentExamplesTable from './IntentExamplesTable'; +import { addIntent, turnIntentIntoService } from 'services/intents'; import LoadingDialog from '../../../components/LoadingDialog'; -import ConnectServiceToIntentModal from 'pages/ConnectServiceToIntentModal'; import withAuthorization, { ROLES } from 'hoc/with-authorization'; -import { isHiddenFeaturesEnabled, RESPONSE_TEXT_LENGTH } from 'constants/config'; -import { deleteResponse, editResponse } from '../../../services/responses'; -import { Rule, RuleDTO } from '../../../types/rule'; -import { addStoryOrRule, deleteStoryOrRule } from '../../../services/stories'; import IntentTabList from './IntentTabList'; -import useStore from "../../../store/store"; +import IntentDetails from './IntentDetails'; +import { IntentWithExamplesCount } from 'types/intentWithExampleCounts'; -type Response = { - name: string; - text: string; -} +// 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_'), +}); const Intents: FC = () => { const { t } = useTranslation(); const queryClient = useQueryClient(); const toast = useToast(); - - const [intentResponseName, setIntentResponseName] = useState(''); - const [intentResponseText, setIntentResponseText] = useState(''); - const [searchParams] = useSearchParams(); - const [filter, setFilter] = useState(''); - const [isMarkedForService, setIsMarkedForService] = useState(false); + const [intents, setIntents] = useState([]); + const [selectedIntent, setSelectedIntent] = useState(null); const [refreshing, setRefreshing] = useState(false); + const [filter, setFilter] = useState(''); + const [turnIntentToServiceIntent, setTurnIntentToServiceIntent] = useState(null); - const [editingIntentTitle, setEditingIntentTitle] = useState(null); - const [selectedIntent, setSelectedIntent] = useState(null); - const [intentRule, setIntentRule] = useState(''); - - const [deletableIntent, setDeletableIntent] = useState(null); - const [connectableIntent, setConnectableIntent] = useState( - null - ); - const [turnIntentToServiceIntent, setTurnIntentToServiceIntent] = - useState(null); - - let intentParam; - - const updateMarkForService = (value: boolean) => { - refetch().then(r => { - if(!r.data) { - markIntentServiceMutation.mutate({ name: selectedIntent?.id ?? '', isForService: value }) - } - }); - } - - const { data: isPossibleToUpdateMark, refetch } = useQuery({ queryKey: [`intents/is-marked-for-service?intent=${selectedIntent?.id}`]}) - - const serviceEligable = () => { - const roles = useStore.getState().userInfo?.authorities; - if(roles && roles.length > 0) { - return roles?.includes(ROLES.ROLE_ADMINISTRATOR) || (roles?.includes(ROLES.ROLE_SERVICE_MANAGER) && roles?.includes(ROLES.ROLE_CHATBOT_TRAINER)) - } - return false; - } - - const { - data: intentsFullResponse, - isLoading, - } = useQuery({ - queryKey: ['intents/full'], - }); - - const { data: entities } = useQuery({ - queryKey: ['entities'], - }); - - const { data: responsesFullResponse } = useQuery({ - queryKey: ['responses-list'], + const { data: intentsResponse, isLoading } = useQuery({ + queryKey: ['intents/with-examples-count'], }); - const { data: rulesFullResponse } = useQuery({ - queryKey: ['rules'], - }) - - let intentsFullList = intentsFullResponse?.response?.intents; - let intents: Intent[] = []; - - let intentResponsesFullList = responsesFullResponse ? responsesFullResponse[0].response : null; - let intentResponses: Response[] = []; - - let rulesFullList = rulesFullResponse?.response; - let rules: Rule[] = []; - - if (intentsFullList) { - intentsFullList.forEach((intent: any) => { - const countExamples = intent.examples.length; - const newIntent: Intent = { - id: intent.title, - description: null, - inModel: intent.inmodel, - modifiedAt: intent.modifiedAt, - examplesCount: countExamples, - examples: intent.examples, - serviceId: intent.serviceId, - isCommon: intent.title.startsWith('common_'), - }; - intents.push(newIntent); - }); - intentParam = searchParams.get('intent'); - } - - if (intentResponsesFullList) { - intentResponsesFullList?.forEach((response: any) => { - const newIntentResponse: Response = { - name: response.name, - text: response.text, - } - intentResponses.push(newIntentResponse); - }); - } - - if (rulesFullList) { - rulesFullList.forEach((rule: any) => { - rules.push(rule); - }); - } - useEffect(() => { - if (!intentParam || intentsFullList?.length !== intents?.length) return; - - const queryIntent = intents.find( - (intent) => intent.id === intentParam - ); - - if (queryIntent) { - setSelectedIntent(queryIntent); + if (intentsResponse) { + setIntents(intentsResponse.response.intents.map((intent) => intentResponseToIntent(intent))); } - }, [intentParam]); + }, [intentsResponse]); const queryRefresh = useCallback( - function queryRefresh(selectIntent: string | null) { - setSelectedIntent(null); - setIntentResponseName(null); - setIntentResponseText(null); - setIntentRule(null); - - queryClient.fetchQuery(['intents/full']).then((res: any) => { - setRefreshing(false); + async (newIntent?: string) => { + const response = await queryClient.fetchQuery(['intents/with-examples-count']); - if (intents.length > 0) { - const newSelectedIntent = res.response.intents.find((intent: any) => intent.title === selectIntent) || null; - if (newSelectedIntent) { - setSelectedIntent({ - id: newSelectedIntent.title, - description: null, - inModel: newSelectedIntent.inmodel, - modifiedAt: newSelectedIntent.modifiedAt, - examplesCount: newSelectedIntent.examples.length, - examples: newSelectedIntent.examples, - serviceId: newSelectedIntent.serviceId, - isForService: newSelectedIntent.isForService - }); - setIsMarkedForService(newSelectedIntent.isForService ? newSelectedIntent.isForService : false); + if (response) { + setIntents(response.response.intents.map((intent) => intentResponseToIntent(intent))); - queryClient.fetchQuery(['responses-list']).then((res: any) => { - if (intentResponses.length > 0) { - const intentExistingResponse = res[0].response.find((response: any) => `utter_${newSelectedIntent.title}` === response.name); - if (intentExistingResponse) { - setIntentResponseText(intentExistingResponse.text); - setIntentResponseName(intentExistingResponse.name); - } - } - }) + const selectedIntent = response.response.intents.find( + (intent) => intent.id === newIntent + ) as IntentWithExamplesCountResponse; - queryClient.fetchQuery(['rules']).then((res: any) => { - if (rules.length > 0) { - const intentExistingRule = res.response.find((rule: any) => rule.id === `rule_${newSelectedIntent.title}`) - if (intentExistingRule) { - setIntentRule(intentExistingRule.id); - } - } - }) - } - } - }); + setSelectedIntent(intentResponseToIntent(selectedIntent)); + } }, - [intents, intentResponses, rules] + [queryClient] ); - 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 addExamplesMutation = useMutation({ - mutationFn: (addExamplesData: { - intentName: string; - intentExamples: string[]; - newExamples: string; - }) => addExample(addExamplesData), - onMutate: () => { - setRefreshing(true); - }, - onSuccess: () => { - toast.open({ - type: 'success', - title: t('global.notification'), - message: t('toast.newExampleAdded'), - }); - }, - onError: (error: AxiosError) => { - toast.open({ - type: 'error', - title: t('global.notificationError'), - message: error.message, - }); - }, - onSettled: () => { - queryRefresh(selectedIntent!.id); - }, - }); + useEffect(() => { + let intentParam = searchParams.get('intent'); + if (!intentParam) return; - const deleteIntentMutation = useMutation({ - mutationFn: (name: string) => deleteIntent({ name }), - onMutate: () => { - setRefreshing(true); - setDeletableIntent(null); - setConnectableIntent(null); - setSelectedIntent(null); - }, - onSuccess: async () => { - await queryClient.invalidateQueries(['intents/full']); - 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: () => { - queryRefresh(''); - }, - }); + const queryIntent = intents.find((intent) => intent.id === intentParam); - const intentModifiedMutation = useMutation({ - mutationFn: (data: { intentName: string }) => getLastModified(data), - }); + if (queryIntent) { + setSelectedIntent(queryIntent); + } + }, [intents, searchParams]); + // TODO: This is not used at all at the moment + // TODO: If this is needed at some point, errors should be fixed + // TODO: Possibly relevant https://github.com/buerokratt/Training-Module/pull/663 const turnIntentIntoServiceMutation = useMutation({ - mutationFn: ({ intent }: { intent: Intent }) => - turnIntentIntoService(intent), + mutationFn: ({ intent }: { intent: Intent }) => turnIntentIntoService(intent), onMutate: () => { setRefreshing(true); }, @@ -305,41 +109,12 @@ const Intents: FC = () => { }, }); - useDocumentEscapeListener(() => setEditingIntentTitle(null)); - - - const examplesData = useMemo( - () => selectedIntent?.examples.map((example, index) => ({ id: index, value: example })), - [selectedIntent?.examples] - ); - const handleTabsValueChange = useCallback( (value: string) => { - setEditingIntentTitle(null); - setSelectedIntent(null); - setIntentResponseName(null); - setIntentResponseText(null); - - if (!intents) return; const selectedIntent = intents.find((intent) => intent.id === value); - if (selectedIntent) { - queryRefresh(selectedIntent?.id || ''); - intentModifiedMutation.mutate( - { intentName: selectedIntent.id }, - { - onSuccess: (data) => { - selectedIntent.modifiedAt = data.response; - setSelectedIntent(selectedIntent); - }, - onError: (error) => { - selectedIntent.modifiedAt = ''; - setSelectedIntent(selectedIntent); - }, - } - ); - } + setSelectedIntent(selectedIntent!); }, - [intentModifiedMutation, intents, queryRefresh] + [intents] ); const newIntentMutation = useMutation({ @@ -363,389 +138,11 @@ const Intents: FC = () => { }, onSettled: () => { setRefreshing(false); - queryRefresh(filter.trim().replace(/\s+/g, '_')) + queryRefresh(filter.trim().replace(/\s+/g, '_')); setFilter(''); }, }); - const intentEditMutation = useMutation({ - mutationFn: (editIntentData: { oldName: string; newName: string }) => - editIntent(editIntentData), - onMutate: () => { - setRefreshing(true); - }, - onSuccess: async () => { - await queryClient.invalidateQueries(['intents/full']); - toast.open({ - type: 'success', - title: t('global.notification'), - message: t('toast.intentTitleSaved'), - }); - }, - onError: (error: AxiosError) => { - toast.open({ - type: 'error', - title: t('global.notificationError'), - message: error.message, - }); - }, - onSettled: () => { - setEditingIntentTitle(null); - setRefreshing(false); - }, - }); - - const markIntentServiceMutation = useMutation({ - mutationFn: (data: { name: string, isForService: boolean }) => markForService(data), - onMutate: () => { - setRefreshing(true); - }, - onSuccess: () => { - toast.open({ - type: 'success', - title: t('global.notification'), - message: t('toast.intentUpdated'), - }); - setIsMarkedForService(!isMarkedForService); - }, - onError: (error: AxiosError) => { - toast.open({ - type: 'error', - title: t('global.notificationError'), - message: error.message, - }); - }, - onSettled: () => { - setRefreshing(false); - }, - }); - - const intentModelMutation = useMutation({ - mutationFn: (intentModelData: { name: string; inModel: boolean }) => - addRemoveIntentModel(intentModelData), - onMutate: () => { - setRefreshing(true); - }, - onSuccess: () => { - 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'), - }); - } - }, - onError: (error: AxiosError) => { - toast.open({ - type: 'error', - title: t('global.notificationError'), - message: error.message, - }); - }, - onSettled: () => { - setEditingIntentTitle(null); - setRefreshing(false); - queryRefresh(selectedIntent?.id || ''); - }, - }); - - 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 intentUploadMutation = useMutation({ - mutationFn: ({ - intentName, - formData, - }: { - intentName: string; - formData: File; - }) => uploadExamples(intentName, formData), - onMutate: () => { - setRefreshing(true); - }, - 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, - }); - }, - onSettled: () => { - setRefreshing(false); - queryRefresh(selectedIntent?.id || ''); - }, - }); - - const handleNewExample = (example: string) => { - if (!selectedIntent) return; - addExamplesMutation.mutate({ - intentName: selectedIntent.id, - intentExamples: selectedIntent.examples, - newExamples: example.replace(/(\t|\n)+/g, ' ').trim(), - }); - }; - - 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 addOrEditResponseMutation = useMutation({ - mutationFn: (intentResponseData: { - id: string, - responseText: string, - update: boolean - }) => editResponse(intentResponseData.id, intentResponseData.responseText, intentResponseData.update), - onMutate: () => { - setRefreshing(true); - }, - onSuccess: async () => { - await queryClient.invalidateQueries(['response-list']); - toast.open({ - type: 'success', - title: t('global.notification'), - message: t('toast.newResponseAdded'), - }); - }, - onError: (error: AxiosError) => { - toast.open({ - type: 'error', - title: t('global.notificationError'), - message: error.message, - }); - }, - onSettled: () => { - setRefreshing(false); - }, - }); - - const deleteResponseMutation = useMutation({ - mutationFn: (response: string) => deleteResponse({ response }), - onMutate: () => { - setRefreshing(true); - }, - onSuccess: async () => { - await queryClient.invalidateQueries(['response-list']); - toast.open({ - type: 'success', - title: t('global.notification'), - message: t('toast.responseDeleted'), - }); - }, - onError: (error: AxiosError) => { - toast.open({ - type: 'error', - title: t('global.notificationError'), - message: error.message, - }); - }, - onSettled: () => { - setRefreshing(false); - } - }) - - const addRuleMutation = useMutation({ - mutationFn: ({ data }: {data: RuleDTO}) => addStoryOrRule(data as RuleDTO, "rules"), - onMutate: () => { - setRefreshing(true); - }, - onSuccess: () => { - toast.open({ - type: 'success', - title: t('global.notification'), - message: t('toast.storyAdded'), - }); - }, - onError: (error: AxiosError) => { - toast.open({ - type: 'error', - title: t('global.notificationError'), - message: error.message, - }); - }, - onSettled: () => { - setRefreshing(false); - }, - }); - - const deleteRuleMutation = useMutation({ - mutationFn: (id: string | number) => deleteStoryOrRule(id, 'rules'), - onMutate: () => { - setRefreshing(true); - }, - onSuccess: async () => { - await queryClient.invalidateQueries(['rules']); - toast.open({ - type: 'success', - title: t('global.notification'), - message: t('toast.storyDeleted'), - }); - }, - onError: (error: AxiosError) => { - toast.open({ - type: 'error', - title: t('global.notificationError'), - message: error.message, - }); - }, - onSettled: () => { - setRefreshing(false); - }, - }); - - const deleteRuleWithIntentMutation = useMutation({ - mutationFn: (id: string | number) => deleteStoryOrRule(id, 'rules'), - onMutate: () => { - setRefreshing(true); - }, - onSuccess: async () => { - await queryClient.invalidateQueries(['response-list']); - await queryClient.invalidateQueries(['rules']); - toast.open({ - type: 'success', - title: t('global.notification'), - message: t('toast.storyDeleted'), - }); - }, - onError: (error: AxiosError) => { - toast.open({ - type: 'error', - title: t('global.notificationError'), - message: error.message, - }); - }, - onSettled: () => { - deleteIntentMutation.mutate(deletableIntent!.id); - setRefreshing(false); - }, - }); - - const editIntentName = async () => { - if (!selectedIntent || !editingIntentTitle) return; - - const newName = editingIntentTitle.replace(/\s+/g, '_'); - - await intentEditMutation.mutateAsync({ - oldName: selectedIntent.id, - newName, - }); - queryRefresh(newName); - } - - const handleDeleteIntent = async () => { - if (intentRule) { - await deleteRuleWithIntentMutation.mutateAsync(intentRule); - } else { - await deleteIntentMutation.mutateAsync(deletableIntent!.id) - } - } - - const handleIntentResponseSubmit = async (newId?: string) => { - if (!intentResponseText || intentResponseText == '' || !selectedIntent) return; - - const intentId = newId || selectedIntent.id; - - await addOrEditResponseMutation.mutate({ - id: `utter_${intentId}`, - responseText: intentResponseText, - update: !!intentResponseName - }); - - if (!intentResponseName) { - await addRuleMutation.mutate({ - data: { - rule: `rule_${intentId}`, - steps: [ - { - intent: intentId, - }, - { - action: `utter_${intentId}`, - } - ] - } - }); - } - - if (editingIntentTitle) { - await intentEditMutation.mutateAsync({ - oldName: selectedIntent.id, - newName: newId, - }); - queryRefresh(intentId); - } - } - if (isLoading) return <>Loading...; return ( @@ -759,28 +156,18 @@ const Intents: FC = () => { value={selectedIntent?.id || undefined} onValueChange={handleTabsValueChange} > - +
setFilter(e.target.value)} hideLabel /> - @@ -790,243 +177,29 @@ const Intents: FC = () => { intents={intents} filter={filter} onDismiss={() => { - if(!selectedIntent?.isCommon) return; + if (!selectedIntent?.isCommon) return; setSelectedIntent(null); - setEditingIntentTitle(null); - setIntentResponseName(null); - setIntentResponseText(null); }} /> {selectedIntent && ( - -
- - - - {editingIntentTitle ? ( - - setEditingIntentTitle(e.target.value) - } - hideLabel - /> - ) : ( -

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

- )} - {editingIntentTitle ? ( - - ) : ( - - )} - -

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

- - {serviceEligable() && ( - updateMarkForService(value)} - checked={isMarkedForService} - disabled={isPossibleToUpdateMark} - /> - - )} - - - - {selectedIntent.inModel ? ( - - ) : ( - - )} - { - isHiddenFeaturesEnabled && serviceEligable() && ( - - - - - - ) - } - - - -
-
- {selectedIntent?.examples && ( - -
- -
-
- - -

{t('training.intents.responseTitle')}

- setIntentResponseText(e.target.value)} - disableHeightResize - /> - - - -
- - )} -
-
+ )} )} - {deletableIntent !== null && ( - setDeletableIntent(null)} - footer={ - <> - - - - } - > -

{t('global.removeValidation')}

-
- )} - - {connectableIntent !== null && ( - setConnectableIntent(null)} - /> - )} {turnIntentToServiceIntent !== null && ( setTurnIntentToServiceIntent(null)} footer={ <> - )} - {(refreshing) && ( + {refreshing && (

{t('global.updatingDataBody')}

diff --git a/GUI/src/services/intents.ts b/GUI/src/services/intents.ts index b439457d..43e5eb1e 100644 --- a/GUI/src/services/intents.ts +++ b/GUI/src/services/intents.ts @@ -6,17 +6,19 @@ export async function addIntent(newIntentData: { name: string }) { return data; } -export async function markForService(markData: { name: string, isForService: boolean }) { - const { data } = await rasaApi.get(`/intents/mark-for-service?name=${markData.name}&isForService=${markData.isForService}`); +export async function markForService(markData: { name: string; isForService: boolean }) { + const { data } = await rasaApi.get( + `/intents/mark-for-service?name=${markData.name}&isForService=${markData.isForService}` + ); return data; } -export async function addIntentWithExample(newIntentExample: { intentName: string,newExamples: string }) { +export async function addIntentWithExample(newIntentExample: { intentName: string; newExamples: string }) { const { data } = await rasaApi.post('/intents/add-with-example', newIntentExample); return data; } -export async function editIntent(editIntentData: {oldName: string, newName: string}) { +export async function editIntent(editIntentData: { oldName: string; newName: string }) { const { data } = await rasaApi.post(`intents/update`, editIntentData); return data; } @@ -26,39 +28,49 @@ export async function deleteIntent(deleteIntentData: { name: string }) { return data; } -export async function addRemoveIntentModel(intentModelData: {name: string, inModel: boolean}) { +export async function addRemoveIntentModel(intentModelData: { name: string; inModel: boolean }) { const { data } = await rasaApi.post(`intents/add-remove-from-model`, intentModelData); return data; } -export async function getLastModified(intentModifiedData: {intentName: string}) { +export async function getLastModified(intentModifiedData: { intentName: string }) { const { data } = await rasaApi.post(`intents/last-modified`, intentModifiedData); return data; } -export async function addExample(addExampleData: { intentName: string, intentExamples: string[], newExamples: string }) { - const { data } = await rasaApi.post<{ intentName: string; example: string; }>(`intents/examples/add`, addExampleData); +export async function addExample(addExampleData: { + intentName: string; + intentExamples: string[]; + newExamples: string; +}) { + const { data } = await rasaApi.post<{ intentName: string; example: string }>(`intents/examples/add`, addExampleData); return data; } export async function addExampleFromHistory(intentName: string, exampleData: { example: string }) { const request = { intentName: intentName, intentExamples: [], newExamples: exampleData.example }; - const {data} = await rasaApi.post<{ intentName: string; example: string; }>(`intents/examples/add`, request); + const { data } = await rasaApi.post<{ intentName: string; example: string }>(`intents/examples/add`, request); return data; } -export async function editExample(editExampleData: { intentName: string, oldExample: string, newExample: string }) { - const { data } = await rasaApi.post<{ intentName: string; example: string; }>(`intents/examples/update`, editExampleData); +export async function editExample(editExampleData: { intentName: string; oldExample: string; newExample: string }) { + const { data } = await rasaApi.post<{ intentName: string; example: string }>( + `intents/examples/update`, + editExampleData + ); return data; } -export async function deleteExample(deleteExampleData: { intentName: string, example: string }) { - const { data } = await rasaApi.post<{ intentName: string; example: string; }>(`intents/examples/delete`, deleteExampleData); +export async function deleteExample(deleteExampleData: { intentName: string; example: string }) { + const { data } = await rasaApi.post<{ intentName: string; example: string }>( + `intents/examples/delete`, + deleteExampleData + ); return data; } export async function downloadExamples(downloadExampleData: { intentName: string }) { - const { data } = await rasaApi.post<{ intentName: string; }>(`intents/download`, downloadExampleData); + const { data } = await rasaApi.post(`intents/download`, downloadExampleData); return data; } @@ -70,24 +82,15 @@ export async function uploadExamples(intentName: string, formData: File) { return data; } - -export async function turnExampleIntoIntent(data: { - exampleName: string; - intentName: string; -}): Promise { +export async function turnExampleIntoIntent(data: { exampleName: string; intentName: string }): Promise { await rasaApi.post('intents/add', { intent: data.exampleName, }); - await rasaApi.post( - 'intents/examples/delete', - { intent: data.intentName, example: data.exampleName } - ); + await rasaApi.post('intents/examples/delete', { intent: data.intentName, example: data.exampleName }); } -export async function turnIntentIntoService( - intent: Intent -): Promise { +export async function turnIntentIntoService(intent: Intent): Promise { await rasaApi.post('intents/turn-into-service', { - intentName: intent.id + intentName: intent.id, }); } diff --git a/GUI/src/types/intentWithExampleCounts.ts b/GUI/src/types/intentWithExampleCounts.ts new file mode 100644 index 00000000..bf181bfb --- /dev/null +++ b/GUI/src/types/intentWithExampleCounts.ts @@ -0,0 +1,3 @@ +import { Intent } from './intent'; + +export type IntentWithExamplesCount = Pick; diff --git a/GUI/src/types/rule.ts b/GUI/src/types/rule.ts index 84881188..896a9270 100644 --- a/GUI/src/types/rule.ts +++ b/GUI/src/types/rule.ts @@ -1,6 +1,21 @@ +type RuleStep = { + intent?: string; + action?: string; + active_loop?: string | null; + slot_was_set?: + | Array> + | { + requested_slot: string | null; + slot?: string; + }; + entities?: Array<{ + [key: string]: string; + }>; +}; + export interface Rule { id: string; - steps: string | string[]; + steps: RuleStep | RuleStep[]; conversation_start?: string; wait_for_user_input?: string; } diff --git a/GUI/src/utils/compare.ts b/GUI/src/utils/compare.ts index 1e049812..0c877b79 100644 --- a/GUI/src/utils/compare.ts +++ b/GUI/src/utils/compare.ts @@ -1,11 +1,11 @@ -import { Intent } from "types/intent"; +import { IntentWithExamplesCount } from 'types/intentWithExampleCounts'; -export const compareInModel= (a: Intent, b: Intent) => { - if(a.inModel === b.inModel) return 0; - if(a.inModel) return -1; +export const compareInModel = (a: IntentWithExamplesCount, b: IntentWithExamplesCount) => { + if (a.inModel === b.inModel) return 0; + if (a.inModel) return -1; return 1; -} +}; -export const compareInModelReversed= (a: Intent, b: Intent) => { +export const compareInModelReversed = (a: IntentWithExamplesCount, b: IntentWithExamplesCount) => { return -1 * compareInModel(a, b); -} +}; diff --git a/docker-compose.yml b/docker-compose.yml index 2a889af3..bc87a5b6 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -249,6 +249,19 @@ services: networks: - bykstack + opensearch-dashboards: + image: opensearchproject/opensearch-dashboards:2.11.1 + container_name: opensearch-dashboards + ports: + - 5601:5601 + expose: + - "5601" + environment: + OPENSEARCH_HOSTS: '["http://opensearch:9200"]' + DISABLE_SECURITY_DASHBOARDS_PLUGIN: true + networks: + - bykstack + rasa: image: rasa container_name: rasa