hexabot/frontend/src/app-components/inputs/NlpPatternSelect.tsx

324 lines
10 KiB
TypeScript
Raw Normal View History

2024-11-25 08:51:19 +00:00
/*
* 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).
*/
2024-11-25 18:01:35 +00:00
import { Cancel } from "@mui/icons-material";
2024-11-25 08:51:19 +00:00
import {
Box,
2024-11-26 11:17:54 +00:00
Chip,
2024-11-25 08:51:19 +00:00
CircularProgress,
IconButton,
InputAdornment,
2024-11-26 06:12:27 +00:00
Skeleton,
2024-11-25 08:51:19 +00:00
Typography,
useTheme,
} from "@mui/material";
import Autocomplete from "@mui/material/Autocomplete";
2024-11-26 11:17:54 +00:00
import { forwardRef, SyntheticEvent, useRef } from "react";
2024-11-25 08:51:19 +00:00
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";
2024-11-26 06:12:27 +00:00
import { INlpValue } from "@/types/nlp-value.types";
2024-11-25 08:51:19 +00:00
2024-11-26 11:17:54 +00:00
type NlpPatternSelectProps = {
patterns: NlpPattern[];
onChange: (
_event: SyntheticEvent<Element, Event> | undefined,
patterns: NlpPattern[],
) => void;
};
2024-11-25 08:51:19 +00:00
2024-11-25 18:01:35 +00:00
const NlpPatternSelect = (
{ patterns, onChange }: NlpPatternSelectProps,
ref,
) => {
2024-11-26 06:12:27 +00:00
const inputRef = useRef(null);
2024-11-25 08:51:19 +00:00
const theme = useTheme();
const { t } = useTranslate();
2024-11-26 11:17:54 +00:00
const { searchPayload } = useSearch<INlpEntity>({
2024-11-25 08:51:19 +00:00
$iLike: ["name"],
});
const { data: options, isLoading } = useFind(
{ entity: EntityType.NLP_ENTITY, format: Format.FULL },
{ hasCount: false, params: searchPayload },
);
const getNlpValueFromCache = useGetFromCache(EntityType.NLP_VALUE);
2024-11-26 11:17:54 +00:00
const handleNlpEntityChange = (
2024-11-25 08:51:19 +00:00
_event: SyntheticEvent<Element, Event>,
entities: INlpEntity[],
2024-11-26 11:17:54 +00:00
): void => {
const intersection = patterns.filter(({ entity: entityName }) =>
2024-11-25 08:51:19 +00:00
entities.find(({ name }) => name === entityName),
);
const additions = entities.filter(
({ name }) =>
2024-11-26 11:17:54 +00:00
!patterns.find(({ entity: entityName }) => name === entityName),
2024-11-25 08:51:19 +00:00
);
2024-11-26 11:17:54 +00:00
const newSelection = [
2024-11-25 08:51:19 +00:00
...intersection,
...additions.map(
({ name }) =>
({
entity: name,
match: "entity",
value: name,
} as NlpPattern),
),
];
2024-11-26 11:17:54 +00:00
onChange(undefined, newSelection);
};
const handleNlpValueChange = (
{ id, name }: Pick<INlpEntity, "id" | "name">,
valueId: string,
): void => {
const newSelection = patterns.slice(0);
const update = newSelection.find(({ entity: e }) => e === name);
2024-11-25 08:51:19 +00:00
if (!update) {
throw new Error("Unable to find nlp entity");
}
2024-11-26 11:17:54 +00:00
if (valueId === id) {
2024-11-25 08:51:19 +00:00
update.match = "entity";
2024-11-26 11:17:54 +00:00
update.value = name;
2024-11-25 08:51:19 +00:00
} else {
2024-11-25 18:01:35 +00:00
const value = getNlpValueFromCache(valueId);
if (!value) {
throw new Error("Unable to find nlp value in cache");
}
2024-11-25 08:51:19 +00:00
update.match = "value";
update.value = value.value;
}
2024-11-25 18:01:35 +00:00
onChange(undefined, newSelection);
2024-11-25 08:51:19 +00:00
};
2024-11-26 06:12:27 +00:00
if (!options.length) {
return (
<Skeleton animation="wave" variant="rounded" width="100%" height={56} />
);
}
2024-11-25 18:01:35 +00:00
const defaultValue =
options.filter(({ name }) =>
2024-11-26 11:17:54 +00:00
patterns.find(({ entity: entityName }) => entityName === name),
2024-11-25 18:01:35 +00:00
) || {};
2024-11-25 08:51:19 +00:00
return (
<Autocomplete
ref={ref}
size="medium"
disabled={options.length === 0}
2024-11-26 12:25:53 +00:00
value={defaultValue}
2024-11-25 08:51:19 +00:00
multiple={true}
options={options}
onChange={handleNlpEntityChange}
renderOption={(props, { name, doc }, { selected }) => (
<Box
component="li"
{...props}
p={2}
display="flex"
flexDirection="column"
style={{
alignItems: "start",
}}
sx={{
backgroundColor: selected
? theme.palette.action.selected
: "inherit",
"&:hover": {
backgroundColor: theme.palette.action.hover,
},
cursor: "pointer",
}}
>
<Typography variant="body1" fontWeight="bold">
{name}
</Typography>
{doc && (
<Typography variant="body2" color="textSecondary">
{doc}
</Typography>
)}
</Box>
)}
getOptionLabel={({ name }) => name}
isOptionEqualToValue={(option, value) => option.id === value.id}
freeSolo={false}
loading={isLoading}
2024-11-25 18:01:35 +00:00
renderTags={(entities, getTagProps) => {
return (
<Box
sx={{
display: "flex",
flexWrap: "wrap",
gap: 0.5,
2024-11-26 11:17:54 +00:00
mx: "0.5rem",
2024-11-25 18:01:35 +00:00
}}
>
2024-11-26 11:17:54 +00:00
{entities.map(({ id, name, values }, index) => {
2024-11-25 18:01:35 +00:00
const { key, onDelete } = getTagProps({ index });
2024-11-26 11:17:54 +00:00
const nlpValues = values.map((vId) =>
2024-11-26 06:12:27 +00:00
getNlpValueFromCache(vId),
) as INlpValue[];
2024-11-26 11:17:54 +00:00
const selectedValue = patterns.find(
(e) => e.entity === name,
2024-11-26 06:12:27 +00:00
)?.value;
2024-11-26 11:17:54 +00:00
const { id: selectedId = id } =
nlpValues.find(({ value }) => value === selectedValue) || {};
2024-11-25 18:01:35 +00:00
return (
<Autocomplete
size="small"
2024-11-26 12:25:53 +00:00
value={selectedId}
2024-11-26 11:17:54 +00:00
options={[id].concat(values)}
2024-11-25 18:01:35 +00:00
multiple={false}
key={key}
getOptionLabel={(option) => {
const nlpValueCache = getNlpValueFromCache(option);
if (nlpValueCache) {
return nlpValueCache?.value;
}
2024-11-26 11:17:54 +00:00
if (option === id) {
2024-11-25 18:01:35 +00:00
return t("label.any");
}
return option;
}}
2024-11-26 06:12:27 +00:00
freeSolo={false}
2024-11-25 18:01:35 +00:00
disableClearable
popupIcon={false}
2024-11-26 11:17:54 +00:00
onChange={(e, valueId) =>
handleNlpValueChange({ id, name }, valueId)
}
2024-11-25 18:01:35 +00:00
sx={{
minWidth: 50,
".MuiAutocomplete-input": {
minWidth: "100px !important",
2024-11-25 08:51:19 +00:00
},
2024-11-25 18:01:35 +00:00
"& .MuiOutlinedInput-root": {
paddingRight: "2rem !important",
"&.MuiInputBase-sizeSmall": {
padding: "0 6px 0 0 !important",
},
},
}}
2024-11-26 11:17:54 +00:00
renderInput={(props) => (
<Input
{...props}
InputProps={{
...props.InputProps,
readOnly: true,
sx: {
padding: 0,
overflow: "hidden",
cursor: "pointer",
fontSize: "14px",
},
startAdornment: (
<InputAdornment position="start">
<Chip
2024-11-25 18:01:35 +00:00
sx={{
2024-11-26 11:17:54 +00:00
p: "0 0.3rem",
border: "none",
borderRadius: 0,
2024-11-25 18:01:35 +00:00
}}
2024-11-26 11:17:54 +00:00
color="primary"
label={name}
variant="role"
/>
</InputAdornment>
),
endAdornment: (
<InputAdornment position="end">
{isLoading ? (
<CircularProgress color="inherit" size={20} />
) : (
<IconButton
sx={{ p: 0, pr: "2px" }}
onClick={(e) => {
onDelete(e);
2024-11-25 18:01:35 +00:00
2024-11-26 11:17:54 +00:00
onChange(
undefined,
patterns.filter((p) => p.entity !== name),
);
}}
edge="end"
size="small"
>
<Cancel
sx={{
fontSize: "16px",
transition: ".05s",
"&:hover": {
color: theme.palette.grey[700],
},
2024-11-26 06:12:27 +00:00
}}
2024-11-26 11:17:54 +00:00
htmlColor={theme.palette.grey[500]}
/>
</IconButton>
)}
</InputAdornment>
),
}}
/>
)}
2024-11-25 18:01:35 +00:00
/>
);
})}
</Box>
);
}}
2024-11-25 08:51:19 +00:00
renderInput={(props) => (
<Input
{...props}
2024-11-26 11:17:54 +00:00
sx={{
"& .MuiOutlinedInput-root": {
paddingRight: "6px !important",
},
}}
size="small"
2024-11-25 08:51:19 +00:00
label={t("label.nlp")}
InputProps={{
...props.InputProps,
2024-11-26 06:12:27 +00:00
inputRef,
onClick: (event) => {
if (event.target !== inputRef.current) {
event.stopPropagation();
event.preventDefault();
}
},
2024-11-25 08:51:19 +00:00
endAdornment: isLoading ? (
<CircularProgress color="inherit" size={20} />
) : null,
}}
/>
)}
/>
);
};
NlpPatternSelect.displayName = "NlpPatternSelect";
export default forwardRef(NlpPatternSelect) as (
props: NlpPatternSelectProps & {
ref?: React.ForwardedRef<HTMLDivElement>;
},
) => ReturnType<typeof NlpPatternSelect>;