mirror of
https://github.com/hexastack/hexabot
synced 2025-06-26 18:27:28 +00:00
fix(frontend): resolve file conflicts
This commit is contained in:
commit
fba71210a2
@ -10,6 +10,7 @@ import { HttpModule } from '@nestjs/axios';
|
||||
import { Global, Module } from '@nestjs/common';
|
||||
import { InjectDynamicProviders } from 'nestjs-dynamic-providers';
|
||||
|
||||
import { ChatModule } from '@/chat/chat.module';
|
||||
import { CmsModule } from '@/cms/cms.module';
|
||||
import { NlpModule } from '@/nlp/nlp.module';
|
||||
|
||||
@ -26,7 +27,7 @@ import { HelperService } from './helper.service';
|
||||
'dist/.hexabot/custom/extensions/helpers/**/*.helper.js',
|
||||
)
|
||||
@Module({
|
||||
imports: [HttpModule, NlpModule, CmsModule],
|
||||
imports: [HttpModule, NlpModule, CmsModule, ChatModule],
|
||||
controllers: [HelperController],
|
||||
providers: [HelperService],
|
||||
exports: [HelperService],
|
||||
|
@ -29,6 +29,7 @@ export const DialogFormButtons = ({
|
||||
return (
|
||||
<Grid
|
||||
p="0.3rem 1rem"
|
||||
gap={1}
|
||||
width="100%"
|
||||
display="flex"
|
||||
justifyContent="space-between"
|
||||
@ -40,7 +41,7 @@ export const DialogFormButtons = ({
|
||||
startIcon={<CloseIcon />}
|
||||
{...cancelButtonProps}
|
||||
>
|
||||
{t(cancelButtonTitle)}
|
||||
{cancelButtonProps?.text || t(cancelButtonTitle)}
|
||||
</Button>
|
||||
<Button
|
||||
variant="contained"
|
||||
@ -48,7 +49,7 @@ export const DialogFormButtons = ({
|
||||
startIcon={<CheckIcon />}
|
||||
{...confirmButtonProps}
|
||||
>
|
||||
{t(confirmButtonTitle)}
|
||||
{confirmButtonProps?.text || t(confirmButtonTitle)}
|
||||
</Button>
|
||||
</Grid>
|
||||
);
|
||||
|
@ -25,18 +25,19 @@ import {
|
||||
|
||||
const SelectableBox = styled(Box)({
|
||||
position: "relative",
|
||||
height: "30px",
|
||||
marginBottom: "1rem",
|
||||
"& .highlight, & .editable": {
|
||||
position: "absolute",
|
||||
top: 0,
|
||||
display: "block",
|
||||
width: "100%",
|
||||
padding: "4px",
|
||||
padding: "0 4px",
|
||||
lineHeight: 1.5,
|
||||
whiteSpaceCollapse: "preserve",
|
||||
},
|
||||
"& .editable": {
|
||||
position: "relative",
|
||||
backgroundColor: "transparent",
|
||||
padding: "0px 4px",
|
||||
color: "#000",
|
||||
},
|
||||
});
|
||||
@ -169,10 +170,10 @@ const Selectable: FC<SelectableProps> = ({
|
||||
) {
|
||||
const inputContainer = editableRef.current;
|
||||
let substring: string = "";
|
||||
let input: HTMLInputElement | null = null;
|
||||
let input: HTMLTextAreaElement | null = null;
|
||||
|
||||
if (inputContainer) {
|
||||
input = inputContainer.querySelector("input");
|
||||
input = inputContainer.querySelector("textarea");
|
||||
|
||||
if (
|
||||
input &&
|
||||
@ -267,6 +268,7 @@ const Selectable: FC<SelectableProps> = ({
|
||||
/>
|
||||
))}
|
||||
<Input
|
||||
multiline
|
||||
ref={editableRef}
|
||||
className="editable"
|
||||
fullWidth
|
||||
|
@ -184,7 +184,6 @@ export default function NlpSample() {
|
||||
{ defaultValues: data },
|
||||
{
|
||||
maxWidth: "md",
|
||||
hasButtons: false,
|
||||
},
|
||||
);
|
||||
},
|
||||
|
@ -6,19 +6,55 @@
|
||||
* 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 { FC, Fragment } from "react";
|
||||
import AddIcon from "@mui/icons-material/Add";
|
||||
import DeleteIcon from "@mui/icons-material/Delete";
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
Chip,
|
||||
debounce,
|
||||
FormControl,
|
||||
FormControlLabel,
|
||||
FormLabel,
|
||||
IconButton,
|
||||
Radio,
|
||||
RadioGroup,
|
||||
Typography,
|
||||
} from "@mui/material";
|
||||
import { FC, Fragment, useCallback, useEffect, useMemo, useState } from "react";
|
||||
import { Controller, useFieldArray, useForm } from "react-hook-form";
|
||||
import { useQuery } from "react-query";
|
||||
|
||||
import { ContentContainer, ContentItem } from "@/app-components/dialogs";
|
||||
import AutoCompleteEntitySelect from "@/app-components/inputs/AutoCompleteEntitySelect";
|
||||
import AutoCompleteSelect from "@/app-components/inputs/AutoCompleteSelect";
|
||||
import Selectable from "@/app-components/inputs/Selectable";
|
||||
import { useCreate } from "@/hooks/crud/useCreate";
|
||||
import { useGetFromCache } from "@/hooks/crud/useGet";
|
||||
import { useUpdate } from "@/hooks/crud/useUpdate";
|
||||
import { useApiClient } from "@/hooks/useApiClient";
|
||||
import { useNlp } from "@/hooks/useNlp";
|
||||
import { useToast } from "@/hooks/useToast";
|
||||
import { useTranslate } from "@/hooks/useTranslate";
|
||||
import { EntityType } from "@/services/types";
|
||||
import { ComponentFormProps } from "@/types/common/dialogs.types";
|
||||
import { EntityType, Format } from "@/services/types";
|
||||
import {
|
||||
ComponentFormProps,
|
||||
FormButtonsProps,
|
||||
} from "@/types/common/dialogs.types";
|
||||
import { ILanguage } from "@/types/language.types";
|
||||
import { INlpEntity } from "@/types/nlp-entity.types";
|
||||
import {
|
||||
INlpDatasetKeywordEntity,
|
||||
INlpDatasetPatternEntity,
|
||||
INlpDatasetSample,
|
||||
INlpDatasetSampleAttributes,
|
||||
INlpDatasetTraitEntity,
|
||||
INlpSample,
|
||||
INlpSampleFormAttributes,
|
||||
INlpSampleFull,
|
||||
NlpSampleType,
|
||||
} from "@/types/nlp-sample.types";
|
||||
|
||||
import NlpDatasetSample from "./NlpTrainForm";
|
||||
import { INlpValue } from "@/types/nlp-value.types";
|
||||
|
||||
export const NlpSampleForm: FC<ComponentFormProps<INlpDatasetSample>> = ({
|
||||
data: { defaultValues: nlpDatasetSample },
|
||||
@ -28,15 +64,134 @@ export const NlpSampleForm: FC<ComponentFormProps<INlpDatasetSample>> = ({
|
||||
}) => {
|
||||
const { t } = useTranslate();
|
||||
const { toast } = useToast();
|
||||
const { mutate: updateSample } = useUpdate(EntityType.NLP_SAMPLE, {
|
||||
const options = {
|
||||
onError: () => {
|
||||
toast.error(t("message.internal_server_error"));
|
||||
},
|
||||
onSuccess: () => {
|
||||
toast.success(t("message.success_save"));
|
||||
},
|
||||
};
|
||||
const { mutate: createSample } = useCreate<
|
||||
EntityType.NLP_SAMPLE,
|
||||
INlpDatasetSampleAttributes,
|
||||
INlpSample,
|
||||
INlpSampleFull
|
||||
>(EntityType.NLP_SAMPLE, {
|
||||
...options,
|
||||
onSuccess: () => {
|
||||
options.onSuccess();
|
||||
refetchAllEntities();
|
||||
reset({
|
||||
...defaultValues,
|
||||
text: "",
|
||||
});
|
||||
},
|
||||
});
|
||||
const onSubmitForm = (form: INlpSampleFormAttributes) => {
|
||||
const { mutate: updateSample } = useUpdate<
|
||||
EntityType.NLP_SAMPLE,
|
||||
INlpDatasetSampleAttributes
|
||||
>(EntityType.NLP_SAMPLE, options);
|
||||
const {
|
||||
allTraitEntities,
|
||||
allKeywordEntities,
|
||||
allPatternEntities,
|
||||
refetchAllEntities,
|
||||
} = useNlp();
|
||||
const getNlpValueFromCache = useGetFromCache(EntityType.NLP_VALUE);
|
||||
const defaultValues: INlpSampleFormAttributes = useMemo(
|
||||
() => ({
|
||||
type: nlpDatasetSample?.type || NlpSampleType.train,
|
||||
text: nlpDatasetSample?.text || "",
|
||||
language: nlpDatasetSample?.language || null,
|
||||
traitEntities: [...allTraitEntities.values()].map((e) => {
|
||||
return {
|
||||
entity: e.name,
|
||||
value:
|
||||
(nlpDatasetSample?.entities || []).find(
|
||||
(se) => se.entity === e.name,
|
||||
)?.value || "",
|
||||
};
|
||||
}) as INlpDatasetTraitEntity[],
|
||||
keywordEntities: (nlpDatasetSample?.entities || []).filter((e) =>
|
||||
allKeywordEntities.has(e.entity),
|
||||
) as INlpDatasetKeywordEntity[],
|
||||
}),
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[allKeywordEntities, allTraitEntities, JSON.stringify(nlpDatasetSample)],
|
||||
);
|
||||
const { handleSubmit, control, register, reset, setValue, watch } =
|
||||
useForm<INlpSampleFormAttributes>({
|
||||
defaultValues,
|
||||
});
|
||||
const currentText = watch("text");
|
||||
const currentType = watch("type");
|
||||
const { apiClient } = useApiClient();
|
||||
const [patternEntities, setPatternEntities] = useState<
|
||||
INlpDatasetPatternEntity[]
|
||||
>([]);
|
||||
const { fields: traitEntities, update: updateTraitEntity } = useFieldArray({
|
||||
control,
|
||||
name: "traitEntities",
|
||||
});
|
||||
const {
|
||||
fields: keywordEntities,
|
||||
insert: insertKeywordEntity,
|
||||
update: updateKeywordEntity,
|
||||
remove: removeKeywordEntity,
|
||||
} = useFieldArray({
|
||||
control,
|
||||
name: "keywordEntities",
|
||||
});
|
||||
// Auto-predict on text change
|
||||
const debounceSetText = useCallback(
|
||||
debounce((text: string) => {
|
||||
setValue("text", text);
|
||||
}, 400),
|
||||
[setValue],
|
||||
);
|
||||
const { isLoading } = useQuery({
|
||||
queryKey: ["nlp-prediction", currentText],
|
||||
queryFn: async () => {
|
||||
return await apiClient.predictNlp(currentText);
|
||||
},
|
||||
onSuccess: (prediction) => {
|
||||
const predictedTraitEntities: INlpDatasetTraitEntity[] =
|
||||
prediction.entities.filter((e) => allTraitEntities.has(e.entity));
|
||||
const predictedKeywordEntities = prediction.entities.filter((e) =>
|
||||
allKeywordEntities.has(e.entity),
|
||||
) as INlpDatasetKeywordEntity[];
|
||||
const predictedPatternEntities = prediction.entities.filter((e) =>
|
||||
allPatternEntities.has(e.entity),
|
||||
) as INlpDatasetKeywordEntity[];
|
||||
const language = prediction.entities.find(
|
||||
({ entity }) => entity === "language",
|
||||
);
|
||||
|
||||
setValue("language", language?.value || "");
|
||||
setValue("traitEntities", predictedTraitEntities);
|
||||
setValue("keywordEntities", predictedKeywordEntities);
|
||||
setPatternEntities(predictedPatternEntities);
|
||||
},
|
||||
enabled:
|
||||
// Inbox sample update
|
||||
nlpDatasetSample?.type === "inbox" ||
|
||||
// New sample
|
||||
(!nlpDatasetSample && !!currentText),
|
||||
});
|
||||
const findInsertIndex = (newItem: INlpDatasetKeywordEntity): number => {
|
||||
const index = keywordEntities.findIndex(
|
||||
(entity) => entity.start && newItem.start && entity.start > newItem.start,
|
||||
);
|
||||
|
||||
return index === -1 ? keywordEntities.length : index;
|
||||
};
|
||||
const [selection, setSelection] = useState<{
|
||||
value: string;
|
||||
start: number;
|
||||
end: number;
|
||||
} | null>(null);
|
||||
const onSubmitForm = async (form: INlpSampleFormAttributes) => {
|
||||
if (nlpDatasetSample?.id) {
|
||||
updateSample(
|
||||
{
|
||||
@ -54,15 +209,328 @@ export const NlpSampleForm: FC<ComponentFormProps<INlpDatasetSample>> = ({
|
||||
},
|
||||
},
|
||||
);
|
||||
} else {
|
||||
createSample({
|
||||
text: form.text,
|
||||
type: form.type,
|
||||
entities: [...form.traitEntities, ...form.keywordEntities],
|
||||
language: form.language,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
reset(defaultValues);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [JSON.stringify(defaultValues)]);
|
||||
|
||||
const cancelButtonProps = {
|
||||
sx: {
|
||||
display: "inline-block",
|
||||
overflow: "hidden",
|
||||
textAlign: "left",
|
||||
whiteSpace: "nowrap",
|
||||
textOverflow: "ellipsis",
|
||||
"& .MuiButton-startIcon": {
|
||||
top: "4px",
|
||||
margin: "auto 4px auto auto",
|
||||
display: "inline-block",
|
||||
position: "relative",
|
||||
},
|
||||
},
|
||||
color: "secondary",
|
||||
variant: "contained",
|
||||
onClick: () => {
|
||||
const newKeywordEntity = {
|
||||
...selection,
|
||||
entity: "",
|
||||
} as INlpDatasetKeywordEntity;
|
||||
const newIndex = findInsertIndex(newKeywordEntity);
|
||||
|
||||
selection && insertKeywordEntity(newIndex, newKeywordEntity);
|
||||
setSelection(null);
|
||||
},
|
||||
disabled: !selection?.value,
|
||||
startIcon: <AddIcon />,
|
||||
} satisfies FormButtonsProps["cancelButtonProps"];
|
||||
const confirmButtonProps = {
|
||||
sx: { minWidth: "120px" },
|
||||
value: "button.validate",
|
||||
variant: "contained",
|
||||
disabled: !(
|
||||
currentText !== "" &&
|
||||
currentType !== NlpSampleType.inbox &&
|
||||
traitEntities.every((e) => e.value !== "") &&
|
||||
keywordEntities.every((e) => e.value !== "")
|
||||
),
|
||||
onClick: handleSubmit(onSubmitForm),
|
||||
} satisfies FormButtonsProps["confirmButtonProps"];
|
||||
|
||||
return (
|
||||
<Wrapper onSubmit={() => {}} {...WrapperProps}>
|
||||
<NlpDatasetSample
|
||||
sample={nlpDatasetSample || undefined}
|
||||
submitForm={onSubmitForm}
|
||||
/>
|
||||
<Wrapper
|
||||
onSubmit={() => {}}
|
||||
{...WrapperProps}
|
||||
cancelButtonProps={{
|
||||
...WrapperProps?.cancelButtonProps,
|
||||
text: !selection?.value
|
||||
? t("button.select_some_text")
|
||||
: t("button.add_nlp_entity", { 0: selection.value }),
|
||||
...cancelButtonProps,
|
||||
}}
|
||||
confirmButtonProps={{
|
||||
...WrapperProps?.confirmButtonProps,
|
||||
...confirmButtonProps,
|
||||
}}
|
||||
>
|
||||
<form onSubmit={handleSubmit(onSubmitForm)}>
|
||||
<ContentContainer>
|
||||
<ContentItem
|
||||
display="flex"
|
||||
flexDirection="row"
|
||||
justifyContent="space-between"
|
||||
>
|
||||
<Typography variant="h6" display="inline-block">
|
||||
{t("title.nlp_train")}
|
||||
</Typography>
|
||||
<FormControl>
|
||||
<FormLabel>{t("label.type")}</FormLabel>
|
||||
<RadioGroup
|
||||
row
|
||||
defaultValue={
|
||||
nlpDatasetSample?.type === NlpSampleType.test
|
||||
? NlpSampleType.test
|
||||
: NlpSampleType.train
|
||||
}
|
||||
>
|
||||
{Object.values(NlpSampleType)
|
||||
.filter((type) => type !== "inbox")
|
||||
.map((type, index) => (
|
||||
<FormControlLabel
|
||||
key={index}
|
||||
value={type}
|
||||
control={<Radio {...register("type")} />}
|
||||
label={t(`label.${type}`)}
|
||||
/>
|
||||
))}
|
||||
</RadioGroup>
|
||||
</FormControl>
|
||||
</ContentItem>
|
||||
<ContentItem>
|
||||
<Selectable
|
||||
defaultValue={currentText}
|
||||
keywordEntities={keywordEntities}
|
||||
patternEntities={patternEntities}
|
||||
placeholder={t("placeholder.nlp_sample_text")}
|
||||
onSelect={(newSelection, start, end) => {
|
||||
newSelection !== selection?.value &&
|
||||
setSelection({
|
||||
value: newSelection,
|
||||
start,
|
||||
end,
|
||||
});
|
||||
}}
|
||||
onChange={({ text, entities }) => {
|
||||
debounceSetText(text);
|
||||
setValue(
|
||||
"keywordEntities",
|
||||
entities.map(({ entity, value, start, end }) => ({
|
||||
entity,
|
||||
value,
|
||||
start,
|
||||
end,
|
||||
})),
|
||||
);
|
||||
setPatternEntities([]);
|
||||
}}
|
||||
loading={isLoading}
|
||||
/>
|
||||
</ContentItem>
|
||||
<Box display="flex" flexDirection="column">
|
||||
<ContentItem
|
||||
display="flex"
|
||||
flexDirection="row"
|
||||
maxWidth="50%"
|
||||
gap={2}
|
||||
>
|
||||
<Controller
|
||||
name="language"
|
||||
control={control}
|
||||
render={({ field }) => {
|
||||
const { onChange, ...rest } = field;
|
||||
|
||||
return (
|
||||
<AutoCompleteEntitySelect<ILanguage, "title", false>
|
||||
fullWidth={true}
|
||||
autoFocus
|
||||
searchFields={["title", "code"]}
|
||||
entity={EntityType.LANGUAGE}
|
||||
format={Format.BASIC}
|
||||
labelKey="title"
|
||||
idKey="code"
|
||||
label={t("label.language")}
|
||||
multiple={false}
|
||||
{...field}
|
||||
onChange={(_e, selected) => {
|
||||
onChange(selected?.code);
|
||||
}}
|
||||
{...rest}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</ContentItem>
|
||||
{traitEntities.map((traitEntity, index) => (
|
||||
<ContentItem
|
||||
key={traitEntity.id}
|
||||
display="flex"
|
||||
flexDirection="row"
|
||||
maxWidth="50%"
|
||||
gap={2}
|
||||
>
|
||||
<Controller
|
||||
name={`traitEntities.${index}`}
|
||||
rules={{ required: true }}
|
||||
control={control}
|
||||
render={({ field }) => {
|
||||
const { onChange: _, value, ...rest } = field;
|
||||
const options = (
|
||||
allTraitEntities.get(traitEntity.entity)?.values || []
|
||||
).map((v) => getNlpValueFromCache(v)!);
|
||||
|
||||
return (
|
||||
<>
|
||||
<AutoCompleteSelect<INlpValue, "value", false>
|
||||
fullWidth={true}
|
||||
options={options}
|
||||
idKey="value"
|
||||
labelKey="value"
|
||||
label={value.entity}
|
||||
multiple={false}
|
||||
value={value.value}
|
||||
onChange={(_e, selected, ..._) => {
|
||||
updateTraitEntity(index, {
|
||||
entity: value.entity,
|
||||
value: selected?.value || "",
|
||||
});
|
||||
}}
|
||||
{...rest}
|
||||
/>
|
||||
{value?.confidence &&
|
||||
typeof value?.confidence === "number" && (
|
||||
<Chip
|
||||
sx={{ marginTop: 0.5 }}
|
||||
variant="available"
|
||||
label={`${(value?.confidence * 100).toFixed(
|
||||
2,
|
||||
)}% ${t("label.confidence")}`}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</ContentItem>
|
||||
))}
|
||||
</Box>
|
||||
<Box display="flex" flexDirection="column">
|
||||
{keywordEntities.map((keywordEntity, index) => (
|
||||
<ContentItem
|
||||
key={keywordEntity.id}
|
||||
display="flex"
|
||||
maxWidth="50%"
|
||||
gap={2}
|
||||
>
|
||||
<IconButton onClick={() => removeKeywordEntity(index)}>
|
||||
<DeleteIcon />
|
||||
</IconButton>
|
||||
<Controller
|
||||
name={`keywordEntities.${index}.entity`}
|
||||
control={control}
|
||||
render={({ field }) => {
|
||||
const { onChange: _, ...rest } = field;
|
||||
const options = [...allKeywordEntities.values()];
|
||||
|
||||
return (
|
||||
<AutoCompleteSelect<INlpEntity, "name", false>
|
||||
fullWidth={true}
|
||||
options={options}
|
||||
idKey="name"
|
||||
labelKey="name"
|
||||
label={t("label.nlp_entity")}
|
||||
multiple={false}
|
||||
onChange={(_e, selected, ..._) => {
|
||||
updateKeywordEntity(index, {
|
||||
...keywordEntities[index],
|
||||
entity: selected?.name || "",
|
||||
});
|
||||
}}
|
||||
{...rest}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
<Controller
|
||||
name={`keywordEntities.${index}.value`}
|
||||
control={control}
|
||||
render={({ field }) => {
|
||||
const { onChange: _, value, ...rest } = field;
|
||||
const options = (
|
||||
allKeywordEntities.get(keywordEntity.entity)?.values || []
|
||||
).map((v) => getNlpValueFromCache(v)!);
|
||||
|
||||
return (
|
||||
<AutoCompleteSelect<
|
||||
INlpValue,
|
||||
"value",
|
||||
false,
|
||||
false,
|
||||
true
|
||||
>
|
||||
sx={{ width: "50%" }}
|
||||
idKey="value"
|
||||
labelKey="value"
|
||||
label={t("label.value")}
|
||||
multiple={false}
|
||||
options={options}
|
||||
value={value}
|
||||
freeSolo={true}
|
||||
getOptionLabel={(option) => {
|
||||
return typeof option === "string"
|
||||
? option
|
||||
: option.value;
|
||||
}}
|
||||
onChange={(_e, selected, ..._) => {
|
||||
selected &&
|
||||
updateKeywordEntity(index, {
|
||||
...keywordEntity,
|
||||
value:
|
||||
typeof selected === "string"
|
||||
? selected
|
||||
: selected.value,
|
||||
});
|
||||
}}
|
||||
{...rest}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</ContentItem>
|
||||
))}
|
||||
</Box>
|
||||
</ContentContainer>
|
||||
<ContentItem display="flex" justifyContent="space-between">
|
||||
{nlpDatasetSample ? null : (
|
||||
<>
|
||||
<Button {...cancelButtonProps}>
|
||||
{!selection?.value
|
||||
? t("button.select_some_text")
|
||||
: t("button.add_nlp_entity", { 0: selection.value })}
|
||||
</Button>
|
||||
<Button {...confirmButtonProps}>{t("button.validate")}</Button>
|
||||
</>
|
||||
)}
|
||||
</ContentItem>
|
||||
</form>
|
||||
</Wrapper>
|
||||
);
|
||||
};
|
||||
|
@ -1,460 +0,0 @@
|
||||
/*
|
||||
* 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 AddIcon from "@mui/icons-material/Add";
|
||||
import Check from "@mui/icons-material/Check";
|
||||
import DeleteIcon from "@mui/icons-material/Delete";
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
Chip,
|
||||
debounce,
|
||||
FormControl,
|
||||
FormControlLabel,
|
||||
FormLabel,
|
||||
IconButton,
|
||||
Radio,
|
||||
RadioGroup,
|
||||
Typography,
|
||||
} from "@mui/material";
|
||||
import { FC, useCallback, useEffect, useMemo, useState } from "react";
|
||||
import { Controller, useFieldArray, useForm } from "react-hook-form";
|
||||
import { useQuery } from "react-query";
|
||||
|
||||
import { ContentContainer, ContentItem } from "@/app-components/dialogs";
|
||||
import AutoCompleteEntitySelect from "@/app-components/inputs/AutoCompleteEntitySelect";
|
||||
import AutoCompleteSelect from "@/app-components/inputs/AutoCompleteSelect";
|
||||
import Selectable from "@/app-components/inputs/Selectable";
|
||||
import { useGetFromCache } from "@/hooks/crud/useGet";
|
||||
import { useApiClient } from "@/hooks/useApiClient";
|
||||
import { useNlp } from "@/hooks/useNlp";
|
||||
import { useTranslate } from "@/hooks/useTranslate";
|
||||
import { EntityType, Format } from "@/services/types";
|
||||
import { ILanguage } from "@/types/language.types";
|
||||
import { INlpEntity } from "@/types/nlp-entity.types";
|
||||
import {
|
||||
INlpDatasetKeywordEntity,
|
||||
INlpDatasetPatternEntity,
|
||||
INlpDatasetSample,
|
||||
INlpDatasetTraitEntity,
|
||||
INlpSampleFormAttributes,
|
||||
NlpSampleType,
|
||||
} from "@/types/nlp-sample.types";
|
||||
import { INlpValue } from "@/types/nlp-value.types";
|
||||
|
||||
type NlpDatasetSampleProps = {
|
||||
sample?: INlpDatasetSample;
|
||||
submitForm: (params: INlpSampleFormAttributes) => void;
|
||||
};
|
||||
|
||||
const NlpDatasetSample: FC<NlpDatasetSampleProps> = ({
|
||||
sample,
|
||||
submitForm,
|
||||
}) => {
|
||||
const { t } = useTranslate();
|
||||
const {
|
||||
allTraitEntities,
|
||||
allKeywordEntities,
|
||||
allPatternEntities,
|
||||
refetchAllEntities,
|
||||
} = useNlp();
|
||||
const getNlpValueFromCache = useGetFromCache(EntityType.NLP_VALUE);
|
||||
const defaultValues: INlpSampleFormAttributes = useMemo(
|
||||
() => ({
|
||||
type: sample?.type || NlpSampleType.train,
|
||||
text: sample?.text || "",
|
||||
language: sample?.language || null,
|
||||
traitEntities: [...allTraitEntities.values()].map((e) => {
|
||||
return {
|
||||
entity: e.name,
|
||||
value:
|
||||
(sample?.entities || []).find((se) => se.entity === e.name)
|
||||
?.value || "",
|
||||
};
|
||||
}) as INlpDatasetTraitEntity[],
|
||||
keywordEntities: (sample?.entities || []).filter((e) =>
|
||||
allKeywordEntities.has(e.entity),
|
||||
) as INlpDatasetKeywordEntity[],
|
||||
}),
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[allKeywordEntities, allTraitEntities, JSON.stringify(sample)],
|
||||
);
|
||||
const { handleSubmit, control, register, reset, setValue, watch } =
|
||||
useForm<INlpSampleFormAttributes>({
|
||||
defaultValues,
|
||||
});
|
||||
const currentText = watch("text");
|
||||
const currentType = watch("type");
|
||||
const { apiClient } = useApiClient();
|
||||
const [patternEntities, setPatternEntities] = useState<
|
||||
INlpDatasetPatternEntity[]
|
||||
>([]);
|
||||
const { fields: traitEntities, update: updateTraitEntity } = useFieldArray({
|
||||
control,
|
||||
name: "traitEntities",
|
||||
});
|
||||
const {
|
||||
fields: keywordEntities,
|
||||
insert: insertKeywordEntity,
|
||||
update: updateKeywordEntity,
|
||||
remove: removeKeywordEntity,
|
||||
} = useFieldArray({
|
||||
control,
|
||||
name: "keywordEntities",
|
||||
});
|
||||
// Auto-predict on text change
|
||||
const debounceSetText = useCallback(
|
||||
debounce((text: string) => {
|
||||
setValue("text", text);
|
||||
}, 400),
|
||||
[setValue],
|
||||
);
|
||||
const { isLoading } = useQuery({
|
||||
queryKey: ["nlp-prediction", currentText],
|
||||
queryFn: async () => {
|
||||
return await apiClient.predictNlp(currentText);
|
||||
},
|
||||
onSuccess: (prediction) => {
|
||||
const predictedTraitEntities: INlpDatasetTraitEntity[] =
|
||||
prediction.entities.filter((e) => allTraitEntities.has(e.entity));
|
||||
const predictedKeywordEntities = prediction.entities.filter((e) =>
|
||||
allKeywordEntities.has(e.entity),
|
||||
) as INlpDatasetKeywordEntity[];
|
||||
const predictedPatternEntities = prediction.entities.filter((e) =>
|
||||
allPatternEntities.has(e.entity),
|
||||
) as INlpDatasetKeywordEntity[];
|
||||
const language = prediction.entities.find(
|
||||
({ entity }) => entity === "language",
|
||||
);
|
||||
|
||||
setValue("language", language?.value || "");
|
||||
setValue("traitEntities", predictedTraitEntities);
|
||||
setValue("keywordEntities", predictedKeywordEntities);
|
||||
setPatternEntities(predictedPatternEntities);
|
||||
},
|
||||
enabled:
|
||||
// Inbox sample update
|
||||
sample?.type === "inbox" ||
|
||||
// New sample
|
||||
(!sample && !!currentText),
|
||||
});
|
||||
const findInsertIndex = (newItem: INlpDatasetKeywordEntity): number => {
|
||||
const index = keywordEntities.findIndex(
|
||||
(entity) => entity.start && newItem.start && entity.start > newItem.start,
|
||||
);
|
||||
|
||||
return index === -1 ? keywordEntities.length : index;
|
||||
};
|
||||
const [selection, setSelection] = useState<{
|
||||
value: string;
|
||||
start: number;
|
||||
end: number;
|
||||
} | null>(null);
|
||||
const onSubmitForm = (form: INlpSampleFormAttributes) => {
|
||||
submitForm(form);
|
||||
refetchAllEntities();
|
||||
reset({
|
||||
...defaultValues,
|
||||
text: "",
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
reset(defaultValues);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [JSON.stringify(defaultValues)]);
|
||||
|
||||
return (
|
||||
<Box className="nlp-train" sx={{ position: "relative", p: 2 }}>
|
||||
<form onSubmit={handleSubmit(onSubmitForm)}>
|
||||
<ContentContainer>
|
||||
<ContentItem
|
||||
display="flex"
|
||||
flexDirection="row"
|
||||
justifyContent="space-between"
|
||||
>
|
||||
<Typography variant="h6" display="inline-block">
|
||||
{t("title.nlp_train")}
|
||||
</Typography>
|
||||
<FormControl>
|
||||
<FormLabel>{t("label.type")}</FormLabel>
|
||||
<RadioGroup
|
||||
row
|
||||
defaultValue={
|
||||
sample?.type === NlpSampleType.test
|
||||
? NlpSampleType.test
|
||||
: NlpSampleType.train
|
||||
}
|
||||
>
|
||||
{Object.values(NlpSampleType)
|
||||
.filter((type) => type !== "inbox")
|
||||
.map((type, index) => (
|
||||
<FormControlLabel
|
||||
key={index}
|
||||
value={type}
|
||||
control={<Radio {...register("type")} />}
|
||||
label={t(`label.${type}`)}
|
||||
/>
|
||||
))}
|
||||
</RadioGroup>
|
||||
</FormControl>
|
||||
</ContentItem>
|
||||
<ContentItem>
|
||||
<Selectable
|
||||
defaultValue={currentText}
|
||||
keywordEntities={keywordEntities}
|
||||
patternEntities={patternEntities}
|
||||
placeholder={t("placeholder.nlp_sample_text")}
|
||||
onSelect={(selection, start, end) => {
|
||||
setSelection({
|
||||
value: selection,
|
||||
start,
|
||||
end,
|
||||
});
|
||||
}}
|
||||
onChange={({ text, entities }) => {
|
||||
debounceSetText(text);
|
||||
setValue(
|
||||
"keywordEntities",
|
||||
entities.map(({ entity, value, start, end }) => ({
|
||||
entity,
|
||||
value,
|
||||
start,
|
||||
end,
|
||||
})),
|
||||
);
|
||||
setPatternEntities([]);
|
||||
}}
|
||||
loading={isLoading}
|
||||
/>
|
||||
</ContentItem>
|
||||
<Box display="flex" flexDirection="column">
|
||||
{/* Language selection */}
|
||||
<ContentItem
|
||||
display="flex"
|
||||
flexDirection="row"
|
||||
maxWidth="50%"
|
||||
gap={2}
|
||||
>
|
||||
<Controller
|
||||
name="language"
|
||||
control={control}
|
||||
render={({ field }) => {
|
||||
const { onChange, ...rest } = field;
|
||||
|
||||
return (
|
||||
<AutoCompleteEntitySelect<ILanguage, "title", false>
|
||||
fullWidth={true}
|
||||
autoFocus
|
||||
searchFields={["title", "code"]}
|
||||
entity={EntityType.LANGUAGE}
|
||||
format={Format.BASIC}
|
||||
labelKey="title"
|
||||
idKey="code"
|
||||
label={t("label.language")}
|
||||
multiple={false}
|
||||
{...field}
|
||||
onChange={(_e, selected) => {
|
||||
onChange(selected?.code);
|
||||
}}
|
||||
{...rest}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</ContentItem>
|
||||
{/* Trait entities */}
|
||||
{traitEntities.map((traitEntity, index) => (
|
||||
<ContentItem
|
||||
key={traitEntity.id}
|
||||
display="flex"
|
||||
flexDirection="row"
|
||||
maxWidth="50%"
|
||||
gap={2}
|
||||
>
|
||||
<Controller
|
||||
name={`traitEntities.${index}`}
|
||||
rules={{ required: true }}
|
||||
control={control}
|
||||
render={({ field }) => {
|
||||
const { onChange: _, value, ...rest } = field;
|
||||
const options = (
|
||||
allTraitEntities.get(traitEntity.entity)?.values || []
|
||||
).map((v) => getNlpValueFromCache(v)!);
|
||||
|
||||
return (
|
||||
<>
|
||||
<AutoCompleteSelect<INlpValue, "value", false>
|
||||
fullWidth={true}
|
||||
options={options}
|
||||
idKey="value"
|
||||
labelKey="value"
|
||||
label={value.entity}
|
||||
multiple={false}
|
||||
value={value.value}
|
||||
onChange={(_e, selected, ..._) => {
|
||||
updateTraitEntity(index, {
|
||||
entity: value.entity,
|
||||
value: selected?.value || "",
|
||||
});
|
||||
}}
|
||||
{...rest}
|
||||
/>
|
||||
{value?.confidence &&
|
||||
typeof value?.confidence === "number" && (
|
||||
<Chip
|
||||
sx={{ marginTop: 0.5 }}
|
||||
variant="available"
|
||||
label={`${(value?.confidence * 100).toFixed(
|
||||
2,
|
||||
)}% ${t("label.confidence")}`}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</ContentItem>
|
||||
))}
|
||||
</Box>
|
||||
{
|
||||
/* Keyword entities */
|
||||
}
|
||||
<Box display="flex" flexDirection="column">
|
||||
{keywordEntities.map((keywordEntity, index) => (
|
||||
<ContentItem
|
||||
key={keywordEntity.id}
|
||||
display="flex"
|
||||
maxWidth="50%"
|
||||
gap={2}
|
||||
>
|
||||
<IconButton onClick={() => removeKeywordEntity(index)}>
|
||||
<DeleteIcon />
|
||||
</IconButton>
|
||||
<Controller
|
||||
name={`keywordEntities.${index}.entity`}
|
||||
control={control}
|
||||
render={({ field }) => {
|
||||
const { onChange: _, ...rest } = field;
|
||||
const options = [...allKeywordEntities.values()];
|
||||
|
||||
return (
|
||||
<AutoCompleteSelect<INlpEntity, "name", false>
|
||||
fullWidth={true}
|
||||
options={options}
|
||||
idKey="name"
|
||||
labelKey="name"
|
||||
label={t("label.nlp_entity")}
|
||||
multiple={false}
|
||||
onChange={(_e, selected, ..._) => {
|
||||
updateKeywordEntity(index, {
|
||||
...keywordEntities[index],
|
||||
entity: selected?.name || "",
|
||||
});
|
||||
}}
|
||||
{...rest}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
<Controller
|
||||
name={`keywordEntities.${index}.value`}
|
||||
control={control}
|
||||
render={({ field }) => {
|
||||
const { onChange: _, value, ...rest } = field;
|
||||
const options = (
|
||||
allKeywordEntities.get(keywordEntity.entity)?.values || []
|
||||
).map((v) => getNlpValueFromCache(v)!);
|
||||
|
||||
return (
|
||||
<AutoCompleteSelect<
|
||||
INlpValue,
|
||||
"value",
|
||||
false,
|
||||
false,
|
||||
true
|
||||
>
|
||||
sx={{ width: "50%" }}
|
||||
idKey="value"
|
||||
labelKey="value"
|
||||
label={t("label.value")}
|
||||
multiple={false}
|
||||
options={options}
|
||||
value={value}
|
||||
freeSolo={true}
|
||||
getOptionLabel={(option) => {
|
||||
return typeof option === "string"
|
||||
? option
|
||||
: option.value;
|
||||
}}
|
||||
onChange={(_e, selected, ..._) => {
|
||||
selected &&
|
||||
updateKeywordEntity(index, {
|
||||
...keywordEntity,
|
||||
value:
|
||||
typeof selected === "string"
|
||||
? selected
|
||||
: selected.value,
|
||||
});
|
||||
}}
|
||||
{...rest}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</ContentItem>
|
||||
))}
|
||||
</Box>
|
||||
</ContentContainer>
|
||||
<ContentItem display="flex" justifyContent="space-between">
|
||||
<Button
|
||||
startIcon={<AddIcon />}
|
||||
color="secondary"
|
||||
variant="contained"
|
||||
disabled={!selection?.value}
|
||||
onClick={() => {
|
||||
const newKeywordEntity = {
|
||||
...selection,
|
||||
entity: "",
|
||||
} as INlpDatasetKeywordEntity;
|
||||
const newIndex = findInsertIndex(newKeywordEntity);
|
||||
|
||||
selection && insertKeywordEntity(newIndex, newKeywordEntity);
|
||||
setSelection(null);
|
||||
}}
|
||||
>
|
||||
{!selection?.value
|
||||
? t("button.select_some_text")
|
||||
: t("button.add_nlp_entity", { 0: selection.value })}
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
variant="contained"
|
||||
startIcon={<Check />}
|
||||
onClick={handleSubmit(onSubmitForm)}
|
||||
disabled={
|
||||
!(
|
||||
currentText !== "" &&
|
||||
currentType !== NlpSampleType.inbox &&
|
||||
traitEntities.every((e) => e.value !== "") &&
|
||||
keywordEntities.every((e) => e.value !== "")
|
||||
)
|
||||
}
|
||||
type="submit"
|
||||
>
|
||||
{t("button.validate")}
|
||||
</Button>
|
||||
</ContentItem>
|
||||
</form>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
NlpDatasetSample.displayName = "NlpTrain";
|
||||
|
||||
export default NlpDatasetSample;
|
@ -13,22 +13,14 @@ import { useRouter } from "next/router";
|
||||
import React from "react";
|
||||
|
||||
import { TabPanel } from "@/app-components/tabs/TabPanel";
|
||||
import { useCreate } from "@/hooks/crud/useCreate";
|
||||
import { useFind } from "@/hooks/crud/useFind";
|
||||
import { useToast } from "@/hooks/useToast";
|
||||
import { useTranslate } from "@/hooks/useTranslate";
|
||||
import { PageHeader } from "@/layout/content/PageHeader";
|
||||
import { EntityType, Format } from "@/services/types";
|
||||
import {
|
||||
INlpDatasetSampleAttributes,
|
||||
INlpSample,
|
||||
INlpSampleFormAttributes,
|
||||
INlpSampleFull,
|
||||
} from "@/types/nlp-sample.types";
|
||||
|
||||
import NlpDatasetCounter from "./components/NlpDatasetCounter";
|
||||
import NlpSample from "./components/NlpSample";
|
||||
import NlpDatasetSample from "./components/NlpTrainForm";
|
||||
import { NlpSampleForm } from "./components/NlpSampleForm";
|
||||
import { NlpValues } from "./components/NlpValue";
|
||||
|
||||
const NlpEntity = dynamic(() => import("./components/NlpEntity"));
|
||||
@ -61,28 +53,6 @@ export const Nlp = ({
|
||||
);
|
||||
};
|
||||
const { t } = useTranslate();
|
||||
const { toast } = useToast();
|
||||
const { mutate: createSample } = useCreate<
|
||||
EntityType.NLP_SAMPLE,
|
||||
INlpDatasetSampleAttributes,
|
||||
INlpSample,
|
||||
INlpSampleFull
|
||||
>(EntityType.NLP_SAMPLE, {
|
||||
onError: () => {
|
||||
toast.error(t("message.internal_server_error"));
|
||||
},
|
||||
onSuccess: () => {
|
||||
toast.success(t("message.success_save"));
|
||||
},
|
||||
});
|
||||
const onSubmitForm = (params: INlpSampleFormAttributes) => {
|
||||
createSample({
|
||||
text: params.text,
|
||||
type: params.type,
|
||||
entities: [...params.traitEntities, ...params.keywordEntities],
|
||||
language: params.language,
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<Grid container gap={2} flexDirection="column">
|
||||
@ -90,8 +60,8 @@ export const Nlp = ({
|
||||
<Grid item xs={12}>
|
||||
<Grid container flexDirection="row">
|
||||
<Grid item xs={7}>
|
||||
<Paper>
|
||||
<NlpDatasetSample submitForm={onSubmitForm} />
|
||||
<Paper sx={{ px: 3, py: 2 }}>
|
||||
<NlpSampleForm data={{ defaultValues: null }} />
|
||||
</Paper>
|
||||
</Grid>
|
||||
<Grid item xs={5} pl={2}>
|
||||
|
@ -162,8 +162,8 @@ export interface FormDialogProps
|
||||
export interface FormButtonsProps {
|
||||
onSubmit?: (e: BaseSyntheticEvent) => void;
|
||||
onCancel?: () => void;
|
||||
cancelButtonProps?: ButtonProps;
|
||||
confirmButtonProps?: ButtonProps;
|
||||
cancelButtonProps?: ButtonProps & { text?: string };
|
||||
confirmButtonProps?: ButtonProps & { text?: string };
|
||||
}
|
||||
|
||||
export type TPayload<D, P = unknown> = {
|
||||
|
@ -55,6 +55,8 @@
|
||||
.sc-message--wrapper {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
word-break: break-all;
|
||||
|
||||
.sc-message--text {
|
||||
padding: 10px 20px;
|
||||
|
Loading…
Reference in New Issue
Block a user