diff --git a/frontend/src/app-components/inputs/Selectable.tsx b/frontend/src/app-components/inputs/Selectable.tsx
index 70fac37b..09ab6197 100644
--- a/frontend/src/app-components/inputs/Selectable.tsx
+++ b/frontend/src/app-components/inputs/Selectable.tsx
@@ -6,11 +6,22 @@
* 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, CircularProgress, Input, styled } from "@mui/material";
+import { Box, CircularProgress, Input, styled, Tooltip } from "@mui/material";
import randomSeed from "random-seed";
-import { FC, useCallback, useEffect, useMemo, useRef, useState } from "react";
+import {
+ CSSProperties,
+ FC,
+ useCallback,
+ useEffect,
+ useMemo,
+ useRef,
+ useState,
+} from "react";
-import { INlpDatasetKeywordEntity } from "../../types/nlp-sample.types";
+import {
+ INlpDatasetKeywordEntity,
+ INlpDatasetPatternEntity,
+} from "../../types/nlp-sample.types";
const SelectableBox = styled(Box)({
position: "relative",
@@ -40,22 +51,62 @@ const COLORS = [
{ name: "orange", bg: "#E6A23C" },
];
const UNKNOWN_COLOR = { name: "grey", bg: "#aaaaaa" };
-const TODAY = new Date().toDateString();
-const getColor = (no: number) => {
- const rand = randomSeed.create(TODAY);
+const NOW = (+new Date()).toString();
+const getColor = (no: number, seedPrefix: string = "") => {
+ const rand = randomSeed.create(seedPrefix + NOW);
const startIndex = rand(COLORS.length);
const color =
no < 0 ? UNKNOWN_COLOR : COLORS[(startIndex + no) % COLORS.length];
return {
backgroundColor: color.bg,
- opacity: 0.3,
+ opacity: 0.2,
};
};
+interface INlpSelectionEntity {
+ start: string;
+ entity: string;
+ value: string;
+ end: string;
+ style: CSSProperties;
+}
+const SelectionEntityBackground: React.FC<{
+ selectionEntity: INlpSelectionEntity;
+}> = ({ selectionEntity: e }) => {
+ return (
+
+ {e.start}
+
+ {e.value}
+
+ {e.end}
+
+ );
+};
+
type SelectableProps = {
defaultValue?: string;
- entities?: INlpDatasetKeywordEntity[];
+ keywordEntities?: INlpDatasetKeywordEntity[];
+ patternEntities?: INlpDatasetPatternEntity[];
placeholder?: string;
onSelect: (str: string, start: number, end: number) => void;
onChange: (sample: {
@@ -65,9 +116,27 @@ type SelectableProps = {
loading?: boolean;
};
+const buildSelectionEntities = (
+ text: string,
+ entities: INlpDatasetKeywordEntity[] | INlpDatasetPatternEntity[],
+): INlpSelectionEntity[] => {
+ return entities?.map((e, index) => {
+ const start = e.start ? e.start : text.indexOf(e.value);
+ const end = e.end ? e.end : start + e.value.length;
+
+ return {
+ start: text.substring(0, start),
+ entity: e.entity,
+ value: text.substring(start, end),
+ end: text.substring(end),
+ style: getColor(e.entity ? index : -1, e.entity),
+ };
+ });
+};
const Selectable: FC = ({
defaultValue,
- entities = [],
+ keywordEntities = [],
+ patternEntities = [],
placeholder = "",
onChange,
onSelect,
@@ -76,20 +145,13 @@ const Selectable: FC = ({
const [text, setText] = useState(defaultValue || "");
const editableRef = useRef(null);
const selectableRef = useRef(null);
- const selectedEntities = useMemo(
- () =>
- entities?.map((e, index) => {
- const start = e.start ? e.start : text.indexOf(e.value);
- const end = e.end ? e.end : start + e.value.length;
-
- return {
- start: text.substring(0, start),
- value: text.substring(start, end),
- end: text.substring(end),
- style: getColor(e.entity ? index : -1),
- };
- }),
- [entities, text],
+ const selectedKeywordEntities = useMemo(
+ () => buildSelectionEntities(text, keywordEntities),
+ [keywordEntities, text],
+ );
+ const selectedPatternEntities = useMemo(
+ () => buildSelectionEntities(text, patternEntities),
+ [patternEntities, text],
);
useEffect(() => {
@@ -143,7 +205,7 @@ const Selectable: FC = ({
const handleTextChange = useCallback(
(newText: string) => {
const oldText = text;
- const oldEntities = [...entities];
+ const oldEntities = [...keywordEntities];
const newEntities: INlpDatasetKeywordEntity[] = [];
const findCharDiff = (oldStr: string, newStr: string): number => {
const minLength = Math.min(oldStr.length, newStr.length);
@@ -187,17 +249,22 @@ const Selectable: FC = ({
onChange({ text: newText, entities: newEntities });
},
- [text, onChange, entities],
+ [text, onChange, keywordEntities],
);
return (
- {selectedEntities?.map((e, idx) => (
-
- {e.start}
- {e.value}
- {e.end}
-
+ {selectedPatternEntities?.map((e, idx) => (
+
+ ))}
+ {selectedKeywordEntities?.map((e, idx) => (
+
))}
= ({
submitForm,
}) => {
const { t } = useTranslate();
- const { data: entities, refetch: refetchEntities } = useFind(
- {
- entity: EntityType.NLP_ENTITY,
- format: Format.FULL,
- },
- {
- hasCount: false,
- },
- );
+ const {
+ allTraitEntities,
+ allKeywordEntities,
+ allPatternEntities,
+ refetchAllEntities,
+ } = useNlp();
const getNlpValueFromCache = useGetFromCache(EntityType.NLP_VALUE);
- // eslint-disable-next-line react-hooks/exhaustive-deps
const defaultValues: INlpSampleFormAttributes = useMemo(
() => ({
type: sample?.type || NlpSampleType.train,
text: sample?.text || "",
language: sample?.language || null,
- traitEntities: (entities || [])
- .filter(({ lookups }) => {
- return lookups.includes("trait");
- })
- .map((e) => {
- return {
- entity: e.name,
- value: sample
- ? sample.entities.find(({ entity }) => entity === e.name)?.value
- : "",
- } as INlpDatasetTraitEntity;
- }),
- keywordEntities: (sample?.entities || []).filter(
- (e) => "start" in e && typeof e.start === "number",
+ 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[],
}),
- [sample, entities],
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ [allKeywordEntities, allTraitEntities, JSON.stringify(sample)],
);
const { handleSubmit, control, register, reset, setValue, watch } =
useForm({
@@ -97,6 +91,9 @@ const NlpDatasetSample: FC = ({
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",
@@ -122,22 +119,29 @@ const NlpDatasetSample: FC = ({
queryFn: async () => {
return await apiClient.predictNlp(currentText);
},
- onSuccess: (result) => {
- const traitEntities: INlpDatasetTraitEntity[] = result.entities.filter(
- (e) => !("start" in e && "end" in e) && e.entity !== "language",
- );
- const keywordEntities = result.entities.filter(
- (e) => "start" in e && "end" in e,
+ 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 language = result.entities.find(
+ 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", traitEntities);
- setValue("keywordEntities", keywordEntities);
+ setValue("traitEntities", predictedTraitEntities);
+ setValue("keywordEntities", predictedKeywordEntities);
+ setPatternEntities(predictedPatternEntities);
},
- enabled: !sample && !!currentText,
+ enabled:
+ // Inbox sample update
+ sample?.type === "inbox" ||
+ // New sample
+ (!sample && !!currentText),
});
const findInsertIndex = (newItem: INlpDatasetKeywordEntity): number => {
const index = keywordEntities.findIndex(
@@ -153,7 +157,7 @@ const NlpDatasetSample: FC = ({
} | null>(null);
const onSubmitForm = (form: INlpSampleFormAttributes) => {
submitForm(form);
- refetchEntities();
+ refetchAllEntities();
reset({
...defaultValues,
text: "",
@@ -203,7 +207,8 @@ const NlpDatasetSample: FC = ({
{
setSelection({
@@ -223,11 +228,13 @@ const NlpDatasetSample: FC = ({
end,
})),
);
+ setPatternEntities([]);
}}
loading={isLoading}
/>
+ {/* Language selection */}
= ({
}}
/>
+ {/* Trait entities */}
{traitEntities.map((traitEntity, index) => (
= ({
control={control}
render={({ field }) => {
const { onChange: _, value, ...rest } = field;
- const entity = entities?.find(
- ({ name }) => name === traitEntity.entity,
- );
- const options =
- entity?.values.map(
- (v) => getNlpValueFromCache(v) as INlpValue,
- ) || [];
+ const options = (
+ allTraitEntities.get(traitEntity.entity)?.values || []
+ ).map((v) => getNlpValueFromCache(v)!);
return (
<>
@@ -318,7 +322,9 @@ const NlpDatasetSample: FC = ({
))}
-
+ {
+ /* Keyword entities */
+ }
{keywordEntities.map((keywordEntity, index) => (
= ({
control={control}
render={({ field }) => {
const { onChange: _, ...rest } = field;
+ const options = [...allKeywordEntities.values()];
return (
-
+
fullWidth={true}
- searchFields={["name"]}
- entity={EntityType.NLP_ENTITY}
- format={Format.FULL}
+ options={options}
idKey="name"
labelKey="name"
label={t("label.nlp_entity")}
multiple={false}
- preprocess={(options) => {
- return options.filter(
- ({ lookups }) =>
- lookups.includes("keywords") ||
- lookups.includes("pattern"),
- );
- }}
onChange={(_e, selected, ..._) => {
updateKeywordEntity(index, {
...keywordEntities[index],
@@ -369,13 +367,9 @@ const NlpDatasetSample: FC = ({
control={control}
render={({ field }) => {
const { onChange: _, value, ...rest } = field;
- const entity = entities?.find(
- ({ name }) => name === keywordEntity.entity,
- );
- const options =
- entity?.values.map(
- (v) => getNlpValueFromCache(v) as INlpValue,
- ) || [];
+ const options = (
+ allKeywordEntities.get(keywordEntity.entity)?.values || []
+ ).map((v) => getNlpValueFromCache(v)!);
return (
{
+ const intialMap = new Map();
+
+ return entities
+ .filter(({ lookups }) => {
+ return lookups.includes(lookup);
+ }).reduce((acc, curr) => {
+ acc.set(curr.name, curr);
+
+ return acc;
+ }, intialMap)
+}
+
+export const useNlp = () => {
+ const { data: allEntities, refetch: refetchAllEntities } = useFind(
+ {
+ entity: EntityType.NLP_ENTITY,
+ format: Format.FULL,
+ },
+ {
+ hasCount: false,
+ },
+ );
+ const allTraitEntities = useMemo(() => {
+ return buildNlpEntityMap((allEntities || []), 'trait')
+ }, [allEntities]);
+ const allKeywordEntities = useMemo(() => {
+ return buildNlpEntityMap((allEntities || []), 'keywords')
+ }, [allEntities]);
+ const allPatternEntities = useMemo(() => {
+ return buildNlpEntityMap((allEntities || []), 'pattern')
+ }, [allEntities]);
+
+ return {
+ allTraitEntities,
+ allKeywordEntities,
+ allPatternEntities,
+ refetchAllEntities
+ }
+};
diff --git a/frontend/src/types/nlp-sample.types.ts b/frontend/src/types/nlp-sample.types.ts
index 8069b95a..1884ce85 100644
--- a/frontend/src/types/nlp-sample.types.ts
+++ b/frontend/src/types/nlp-sample.types.ts
@@ -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.
@@ -52,6 +52,8 @@ export interface INlpDatasetKeywordEntity extends INlpDatasetTraitEntity {
end: number;
}
+export interface INlpDatasetPatternEntity extends INlpDatasetKeywordEntity {}
+
export interface INlpSampleFormAttributes
extends Omit {
traitEntities: INlpDatasetTraitEntity[];