diff --git a/api/src/chat/repositories/message.repository.ts b/api/src/chat/repositories/message.repository.ts index 62b5f9a62..cd3515dd9 100644 --- a/api/src/chat/repositories/message.repository.ts +++ b/api/src/chat/repositories/message.repository.ts @@ -82,6 +82,7 @@ export class MessageRepository extends BaseRepository< text: _doc.message.text, type: NlpSampleState.inbox, trained: false, + // @TODO : We need to define the language in the message entity language: defaultLang.id, }; try { diff --git a/api/src/i18n/services/language.service.ts b/api/src/i18n/services/language.service.ts index 3b146f3f9..26f09c630 100644 --- a/api/src/i18n/services/language.service.ts +++ b/api/src/i18n/services/language.service.ts @@ -56,4 +56,14 @@ export class LanguageService extends BaseService { async getDefaultLanguage() { return await this.findOne({ default: true }); } + + /** + * Retrieves the language by code. + * + * @returns A promise that resolves to the `Language` object. + */ + @Cacheable(DEFAULT_LANGUAGE_CACHE_KEY) + async getLanguageByCode(code: string) { + return await this.findOne({ code }); + } } diff --git a/api/src/nlp/controllers/nlp-sample.controller.spec.ts b/api/src/nlp/controllers/nlp-sample.controller.spec.ts index e8337bda8..86fb6230c 100644 --- a/api/src/nlp/controllers/nlp-sample.controller.spec.ts +++ b/api/src/nlp/controllers/nlp-sample.controller.spec.ts @@ -199,7 +199,7 @@ describe('NlpSampleController', () => { trained: true, type: NlpSampleState.test, entities: [], - language: enLang.id, + language: 'en', }; const result = await nlpSampleController.create(nlSample); expect(result).toEqualPayload({ @@ -279,7 +279,7 @@ describe('NlpSampleController', () => { value: 'update', }, ], - language: frLang.id, + language: 'fr', }); const updatedSample = { text: 'updated', @@ -302,15 +302,12 @@ describe('NlpSampleController', () => { }); it('should throw exception when nlp sample id not found', async () => { - const frLang = await languageService.findOne({ - code: 'fr', - }); await expect( nlpSampleController.updateOne(byeJhonSampleId, { text: 'updated', trained: true, type: NlpSampleState.test, - language: frLang.id, + language: 'fr', }), ).rejects.toThrow(NotFoundException); }); diff --git a/api/src/nlp/controllers/nlp-sample.controller.ts b/api/src/nlp/controllers/nlp-sample.controller.ts index c1d69a349..8e28fe18f 100644 --- a/api/src/nlp/controllers/nlp-sample.controller.ts +++ b/api/src/nlp/controllers/nlp-sample.controller.ts @@ -122,21 +122,24 @@ export class NlpSampleController extends BaseController< @CsrfCheck(true) @Post() async create( - @Body() { entities: nlpEntities, ...createNlpSampleDto }: NlpSampleDto, + @Body() + { + entities: nlpEntities, + language: languageCode, + ...createNlpSampleDto + }: NlpSampleDto, ): Promise { - const nlpSample = await this.nlpSampleService.create( - createNlpSampleDto as NlpSampleCreateDto, - ); + const language = await this.languageService.getLanguageByCode(languageCode); + const nlpSample = await this.nlpSampleService.create({ + ...createNlpSampleDto, + language: language.id, + }); const entities = await this.nlpSampleEntityService.storeSampleEntities( nlpSample, nlpEntities, ); - const language = await this.languageService.findOne( - createNlpSampleDto.language, - ); - return { ...nlpSample, entities, @@ -250,7 +253,11 @@ export class NlpSampleController extends BaseController< async findPage( @Query(PageQueryPipe) pageQuery: PageQueryDto, @Query(PopulatePipe) populate: string[], - @Query(new SearchFilterPipe({ allowedFields: ['text', 'type'] })) + @Query( + new SearchFilterPipe({ + allowedFields: ['text', 'type', 'language'], + }), + ) filters: TFilterQuery, ) { return this.canPopulate(populate) @@ -270,11 +277,12 @@ export class NlpSampleController extends BaseController< @Patch(':id') async updateOne( @Param('id') id: string, - @Body() updateNlpSampleDto: NlpSampleDto, + @Body() { entities, language: languageCode, ...sampleAttrs }: NlpSampleDto, ): Promise { - const { entities, ...sampleAttrs } = updateNlpSampleDto; + const language = await this.languageService.getLanguageByCode(languageCode); const sample = await this.nlpSampleService.updateOne(id, { ...sampleAttrs, + language: language.id, trained: false, }); @@ -288,8 +296,6 @@ export class NlpSampleController extends BaseController< const updatedSampleEntities = await this.nlpSampleEntityService.storeSampleEntities(sample, entities); - const language = await this.languageService.findOne(sampleAttrs.language); - return { ...sample, language, diff --git a/api/src/nlp/dto/nlp-sample.dto.ts b/api/src/nlp/dto/nlp-sample.dto.ts index ffc053f3d..3c42ab6f0 100644 --- a/api/src/nlp/dto/nlp-sample.dto.ts +++ b/api/src/nlp/dto/nlp-sample.dto.ts @@ -43,7 +43,7 @@ export class NlpSampleCreateDto { @IsOptional() type?: NlpSampleState; - @ApiProperty({ description: 'NLP sample language', type: String }) + @ApiProperty({ description: 'NLP sample language id', type: String }) @IsString() @IsNotEmpty() @IsObjectId({ message: 'Language must be a valid ObjectId' }) @@ -56,6 +56,11 @@ export class NlpSampleDto extends NlpSampleCreateDto { }) @IsOptional() entities?: NlpSampleEntityValue[]; + + @ApiProperty({ description: 'NLP sample language code', type: String }) + @IsString() + @IsNotEmpty() + language: string; } export class NlpSampleUpdateDto extends PartialType(NlpSampleCreateDto) {} diff --git a/frontend/src/components/nlp/NlpSampleDialog.tsx b/frontend/src/components/nlp/NlpSampleDialog.tsx index aee45a4f3..2236ca364 100644 --- a/frontend/src/components/nlp/NlpSampleDialog.tsx +++ b/frontend/src/components/nlp/NlpSampleDialog.tsx @@ -17,14 +17,14 @@ import { DialogControlProps } from "@/hooks/useDialog"; import { useToast } from "@/hooks/useToast"; import { EntityType } from "@/services/types"; import { + INlpDatasetSample, INlpDatasetSampleAttributes, INlpSampleFormAttributes, - INlpSampleFull, } from "@/types/nlp-sample.types"; import NlpDatasetSample from "./components/NlpTrainForm"; -export type NlpSampleDialogProps = DialogControlProps; +export type NlpSampleDialogProps = DialogControlProps; export const NlpSampleDialog: FC = ({ open, data: sample, @@ -44,15 +44,16 @@ export const NlpSampleDialog: FC = ({ toast.success(t("message.success_save")); }, }); - const onSubmitForm = (params: INlpSampleFormAttributes) => { + const onSubmitForm = (form: INlpSampleFormAttributes) => { if (sample?.id) { updateSample( { id: sample.id, params: { - text: params.text, - type: params.type, - entities: [...params.keywordEntities, ...params.traitEntities], + text: form.text, + type: form.type, + entities: [...form.keywordEntities, ...form.traitEntities], + language: form.language, }, }, { diff --git a/frontend/src/components/nlp/components/NlpSample.tsx b/frontend/src/components/nlp/components/NlpSample.tsx index b98e9ae38..7bfe8a197 100644 --- a/frontend/src/components/nlp/components/NlpSample.tsx +++ b/frontend/src/components/nlp/components/NlpSample.tsx @@ -26,6 +26,7 @@ import { useTranslation } from "react-i18next"; import { DeleteDialog } from "@/app-components/dialogs"; import { ChipEntity } from "@/app-components/displays/ChipEntity"; +import AutoCompleteEntitySelect from "@/app-components/inputs/AutoCompleteEntitySelect"; import { FilterTextfield } from "@/app-components/inputs/FilterTextfield"; import { Input } from "@/app-components/inputs/Input"; import { @@ -43,9 +44,10 @@ import { useHasPermission } from "@/hooks/useHasPermission"; import { useSearch } from "@/hooks/useSearch"; import { useToast } from "@/hooks/useToast"; import { EntityType, Format } from "@/services/types"; +import { ILanguage } from "@/types/language.types"; import { + INlpDatasetSample, INlpSample, - INlpSampleFull, NlpSampleType, } from "@/types/nlp-sample.types"; import { INlpSampleEntity } from "@/types/nlp-sample_entity.types"; @@ -66,12 +68,17 @@ export default function NlpSample() { const { apiUrl } = useConfig(); const { toast } = useToast(); const { t } = useTranslation(); - const [dataset, setDataSet] = useState(""); + const [type, setType] = useState(undefined); + const [language, setLanguage] = useState(undefined); const hasPermission = useHasPermission(); const getNlpEntityFromCache = useGetFromCache(EntityType.NLP_ENTITY); const getNlpValueFromCache = useGetFromCache(EntityType.NLP_VALUE); + const getSampleEntityFromCache = useGetFromCache( + EntityType.NLP_SAMPLE_ENTITY, + ); + const getLanguageFromCache = useGetFromCache(EntityType.LANGUAGE); const { onSearch, searchPayload } = useSearch({ - $eq: dataset === "" ? [] : [{ type: dataset as NlpSampleType }], + $eq: [...(type ? [{ type }] : []), ...(language ? [{ language }] : [])], $iLike: ["text"], }); const { mutateAsync: deleteNlpSample } = useDelete(EntityType.NLP_SAMPLE, { @@ -90,21 +97,29 @@ export default function NlpSample() { }, ); const deleteDialogCtl = useDialog(false); - const editDialogCtl = useDialog(false); + const editDialogCtl = useDialog(false); const importDialogCtl = useDialog(false); - const actionColumns = getActionsColumn( + const actionColumns = getActionsColumn( [ { label: ActionColumnLabel.Edit, - action: ({ entities, ...rest }) => { - const data: INlpSampleFull = { + action: ({ entities, language, ...rest }) => { + const lang = getLanguageFromCache(language) as ILanguage; + const data: INlpDatasetSample = { ...rest, - entities: entities?.map(({ end, start, value, entity }) => ({ - end, - start, - value: getNlpValueFromCache(value)?.value, - entity: getNlpEntityFromCache(entity)?.name, - })) as unknown as INlpSampleEntity[], + entities: entities?.map((e) => { + const sampleEntity = getSampleEntityFromCache(e); + const { end, start, value, entity } = + sampleEntity as INlpSampleEntity; + + return { + end, + start, + value: getNlpValueFromCache(value)?.value || "", + entity: getNlpEntityFromCache(entity)?.name || "", + }; + }), + language: lang.code, }; editDialogCtl.openDialog(data); @@ -119,7 +134,7 @@ export default function NlpSample() { ], t("label.operations"), ); - const columns: GridColDef[] = [ + const columns: GridColDef[] = [ { flex: 1, field: "text", @@ -132,38 +147,53 @@ export default function NlpSample() { flex: 1, field: "entities", renderCell: ({ row }) => - row.entities.map((entity) => ( - ( - - {value} - {` `}={` `} - - - } - /> - )} - entity={EntityType.NLP_ENTITY} - /> - )), + row.entities + .map((e) => getSampleEntityFromCache(e) as INlpSampleEntity) + .map((entity) => ( + ( + + {value} + {` `}={` `} + + + } + /> + )} + entity={EntityType.NLP_ENTITY} + /> + )), headerName: t("label.entities"), sortable: false, disableColumnMenu: true, renderHeader, }, + { + maxWidth: 90, + field: "language", + renderCell: ({ row }) => { + const language = getLanguageFromCache(row.language); + + return language?.title; + }, + headerName: t("label.language"), + sortable: true, + disableColumnMenu: true, + renderHeader, + }, { maxWidth: 90, field: "type", @@ -232,18 +262,33 @@ export default function NlpSample() { fullWidth={false} sx={{ minWidth: "256px" }} /> + + fullWidth={false} + sx={{ + minWidth: "150px", + }} + autoFocus + searchFields={["title", "code"]} + entity={EntityType.LANGUAGE} + format={Format.BASIC} + labelKey="title" + label={t("label.language")} + multiple={false} + onChange={(_e, selected) => setLanguage(selected?.id)} + /> setDataSet(e.target.value)} + value={type} + onChange={(e) => setType(e.target.value as NlpSampleType)} SelectProps={{ - ...(dataset !== "" && { + ...(type && { IconComponent: () => ( - setDataSet("")}> + setType(undefined)}> ), @@ -288,7 +333,7 @@ export default function NlpSample() { variant="contained" href={buildURL( apiUrl, - `nlpsample/export${dataset ? `?type=${dataset}` : ""}`, + `nlpsample/export${type ? `?type=${type}` : ""}`, )} startIcon={} > diff --git a/frontend/src/components/nlp/components/NlpTrainForm.tsx b/frontend/src/components/nlp/components/NlpTrainForm.tsx index e43a86d47..efe616625 100644 --- a/frontend/src/components/nlp/components/NlpTrainForm.tsx +++ b/frontend/src/components/nlp/components/NlpTrainForm.tsx @@ -36,18 +36,19 @@ import { useFind } from "@/hooks/crud/useFind"; import { useGetFromCache } from "@/hooks/crud/useGet"; import { useApiClient } from "@/hooks/useApiClient"; import { EntityType, Format } from "@/services/types"; +import { ILanguage } from "@/types/language.types"; import { INlpEntity } from "@/types/nlp-entity.types"; import { INlpDatasetKeywordEntity, + INlpDatasetSample, INlpDatasetTraitEntity, INlpSampleFormAttributes, - INlpSampleFull, NlpSampleType, } from "@/types/nlp-sample.types"; import { INlpValue } from "@/types/nlp-value.types"; type NlpDatasetSampleProps = { - sample?: INlpSampleFull; + sample?: INlpDatasetSample; submitForm: (params: INlpSampleFormAttributes) => void; }; @@ -90,7 +91,7 @@ const NlpDatasetSample: FC = ({ lookups.includes("trait"), ); const sampleTraitEntities = sample.entities.filter( - (e) => typeof e.start === "undefined", + (e) => "start" in e && typeof e.start === "undefined", ); if (sampleTraitEntities.length === traitEntities.length) { @@ -112,9 +113,12 @@ const NlpDatasetSample: FC = ({ defaultValues: { type: sample?.type || NlpSampleType.train, text: sample?.text || "", + language: sample?.language, traitEntities: defaultTraitEntities, keywordEntities: - sample?.entities.filter((e) => typeof e.start === "number") || [], + sample?.entities.filter( + (e) => "start" in e && typeof e.start === "number", + ) || [], }, }); const currentText = watch("text"); @@ -167,7 +171,7 @@ const NlpDatasetSample: FC = ({ const findInsertIndex = (newItem: INlpDatasetKeywordEntity): number => { const index = keywordEntities.findIndex( - (entity) => entity.start > newItem.start, + (entity) => entity.start && newItem.start && entity.start > newItem.start, ); return index === -1 ? keywordEntities.length : index; @@ -177,11 +181,15 @@ const NlpDatasetSample: FC = ({ start: number; end: number; } | null>(null); - const onSubmitForm = (params: INlpSampleFormAttributes) => { - submitForm(params); - reset(); - removeTraitEntity(); - removeKeywordEntity(); + const onSubmitForm = (form: INlpSampleFormAttributes) => { + submitForm(form); + reset({ + type: form?.type || NlpSampleType.train, + text: "", + language: form?.language, + traitEntities: defaultTraitEntities, + keywordEntities: [], + }); refetchEntities(); }; @@ -247,6 +255,37 @@ const NlpDatasetSample: FC = ({ /> + + { + const { onChange, ...rest } = field; + + return ( + + 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} + /> + ); + }} + /> + {traitEntities.map((traitEntity, index) => ( id, + processStrategy: processCommonStrategy, + }, +); + +export const TranslationEntity = new schema.Entity( + EntityType.TRANSLATION, undefined, { idAttribute: ({ id }) => id, processStrategy: processCommonStrategy, }, ); + export const NlpValueEntity = new schema.Entity( EntityType.NLP_VALUE, undefined, @@ -201,27 +211,28 @@ export const NlpEntityEntity = new schema.Entity( }, ); +NlpValueEntity.define({ + entity: NlpEntityEntity, +}); + export const NlpSampleEntityEntity = new schema.Entity( EntityType.NLP_SAMPLE_ENTITY, - undefined, { - idAttribute: ({ id }) => id, - processStrategy: processCommonStrategy, + entity: NlpEntityEntity, + value: NlpValueEntity, }, -); - -export const LanguageEntity = new schema.Entity( - EntityType.LANGUAGE, - undefined, { idAttribute: ({ id }) => id, processStrategy: processCommonStrategy, }, ); -export const TranslationEntity = new schema.Entity( - EntityType.TRANSLATION, - undefined, +export const NlpSampleEntity = new schema.Entity( + EntityType.NLP_SAMPLE, + { + entities: [NlpSampleEntityEntity], + language: LanguageEntity, + }, { idAttribute: ({ id }) => id, processStrategy: processCommonStrategy, diff --git a/frontend/src/types/base.types.ts b/frontend/src/types/base.types.ts index 7ca8949fa..e063f9fec 100644 --- a/frontend/src/types/base.types.ts +++ b/frontend/src/types/base.types.ts @@ -100,7 +100,7 @@ export const POPULATE_BY_TYPE = { "trigger_labels", "assignTo", ], - [EntityType.NLP_SAMPLE]: ["entities"], + [EntityType.NLP_SAMPLE]: ["language", "entities"], [EntityType.NLP_SAMPLE_ENTITY]: ["sample", "entity", "value"], [EntityType.NLP_ENTITY]: ["values"], [EntityType.NLP_VALUE]: ["entity"], diff --git a/frontend/src/types/nlp-sample.types.ts b/frontend/src/types/nlp-sample.types.ts index ebb4e541c..365dc3b87 100644 --- a/frontend/src/types/nlp-sample.types.ts +++ b/frontend/src/types/nlp-sample.types.ts @@ -10,6 +10,7 @@ import { EntityType, Format } from "@/services/types"; import { IBaseSchema, IFormat, OmitPopulate } from "./base.types"; +import { ILanguage } from "./language.types"; import { INlpSampleEntity } from "./nlp-sample_entity.types"; export enum NlpSampleType { @@ -23,6 +24,7 @@ export interface INlpSampleAttributes { trained?: boolean; type?: NlpSampleType; entities: string[]; + language: string; } export interface INlpSampleStub @@ -31,14 +33,15 @@ export interface INlpSampleStub export interface INlpSample extends INlpSampleStub, IFormat { entities: string[]; + language: string; } export interface INlpSampleFull extends INlpSampleStub, IFormat { entities: INlpSampleEntity[]; + language: ILanguage; } // Dataset Trainer - export interface INlpDatasetTraitEntity { entity: string; // entity name value: string; // value name @@ -60,3 +63,7 @@ export interface INlpDatasetSampleAttributes extends Omit { entities: (INlpDatasetTraitEntity | INlpDatasetKeywordEntity)[]; } + +export interface INlpDatasetSample + extends IBaseSchema, + INlpDatasetSampleAttributes {} \ No newline at end of file