diff --git a/frontend/public/locales/en/translation.json b/frontend/public/locales/en/translation.json index 259cd6a..26366b6 100644 --- a/frontend/public/locales/en/translation.json +++ b/frontend/public/locales/en/translation.json @@ -456,7 +456,8 @@ "is_rtl": "RTL", "original_text": "Original Text", "inputs": "Inputs", - "outputs": "Outputs" + "outputs": "Outputs", + "any": "- Any -" }, "placeholder": { "your_username": "Your username", diff --git a/frontend/public/locales/fr/translation.json b/frontend/public/locales/fr/translation.json index 8ad387c..cffef81 100644 --- a/frontend/public/locales/fr/translation.json +++ b/frontend/public/locales/fr/translation.json @@ -456,8 +456,9 @@ "is_default": "Par Défaut", "is_rtl": "RTL", "original_text": "Texte par défaut", - "inputs": "Port d'entré", - "outputs": "Port de sorti" + "inputs": "Ports d'entrée", + "outputs": "Ports de sortie", + "any": "- Toutes -" }, "placeholder": { "your_username": "Votre nom d'utilisateur", diff --git a/frontend/src/app-components/inputs/NlpPatternSelect.tsx b/frontend/src/app-components/inputs/NlpPatternSelect.tsx new file mode 100644 index 0000000..63b193b --- /dev/null +++ b/frontend/src/app-components/inputs/NlpPatternSelect.tsx @@ -0,0 +1,319 @@ +/* + * Copyright © 2024 Hexastack. All rights reserved. + * + * Licensed under the GNU Affero General Public License v3.0 (AGPLv3) with the following additional terms: + * 1. The name "Hexabot" is a trademark of Hexastack. You may not use this name in derivative works without express written permission. + * 2. All derivative works must include clear attribution to the original creator and software, Hexastack and Hexabot, in a prominent location (e.g., in the software's "About" section, documentation, and README file). + */ + +import { Cancel } from "@mui/icons-material"; +import { + Box, + Chip, + CircularProgress, + IconButton, + InputAdornment, + Skeleton, + Typography, + useTheme, +} from "@mui/material"; +import Autocomplete from "@mui/material/Autocomplete"; +import { forwardRef, SyntheticEvent, useRef } from "react"; + +import { Input } from "@/app-components/inputs/Input"; +import { useFind } from "@/hooks/crud/useFind"; +import { useGetFromCache } from "@/hooks/crud/useGet"; +import { useSearch } from "@/hooks/useSearch"; +import { useTranslate } from "@/hooks/useTranslate"; +import { EntityType, Format } from "@/services/types"; +import { NlpPattern } from "@/types/block.types"; +import { INlpEntity } from "@/types/nlp-entity.types"; +import { INlpValue } from "@/types/nlp-value.types"; + +type NlpPatternSelectProps = { + patterns: NlpPattern[]; + onChange: (patterns: NlpPattern[]) => void; +}; + +const NlpPatternSelect = ( + { patterns, onChange }: NlpPatternSelectProps, + ref, +) => { + const inputRef = useRef(null); + const theme = useTheme(); + const { t } = useTranslate(); + const { searchPayload } = useSearch({ + $iLike: ["name"], + }); + const { data: options, isLoading } = useFind( + { entity: EntityType.NLP_ENTITY, format: Format.FULL }, + { hasCount: false, params: searchPayload }, + ); + const getNlpValueFromCache = useGetFromCache(EntityType.NLP_VALUE); + const handleNlpEntityChange = ( + _event: SyntheticEvent, + entities: INlpEntity[], + ): void => { + const intersection = patterns.filter(({ entity: entityName }) => + entities.find(({ name }) => name === entityName), + ); + const additions = entities.filter( + ({ name }) => + !patterns.find(({ entity: entityName }) => name === entityName), + ); + const newSelection = [ + ...intersection, + ...additions.map( + ({ name }) => + ({ + entity: name, + match: "entity", + value: name, + } as NlpPattern), + ), + ]; + + onChange(newSelection); + }; + const handleNlpValueChange = ( + { id, name }: Pick, + valueId: string, + ): void => { + const newSelection = patterns.slice(0); + const update = newSelection.find(({ entity: e }) => e === name); + + if (!update) { + throw new Error("Unable to find nlp entity"); + } + + if (valueId === id) { + update.match = "entity"; + update.value = name; + } else { + const value = getNlpValueFromCache(valueId); + + if (!value) { + throw new Error("Unable to find nlp value in cache"); + } + update.match = "value"; + update.value = value.value; + } + + onChange(newSelection); + }; + + if (!options.length) { + return ( + + ); + } + + const defaultValue = + options.filter(({ name }) => + patterns.find(({ entity: entityName }) => entityName === name), + ) || {}; + + return ( + ( + + + {name} + + {doc && ( + + {doc} + + )} + + )} + getOptionLabel={({ name }) => name} + isOptionEqualToValue={(option, value) => option.id === value.id} + freeSolo={false} + loading={isLoading} + renderTags={(entities, getTagProps) => { + return ( + + {entities.map(({ id, name, values }, index) => { + const { key, onDelete } = getTagProps({ index }); + const nlpValues = values.map((vId) => + getNlpValueFromCache(vId), + ) as INlpValue[]; + const selectedValue = patterns.find( + (e) => e.entity === name, + )?.value; + const { id: selectedId = id } = + nlpValues.find(({ value }) => value === selectedValue) || {}; + + return ( + { + const nlpValueCache = getNlpValueFromCache(option); + + if (nlpValueCache) { + return nlpValueCache?.value; + } + + if (option === id) { + return t("label.any"); + } + + return option; + }} + freeSolo={false} + disableClearable + popupIcon={false} + onChange={(e, valueId) => + handleNlpValueChange({ id, name }, valueId) + } + sx={{ + minWidth: 50, + ".MuiAutocomplete-input": { + minWidth: "100px !important", + }, + "& .MuiOutlinedInput-root": { + paddingRight: "2rem !important", + "&.MuiInputBase-sizeSmall": { + padding: "0 6px 0 0 !important", + }, + }, + }} + renderInput={(props) => ( + + + + ), + endAdornment: ( + + {isLoading ? ( + + ) : ( + { + onDelete(e); + + onChange( + patterns.filter((p) => p.entity !== name), + ); + }} + edge="end" + size="small" + > + + + )} + + ), + }} + /> + )} + /> + ); + })} + + ); + }} + renderInput={(props) => ( + { + if (event.target !== inputRef.current) { + event.stopPropagation(); + event.preventDefault(); + } + }, + endAdornment: isLoading ? ( + + ) : null, + }} + /> + )} + /> + ); +}; + +NlpPatternSelect.displayName = "NlpPatternSelect"; + +export default forwardRef(NlpPatternSelect) as ( + props: NlpPatternSelectProps & { + ref?: React.ForwardedRef; + }, +) => ReturnType; diff --git a/frontend/src/components/visual-editor/BlockDialog.tsx b/frontend/src/components/visual-editor/BlockDialog.tsx index 5088621..27bb00e 100644 --- a/frontend/src/components/visual-editor/BlockDialog.tsx +++ b/frontend/src/components/visual-editor/BlockDialog.tsx @@ -35,7 +35,7 @@ import { useTranslate } from "@/hooks/useTranslate"; import { EntityType } from "@/services/types"; import { OutgoingMessageFormat } from "@/types/message.types"; -import { IBlockAttributes, IBlock } from "../../types/block.types"; +import { IBlock, IBlockAttributes } from "../../types/block.types"; import BlockFormProvider from "./form/BlockFormProvider"; import { MessageForm } from "./form/MessageForm"; @@ -69,24 +69,31 @@ const BlockDialog: FC = ({ toast.success(t("message.success_save")); }, }); - const methods = useForm({ - defaultValues: { - name: block?.name || "", - patterns: block?.patterns || [], - trigger_labels: block?.trigger_labels || [], - trigger_channels: block?.trigger_channels || [], - options: block?.options || { - typing: 0, - content: { - display: OutgoingMessageFormat.list, - top_element_style: "compact", - limit: 2, - }, - assignTo: block?.options?.assignTo, + const DEFAULT_VALUES = { + name: block?.name || "", + patterns: block?.patterns || [], + trigger_labels: block?.trigger_labels || [], + trigger_channels: block?.trigger_channels || [], + options: block?.options || { + typing: 0, + content: { + display: OutgoingMessageFormat.list, + top_element_style: "compact", + limit: 2, + }, + assignTo: block?.options?.assignTo, + fallback: block?.options?.fallback || { + active: true, + message: [], + max_attempts: 1, }, - assign_labels: block?.assign_labels || [], - message: block?.message || [""], }, + assign_labels: block?.assign_labels || [], + message: block?.message || [""], + capture_vars: block?.capture_vars || [], + } as IBlockAttributes; + const methods = useForm({ + defaultValues: DEFAULT_VALUES, }); const { reset, @@ -114,10 +121,8 @@ const BlockDialog: FC = ({ }, [open, reset]); useEffect(() => { - if (block) { - reset({ - name: block.name, - }); + if (block && open) { + reset(DEFAULT_VALUES); } else { reset(); } diff --git a/frontend/src/components/visual-editor/form/inputs/triggers/PatternInput.tsx b/frontend/src/components/visual-editor/form/inputs/triggers/PatternInput.tsx index c0152bb..0bab40f 100644 --- a/frontend/src/components/visual-editor/form/inputs/triggers/PatternInput.tsx +++ b/frontend/src/components/visual-editor/form/inputs/triggers/PatternInput.tsx @@ -6,14 +6,14 @@ * 2. All derivative works must include clear attribution to the original creator and software, Hexastack and Hexabot, in a prominent location (e.g., in the software's "About" section, documentation, and README file). */ -import { Box, Grid, MenuItem, TextFieldProps, Typography } from "@mui/material"; +import { Grid, MenuItem, TextFieldProps } from "@mui/material"; import { FC, useEffect, useState } from "react"; import { RegisterOptions, useFormContext } from "react-hook-form"; import AutoCompleteEntitySelect from "@/app-components/inputs/AutoCompleteEntitySelect"; import { Input } from "@/app-components/inputs/Input"; +import NlpPatternSelect from "@/app-components/inputs/NlpPatternSelect"; import { RegexInput } from "@/app-components/inputs/RegexInput"; -import { useGetFromCache } from "@/hooks/crud/useGet"; import { useTranslate } from "@/hooks/useTranslate"; import { EntityType, Format } from "@/services/types"; import { @@ -25,8 +25,6 @@ import { PayloadPattern, } from "@/types/block.types"; import { IMenuItem } from "@/types/menu.types"; -import { INlpEntity } from "@/types/nlp-entity.types"; -import { INlpValue } from "@/types/nlp-value.types"; import { ContentPostbackInput } from "./ContentPostbackInput"; import { PostbackInput } from "./PostbackInput"; @@ -71,7 +69,7 @@ const PatternInput: FC = ({ register, formState: { errors }, } = useFormContext(); - const getNlpEntityFromCache = useGetFromCache(EntityType.NLP_ENTITY); + // const getNlpEntityFromCache = useGetFromCache(EntityType.NLP_ENTITY); const [pattern, setPattern] = useState(value); const [patternType, setPatternType] = useState(getType(value)); const types = [ @@ -147,83 +145,12 @@ const PatternInput: FC = ({ - {patternType === "nlp" ? ( - - value={(pattern as NlpPattern[]).map((v) => - "value" in v && v.value ? v.value : v.entity, - )} - searchFields={["value", "label"]} - entity={EntityType.NLP_VALUE} - format={Format.FULL} - idKey="value" - labelKey="value" - label={t("label.nlp")} - multiple={true} - onChange={(_e, data) => { - setPattern( - data.map((d) => { - const entity = getNlpEntityFromCache(d.entity) as INlpEntity; - - return d.value === "any" - ? { - match: "entity", - entity: entity.name, - } - : { - match: "value", - entity: entity.name, - value: d.value, - }; - }), - ); - }} - getOptionLabel={(option) => { - const entity = getNlpEntityFromCache(option.entity) as INlpEntity; - - return `${entity.name}=${option.value}`; - }} - groupBy={(option) => { - const entity = getNlpEntityFromCache(option.entity) as INlpEntity; - - return entity.name; - }} - renderGroup={(params) => ( -
  • - - {params.group} - - {params.children} -
  • - )} - preprocess={(options) => { - return options.reduce((acc, curr) => { - const entity = getNlpEntityFromCache(curr.entity) as INlpEntity; - - if (entity.lookups.includes("keywords")) { - const exists = acc.find( - ({ value, id }) => value === "any" && id === entity.id, - ); - - if (!exists) { - acc.push({ - entity: entity.id, - id: entity.id, - value: "any", - } as INlpValue); - } - } - acc.push(curr); - - return acc; - }, [] as INlpValue[]); - }} + {patternType === "nlp" && ( + - ) : null} + )} {patternType === "menu" ? ( value={pattern ? (pattern as PayloadPattern).value : null} diff --git a/frontend/src/types/block.types.ts b/frontend/src/types/block.types.ts index 352117b..5eb686d 100644 --- a/frontend/src/types/block.types.ts +++ b/frontend/src/types/block.types.ts @@ -69,16 +69,11 @@ export interface PayloadPattern { type?: PayloadType; } -export type NlpPattern = - | { - entity: string; - match: "entity"; - } - | { - entity: string; - match: "value"; - value: string; - }; +export type NlpPattern = { + entity: string; + match: "value" | "entity"; + value: string; +}; export type Pattern = null | string | PayloadPattern | NlpPattern[];