mirror of
https://github.com/hexastack/hexabot
synced 2025-06-26 18:27:28 +00:00
Merge branch 'main' into 48-request-context-vars-permanent-option
This commit is contained in:
1
.github/workflows/docker.yml
vendored
1
.github/workflows/docker.yml
vendored
@@ -77,6 +77,7 @@ jobs:
|
||||
context: ./widget/
|
||||
file: ./widget/Dockerfile
|
||||
platforms: linux/amd64,linux/arm64
|
||||
target: production
|
||||
push: true
|
||||
tags: hexastack/hexabot-widget:latest
|
||||
|
||||
|
||||
86
Makefile
86
Makefile
@@ -1,64 +1,60 @@
|
||||
COMPOSE_FILES := -f ./docker/docker-compose.yml
|
||||
# Makefile
|
||||
FOLDER := ./docker
|
||||
|
||||
# Function to add service files
|
||||
define add_service
|
||||
ifeq ($(PROD), true)
|
||||
COMPOSE_FILES += -f ./docker/docker-compose.$(1).yml
|
||||
ifneq ($(wildcard ./docker/docker-compose.$(1).prod.yml),)
|
||||
COMPOSE_FILES += -f ./docker/docker-compose.$(1).prod.yml
|
||||
endif
|
||||
else ifeq ($(DEV_MODE), true)
|
||||
COMPOSE_FILES += -f ./docker/docker-compose.$(1).yml
|
||||
ifneq ($(wildcard ./docker/docker-compose.$(1).dev.yml),)
|
||||
COMPOSE_FILES += -f ./docker/docker-compose.$(1).dev.yml
|
||||
endif
|
||||
endif
|
||||
# The services that can be toggled
|
||||
SERVICES := nginx nlu smtp4dev
|
||||
|
||||
# Function to dynamically add Docker Compose files based on enabled services
|
||||
define compose_files
|
||||
$(foreach service,$(SERVICES),$(if $($(shell echo $(service) | tr a-z A-Z)), -f $(FOLDER)/docker-compose.$(service).yml))
|
||||
endef
|
||||
|
||||
# Function to dynamically add Docker Compose dev files based on enabled services and file existence
|
||||
define compose_dev_files
|
||||
$(foreach service,$(SERVICES), \
|
||||
$(if $($(shell echo $(service) | tr a-z A-Z)), \
|
||||
$(if $(shell [ -f $(FOLDER)/docker-compose.$(service).dev.yml ] && echo yes), -f $(FOLDER)/docker-compose.$(service).dev.yml)))
|
||||
endef
|
||||
|
||||
# Function to set up COMPOSE_FILES
|
||||
define compose_files
|
||||
ifeq ($(1), true)
|
||||
ifneq ($(wildcard ./docker/docker-compose.dev.yml),)
|
||||
COMPOSE_FILES += -f ./docker/docker-compose.dev.yml
|
||||
endif
|
||||
endif
|
||||
ifneq ($(NGINX),)
|
||||
$(eval $(call add_service,nginx))
|
||||
endif
|
||||
ifneq ($(NLU),)
|
||||
$(eval $(call add_service,nlu))
|
||||
endif
|
||||
# Function to dynamically add Docker Compose dev files based on enabled services and file existence
|
||||
define compose_prod_files
|
||||
$(foreach service,$(SERVICES), \
|
||||
$(if $($(shell echo $(service) | tr a-z A-Z)), \
|
||||
$(if $(shell [ -f $(FOLDER)/docker-compose.$(service).prod.yml ] && echo yes), -f $(FOLDER)/docker-compose.$(service).dev.yml)))
|
||||
endef
|
||||
|
||||
# Ensure .env file exists and matches .env.example
|
||||
check-env:
|
||||
@if [ ! -f "./docker/.env" ]; then \
|
||||
@if [ ! -f "$(FOLDER)/.env" ]; then \
|
||||
echo "Error: .env file does not exist. Creating one now from .env.example ..."; \
|
||||
cp ./docker/.env.example ./docker/.env; \
|
||||
cp $(FOLDER)/.env.example $(FOLDER)/.env; \
|
||||
fi
|
||||
@echo "Checking .env file for missing variables..."
|
||||
@awk -F '=' 'NR==FNR {a[$$1]; next} !($$1 in a) {print "Missing env var: " $$1}' ./docker/.env ./docker/.env.example
|
||||
@awk -F '=' 'NR==FNR {a[$$1]; next} !($$1 in a) {print "Missing env var: " $$1}' $(FOLDER)/.env $(FOLDER)/.env.example
|
||||
|
||||
init:
|
||||
cp ./docker/.env.example ./docker/.env
|
||||
|
||||
dev: check-env
|
||||
$(eval $(call compose_files,true))
|
||||
docker compose $(COMPOSE_FILES) up -d
|
||||
cp $(FOLDER)/.env.example $(FOLDER)/.env
|
||||
|
||||
# Start command: runs docker-compose with the main file and any additional service files
|
||||
start: check-env
|
||||
$(eval $(call compose_files,false))
|
||||
docker compose $(COMPOSE_FILES) up -d
|
||||
@docker compose -f $(FOLDER)/docker-compose.yml $(call compose_files) up -d
|
||||
|
||||
stop: check-env
|
||||
$(eval $(call compose_files,true))
|
||||
docker compose $(COMPOSE_FILES) down
|
||||
# Dev command: runs docker-compose with the main file, dev file, and any additional service dev files (if they exist)
|
||||
dev: check-env
|
||||
@docker compose -f $(FOLDER)/docker-compose.yml -f $(FOLDER)/docker-compose.dev.yml $(call compose_files) $(call compose_dev_files) up -d
|
||||
|
||||
destroy: check-env
|
||||
$(eval $(call compose_files,true))
|
||||
docker compose $(COMPOSE_FILES) down -v
|
||||
# Start command: runs docker-compose with the main file and any additional service files
|
||||
start-prod: check-env
|
||||
@docker compose -f $(FOLDER)/docker-compose.yml -f $(FOLDER)/docker-compose.prod.yml $(call compose_files) $(call compose_prod_files) up -d
|
||||
|
||||
# Stop command: stops the running containers
|
||||
stop:
|
||||
@docker compose -f $(FOLDER)/docker-compose.yml -f $(FOLDER)/docker-compose.dev.yml $(call compose_files) $(call compose_dev_files) $(call compose_prod_files) down
|
||||
|
||||
# Destroy command: stops the running containers and removes the volumes
|
||||
destroy:
|
||||
@docker compose -f $(FOLDER)/docker-compose.yml -f $(FOLDER)/docker-compose.dev.yml $(call compose_files) $(call compose_dev_files) $(call compose_prod_files) down -v
|
||||
|
||||
# Migrate command:
|
||||
migrate-up:
|
||||
$(eval $(call compose_files,false))
|
||||
docker-compose $(COMPOSE_FILES) up --no-deps -d database-init
|
||||
@docker compose -f $(FOLDER)/docker-compose.yml -f $(FOLDER)/docker-compose.dev.yml $(call compose_files) $(call compose_dev_files) up --no-deps -d database-init
|
||||
|
||||
@@ -11,6 +11,8 @@
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
|
||||
import escapeRegExp from 'lodash/escapeRegExp';
|
||||
|
||||
// Get the argument passed (e.g., "all-users-fr")
|
||||
const arg: string | undefined = process.argv[2];
|
||||
|
||||
@@ -25,7 +27,7 @@ const templatePath: string = path.join(__dirname, '../config/template.ts');
|
||||
|
||||
// Check if a migration with the same name (excluding timestamp) already exists
|
||||
const migrationExists: boolean = fs.readdirSync(migrationsDir).some((file) => {
|
||||
const regex = new RegExp(`^[0-9]+-${arg}\.ts$`);
|
||||
const regex = new RegExp(`^[0-9]+-${escapeRegExp(arg)}\\.ts$`);
|
||||
return regex.test(file);
|
||||
});
|
||||
|
||||
|
||||
@@ -26,13 +26,13 @@ import conversationSchema, {
|
||||
import labelSchema, { Label } from '@/chat/schemas/label.schema';
|
||||
import messageSchema, { Message } from '@/chat/schemas/message.schema';
|
||||
import subscriberSchema, { Subscriber } from '@/chat/schemas/subscriber.schema';
|
||||
import translationSchema, {
|
||||
Translation,
|
||||
} from '@/chat/schemas/translation.schema';
|
||||
import { ContentType } from '@/cms/schemas/content-type.schema';
|
||||
import contentSchema, { Content } from '@/cms/schemas/content.schema';
|
||||
import menuSchema, { Menu } from '@/cms/schemas/menu.schema';
|
||||
import { config } from '@/config';
|
||||
import translationSchema, {
|
||||
Translation,
|
||||
} from '@/i18n/schemas/translation.schema';
|
||||
import nlpEntitySchema, { NlpEntity } from '@/nlp/schemas/nlp-entity.schema';
|
||||
import nlpSampleEntitySchema, {
|
||||
NlpSampleEntity,
|
||||
|
||||
7
api/package-lock.json
generated
7
api/package-lock.json
generated
@@ -67,6 +67,7 @@
|
||||
"@types/express": "^4.17.17",
|
||||
"@types/express-session": "^1.17.10",
|
||||
"@types/jest": "^29.5.2",
|
||||
"@types/lodash": "^4.17.9",
|
||||
"@types/minio": "^7.1.1",
|
||||
"@types/module-alias": "^2.0.4",
|
||||
"@types/multer": "^1.4.11",
|
||||
@@ -6027,6 +6028,12 @@
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/lodash": {
|
||||
"version": "4.17.9",
|
||||
"resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.9.tgz",
|
||||
"integrity": "sha512-w9iWudx1XWOHW5lQRS9iKpK/XuRhnN+0T7HvdCCd802FYkT1AMTnxndJHGrNJwRoRHkslGr4S29tjm1cT7x/7w==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@types/mime": {
|
||||
"version": "1.3.5",
|
||||
"resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz",
|
||||
|
||||
@@ -91,6 +91,7 @@
|
||||
"@types/express": "^4.17.17",
|
||||
"@types/express-session": "^1.17.10",
|
||||
"@types/jest": "^29.5.2",
|
||||
"@types/lodash": "^4.17.9",
|
||||
"@types/module-alias": "^2.0.4",
|
||||
"@types/multer": "^1.4.11",
|
||||
"@types/node": "^20.3.1",
|
||||
|
||||
@@ -32,7 +32,7 @@ import { ChannelModule } from './channel/channel.module';
|
||||
import { ChatModule } from './chat/chat.module';
|
||||
import { CmsModule } from './cms/cms.module';
|
||||
import { config } from './config';
|
||||
import { ExtendedI18nModule } from './extended-18n.module';
|
||||
import { I18nModule } from './i18n/i18n.module';
|
||||
import { LoggerModule } from './logger/logger.module';
|
||||
import { DtoUpdateMiddleware } from './middlewares/dto.update.middleware';
|
||||
import { NlpModule } from './nlp/nlp.module';
|
||||
@@ -44,7 +44,7 @@ import idPlugin from './utils/schema-plugin/id.plugin';
|
||||
import { WebsocketModule } from './websocket/websocket.module';
|
||||
|
||||
const i18nOptions: I18nOptions = {
|
||||
fallbackLanguage: config.chatbot.lang.default,
|
||||
fallbackLanguage: 'en',
|
||||
loaderOptions: {
|
||||
path: path.join(__dirname, '/config/i18n/'),
|
||||
watch: true,
|
||||
@@ -120,7 +120,7 @@ const i18nOptions: I18nOptions = {
|
||||
ignoreErrors: false,
|
||||
}),
|
||||
CsrfModule,
|
||||
ExtendedI18nModule.forRoot(i18nOptions),
|
||||
I18nModule.forRoot(i18nOptions),
|
||||
CacheModule.register({
|
||||
isGlobal: true,
|
||||
ttl: config.cache.ttl,
|
||||
|
||||
@@ -9,11 +9,11 @@
|
||||
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
import { ExtendedI18nService } from './extended-i18n.service';
|
||||
import { I18nService } from './i18n/services/i18n.service';
|
||||
|
||||
@Injectable()
|
||||
export class AppService {
|
||||
constructor(private readonly i18n: ExtendedI18nService) {}
|
||||
constructor(private readonly i18n: I18nService) {}
|
||||
|
||||
getHello(): string {
|
||||
return this.i18n.t('welcome', { lang: 'en' });
|
||||
|
||||
@@ -13,6 +13,7 @@ import { THydratedDocument } from 'mongoose';
|
||||
import { FileType } from '@/chat/schemas/types/attachment';
|
||||
import { config } from '@/config';
|
||||
import { BaseSchema } from '@/utils/generics/base-schema';
|
||||
import { buildURL } from '@/utils/helpers/URL';
|
||||
|
||||
import { MIME_REGEX } from '../utilities';
|
||||
|
||||
@@ -89,7 +90,10 @@ export class Attachment extends BaseSchema {
|
||||
attachmentId: string,
|
||||
attachmentName: string = '',
|
||||
): string {
|
||||
return `${config.parameters.apiUrl}/attachment/download/${attachmentId}/${attachmentName}`;
|
||||
return buildURL(
|
||||
config.parameters.apiUrl,
|
||||
`/attachment/download/${attachmentId}/${attachmentName}`,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -119,7 +123,10 @@ export const AttachmentModel: ModelDefinition = {
|
||||
|
||||
AttachmentModel.schema.virtual('url').get(function () {
|
||||
if (this._id && this.name)
|
||||
return `${config.apiPath}/attachment/download/${this._id}/${this.name}`;
|
||||
return buildURL(
|
||||
config.apiPath,
|
||||
`/attachment/download/${this._id}/${this.name}`,
|
||||
);
|
||||
|
||||
return '';
|
||||
});
|
||||
|
||||
@@ -23,7 +23,6 @@ import { ContextVarController } from './controllers/context-var.controller';
|
||||
import { LabelController } from './controllers/label.controller';
|
||||
import { MessageController } from './controllers/message.controller';
|
||||
import { SubscriberController } from './controllers/subscriber.controller';
|
||||
import { TranslationController } from './controllers/translation.controller';
|
||||
import { BlockRepository } from './repositories/block.repository';
|
||||
import { CategoryRepository } from './repositories/category.repository';
|
||||
import { ContextVarRepository } from './repositories/context-var.repository';
|
||||
@@ -31,7 +30,6 @@ import { ConversationRepository } from './repositories/conversation.repository';
|
||||
import { LabelRepository } from './repositories/label.repository';
|
||||
import { MessageRepository } from './repositories/message.repository';
|
||||
import { SubscriberRepository } from './repositories/subscriber.repository';
|
||||
import { TranslationRepository } from './repositories/translation.repository';
|
||||
import { BlockModel } from './schemas/block.schema';
|
||||
import { CategoryModel } from './schemas/category.schema';
|
||||
import { ContextVarModel } from './schemas/context-var.schema';
|
||||
@@ -39,10 +37,8 @@ import { ConversationModel } from './schemas/conversation.schema';
|
||||
import { LabelModel } from './schemas/label.schema';
|
||||
import { MessageModel } from './schemas/message.schema';
|
||||
import { SubscriberModel } from './schemas/subscriber.schema';
|
||||
import { TranslationModel } from './schemas/translation.schema';
|
||||
import { CategorySeeder } from './seeds/category.seed';
|
||||
import { ContextVarSeeder } from './seeds/context-var.seed';
|
||||
import { TranslationSeeder } from './seeds/translation.seed';
|
||||
import { BlockService } from './services/block.service';
|
||||
import { BotService } from './services/bot.service';
|
||||
import { CategoryService } from './services/category.service';
|
||||
@@ -52,7 +48,6 @@ import { ConversationService } from './services/conversation.service';
|
||||
import { LabelService } from './services/label.service';
|
||||
import { MessageService } from './services/message.service';
|
||||
import { SubscriberService } from './services/subscriber.service';
|
||||
import { TranslationService } from './services/translation.service';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
@@ -63,7 +58,6 @@ import { TranslationService } from './services/translation.service';
|
||||
BlockModel,
|
||||
MessageModel,
|
||||
SubscriberModel,
|
||||
TranslationModel,
|
||||
ConversationModel,
|
||||
SubscriberModel,
|
||||
]),
|
||||
@@ -81,7 +75,6 @@ import { TranslationService } from './services/translation.service';
|
||||
BlockController,
|
||||
MessageController,
|
||||
SubscriberController,
|
||||
TranslationController,
|
||||
],
|
||||
providers: [
|
||||
CategoryRepository,
|
||||
@@ -90,7 +83,6 @@ import { TranslationService } from './services/translation.service';
|
||||
BlockRepository,
|
||||
MessageRepository,
|
||||
SubscriberRepository,
|
||||
TranslationRepository,
|
||||
ConversationRepository,
|
||||
CategoryService,
|
||||
ContextVarService,
|
||||
@@ -98,13 +90,11 @@ import { TranslationService } from './services/translation.service';
|
||||
BlockService,
|
||||
MessageService,
|
||||
SubscriberService,
|
||||
TranslationService,
|
||||
CategorySeeder,
|
||||
ContextVarSeeder,
|
||||
ConversationService,
|
||||
ChatService,
|
||||
BotService,
|
||||
TranslationSeeder,
|
||||
],
|
||||
exports: [SubscriberService, MessageService, LabelService, BlockService],
|
||||
})
|
||||
|
||||
@@ -19,7 +19,10 @@ import { AttachmentService } from '@/attachment/services/attachment.service';
|
||||
import { ContentRepository } from '@/cms/repositories/content.repository';
|
||||
import { ContentModel } from '@/cms/schemas/content.schema';
|
||||
import { ContentService } from '@/cms/services/content.service';
|
||||
import { ExtendedI18nService } from '@/extended-i18n.service';
|
||||
import { LanguageRepository } from '@/i18n/repositories/language.repository';
|
||||
import { LanguageModel } from '@/i18n/schemas/language.schema';
|
||||
import { I18nService } from '@/i18n/services/i18n.service';
|
||||
import { LanguageService } from '@/i18n/services/language.service';
|
||||
import { LoggerService } from '@/logger/logger.service';
|
||||
import { PluginService } from '@/plugins/plugins.service';
|
||||
import { SettingService } from '@/setting/services/setting.service';
|
||||
@@ -86,6 +89,7 @@ describe('BlockController', () => {
|
||||
UserModel,
|
||||
RoleModel,
|
||||
PermissionModel,
|
||||
LanguageModel,
|
||||
]),
|
||||
],
|
||||
providers: [
|
||||
@@ -97,6 +101,7 @@ describe('BlockController', () => {
|
||||
UserRepository,
|
||||
RoleRepository,
|
||||
PermissionRepository,
|
||||
LanguageRepository,
|
||||
BlockService,
|
||||
LabelService,
|
||||
CategoryService,
|
||||
@@ -105,10 +110,11 @@ describe('BlockController', () => {
|
||||
UserService,
|
||||
RoleService,
|
||||
PermissionService,
|
||||
LanguageService,
|
||||
PluginService,
|
||||
LoggerService,
|
||||
{
|
||||
provide: ExtendedI18nService,
|
||||
provide: I18nService,
|
||||
useValue: {
|
||||
t: jest.fn().mockImplementation((t) => t),
|
||||
},
|
||||
|
||||
@@ -18,7 +18,7 @@ import { AttachmentService } from '@/attachment/services/attachment.service';
|
||||
import { ContentRepository } from '@/cms/repositories/content.repository';
|
||||
import { ContentModel } from '@/cms/schemas/content.schema';
|
||||
import { ContentService } from '@/cms/services/content.service';
|
||||
import { ExtendedI18nService } from '@/extended-i18n.service';
|
||||
import { I18nService } from '@/i18n/services/i18n.service';
|
||||
import { LoggerService } from '@/logger/logger.service';
|
||||
import { PluginService } from '@/plugins/plugins.service';
|
||||
import { SettingService } from '@/setting/services/setting.service';
|
||||
@@ -77,7 +77,7 @@ describe('CategoryController', () => {
|
||||
},
|
||||
LoggerService,
|
||||
{
|
||||
provide: ExtendedI18nService,
|
||||
provide: I18nService,
|
||||
useValue: {
|
||||
t: jest.fn().mockImplementation((t) => t),
|
||||
},
|
||||
|
||||
@@ -19,7 +19,7 @@ import { ChannelService } from '@/channel/channel.service';
|
||||
import { MenuRepository } from '@/cms/repositories/menu.repository';
|
||||
import { MenuModel } from '@/cms/schemas/menu.schema';
|
||||
import { MenuService } from '@/cms/services/menu.service';
|
||||
import { ExtendedI18nService } from '@/extended-i18n.service';
|
||||
import { I18nService } from '@/i18n/services/i18n.service';
|
||||
import { LoggerService } from '@/logger/logger.service';
|
||||
import { NlpService } from '@/nlp/services/nlp.service';
|
||||
import { SettingService } from '@/setting/services/setting.service';
|
||||
@@ -92,7 +92,7 @@ describe('MessageController', () => {
|
||||
MenuService,
|
||||
MenuRepository,
|
||||
{
|
||||
provide: ExtendedI18nService,
|
||||
provide: I18nService,
|
||||
useValue: {
|
||||
t: jest.fn().mockImplementation((t) => t),
|
||||
},
|
||||
|
||||
@@ -11,6 +11,7 @@ import { Injectable, Optional } from '@nestjs/common';
|
||||
import { InjectModel } from '@nestjs/mongoose';
|
||||
import { Model } from 'mongoose';
|
||||
|
||||
import { LanguageService } from '@/i18n/services/language.service';
|
||||
import { LoggerService } from '@/logger/logger.service';
|
||||
import { NlpSampleCreateDto } from '@/nlp/dto/nlp-sample.dto';
|
||||
import { NlpSampleState } from '@/nlp/schemas/types';
|
||||
@@ -36,10 +37,13 @@ export class MessageRepository extends BaseRepository<
|
||||
|
||||
private readonly logger: LoggerService;
|
||||
|
||||
private readonly languageService: LanguageService;
|
||||
|
||||
constructor(
|
||||
@InjectModel(Message.name) readonly model: Model<AnyMessage>,
|
||||
@Optional() nlpSampleService?: NlpSampleService,
|
||||
@Optional() logger?: LoggerService,
|
||||
@Optional() languageService?: LanguageService,
|
||||
) {
|
||||
super(
|
||||
model,
|
||||
@@ -49,6 +53,7 @@ export class MessageRepository extends BaseRepository<
|
||||
);
|
||||
this.logger = logger;
|
||||
this.nlpSampleService = nlpSampleService;
|
||||
this.languageService = languageService;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -72,10 +77,13 @@ export class MessageRepository extends BaseRepository<
|
||||
'message' in _doc &&
|
||||
'text' in _doc.message
|
||||
) {
|
||||
const defaultLang = await this.languageService?.getDefaultLanguage();
|
||||
const record: NlpSampleCreateDto = {
|
||||
text: _doc.message.text,
|
||||
type: NlpSampleState.inbox,
|
||||
trained: false,
|
||||
// @TODO : We need to define the language in the message entity
|
||||
language: defaultLang.id,
|
||||
};
|
||||
try {
|
||||
await this.nlpSampleService.findOneOrCreate(record, record);
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
* 3. SaaS Restriction: This software, or any derivative of it, may not be used to offer a competing product or service (SaaS) without prior written consent from Hexastack. Offering the software as a service or using it in a commercial cloud environment without express permission is strictly prohibited.
|
||||
*/
|
||||
|
||||
import { CACHE_MANAGER } from '@nestjs/cache-manager';
|
||||
import { EventEmitter2 } from '@nestjs/event-emitter';
|
||||
import { MongooseModule } from '@nestjs/mongoose';
|
||||
import { Test } from '@nestjs/testing';
|
||||
@@ -24,11 +25,14 @@ import { ContentTypeModel } from '@/cms/schemas/content-type.schema';
|
||||
import { Content, ContentModel } from '@/cms/schemas/content.schema';
|
||||
import { ContentTypeService } from '@/cms/services/content-type.service';
|
||||
import { ContentService } from '@/cms/services/content.service';
|
||||
import { ExtendedI18nService } from '@/extended-i18n.service';
|
||||
import OfflineHandler from '@/extensions/channels/offline/index.channel';
|
||||
import { OFFLINE_CHANNEL_NAME } from '@/extensions/channels/offline/settings';
|
||||
import { Offline } from '@/extensions/channels/offline/types';
|
||||
import OfflineEventWrapper from '@/extensions/channels/offline/wrapper';
|
||||
import { LanguageRepository } from '@/i18n/repositories/language.repository';
|
||||
import { LanguageModel } from '@/i18n/schemas/language.schema';
|
||||
import { I18nService } from '@/i18n/services/i18n.service';
|
||||
import { LanguageService } from '@/i18n/services/language.service';
|
||||
import { LoggerService } from '@/logger/logger.service';
|
||||
import { PluginService } from '@/plugins/plugins.service';
|
||||
import { Settings } from '@/setting/schemas/types';
|
||||
@@ -94,6 +98,7 @@ describe('BlockService', () => {
|
||||
ContentModel,
|
||||
AttachmentModel,
|
||||
LabelModel,
|
||||
LanguageModel,
|
||||
]),
|
||||
],
|
||||
providers: [
|
||||
@@ -102,18 +107,20 @@ describe('BlockService', () => {
|
||||
ContentTypeRepository,
|
||||
ContentRepository,
|
||||
AttachmentRepository,
|
||||
LanguageRepository,
|
||||
BlockService,
|
||||
CategoryService,
|
||||
ContentTypeService,
|
||||
ContentService,
|
||||
AttachmentService,
|
||||
LanguageService,
|
||||
{
|
||||
provide: PluginService,
|
||||
useValue: {},
|
||||
},
|
||||
LoggerService,
|
||||
{
|
||||
provide: ExtendedI18nService,
|
||||
provide: I18nService,
|
||||
useValue: {
|
||||
t: jest.fn().mockImplementation((t) => {
|
||||
return t === 'Welcome' ? 'Bienvenue' : t;
|
||||
@@ -132,6 +139,14 @@ describe('BlockService', () => {
|
||||
},
|
||||
},
|
||||
EventEmitter2,
|
||||
{
|
||||
provide: CACHE_MANAGER,
|
||||
useValue: {
|
||||
del: jest.fn(),
|
||||
get: jest.fn(),
|
||||
set: jest.fn(),
|
||||
},
|
||||
},
|
||||
],
|
||||
}).compile();
|
||||
blockService = module.get<BlockService>(BlockService);
|
||||
|
||||
@@ -13,7 +13,8 @@ import { Attachment } from '@/attachment/schemas/attachment.schema';
|
||||
import { AttachmentService } from '@/attachment/services/attachment.service';
|
||||
import EventWrapper from '@/channel/lib/EventWrapper';
|
||||
import { ContentService } from '@/cms/services/content.service';
|
||||
import { ExtendedI18nService } from '@/extended-i18n.service';
|
||||
import { I18nService } from '@/i18n/services/i18n.service';
|
||||
import { LanguageService } from '@/i18n/services/language.service';
|
||||
import { LoggerService } from '@/logger/logger.service';
|
||||
import { Nlp } from '@/nlp/lib/types';
|
||||
import { PluginService } from '@/plugins/plugins.service';
|
||||
@@ -44,7 +45,8 @@ export class BlockService extends BaseService<Block, BlockPopulate, BlockFull> {
|
||||
private readonly settingService: SettingService,
|
||||
private readonly pluginService: PluginService,
|
||||
private readonly logger: LoggerService,
|
||||
protected readonly i18n: ExtendedI18nService,
|
||||
protected readonly i18n: I18nService,
|
||||
protected readonly languageService: LanguageService,
|
||||
) {
|
||||
super(repository);
|
||||
}
|
||||
@@ -109,12 +111,9 @@ export class BlockService extends BaseService<Block, BlockPopulate, BlockFull> {
|
||||
// Check & catch user language through NLP
|
||||
const nlp = event.getNLP();
|
||||
if (nlp) {
|
||||
const settings = await this.settingService.getSettings();
|
||||
const languages = await this.languageService.getLanguages();
|
||||
const lang = nlp.entities.find((e) => e.entity === 'language');
|
||||
if (
|
||||
lang &&
|
||||
settings.nlp_settings.languages.indexOf(lang.value) !== -1
|
||||
) {
|
||||
if (lang && Object.keys(languages).indexOf(lang.value) !== -1) {
|
||||
const profile = event.getSender();
|
||||
profile.language = lang.value;
|
||||
event.setSender(profile);
|
||||
@@ -372,12 +371,11 @@ export class BlockService extends BaseService<Block, BlockPopulate, BlockFull> {
|
||||
subscriberContext: SubscriberContext,
|
||||
settings: Settings,
|
||||
): string {
|
||||
const lang =
|
||||
context && context.user && context.user.language
|
||||
? context.user.language
|
||||
: settings.nlp_settings.default_lang;
|
||||
// Translate
|
||||
text = this.i18n.t(text, { lang, defaultValue: text });
|
||||
text = this.i18n.t(text, {
|
||||
lang: context.user.language,
|
||||
defaultValue: text,
|
||||
});
|
||||
// Replace context tokens
|
||||
text = this.processTokenReplacements(
|
||||
text,
|
||||
|
||||
@@ -25,10 +25,13 @@ import { MenuModel } from '@/cms/schemas/menu.schema';
|
||||
import { ContentTypeService } from '@/cms/services/content-type.service';
|
||||
import { ContentService } from '@/cms/services/content.service';
|
||||
import { MenuService } from '@/cms/services/menu.service';
|
||||
import { ExtendedI18nService } from '@/extended-i18n.service';
|
||||
import { offlineEventText } from '@/extensions/channels/offline/__test__/events.mock';
|
||||
import OfflineHandler from '@/extensions/channels/offline/index.channel';
|
||||
import OfflineEventWrapper from '@/extensions/channels/offline/wrapper';
|
||||
import { LanguageRepository } from '@/i18n/repositories/language.repository';
|
||||
import { LanguageModel } from '@/i18n/schemas/language.schema';
|
||||
import { I18nService } from '@/i18n/services/i18n.service';
|
||||
import { LanguageService } from '@/i18n/services/language.service';
|
||||
import { LoggerService } from '@/logger/logger.service';
|
||||
import { NlpEntityRepository } from '@/nlp/repositories/nlp-entity.repository';
|
||||
import { NlpSampleEntityRepository } from '@/nlp/repositories/nlp-sample-entity.repository';
|
||||
@@ -111,6 +114,7 @@ describe('BlockService', () => {
|
||||
NlpSampleEntityModel,
|
||||
NlpSampleModel,
|
||||
ContextVarModel,
|
||||
LanguageModel,
|
||||
]),
|
||||
],
|
||||
providers: [
|
||||
@@ -130,6 +134,7 @@ describe('BlockService', () => {
|
||||
NlpEntityRepository,
|
||||
NlpSampleEntityRepository,
|
||||
NlpSampleRepository,
|
||||
LanguageRepository,
|
||||
BlockService,
|
||||
CategoryService,
|
||||
ContentTypeService,
|
||||
@@ -149,13 +154,14 @@ describe('BlockService', () => {
|
||||
NlpService,
|
||||
ContextVarService,
|
||||
ContextVarRepository,
|
||||
LanguageService,
|
||||
{
|
||||
provide: PluginService,
|
||||
useValue: {},
|
||||
},
|
||||
LoggerService,
|
||||
{
|
||||
provide: ExtendedI18nService,
|
||||
provide: I18nService,
|
||||
useValue: {
|
||||
t: jest.fn().mockImplementation((t) => t),
|
||||
},
|
||||
|
||||
@@ -16,7 +16,7 @@ export const config: Config = {
|
||||
translationFilename: process.env.I18N_TRANSLATION_FILENAME || 'messages',
|
||||
},
|
||||
appPath: process.cwd(),
|
||||
apiPath: process.env.API_ORIGIN,
|
||||
apiPath: process.env.API_ORIGIN || 'http://localhost:4000',
|
||||
frontendPath: process.env.FRONTEND_ORIGIN
|
||||
? process.env.FRONTEND_ORIGIN.split(',')[0]
|
||||
: 'http://localhost:8080',
|
||||
@@ -120,10 +120,6 @@ export const config: Config = {
|
||||
limit: 10,
|
||||
},
|
||||
chatbot: {
|
||||
lang: {
|
||||
default: 'en',
|
||||
available: ['en', 'fr'],
|
||||
},
|
||||
messages: {
|
||||
track_delivery: false,
|
||||
track_read: false,
|
||||
|
||||
@@ -15,7 +15,6 @@ type TJwtOptions = {
|
||||
secret: string;
|
||||
expiresIn: string;
|
||||
};
|
||||
type TLanguage = 'en' | 'fr' | 'ar' | 'tn';
|
||||
type TMethods = 'GET' | 'PATCH' | 'POST' | 'DELETE' | 'OPTIONS' | 'HEAD';
|
||||
type TLogLevel = 'log' | 'fatal' | 'error' | 'warn' | 'debug' | 'verbose';
|
||||
type TCacheConfig = {
|
||||
@@ -87,10 +86,6 @@ export type Config = {
|
||||
limit: number;
|
||||
};
|
||||
chatbot: {
|
||||
lang: {
|
||||
default: TLanguage;
|
||||
available: TLanguage[];
|
||||
};
|
||||
messages: {
|
||||
track_delivery: boolean;
|
||||
track_read: boolean;
|
||||
|
||||
@@ -1,44 +0,0 @@
|
||||
/*
|
||||
* 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).
|
||||
* 3. SaaS Restriction: This software, or any derivative of it, may not be used to offer a competing product or service (SaaS) without prior written consent from Hexastack. Offering the software as a service or using it in a commercial cloud environment without express permission is strictly prohibited.
|
||||
*/
|
||||
|
||||
import { DynamicModule, Global, Inject, Module } from '@nestjs/common';
|
||||
import { HttpAdapterHost } from '@nestjs/core';
|
||||
import {
|
||||
I18N_OPTIONS,
|
||||
I18N_TRANSLATIONS,
|
||||
I18nModule,
|
||||
I18nOptions,
|
||||
I18nTranslation,
|
||||
} from 'nestjs-i18n';
|
||||
import { Observable } from 'rxjs';
|
||||
|
||||
import { ExtendedI18nService } from './extended-i18n.service';
|
||||
|
||||
@Global()
|
||||
@Module({})
|
||||
export class ExtendedI18nModule extends I18nModule {
|
||||
constructor(
|
||||
i18n: ExtendedI18nService,
|
||||
@Inject(I18N_TRANSLATIONS)
|
||||
translations: Observable<I18nTranslation>,
|
||||
@Inject(I18N_OPTIONS) i18nOptions: I18nOptions,
|
||||
adapter: HttpAdapterHost,
|
||||
) {
|
||||
super(i18n, translations, i18nOptions, adapter);
|
||||
}
|
||||
|
||||
static forRoot(options: I18nOptions): DynamicModule {
|
||||
const { providers, exports } = super.forRoot(options);
|
||||
return {
|
||||
module: ExtendedI18nModule,
|
||||
providers: providers.concat(ExtendedI18nService),
|
||||
exports: exports.concat(ExtendedI18nService),
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -15,7 +15,7 @@ import { ChannelService } from '@/channel/channel.service';
|
||||
import { MessageService } from '@/chat/services/message.service';
|
||||
import { SubscriberService } from '@/chat/services/subscriber.service';
|
||||
import { MenuService } from '@/cms/services/menu.service';
|
||||
import { ExtendedI18nService } from '@/extended-i18n.service';
|
||||
import { I18nService } from '@/i18n/services/i18n.service';
|
||||
import { LoggerService } from '@/logger/logger.service';
|
||||
import { NlpService } from '@/nlp/services/nlp.service';
|
||||
import { SettingCreateDto } from '@/setting/dto/setting.dto';
|
||||
@@ -38,7 +38,7 @@ export default class LiveChatTesterHandler extends OfflineHandler {
|
||||
nlpService: NlpService,
|
||||
logger: LoggerService,
|
||||
eventEmitter: EventEmitter2,
|
||||
i18n: ExtendedI18nService,
|
||||
i18n: I18nService,
|
||||
subscriberService: SubscriberService,
|
||||
attachmentService: AttachmentService,
|
||||
messageService: MessageService,
|
||||
|
||||
@@ -35,7 +35,7 @@ import { SubscriberService } from '@/chat/services/subscriber.service';
|
||||
import { MenuRepository } from '@/cms/repositories/menu.repository';
|
||||
import { MenuModel } from '@/cms/schemas/menu.schema';
|
||||
import { MenuService } from '@/cms/services/menu.service';
|
||||
import { ExtendedI18nService } from '@/extended-i18n.service';
|
||||
import { I18nService } from '@/i18n/services/i18n.service';
|
||||
import { LoggerService } from '@/logger/logger.service';
|
||||
import { NlpService } from '@/nlp/services/nlp.service';
|
||||
import { SettingService } from '@/setting/services/setting.service';
|
||||
@@ -113,7 +113,7 @@ describe('Offline Handler', () => {
|
||||
EventEmitter2,
|
||||
LoggerService,
|
||||
{
|
||||
provide: ExtendedI18nService,
|
||||
provide: I18nService,
|
||||
useValue: {
|
||||
t: jest.fn().mockImplementation((t) => t),
|
||||
},
|
||||
|
||||
@@ -25,7 +25,7 @@ import { SubscriberService } from '@/chat/services/subscriber.service';
|
||||
import { MenuRepository } from '@/cms/repositories/menu.repository';
|
||||
import { MenuModel } from '@/cms/schemas/menu.schema';
|
||||
import { MenuService } from '@/cms/services/menu.service';
|
||||
import { ExtendedI18nService } from '@/extended-i18n.service';
|
||||
import { I18nService } from '@/i18n/services/i18n.service';
|
||||
import { LoggerService } from '@/logger/logger.service';
|
||||
import { NlpService } from '@/nlp/services/nlp.service';
|
||||
import { SettingService } from '@/setting/services/setting.service';
|
||||
@@ -90,7 +90,7 @@ describe(`Offline event wrapper`, () => {
|
||||
EventEmitter2,
|
||||
LoggerService,
|
||||
{
|
||||
provide: ExtendedI18nService,
|
||||
provide: I18nService,
|
||||
useValue: {
|
||||
t: jest.fn().mockImplementation((t) => t),
|
||||
},
|
||||
|
||||
@@ -50,7 +50,7 @@ import { SubscriberService } from '@/chat/services/subscriber.service';
|
||||
import { Content } from '@/cms/schemas/content.schema';
|
||||
import { MenuService } from '@/cms/services/menu.service';
|
||||
import { config } from '@/config';
|
||||
import { ExtendedI18nService } from '@/extended-i18n.service';
|
||||
import { I18nService } from '@/i18n/services/i18n.service';
|
||||
import { LoggerService } from '@/logger/logger.service';
|
||||
import { NlpService } from '@/nlp/services/nlp.service';
|
||||
import { SettingCreateDto } from '@/setting/dto/setting.dto';
|
||||
@@ -73,7 +73,7 @@ export default class OfflineHandler extends ChannelHandler {
|
||||
nlpService: NlpService,
|
||||
logger: LoggerService,
|
||||
protected readonly eventEmitter: EventEmitter2,
|
||||
protected readonly i18n: ExtendedI18nService,
|
||||
protected readonly i18n: I18nService,
|
||||
protected readonly subscriberService: SubscriberService,
|
||||
protected readonly attachmentService: AttachmentService,
|
||||
protected readonly messageService: MessageService,
|
||||
@@ -477,7 +477,7 @@ export default class OfflineHandler extends ChannelHandler {
|
||||
...channelData,
|
||||
name: this.getChannel(),
|
||||
},
|
||||
language: config.chatbot.lang.default,
|
||||
language: '',
|
||||
locale: '',
|
||||
timezone: 0,
|
||||
gender: 'male',
|
||||
|
||||
@@ -29,6 +29,13 @@ export const baseNlpEntity = {
|
||||
builtin: true,
|
||||
};
|
||||
|
||||
export const baseLanguage = {
|
||||
...modelInstance,
|
||||
title: 'English',
|
||||
code: 'en',
|
||||
isDefault: true,
|
||||
};
|
||||
|
||||
export const entitiesMock: NlpEntityFull[] = [
|
||||
{
|
||||
...baseNlpEntity,
|
||||
@@ -89,6 +96,7 @@ export const samplesMock: NlpSampleFull[] = [
|
||||
],
|
||||
trained: false,
|
||||
type: NlpSampleState.train,
|
||||
language: baseLanguage,
|
||||
},
|
||||
{
|
||||
...modelInstance,
|
||||
@@ -112,5 +120,6 @@ export const samplesMock: NlpSampleFull[] = [
|
||||
],
|
||||
trained: false,
|
||||
type: NlpSampleState.train,
|
||||
language: baseLanguage,
|
||||
},
|
||||
];
|
||||
|
||||
@@ -23,6 +23,10 @@ export const nlpEmptyFormated: DatasetType = {
|
||||
name: 'product',
|
||||
elements: ['pizza', 'sandwich'],
|
||||
},
|
||||
{
|
||||
elements: ['en', 'fr'],
|
||||
name: 'language',
|
||||
},
|
||||
],
|
||||
entity_synonyms: [
|
||||
{
|
||||
@@ -34,17 +38,33 @@ export const nlpEmptyFormated: DatasetType = {
|
||||
|
||||
export const nlpFormatted: DatasetType = {
|
||||
common_examples: [
|
||||
{ text: 'Hello', intent: 'greeting', entities: [] },
|
||||
{
|
||||
text: 'Hello',
|
||||
intent: 'greeting',
|
||||
entities: [
|
||||
{
|
||||
entity: 'language',
|
||||
value: 'en',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
text: 'i want to order a pizza',
|
||||
intent: 'order',
|
||||
entities: [{ entity: 'product', value: 'pizza', start: 19, end: 23 }],
|
||||
entities: [
|
||||
{ entity: 'product', value: 'pizza', start: 19, end: 23 },
|
||||
{
|
||||
entity: 'language',
|
||||
value: 'en',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
regex_features: [],
|
||||
lookup_tables: [
|
||||
{ name: 'intent', elements: ['greeting', 'order'] },
|
||||
{ name: 'product', elements: ['pizza', 'sandwich'] },
|
||||
{ name: 'language', elements: ['en', 'fr'] },
|
||||
],
|
||||
entity_synonyms: [
|
||||
{
|
||||
|
||||
@@ -8,10 +8,14 @@
|
||||
*/
|
||||
|
||||
import { HttpModule } from '@nestjs/axios';
|
||||
import { CACHE_MANAGER } from '@nestjs/cache-manager';
|
||||
import { EventEmitter2 } from '@nestjs/event-emitter';
|
||||
import { MongooseModule } from '@nestjs/mongoose';
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
|
||||
import { LanguageRepository } from '@/i18n/repositories/language.repository';
|
||||
import { LanguageModel } from '@/i18n/schemas/language.schema';
|
||||
import { LanguageService } from '@/i18n/services/language.service';
|
||||
import { LoggerService } from '@/logger/logger.service';
|
||||
import { NlpEntityRepository } from '@/nlp/repositories/nlp-entity.repository';
|
||||
import { NlpSampleEntityRepository } from '@/nlp/repositories/nlp-sample-entity.repository';
|
||||
@@ -56,26 +60,11 @@ describe('NLP Default Helper', () => {
|
||||
NlpValueModel,
|
||||
NlpSampleModel,
|
||||
NlpSampleEntityModel,
|
||||
LanguageModel,
|
||||
]),
|
||||
HttpModule,
|
||||
],
|
||||
providers: [
|
||||
LoggerService,
|
||||
{
|
||||
provide: SettingService,
|
||||
useValue: {
|
||||
getSettings: jest.fn(() => ({
|
||||
nlp_settings: {
|
||||
provider: 'default',
|
||||
endpoint: 'path',
|
||||
token: 'token',
|
||||
languages: ['fr', 'ar', 'tn'],
|
||||
default_lang: 'fr',
|
||||
threshold: '0.5',
|
||||
},
|
||||
})),
|
||||
},
|
||||
},
|
||||
NlpService,
|
||||
NlpSampleService,
|
||||
NlpSampleRepository,
|
||||
@@ -85,8 +74,32 @@ describe('NLP Default Helper', () => {
|
||||
NlpValueRepository,
|
||||
NlpSampleEntityService,
|
||||
NlpSampleEntityRepository,
|
||||
LanguageService,
|
||||
LanguageRepository,
|
||||
EventEmitter2,
|
||||
DefaultNlpHelper,
|
||||
LoggerService,
|
||||
{
|
||||
provide: SettingService,
|
||||
useValue: {
|
||||
getSettings: jest.fn(() => ({
|
||||
nlp_settings: {
|
||||
provider: 'default',
|
||||
endpoint: 'path',
|
||||
token: 'token',
|
||||
threshold: '0.5',
|
||||
},
|
||||
})),
|
||||
},
|
||||
},
|
||||
{
|
||||
provide: CACHE_MANAGER,
|
||||
useValue: {
|
||||
del: jest.fn(),
|
||||
get: jest.fn(),
|
||||
set: jest.fn(),
|
||||
},
|
||||
},
|
||||
],
|
||||
}).compile();
|
||||
settingService = module.get<SettingService>(SettingService);
|
||||
@@ -103,15 +116,15 @@ describe('NLP Default Helper', () => {
|
||||
expect(nlp).toBeDefined();
|
||||
});
|
||||
|
||||
it('should format empty training set properly', () => {
|
||||
it('should format empty training set properly', async () => {
|
||||
const nlp = nlpService.getNLP();
|
||||
const results = nlp.format([], entitiesMock);
|
||||
const results = await nlp.format([], entitiesMock);
|
||||
expect(results).toEqual(nlpEmptyFormated);
|
||||
});
|
||||
|
||||
it('should format training set properly', () => {
|
||||
it('should format training set properly', async () => {
|
||||
const nlp = nlpService.getNLP();
|
||||
const results = nlp.format(samplesMock, entitiesMock);
|
||||
const results = await nlp.format(samplesMock, entitiesMock);
|
||||
expect(results).toEqual(nlpFormatted);
|
||||
});
|
||||
|
||||
|
||||
@@ -13,21 +13,14 @@ import { Injectable } from '@nestjs/common';
|
||||
import { LoggerService } from '@/logger/logger.service';
|
||||
import BaseNlpHelper from '@/nlp/lib/BaseNlpHelper';
|
||||
import { Nlp } from '@/nlp/lib/types';
|
||||
import { NlpEntity, NlpEntityFull } from '@/nlp/schemas/nlp-entity.schema';
|
||||
import { NlpEntityFull } from '@/nlp/schemas/nlp-entity.schema';
|
||||
import { NlpSampleFull } from '@/nlp/schemas/nlp-sample.schema';
|
||||
import { NlpValue } from '@/nlp/schemas/nlp-value.schema';
|
||||
import { NlpEntityService } from '@/nlp/services/nlp-entity.service';
|
||||
import { NlpSampleService } from '@/nlp/services/nlp-sample.service';
|
||||
import { NlpService } from '@/nlp/services/nlp.service';
|
||||
import { buildURL } from '@/utils/helpers/URL';
|
||||
|
||||
import {
|
||||
CommonExample,
|
||||
DatasetType,
|
||||
EntitySynonym,
|
||||
ExampleEntity,
|
||||
LookupTable,
|
||||
NlpParseResultType,
|
||||
} from './types';
|
||||
import { DatasetType, NlpParseResultType } from './types';
|
||||
|
||||
@Injectable()
|
||||
export default class DefaultNlpHelper extends BaseNlpHelper {
|
||||
@@ -61,69 +54,16 @@ export default class DefaultNlpHelper extends BaseNlpHelper {
|
||||
* @param entities - All available entities
|
||||
* @returns {DatasetType} - The formatted RASA training set
|
||||
*/
|
||||
format(samples: NlpSampleFull[], entities: NlpEntityFull[]): DatasetType {
|
||||
const entityMap = NlpEntity.getEntityMap(entities);
|
||||
const valueMap = NlpValue.getValueMap(
|
||||
NlpValue.getValuesFromEntities(entities),
|
||||
async format(
|
||||
samples: NlpSampleFull[],
|
||||
entities: NlpEntityFull[],
|
||||
): Promise<DatasetType> {
|
||||
const nluData = await this.nlpSampleService.formatRasaNlu(
|
||||
samples,
|
||||
entities,
|
||||
);
|
||||
|
||||
const common_examples: CommonExample[] = samples
|
||||
.filter((s) => s.entities.length > 0)
|
||||
.map((s) => {
|
||||
const intent = s.entities.find(
|
||||
(e) => entityMap[e.entity].name === 'intent',
|
||||
);
|
||||
if (!intent) {
|
||||
throw new Error('Unable to find the `intent` nlp entity.');
|
||||
}
|
||||
const sampleEntities: ExampleEntity[] = s.entities
|
||||
.filter((e) => entityMap[<string>e.entity].name !== 'intent')
|
||||
.map((e) => {
|
||||
const res: ExampleEntity = {
|
||||
entity: entityMap[<string>e.entity].name,
|
||||
value: valueMap[<string>e.value].value,
|
||||
};
|
||||
if ('start' in e && 'end' in e) {
|
||||
Object.assign(res, {
|
||||
start: e.start,
|
||||
end: e.end,
|
||||
});
|
||||
}
|
||||
return res;
|
||||
});
|
||||
return {
|
||||
text: s.text,
|
||||
intent: valueMap[intent.value].value,
|
||||
entities: sampleEntities,
|
||||
};
|
||||
});
|
||||
const lookup_tables: LookupTable[] = entities.map((e) => {
|
||||
return {
|
||||
name: e.name,
|
||||
elements: e.values.map((v) => {
|
||||
return v.value;
|
||||
}),
|
||||
};
|
||||
});
|
||||
const entity_synonyms = entities
|
||||
.reduce((acc, e) => {
|
||||
const synonyms = e.values.map((v) => {
|
||||
return {
|
||||
value: v.value,
|
||||
synonyms: v.expressions,
|
||||
};
|
||||
});
|
||||
return acc.concat(synonyms);
|
||||
}, [] as EntitySynonym[])
|
||||
.filter((s) => {
|
||||
return s.synonyms.length > 0;
|
||||
});
|
||||
return {
|
||||
common_examples,
|
||||
regex_features: [],
|
||||
lookup_tables,
|
||||
entity_synonyms,
|
||||
};
|
||||
return nluData;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -138,10 +78,10 @@ export default class DefaultNlpHelper extends BaseNlpHelper {
|
||||
entities: NlpEntityFull[],
|
||||
): Promise<any> {
|
||||
const self = this;
|
||||
const nluData: DatasetType = self.format(samples, entities);
|
||||
const nluData: DatasetType = await self.format(samples, entities);
|
||||
// Train samples
|
||||
const result = await this.httpService.axiosRef.post(
|
||||
`${this.settings.endpoint}/train`,
|
||||
buildURL(this.settings.endpoint, `/train`),
|
||||
nluData,
|
||||
{
|
||||
params: {
|
||||
@@ -169,10 +109,10 @@ export default class DefaultNlpHelper extends BaseNlpHelper {
|
||||
entities: NlpEntityFull[],
|
||||
): Promise<any> {
|
||||
const self = this;
|
||||
const nluTestData: DatasetType = self.format(samples, entities);
|
||||
const nluTestData: DatasetType = await self.format(samples, entities);
|
||||
// Evaluate model with test samples
|
||||
return await this.httpService.axiosRef.post(
|
||||
`${this.settings.endpoint}/evaluate`,
|
||||
buildURL(this.settings.endpoint, `/evaluate`),
|
||||
nluTestData,
|
||||
{
|
||||
params: {
|
||||
@@ -251,7 +191,7 @@ export default class DefaultNlpHelper extends BaseNlpHelper {
|
||||
try {
|
||||
const { data: nlp } =
|
||||
await this.httpService.axiosRef.post<NlpParseResultType>(
|
||||
`${this.settings.endpoint}/parse`,
|
||||
buildURL(this.settings.endpoint, '/parse'),
|
||||
{
|
||||
q: text,
|
||||
project,
|
||||
|
||||
181
api/src/i18n/controllers/language.controller.spec.ts
Normal file
181
api/src/i18n/controllers/language.controller.spec.ts
Normal file
@@ -0,0 +1,181 @@
|
||||
/*
|
||||
* 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).
|
||||
* 3. SaaS Restriction: This software, or any derivative of it, may not be used to offer a competing product or service (SaaS) without prior written consent from Hexastack. Offering the software as a service or using it in a commercial cloud environment without express permission is strictly prohibited.
|
||||
*/
|
||||
|
||||
import { CACHE_MANAGER } from '@nestjs/cache-manager';
|
||||
import { BadRequestException, NotFoundException } from '@nestjs/common';
|
||||
import { EventEmitter2 } from '@nestjs/event-emitter';
|
||||
import { MongooseModule } from '@nestjs/mongoose';
|
||||
import { Test } from '@nestjs/testing';
|
||||
|
||||
import { I18nService } from '@/i18n/services/i18n.service';
|
||||
import { LoggerService } from '@/logger/logger.service';
|
||||
import { NOT_FOUND_ID } from '@/utils/constants/mock';
|
||||
import {
|
||||
installLanguageFixtures,
|
||||
languageFixtures,
|
||||
} from '@/utils/test/fixtures/language';
|
||||
import { getPageQuery } from '@/utils/test/pagination';
|
||||
import {
|
||||
closeInMongodConnection,
|
||||
rootMongooseTestModule,
|
||||
} from '@/utils/test/test';
|
||||
|
||||
import { LanguageController } from './language.controller';
|
||||
import { LanguageUpdateDto } from '../dto/language.dto';
|
||||
import { LanguageRepository } from '../repositories/language.repository';
|
||||
import { Language, LanguageModel } from '../schemas/language.schema';
|
||||
import { LanguageService } from '../services/language.service';
|
||||
|
||||
describe('LanguageController', () => {
|
||||
let languageController: LanguageController;
|
||||
let languageService: LanguageService;
|
||||
let language: Language;
|
||||
|
||||
beforeAll(async () => {
|
||||
const module = await Test.createTestingModule({
|
||||
imports: [
|
||||
rootMongooseTestModule(installLanguageFixtures),
|
||||
MongooseModule.forFeature([LanguageModel]),
|
||||
],
|
||||
providers: [
|
||||
LanguageController,
|
||||
LanguageService,
|
||||
LanguageRepository,
|
||||
LoggerService,
|
||||
{
|
||||
provide: I18nService,
|
||||
useValue: {
|
||||
t: jest.fn().mockImplementation((t) => t),
|
||||
initDynamicLanguages: jest.fn(),
|
||||
},
|
||||
},
|
||||
{
|
||||
provide: CACHE_MANAGER,
|
||||
useValue: {
|
||||
del: jest.fn(),
|
||||
get: jest.fn(),
|
||||
set: jest.fn(),
|
||||
},
|
||||
},
|
||||
LoggerService,
|
||||
EventEmitter2,
|
||||
],
|
||||
}).compile();
|
||||
languageService = module.get<LanguageService>(LanguageService);
|
||||
languageController = module.get<LanguageController>(LanguageController);
|
||||
language = await languageService.findOne({ code: 'en' });
|
||||
});
|
||||
|
||||
afterEach(jest.clearAllMocks);
|
||||
afterAll(closeInMongodConnection);
|
||||
|
||||
describe('count', () => {
|
||||
it('should count languages', async () => {
|
||||
jest.spyOn(languageService, 'count');
|
||||
const result = await languageController.filterCount();
|
||||
|
||||
expect(languageService.count).toHaveBeenCalled();
|
||||
expect(result).toEqual({ count: languageFixtures.length });
|
||||
});
|
||||
});
|
||||
|
||||
describe('findOne', () => {
|
||||
it('should find one translation by id', async () => {
|
||||
jest.spyOn(languageService, 'findOne');
|
||||
const result = await languageController.findOne(language.id);
|
||||
|
||||
expect(languageService.findOne).toHaveBeenCalledWith(language.id);
|
||||
expect(result).toEqualPayload(
|
||||
languageFixtures.find(({ code }) => code === language.code),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('findPage', () => {
|
||||
const pageQuery = getPageQuery<Language>({ sort: ['code', 'asc'] });
|
||||
it('should find languages', async () => {
|
||||
jest.spyOn(languageService, 'findPage');
|
||||
const result = await languageController.findPage(pageQuery, {});
|
||||
|
||||
expect(languageService.findPage).toHaveBeenCalledWith({}, pageQuery);
|
||||
expect(result).toEqualPayload(
|
||||
languageFixtures.sort(({ code: codeA }, { code: codeB }) => {
|
||||
if (codeA < codeB) {
|
||||
return -1;
|
||||
}
|
||||
if (codeA > codeB) {
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('updateOne', () => {
|
||||
const translationUpdateDto: LanguageUpdateDto = {
|
||||
title: 'English (US)',
|
||||
};
|
||||
it('should update one language by id', async () => {
|
||||
jest.spyOn(languageService, 'updateOne');
|
||||
const result = await languageController.updateOne(
|
||||
language.id,
|
||||
translationUpdateDto,
|
||||
);
|
||||
|
||||
expect(languageService.updateOne).toHaveBeenCalledWith(
|
||||
language.id,
|
||||
translationUpdateDto,
|
||||
);
|
||||
expect(result).toEqualPayload({
|
||||
...languageFixtures.find(({ code }) => code === language.code),
|
||||
...translationUpdateDto,
|
||||
});
|
||||
});
|
||||
|
||||
it('should mark a language as default', async () => {
|
||||
jest.spyOn(languageService, 'updateOne');
|
||||
const translationUpdateDto = { isDefault: true };
|
||||
const frLang = await languageService.findOne({ code: 'fr' });
|
||||
const result = await languageController.updateOne(
|
||||
frLang.id,
|
||||
translationUpdateDto,
|
||||
);
|
||||
|
||||
expect(languageService.updateOne).toHaveBeenCalledWith(
|
||||
frLang.id,
|
||||
translationUpdateDto,
|
||||
);
|
||||
expect(result).toEqualPayload({
|
||||
...languageFixtures.find(({ code }) => code === frLang.code),
|
||||
...translationUpdateDto,
|
||||
});
|
||||
|
||||
const enLang = await languageService.findOne({ code: 'en' });
|
||||
expect(enLang.isDefault).toBe(false);
|
||||
});
|
||||
|
||||
it('should throw a NotFoundException when attempting to update a translation by id', async () => {
|
||||
jest.spyOn(languageService, 'updateOne');
|
||||
await expect(
|
||||
languageController.updateOne(NOT_FOUND_ID, translationUpdateDto),
|
||||
).rejects.toThrow(NotFoundException);
|
||||
});
|
||||
});
|
||||
|
||||
describe('deleteOne', () => {
|
||||
it('should throw when attempting to delete the default language', async () => {
|
||||
const defaultLang = await languageService.findOne({ isDefault: true });
|
||||
|
||||
await expect(
|
||||
languageController.deleteOne(defaultLang.id),
|
||||
).rejects.toThrow(BadRequestException);
|
||||
});
|
||||
});
|
||||
});
|
||||
154
api/src/i18n/controllers/language.controller.ts
Normal file
154
api/src/i18n/controllers/language.controller.ts
Normal file
@@ -0,0 +1,154 @@
|
||||
/*
|
||||
* 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).
|
||||
* 3. SaaS Restriction: This software, or any derivative of it, may not be used to offer a competing product or service (SaaS) without prior written consent from Hexastack. Offering the software as a service or using it in a commercial cloud environment without express permission is strictly prohibited.
|
||||
*/
|
||||
|
||||
import {
|
||||
BadRequestException,
|
||||
Body,
|
||||
Controller,
|
||||
Delete,
|
||||
Get,
|
||||
HttpCode,
|
||||
NotFoundException,
|
||||
Param,
|
||||
Patch,
|
||||
Post,
|
||||
Query,
|
||||
UseInterceptors,
|
||||
} from '@nestjs/common';
|
||||
import { CsrfCheck } from '@tekuconcept/nestjs-csrf';
|
||||
import { TFilterQuery } from 'mongoose';
|
||||
|
||||
import { CsrfInterceptor } from '@/interceptors/csrf.interceptor';
|
||||
import { LoggerService } from '@/logger/logger.service';
|
||||
import { BaseController } from '@/utils/generics/base-controller';
|
||||
import { DeleteResult } from '@/utils/generics/base-repository';
|
||||
import { PageQueryDto } from '@/utils/pagination/pagination-query.dto';
|
||||
import { PageQueryPipe } from '@/utils/pagination/pagination-query.pipe';
|
||||
import { SearchFilterPipe } from '@/utils/pipes/search-filter.pipe';
|
||||
|
||||
import { LanguageCreateDto, LanguageUpdateDto } from '../dto/language.dto';
|
||||
import { Language } from '../schemas/language.schema';
|
||||
import { LanguageService } from '../services/language.service';
|
||||
|
||||
@UseInterceptors(CsrfInterceptor)
|
||||
@Controller('language')
|
||||
export class LanguageController extends BaseController<Language> {
|
||||
constructor(
|
||||
private readonly languageService: LanguageService,
|
||||
private readonly logger: LoggerService,
|
||||
) {
|
||||
super(languageService);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves a paginated list of categories based on provided filters and pagination settings.
|
||||
* @param pageQuery - The pagination settings.
|
||||
* @param filters - The filters to apply to the language search.
|
||||
* @returns A Promise that resolves to a paginated list of categories.
|
||||
*/
|
||||
@Get()
|
||||
async findPage(
|
||||
@Query(PageQueryPipe) pageQuery: PageQueryDto<Language>,
|
||||
@Query(new SearchFilterPipe<Language>({ allowedFields: ['title', 'code'] }))
|
||||
filters: TFilterQuery<Language>,
|
||||
) {
|
||||
return await this.languageService.findPage(filters, pageQuery);
|
||||
}
|
||||
|
||||
/**
|
||||
* Counts the filtered number of categories.
|
||||
* @returns A promise that resolves to an object representing the filtered number of categories.
|
||||
*/
|
||||
@Get('count')
|
||||
async filterCount(
|
||||
@Query(
|
||||
new SearchFilterPipe<Language>({
|
||||
allowedFields: ['title', 'code'],
|
||||
}),
|
||||
)
|
||||
filters?: TFilterQuery<Language>,
|
||||
) {
|
||||
return await this.count(filters);
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds a language by its ID.
|
||||
* @param id - The ID of the language to find.
|
||||
* @returns A Promise that resolves to the found language.
|
||||
*/
|
||||
@Get(':id')
|
||||
async findOne(@Param('id') id: string): Promise<Language> {
|
||||
const doc = await this.languageService.findOne(id);
|
||||
if (!doc) {
|
||||
this.logger.warn(`Unable to find Language by id ${id}`);
|
||||
throw new NotFoundException(`Language with ID ${id} not found`);
|
||||
}
|
||||
return doc;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new language.
|
||||
* @param language - The data of the language to be created.
|
||||
* @returns A Promise that resolves to the created language.
|
||||
*/
|
||||
@CsrfCheck(true)
|
||||
@Post()
|
||||
async create(@Body() language: LanguageCreateDto): Promise<Language> {
|
||||
return await this.languageService.create(language);
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates an existing language.
|
||||
* @param id - The ID of the language to be updated.
|
||||
* @param languageUpdate - The updated data for the language.
|
||||
* @returns A Promise that resolves to the updated language.
|
||||
*/
|
||||
@CsrfCheck(true)
|
||||
@Patch(':id')
|
||||
async updateOne(
|
||||
@Param('id') id: string,
|
||||
@Body() languageUpdate: LanguageUpdateDto,
|
||||
): Promise<Language> {
|
||||
if ('isDefault' in languageUpdate) {
|
||||
if (languageUpdate.isDefault) {
|
||||
// A new default language is define, make sure that only one is marked as default
|
||||
await this.languageService.updateMany({}, { isDefault: false });
|
||||
} else {
|
||||
throw new BadRequestException('Should not be able to disable default');
|
||||
}
|
||||
}
|
||||
|
||||
const result = await this.languageService.updateOne(id, languageUpdate);
|
||||
if (!result) {
|
||||
this.logger.warn(`Unable to update Language by id ${id}`);
|
||||
throw new NotFoundException(`Language with ID ${id} not found`);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes a language by its ID.
|
||||
* @param id - The ID of the language to be deleted.
|
||||
* @returns A Promise that resolves to the deletion result.
|
||||
*/
|
||||
@CsrfCheck(true)
|
||||
@Delete(':id')
|
||||
@HttpCode(204)
|
||||
async deleteOne(@Param('id') id: string): Promise<DeleteResult> {
|
||||
const result = await this.languageService.deleteOne({
|
||||
isDefault: false, // Prevent deleting the default language
|
||||
_id: id,
|
||||
});
|
||||
if (result.deletedCount === 0) {
|
||||
this.logger.warn(`Unable to delete Language by id ${id}`);
|
||||
throw new BadRequestException(`Unable to delete Language with ID ${id}`);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
@@ -17,13 +17,23 @@ import { AttachmentRepository } from '@/attachment/repositories/attachment.repos
|
||||
import { AttachmentModel } from '@/attachment/schemas/attachment.schema';
|
||||
import { AttachmentService } from '@/attachment/services/attachment.service';
|
||||
import { ChannelService } from '@/channel/channel.service';
|
||||
import { MessageController } from '@/chat/controllers/message.controller';
|
||||
import { BlockRepository } from '@/chat/repositories/block.repository';
|
||||
import { MessageRepository } from '@/chat/repositories/message.repository';
|
||||
import { SubscriberRepository } from '@/chat/repositories/subscriber.repository';
|
||||
import { BlockModel } from '@/chat/schemas/block.schema';
|
||||
import { MessageModel } from '@/chat/schemas/message.schema';
|
||||
import { SubscriberModel } from '@/chat/schemas/subscriber.schema';
|
||||
import { BlockService } from '@/chat/services/block.service';
|
||||
import { MessageService } from '@/chat/services/message.service';
|
||||
import { SubscriberService } from '@/chat/services/subscriber.service';
|
||||
import { ContentRepository } from '@/cms/repositories/content.repository';
|
||||
import { MenuRepository } from '@/cms/repositories/menu.repository';
|
||||
import { ContentModel } from '@/cms/schemas/content.schema';
|
||||
import { MenuModel } from '@/cms/schemas/menu.schema';
|
||||
import { ContentService } from '@/cms/services/content.service';
|
||||
import { MenuService } from '@/cms/services/menu.service';
|
||||
import { ExtendedI18nService } from '@/extended-i18n.service';
|
||||
import { I18nService } from '@/i18n/services/i18n.service';
|
||||
import { LoggerService } from '@/logger/logger.service';
|
||||
import { NlpService } from '@/nlp/services/nlp.service';
|
||||
import { PluginService } from '@/plugins/plugins.service';
|
||||
@@ -39,20 +49,13 @@ import {
|
||||
rootMongooseTestModule,
|
||||
} from '@/utils/test/test';
|
||||
|
||||
import { MessageController } from './message.controller';
|
||||
import { TranslationController } from './translation.controller';
|
||||
import { TranslationUpdateDto } from '../dto/translation.dto';
|
||||
import { BlockRepository } from '../repositories/block.repository';
|
||||
import { MessageRepository } from '../repositories/message.repository';
|
||||
import { SubscriberRepository } from '../repositories/subscriber.repository';
|
||||
import { LanguageRepository } from '../repositories/language.repository';
|
||||
import { TranslationRepository } from '../repositories/translation.repository';
|
||||
import { BlockModel } from '../schemas/block.schema';
|
||||
import { MessageModel } from '../schemas/message.schema';
|
||||
import { SubscriberModel } from '../schemas/subscriber.schema';
|
||||
import { LanguageModel } from '../schemas/language.schema';
|
||||
import { Translation, TranslationModel } from '../schemas/translation.schema';
|
||||
import { BlockService } from '../services/block.service';
|
||||
import { MessageService } from '../services/message.service';
|
||||
import { SubscriberService } from '../services/subscriber.service';
|
||||
import { LanguageService } from '../services/language.service';
|
||||
import { TranslationService } from '../services/translation.service';
|
||||
|
||||
describe('TranslationController', () => {
|
||||
@@ -73,6 +76,7 @@ describe('TranslationController', () => {
|
||||
MenuModel,
|
||||
BlockModel,
|
||||
ContentModel,
|
||||
LanguageModel,
|
||||
]),
|
||||
],
|
||||
providers: [
|
||||
@@ -114,10 +118,10 @@ describe('TranslationController', () => {
|
||||
EventEmitter2,
|
||||
LoggerService,
|
||||
{
|
||||
provide: ExtendedI18nService,
|
||||
provide: I18nService,
|
||||
useValue: {
|
||||
t: jest.fn().mockImplementation((t) => t),
|
||||
initDynamicTranslations: jest.fn(),
|
||||
refreshDynamicTranslations: jest.fn(),
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -129,6 +133,8 @@ describe('TranslationController', () => {
|
||||
},
|
||||
},
|
||||
LoggerService,
|
||||
LanguageService,
|
||||
LanguageRepository,
|
||||
],
|
||||
}).compile();
|
||||
translationService = module.get<TranslationService>(TranslationService);
|
||||
@@ -8,15 +8,18 @@
|
||||
*/
|
||||
|
||||
import {
|
||||
BadRequestException,
|
||||
Body,
|
||||
Controller,
|
||||
Delete,
|
||||
Get,
|
||||
HttpCode,
|
||||
NotFoundException,
|
||||
Param,
|
||||
Patch,
|
||||
Post,
|
||||
Query,
|
||||
UseInterceptors,
|
||||
Post,
|
||||
} from '@nestjs/common';
|
||||
import { CsrfCheck } from '@tekuconcept/nestjs-csrf';
|
||||
import { TFilterQuery } from 'mongoose';
|
||||
@@ -25,18 +28,21 @@ import { CsrfInterceptor } from '@/interceptors/csrf.interceptor';
|
||||
import { LoggerService } from '@/logger/logger.service';
|
||||
import { SettingService } from '@/setting/services/setting.service';
|
||||
import { BaseController } from '@/utils/generics/base-controller';
|
||||
import { DeleteResult } from '@/utils/generics/base-repository';
|
||||
import { PageQueryDto } from '@/utils/pagination/pagination-query.dto';
|
||||
import { PageQueryPipe } from '@/utils/pagination/pagination-query.pipe';
|
||||
import { SearchFilterPipe } from '@/utils/pipes/search-filter.pipe';
|
||||
|
||||
import { TranslationUpdateDto } from '../dto/translation.dto';
|
||||
import { Translation } from '../schemas/translation.schema';
|
||||
import { LanguageService } from '../services/language.service';
|
||||
import { TranslationService } from '../services/translation.service';
|
||||
|
||||
@UseInterceptors(CsrfInterceptor)
|
||||
@Controller('translation')
|
||||
export class TranslationController extends BaseController<Translation> {
|
||||
constructor(
|
||||
private readonly languageService: LanguageService,
|
||||
private readonly translationService: TranslationService,
|
||||
private readonly settingService: SettingService,
|
||||
private readonly logger: LoggerService,
|
||||
@@ -103,40 +109,56 @@ export class TranslationController extends BaseController<Translation> {
|
||||
@CsrfCheck(true)
|
||||
@Post('refresh')
|
||||
async refresh(): Promise<any> {
|
||||
const settings = await this.settingService.getSettings();
|
||||
const languages = settings.nlp_settings.languages;
|
||||
const defaultTrans: Translation['translations'] = languages.reduce(
|
||||
(acc, curr) => {
|
||||
acc[curr] = '';
|
||||
return acc;
|
||||
},
|
||||
{} as { [key: string]: string },
|
||||
);
|
||||
const defaultLanguage = await this.languageService.getDefaultLanguage();
|
||||
const languages = await this.languageService.getLanguages();
|
||||
const defaultTrans: Translation['translations'] = Object.keys(languages)
|
||||
.filter((lang) => lang !== defaultLanguage.code)
|
||||
.reduce(
|
||||
(acc, curr) => {
|
||||
acc[curr] = '';
|
||||
return acc;
|
||||
},
|
||||
{} as { [key: string]: string },
|
||||
);
|
||||
// Scan Blocks
|
||||
return this.translationService
|
||||
.getAllBlockStrings()
|
||||
.then(async (strings: string[]) => {
|
||||
const settingStrings =
|
||||
await this.translationService.getSettingStrings();
|
||||
// Scan global settings
|
||||
strings = strings.concat(settingStrings);
|
||||
// Filter unique and not empty messages
|
||||
strings = strings.filter((str, pos) => {
|
||||
return str && strings.indexOf(str) == pos;
|
||||
});
|
||||
// Perform refresh
|
||||
const queue = strings.map((str) =>
|
||||
this.translationService.findOneOrCreate(
|
||||
{ str },
|
||||
{ str, translations: defaultTrans as any, translated: 100 },
|
||||
),
|
||||
);
|
||||
return Promise.all(queue).then(() => {
|
||||
// Purge non existing translations
|
||||
return this.translationService.deleteMany({
|
||||
str: { $nin: strings },
|
||||
});
|
||||
});
|
||||
});
|
||||
let strings = await this.translationService.getAllBlockStrings();
|
||||
const settingStrings = await this.translationService.getSettingStrings();
|
||||
// Scan global settings
|
||||
strings = strings.concat(settingStrings);
|
||||
// Filter unique and not empty messages
|
||||
strings = strings.filter((str, pos) => {
|
||||
return str && strings.indexOf(str) == pos;
|
||||
});
|
||||
// Perform refresh
|
||||
const queue = strings.map((str) =>
|
||||
this.translationService.findOneOrCreate(
|
||||
{ str },
|
||||
{ str, translations: defaultTrans },
|
||||
),
|
||||
);
|
||||
await Promise.all(queue);
|
||||
// Purge non existing translations
|
||||
return this.translationService.deleteMany({
|
||||
str: { $nin: strings },
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes a translation by its ID.
|
||||
* @param id - The ID of the translation to be deleted.
|
||||
* @returns A Promise that resolves to the deletion result.
|
||||
*/
|
||||
@CsrfCheck(true)
|
||||
@Delete(':id')
|
||||
@HttpCode(204)
|
||||
async deleteOne(@Param('id') id: string): Promise<DeleteResult> {
|
||||
const result = await this.translationService.deleteOne(id);
|
||||
if (result.deletedCount === 0) {
|
||||
this.logger.warn(`Unable to delete Translation by id ${id}`);
|
||||
throw new BadRequestException(
|
||||
`Unable to delete Translation with ID ${id}`,
|
||||
);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
35
api/src/i18n/dto/language.dto.ts
Normal file
35
api/src/i18n/dto/language.dto.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
/*
|
||||
* 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).
|
||||
* 3. SaaS Restriction: This software, or any derivative of it, may not be used to offer a competing product or service (SaaS) without prior written consent from Hexastack. Offering the software as a service or using it in a commercial cloud environment without express permission is strictly prohibited.
|
||||
*/
|
||||
|
||||
import { PartialType } from '@nestjs/mapped-types';
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { IsBoolean, IsNotEmpty, IsOptional, IsString } from 'class-validator';
|
||||
|
||||
export class LanguageCreateDto {
|
||||
@ApiProperty({ description: 'Language Title', type: String })
|
||||
@IsNotEmpty()
|
||||
@IsString()
|
||||
title: string;
|
||||
|
||||
@ApiProperty({ description: 'Language Code', type: String })
|
||||
@IsNotEmpty()
|
||||
@IsString()
|
||||
code: string;
|
||||
|
||||
@ApiProperty({ description: 'Whether Language is RTL', type: Boolean })
|
||||
@IsBoolean()
|
||||
isRTL: boolean;
|
||||
|
||||
@ApiProperty({ description: 'Is Default Language ?', type: Boolean })
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
isDefault?: boolean;
|
||||
}
|
||||
|
||||
export class LanguageUpdateDto extends PartialType(LanguageCreateDto) {}
|
||||
79
api/src/i18n/i18n.module.ts
Normal file
79
api/src/i18n/i18n.module.ts
Normal file
@@ -0,0 +1,79 @@
|
||||
/*
|
||||
* 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).
|
||||
* 3. SaaS Restriction: This software, or any derivative of it, may not be used to offer a competing product or service (SaaS) without prior written consent from Hexastack. Offering the software as a service or using it in a commercial cloud environment without express permission is strictly prohibited.
|
||||
*/
|
||||
|
||||
import {
|
||||
DynamicModule,
|
||||
forwardRef,
|
||||
Global,
|
||||
Inject,
|
||||
Module,
|
||||
} from '@nestjs/common';
|
||||
import { HttpAdapterHost } from '@nestjs/core';
|
||||
import { MongooseModule } from '@nestjs/mongoose';
|
||||
import {
|
||||
I18N_OPTIONS,
|
||||
I18N_TRANSLATIONS,
|
||||
I18nOptions,
|
||||
I18nTranslation,
|
||||
I18nModule as NativeI18nModule,
|
||||
} from 'nestjs-i18n';
|
||||
import { Observable } from 'rxjs';
|
||||
|
||||
import { ChatModule } from '@/chat/chat.module';
|
||||
|
||||
import { LanguageController } from './controllers/language.controller';
|
||||
import { TranslationController } from './controllers/translation.controller';
|
||||
import { LanguageRepository } from './repositories/language.repository';
|
||||
import { TranslationRepository } from './repositories/translation.repository';
|
||||
import { LanguageModel } from './schemas/language.schema';
|
||||
import { TranslationModel } from './schemas/translation.schema';
|
||||
import { LanguageSeeder } from './seeds/language.seed';
|
||||
import { TranslationSeeder } from './seeds/translation.seed';
|
||||
import { I18nService } from './services/i18n.service';
|
||||
import { LanguageService } from './services/language.service';
|
||||
import { TranslationService } from './services/translation.service';
|
||||
|
||||
@Global()
|
||||
@Module({})
|
||||
export class I18nModule extends NativeI18nModule {
|
||||
constructor(
|
||||
i18n: I18nService,
|
||||
@Inject(I18N_TRANSLATIONS)
|
||||
translations: Observable<I18nTranslation>,
|
||||
@Inject(I18N_OPTIONS) i18nOptions: I18nOptions,
|
||||
adapter: HttpAdapterHost,
|
||||
) {
|
||||
super(i18n, translations, i18nOptions, adapter);
|
||||
}
|
||||
|
||||
static forRoot(options: I18nOptions): DynamicModule {
|
||||
const { imports, providers, controllers, exports } = super.forRoot(options);
|
||||
return {
|
||||
module: I18nModule,
|
||||
imports: (imports || []).concat([
|
||||
MongooseModule.forFeature([LanguageModel, TranslationModel]),
|
||||
forwardRef(() => ChatModule),
|
||||
]),
|
||||
controllers: (controllers || []).concat([
|
||||
LanguageController,
|
||||
TranslationController,
|
||||
]),
|
||||
providers: providers.concat([
|
||||
I18nService,
|
||||
LanguageRepository,
|
||||
LanguageService,
|
||||
LanguageSeeder,
|
||||
TranslationRepository,
|
||||
TranslationService,
|
||||
TranslationSeeder,
|
||||
]),
|
||||
exports: exports.concat(I18nService, LanguageService),
|
||||
};
|
||||
}
|
||||
}
|
||||
53
api/src/i18n/repositories/language.repository.ts
Normal file
53
api/src/i18n/repositories/language.repository.ts
Normal file
@@ -0,0 +1,53 @@
|
||||
/*
|
||||
* 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).
|
||||
* 3. SaaS Restriction: This software, or any derivative of it, may not be used to offer a competing product or service (SaaS) without prior written consent from Hexastack. Offering the software as a service or using it in a commercial cloud environment without express permission is strictly prohibited.
|
||||
*/
|
||||
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { EventEmitter2 } from '@nestjs/event-emitter';
|
||||
import { InjectModel } from '@nestjs/mongoose';
|
||||
import { Document, Model, Query, TFilterQuery } from 'mongoose';
|
||||
|
||||
import { BaseRepository, DeleteResult } from '@/utils/generics/base-repository';
|
||||
|
||||
import { Language } from '../schemas/language.schema';
|
||||
|
||||
@Injectable()
|
||||
export class LanguageRepository extends BaseRepository<Language> {
|
||||
constructor(
|
||||
@InjectModel(Language.name) readonly model: Model<Language>,
|
||||
private readonly eventEmitter: EventEmitter2,
|
||||
) {
|
||||
super(model, Language);
|
||||
}
|
||||
|
||||
/**
|
||||
* Pre-delete hook that triggers before an language is deleted.
|
||||
*
|
||||
* @param query The query used to delete the language.
|
||||
* @param criteria The filter criteria used to find the language for deletion.
|
||||
*/
|
||||
async preDelete(
|
||||
_query: Query<
|
||||
DeleteResult,
|
||||
Document<Language, any, any>,
|
||||
unknown,
|
||||
Language,
|
||||
'deleteOne' | 'deleteMany'
|
||||
>,
|
||||
criteria: TFilterQuery<Language>,
|
||||
): Promise<void> {
|
||||
if (criteria._id) {
|
||||
const language = await this.findOne(
|
||||
typeof criteria === 'string' ? { _id: criteria } : criteria,
|
||||
);
|
||||
this.eventEmitter.emit('hook:language:delete', language);
|
||||
} else {
|
||||
throw new Error('Attempted to delete language using unknown criteria');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -14,7 +14,7 @@ import { Document, Model, Query, Types } from 'mongoose';
|
||||
|
||||
import { BaseRepository, DeleteResult } from '@/utils/generics/base-repository';
|
||||
|
||||
import { Translation } from '../schemas/translation.schema';
|
||||
import { Translation } from '../../i18n/schemas/translation.schema';
|
||||
|
||||
@Injectable()
|
||||
export class TranslationRepository extends BaseRepository<Translation> {
|
||||
52
api/src/i18n/schemas/language.schema.ts
Normal file
52
api/src/i18n/schemas/language.schema.ts
Normal file
@@ -0,0 +1,52 @@
|
||||
/*
|
||||
* 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).
|
||||
* 3. SaaS Restriction: This software, or any derivative of it, may not be used to offer a competing product or service (SaaS) without prior written consent from Hexastack. Offering the software as a service or using it in a commercial cloud environment without express permission is strictly prohibited.
|
||||
*/
|
||||
|
||||
import { Prop, Schema, SchemaFactory, ModelDefinition } from '@nestjs/mongoose';
|
||||
import { THydratedDocument } from 'mongoose';
|
||||
|
||||
import { BaseSchema } from '@/utils/generics/base-schema';
|
||||
import { LifecycleHookManager } from '@/utils/generics/lifecycle-hook-manager';
|
||||
|
||||
@Schema({ timestamps: true })
|
||||
export class Language extends BaseSchema {
|
||||
@Prop({
|
||||
type: String,
|
||||
required: true,
|
||||
unique: true,
|
||||
})
|
||||
title: string;
|
||||
|
||||
@Prop({
|
||||
type: String,
|
||||
required: true,
|
||||
unique: true,
|
||||
})
|
||||
code: string;
|
||||
|
||||
@Prop({
|
||||
type: Boolean,
|
||||
default: false,
|
||||
})
|
||||
isDefault?: boolean;
|
||||
|
||||
@Prop({
|
||||
type: Boolean,
|
||||
default: false,
|
||||
})
|
||||
isRTL?: boolean;
|
||||
}
|
||||
|
||||
export const LanguageModel: ModelDefinition = LifecycleHookManager.attach({
|
||||
name: Language.name,
|
||||
schema: SchemaFactory.createForClass(Language),
|
||||
});
|
||||
|
||||
export type LanguageDocument = THydratedDocument<Language>;
|
||||
|
||||
export default LanguageModel.schema;
|
||||
@@ -11,6 +11,7 @@ import { Prop, Schema, SchemaFactory, ModelDefinition } from '@nestjs/mongoose';
|
||||
import { THydratedDocument } from 'mongoose';
|
||||
|
||||
import { BaseSchema } from '@/utils/generics/base-schema';
|
||||
import { LifecycleHookManager } from '@/utils/generics/lifecycle-hook-manager';
|
||||
|
||||
@Schema({ timestamps: true })
|
||||
export class Translation extends BaseSchema {
|
||||
@@ -26,17 +27,12 @@ export class Translation extends BaseSchema {
|
||||
required: true,
|
||||
})
|
||||
translations: Record<string, string>;
|
||||
|
||||
@Prop({
|
||||
type: Number,
|
||||
})
|
||||
translated: number;
|
||||
}
|
||||
|
||||
export const TranslationModel: ModelDefinition = {
|
||||
export const TranslationModel: ModelDefinition = LifecycleHookManager.attach({
|
||||
name: Translation.name,
|
||||
schema: SchemaFactory.createForClass(Translation),
|
||||
};
|
||||
});
|
||||
|
||||
export type TranslationDocument = THydratedDocument<Translation>;
|
||||
|
||||
24
api/src/i18n/seeds/language.seed-model.ts
Normal file
24
api/src/i18n/seeds/language.seed-model.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
/*
|
||||
* 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).
|
||||
* 3. SaaS Restriction: This software, or any derivative of it, may not be used to offer a competing product or service (SaaS) without prior written consent from Hexastack. Offering the software as a service or using it in a commercial cloud environment without express permission is strictly prohibited.
|
||||
*/
|
||||
|
||||
import { LanguageCreateDto } from '../dto/language.dto';
|
||||
|
||||
export const languageModels: LanguageCreateDto[] = [
|
||||
{
|
||||
title: 'English',
|
||||
code: 'en',
|
||||
isRTL: false,
|
||||
isDefault: true,
|
||||
},
|
||||
{
|
||||
title: 'Français',
|
||||
code: 'fr',
|
||||
isRTL: false,
|
||||
},
|
||||
];
|
||||
22
api/src/i18n/seeds/language.seed.ts
Normal file
22
api/src/i18n/seeds/language.seed.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
/*
|
||||
* 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).
|
||||
* 3. SaaS Restriction: This software, or any derivative of it, may not be used to offer a competing product or service (SaaS) without prior written consent from Hexastack. Offering the software as a service or using it in a commercial cloud environment without express permission is strictly prohibited.
|
||||
*/
|
||||
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
import { BaseSeeder } from '@/utils/generics/base-seeder';
|
||||
|
||||
import { LanguageRepository } from '../repositories/language.repository';
|
||||
import { Language } from '../schemas/language.schema';
|
||||
|
||||
@Injectable()
|
||||
export class LanguageSeeder extends BaseSeeder<Language> {
|
||||
constructor(private readonly languageRepository: LanguageRepository) {
|
||||
super(languageRepository);
|
||||
}
|
||||
}
|
||||
@@ -8,22 +8,22 @@
|
||||
*/
|
||||
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { OnEvent } from '@nestjs/event-emitter';
|
||||
import { I18nService, Path, PathValue, TranslateOptions } from 'nestjs-i18n';
|
||||
import {
|
||||
I18nService as NativeI18nService,
|
||||
Path,
|
||||
PathValue,
|
||||
TranslateOptions,
|
||||
} from 'nestjs-i18n';
|
||||
import { IfAnyOrNever } from 'nestjs-i18n/dist/types';
|
||||
|
||||
import { Translation } from './chat/schemas/translation.schema';
|
||||
import { config } from './config';
|
||||
import { config } from '@/config';
|
||||
import { Translation } from '@/i18n/schemas/translation.schema';
|
||||
|
||||
@Injectable()
|
||||
export class ExtendedI18nService<
|
||||
export class I18nService<
|
||||
K = Record<string, unknown>,
|
||||
> extends I18nService<K> {
|
||||
private dynamicTranslations: Record<string, Record<string, string>> =
|
||||
config.chatbot.lang.available.reduce(
|
||||
(acc, curr) => ({ ...acc, [curr]: {} }),
|
||||
{},
|
||||
);
|
||||
> extends NativeI18nService<K> {
|
||||
private dynamicTranslations: Record<string, Record<string, string>> = {};
|
||||
|
||||
t<P extends Path<K> = any, R = PathValue<K, P>>(
|
||||
key: P,
|
||||
@@ -35,17 +35,19 @@ export class ExtendedI18nService<
|
||||
...options,
|
||||
};
|
||||
let { lang } = options;
|
||||
lang = lang ?? this.i18nOptions.fallbackLanguage;
|
||||
lang = this.resolveLanguage(lang);
|
||||
|
||||
// Translate block message, button text, ...
|
||||
if (lang in this.dynamicTranslations) {
|
||||
if (key in this.dynamicTranslations[lang]) {
|
||||
return this.dynamicTranslations[lang][key] as IfAnyOrNever<
|
||||
R,
|
||||
string,
|
||||
R
|
||||
>;
|
||||
if (this.dynamicTranslations[lang][key]) {
|
||||
return this.dynamicTranslations[lang][key] as IfAnyOrNever<
|
||||
R,
|
||||
string,
|
||||
R
|
||||
>;
|
||||
}
|
||||
return options.defaultValue as IfAnyOrNever<R, string, R>;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -54,15 +56,13 @@ export class ExtendedI18nService<
|
||||
return super.t<P, R>(key, options);
|
||||
}
|
||||
|
||||
@OnEvent('hook:i18n:refresh')
|
||||
initDynamicTranslations(translations: Translation[]) {
|
||||
refreshDynamicTranslations(translations: Translation[]) {
|
||||
this.dynamicTranslations = translations.reduce((acc, curr) => {
|
||||
const { str, translations } = curr;
|
||||
Object.entries(translations)
|
||||
.filter(([lang]) => lang in acc)
|
||||
.forEach(([lang, t]) => {
|
||||
acc[lang][str] = t;
|
||||
});
|
||||
Object.entries(translations).forEach(([lang, t]) => {
|
||||
acc[lang] = acc[lang] || {};
|
||||
acc[lang][str] = t;
|
||||
});
|
||||
|
||||
return acc;
|
||||
}, this.dynamicTranslations);
|
||||
68
api/src/i18n/services/language.service.ts
Normal file
68
api/src/i18n/services/language.service.ts
Normal file
@@ -0,0 +1,68 @@
|
||||
/*
|
||||
* 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).
|
||||
* 3. SaaS Restriction: This software, or any derivative of it, may not be used to offer a competing product or service (SaaS) without prior written consent from Hexastack. Offering the software as a service or using it in a commercial cloud environment without express permission is strictly prohibited.
|
||||
*/
|
||||
|
||||
import { CACHE_MANAGER } from '@nestjs/cache-manager';
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { Cache } from 'cache-manager';
|
||||
|
||||
import {
|
||||
DEFAULT_LANGUAGE_CACHE_KEY,
|
||||
LANGUAGES_CACHE_KEY,
|
||||
} from '@/utils/constants/cache';
|
||||
import { Cacheable } from '@/utils/decorators/cacheable.decorator';
|
||||
import { BaseService } from '@/utils/generics/base-service';
|
||||
|
||||
import { LanguageRepository } from '../repositories/language.repository';
|
||||
import { Language } from '../schemas/language.schema';
|
||||
|
||||
@Injectable()
|
||||
export class LanguageService extends BaseService<Language> {
|
||||
constructor(
|
||||
readonly repository: LanguageRepository,
|
||||
@Inject(CACHE_MANAGER) private readonly cacheManager: Cache,
|
||||
) {
|
||||
super(repository);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves all available languages from the repository.
|
||||
*
|
||||
* @returns A promise that resolves to an object where each key is a language code
|
||||
* and the corresponding value is the `Language` object.
|
||||
*/
|
||||
@Cacheable(LANGUAGES_CACHE_KEY)
|
||||
async getLanguages() {
|
||||
const languages = await this.findAll();
|
||||
return languages.reduce((acc, curr) => {
|
||||
return {
|
||||
...acc,
|
||||
[curr.code]: curr,
|
||||
};
|
||||
}, {});
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the default language from the repository.
|
||||
*
|
||||
* @returns A promise that resolves to the default `Language` object.
|
||||
*/
|
||||
@Cacheable(DEFAULT_LANGUAGE_CACHE_KEY)
|
||||
async getDefaultLanguage() {
|
||||
return await this.findOne({ isDefault: true });
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the language by code.
|
||||
*
|
||||
* @returns A promise that resolves to the `Language` object.
|
||||
*/
|
||||
async getLanguageByCode(code: string) {
|
||||
return await this.findOne({ code });
|
||||
}
|
||||
}
|
||||
266
api/src/i18n/services/translation.service.spec.ts
Normal file
266
api/src/i18n/services/translation.service.spec.ts
Normal file
@@ -0,0 +1,266 @@
|
||||
/*
|
||||
* 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).
|
||||
* 3. SaaS Restriction: This software, or any derivative of it, may not be used to offer a competing product or service (SaaS) without prior written consent from Hexastack. Offering the software as a service or using it in a commercial cloud environment without express permission is strictly prohibited.
|
||||
*/
|
||||
|
||||
import { EventEmitter2 } from '@nestjs/event-emitter';
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
|
||||
import { I18nService } from '@/i18n/services/i18n.service';
|
||||
import { Settings } from '@/setting/schemas/types';
|
||||
import { SettingService } from '@/setting/services/setting.service';
|
||||
|
||||
import { Block } from '../../chat/schemas/block.schema';
|
||||
import { BlockOptions } from '../../chat/schemas/types/options';
|
||||
import { BlockService } from '../../chat/services/block.service';
|
||||
import { TranslationRepository } from '../repositories/translation.repository';
|
||||
import { TranslationService } from '../services/translation.service';
|
||||
|
||||
describe('TranslationService', () => {
|
||||
let service: TranslationService;
|
||||
let settingService: SettingService;
|
||||
let i18nService: I18nService;
|
||||
|
||||
beforeEach(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
providers: [
|
||||
TranslationService,
|
||||
{
|
||||
provide: TranslationRepository,
|
||||
useValue: {
|
||||
findAll: jest.fn().mockResolvedValue([
|
||||
{
|
||||
key: 'test',
|
||||
value: 'test',
|
||||
lang: 'en',
|
||||
},
|
||||
]),
|
||||
},
|
||||
},
|
||||
{
|
||||
provide: BlockService,
|
||||
useValue: {
|
||||
find: jest.fn().mockResolvedValue([
|
||||
{
|
||||
id: 'blockId',
|
||||
message: ['Test message'],
|
||||
options: {
|
||||
fallback: {
|
||||
message: ['Fallback message'],
|
||||
},
|
||||
},
|
||||
} as Block,
|
||||
]),
|
||||
},
|
||||
},
|
||||
{
|
||||
provide: SettingService,
|
||||
useValue: {
|
||||
getSettings: jest.fn().mockResolvedValue({
|
||||
chatbot_settings: {
|
||||
global_fallback: true,
|
||||
fallback_message: ['Global fallback message'],
|
||||
},
|
||||
} as Settings),
|
||||
},
|
||||
},
|
||||
{
|
||||
provide: I18nService,
|
||||
useValue: {
|
||||
refreshDynamicTranslations: jest.fn(),
|
||||
},
|
||||
},
|
||||
EventEmitter2,
|
||||
],
|
||||
}).compile();
|
||||
|
||||
service = module.get<TranslationService>(TranslationService);
|
||||
settingService = module.get<SettingService>(SettingService);
|
||||
i18nService = module.get<I18nService>(I18nService);
|
||||
});
|
||||
|
||||
it('should call refreshDynamicTranslations with translations from findAll', async () => {
|
||||
jest.spyOn(i18nService, 'refreshDynamicTranslations');
|
||||
await service.resetI18nTranslations();
|
||||
expect(i18nService.refreshDynamicTranslations).toHaveBeenCalledWith([
|
||||
{
|
||||
key: 'test',
|
||||
value: 'test',
|
||||
lang: 'en',
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('should return an array of strings from all blocks', async () => {
|
||||
const strings = await service.getAllBlockStrings();
|
||||
expect(strings).toEqual(['Test message', 'Fallback message']);
|
||||
});
|
||||
|
||||
it('should return an array of strings from the settings when global fallback is enabled', async () => {
|
||||
const strings = await service.getSettingStrings();
|
||||
expect(strings).toEqual(['Global fallback message']);
|
||||
});
|
||||
|
||||
it('should return an empty array from the settings when global fallback is disabled', async () => {
|
||||
jest.spyOn(settingService, 'getSettings').mockResolvedValueOnce({
|
||||
chatbot_settings: {
|
||||
global_fallback: false,
|
||||
fallback_message: ['Global fallback message'],
|
||||
},
|
||||
} as Settings);
|
||||
|
||||
const strings = await service.getSettingStrings();
|
||||
expect(strings).toEqual([]);
|
||||
});
|
||||
|
||||
it('should return an array of strings from a block with a quick reply message', () => {
|
||||
const block = {
|
||||
id: 'blockId',
|
||||
name: 'Test Block',
|
||||
category: 'Test Category',
|
||||
position: { x: 0, y: 0 },
|
||||
message: {
|
||||
text: 'Test message',
|
||||
quickReplies: [
|
||||
{
|
||||
title: 'Quick reply 1',
|
||||
},
|
||||
{
|
||||
title: 'Quick reply 2',
|
||||
},
|
||||
],
|
||||
},
|
||||
options: {
|
||||
fallback: {
|
||||
active: true,
|
||||
message: ['Fallback message'],
|
||||
max_attempts: 3,
|
||||
} as BlockOptions,
|
||||
},
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
} as Block;
|
||||
const strings = service.getBlockStrings(block);
|
||||
expect(strings).toEqual([
|
||||
'Test message',
|
||||
'Quick reply 1',
|
||||
'Quick reply 2',
|
||||
'Fallback message',
|
||||
]);
|
||||
});
|
||||
|
||||
it('should return an array of strings from a block with a button message', () => {
|
||||
const block = {
|
||||
id: 'blockId',
|
||||
name: 'Test Block',
|
||||
category: 'Test Category',
|
||||
position: { x: 0, y: 0 },
|
||||
message: {
|
||||
text: 'Test message',
|
||||
buttons: [
|
||||
{
|
||||
title: 'Button 1',
|
||||
},
|
||||
{
|
||||
title: 'Button 2',
|
||||
},
|
||||
],
|
||||
},
|
||||
options: {
|
||||
fallback: {
|
||||
active: true,
|
||||
message: ['Fallback message'],
|
||||
max_attempts: 3,
|
||||
} as BlockOptions,
|
||||
},
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
} as Block;
|
||||
const strings = service.getBlockStrings(block);
|
||||
expect(strings).toEqual([
|
||||
'Test message',
|
||||
'Button 1',
|
||||
'Button 2',
|
||||
'Fallback message',
|
||||
]);
|
||||
});
|
||||
|
||||
it('should return an array of strings from a block with a text message', () => {
|
||||
const block = {
|
||||
id: 'blockId',
|
||||
name: 'Test Block',
|
||||
category: 'Test Category',
|
||||
position: { x: 0, y: 0 },
|
||||
message: ['Test message'], // Text message as an array
|
||||
options: {
|
||||
fallback: {
|
||||
active: true,
|
||||
message: ['Fallback message'],
|
||||
max_attempts: 3,
|
||||
} as BlockOptions,
|
||||
},
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
} as Block;
|
||||
const strings = service.getBlockStrings(block);
|
||||
expect(strings).toEqual(['Test message', 'Fallback message']);
|
||||
});
|
||||
|
||||
it('should return an array of strings from a block with a nested message object', () => {
|
||||
const block = {
|
||||
id: 'blockId',
|
||||
name: 'Test Block',
|
||||
category: 'Test Category',
|
||||
position: { x: 0, y: 0 },
|
||||
message: {
|
||||
text: 'Test message', // Nested text message
|
||||
},
|
||||
options: {
|
||||
fallback: {
|
||||
active: true,
|
||||
message: ['Fallback message'],
|
||||
max_attempts: 3,
|
||||
} as BlockOptions,
|
||||
},
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
} as Block;
|
||||
const strings = service.getBlockStrings(block);
|
||||
expect(strings).toEqual(['Test message', 'Fallback message']);
|
||||
});
|
||||
|
||||
it('should handle different message formats in getBlockStrings', () => {
|
||||
// Covers lines 54-60, 65
|
||||
|
||||
// Test with an array message (line 54-57)
|
||||
const block1 = {
|
||||
id: 'blockId1',
|
||||
message: ['This is a text message'],
|
||||
options: { fallback: { message: ['Fallback message'] } },
|
||||
} as Block;
|
||||
const strings1 = service.getBlockStrings(block1);
|
||||
expect(strings1).toEqual(['This is a text message', 'Fallback message']);
|
||||
|
||||
// Test with an object message (line 58-60)
|
||||
const block2 = {
|
||||
id: 'blockId2',
|
||||
message: { text: 'Another text message' },
|
||||
options: { fallback: { message: ['Fallback message'] } },
|
||||
} as Block;
|
||||
const strings2 = service.getBlockStrings(block2);
|
||||
expect(strings2).toEqual(['Another text message', 'Fallback message']);
|
||||
|
||||
// Test a block without a fallback (line 65)
|
||||
const block3 = {
|
||||
id: 'blockId3',
|
||||
message: { text: 'Another test message' },
|
||||
options: {},
|
||||
} as Block;
|
||||
const strings3 = service.getBlockStrings(block3);
|
||||
expect(strings3).toEqual(['Another test message']);
|
||||
});
|
||||
});
|
||||
@@ -10,13 +10,13 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { OnEvent } from '@nestjs/event-emitter';
|
||||
|
||||
import { ExtendedI18nService } from '@/extended-i18n.service';
|
||||
import { I18nService } from '@/i18n/services/i18n.service';
|
||||
import { SettingService } from '@/setting/services/setting.service';
|
||||
import { BaseService } from '@/utils/generics/base-service';
|
||||
|
||||
import { BlockService } from './block.service';
|
||||
import { Block } from '../../chat/schemas/block.schema';
|
||||
import { BlockService } from '../../chat/services/block.service';
|
||||
import { TranslationRepository } from '../repositories/translation.repository';
|
||||
import { Block } from '../schemas/block.schema';
|
||||
import { Translation } from '../schemas/translation.schema';
|
||||
|
||||
@Injectable()
|
||||
@@ -25,7 +25,7 @@ export class TranslationService extends BaseService<Translation> {
|
||||
readonly repository: TranslationRepository,
|
||||
private readonly blockService: BlockService,
|
||||
private readonly settingService: SettingService,
|
||||
private readonly i18n: ExtendedI18nService,
|
||||
private readonly i18n: I18nService,
|
||||
) {
|
||||
super(repository);
|
||||
this.resetI18nTranslations();
|
||||
@@ -33,7 +33,7 @@ export class TranslationService extends BaseService<Translation> {
|
||||
|
||||
public async resetI18nTranslations() {
|
||||
const translations = await this.findAll();
|
||||
this.i18n.initDynamicTranslations(translations);
|
||||
this.i18n.refreshDynamicTranslations(translations);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -51,6 +51,7 @@ async function bootstrap() {
|
||||
app.useGlobalPipes(
|
||||
new ValidationPipe({
|
||||
whitelist: true,
|
||||
transform: true,
|
||||
// forbidNonWhitelisted: true,
|
||||
}),
|
||||
new ObjectIdPipe(),
|
||||
|
||||
@@ -18,7 +18,10 @@ import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { AttachmentRepository } from '@/attachment/repositories/attachment.repository';
|
||||
import { AttachmentModel } from '@/attachment/schemas/attachment.schema';
|
||||
import { AttachmentService } from '@/attachment/services/attachment.service';
|
||||
import { ExtendedI18nService } from '@/extended-i18n.service';
|
||||
import { LanguageRepository } from '@/i18n/repositories/language.repository';
|
||||
import { Language, LanguageModel } from '@/i18n/schemas/language.schema';
|
||||
import { I18nService } from '@/i18n/services/i18n.service';
|
||||
import { LanguageService } from '@/i18n/services/language.service';
|
||||
import { LoggerService } from '@/logger/logger.service';
|
||||
import { SettingRepository } from '@/setting/repositories/setting.repository';
|
||||
import { SettingModel } from '@/setting/schemas/setting.schema';
|
||||
@@ -57,7 +60,9 @@ describe('NlpSampleController', () => {
|
||||
let nlpEntityService: NlpEntityService;
|
||||
let nlpValueService: NlpValueService;
|
||||
let attachmentService: AttachmentService;
|
||||
let languageService: LanguageService;
|
||||
let byeJhonSampleId: string;
|
||||
let languages: Language[];
|
||||
|
||||
beforeAll(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
@@ -74,6 +79,7 @@ describe('NlpSampleController', () => {
|
||||
NlpEntityModel,
|
||||
NlpValueModel,
|
||||
SettingModel,
|
||||
LanguageModel,
|
||||
]),
|
||||
],
|
||||
providers: [
|
||||
@@ -88,13 +94,15 @@ describe('NlpSampleController', () => {
|
||||
NlpValueRepository,
|
||||
NlpSampleService,
|
||||
NlpSampleEntityService,
|
||||
LanguageRepository,
|
||||
LanguageService,
|
||||
EventEmitter2,
|
||||
NlpService,
|
||||
SettingRepository,
|
||||
SettingService,
|
||||
SettingSeeder,
|
||||
{
|
||||
provide: ExtendedI18nService,
|
||||
provide: I18nService,
|
||||
useValue: {
|
||||
t: jest.fn().mockImplementation((t) => t),
|
||||
},
|
||||
@@ -122,6 +130,8 @@ describe('NlpSampleController', () => {
|
||||
})
|
||||
).id;
|
||||
attachmentService = module.get<AttachmentService>(AttachmentService);
|
||||
languageService = module.get<LanguageService>(LanguageService);
|
||||
languages = await languageService.findAll();
|
||||
});
|
||||
afterAll(async () => {
|
||||
await closeInMongodConnection();
|
||||
@@ -134,7 +144,7 @@ describe('NlpSampleController', () => {
|
||||
const pageQuery = getPageQuery<NlpSample>({ sort: ['text', 'desc'] });
|
||||
const result = await nlpSampleController.findPage(
|
||||
pageQuery,
|
||||
['entities'],
|
||||
['language', 'entities'],
|
||||
{},
|
||||
);
|
||||
const nlpSamples = await nlpSampleService.findAll();
|
||||
@@ -146,6 +156,7 @@ describe('NlpSampleController', () => {
|
||||
entities: nlpSampleEntities.filter((currSampleEntity) => {
|
||||
return currSampleEntity.sample === currSample.id;
|
||||
}),
|
||||
language: languages.find((lang) => lang.id === currSample.language),
|
||||
};
|
||||
acc.push(sampleWithEntities);
|
||||
return acc;
|
||||
@@ -163,7 +174,12 @@ describe('NlpSampleController', () => {
|
||||
['invalidCriteria'],
|
||||
{},
|
||||
);
|
||||
expect(result).toEqualPayload(nlpSampleFixtures);
|
||||
expect(result).toEqualPayload(
|
||||
nlpSampleFixtures.map((sample) => ({
|
||||
...sample,
|
||||
language: languages[sample.language].id,
|
||||
})),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -177,14 +193,19 @@ describe('NlpSampleController', () => {
|
||||
|
||||
describe('create', () => {
|
||||
it('should create nlp sample', async () => {
|
||||
const enLang = await languageService.findOne({ code: 'en' });
|
||||
const nlSample: NlpSampleDto = {
|
||||
text: 'text1',
|
||||
trained: true,
|
||||
type: NlpSampleState.test,
|
||||
entities: [],
|
||||
language: 'en',
|
||||
};
|
||||
const result = await nlpSampleController.create(nlSample);
|
||||
expect(result).toEqualPayload(nlSample);
|
||||
expect(result).toEqualPayload({
|
||||
...nlSample,
|
||||
language: enLang,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -209,7 +230,10 @@ describe('NlpSampleController', () => {
|
||||
const result = await nlpSampleController.findOne(yessSample.id, [
|
||||
'invalidCreteria',
|
||||
]);
|
||||
expect(result).toEqualPayload(nlpSampleFixtures[0]);
|
||||
expect(result).toEqualPayload({
|
||||
...nlpSampleFixtures[0],
|
||||
language: languages[nlpSampleFixtures[0].language].id,
|
||||
});
|
||||
});
|
||||
|
||||
it('should find a nlp sample and populate its entities', async () => {
|
||||
@@ -225,6 +249,7 @@ describe('NlpSampleController', () => {
|
||||
const samplesWithEntities = {
|
||||
...nlpSampleFixtures[0],
|
||||
entities: [yessSampleEntity],
|
||||
language: languages[nlpSampleFixtures[0].language],
|
||||
};
|
||||
expect(result).toEqualPayload(samplesWithEntities);
|
||||
});
|
||||
@@ -241,6 +266,9 @@ describe('NlpSampleController', () => {
|
||||
const yessSample = await nlpSampleService.findOne({
|
||||
text: 'yess',
|
||||
});
|
||||
const frLang = await languageService.findOne({
|
||||
code: 'fr',
|
||||
});
|
||||
const result = await nlpSampleController.updateOne(yessSample.id, {
|
||||
text: 'updated',
|
||||
trained: true,
|
||||
@@ -251,6 +279,7 @@ describe('NlpSampleController', () => {
|
||||
value: 'update',
|
||||
},
|
||||
],
|
||||
language: 'fr',
|
||||
});
|
||||
const updatedSample = {
|
||||
text: 'updated',
|
||||
@@ -263,11 +292,13 @@ describe('NlpSampleController', () => {
|
||||
value: expect.stringMatching(/^[a-z0-9]+$/),
|
||||
},
|
||||
],
|
||||
language: frLang,
|
||||
};
|
||||
expect(result.text).toEqual(updatedSample.text);
|
||||
expect(result.type).toEqual(updatedSample.type);
|
||||
expect(result.trained).toEqual(updatedSample.trained);
|
||||
expect(result.entities).toMatchObject(updatedSample.entities);
|
||||
expect(result.language).toEqualPayload(updatedSample.language);
|
||||
});
|
||||
|
||||
it('should throw exception when nlp sample id not found', async () => {
|
||||
@@ -276,6 +307,7 @@ describe('NlpSampleController', () => {
|
||||
text: 'updated',
|
||||
trained: true,
|
||||
type: NlpSampleState.test,
|
||||
language: 'fr',
|
||||
}),
|
||||
).rejects.toThrow(NotFoundException);
|
||||
});
|
||||
@@ -352,7 +384,7 @@ describe('NlpSampleController', () => {
|
||||
).id;
|
||||
const mockCsvData: string = [
|
||||
`text,intent,language`,
|
||||
`Was kostet dieser bmw,preis,de`,
|
||||
`How much does a BMW cost?,price,en`,
|
||||
].join('\n');
|
||||
jest.spyOn(fs, 'existsSync').mockReturnValueOnce(true);
|
||||
jest.spyOn(fs, 'readFileSync').mockReturnValueOnce(mockCsvData);
|
||||
@@ -361,17 +393,14 @@ describe('NlpSampleController', () => {
|
||||
const intentEntityResult = await nlpEntityService.findOne({
|
||||
name: 'intent',
|
||||
});
|
||||
const languageEntityResult = await nlpEntityService.findOne({
|
||||
name: 'language',
|
||||
});
|
||||
const preisValueResult = await nlpValueService.findOne({
|
||||
value: 'preis',
|
||||
});
|
||||
const deValueResult = await nlpValueService.findOne({
|
||||
value: 'de',
|
||||
const priceValueResult = await nlpValueService.findOne({
|
||||
value: 'price',
|
||||
});
|
||||
const textSampleResult = await nlpSampleService.findOne({
|
||||
text: 'Was kostet dieser bmw',
|
||||
text: 'How much does a BMW cost?',
|
||||
});
|
||||
const language = await languageService.findOne({
|
||||
code: 'en',
|
||||
});
|
||||
const intentEntity = {
|
||||
name: 'intent',
|
||||
@@ -379,40 +408,24 @@ describe('NlpSampleController', () => {
|
||||
doc: '',
|
||||
builtin: false,
|
||||
};
|
||||
const languageEntity = {
|
||||
name: 'language',
|
||||
lookups: ['trait'],
|
||||
builtin: false,
|
||||
doc: '',
|
||||
};
|
||||
const preisVlueEntity = await nlpEntityService.findOne({
|
||||
const priceValueEntity = await nlpEntityService.findOne({
|
||||
name: 'intent',
|
||||
});
|
||||
const preisValue = {
|
||||
value: 'preis',
|
||||
const priceValue = {
|
||||
value: 'price',
|
||||
expressions: [],
|
||||
builtin: false,
|
||||
entity: preisVlueEntity.id,
|
||||
};
|
||||
const deValueEntity = await nlpEntityService.findOne({
|
||||
name: 'language',
|
||||
});
|
||||
const deValue = {
|
||||
value: 'de',
|
||||
expressions: [],
|
||||
builtin: false,
|
||||
entity: deValueEntity.id,
|
||||
entity: priceValueEntity.id,
|
||||
};
|
||||
const textSample = {
|
||||
text: 'Was kostet dieser bmw',
|
||||
text: 'How much does a BMW cost?',
|
||||
trained: false,
|
||||
type: 'train',
|
||||
language: language.id,
|
||||
};
|
||||
|
||||
expect(languageEntityResult).toEqualPayload(languageEntity);
|
||||
expect(intentEntityResult).toEqualPayload(intentEntity);
|
||||
expect(preisValueResult).toEqualPayload(preisValue);
|
||||
expect(deValueResult).toEqualPayload(deValue);
|
||||
expect(priceValueResult).toEqualPayload(priceValue);
|
||||
expect(textSampleResult).toEqualPayload(textSample);
|
||||
expect(result).toEqual({ success: true });
|
||||
});
|
||||
|
||||
@@ -34,6 +34,7 @@ import Papa from 'papaparse';
|
||||
|
||||
import { AttachmentService } from '@/attachment/services/attachment.service';
|
||||
import { config } from '@/config';
|
||||
import { LanguageService } from '@/i18n/services/language.service';
|
||||
import { CsrfInterceptor } from '@/interceptors/csrf.interceptor';
|
||||
import { LoggerService } from '@/logger/logger.service';
|
||||
import { BaseController } from '@/utils/generics/base-controller';
|
||||
@@ -70,6 +71,7 @@ export class NlpSampleController extends BaseController<
|
||||
private readonly nlpEntityService: NlpEntityService,
|
||||
private readonly logger: LoggerService,
|
||||
private readonly nlpService: NlpService,
|
||||
private readonly languageService: LanguageService,
|
||||
) {
|
||||
super(nlpSampleService);
|
||||
}
|
||||
@@ -91,7 +93,7 @@ export class NlpSampleController extends BaseController<
|
||||
type ? { type } : {},
|
||||
);
|
||||
const entities = await this.nlpEntityService.findAllAndPopulate();
|
||||
const result = this.nlpSampleService.formatRasaNlu(samples, entities);
|
||||
const result = await this.nlpSampleService.formatRasaNlu(samples, entities);
|
||||
|
||||
// Sending the JSON data as a file
|
||||
const buffer = Buffer.from(JSON.stringify(result));
|
||||
@@ -120,11 +122,18 @@ export class NlpSampleController extends BaseController<
|
||||
@CsrfCheck(true)
|
||||
@Post()
|
||||
async create(
|
||||
@Body() { entities: nlpEntities, ...createNlpSampleDto }: NlpSampleDto,
|
||||
@Body()
|
||||
{
|
||||
entities: nlpEntities,
|
||||
language: languageCode,
|
||||
...createNlpSampleDto
|
||||
}: NlpSampleDto,
|
||||
): Promise<NlpSampleFull> {
|
||||
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,
|
||||
@@ -134,6 +143,7 @@ export class NlpSampleController extends BaseController<
|
||||
return {
|
||||
...nlpSample,
|
||||
entities,
|
||||
language,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -243,7 +253,11 @@ export class NlpSampleController extends BaseController<
|
||||
async findPage(
|
||||
@Query(PageQueryPipe) pageQuery: PageQueryDto<NlpSample>,
|
||||
@Query(PopulatePipe) populate: string[],
|
||||
@Query(new SearchFilterPipe<NlpSample>({ allowedFields: ['text', 'type'] }))
|
||||
@Query(
|
||||
new SearchFilterPipe<NlpSample>({
|
||||
allowedFields: ['text', 'type', 'language'],
|
||||
}),
|
||||
)
|
||||
filters: TFilterQuery<NlpSample>,
|
||||
) {
|
||||
return this.canPopulate(populate)
|
||||
@@ -263,12 +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<NlpSampleFull> {
|
||||
const { text, type, entities } = updateNlpSampleDto;
|
||||
const language = await this.languageService.getLanguageByCode(languageCode);
|
||||
const sample = await this.nlpSampleService.updateOne(id, {
|
||||
text,
|
||||
type,
|
||||
...sampleAttrs,
|
||||
language: language.id,
|
||||
trained: false,
|
||||
});
|
||||
|
||||
@@ -284,6 +298,7 @@ export class NlpSampleController extends BaseController<
|
||||
|
||||
return {
|
||||
...sample,
|
||||
language,
|
||||
entities: updatedSampleEntities,
|
||||
};
|
||||
}
|
||||
@@ -366,6 +381,8 @@ export class NlpSampleController extends BaseController<
|
||||
}
|
||||
// Remove data with no intent
|
||||
const filteredData = result.data.filter((d) => d.intent !== 'none');
|
||||
const languages = await this.languageService.getLanguages();
|
||||
const defaultLanguage = await this.languageService.getDefaultLanguage();
|
||||
// Reduce function to ensure executing promises one by one
|
||||
for (const d of filteredData) {
|
||||
try {
|
||||
@@ -375,15 +392,25 @@ export class NlpSampleController extends BaseController<
|
||||
});
|
||||
|
||||
// Skip if sample already exists
|
||||
|
||||
if (Array.isArray(existingSamples) && existingSamples.length > 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Fallback to default language if 'language' is missing or invalid
|
||||
if (!d.language || !(d.language in languages)) {
|
||||
if (d.language) {
|
||||
this.logger.warn(
|
||||
`Language "${d.language}" does not exist, falling back to default.`,
|
||||
);
|
||||
}
|
||||
d.language = defaultLanguage.code;
|
||||
}
|
||||
|
||||
// Create a new sample dto
|
||||
const sample: NlpSampleCreateDto = {
|
||||
text: d.text,
|
||||
trained: false,
|
||||
language: languages[d.language].id,
|
||||
};
|
||||
|
||||
// Create a new sample entity dto
|
||||
|
||||
@@ -16,27 +16,38 @@ import {
|
||||
IsString,
|
||||
} from 'class-validator';
|
||||
|
||||
import { IsObjectId } from '@/utils/validation-rules/is-object-id';
|
||||
|
||||
import { NlpSampleEntityValue, NlpSampleState } from '../schemas/types';
|
||||
|
||||
export class NlpSampleCreateDto {
|
||||
@ApiProperty({ description: 'nlp sample text', type: String })
|
||||
@ApiProperty({ description: 'NLP sample text', type: String })
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
text: string;
|
||||
|
||||
@ApiPropertyOptional({ description: 'nlp sample is trained', type: Boolean })
|
||||
@ApiPropertyOptional({
|
||||
description: 'If NLP sample is trained',
|
||||
type: Boolean,
|
||||
})
|
||||
@IsBoolean()
|
||||
@IsOptional()
|
||||
trained?: boolean;
|
||||
|
||||
@ApiPropertyOptional({
|
||||
description: 'nlp sample type',
|
||||
description: 'NLP sample type',
|
||||
enum: Object.values(NlpSampleState),
|
||||
})
|
||||
@IsString()
|
||||
@IsIn(Object.values(NlpSampleState))
|
||||
@IsOptional()
|
||||
type?: NlpSampleState;
|
||||
|
||||
@ApiProperty({ description: 'NLP sample language id', type: String })
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
@IsObjectId({ message: 'Language must be a valid ObjectId' })
|
||||
language: string;
|
||||
}
|
||||
|
||||
export class NlpSampleDto extends NlpSampleCreateDto {
|
||||
@@ -45,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) {}
|
||||
|
||||
@@ -11,6 +11,8 @@ import { EventEmitter2 } from '@nestjs/event-emitter';
|
||||
import { MongooseModule } from '@nestjs/mongoose';
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
|
||||
import { LanguageRepository } from '@/i18n/repositories/language.repository';
|
||||
import { Language, LanguageModel } from '@/i18n/schemas/language.schema';
|
||||
import { nlpSampleFixtures } from '@/utils/test/fixtures/nlpsample';
|
||||
import {
|
||||
installNlpSampleEntityFixtures,
|
||||
@@ -37,8 +39,10 @@ import { NlpValueModel } from '../schemas/nlp-value.schema';
|
||||
describe('NlpSampleEntityRepository', () => {
|
||||
let nlpSampleEntityRepository: NlpSampleEntityRepository;
|
||||
let nlpEntityRepository: NlpEntityRepository;
|
||||
let languageRepository: LanguageRepository;
|
||||
let nlpSampleEntities: NlpSampleEntity[];
|
||||
let nlpEntities: NlpEntity[];
|
||||
let languages: Language[];
|
||||
|
||||
beforeAll(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
@@ -49,12 +53,14 @@ describe('NlpSampleEntityRepository', () => {
|
||||
NlpEntityModel,
|
||||
NlpValueModel,
|
||||
NlpSampleModel,
|
||||
LanguageModel,
|
||||
]),
|
||||
],
|
||||
providers: [
|
||||
NlpSampleEntityRepository,
|
||||
NlpEntityRepository,
|
||||
NlpValueRepository,
|
||||
LanguageRepository,
|
||||
EventEmitter2,
|
||||
],
|
||||
}).compile();
|
||||
@@ -62,8 +68,10 @@ describe('NlpSampleEntityRepository', () => {
|
||||
NlpSampleEntityRepository,
|
||||
);
|
||||
nlpEntityRepository = module.get<NlpEntityRepository>(NlpEntityRepository);
|
||||
languageRepository = module.get<LanguageRepository>(LanguageRepository);
|
||||
nlpSampleEntities = await nlpSampleEntityRepository.findAll();
|
||||
nlpEntities = await nlpEntityRepository.findAll();
|
||||
languages = await languageRepository.findAll();
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
@@ -81,7 +89,10 @@ describe('NlpSampleEntityRepository', () => {
|
||||
...nlpSampleEntityFixtures[0],
|
||||
entity: nlpEntities[0],
|
||||
value: { ...nlpValueFixtures[0], entity: nlpEntities[0].id },
|
||||
sample: nlpSampleFixtures[0],
|
||||
sample: {
|
||||
...nlpSampleFixtures[0],
|
||||
language: languages[nlpSampleFixtures[0].language].id,
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -117,7 +128,10 @@ describe('NlpSampleEntityRepository', () => {
|
||||
...curr,
|
||||
entity: nlpEntities[curr.entity],
|
||||
value: nlpValueFixturesWithEntities[curr.value],
|
||||
sample: nlpSampleFixtures[curr.sample],
|
||||
sample: {
|
||||
...nlpSampleFixtures[curr.sample],
|
||||
language: languages[nlpSampleFixtures[curr.sample].language].id,
|
||||
},
|
||||
};
|
||||
acc.push(sampleEntityWithPopulate);
|
||||
return acc;
|
||||
|
||||
@@ -11,6 +11,8 @@ import { EventEmitter2 } from '@nestjs/event-emitter';
|
||||
import { MongooseModule } from '@nestjs/mongoose';
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
|
||||
import { LanguageRepository } from '@/i18n/repositories/language.repository';
|
||||
import { Language, LanguageModel } from '@/i18n/schemas/language.schema';
|
||||
import { nlpSampleFixtures } from '@/utils/test/fixtures/nlpsample';
|
||||
import { installNlpSampleEntityFixtures } from '@/utils/test/fixtures/nlpsampleentity';
|
||||
import { getPageQuery } from '@/utils/test/pagination';
|
||||
@@ -30,18 +32,25 @@ import { NlpSampleModel, NlpSample } from '../schemas/nlp-sample.schema';
|
||||
describe('NlpSampleRepository', () => {
|
||||
let nlpSampleRepository: NlpSampleRepository;
|
||||
let nlpSampleEntityRepository: NlpSampleEntityRepository;
|
||||
let languageRepository: LanguageRepository;
|
||||
let nlpSampleEntity: NlpSampleEntity;
|
||||
let noNlpSample: NlpSample;
|
||||
let languages: Language[];
|
||||
|
||||
beforeAll(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
imports: [
|
||||
rootMongooseTestModule(installNlpSampleEntityFixtures),
|
||||
MongooseModule.forFeature([NlpSampleModel, NlpSampleEntityModel]),
|
||||
MongooseModule.forFeature([
|
||||
NlpSampleModel,
|
||||
NlpSampleEntityModel,
|
||||
LanguageModel,
|
||||
]),
|
||||
],
|
||||
providers: [
|
||||
NlpSampleRepository,
|
||||
NlpSampleEntityRepository,
|
||||
LanguageRepository,
|
||||
EventEmitter2,
|
||||
],
|
||||
}).compile();
|
||||
@@ -49,10 +58,12 @@ describe('NlpSampleRepository', () => {
|
||||
nlpSampleEntityRepository = module.get<NlpSampleEntityRepository>(
|
||||
NlpSampleEntityRepository,
|
||||
);
|
||||
languageRepository = module.get<LanguageRepository>(LanguageRepository);
|
||||
noNlpSample = await nlpSampleRepository.findOne({ text: 'No' });
|
||||
nlpSampleEntity = await nlpSampleEntityRepository.findOne({
|
||||
sample: noNlpSample.id,
|
||||
});
|
||||
languages = await languageRepository.findAll();
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
@@ -69,6 +80,7 @@ describe('NlpSampleRepository', () => {
|
||||
expect(result).toEqualPayload({
|
||||
...nlpSampleFixtures[1],
|
||||
entities: [nlpSampleEntity],
|
||||
language: languages[nlpSampleFixtures[1].language],
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -92,6 +104,7 @@ describe('NlpSampleRepository', () => {
|
||||
entities: nlpSampleEntities.filter((currSampleEntity) => {
|
||||
return currSampleEntity.sample === currSample.id;
|
||||
}),
|
||||
language: languages.find((lang) => currSample.language === lang.id),
|
||||
};
|
||||
acc.push(sampleWithEntities);
|
||||
return acc;
|
||||
|
||||
@@ -8,9 +8,10 @@
|
||||
*/
|
||||
|
||||
import { ModelDefinition, Prop, Schema, SchemaFactory } from '@nestjs/mongoose';
|
||||
import { Exclude, Type } from 'class-transformer';
|
||||
import { THydratedDocument } from 'mongoose';
|
||||
import { Exclude, Transform, Type } from 'class-transformer';
|
||||
import { THydratedDocument, Schema as MongooseSchema } from 'mongoose';
|
||||
|
||||
import { Language } from '@/i18n/schemas/language.schema';
|
||||
import { BaseSchema } from '@/utils/generics/base-schema';
|
||||
import { LifecycleHookManager } from '@/utils/generics/lifecycle-hook-manager';
|
||||
import { TFilterPopulateFields } from '@/utils/types/filter.types';
|
||||
@@ -41,16 +42,32 @@ export class NlpSampleStub extends BaseSchema {
|
||||
default: NlpSampleState.train,
|
||||
})
|
||||
type?: keyof typeof NlpSampleState;
|
||||
|
||||
/**
|
||||
* The language of the sample.
|
||||
*/
|
||||
@Prop({
|
||||
type: MongooseSchema.Types.ObjectId,
|
||||
ref: 'Language',
|
||||
required: false,
|
||||
})
|
||||
language: unknown | null;
|
||||
}
|
||||
|
||||
@Schema({ timestamps: true })
|
||||
export class NlpSample extends NlpSampleStub {
|
||||
@Transform(({ obj }) => obj.language.toString())
|
||||
language: string | null;
|
||||
|
||||
@Exclude()
|
||||
entities?: never;
|
||||
}
|
||||
|
||||
@Schema({ timestamps: true })
|
||||
export class NlpSampleFull extends NlpSampleStub {
|
||||
@Type(() => Language)
|
||||
language: Language | null;
|
||||
|
||||
@Type(() => NlpSampleEntity)
|
||||
entities: NlpSampleEntity[];
|
||||
}
|
||||
@@ -75,4 +92,7 @@ export type NlpSamplePopulate = keyof TFilterPopulateFields<
|
||||
NlpSampleStub
|
||||
>;
|
||||
|
||||
export const NLP_SAMPLE_POPULATE: NlpSamplePopulate[] = ['entities'];
|
||||
export const NLP_SAMPLE_POPULATE: NlpSamplePopulate[] = [
|
||||
'language',
|
||||
'entities',
|
||||
];
|
||||
|
||||
@@ -10,12 +10,6 @@
|
||||
import { NlpEntityCreateDto } from '../dto/nlp-entity.dto';
|
||||
|
||||
export const nlpEntityModels: NlpEntityCreateDto[] = [
|
||||
{
|
||||
name: 'language',
|
||||
lookups: ['trait'],
|
||||
doc: `"language" refers to the language of the text sent by the end user`,
|
||||
builtin: true,
|
||||
},
|
||||
{
|
||||
name: 'intent',
|
||||
lookups: ['trait'],
|
||||
|
||||
@@ -7,16 +7,6 @@
|
||||
* 3. SaaS Restriction: This software, or any derivative of it, may not be used to offer a competing product or service (SaaS) without prior written consent from Hexastack. Offering the software as a service or using it in a commercial cloud environment without express permission is strictly prohibited.
|
||||
*/
|
||||
|
||||
import { config } from '@/config';
|
||||
|
||||
import { NlpValueCreateDto } from '../dto/nlp-value.dto';
|
||||
|
||||
export const nlpValueModels: NlpValueCreateDto[] = [
|
||||
...config.chatbot.lang.available.map((lang: string) => {
|
||||
return {
|
||||
entity: 'language',
|
||||
value: lang,
|
||||
builtin: true,
|
||||
};
|
||||
}),
|
||||
];
|
||||
export const nlpValueModels: NlpValueCreateDto[] = [];
|
||||
|
||||
@@ -11,6 +11,8 @@ import { EventEmitter2 } from '@nestjs/event-emitter';
|
||||
import { MongooseModule } from '@nestjs/mongoose';
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
|
||||
import { LanguageRepository } from '@/i18n/repositories/language.repository';
|
||||
import { Language, LanguageModel } from '@/i18n/schemas/language.schema';
|
||||
import { nlpSampleFixtures } from '@/utils/test/fixtures/nlpsample';
|
||||
import {
|
||||
installNlpSampleEntityFixtures,
|
||||
@@ -42,7 +44,9 @@ describe('NlpSampleEntityService', () => {
|
||||
let nlpSampleEntityRepository: NlpSampleEntityRepository;
|
||||
let nlpSampleEntities: NlpSampleEntity[];
|
||||
let nlpEntityRepository: NlpEntityRepository;
|
||||
let languageRepository: LanguageRepository;
|
||||
let nlpEntities: NlpEntity[];
|
||||
let languages: Language[];
|
||||
let nlpEntityService: NlpEntityService;
|
||||
let nlpValueService: NlpValueService;
|
||||
|
||||
@@ -55,12 +59,14 @@ describe('NlpSampleEntityService', () => {
|
||||
NlpEntityModel,
|
||||
NlpSampleModel,
|
||||
NlpValueModel,
|
||||
LanguageModel,
|
||||
]),
|
||||
],
|
||||
providers: [
|
||||
NlpSampleEntityRepository,
|
||||
NlpEntityRepository,
|
||||
NlpValueRepository,
|
||||
LanguageRepository,
|
||||
NlpSampleEntityService,
|
||||
NlpEntityService,
|
||||
NlpValueService,
|
||||
@@ -74,6 +80,7 @@ describe('NlpSampleEntityService', () => {
|
||||
NlpSampleEntityRepository,
|
||||
);
|
||||
nlpEntityRepository = module.get<NlpEntityRepository>(NlpEntityRepository);
|
||||
languageRepository = module.get<LanguageRepository>(LanguageRepository);
|
||||
nlpSampleEntityService = module.get<NlpSampleEntityService>(
|
||||
NlpSampleEntityService,
|
||||
);
|
||||
@@ -81,6 +88,7 @@ describe('NlpSampleEntityService', () => {
|
||||
nlpValueService = module.get<NlpValueService>(NlpValueService);
|
||||
nlpSampleEntities = await nlpSampleEntityRepository.findAll();
|
||||
nlpEntities = await nlpEntityRepository.findAll();
|
||||
languages = await languageRepository.findAll();
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
@@ -98,7 +106,10 @@ describe('NlpSampleEntityService', () => {
|
||||
...nlpSampleEntityFixtures[0],
|
||||
entity: nlpEntities[0],
|
||||
value: { ...nlpValueFixtures[0], entity: nlpEntities[0].id },
|
||||
sample: nlpSampleFixtures[0],
|
||||
sample: {
|
||||
...nlpSampleFixtures[0],
|
||||
language: languages[nlpSampleFixtures[0].language].id,
|
||||
},
|
||||
};
|
||||
expect(result).toEqualPayload(sampleEntityWithPopulate);
|
||||
});
|
||||
@@ -135,7 +146,10 @@ describe('NlpSampleEntityService', () => {
|
||||
...curr,
|
||||
entity: nlpEntities[curr.entity],
|
||||
value: nlpValueFixturesWithEntities[curr.value],
|
||||
sample: nlpSampleFixtures[curr.sample],
|
||||
sample: {
|
||||
...nlpSampleFixtures[curr.sample],
|
||||
language: languages[nlpSampleFixtures[curr.sample].language].id,
|
||||
},
|
||||
};
|
||||
acc.push(sampleEntityWithPopulate);
|
||||
return acc;
|
||||
|
||||
@@ -7,10 +7,14 @@
|
||||
* 3. SaaS Restriction: This software, or any derivative of it, may not be used to offer a competing product or service (SaaS) without prior written consent from Hexastack. Offering the software as a service or using it in a commercial cloud environment without express permission is strictly prohibited.
|
||||
*/
|
||||
|
||||
import { CACHE_MANAGER } from '@nestjs/cache-manager';
|
||||
import { EventEmitter2 } from '@nestjs/event-emitter';
|
||||
import { MongooseModule } from '@nestjs/mongoose';
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
|
||||
import { LanguageRepository } from '@/i18n/repositories/language.repository';
|
||||
import { Language, LanguageModel } from '@/i18n/schemas/language.schema';
|
||||
import { LanguageService } from '@/i18n/services/language.service';
|
||||
import { nlpSampleFixtures } from '@/utils/test/fixtures/nlpsample';
|
||||
import { installNlpSampleEntityFixtures } from '@/utils/test/fixtures/nlpsampleentity';
|
||||
import { getPageQuery } from '@/utils/test/pagination';
|
||||
@@ -39,8 +43,10 @@ describe('NlpSampleService', () => {
|
||||
let nlpSampleService: NlpSampleService;
|
||||
let nlpSampleEntityRepository: NlpSampleEntityRepository;
|
||||
let nlpSampleRepository: NlpSampleRepository;
|
||||
let languageRepository: LanguageRepository;
|
||||
let noNlpSample: NlpSample;
|
||||
let nlpSampleEntity: NlpSampleEntity;
|
||||
let languages: Language[];
|
||||
|
||||
beforeAll(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
@@ -51,6 +57,7 @@ describe('NlpSampleService', () => {
|
||||
NlpSampleEntityModel,
|
||||
NlpValueModel,
|
||||
NlpEntityModel,
|
||||
LanguageModel,
|
||||
]),
|
||||
],
|
||||
providers: [
|
||||
@@ -58,11 +65,21 @@ describe('NlpSampleService', () => {
|
||||
NlpSampleEntityRepository,
|
||||
NlpEntityRepository,
|
||||
NlpValueRepository,
|
||||
LanguageRepository,
|
||||
NlpSampleService,
|
||||
NlpSampleEntityService,
|
||||
NlpEntityService,
|
||||
NlpValueService,
|
||||
LanguageService,
|
||||
EventEmitter2,
|
||||
{
|
||||
provide: CACHE_MANAGER,
|
||||
useValue: {
|
||||
del: jest.fn(),
|
||||
get: jest.fn(),
|
||||
set: jest.fn(),
|
||||
},
|
||||
},
|
||||
],
|
||||
}).compile();
|
||||
nlpSampleService = module.get<NlpSampleService>(NlpSampleService);
|
||||
@@ -73,10 +90,12 @@ describe('NlpSampleService', () => {
|
||||
nlpSampleEntityRepository = module.get<NlpSampleEntityRepository>(
|
||||
NlpSampleEntityRepository,
|
||||
);
|
||||
languageRepository = module.get<LanguageRepository>(LanguageRepository);
|
||||
noNlpSample = await nlpSampleService.findOne({ text: 'No' });
|
||||
nlpSampleEntity = await nlpSampleEntityRepository.findOne({
|
||||
sample: noNlpSample.id,
|
||||
});
|
||||
languages = await languageRepository.findAll();
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
@@ -91,6 +110,7 @@ describe('NlpSampleService', () => {
|
||||
const sampleWithEntities = {
|
||||
...nlpSampleFixtures[1],
|
||||
entities: [nlpSampleEntity],
|
||||
language: languages[nlpSampleFixtures[1].language],
|
||||
};
|
||||
expect(result).toEqualPayload(sampleWithEntities);
|
||||
});
|
||||
@@ -110,6 +130,7 @@ describe('NlpSampleService', () => {
|
||||
entities: nlpSampleEntities.filter((currSampleEntity) => {
|
||||
return currSampleEntity.sample === currSample.id;
|
||||
}),
|
||||
language: languages.find((lang) => lang.id === currSample.language),
|
||||
};
|
||||
acc.push(sampleWithEntities);
|
||||
return acc;
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
*/
|
||||
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { OnEvent } from '@nestjs/event-emitter';
|
||||
|
||||
import {
|
||||
CommonExample,
|
||||
@@ -16,6 +17,8 @@ import {
|
||||
ExampleEntity,
|
||||
LookupTable,
|
||||
} from '@/extensions/helpers/nlp/default/types';
|
||||
import { Language } from '@/i18n/schemas/language.schema';
|
||||
import { LanguageService } from '@/i18n/services/language.service';
|
||||
import { BaseService } from '@/utils/generics/base-service';
|
||||
|
||||
import { NlpSampleRepository } from '../repositories/nlp-sample.repository';
|
||||
@@ -33,7 +36,10 @@ export class NlpSampleService extends BaseService<
|
||||
NlpSamplePopulate,
|
||||
NlpSampleFull
|
||||
> {
|
||||
constructor(readonly repository: NlpSampleRepository) {
|
||||
constructor(
|
||||
readonly repository: NlpSampleRepository,
|
||||
private readonly languageService: LanguageService,
|
||||
) {
|
||||
super(repository);
|
||||
}
|
||||
|
||||
@@ -56,10 +62,10 @@ export class NlpSampleService extends BaseService<
|
||||
*
|
||||
* @returns The formatted Rasa NLU training dataset.
|
||||
*/
|
||||
formatRasaNlu(
|
||||
async formatRasaNlu(
|
||||
samples: NlpSampleFull[],
|
||||
entities: NlpEntityFull[],
|
||||
): DatasetType {
|
||||
): Promise<DatasetType> {
|
||||
const entityMap = NlpEntity.getEntityMap(entities);
|
||||
const valueMap = NlpValue.getValueMap(
|
||||
NlpValue.getValuesFromEntities(entities),
|
||||
@@ -88,21 +94,34 @@ export class NlpSampleService extends BaseService<
|
||||
});
|
||||
}
|
||||
return res;
|
||||
})
|
||||
// TODO : place language at the same level as the intent
|
||||
.concat({
|
||||
entity: 'language',
|
||||
value: s.language.code,
|
||||
});
|
||||
|
||||
return {
|
||||
text: s.text,
|
||||
intent: valueMap[intent.value].value,
|
||||
entities: sampleEntities,
|
||||
};
|
||||
});
|
||||
const lookup_tables: LookupTable[] = entities.map((e) => {
|
||||
return {
|
||||
name: e.name,
|
||||
elements: e.values.map((v) => {
|
||||
return v.value;
|
||||
}),
|
||||
};
|
||||
});
|
||||
|
||||
const languages = await this.languageService.getLanguages();
|
||||
const lookup_tables: LookupTable[] = entities
|
||||
.map((e) => {
|
||||
return {
|
||||
name: e.name,
|
||||
elements: e.values.map((v) => {
|
||||
return v.value;
|
||||
}),
|
||||
};
|
||||
})
|
||||
.concat({
|
||||
name: 'language',
|
||||
elements: Object.keys(languages),
|
||||
});
|
||||
const entity_synonyms = entities
|
||||
.reduce((acc, e) => {
|
||||
const synonyms = e.values.map((v) => {
|
||||
@@ -123,4 +142,21 @@ export class NlpSampleService extends BaseService<
|
||||
entity_synonyms,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* When a language gets deleted, we need to set related samples to null
|
||||
*
|
||||
* @param language The language that has been deleted.
|
||||
*/
|
||||
@OnEvent('hook:language:delete')
|
||||
async handleLanguageDelete(language: Language) {
|
||||
await this.updateMany(
|
||||
{
|
||||
language: language.id,
|
||||
},
|
||||
{
|
||||
language: null,
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,8 +13,10 @@ import { CategorySeeder } from './chat/seeds/category.seed';
|
||||
import { categoryModels } from './chat/seeds/category.seed-model';
|
||||
import { ContextVarSeeder } from './chat/seeds/context-var.seed';
|
||||
import { contextVarModels } from './chat/seeds/context-var.seed-model';
|
||||
import { TranslationSeeder } from './chat/seeds/translation.seed';
|
||||
import { translationModels } from './chat/seeds/translation.seed-model';
|
||||
import { LanguageSeeder } from './i18n/seeds/language.seed';
|
||||
import { languageModels } from './i18n/seeds/language.seed-model';
|
||||
import { TranslationSeeder } from './i18n/seeds/translation.seed';
|
||||
import { translationModels } from './i18n/seeds/translation.seed-model';
|
||||
import { LoggerService } from './logger/logger.service';
|
||||
import { NlpEntitySeeder } from './nlp/seeds/nlp-entity.seed';
|
||||
import { nlpEntityModels } from './nlp/seeds/nlp-entity.seed-model';
|
||||
@@ -40,6 +42,7 @@ export async function seedDatabase(app: INestApplicationContext) {
|
||||
const settingSeeder = app.get(SettingSeeder);
|
||||
const permissionSeeder = app.get(PermissionSeeder);
|
||||
const userSeeder = app.get(UserSeeder);
|
||||
const languageSeeder = app.get(LanguageSeeder);
|
||||
const translationSeeder = app.get(TranslationSeeder);
|
||||
const nlpEntitySeeder = app.get(NlpEntitySeeder);
|
||||
const nlpValueSeeder = app.get(NlpValueSeeder);
|
||||
@@ -127,6 +130,14 @@ export async function seedDatabase(app: INestApplicationContext) {
|
||||
throw e;
|
||||
}
|
||||
|
||||
// Seed languages
|
||||
try {
|
||||
await languageSeeder.seed(languageModels);
|
||||
} catch (e) {
|
||||
logger.error('Unable to seed the database with languages!');
|
||||
throw e;
|
||||
}
|
||||
|
||||
// Seed translations
|
||||
try {
|
||||
await translationSeeder.seed(translationModels);
|
||||
|
||||
@@ -12,7 +12,7 @@ import { EventEmitter2 } from '@nestjs/event-emitter';
|
||||
import { MongooseModule } from '@nestjs/mongoose';
|
||||
import { Test } from '@nestjs/testing';
|
||||
|
||||
import { ExtendedI18nService } from '@/extended-i18n.service';
|
||||
import { I18nService } from '@/i18n/services/i18n.service';
|
||||
import { LoggerService } from '@/logger/logger.service';
|
||||
import {
|
||||
installSettingFixtures,
|
||||
@@ -47,7 +47,7 @@ describe('SettingController', () => {
|
||||
LoggerService,
|
||||
EventEmitter2,
|
||||
{
|
||||
provide: ExtendedI18nService,
|
||||
provide: I18nService,
|
||||
useValue: {
|
||||
t: jest.fn().mockImplementation((t) => t),
|
||||
},
|
||||
|
||||
@@ -12,8 +12,7 @@ import { EventEmitter2 } from '@nestjs/event-emitter';
|
||||
import { InjectModel } from '@nestjs/mongoose';
|
||||
import { Document, Model, Query, Types } from 'mongoose';
|
||||
|
||||
import { config } from '@/config';
|
||||
import { ExtendedI18nService } from '@/extended-i18n.service';
|
||||
import { I18nService } from '@/i18n/services/i18n.service';
|
||||
import { BaseRepository } from '@/utils/generics/base-repository';
|
||||
|
||||
import { Setting } from '../schemas/setting.schema';
|
||||
@@ -23,7 +22,7 @@ export class SettingRepository extends BaseRepository<Setting> {
|
||||
constructor(
|
||||
@InjectModel(Setting.name) readonly model: Model<Setting>,
|
||||
private readonly eventEmitter: EventEmitter2,
|
||||
private readonly i18n: ExtendedI18nService,
|
||||
private readonly i18n: I18nService,
|
||||
) {
|
||||
super(model, Setting);
|
||||
}
|
||||
@@ -65,8 +64,7 @@ export class SettingRepository extends BaseRepository<Setting> {
|
||||
* Emits an event after a `Setting` has been updated.
|
||||
*
|
||||
* This method is used to synchronize global settings by emitting an event
|
||||
* based on the `group` and `label` of the `Setting`. It also updates the i18n
|
||||
* default language setting when the `default_lang` label is updated.
|
||||
* based on the `group` and `label` of the `Setting`.
|
||||
*
|
||||
* @param _query The Mongoose query object used to find and update the document.
|
||||
* @param setting The updated `Setting` object.
|
||||
@@ -86,33 +84,5 @@ export class SettingRepository extends BaseRepository<Setting> {
|
||||
'hook:settings:' + setting.group + ':' + setting.label,
|
||||
setting,
|
||||
);
|
||||
|
||||
if (setting.label === 'default_lang') {
|
||||
// @todo : check if this actually updates the default lang
|
||||
this.i18n.resolveLanguage(setting.value as string);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets default values before creating a `Setting` document.
|
||||
*
|
||||
* If the setting is part of the `nlp_settings` group, it sets specific values
|
||||
* for `languages` and `default_lang` labels, using configuration values from the
|
||||
* chatbot settings.
|
||||
*
|
||||
* @param setting The `Setting` document to be created.
|
||||
*/
|
||||
async preCreate(
|
||||
setting: Document<unknown, unknown, Setting> &
|
||||
Setting & { _id: Types.ObjectId },
|
||||
) {
|
||||
if (setting.group === 'nlp_settings') {
|
||||
if (setting.label === 'languages') {
|
||||
setting.value = config.chatbot.lang.available;
|
||||
} else if (setting.label === 'default_lang') {
|
||||
setting.value = config.chatbot.lang.default;
|
||||
setting.options = config.chatbot.lang.available;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -98,8 +98,6 @@ export type SettingDict = { [group: string]: Setting[] };
|
||||
|
||||
export type Settings = {
|
||||
nlp_settings: {
|
||||
default_lang: string;
|
||||
languages: string[];
|
||||
threshold: string;
|
||||
provider: string;
|
||||
endpoint: string;
|
||||
|
||||
@@ -7,8 +7,6 @@
|
||||
* 3. SaaS Restriction: This software, or any derivative of it, may not be used to offer a competing product or service (SaaS) without prior written consent from Hexastack. Offering the software as a service or using it in a commercial cloud environment without express permission is strictly prohibited.
|
||||
*/
|
||||
|
||||
import { config } from '@/config';
|
||||
|
||||
import { SettingCreateDto } from '../dto/setting.dto';
|
||||
import { SettingType } from '../schemas/types';
|
||||
|
||||
@@ -67,26 +65,6 @@ export const settingModels: SettingCreateDto[] = [
|
||||
type: SettingType.text,
|
||||
weight: 3,
|
||||
},
|
||||
{
|
||||
group: 'nlp_settings',
|
||||
label: 'languages',
|
||||
value: [],
|
||||
options: [],
|
||||
type: SettingType.select,
|
||||
config: {
|
||||
multiple: true,
|
||||
allowCreate: true,
|
||||
},
|
||||
weight: 4,
|
||||
},
|
||||
{
|
||||
group: 'nlp_settings',
|
||||
label: 'default_lang',
|
||||
value: config.chatbot.lang.default,
|
||||
options: [], // NOTE : will be set onBeforeCreate from config
|
||||
type: SettingType.select,
|
||||
weight: 5,
|
||||
},
|
||||
{
|
||||
group: 'nlp_settings',
|
||||
label: 'threshold',
|
||||
@@ -97,7 +75,7 @@ export const settingModels: SettingCreateDto[] = [
|
||||
max: 1,
|
||||
step: 0.01,
|
||||
},
|
||||
weight: 6,
|
||||
weight: 4,
|
||||
},
|
||||
{
|
||||
group: 'contact',
|
||||
|
||||
@@ -12,7 +12,7 @@ import { EventEmitter2 } from '@nestjs/event-emitter';
|
||||
import { MongooseModule } from '@nestjs/mongoose';
|
||||
import { Test } from '@nestjs/testing';
|
||||
|
||||
import { ExtendedI18nService } from '@/extended-i18n.service';
|
||||
import { I18nService } from '@/i18n/services/i18n.service';
|
||||
import { LoggerService } from '@/logger/logger.service';
|
||||
import {
|
||||
installSettingFixtures,
|
||||
@@ -51,7 +51,7 @@ describe('SettingService', () => {
|
||||
SettingSeeder,
|
||||
EventEmitter2,
|
||||
{
|
||||
provide: ExtendedI18nService,
|
||||
provide: I18nService,
|
||||
useValue: {
|
||||
t: jest.fn().mockImplementation((t) => t),
|
||||
},
|
||||
|
||||
@@ -23,7 +23,10 @@ import { SentMessageInfo } from 'nodemailer';
|
||||
import { AttachmentRepository } from '@/attachment/repositories/attachment.repository';
|
||||
import { AttachmentModel } from '@/attachment/schemas/attachment.schema';
|
||||
import { AttachmentService } from '@/attachment/services/attachment.service';
|
||||
import { ExtendedI18nService } from '@/extended-i18n.service';
|
||||
import { LanguageRepository } from '@/i18n/repositories/language.repository';
|
||||
import { LanguageModel } from '@/i18n/schemas/language.schema';
|
||||
import { I18nService } from '@/i18n/services/i18n.service';
|
||||
import { LanguageService } from '@/i18n/services/language.service';
|
||||
import { LoggerService } from '@/logger/logger.service';
|
||||
import { installUserFixtures } from '@/utils/test/fixtures/user';
|
||||
import {
|
||||
@@ -69,6 +72,7 @@ describe('AuthController', () => {
|
||||
PermissionModel,
|
||||
InvitationModel,
|
||||
AttachmentModel,
|
||||
LanguageModel,
|
||||
]),
|
||||
],
|
||||
providers: [
|
||||
@@ -86,6 +90,8 @@ describe('AuthController', () => {
|
||||
PermissionRepository,
|
||||
InvitationRepository,
|
||||
InvitationService,
|
||||
LanguageRepository,
|
||||
LanguageService,
|
||||
JwtService,
|
||||
{
|
||||
provide: MailerService,
|
||||
@@ -106,7 +112,7 @@ describe('AuthController', () => {
|
||||
EventEmitter2,
|
||||
ValidateAccountService,
|
||||
{
|
||||
provide: ExtendedI18nService,
|
||||
provide: I18nService,
|
||||
useValue: {
|
||||
t: jest.fn().mockImplementation((t) => t),
|
||||
},
|
||||
|
||||
@@ -20,7 +20,10 @@ import { SentMessageInfo } from 'nodemailer';
|
||||
import { AttachmentRepository } from '@/attachment/repositories/attachment.repository';
|
||||
import { AttachmentModel } from '@/attachment/schemas/attachment.schema';
|
||||
import { AttachmentService } from '@/attachment/services/attachment.service';
|
||||
import { ExtendedI18nService } from '@/extended-i18n.service';
|
||||
import { LanguageRepository } from '@/i18n/repositories/language.repository';
|
||||
import { LanguageModel } from '@/i18n/schemas/language.schema';
|
||||
import { I18nService } from '@/i18n/services/i18n.service';
|
||||
import { LanguageService } from '@/i18n/services/language.service';
|
||||
import { LoggerService } from '@/logger/logger.service';
|
||||
import { IGNORED_TEST_FIELDS } from '@/utils/test/constants';
|
||||
import { installPermissionFixtures } from '@/utils/test/fixtures/permission';
|
||||
@@ -75,6 +78,7 @@ describe('UserController', () => {
|
||||
PermissionModel,
|
||||
InvitationModel,
|
||||
AttachmentModel,
|
||||
LanguageModel,
|
||||
]),
|
||||
JwtModule,
|
||||
],
|
||||
@@ -108,9 +112,11 @@ describe('UserController', () => {
|
||||
},
|
||||
AttachmentService,
|
||||
AttachmentRepository,
|
||||
LanguageService,
|
||||
LanguageRepository,
|
||||
ValidateAccountService,
|
||||
{
|
||||
provide: ExtendedI18nService,
|
||||
provide: I18nService,
|
||||
useValue: {
|
||||
t: jest.fn().mockImplementation((t) => t),
|
||||
},
|
||||
|
||||
@@ -35,6 +35,7 @@ describe('PermissionRepository', () => {
|
||||
let permissionRepository: PermissionRepository;
|
||||
let permissionModel: Model<Permission>;
|
||||
let permission: Permission;
|
||||
let permissionToDelete: Permission;
|
||||
|
||||
beforeAll(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
@@ -59,6 +60,9 @@ describe('PermissionRepository', () => {
|
||||
permission = await permissionRepository.findOne({
|
||||
action: Action.CREATE,
|
||||
});
|
||||
permissionToDelete = await permissionRepository.findOne({
|
||||
action: Action.UPDATE,
|
||||
});
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
@@ -112,4 +116,36 @@ describe('PermissionRepository', () => {
|
||||
expect(result).toEqualPayload(permissionsWithRolesAndModels);
|
||||
});
|
||||
});
|
||||
|
||||
describe('deleteOne', () => {
|
||||
it('should delete a permission by id', async () => {
|
||||
jest.spyOn(permissionModel, 'deleteOne');
|
||||
const result = await permissionRepository.deleteOne(
|
||||
permissionToDelete.id,
|
||||
);
|
||||
|
||||
expect(permissionModel.deleteOne).toHaveBeenCalledWith({
|
||||
_id: permissionToDelete.id,
|
||||
});
|
||||
|
||||
expect(result).toEqual({
|
||||
acknowledged: true,
|
||||
deletedCount: 1,
|
||||
});
|
||||
|
||||
const permissions = await permissionRepository.find({
|
||||
role: permissionToDelete.id,
|
||||
});
|
||||
expect(permissions.length).toEqual(0);
|
||||
});
|
||||
|
||||
it('should fail to delete a permission that does not exist', async () => {
|
||||
expect(
|
||||
await permissionRepository.deleteOne(permissionToDelete.id),
|
||||
).toEqual({
|
||||
acknowledged: true,
|
||||
deletedCount: 0,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -24,8 +24,8 @@ import { PermissionRepository } from '../repositories/permission.repository';
|
||||
import { RoleRepository } from '../repositories/role.repository';
|
||||
import { UserRepository } from '../repositories/user.repository';
|
||||
import { PermissionModel } from '../schemas/permission.schema';
|
||||
import { RoleModel, Role } from '../schemas/role.schema';
|
||||
import { UserModel, User } from '../schemas/user.schema';
|
||||
import { Role, RoleModel } from '../schemas/role.schema';
|
||||
import { User, UserModel } from '../schemas/user.schema';
|
||||
|
||||
describe('RoleRepository', () => {
|
||||
let roleRepository: RoleRepository;
|
||||
@@ -34,6 +34,7 @@ describe('RoleRepository', () => {
|
||||
let roleModel: Model<Role>;
|
||||
let role: Role;
|
||||
let users: User[];
|
||||
let roleToDelete: Role;
|
||||
|
||||
beforeAll(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
@@ -57,6 +58,9 @@ describe('RoleRepository', () => {
|
||||
users = (await userRepository.findAll()).filter((user) =>
|
||||
user.roles.includes(role.id),
|
||||
);
|
||||
roleToDelete = await roleRepository.findOne({
|
||||
name: 'manager',
|
||||
});
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
@@ -106,4 +110,31 @@ describe('RoleRepository', () => {
|
||||
expect(result).toEqualPayload(rolesWithPermissionsAndUsers);
|
||||
});
|
||||
});
|
||||
|
||||
describe('deleteOne', () => {
|
||||
it('should delete a role by id', async () => {
|
||||
jest.spyOn(roleModel, 'deleteOne');
|
||||
const result = await roleRepository.deleteOne(roleToDelete.id);
|
||||
|
||||
expect(roleModel.deleteOne).toHaveBeenCalledWith({
|
||||
_id: roleToDelete.id,
|
||||
});
|
||||
expect(result).toEqual({
|
||||
acknowledged: true,
|
||||
deletedCount: 1,
|
||||
});
|
||||
|
||||
const permissions = await permissionRepository.find({
|
||||
role: roleToDelete.id,
|
||||
});
|
||||
expect(permissions.length).toEqual(0);
|
||||
});
|
||||
|
||||
it('should fail to delete a role that does not exist', async () => {
|
||||
expect(await roleRepository.deleteOne(roleToDelete.id)).toEqual({
|
||||
acknowledged: true,
|
||||
deletedCount: 0,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -96,7 +96,7 @@ export class RoleRepository extends BaseRepository<
|
||||
*
|
||||
* @returns The result of the delete operation.
|
||||
*/
|
||||
async deleteOneQuery(id: string) {
|
||||
async deleteOne(id: string) {
|
||||
const result = await this.model.deleteOne({ _id: id }).exec();
|
||||
if (result.deletedCount > 0) {
|
||||
await this.permissionModel.deleteMany({ role: id });
|
||||
|
||||
@@ -100,6 +100,11 @@ export const modelModels: ModelCreateDto[] = [
|
||||
identity: 'subscriber',
|
||||
attributes: {},
|
||||
},
|
||||
{
|
||||
name: 'Language',
|
||||
identity: 'language',
|
||||
attributes: {},
|
||||
},
|
||||
{
|
||||
name: 'Translation',
|
||||
identity: 'translation',
|
||||
|
||||
@@ -16,7 +16,10 @@ import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { ISendMailOptions, MailerService } from '@nestjs-modules/mailer';
|
||||
import { SentMessageInfo } from 'nodemailer';
|
||||
|
||||
import { ExtendedI18nService } from '@/extended-i18n.service';
|
||||
import { LanguageRepository } from '@/i18n/repositories/language.repository';
|
||||
import { LanguageModel } from '@/i18n/schemas/language.schema';
|
||||
import { I18nService } from '@/i18n/services/i18n.service';
|
||||
import { LanguageService } from '@/i18n/services/language.service';
|
||||
import { LoggerService } from '@/logger/logger.service';
|
||||
import { IGNORED_TEST_FIELDS } from '@/utils/test/constants';
|
||||
import {
|
||||
@@ -55,6 +58,7 @@ describe('InvitationService', () => {
|
||||
RoleModel,
|
||||
PermissionModel,
|
||||
InvitationModel,
|
||||
LanguageModel,
|
||||
]),
|
||||
JwtModule,
|
||||
],
|
||||
@@ -66,10 +70,12 @@ describe('InvitationService', () => {
|
||||
PermissionRepository,
|
||||
InvitationRepository,
|
||||
InvitationService,
|
||||
LanguageRepository,
|
||||
LanguageService,
|
||||
JwtService,
|
||||
Logger,
|
||||
{
|
||||
provide: ExtendedI18nService,
|
||||
provide: I18nService,
|
||||
useValue: {
|
||||
t: jest.fn().mockImplementation((t) => t),
|
||||
},
|
||||
|
||||
@@ -17,7 +17,8 @@ import { JwtService, JwtSignOptions } from '@nestjs/jwt';
|
||||
import { MailerService } from '@nestjs-modules/mailer';
|
||||
|
||||
import { config } from '@/config';
|
||||
import { ExtendedI18nService } from '@/extended-i18n.service';
|
||||
import { I18nService } from '@/i18n/services/i18n.service';
|
||||
import { LanguageService } from '@/i18n/services/language.service';
|
||||
import { LoggerService } from '@/logger/logger.service';
|
||||
import { BaseService } from '@/utils/generics/base-service';
|
||||
|
||||
@@ -41,7 +42,8 @@ export class InvitationService extends BaseService<
|
||||
@Inject(JwtService) private readonly jwtService: JwtService,
|
||||
@Optional() private readonly mailerService: MailerService | undefined,
|
||||
private logger: LoggerService,
|
||||
protected readonly i18n: ExtendedI18nService,
|
||||
protected readonly i18n: I18nService,
|
||||
public readonly languageService: LanguageService,
|
||||
) {
|
||||
super(repository);
|
||||
}
|
||||
@@ -63,6 +65,7 @@ export class InvitationService extends BaseService<
|
||||
const jwt = await this.sign(dto);
|
||||
if (this.mailerService) {
|
||||
try {
|
||||
const defaultLanguage = await this.languageService.getDefaultLanguage();
|
||||
await this.mailerService.sendMail({
|
||||
to: dto.email,
|
||||
template: 'invitation.mjml',
|
||||
@@ -70,7 +73,7 @@ export class InvitationService extends BaseService<
|
||||
token: jwt,
|
||||
// TODO: Which language should we use?
|
||||
t: (key: string) =>
|
||||
this.i18n.t(key, { lang: config.chatbot.lang.default }),
|
||||
this.i18n.t(key, { lang: defaultLanguage.code }),
|
||||
},
|
||||
subject: this.i18n.t('invitation_subject'),
|
||||
});
|
||||
|
||||
@@ -21,7 +21,10 @@ import { SentMessageInfo } from 'nodemailer';
|
||||
import { AttachmentRepository } from '@/attachment/repositories/attachment.repository';
|
||||
import { AttachmentModel } from '@/attachment/schemas/attachment.schema';
|
||||
import { AttachmentService } from '@/attachment/services/attachment.service';
|
||||
import { ExtendedI18nService } from '@/extended-i18n.service';
|
||||
import { LanguageRepository } from '@/i18n/repositories/language.repository';
|
||||
import { LanguageModel } from '@/i18n/schemas/language.schema';
|
||||
import { I18nService } from '@/i18n/services/i18n.service';
|
||||
import { LanguageService } from '@/i18n/services/language.service';
|
||||
import { LoggerService } from '@/logger/logger.service';
|
||||
import { installUserFixtures, users } from '@/utils/test/fixtures/user';
|
||||
import {
|
||||
@@ -52,6 +55,7 @@ describe('PasswordResetService', () => {
|
||||
RoleModel,
|
||||
PermissionModel,
|
||||
AttachmentModel,
|
||||
LanguageModel,
|
||||
]),
|
||||
JwtModule,
|
||||
],
|
||||
@@ -62,6 +66,8 @@ describe('PasswordResetService', () => {
|
||||
AttachmentService,
|
||||
AttachmentRepository,
|
||||
RoleRepository,
|
||||
LanguageService,
|
||||
LanguageRepository,
|
||||
LoggerService,
|
||||
PasswordResetService,
|
||||
JwtService,
|
||||
@@ -75,7 +81,7 @@ describe('PasswordResetService', () => {
|
||||
},
|
||||
},
|
||||
{
|
||||
provide: ExtendedI18nService,
|
||||
provide: I18nService,
|
||||
useValue: {
|
||||
t: jest.fn().mockImplementation((t) => t),
|
||||
},
|
||||
|
||||
@@ -21,7 +21,8 @@ import { MailerService } from '@nestjs-modules/mailer';
|
||||
import { compareSync } from 'bcryptjs';
|
||||
|
||||
import { config } from '@/config';
|
||||
import { ExtendedI18nService } from '@/extended-i18n.service';
|
||||
import { I18nService } from '@/i18n/services/i18n.service';
|
||||
import { LanguageService } from '@/i18n/services/language.service';
|
||||
import { LoggerService } from '@/logger/logger.service';
|
||||
|
||||
import { UserService } from './user.service';
|
||||
@@ -34,7 +35,8 @@ export class PasswordResetService {
|
||||
@Optional() private readonly mailerService: MailerService | undefined,
|
||||
private logger: LoggerService,
|
||||
private readonly userService: UserService,
|
||||
public readonly i18n: ExtendedI18nService,
|
||||
public readonly i18n: I18nService,
|
||||
public readonly languageService: LanguageService,
|
||||
) {}
|
||||
|
||||
public readonly jwtSignOptions: JwtSignOptions = {
|
||||
@@ -59,6 +61,7 @@ export class PasswordResetService {
|
||||
|
||||
if (this.mailerService) {
|
||||
try {
|
||||
const defaultLanguage = await this.languageService.getDefaultLanguage();
|
||||
await this.mailerService.sendMail({
|
||||
to: dto.email,
|
||||
template: 'password_reset.mjml',
|
||||
@@ -66,7 +69,7 @@ export class PasswordResetService {
|
||||
token: jwt,
|
||||
first_name: user.first_name,
|
||||
t: (key: string) =>
|
||||
this.i18n.t(key, { lang: config.chatbot.lang.default }),
|
||||
this.i18n.t(key, { lang: defaultLanguage.code }),
|
||||
},
|
||||
subject: this.i18n.t('password_reset_subject'),
|
||||
});
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
* 3. SaaS Restriction: This software, or any derivative of it, may not be used to offer a competing product or service (SaaS) without prior written consent from Hexastack. Offering the software as a service or using it in a commercial cloud environment without express permission is strictly prohibited.
|
||||
*/
|
||||
|
||||
import { CACHE_MANAGER } from '@nestjs/cache-manager';
|
||||
import { EventEmitter2 } from '@nestjs/event-emitter';
|
||||
import { JwtModule } from '@nestjs/jwt';
|
||||
import { MongooseModule } from '@nestjs/mongoose';
|
||||
@@ -17,7 +18,10 @@ import { SentMessageInfo } from 'nodemailer';
|
||||
import { AttachmentRepository } from '@/attachment/repositories/attachment.repository';
|
||||
import { AttachmentModel } from '@/attachment/schemas/attachment.schema';
|
||||
import { AttachmentService } from '@/attachment/services/attachment.service';
|
||||
import { ExtendedI18nService } from '@/extended-i18n.service';
|
||||
import { LanguageRepository } from '@/i18n/repositories/language.repository';
|
||||
import { LanguageModel } from '@/i18n/schemas/language.schema';
|
||||
import { I18nService } from '@/i18n/services/i18n.service';
|
||||
import { LanguageService } from '@/i18n/services/language.service';
|
||||
import { LoggerService } from '@/logger/logger.service';
|
||||
import { installUserFixtures, users } from '@/utils/test/fixtures/user';
|
||||
import {
|
||||
@@ -46,6 +50,7 @@ describe('ValidateAccountService', () => {
|
||||
RoleModel,
|
||||
PermissionModel,
|
||||
AttachmentModel,
|
||||
LanguageModel,
|
||||
]),
|
||||
JwtModule,
|
||||
],
|
||||
@@ -56,6 +61,8 @@ describe('ValidateAccountService', () => {
|
||||
UserRepository,
|
||||
RoleService,
|
||||
RoleRepository,
|
||||
LanguageService,
|
||||
LanguageRepository,
|
||||
LoggerService,
|
||||
{
|
||||
provide: MailerService,
|
||||
@@ -69,11 +76,19 @@ describe('ValidateAccountService', () => {
|
||||
EventEmitter2,
|
||||
ValidateAccountService,
|
||||
{
|
||||
provide: ExtendedI18nService,
|
||||
provide: I18nService,
|
||||
useValue: {
|
||||
t: jest.fn().mockImplementation((t) => t),
|
||||
},
|
||||
},
|
||||
{
|
||||
provide: CACHE_MANAGER,
|
||||
useValue: {
|
||||
del: jest.fn(),
|
||||
get: jest.fn(),
|
||||
set: jest.fn(),
|
||||
},
|
||||
},
|
||||
],
|
||||
}).compile();
|
||||
validateAccountService = module.get<ValidateAccountService>(
|
||||
|
||||
@@ -18,7 +18,9 @@ import { JwtService, JwtSignOptions } from '@nestjs/jwt';
|
||||
import { MailerService } from '@nestjs-modules/mailer';
|
||||
|
||||
import { config } from '@/config';
|
||||
import { ExtendedI18nService } from '@/extended-i18n.service';
|
||||
import { I18nService } from '@/i18n/services/i18n.service';
|
||||
import { LanguageService } from '@/i18n/services/language.service';
|
||||
import { LoggerService } from '@/logger/logger.service';
|
||||
|
||||
import { UserService } from './user.service';
|
||||
import { UserCreateDto } from '../dto/user.dto';
|
||||
@@ -35,7 +37,9 @@ export class ValidateAccountService {
|
||||
@Inject(JwtService) private readonly jwtService: JwtService,
|
||||
private readonly userService: UserService,
|
||||
@Optional() private readonly mailerService: MailerService | undefined,
|
||||
private readonly i18n: ExtendedI18nService,
|
||||
private logger: LoggerService,
|
||||
private readonly i18n: I18nService,
|
||||
private readonly languageService: LanguageService,
|
||||
) {}
|
||||
|
||||
/**
|
||||
@@ -73,17 +77,28 @@ export class ValidateAccountService {
|
||||
const confirmationToken = await this.sign({ email: dto.email });
|
||||
|
||||
if (this.mailerService) {
|
||||
await this.mailerService.sendMail({
|
||||
to: dto.email,
|
||||
template: 'account_confirmation.mjml',
|
||||
context: {
|
||||
token: confirmationToken,
|
||||
first_name: dto.first_name,
|
||||
t: (key: string) =>
|
||||
this.i18n.t(key, { lang: config.chatbot.lang.default }),
|
||||
},
|
||||
subject: this.i18n.t('account_confirmation_subject'),
|
||||
});
|
||||
try {
|
||||
const defaultLanguage = await this.languageService.getDefaultLanguage();
|
||||
await this.mailerService.sendMail({
|
||||
to: dto.email,
|
||||
template: 'account_confirmation.mjml',
|
||||
context: {
|
||||
token: confirmationToken,
|
||||
first_name: dto.first_name,
|
||||
t: (key: string) =>
|
||||
this.i18n.t(key, { lang: defaultLanguage.code }),
|
||||
},
|
||||
subject: this.i18n.t('account_confirmation_subject'),
|
||||
});
|
||||
} catch (e) {
|
||||
this.logger.error(
|
||||
'Could not send email',
|
||||
e.message,
|
||||
e.stack,
|
||||
'ValidateAccount',
|
||||
);
|
||||
throw new InternalServerErrorException('Could not send email');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -26,6 +26,7 @@ export type TModel =
|
||||
| 'conversation'
|
||||
| 'message'
|
||||
| 'subscriber'
|
||||
| 'language'
|
||||
| 'translation'
|
||||
| 'botstats'
|
||||
| 'menu'
|
||||
|
||||
@@ -13,3 +13,7 @@ export const SETTING_CACHE_KEY = 'settings';
|
||||
export const PERMISSION_CACHE_KEY = 'permissions';
|
||||
|
||||
export const MENU_CACHE_KEY = 'menu';
|
||||
|
||||
export const LANGUAGES_CACHE_KEY = 'languages';
|
||||
|
||||
export const DEFAULT_LANGUAGE_CACHE_KEY = 'default_language';
|
||||
|
||||
9
api/src/utils/helpers/URL.ts
Normal file
9
api/src/utils/helpers/URL.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
export const buildURL = (baseUrl: string, relativePath: string): string => {
|
||||
try {
|
||||
const url = new URL(relativePath, baseUrl);
|
||||
|
||||
return url.toString();
|
||||
} catch {
|
||||
throw new Error(`Invalid base URL: ${baseUrl}`);
|
||||
}
|
||||
};
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
ArgumentMetadata,
|
||||
Logger,
|
||||
} from '@nestjs/common';
|
||||
import escapeRegExp from 'lodash/escapeRegExp';
|
||||
import { TFilterQuery, Types } from 'mongoose';
|
||||
|
||||
import {
|
||||
@@ -36,9 +37,8 @@ export class SearchFilterPipe<T>
|
||||
}
|
||||
|
||||
private getRegexValue(val: string) {
|
||||
const quote = (str: string) =>
|
||||
str.replace(/([.?*+^$[\]\\(){}|-])/g, '\\$1');
|
||||
return new RegExp(quote(val), 'i');
|
||||
const escapedRegExp = escapeRegExp(val);
|
||||
return new RegExp(escapedRegExp, 'i');
|
||||
}
|
||||
|
||||
private isAllowedField(field: string) {
|
||||
|
||||
33
api/src/utils/test/fixtures/language.ts
vendored
Normal file
33
api/src/utils/test/fixtures/language.ts
vendored
Normal file
@@ -0,0 +1,33 @@
|
||||
/*
|
||||
* 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).
|
||||
* 3. SaaS Restriction: This software, or any derivative of it, may not be used to offer a competing product or service (SaaS) without prior written consent from Hexastack. Offering the software as a service or using it in a commercial cloud environment without express permission is strictly prohibited.
|
||||
*/
|
||||
|
||||
import mongoose from 'mongoose';
|
||||
|
||||
import { LanguageUpdateDto } from '@/i18n/dto/language.dto';
|
||||
import { LanguageModel } from '@/i18n/schemas/language.schema';
|
||||
|
||||
export const languageFixtures: LanguageUpdateDto[] = [
|
||||
{
|
||||
title: 'English',
|
||||
code: 'en',
|
||||
isDefault: true,
|
||||
isRTL: false,
|
||||
},
|
||||
{
|
||||
title: 'Français',
|
||||
code: 'fr',
|
||||
isDefault: false,
|
||||
isRTL: false,
|
||||
},
|
||||
];
|
||||
|
||||
export const installLanguageFixtures = async () => {
|
||||
const Language = mongoose.model(LanguageModel.name, LanguageModel.schema);
|
||||
return await Language.insertMany(languageFixtures);
|
||||
};
|
||||
6
api/src/utils/test/fixtures/nlpentity.ts
vendored
6
api/src/utils/test/fixtures/nlpentity.ts
vendored
@@ -25,12 +25,6 @@ export const nlpEntityFixtures: NlpEntityCreateDto[] = [
|
||||
doc: '',
|
||||
builtin: false,
|
||||
},
|
||||
{
|
||||
name: 'language',
|
||||
lookups: ['trait'],
|
||||
doc: '',
|
||||
builtin: false,
|
||||
},
|
||||
{
|
||||
name: 'built_in',
|
||||
lookups: ['trait'],
|
||||
|
||||
16
api/src/utils/test/fixtures/nlpsample.ts
vendored
16
api/src/utils/test/fixtures/nlpsample.ts
vendored
@@ -13,23 +13,28 @@ import { NlpSampleCreateDto } from '@/nlp/dto/nlp-sample.dto';
|
||||
import { NlpSampleModel, NlpSample } from '@/nlp/schemas/nlp-sample.schema';
|
||||
import { NlpSampleState } from '@/nlp/schemas/types';
|
||||
|
||||
import { installLanguageFixtures } from './language';
|
||||
import { getFixturesWithDefaultValues } from '../defaultValues';
|
||||
import { TFixturesDefaultValues } from '../types';
|
||||
|
||||
const nlpSamples: NlpSampleCreateDto[] = [
|
||||
{
|
||||
text: 'yess',
|
||||
language: '0',
|
||||
},
|
||||
{
|
||||
text: 'No',
|
||||
language: '0',
|
||||
},
|
||||
{
|
||||
text: 'Hello',
|
||||
trained: true,
|
||||
language: '0',
|
||||
},
|
||||
{
|
||||
text: 'Bye Jhon',
|
||||
trained: true,
|
||||
language: '0',
|
||||
},
|
||||
];
|
||||
|
||||
@@ -44,6 +49,15 @@ export const nlpSampleFixtures = getFixturesWithDefaultValues<NlpSample>({
|
||||
});
|
||||
|
||||
export const installNlpSampleFixtures = async () => {
|
||||
const languages = await installLanguageFixtures();
|
||||
|
||||
const NlpSample = mongoose.model(NlpSampleModel.name, NlpSampleModel.schema);
|
||||
return await NlpSample.insertMany(nlpSampleFixtures);
|
||||
return await NlpSample.insertMany(
|
||||
nlpSampleFixtures.map((v) => {
|
||||
return {
|
||||
...v,
|
||||
language: languages[parseInt(v.language)].id,
|
||||
};
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
||||
6
api/src/utils/test/fixtures/nlpvalue.ts
vendored
6
api/src/utils/test/fixtures/nlpvalue.ts
vendored
@@ -45,12 +45,6 @@ export const nlpValueFixtures: NlpValueCreateDto[] = [
|
||||
expressions: ['bye', 'bye bye'],
|
||||
builtin: true,
|
||||
},
|
||||
{
|
||||
entity: '2',
|
||||
value: 'en',
|
||||
expressions: [],
|
||||
builtin: true,
|
||||
},
|
||||
];
|
||||
|
||||
export const installNlpValueFixtures = async () => {
|
||||
|
||||
4
api/src/utils/test/fixtures/translation.ts
vendored
4
api/src/utils/test/fixtures/translation.ts
vendored
@@ -9,8 +9,8 @@
|
||||
|
||||
import mongoose from 'mongoose';
|
||||
|
||||
import { TranslationUpdateDto } from '@/chat/dto/translation.dto';
|
||||
import { TranslationModel } from '@/chat/schemas/translation.schema';
|
||||
import { TranslationUpdateDto } from '@/i18n/dto/translation.dto';
|
||||
import { TranslationModel } from '@/i18n/schemas/translation.schema';
|
||||
|
||||
export const translationFixtures: TranslationUpdateDto[] = [
|
||||
{
|
||||
|
||||
@@ -45,7 +45,8 @@ AUTH_TOKEN=token123
|
||||
LANGUAGE_CLASSIFIER=language-classifier
|
||||
INTENT_CLASSIFIERS=en,fr
|
||||
TFLC_REPO_ID=Hexastack/tflc
|
||||
JISF_REPO_ID=Hexastack/jisf
|
||||
INTENT_CLASSIFIER_REPO_ID=Hexastack/intent-classifier
|
||||
SLOT_FILLER_REPO_ID=Hexastack/slot-filler
|
||||
NLP_PORT=5000
|
||||
|
||||
# Frontend (Next.js)
|
||||
|
||||
@@ -5,5 +5,6 @@ services:
|
||||
build:
|
||||
context: ../nlu
|
||||
dockerfile: Dockerfile
|
||||
pull_policy: build
|
||||
ports:
|
||||
- ${NLP_PORT}:5000
|
||||
|
||||
@@ -1,5 +1 @@
|
||||
version: "3.8"
|
||||
|
||||
widget:
|
||||
build:
|
||||
target: production
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
* 3. SaaS Restriction: This software, or any derivative of it, may not be used to offer a competing product or service (SaaS) without prior written consent from Hexastack. Offering the software as a service or using it in a commercial cloud environment without express permission is strictly prohibited.
|
||||
*/
|
||||
|
||||
import { CheckCircle } from "@mui/icons-material";
|
||||
import AdminPanelSettingsIcon from "@mui/icons-material/AdminPanelSettingsOutlined";
|
||||
import DeleteIcon from "@mui/icons-material/DeleteOutlined";
|
||||
import EditIcon from "@mui/icons-material/EditOutlined";
|
||||
@@ -39,12 +40,16 @@ export enum ActionColumnLabel {
|
||||
Content = "button.content",
|
||||
Fields = "button.fields",
|
||||
Manage_Labels = "title.manage_labels",
|
||||
Toggle = "button.toggle",
|
||||
}
|
||||
|
||||
export interface ActionColumn<T extends GridValidRowModel> {
|
||||
label: ActionColumnLabel;
|
||||
action?: (row: T) => void;
|
||||
requires?: PermissionAction[];
|
||||
getState?: (row: T) => boolean;
|
||||
helperText?: string;
|
||||
isDisabled?: (row: T) => boolean;
|
||||
}
|
||||
|
||||
const BUTTON_WIDTH = 60;
|
||||
@@ -70,6 +75,8 @@ function getIcon(label: ActionColumnLabel) {
|
||||
return <TocOutlinedIcon />;
|
||||
case ActionColumnLabel.Manage_Labels:
|
||||
return <LocalOfferIcon />;
|
||||
case ActionColumnLabel.Toggle:
|
||||
return <CheckCircle />;
|
||||
default:
|
||||
return <></>;
|
||||
}
|
||||
@@ -78,7 +85,7 @@ function getIcon(label: ActionColumnLabel) {
|
||||
function getColor(label: ActionColumnLabel) {
|
||||
switch (label) {
|
||||
case ActionColumnLabel.Edit:
|
||||
return theme.palette.warning.main;
|
||||
return theme.palette.grey[900];
|
||||
case ActionColumnLabel.Delete:
|
||||
return theme.palette.error.main;
|
||||
default:
|
||||
@@ -97,29 +104,46 @@ function StackComponent<T extends GridValidRowModel>({
|
||||
|
||||
return (
|
||||
<Stack height="100%" alignItems="center" direction="row" spacing={0.5}>
|
||||
{actions.map(({ label, action, requires = [] }) => (
|
||||
<GridActionsCellItem
|
||||
key={label}
|
||||
className="actionButton"
|
||||
icon={<Tooltip title={t(label)}>{getIcon(label)}</Tooltip>}
|
||||
label={t(label)}
|
||||
showInMenu={false}
|
||||
sx={{
|
||||
color: "grey",
|
||||
"&:hover": {
|
||||
color: getColor(label),
|
||||
},
|
||||
}}
|
||||
disabled={
|
||||
params.row.builtin &&
|
||||
(requires.includes(PermissionAction.UPDATE) ||
|
||||
requires.includes(PermissionAction.DELETE))
|
||||
}
|
||||
onClick={() => {
|
||||
action && action(params.row);
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
{actions.map(
|
||||
({
|
||||
label,
|
||||
action,
|
||||
requires = [],
|
||||
getState,
|
||||
helperText,
|
||||
isDisabled,
|
||||
}) => (
|
||||
<GridActionsCellItem
|
||||
key={label}
|
||||
className="actionButton"
|
||||
icon={
|
||||
<Tooltip title={helperText || t(label)}>{getIcon(label)}</Tooltip>
|
||||
}
|
||||
label={helperText || t(label)}
|
||||
showInMenu={false}
|
||||
sx={{
|
||||
color:
|
||||
label === ActionColumnLabel.Toggle &&
|
||||
getState &&
|
||||
getState(params.row)
|
||||
? getColor(label)
|
||||
: theme.palette.grey[600],
|
||||
"&:hover": {
|
||||
color: getColor(label),
|
||||
},
|
||||
}}
|
||||
disabled={
|
||||
(isDisabled && isDisabled(params.row)) ||
|
||||
(params.row.builtin &&
|
||||
(requires.includes(PermissionAction.UPDATE) ||
|
||||
requires.includes(PermissionAction.DELETE)))
|
||||
}
|
||||
onClick={() => {
|
||||
action && action(params.row);
|
||||
}}
|
||||
/>
|
||||
),
|
||||
)}
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -14,7 +14,6 @@ import {
|
||||
DialogProps,
|
||||
MenuItem,
|
||||
} from "@mui/material";
|
||||
import { isAbsoluteUrl } from "next/dist/shared/lib/utils";
|
||||
import { useEffect, FC } from "react";
|
||||
import { useForm, Controller } from "react-hook-form";
|
||||
import { useTranslation } from "react-i18next";
|
||||
@@ -26,6 +25,7 @@ import { ContentItem } from "@/app-components/dialogs/layouts/ContentItem";
|
||||
import { Input } from "@/app-components/inputs/Input";
|
||||
import { ToggleableInput } from "@/app-components/inputs/ToggleableInput";
|
||||
import { IMenuItem, IMenuItemAttributes, MenuType } from "@/types/menu.types";
|
||||
import { isAbsoluteUrl } from "@/utils/URL";
|
||||
|
||||
export type MenuDialogProps = DialogProps & {
|
||||
open: boolean;
|
||||
|
||||
151
frontend/src/components/languages/LanguageDialog.tsx
Normal file
151
frontend/src/components/languages/LanguageDialog.tsx
Normal file
@@ -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).
|
||||
* 3. SaaS Restriction: This software, or any derivative of it, may not be used to offer a competing product or service (SaaS) without prior written consent from Hexastack. Offering the software as a service or using it in a commercial cloud environment without express permission is strictly prohibited.
|
||||
*/
|
||||
|
||||
import {
|
||||
Dialog,
|
||||
DialogActions,
|
||||
DialogContent,
|
||||
FormControlLabel,
|
||||
Switch,
|
||||
} from "@mui/material";
|
||||
import { FC, useEffect } from "react";
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import DialogButtons from "@/app-components/buttons/DialogButtons";
|
||||
import { DialogTitle } from "@/app-components/dialogs/DialogTitle";
|
||||
import { ContentContainer } from "@/app-components/dialogs/layouts/ContentContainer";
|
||||
import { ContentItem } from "@/app-components/dialogs/layouts/ContentItem";
|
||||
import { Input } from "@/app-components/inputs/Input";
|
||||
import { useCreate } from "@/hooks/crud/useCreate";
|
||||
import { useUpdate } from "@/hooks/crud/useUpdate";
|
||||
import { DialogControlProps } from "@/hooks/useDialog";
|
||||
import { useToast } from "@/hooks/useToast";
|
||||
import { EntityType } from "@/services/types";
|
||||
import { ILanguage, ILanguageAttributes } from "@/types/language.types";
|
||||
|
||||
export type LanguageDialogProps = DialogControlProps<ILanguage>;
|
||||
export const LanguageDialog: FC<LanguageDialogProps> = ({
|
||||
open,
|
||||
data,
|
||||
closeDialog,
|
||||
...rest
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const { toast } = useToast();
|
||||
const { mutateAsync: createLanguage } = useCreate(EntityType.LANGUAGE, {
|
||||
onError: () => {
|
||||
toast.error(t("message.internal_server_error"));
|
||||
},
|
||||
onSuccess() {
|
||||
closeDialog();
|
||||
toast.success(t("message.success_save"));
|
||||
},
|
||||
});
|
||||
const { mutateAsync: updateLanguage } = useUpdate(EntityType.LANGUAGE, {
|
||||
onError: () => {
|
||||
toast.error(t("message.internal_server_error"));
|
||||
},
|
||||
onSuccess() {
|
||||
closeDialog();
|
||||
toast.success(t("message.success_save"));
|
||||
},
|
||||
});
|
||||
const {
|
||||
reset,
|
||||
register,
|
||||
formState: { errors },
|
||||
handleSubmit,
|
||||
control,
|
||||
} = useForm<ILanguageAttributes>({
|
||||
defaultValues: {
|
||||
title: data?.title || "",
|
||||
code: data?.code || "",
|
||||
isRTL: data?.isRTL || false,
|
||||
},
|
||||
});
|
||||
const validationRules = {
|
||||
title: {
|
||||
required: t("message.title_is_required"),
|
||||
},
|
||||
code: {
|
||||
required: t("message.code_is_required"),
|
||||
},
|
||||
};
|
||||
const onSubmitForm = async (params: ILanguageAttributes) => {
|
||||
if (data) {
|
||||
updateLanguage({ id: data.id, params });
|
||||
} else {
|
||||
createLanguage(params);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (open) reset();
|
||||
}, [open, reset]);
|
||||
|
||||
useEffect(() => {
|
||||
if (data) {
|
||||
reset({
|
||||
title: data.title,
|
||||
code: data.code,
|
||||
isRTL: data.isRTL,
|
||||
});
|
||||
} else {
|
||||
reset();
|
||||
}
|
||||
}, [data, reset]);
|
||||
|
||||
return (
|
||||
<Dialog open={open} fullWidth onClose={closeDialog} {...rest}>
|
||||
<form onSubmit={handleSubmit(onSubmitForm)}>
|
||||
<DialogTitle onClose={closeDialog}>
|
||||
{data ? t("title.edit_label") : t("title.new_label")}
|
||||
</DialogTitle>
|
||||
<DialogContent>
|
||||
<ContentContainer>
|
||||
<ContentItem>
|
||||
<Input
|
||||
label={t("label.title")}
|
||||
error={!!errors.title}
|
||||
{...register("title", validationRules.title)}
|
||||
helperText={errors.title ? errors.title.message : null}
|
||||
multiline={true}
|
||||
/>
|
||||
</ContentItem>
|
||||
<ContentItem>
|
||||
<Input
|
||||
label={t("label.code")}
|
||||
error={!!errors.code}
|
||||
{...register("code", validationRules.code)}
|
||||
helperText={errors.code ? errors.code.message : null}
|
||||
multiline={true}
|
||||
/>
|
||||
</ContentItem>
|
||||
<ContentItem>
|
||||
<Controller
|
||||
name="isRTL"
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<FormControlLabel
|
||||
control={<Switch {...field} checked={field.value} />}
|
||||
label={t("label.is_rtl")}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</ContentItem>
|
||||
</ContentContainer>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<DialogButtons closeDialog={closeDialog} />
|
||||
</DialogActions>
|
||||
</form>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
223
frontend/src/components/languages/index.tsx
Normal file
223
frontend/src/components/languages/index.tsx
Normal file
@@ -0,0 +1,223 @@
|
||||
/*
|
||||
* 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).
|
||||
* 3. SaaS Restriction: This software, or any derivative of it, may not be used to offer a competing product or service (SaaS) without prior written consent from Hexastack. Offering the software as a service or using it in a commercial cloud environment without express permission is strictly prohibited.
|
||||
*/
|
||||
|
||||
import { Flag } from "@mui/icons-material";
|
||||
import AddIcon from "@mui/icons-material/Add";
|
||||
import { Button, Grid, Paper } from "@mui/material";
|
||||
import { GridColDef } from "@mui/x-data-grid";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useQueryClient } from "react-query";
|
||||
|
||||
import { DeleteDialog } from "@/app-components/dialogs/DeleteDialog";
|
||||
import { FilterTextfield } from "@/app-components/inputs/FilterTextfield";
|
||||
import {
|
||||
ActionColumnLabel,
|
||||
useActionColumns,
|
||||
} from "@/app-components/tables/columns/getColumns";
|
||||
import { renderHeader } from "@/app-components/tables/columns/renderHeader";
|
||||
import { DataGrid } from "@/app-components/tables/DataGrid";
|
||||
import { isSameEntity } from "@/hooks/crud/helpers";
|
||||
import { useDelete } from "@/hooks/crud/useDelete";
|
||||
import { useFind } from "@/hooks/crud/useFind";
|
||||
import { useUpdate } from "@/hooks/crud/useUpdate";
|
||||
import { getDisplayDialogs, useDialog } from "@/hooks/useDialog";
|
||||
import { useHasPermission } from "@/hooks/useHasPermission";
|
||||
import { useSearch } from "@/hooks/useSearch";
|
||||
import { useToast } from "@/hooks/useToast";
|
||||
import { PageHeader } from "@/layout/content/PageHeader";
|
||||
import { EntityType } from "@/services/types";
|
||||
import { ILanguage } from "@/types/language.types";
|
||||
import { PermissionAction } from "@/types/permission.types";
|
||||
import { getDateTimeFormatter } from "@/utils/date";
|
||||
|
||||
import { LanguageDialog } from "./LanguageDialog";
|
||||
|
||||
export const Languages = () => {
|
||||
const { t } = useTranslation();
|
||||
const { toast } = useToast();
|
||||
const addDialogCtl = useDialog<ILanguage>(false);
|
||||
const editDialogCtl = useDialog<ILanguage>(false);
|
||||
const deleteDialogCtl = useDialog<string>(false);
|
||||
const queryClient = useQueryClient();
|
||||
const hasPermission = useHasPermission();
|
||||
const { onSearch, searchPayload } = useSearch<ILanguage>({
|
||||
$or: ["title", "code"],
|
||||
});
|
||||
const { dataGridProps, refetch } = useFind(
|
||||
{ entity: EntityType.LANGUAGE },
|
||||
{
|
||||
params: searchPayload,
|
||||
},
|
||||
);
|
||||
const { mutateAsync: updateLanguage } = useUpdate(EntityType.LANGUAGE, {
|
||||
onError: () => {
|
||||
toast.error(t("message.internal_server_error"));
|
||||
},
|
||||
onSuccess() {
|
||||
refetch();
|
||||
toast.success(t("message.success_save"));
|
||||
},
|
||||
});
|
||||
const { mutateAsync: deleteLanguage } = useDelete(EntityType.LANGUAGE, {
|
||||
onError: () => {
|
||||
toast.error(t("message.internal_server_error"));
|
||||
},
|
||||
onSuccess() {
|
||||
queryClient.removeQueries({
|
||||
predicate: ({ queryKey }) => {
|
||||
const [_qType, qEntity] = queryKey;
|
||||
|
||||
return isSameEntity(qEntity, EntityType.NLP_SAMPLE);
|
||||
},
|
||||
});
|
||||
deleteDialogCtl.closeDialog();
|
||||
toast.success(t("message.item_delete_success"));
|
||||
},
|
||||
});
|
||||
const toggleDefault = (row: ILanguage) => {
|
||||
if (!row.isDefault) {
|
||||
updateLanguage({
|
||||
id: row.id,
|
||||
params: {
|
||||
isDefault: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
};
|
||||
const actionColumns = useActionColumns<ILanguage>(
|
||||
EntityType.LANGUAGE,
|
||||
[
|
||||
{
|
||||
label: ActionColumnLabel.Toggle,
|
||||
action: (row) => toggleDefault(row),
|
||||
requires: [PermissionAction.UPDATE],
|
||||
getState: (row) => row.isDefault,
|
||||
helperText: t("button.mark_as_default"),
|
||||
},
|
||||
{
|
||||
label: ActionColumnLabel.Edit,
|
||||
action: (row) => editDialogCtl.openDialog(row),
|
||||
requires: [PermissionAction.UPDATE],
|
||||
},
|
||||
{
|
||||
label: ActionColumnLabel.Delete,
|
||||
action: (row) => deleteDialogCtl.openDialog(row.id),
|
||||
requires: [PermissionAction.DELETE],
|
||||
isDisabled: (row) => row.isDefault,
|
||||
},
|
||||
],
|
||||
t("label.operations"),
|
||||
);
|
||||
const columns: GridColDef<ILanguage>[] = [
|
||||
{ field: "id", headerName: "ID" },
|
||||
{
|
||||
flex: 2,
|
||||
field: "title",
|
||||
headerName: t("label.title"),
|
||||
disableColumnMenu: true,
|
||||
renderHeader,
|
||||
headerAlign: "left",
|
||||
},
|
||||
{
|
||||
flex: 1,
|
||||
field: "code",
|
||||
headerName: t("label.code"),
|
||||
disableColumnMenu: true,
|
||||
renderHeader,
|
||||
headerAlign: "left",
|
||||
},
|
||||
{
|
||||
flex: 1,
|
||||
field: "isDefault",
|
||||
headerName: t("label.is_default"),
|
||||
disableColumnMenu: true,
|
||||
renderHeader,
|
||||
headerAlign: "left",
|
||||
valueGetter: (value) => (value ? t("label.yes") : t("label.no")),
|
||||
},
|
||||
{
|
||||
flex: 1,
|
||||
field: "isRTL",
|
||||
headerName: t("label.is_rtl"),
|
||||
disableColumnMenu: true,
|
||||
renderHeader,
|
||||
headerAlign: "left",
|
||||
valueGetter: (value) => (value ? t("label.yes") : t("label.no")),
|
||||
},
|
||||
{
|
||||
minWidth: 140,
|
||||
field: "createdAt",
|
||||
headerName: t("label.createdAt"),
|
||||
disableColumnMenu: true,
|
||||
renderHeader,
|
||||
resizable: false,
|
||||
headerAlign: "left",
|
||||
valueGetter: (params) =>
|
||||
t("datetime.created_at", getDateTimeFormatter(params)),
|
||||
},
|
||||
{
|
||||
minWidth: 140,
|
||||
field: "updatedAt",
|
||||
headerName: t("label.updatedAt"),
|
||||
disableColumnMenu: true,
|
||||
renderHeader,
|
||||
resizable: false,
|
||||
headerAlign: "left",
|
||||
valueGetter: (params) =>
|
||||
t("datetime.updated_at", getDateTimeFormatter(params)),
|
||||
},
|
||||
actionColumns,
|
||||
];
|
||||
|
||||
return (
|
||||
<Grid container gap={3} flexDirection="column">
|
||||
<LanguageDialog {...getDisplayDialogs(addDialogCtl)} />
|
||||
<LanguageDialog {...getDisplayDialogs(editDialogCtl)} />
|
||||
<DeleteDialog
|
||||
{...deleteDialogCtl}
|
||||
callback={() => {
|
||||
if (deleteDialogCtl?.data) deleteLanguage(deleteDialogCtl.data);
|
||||
}}
|
||||
/>
|
||||
<PageHeader icon={Flag} title={t("title.languages")}>
|
||||
<Grid
|
||||
justifyContent="flex-end"
|
||||
gap={1}
|
||||
container
|
||||
alignItems="center"
|
||||
flexShrink={0}
|
||||
width="max-content"
|
||||
>
|
||||
<Grid item>
|
||||
<FilterTextfield onChange={onSearch} />
|
||||
</Grid>
|
||||
{hasPermission(EntityType.LANGUAGE, PermissionAction.CREATE) ? (
|
||||
<Grid item>
|
||||
<Button
|
||||
startIcon={<AddIcon />}
|
||||
variant="contained"
|
||||
sx={{ float: "right" }}
|
||||
onClick={() => addDialogCtl.openDialog()}
|
||||
>
|
||||
{t("button.add")}
|
||||
</Button>
|
||||
</Grid>
|
||||
) : null}
|
||||
</Grid>
|
||||
</PageHeader>
|
||||
<Grid item xs={12}>
|
||||
<Paper sx={{ padding: 2 }}>
|
||||
<Grid>
|
||||
<DataGrid columns={columns} {...dataGridProps} />
|
||||
</Grid>
|
||||
</Paper>
|
||||
</Grid>
|
||||
</Grid>
|
||||
);
|
||||
};
|
||||
@@ -17,6 +17,7 @@ import AttachmentInput from "@/app-components/attachment/AttachmentInput";
|
||||
import { DialogTitle } from "@/app-components/dialogs/DialogTitle";
|
||||
import { ContentContainer } from "@/app-components/dialogs/layouts/ContentContainer";
|
||||
import { ContentItem } from "@/app-components/dialogs/layouts/ContentItem";
|
||||
import { isSameEntity } from "@/hooks/crud/helpers";
|
||||
import { useApiClient } from "@/hooks/useApiClient";
|
||||
import { DialogControlProps } from "@/hooks/useDialog";
|
||||
import { useToast } from "@/hooks/useToast";
|
||||
@@ -40,11 +41,19 @@ export const NlpImportDialog: FC<NlpImportDialogProps> = ({
|
||||
attachmentId && (await apiClient.importNlpSamples(attachmentId));
|
||||
},
|
||||
onSuccess: () => {
|
||||
queryClient.removeQueries([
|
||||
QueryType.collection,
|
||||
EntityType.NLP_SAMPLE,
|
||||
]);
|
||||
queryClient.removeQueries({
|
||||
predicate: ({ queryKey }) => {
|
||||
const [qType, qEntity] = queryKey;
|
||||
|
||||
return (
|
||||
((qType === QueryType.count || qType === QueryType.collection) &&
|
||||
isSameEntity(qEntity, EntityType.NLP_SAMPLE)) ||
|
||||
isSameEntity(qEntity, EntityType.NLP_SAMPLE_ENTITY) ||
|
||||
isSameEntity(qEntity, EntityType.NLP_ENTITY) ||
|
||||
isSameEntity(qEntity, EntityType.NLP_VALUE)
|
||||
);
|
||||
},
|
||||
});
|
||||
handleCloseDialog();
|
||||
toast.success(t("message.success_save"));
|
||||
},
|
||||
|
||||
@@ -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<INlpSampleFull>;
|
||||
export type NlpSampleDialogProps = DialogControlProps<INlpDatasetSample>;
|
||||
export const NlpSampleDialog: FC<NlpSampleDialogProps> = ({
|
||||
open,
|
||||
data: sample,
|
||||
@@ -44,15 +44,16 @@ export const NlpSampleDialog: FC<NlpSampleDialogProps> = ({
|
||||
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,
|
||||
},
|
||||
},
|
||||
{
|
||||
|
||||
@@ -20,10 +20,11 @@ import { ContentItem } from "@/app-components/dialogs/layouts/ContentItem";
|
||||
import { Input } from "@/app-components/inputs/Input";
|
||||
import MultipleInput from "@/app-components/inputs/MultipleInput";
|
||||
import { useCreate } from "@/hooks/crud/useCreate";
|
||||
import { useGet } from "@/hooks/crud/useGet";
|
||||
import { useUpdate } from "@/hooks/crud/useUpdate";
|
||||
import { DialogControlProps } from "@/hooks/useDialog";
|
||||
import { useToast } from "@/hooks/useToast";
|
||||
import { EntityType } from "@/services/types";
|
||||
import { EntityType, Format } from "@/services/types";
|
||||
import { INlpValue, INlpValueAttributes } from "@/types/nlp-value.types";
|
||||
|
||||
export type TNlpValueAttributesWithRequiredExpressions = INlpValueAttributes & {
|
||||
@@ -44,11 +45,16 @@ export const NlpValueDialog: FC<NlpValueDialogProps> = ({
|
||||
const { t } = useTranslation();
|
||||
const { toast } = useToast();
|
||||
const { query } = useRouter();
|
||||
const { refetch: refetchEntity } = useGet(data?.entity || String(query.id), {
|
||||
entity: EntityType.NLP_ENTITY,
|
||||
format: Format.FULL,
|
||||
});
|
||||
const { mutateAsync: createNlpValue } = useCreate(EntityType.NLP_VALUE, {
|
||||
onError: () => {
|
||||
toast.error(t("message.internal_server_error"));
|
||||
},
|
||||
onSuccess(data) {
|
||||
refetchEntity();
|
||||
closeDialog();
|
||||
toast.success(t("message.success_save"));
|
||||
callback?.(data);
|
||||
|
||||
@@ -19,6 +19,7 @@ import {
|
||||
Grid,
|
||||
IconButton,
|
||||
MenuItem,
|
||||
Stack,
|
||||
} from "@mui/material";
|
||||
import { GridColDef } from "@mui/x-data-grid";
|
||||
import { useState } from "react";
|
||||
@@ -26,6 +27,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 +45,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 +69,17 @@ export default function NlpSample() {
|
||||
const { apiUrl } = useConfig();
|
||||
const { toast } = useToast();
|
||||
const { t } = useTranslation();
|
||||
const [dataset, setDataSet] = useState("");
|
||||
const [type, setType] = useState<NlpSampleType | undefined>(undefined);
|
||||
const [language, setLanguage] = useState<string | undefined>(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<INlpSample>({
|
||||
$eq: dataset === "" ? [] : [{ type: dataset as NlpSampleType }],
|
||||
$eq: [...(type ? [{ type }] : []), ...(language ? [{ language }] : [])],
|
||||
$iLike: ["text"],
|
||||
});
|
||||
const { mutateAsync: deleteNlpSample } = useDelete(EntityType.NLP_SAMPLE, {
|
||||
@@ -90,21 +98,30 @@ export default function NlpSample() {
|
||||
},
|
||||
);
|
||||
const deleteDialogCtl = useDialog<string>(false);
|
||||
const editDialogCtl = useDialog<INlpSampleFull>(false);
|
||||
const editDialogCtl = useDialog<INlpDatasetSample>(false);
|
||||
const importDialogCtl = useDialog<never>(false);
|
||||
const actionColumns = getActionsColumn<INlpSampleFull>(
|
||||
const actionColumns = getActionsColumn<INlpSample>(
|
||||
[
|
||||
{
|
||||
label: ActionColumnLabel.Edit,
|
||||
action: ({ entities, ...rest }) => {
|
||||
const data: INlpSampleFull = {
|
||||
action: ({ entities, language, ...rest }) => {
|
||||
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: language
|
||||
? (getLanguageFromCache(language) as ILanguage).code
|
||||
: null,
|
||||
};
|
||||
|
||||
editDialogCtl.openDialog(data);
|
||||
@@ -119,7 +136,7 @@ export default function NlpSample() {
|
||||
],
|
||||
t("label.operations"),
|
||||
);
|
||||
const columns: GridColDef<INlpSampleFull>[] = [
|
||||
const columns: GridColDef<INlpSample>[] = [
|
||||
{
|
||||
flex: 1,
|
||||
field: "text",
|
||||
@@ -131,39 +148,56 @@ export default function NlpSample() {
|
||||
{
|
||||
flex: 1,
|
||||
field: "entities",
|
||||
renderCell: ({ row }) =>
|
||||
row.entities.map((entity) => (
|
||||
<ChipEntity
|
||||
id={entity.entity}
|
||||
key={entity.id}
|
||||
variant="title"
|
||||
field="name"
|
||||
render={(value) => (
|
||||
<Chip
|
||||
renderCell: ({ row }) => (
|
||||
<Stack direction="row" my={1} spacing={1}>
|
||||
{row.entities
|
||||
.map((e) => getSampleEntityFromCache(e) as INlpSampleEntity)
|
||||
.filter((e) => !!e)
|
||||
.map((entity) => (
|
||||
<ChipEntity
|
||||
key={entity.id}
|
||||
id={entity.entity}
|
||||
variant="title"
|
||||
label={
|
||||
<>
|
||||
{value}
|
||||
{` `}={` `}
|
||||
<ChipEntity
|
||||
id={entity.value}
|
||||
key={entity.value}
|
||||
variant="text"
|
||||
field="value"
|
||||
entity={EntityType.NLP_VALUE}
|
||||
/>
|
||||
</>
|
||||
}
|
||||
field="name"
|
||||
render={(value) => (
|
||||
<Chip
|
||||
variant="title"
|
||||
label={
|
||||
<>
|
||||
{value}
|
||||
{` `}={` `}
|
||||
<ChipEntity
|
||||
id={entity.value}
|
||||
key={entity.value}
|
||||
variant="text"
|
||||
field="value"
|
||||
entity={EntityType.NLP_VALUE}
|
||||
/>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
entity={EntityType.NLP_ENTITY}
|
||||
/>
|
||||
)}
|
||||
entity={EntityType.NLP_ENTITY}
|
||||
/>
|
||||
)),
|
||||
))}
|
||||
</Stack>
|
||||
),
|
||||
headerName: t("label.entities"),
|
||||
sortable: false,
|
||||
disableColumnMenu: true,
|
||||
renderHeader,
|
||||
},
|
||||
{
|
||||
maxWidth: 90,
|
||||
field: "language",
|
||||
renderCell: ({ row }) => {
|
||||
return row.language ? getLanguageFromCache(row.language)?.title : "";
|
||||
},
|
||||
headerName: t("label.language"),
|
||||
sortable: true,
|
||||
disableColumnMenu: true,
|
||||
renderHeader,
|
||||
},
|
||||
{
|
||||
maxWidth: 90,
|
||||
field: "type",
|
||||
@@ -232,18 +266,33 @@ export default function NlpSample() {
|
||||
fullWidth={false}
|
||||
sx={{ minWidth: "256px" }}
|
||||
/>
|
||||
<AutoCompleteEntitySelect<ILanguage, "title", false>
|
||||
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)}
|
||||
/>
|
||||
<Input
|
||||
select
|
||||
fullWidth={false}
|
||||
sx={{
|
||||
width: "150px",
|
||||
minWidth: "150px",
|
||||
}}
|
||||
label={t("label.dataset")}
|
||||
value={dataset}
|
||||
onChange={(e) => setDataSet(e.target.value)}
|
||||
value={type}
|
||||
onChange={(e) => setType(e.target.value as NlpSampleType)}
|
||||
SelectProps={{
|
||||
...(dataset !== "" && {
|
||||
...(type && {
|
||||
IconComponent: () => (
|
||||
<IconButton size="small" onClick={() => setDataSet("")}>
|
||||
<IconButton size="small" onClick={() => setType(undefined)}>
|
||||
<DeleteIcon />
|
||||
</IconButton>
|
||||
),
|
||||
@@ -288,7 +337,7 @@ export default function NlpSample() {
|
||||
variant="contained"
|
||||
href={buildURL(
|
||||
apiUrl,
|
||||
`nlpsample/export${dataset ? `?type=${dataset}` : ""}`,
|
||||
`nlpsample/export${type ? `?type=${type}` : ""}`,
|
||||
)}
|
||||
startIcon={<DownloadIcon />}
|
||||
>
|
||||
|
||||
@@ -23,7 +23,7 @@ import {
|
||||
RadioGroup,
|
||||
Typography,
|
||||
} from "@mui/material";
|
||||
import { FC, useCallback, useMemo, useState } from "react";
|
||||
import { FC, useCallback, useEffect, useMemo, useState } from "react";
|
||||
import { Controller, useFieldArray, useForm } from "react-hook-form";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useQuery } from "react-query";
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -64,68 +65,40 @@ const NlpDatasetSample: FC<NlpDatasetSampleProps> = ({
|
||||
{
|
||||
hasCount: false,
|
||||
},
|
||||
{
|
||||
onSuccess(entities) {
|
||||
// By default append trait entities
|
||||
if (!sample) {
|
||||
removeTraitEntity();
|
||||
(entities || [])
|
||||
.filter(({ lookups }) => lookups.includes("trait"))
|
||||
.forEach(({ name }) => {
|
||||
appendTraitEntity({
|
||||
entity: name,
|
||||
value: "",
|
||||
});
|
||||
});
|
||||
}
|
||||
},
|
||||
},
|
||||
);
|
||||
const getNlpValueFromCache = useGetFromCache(EntityType.NLP_VALUE);
|
||||
// Default trait entities to append to the form
|
||||
const defaultTraitEntities = useMemo(() => {
|
||||
if (!sample || !entities) return [];
|
||||
|
||||
const traitEntities = entities.filter(({ lookups }) =>
|
||||
lookups.includes("trait"),
|
||||
);
|
||||
const sampleTraitEntities = sample.entities.filter(
|
||||
(e) => typeof e.start === "undefined",
|
||||
);
|
||||
|
||||
if (sampleTraitEntities.length === traitEntities.length) {
|
||||
return sampleTraitEntities;
|
||||
}
|
||||
|
||||
const sampleEntityNames = new Set(sampleTraitEntities.map((e) => e.entity));
|
||||
const missingEntities = traitEntities
|
||||
.filter(({ name }) => !sampleEntityNames.has(name))
|
||||
.map(({ name }) => ({
|
||||
entity: name,
|
||||
value: "",
|
||||
}));
|
||||
|
||||
return [...sampleTraitEntities, ...missingEntities];
|
||||
}, [entities, sample]);
|
||||
// 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",
|
||||
) as INlpDatasetKeywordEntity[],
|
||||
}),
|
||||
[sample, entities],
|
||||
);
|
||||
const { handleSubmit, control, register, reset, setValue, watch } =
|
||||
useForm<INlpSampleFormAttributes>({
|
||||
defaultValues: {
|
||||
type: sample?.type || NlpSampleType.train,
|
||||
text: sample?.text || "",
|
||||
traitEntities: defaultTraitEntities,
|
||||
keywordEntities:
|
||||
sample?.entities.filter((e) => typeof e.start === "number") || [],
|
||||
},
|
||||
defaultValues,
|
||||
});
|
||||
const currentText = watch("text");
|
||||
const currentType = watch("type");
|
||||
const { apiClient } = useApiClient();
|
||||
const {
|
||||
fields: traitEntities,
|
||||
append: appendTraitEntity,
|
||||
update: updateTraitEntity,
|
||||
remove: removeTraitEntity,
|
||||
} = useFieldArray({
|
||||
const { fields: traitEntities, update: updateTraitEntity } = useFieldArray({
|
||||
control,
|
||||
name: "traitEntities",
|
||||
});
|
||||
@@ -153,12 +126,16 @@ const NlpDatasetSample: FC<NlpDatasetSampleProps> = ({
|
||||
},
|
||||
onSuccess: (result) => {
|
||||
const traitEntities: INlpDatasetTraitEntity[] = result.entities.filter(
|
||||
(e) => !("start" in e && "end" in e),
|
||||
(e) => !("start" in e && "end" in e) && e.entity !== "language",
|
||||
);
|
||||
const keywordEntities = result.entities.filter(
|
||||
(e) => "start" in e && "end" in e,
|
||||
) as INlpDatasetKeywordEntity[];
|
||||
const language = result.entities.find(
|
||||
({ entity }) => entity === "language",
|
||||
);
|
||||
|
||||
setValue("language", language?.value || "");
|
||||
setValue("traitEntities", traitEntities);
|
||||
setValue("keywordEntities", keywordEntities);
|
||||
},
|
||||
@@ -167,7 +144,7 @@ const NlpDatasetSample: FC<NlpDatasetSampleProps> = ({
|
||||
|
||||
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,14 +154,20 @@ const NlpDatasetSample: FC<NlpDatasetSampleProps> = ({
|
||||
start: number;
|
||||
end: number;
|
||||
} | null>(null);
|
||||
const onSubmitForm = (params: INlpSampleFormAttributes) => {
|
||||
submitForm(params);
|
||||
reset();
|
||||
removeTraitEntity();
|
||||
removeKeywordEntity();
|
||||
const onSubmitForm = (form: INlpSampleFormAttributes) => {
|
||||
submitForm(form);
|
||||
refetchEntities();
|
||||
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)}>
|
||||
@@ -247,6 +230,39 @@ const NlpDatasetSample: FC<NlpDatasetSampleProps> = ({
|
||||
/>
|
||||
</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}
|
||||
|
||||
@@ -142,8 +142,6 @@ export const NlpValues = ({ entityId }: { entityId: string }) => {
|
||||
|
||||
return (
|
||||
<Grid container gap={2} flexDirection="column">
|
||||
{/* <PageHeader title={t("title.nlp_train")} icon={faGraduationCap} /> */}
|
||||
|
||||
<Slide direction={direction} in={true} mountOnEnter unmountOnExit>
|
||||
<Grid item xs={12}>
|
||||
<Box sx={{ padding: 1 }}>
|
||||
|
||||
@@ -81,6 +81,7 @@ export const Nlp = ({
|
||||
text: params.text,
|
||||
type: params.type,
|
||||
entities: [...params.traitEntities, ...params.keywordEntities],
|
||||
language: params.language,
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user