mirror of
https://github.com/hexastack/hexabot
synced 2025-04-10 15:55:55 +00:00
feat: add ability to select an outcome in any block triggers
This commit is contained in:
parent
7e48eb6067
commit
34c299337d
@ -247,6 +247,13 @@
|
||||
"triggers": "Triggers",
|
||||
"payloads": "Payloads",
|
||||
"general_payloads": "General Payloads",
|
||||
"exact_match": "Exact Match",
|
||||
"pattern_match": "Pattern Match",
|
||||
"intent_match": "Intent Match",
|
||||
"interaction": "Interaction",
|
||||
"outcome_match": "Outcome Match",
|
||||
"outcome": "Outcome",
|
||||
"any_outcome": "Any Outcome",
|
||||
"capture": "Capture?",
|
||||
"context_var": "Context Var",
|
||||
"text_message": "Text message",
|
||||
|
@ -247,6 +247,13 @@
|
||||
"triggers": "Déclencheurs",
|
||||
"payloads": "Payloads",
|
||||
"general_payloads": "Payloads généraux",
|
||||
"exact_match": "Comparaison Exacte",
|
||||
"pattern_match": "Expression Régulière",
|
||||
"intent_match": "Intention",
|
||||
"interaction": "Interaction",
|
||||
"outcome": "Résultat",
|
||||
"outcome_match": "Résultat",
|
||||
"any_outcome": "N'importe quel résultat",
|
||||
"capture": "Capturer?",
|
||||
"context_var": "Variable contextuelle",
|
||||
"text_message": "Message texte",
|
||||
|
@ -59,6 +59,7 @@ export const BlockEditForm: FC<ComponentFormProps<IBlock>> = ({
|
||||
const DEFAULT_VALUES = {
|
||||
name: block?.name || "",
|
||||
patterns: block?.patterns || [],
|
||||
outcomes: block?.outcomes || [],
|
||||
trigger_labels: block?.trigger_labels || [],
|
||||
trigger_channels: block?.trigger_channels || [],
|
||||
options: block?.options || {
|
||||
|
@ -17,6 +17,7 @@ import { EntityType } from "@/services/types";
|
||||
import { IBlockAttributes } from "@/types/block.types";
|
||||
import { StdPluginMessage } from "@/types/message.types";
|
||||
|
||||
import { getNamespace } from "@/utils/string";
|
||||
import { useBlock } from "./BlockFormProvider";
|
||||
|
||||
const PluginMessageForm = () => {
|
||||
@ -63,8 +64,7 @@ const PluginMessageForm = () => {
|
||||
<SettingInput
|
||||
setting={setting}
|
||||
field={field}
|
||||
// @TODO : clean this later
|
||||
ns={message.plugin.replaceAll("-", "_")}
|
||||
ns={getNamespace(message.plugin)}
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
|
@ -0,0 +1,151 @@
|
||||
/*
|
||||
* 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 {
|
||||
Autocomplete,
|
||||
Box,
|
||||
Chip,
|
||||
InputAdornment,
|
||||
Skeleton,
|
||||
Typography,
|
||||
} from "@mui/material";
|
||||
import { useMemo, useState } from "react";
|
||||
|
||||
import { Input } from "@/app-components/inputs/Input";
|
||||
import { useGetFromCache } from "@/hooks/crud/useGet";
|
||||
import { useTranslate } from "@/hooks/useTranslate";
|
||||
import { theme } from "@/layout/themes/theme";
|
||||
import { EntityType } from "@/services/types";
|
||||
import { IBlock, PayloadPattern } from "@/types/block.types";
|
||||
|
||||
import { PayloadType } from "@/types/message.types";
|
||||
import { getNamespace } from "@/utils/string";
|
||||
import { useBlock } from "../../BlockFormProvider";
|
||||
|
||||
type PayloadOption = PayloadPattern & {
|
||||
group: string;
|
||||
};
|
||||
|
||||
type OutcomeInputProps = {
|
||||
defaultValue: PayloadPattern;
|
||||
onChange: (pattern: PayloadPattern) => void;
|
||||
};
|
||||
|
||||
export const OutcomeInput = ({ defaultValue, onChange }: OutcomeInputProps) => {
|
||||
const block = useBlock();
|
||||
const [selectedValue, setSelectedValue] = useState(defaultValue);
|
||||
const getBlockFromCache = useGetFromCache(EntityType.BLOCK);
|
||||
const { t } = useTranslate();
|
||||
// Gather previous blocks outcomes
|
||||
const options = useMemo(
|
||||
() =>
|
||||
(block?.previousBlocks || [])
|
||||
.map((b) => getBlockFromCache(b))
|
||||
.filter((b) => b && Array.isArray(b.outcomes) && b.outcomes.length > 0)
|
||||
.map((b) => b as IBlock)
|
||||
.reduce(
|
||||
(acc, b) => {
|
||||
const outcomes = (b.outcomes || []).map((outcome) => ({
|
||||
label: t(`label.${outcome}` as any, {
|
||||
defaultValue: outcome,
|
||||
ns:
|
||||
"plugin" in b.message
|
||||
? getNamespace(b.message.plugin)
|
||||
: undefined,
|
||||
}),
|
||||
value: outcome,
|
||||
group: b.name,
|
||||
type: PayloadType.outcome,
|
||||
}));
|
||||
|
||||
return acc.concat(outcomes);
|
||||
},
|
||||
[
|
||||
{
|
||||
label: t("label.any_outcome"),
|
||||
value: "any",
|
||||
type: PayloadType.outcome,
|
||||
group: "general",
|
||||
},
|
||||
] as PayloadOption[],
|
||||
),
|
||||
[block?.previousBlocks, getBlockFromCache],
|
||||
);
|
||||
|
||||
const isOptionsReady =
|
||||
!defaultValue || options.find((o) => o.value === defaultValue.value);
|
||||
|
||||
if (!isOptionsReady) {
|
||||
return (
|
||||
<Skeleton animation="wave" variant="rounded" width="100%" height={40} />
|
||||
);
|
||||
}
|
||||
|
||||
const selected = defaultValue
|
||||
? options.find((o) => o.value === defaultValue.value)
|
||||
: undefined;
|
||||
|
||||
return (
|
||||
<Autocomplete
|
||||
size="small"
|
||||
fullWidth
|
||||
defaultValue={selected || undefined}
|
||||
value={selected}
|
||||
options={options}
|
||||
multiple={false}
|
||||
disableClearable
|
||||
onChange={(_e, value) => {
|
||||
setSelectedValue(value);
|
||||
const { group: _g, ...payloadPattern } = value;
|
||||
|
||||
onChange(payloadPattern);
|
||||
}}
|
||||
groupBy={({ group }) => group ?? t("label.other")}
|
||||
getOptionLabel={({ label }) => label}
|
||||
isOptionEqualToValue={(option, value) => option.value === value.value}
|
||||
renderGroup={({ key, group, children }) => (
|
||||
<li key={key}>
|
||||
<Typography component="h4" p={2} fontWeight={700} color="primary">
|
||||
{t(`label.${group}`, { defaultValue: group })}
|
||||
</Typography>
|
||||
<Box>{children}</Box>
|
||||
</li>
|
||||
)}
|
||||
renderInput={(props) => (
|
||||
<Input
|
||||
{...props}
|
||||
label={t("label.outcome")}
|
||||
InputProps={{
|
||||
...props.InputProps,
|
||||
startAdornment: (
|
||||
<InputAdornment position="start">
|
||||
<Chip
|
||||
sx={{
|
||||
left: "8px",
|
||||
height: "25px",
|
||||
fontSize: "12px",
|
||||
minWidth: "75px",
|
||||
position: "relative",
|
||||
maxHeight: "30px",
|
||||
borderRadius: "16px",
|
||||
borderColor: theme.palette.grey[400],
|
||||
}}
|
||||
color="primary"
|
||||
label={t(
|
||||
`label.${selectedValue?.type || "outcome"}`,
|
||||
).toLocaleLowerCase()}
|
||||
variant="role"
|
||||
/>
|
||||
</InputAdornment>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
};
|
@ -23,6 +23,7 @@ import {
|
||||
PayloadPattern,
|
||||
} from "@/types/block.types";
|
||||
|
||||
import { OutcomeInput } from "./OutcomeInput";
|
||||
import { PostbackInput } from "./PostbackInput";
|
||||
|
||||
const isRegex = (str: Pattern) => {
|
||||
@ -38,6 +39,8 @@ const getType = (pattern: Pattern): PatternType => {
|
||||
return "menu";
|
||||
} else if (pattern?.type === "content") {
|
||||
return "content";
|
||||
} else if (pattern?.type === "outcome") {
|
||||
return "outcome";
|
||||
} else {
|
||||
return "payload";
|
||||
}
|
||||
@ -67,7 +70,6 @@ const PatternInput: FC<PatternInputProps> = ({
|
||||
} = useFormContext<IBlockAttributes>();
|
||||
const [pattern, setPattern] = useState<Pattern>(value);
|
||||
const patternType = getType(value);
|
||||
const isPostbackType = ["payload", "content", "menu"].includes(patternType);
|
||||
const registerInput = (
|
||||
errorMessage: string,
|
||||
idx: number,
|
||||
@ -100,15 +102,22 @@ const PatternInput: FC<PatternInputProps> = ({
|
||||
onChange={setPattern}
|
||||
/>
|
||||
)}
|
||||
|
||||
{isPostbackType ? (
|
||||
<PostbackInput
|
||||
onChange={(payload) => {
|
||||
payload && setPattern(payload);
|
||||
}}
|
||||
defaultValue={pattern as PayloadPattern}
|
||||
/>
|
||||
) : null}
|
||||
{["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, {
|
||||
|
@ -6,14 +6,13 @@
|
||||
* 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 {
|
||||
Abc,
|
||||
Add,
|
||||
Mouse,
|
||||
PsychologyAlt,
|
||||
RemoveCircleOutline,
|
||||
Spellcheck,
|
||||
} from "@mui/icons-material";
|
||||
import AbcIcon from "@mui/icons-material/Abc";
|
||||
import AddIcon from "@mui/icons-material/Add";
|
||||
import MediationIcon from "@mui/icons-material/Mediation";
|
||||
import MouseIcon from "@mui/icons-material/Mouse";
|
||||
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";
|
||||
@ -79,12 +78,20 @@ const PatternsInput: FC<PatternsInputProps> = ({ value, onChange }) => {
|
||||
|
||||
const actions: DropdownButtonAction[] = useMemo(
|
||||
() => [
|
||||
{ icon: <Spellcheck />, name: "Exact Match", defaultValue: "" },
|
||||
{ icon: <Abc />, name: "Pattern Match", defaultValue: "//" },
|
||||
{ icon: <PsychologyAlt />, name: "Intent Match", defaultValue: [] },
|
||||
{
|
||||
icon: <Mouse />,
|
||||
name: "Interaction",
|
||||
icon: <SpellcheckIcon />,
|
||||
name: t("label.exact_match"),
|
||||
defaultValue: "",
|
||||
},
|
||||
{ icon: <AbcIcon />, name: t("label.pattern_match"), defaultValue: "//" },
|
||||
{
|
||||
icon: <PsychologyAltIcon />,
|
||||
name: t("label.intent_match"),
|
||||
defaultValue: [],
|
||||
},
|
||||
{
|
||||
icon: <MouseIcon />,
|
||||
name: t("label.interaction"),
|
||||
defaultValue: {
|
||||
label: t("label.get_started"),
|
||||
value: "GET_STARTED",
|
||||
@ -92,6 +99,16 @@ const PatternsInput: FC<PatternsInputProps> = ({ value, onChange }) => {
|
||||
group: "general",
|
||||
},
|
||||
},
|
||||
{
|
||||
icon: <MediationIcon />,
|
||||
name: t("label.outcome_match"),
|
||||
defaultValue: {
|
||||
label: t("label.any_outcome"),
|
||||
value: "any",
|
||||
type: PayloadType.outcome,
|
||||
group: "general",
|
||||
},
|
||||
},
|
||||
],
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[],
|
||||
@ -129,7 +146,7 @@ const PatternsInput: FC<PatternsInputProps> = ({ value, onChange }) => {
|
||||
color="error"
|
||||
onClick={() => removeInput(idx)}
|
||||
>
|
||||
<RemoveCircleOutline />
|
||||
<RemoveCircleOutlineIcon />
|
||||
</IconButton>
|
||||
</Box>
|
||||
))
|
||||
@ -140,7 +157,7 @@ const PatternsInput: FC<PatternsInputProps> = ({ value, onChange }) => {
|
||||
label={t("button.add_pattern")}
|
||||
actions={actions}
|
||||
onClick={(action) => addInput(action.defaultValue as Pattern)}
|
||||
icon={<Add />}
|
||||
icon={<AddIcon />}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
|
@ -256,7 +256,7 @@ const VisualEditorProvider: React.FC<VisualEditorContextProps> = ({
|
||||
const createNode = (payload: any) => {
|
||||
payload.position = payload.position || getCentroid();
|
||||
payload.category = payload.category || selectedCategoryId;
|
||||
|
||||
console.log("====", payload);
|
||||
return createBlock(payload, {
|
||||
onSuccess({ id }) {
|
||||
addNode({
|
||||
|
@ -64,7 +64,7 @@ export interface PayloadPattern {
|
||||
value: string;
|
||||
// @todo : rename 'attachment' to 'attachments'
|
||||
// @todo: If undefined, that means the payload could be either quick_reply or button
|
||||
// We will move soon so that it will be a required attribute
|
||||
// We should update soon so that it will be a required attribute
|
||||
type?: PayloadType;
|
||||
}
|
||||
|
||||
@ -81,12 +81,14 @@ export type PatternType =
|
||||
| "nlp"
|
||||
| "menu"
|
||||
| "content"
|
||||
| "outcome"
|
||||
| "payload"
|
||||
| "text";
|
||||
|
||||
export interface IBlockAttributes {
|
||||
name: string;
|
||||
patterns?: Pattern[];
|
||||
outcomes?: string[];
|
||||
trigger_labels?: string[];
|
||||
trigger_channels?: string[];
|
||||
assign_labels?: string[];
|
||||
|
@ -30,6 +30,7 @@ export enum PayloadType {
|
||||
content = "content",
|
||||
quick_reply = "quick_reply",
|
||||
button = "button",
|
||||
outcome = "outcome",
|
||||
}
|
||||
|
||||
export enum FileType {
|
||||
|
@ -14,3 +14,7 @@ export const slugify = (str: string) => {
|
||||
.replace(/\s+/g, "-")
|
||||
.replace(/-+/g, "_");
|
||||
};
|
||||
|
||||
export const getNamespace = (extensionName: string) => {
|
||||
return extensionName.replaceAll("-", "_");
|
||||
};
|
||||
|
Loading…
Reference in New Issue
Block a user