diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml index 36ebce9..f13c4c4 100644 --- a/docker/docker-compose.yml +++ b/docker/docker-compose.yml @@ -11,7 +11,7 @@ services: - db-network - app-network volumes: - - api-data:/app/uploads + - ../api/uploads:/app/uploads depends_on: mongo: condition: service_healthy 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..6b05d9b 100644 --- a/frontend/public/locales/fr/translation.json +++ b/frontend/public/locales/fr/translation.json @@ -457,7 +457,8 @@ "is_rtl": "RTL", "original_text": "Texte par défaut", "inputs": "Port d'entré", - "outputs": "Port de sorti" + "outputs": "Port de sorti", + "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..3d98f4b --- /dev/null +++ b/frontend/src/app-components/inputs/NlpPatternSelect.tsx @@ -0,0 +1,248 @@ +/* + * 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 { RemoveOutlined } from "@mui/icons-material"; +import { + Box, + CircularProgress, + IconButton, + InputAdornment, + Typography, + useTheme, +} from "@mui/material"; +import Autocomplete from "@mui/material/Autocomplete"; +import { forwardRef, SyntheticEvent, useState } 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"; + +type NlpPatternSelectProps = {}; + +const NlpPatternSelect = ({}: NlpPatternSelectProps, ref) => { + const [selected, setSelected] = useState([]); + const theme = useTheme(); + const { t } = useTranslate(); + const { onSearch, 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); + + function handleNlpEntityChange( + _event: SyntheticEvent, + entities: INlpEntity[], + ): void { + const intersection = selected.filter(({ entity: entityName }) => + entities.find(({ name }) => name === entityName), + ); + const additions = entities.filter( + ({ name }) => + !selected.find(({ entity: entityName }) => name === entityName), + ); + const newSelection: NlpPattern[] = [ + ...intersection, + ...additions.map( + ({ name }) => + ({ + entity: name, + match: "entity", + value: name, + } as NlpPattern), + ), + ]; + + setSelected(newSelection); + } + + const handleNlpValueChange = (entity: INlpEntity, valueId: string) => { + const value = getNlpValueFromCache(valueId); + + if (!value) { + throw new Error("Unable to find nlp value in cache"); + } + + const newSelection = [...selected]; + const update = newSelection.find(({ entity: e }) => e === entity.name); + + if (!update) { + throw new Error("Unable to find nlp entity"); + } + + if (value.id === entity.id) { + update.match = "entity"; + update.value = entity.name; + } else { + update.match = "value"; + update.value = value.value; + } + }; + + return ( + + selected.find(({ entity: entityName }) => entityName === name), + )} + multiple={true} + options={options} + onChange={handleNlpEntityChange} + renderOption={(props, { name, doc }, { selected }) => ( + + + {name} + + {doc && ( + + {doc} + + )} + + )} + getOptionLabel={({ name }) => name} + isOptionEqualToValue={(option, value) => option.id === value.id} + freeSolo={false} + loading={isLoading} + renderTags={(entities, getTagProps) => ( + + {entities.map((entity, index) => { + const { key, onDelete } = getTagProps({ index }); + const handleChange = ( + event: SyntheticEvent, + valueId: string, + ) => { + handleNlpValueChange(entity, valueId); + }; + + return ( + + option + ? getNlpValueFromCache(option)?.value || "-" + : t("label.any") + } + disableClearable + popupIcon={false} + onChange={handleChange} + sx={{ + minWidth: 50, + padding: 0, + ".MuiAutocomplete-input": { + minWidth: "100px !important", + }, + "& .MuiOutlinedInput-root": { + paddingRight: "2rem !important", + "&.MuiInputBase-sizeSmall": { + padding: "0 !important", + }, + }, + }} + renderInput={(props) => ( + onSearch(e.target.value)} + InputProps={{ + ...props.InputProps, + startAdornment: ( + + {entity.name} + + ), + endAdornment: ( + + {isLoading ? ( + + ) : ( + + + + )} + + ), + }} + /> + )} + /> + ); + })} + + )} + renderInput={(props) => ( + onSearch(e.target.value)} + InputProps={{ + ...props.InputProps, + 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/form/inputs/triggers/PatternInput.tsx b/frontend/src/components/visual-editor/form/inputs/triggers/PatternInput.tsx index c0152bb..fece843 100644 --- a/frontend/src/components/visual-editor/form/inputs/triggers/PatternInput.tsx +++ b/frontend/src/components/visual-editor/form/inputs/triggers/PatternInput.tsx @@ -12,6 +12,7 @@ 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"; @@ -147,6 +148,7 @@ const PatternInput: FC = ({ + {patternType === "nlp" && } {patternType === "nlp" ? ( value={(pattern as NlpPattern[]).map((v) => 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[];