mirror of
https://github.com/hexastack/hexabot
synced 2025-06-26 18:27:28 +00:00
Merge d4fe58a925
into 4f59fbaeb5
This commit is contained in:
commit
b38a5b4bd0
@ -7,7 +7,7 @@
|
||||
*/
|
||||
|
||||
import { Divider } from "@mui/material";
|
||||
import { Controller, useFormContext } from "react-hook-form";
|
||||
import { Controller, useFieldArray, useFormContext } from "react-hook-form";
|
||||
|
||||
import { ContentContainer, ContentItem } from "@/app-components/dialogs";
|
||||
import AutoCompleteEntitySelect from "@/app-components/inputs/AutoCompleteEntitySelect";
|
||||
@ -24,21 +24,21 @@ export const TriggersForm = () => {
|
||||
const block = useBlock();
|
||||
const { t } = useTranslate();
|
||||
const { control } = useFormContext<IBlockAttributes>();
|
||||
const { fields, append, remove } = useFieldArray({
|
||||
control,
|
||||
name: "patterns",
|
||||
keyName: "fieldId",
|
||||
});
|
||||
|
||||
return (
|
||||
<ContentContainer>
|
||||
<ContentItem>
|
||||
<Controller
|
||||
name="patterns"
|
||||
<PatternsInput
|
||||
control={control}
|
||||
defaultValue={block?.patterns || []}
|
||||
render={({ field }) => (
|
||||
<PatternsInput
|
||||
value={field?.value || []}
|
||||
onChange={field.onChange}
|
||||
minInput={1}
|
||||
/>
|
||||
)}
|
||||
name="patterns"
|
||||
fields={fields}
|
||||
append={append}
|
||||
remove={remove}
|
||||
/>
|
||||
</ContentItem>
|
||||
<Divider orientation="horizontal" flexItem />
|
||||
|
@ -6,146 +6,124 @@
|
||||
* 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, TextFieldProps } from "@mui/material";
|
||||
import { FC, useEffect, useState } from "react";
|
||||
import { RegisterOptions, useFormContext } from "react-hook-form";
|
||||
import { Box } from "@mui/material";
|
||||
import { FC } from "react";
|
||||
import { Control, Controller } from "react-hook-form";
|
||||
|
||||
import { Input } from "@/app-components/inputs/Input";
|
||||
import NlpPatternSelect from "@/app-components/inputs/NlpPatternSelect";
|
||||
import { RegexInput } from "@/app-components/inputs/RegexInput";
|
||||
import { useTranslate } from "@/hooks/useTranslate";
|
||||
import {
|
||||
IBlockAttributes,
|
||||
IBlockFull,
|
||||
NlpPattern,
|
||||
Pattern,
|
||||
PatternType,
|
||||
PayloadPattern,
|
||||
} from "@/types/block.types";
|
||||
import {
|
||||
extractRegexBody,
|
||||
formatWithSlashes,
|
||||
isRegex,
|
||||
isRegexString,
|
||||
} from "@/utils/string";
|
||||
import { NlpPattern, Pattern, PayloadPattern } from "@/types/block.types";
|
||||
import { PatternType } from "@/types/pattern.types";
|
||||
import { getPatternType } from "@/utils/pattern";
|
||||
import { extractRegexBody, formatWithSlashes, isRegex } from "@/utils/string";
|
||||
|
||||
import { OutcomeInput } from "./OutcomeInput";
|
||||
import { PostbackInput } from "./PostbackInput";
|
||||
|
||||
const getPatternType = (pattern: Pattern): PatternType => {
|
||||
if (isRegexString(pattern)) {
|
||||
return "regex";
|
||||
} else if (Array.isArray(pattern)) {
|
||||
return "nlp";
|
||||
} else if (typeof pattern === "object") {
|
||||
if (pattern?.type === "menu") {
|
||||
return "menu";
|
||||
} else if (pattern?.type === "content") {
|
||||
return "content";
|
||||
} else if (pattern?.type === "outcome") {
|
||||
return "outcome";
|
||||
} else {
|
||||
return "payload";
|
||||
}
|
||||
} else {
|
||||
return "text";
|
||||
}
|
||||
};
|
||||
|
||||
type PatternInputProps = {
|
||||
value: Pattern;
|
||||
onChange: (pattern: Pattern) => void;
|
||||
block?: IBlockFull;
|
||||
idx: number;
|
||||
getInputProps?: (index: number) => TextFieldProps;
|
||||
control: Control<any>;
|
||||
basePath: string;
|
||||
};
|
||||
|
||||
const PatternInput: FC<PatternInputProps> = ({
|
||||
value,
|
||||
onChange,
|
||||
idx,
|
||||
getInputProps,
|
||||
}) => {
|
||||
const PatternInput: FC<PatternInputProps> = ({ control, basePath }) => {
|
||||
const { t } = useTranslate();
|
||||
const {
|
||||
register,
|
||||
formState: { errors },
|
||||
} = useFormContext<IBlockAttributes>();
|
||||
const [pattern, setPattern] = useState<Pattern>(value);
|
||||
const patternType = getPatternType(value);
|
||||
const registerInput = (
|
||||
errorMessage: string,
|
||||
idx: number,
|
||||
additionalOptions?: RegisterOptions<IBlockAttributes>,
|
||||
) => {
|
||||
return {
|
||||
...register(`patterns.${idx}`, {
|
||||
required: errorMessage,
|
||||
...additionalOptions,
|
||||
}),
|
||||
helperText: errors.patterns?.[idx]
|
||||
? errors.patterns[idx].message
|
||||
: undefined,
|
||||
error: !!errors.patterns?.[idx],
|
||||
};
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (pattern || pattern === "") {
|
||||
onChange(pattern);
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [pattern]);
|
||||
|
||||
return (
|
||||
<Box display="flex" flexGrow={1}>
|
||||
{patternType === "nlp" && (
|
||||
<NlpPatternSelect
|
||||
patterns={pattern as NlpPattern[]}
|
||||
onChange={setPattern}
|
||||
/>
|
||||
)}
|
||||
{["payload", "content", "menu"].includes(patternType) ? (
|
||||
<PostbackInput
|
||||
onChange={(payload) => {
|
||||
payload && setPattern(payload);
|
||||
}}
|
||||
defaultValue={pattern as PayloadPattern}
|
||||
/>
|
||||
) : null}
|
||||
{patternType === "outcome" ? (
|
||||
<OutcomeInput
|
||||
onChange={(payload) => {
|
||||
payload && setPattern(payload);
|
||||
}}
|
||||
defaultValue={pattern as PayloadPattern}
|
||||
/>
|
||||
) : null}
|
||||
{typeof value === "string" && patternType === "regex" ? (
|
||||
<RegexInput
|
||||
{...registerInput(t("message.regex_is_empty"), idx, {
|
||||
validate: (pattern) => {
|
||||
return isRegex(extractRegexBody(pattern))
|
||||
? true
|
||||
: t("message.regex_is_invalid");
|
||||
},
|
||||
setValueAs: (v) => (isRegexString(v) ? v : formatWithSlashes(v)),
|
||||
})}
|
||||
value={extractRegexBody(value)}
|
||||
label={t("label.regex")}
|
||||
onChange={(e) => onChange(formatWithSlashes(e.target.value))}
|
||||
required
|
||||
/>
|
||||
) : null}
|
||||
{typeof value === "string" && patternType === "text" ? (
|
||||
<Input
|
||||
{...(getInputProps ? getInputProps(idx) : null)}
|
||||
label={t("label.text")}
|
||||
value={value}
|
||||
onChange={(e) => onChange(e.target.value)}
|
||||
/>
|
||||
) : null}
|
||||
</Box>
|
||||
<Controller
|
||||
name={basePath}
|
||||
control={control}
|
||||
rules={{
|
||||
validate: (currentPatternValue: Pattern) => {
|
||||
const type = getPatternType(currentPatternValue);
|
||||
const isEmpty = (val: string) => !val || val === "";
|
||||
|
||||
if (type === PatternType.REGEX || type === PatternType.TEXT) {
|
||||
if (typeof currentPatternValue !== "string") {
|
||||
return t("message.text_is_required");
|
||||
}
|
||||
const value = currentPatternValue.trim();
|
||||
|
||||
if (type === PatternType.REGEX) {
|
||||
const regexBody = extractRegexBody(value);
|
||||
|
||||
if (isEmpty(regexBody)) {
|
||||
return t("message.regex_is_empty");
|
||||
}
|
||||
if (!isRegex(regexBody)) {
|
||||
return t("message.regex_is_invalid");
|
||||
}
|
||||
} else if (type === PatternType.TEXT) {
|
||||
if (isEmpty(value)) {
|
||||
return t("message.text_is_required");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
},
|
||||
}}
|
||||
render={({ field, fieldState }) => {
|
||||
const patternForPath = field.value as Pattern;
|
||||
const currentPatternType = getPatternType(patternForPath);
|
||||
|
||||
return (
|
||||
<Box display="flex" flexGrow={1}>
|
||||
{currentPatternType === PatternType.NLP && (
|
||||
<NlpPatternSelect
|
||||
patterns={patternForPath as NlpPattern[]}
|
||||
onChange={field.onChange}
|
||||
/>
|
||||
)}
|
||||
{[
|
||||
PatternType.PAYLOAD,
|
||||
PatternType.CONTENT,
|
||||
PatternType.MENU,
|
||||
].includes(currentPatternType) ? (
|
||||
<PostbackInput
|
||||
onChange={(payload) => {
|
||||
payload && field.onChange(payload);
|
||||
}}
|
||||
defaultValue={patternForPath as PayloadPattern}
|
||||
/>
|
||||
) : null}
|
||||
{currentPatternType === PatternType.OUTCOME ? (
|
||||
<OutcomeInput
|
||||
onChange={(payload) => {
|
||||
payload && field.onChange(payload);
|
||||
}}
|
||||
defaultValue={patternForPath as PayloadPattern}
|
||||
/>
|
||||
) : null}
|
||||
{typeof patternForPath === "string" &&
|
||||
currentPatternType === PatternType.REGEX ? (
|
||||
<RegexInput
|
||||
value={extractRegexBody(patternForPath as string)}
|
||||
label={t("label.regex")}
|
||||
onChange={(e) =>
|
||||
field.onChange(formatWithSlashes(e.target.value))
|
||||
}
|
||||
required
|
||||
error={fieldState.invalid}
|
||||
helperText={fieldState.error?.message}
|
||||
/>
|
||||
) : null}
|
||||
{typeof patternForPath === "string" &&
|
||||
currentPatternType === PatternType.TEXT ? (
|
||||
<Input
|
||||
label={t("label.text")}
|
||||
value={patternForPath as string}
|
||||
onChange={(e) => field.onChange(e.target.value)}
|
||||
error={fieldState.invalid}
|
||||
helperText={fieldState.error?.message}
|
||||
required
|
||||
/>
|
||||
) : null}
|
||||
</Box>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
|
@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright © 2024 Hexastack. All rights reserved.
|
||||
* Copyright © 2025 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.
|
||||
@ -14,8 +14,13 @@ import PsychologyAltIcon from "@mui/icons-material/PsychologyAlt";
|
||||
import RemoveCircleOutlineIcon from "@mui/icons-material/RemoveCircleOutline";
|
||||
import SpellcheckIcon from "@mui/icons-material/Spellcheck";
|
||||
import { Box, Chip, IconButton, styled, useTheme } from "@mui/material";
|
||||
import { FC, useEffect, useMemo, useState } from "react";
|
||||
import { useFormContext } from "react-hook-form";
|
||||
import { FC, useMemo } from "react";
|
||||
import {
|
||||
Control,
|
||||
FieldArrayWithId,
|
||||
UseFieldArrayAppend,
|
||||
UseFieldArrayRemove,
|
||||
} from "react-hook-form";
|
||||
|
||||
import DropdownButton, {
|
||||
DropdownButtonAction,
|
||||
@ -24,9 +29,6 @@ import { useTranslate } from "@/hooks/useTranslate";
|
||||
import { Pattern } from "@/types/block.types";
|
||||
import { PayloadType } from "@/types/message.types";
|
||||
import { SXStyleOptions } from "@/utils/SXStyleOptions";
|
||||
import { createValueWithId, ValueWithId } from "@/utils/valueWithId";
|
||||
|
||||
import { getInputControls } from "../../utils/inputControls";
|
||||
|
||||
import PatternInput from "./PatternInput";
|
||||
|
||||
@ -41,41 +43,28 @@ const StyledNoPatternsDiv = styled("div")(
|
||||
);
|
||||
|
||||
type PatternsInputProps = {
|
||||
value: Pattern[];
|
||||
onChange: (patterns: Pattern[]) => void;
|
||||
minInput: number;
|
||||
control: Control<any>;
|
||||
name: string;
|
||||
fields: FieldArrayWithId<any, string, "fieldId">[];
|
||||
append: UseFieldArrayAppend<any, string>;
|
||||
remove: UseFieldArrayRemove;
|
||||
};
|
||||
|
||||
const PatternsInput: FC<PatternsInputProps> = ({ value, onChange }) => {
|
||||
const PatternsInput: FC<PatternsInputProps> = ({
|
||||
control,
|
||||
name,
|
||||
fields,
|
||||
append,
|
||||
remove,
|
||||
}) => {
|
||||
const { t } = useTranslate();
|
||||
const theme = useTheme();
|
||||
const [patterns, setPatterns] = useState<ValueWithId<Pattern>[]>(
|
||||
value.map((pattern) => createValueWithId(pattern)),
|
||||
);
|
||||
const {
|
||||
register,
|
||||
formState: { errors },
|
||||
} = useFormContext<any>();
|
||||
const addInput = (defaultValue: Pattern) => {
|
||||
setPatterns([...patterns, createValueWithId<Pattern>(defaultValue)]);
|
||||
append(defaultValue);
|
||||
};
|
||||
const removeInput = (index: number) => {
|
||||
const updatedPatterns = [...patterns];
|
||||
|
||||
updatedPatterns.splice(index, 1);
|
||||
|
||||
setPatterns(updatedPatterns);
|
||||
remove(index);
|
||||
};
|
||||
const updateInput = (index: number) => (p: Pattern) => {
|
||||
patterns[index].value = p;
|
||||
setPatterns([...patterns]);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
onChange(patterns.map(({ value }) => value));
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [patterns]);
|
||||
|
||||
const actions: DropdownButtonAction[] = useMemo(
|
||||
() => [
|
||||
{
|
||||
@ -87,7 +76,7 @@ const PatternsInput: FC<PatternsInputProps> = ({ value, onChange }) => {
|
||||
{
|
||||
icon: <PsychologyAltIcon />,
|
||||
name: t("label.intent_match"),
|
||||
defaultValue: [],
|
||||
defaultValue: [[]],
|
||||
},
|
||||
{
|
||||
icon: <MouseIcon />,
|
||||
@ -117,11 +106,11 @@ const PatternsInput: FC<PatternsInputProps> = ({ value, onChange }) => {
|
||||
return (
|
||||
<Box display="flex" flexDirection="column">
|
||||
<Box display="flex" flexDirection="column">
|
||||
{patterns.length == 0 ? (
|
||||
{fields.length === 0 ? (
|
||||
<StyledNoPatternsDiv>{t("label.no_patterns")}</StyledNoPatternsDiv>
|
||||
) : (
|
||||
patterns.map(({ value, id }, idx) => (
|
||||
<Box display="flex" alignItems="center" mt={2} key={id}>
|
||||
fields.map((field, idx) => (
|
||||
<Box display="flex" alignItems="center" mt={2} key={field.fieldId}>
|
||||
{idx > 0 && (
|
||||
<Chip
|
||||
sx={{ m: 1, color: theme.palette.grey[600] }}
|
||||
@ -131,15 +120,9 @@ const PatternsInput: FC<PatternsInputProps> = ({ value, onChange }) => {
|
||||
/>
|
||||
)}
|
||||
<PatternInput
|
||||
idx={idx}
|
||||
value={value}
|
||||
onChange={updateInput(idx)}
|
||||
getInputProps={getInputControls(
|
||||
"label",
|
||||
errors,
|
||||
register,
|
||||
t("message.text_is_required"),
|
||||
)}
|
||||
control={control}
|
||||
basePath={`${name}.${idx}`}
|
||||
//idx={idx}
|
||||
/>
|
||||
<IconButton
|
||||
size="small"
|
||||
|
@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright © 2024 Hexastack. All rights reserved.
|
||||
* Copyright © 2025 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.
|
||||
@ -8,7 +8,6 @@
|
||||
|
||||
import {
|
||||
Autocomplete,
|
||||
Box,
|
||||
Chip,
|
||||
CircularProgress,
|
||||
InputAdornment,
|
||||
@ -230,7 +229,9 @@ export const PostbackInput = ({
|
||||
<Typography component="h4" p={2} fontWeight={700} color="primary">
|
||||
{t(`label.${group}`)}
|
||||
</Typography>
|
||||
<Box>{children}</Box>
|
||||
<ul style={{ margin: 0, padding: 0, listStyleType: "none" }}>
|
||||
{children}
|
||||
</ul>
|
||||
</li>
|
||||
)}
|
||||
renderInput={(props) => (
|
||||
|
@ -20,6 +20,7 @@ import {
|
||||
StdOutgoingTextMessage,
|
||||
StdPluginMessage,
|
||||
} from "./message.types";
|
||||
import { PatternType } from "./pattern.types";
|
||||
import { IUser } from "./user.types";
|
||||
|
||||
export type Position = {
|
||||
@ -76,14 +77,7 @@ export type NlpPattern = {
|
||||
|
||||
export type Pattern = null | string | PayloadPattern | NlpPattern[];
|
||||
|
||||
export type PatternType =
|
||||
| "regex"
|
||||
| "nlp"
|
||||
| "menu"
|
||||
| "content"
|
||||
| "outcome"
|
||||
| "payload"
|
||||
| "text";
|
||||
export type { PatternType };
|
||||
|
||||
export interface IBlockAttributes {
|
||||
name: string;
|
||||
|
17
frontend/src/types/pattern.types.ts
Normal file
17
frontend/src/types/pattern.types.ts
Normal file
@ -0,0 +1,17 @@
|
||||
/*
|
||||
* Copyright © 2025 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).
|
||||
*/
|
||||
|
||||
export enum PatternType {
|
||||
TEXT = "text",
|
||||
REGEX = "regex",
|
||||
NLP = "nlp",
|
||||
PAYLOAD = "payload",
|
||||
MENU = "menu",
|
||||
CONTENT = "content",
|
||||
OUTCOME = "outcome",
|
||||
}
|
44
frontend/src/utils/pattern.ts
Normal file
44
frontend/src/utils/pattern.ts
Normal file
@ -0,0 +1,44 @@
|
||||
/*
|
||||
* Copyright © 2025 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 { Pattern } from "@/types/block.types";
|
||||
import { PatternType } from "@/types/pattern.types";
|
||||
|
||||
import { isRegexString } from "./string";
|
||||
|
||||
/**
|
||||
* Determines the type of a given pattern and returns the corresponding `PatternType`.
|
||||
* Defaults to returning `PatternType.TEXT` if none of the conditions are met.
|
||||
*
|
||||
* @param pattern - The pattern to evaluate, which can be a string, array, or object.
|
||||
* @returns The determined `PatternType` for the given pattern.
|
||||
*/
|
||||
export const getPatternType = (pattern: Pattern): PatternType => {
|
||||
if (typeof pattern === "string") {
|
||||
return isRegexString(pattern) ? PatternType.REGEX : PatternType.TEXT;
|
||||
}
|
||||
|
||||
if (Array.isArray(pattern)) {
|
||||
return PatternType.NLP;
|
||||
}
|
||||
|
||||
if (pattern && typeof pattern === "object") {
|
||||
switch (pattern.type) {
|
||||
case "menu":
|
||||
return PatternType.MENU;
|
||||
case "content":
|
||||
return PatternType.CONTENT;
|
||||
case "outcome":
|
||||
return PatternType.OUTCOME;
|
||||
default:
|
||||
return PatternType.PAYLOAD;
|
||||
}
|
||||
}
|
||||
|
||||
return PatternType.TEXT;
|
||||
};
|
Loading…
Reference in New Issue
Block a user