mirror of
https://github.com/hexastack/hexabot
synced 2024-12-02 09:05:06 +00:00
update: unify postback types
This commit is contained in:
parent
8859a2289c
commit
2c2cbb94b1
@ -10,12 +10,10 @@ import { Grid, MenuItem, TextFieldProps } from "@mui/material";
|
|||||||
import { FC, useEffect, useState } from "react";
|
import { FC, useEffect, useState } from "react";
|
||||||
import { RegisterOptions, useFormContext } from "react-hook-form";
|
import { RegisterOptions, useFormContext } from "react-hook-form";
|
||||||
|
|
||||||
import AutoCompleteEntitySelect from "@/app-components/inputs/AutoCompleteEntitySelect";
|
|
||||||
import { Input } from "@/app-components/inputs/Input";
|
import { Input } from "@/app-components/inputs/Input";
|
||||||
import NlpPatternSelect from "@/app-components/inputs/NlpPatternSelect";
|
import NlpPatternSelect from "@/app-components/inputs/NlpPatternSelect";
|
||||||
import { RegexInput } from "@/app-components/inputs/RegexInput";
|
import { RegexInput } from "@/app-components/inputs/RegexInput";
|
||||||
import { useTranslate } from "@/hooks/useTranslate";
|
import { useTranslate } from "@/hooks/useTranslate";
|
||||||
import { EntityType, Format } from "@/services/types";
|
|
||||||
import {
|
import {
|
||||||
IBlockAttributes,
|
IBlockAttributes,
|
||||||
IBlockFull,
|
IBlockFull,
|
||||||
@ -24,10 +22,8 @@ import {
|
|||||||
PatternType,
|
PatternType,
|
||||||
PayloadPattern,
|
PayloadPattern,
|
||||||
} from "@/types/block.types";
|
} from "@/types/block.types";
|
||||||
import { IMenuItem } from "@/types/menu.types";
|
|
||||||
|
|
||||||
import { ContentPostbackInput } from "./ContentPostbackInput";
|
import { PostbackInputV2 } from "./PostbackInputV2";
|
||||||
import { PostbackInput } from "./PostbackInput";
|
|
||||||
|
|
||||||
const isRegex = (str: Pattern) => {
|
const isRegex = (str: Pattern) => {
|
||||||
return typeof str === "string" && str.startsWith("/") && str.endsWith("/");
|
return typeof str === "string" && str.startsWith("/") && str.endsWith("/");
|
||||||
@ -77,8 +73,6 @@ const PatternInput: FC<PatternInputProps> = ({
|
|||||||
{ value: "regex", label: t("label.regex") },
|
{ value: "regex", label: t("label.regex") },
|
||||||
{ value: "payload", label: t("label.postback") },
|
{ value: "payload", label: t("label.postback") },
|
||||||
{ value: "nlp", label: t("label.nlp") },
|
{ value: "nlp", label: t("label.nlp") },
|
||||||
{ value: "menu", label: t("label.menu") },
|
|
||||||
{ value: "content", label: t("label.content") },
|
|
||||||
];
|
];
|
||||||
const registerInput = (
|
const registerInput = (
|
||||||
errorMessage: string,
|
errorMessage: string,
|
||||||
@ -110,7 +104,11 @@ const PatternInput: FC<PatternInputProps> = ({
|
|||||||
<Input
|
<Input
|
||||||
select
|
select
|
||||||
label={t("label.type")}
|
label={t("label.type")}
|
||||||
value={patternType}
|
value={
|
||||||
|
["menu", "content", "payload"].includes(patternType)
|
||||||
|
? "payload"
|
||||||
|
: patternType
|
||||||
|
}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
const selected = e.target.value as PatternType;
|
const selected = e.target.value as PatternType;
|
||||||
|
|
||||||
@ -151,39 +149,8 @@ const PatternInput: FC<PatternInputProps> = ({
|
|||||||
onChange={setPattern}
|
onChange={setPattern}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{patternType === "menu" ? (
|
{["payload", "content", "menu"].includes(patternType) ? (
|
||||||
<AutoCompleteEntitySelect<IMenuItem, "title", false>
|
<PostbackInputV2
|
||||||
value={pattern ? (pattern as PayloadPattern).value : null}
|
|
||||||
searchFields={["title"]}
|
|
||||||
entity={EntityType.MENU}
|
|
||||||
format={Format.BASIC}
|
|
||||||
idKey="payload"
|
|
||||||
labelKey="title"
|
|
||||||
label={t("label.menu")}
|
|
||||||
multiple={false}
|
|
||||||
onChange={(_e, menuItem) => {
|
|
||||||
menuItem &&
|
|
||||||
setPattern({
|
|
||||||
label: menuItem?.title,
|
|
||||||
value: menuItem?.payload,
|
|
||||||
type: "menu",
|
|
||||||
} as PayloadPattern);
|
|
||||||
}}
|
|
||||||
preprocess={(items) => {
|
|
||||||
return items.filter((item) => "payload" in item);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
) : null}
|
|
||||||
{patternType === "content" ? (
|
|
||||||
<ContentPostbackInput
|
|
||||||
onChange={(payload) => {
|
|
||||||
payload && setPattern(payload);
|
|
||||||
}}
|
|
||||||
value={pattern ? (pattern as PayloadPattern).value : null}
|
|
||||||
/>
|
|
||||||
) : null}
|
|
||||||
{patternType === "payload" ? (
|
|
||||||
<PostbackInput
|
|
||||||
onChange={(payload) => {
|
onChange={(payload) => {
|
||||||
payload && setPattern(payload);
|
payload && setPattern(payload);
|
||||||
}}
|
}}
|
||||||
|
@ -0,0 +1,234 @@
|
|||||||
|
/*
|
||||||
|
* 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 { Box, Skeleton, Typography } from "@mui/material";
|
||||||
|
import { useMemo } from "react";
|
||||||
|
|
||||||
|
import AutoCompleteSelect from "@/app-components/inputs/AutoCompleteSelect";
|
||||||
|
import { useFind } from "@/hooks/crud/useFind";
|
||||||
|
import { useGetFromCache } from "@/hooks/crud/useGet";
|
||||||
|
import { useTranslate } from "@/hooks/useTranslate";
|
||||||
|
import { EntityType, Format } from "@/services/types";
|
||||||
|
import { IBlock, PayloadPattern } from "@/types/block.types";
|
||||||
|
import {
|
||||||
|
PostBackButton,
|
||||||
|
StdOutgoingButtonsMessage,
|
||||||
|
StdOutgoingQuickRepliesMessage,
|
||||||
|
StdQuickReply,
|
||||||
|
} from "@/types/message.types";
|
||||||
|
|
||||||
|
import { useBlock } from "../../BlockFormProvider";
|
||||||
|
|
||||||
|
type PayloadOption = {
|
||||||
|
id: string;
|
||||||
|
label: string;
|
||||||
|
group?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type ContentPayloadOption = {
|
||||||
|
id: string;
|
||||||
|
label: string;
|
||||||
|
group?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type PostbackInputProps = {
|
||||||
|
value?: string | null;
|
||||||
|
onChange: (pattern: PayloadPattern) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const PostbackInputV2 = ({ value, onChange }: PostbackInputProps) => {
|
||||||
|
const block = useBlock();
|
||||||
|
const getBlockFromCache = useGetFromCache(EntityType.BLOCK);
|
||||||
|
const { data: menu } = useFind(
|
||||||
|
{ entity: EntityType.MENU, format: Format.FULL },
|
||||||
|
{ hasCount: false },
|
||||||
|
);
|
||||||
|
const { data: contents } = useFind(
|
||||||
|
{ entity: EntityType.CONTENT, format: Format.FULL },
|
||||||
|
{
|
||||||
|
hasCount: false,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
const { t } = useTranslate();
|
||||||
|
// General options
|
||||||
|
const generalOptions = [
|
||||||
|
{
|
||||||
|
id: "GET_STARTED",
|
||||||
|
label: t("label.get_started"),
|
||||||
|
group: t("label.general"),
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
id: "VIEW_MORE",
|
||||||
|
label: t("label.view_more"),
|
||||||
|
group: t("label.general"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "LOCATION",
|
||||||
|
label: t("label.location"),
|
||||||
|
group: t("label.general"),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
// Gather previous blocks buttons
|
||||||
|
const btnOptions = useMemo(
|
||||||
|
() =>
|
||||||
|
(block?.previousBlocks || [])
|
||||||
|
.map((b) => getBlockFromCache(b))
|
||||||
|
.filter((b) => {
|
||||||
|
return b && typeof b.message === "object" && "buttons" in b.message;
|
||||||
|
})
|
||||||
|
.map((b) => b as IBlock)
|
||||||
|
.reduce((acc, b) => {
|
||||||
|
const postbackButtons = (
|
||||||
|
(b.message as StdOutgoingButtonsMessage)?.buttons || []
|
||||||
|
)
|
||||||
|
.filter((btn) => btn.type === "postback")
|
||||||
|
.map((btn) => {
|
||||||
|
return { ...btn, group: b.name };
|
||||||
|
});
|
||||||
|
|
||||||
|
return acc.concat(postbackButtons);
|
||||||
|
}, [] as (PostBackButton & { group: string })[])
|
||||||
|
.map((btn) => {
|
||||||
|
return {
|
||||||
|
id: btn.payload,
|
||||||
|
label: btn.title,
|
||||||
|
group: btn.group,
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
[block?.previousBlocks, getBlockFromCache],
|
||||||
|
);
|
||||||
|
// Gather previous blocks quick replies
|
||||||
|
const qrOptions = useMemo(
|
||||||
|
() =>
|
||||||
|
(block?.previousBlocks || [])
|
||||||
|
.map((b) => getBlockFromCache(b))
|
||||||
|
.filter((b) => {
|
||||||
|
return (
|
||||||
|
b && typeof b.message === "object" && "quickReplies" in b.message
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.map((b) => b as IBlock)
|
||||||
|
.reduce((acc, b) => {
|
||||||
|
const postbackQuickReplies = (
|
||||||
|
(b.message as StdOutgoingQuickRepliesMessage)?.quickReplies || []
|
||||||
|
)
|
||||||
|
.filter((btn) => btn.content_type === "text")
|
||||||
|
.map((btn) => {
|
||||||
|
return { ...btn, group: b.name };
|
||||||
|
});
|
||||||
|
|
||||||
|
return acc.concat(postbackQuickReplies);
|
||||||
|
}, [] as (StdQuickReply & { group: string })[])
|
||||||
|
.map((btn) => {
|
||||||
|
return {
|
||||||
|
id: btn.payload as string,
|
||||||
|
label: btn.title as string,
|
||||||
|
group: btn.group,
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
[block?.previousBlocks],
|
||||||
|
);
|
||||||
|
const menuOptions = menu
|
||||||
|
.filter((menu) => menu.payload)
|
||||||
|
.map(({ payload, title }) => ({
|
||||||
|
id: payload as string,
|
||||||
|
label: title as string,
|
||||||
|
group: "menu",
|
||||||
|
}));
|
||||||
|
const contentOptions = useMemo(
|
||||||
|
() =>
|
||||||
|
(block?.previousBlocks || [])
|
||||||
|
.map((bId) => getBlockFromCache(bId))
|
||||||
|
.filter((b) => {
|
||||||
|
return (
|
||||||
|
b &&
|
||||||
|
b.options?.content?.entity &&
|
||||||
|
b.options.content.buttons.length > 0
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.map((b) => b as IBlock)
|
||||||
|
.map((b) => {
|
||||||
|
const availableContents = (contents || []).filter(
|
||||||
|
({ entity, status }) =>
|
||||||
|
status && entity === b.options?.content?.entity,
|
||||||
|
);
|
||||||
|
|
||||||
|
return (b.options?.content?.buttons || []).reduce((payloads, btn) => {
|
||||||
|
// Return a payload for each node/button combination
|
||||||
|
payloads.push({
|
||||||
|
id: btn.title,
|
||||||
|
label: btn.title,
|
||||||
|
group: "content",
|
||||||
|
});
|
||||||
|
|
||||||
|
return availableContents.reduce((acc, n) => {
|
||||||
|
acc.push({
|
||||||
|
id: n.title,
|
||||||
|
label: n.title,
|
||||||
|
group: "content",
|
||||||
|
});
|
||||||
|
|
||||||
|
return acc;
|
||||||
|
}, payloads);
|
||||||
|
}, [] as ContentPayloadOption[]);
|
||||||
|
})
|
||||||
|
.flat(),
|
||||||
|
[block?.previousBlocks, contents, getBlockFromCache],
|
||||||
|
);
|
||||||
|
// Concat all previous blocks
|
||||||
|
const options = [
|
||||||
|
...generalOptions,
|
||||||
|
...btnOptions,
|
||||||
|
...qrOptions,
|
||||||
|
...menuOptions,
|
||||||
|
...contentOptions,
|
||||||
|
];
|
||||||
|
const existOption = options.find((e) => e.id === value);
|
||||||
|
|
||||||
|
if (!existOption) {
|
||||||
|
return (
|
||||||
|
<Skeleton animation="wave" variant="rounded" width="100%" height={56} />
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<AutoCompleteSelect<PayloadOption, "label", false>
|
||||||
|
value={value}
|
||||||
|
options={options}
|
||||||
|
labelKey="label"
|
||||||
|
label={t("label.postback")}
|
||||||
|
multiple={false}
|
||||||
|
onChange={(_e, content) => {
|
||||||
|
if (content) {
|
||||||
|
onChange({
|
||||||
|
label: content.label,
|
||||||
|
value: content.id,
|
||||||
|
type: ["content", "menu"].includes(content.group || "")
|
||||||
|
? content.group
|
||||||
|
: undefined,
|
||||||
|
} as PayloadPattern);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
groupBy={(option) => {
|
||||||
|
return option.group ?? t("label.other");
|
||||||
|
}}
|
||||||
|
getOptionLabel={({ group, label }) => `${group}:${label}`}
|
||||||
|
renderGroup={(params) => (
|
||||||
|
<li key={params.key}>
|
||||||
|
<Typography component="h4" p={2} fontWeight={700} color="primary">
|
||||||
|
{params.group}
|
||||||
|
</Typography>
|
||||||
|
<Box>{params.children}</Box>
|
||||||
|
</li>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
Loading…
Reference in New Issue
Block a user