feat: fetch remote i18n

This commit is contained in:
Mohamed Marrouchi 2024-10-18 17:50:35 +01:00
parent 08b1deae50
commit 85cc85e4db
50 changed files with 1823 additions and 1553 deletions

View File

@ -72,4 +72,4 @@ module.exports = {
}, },
], ],
}, },
}; };

View File

@ -26,7 +26,7 @@ import ChannelHandler from './lib/Handler';
@Injectable() @Injectable()
export class ChannelService { export class ChannelService {
private registry: Map<string, ChannelHandler> = new Map(); private registry: Map<string, ChannelHandler<string>> = new Map();
constructor( constructor(
private readonly logger: LoggerService, private readonly logger: LoggerService,
@ -40,7 +40,10 @@ export class ChannelService {
* @param channel - The channel handler associated with the channel name. * @param channel - The channel handler associated with the channel name.
* @typeParam C The channel handler's type that extends `ChannelHandler`. * @typeParam C The channel handler's type that extends `ChannelHandler`.
*/ */
public setChannel<C extends ChannelHandler>(name: string, channel: C) { public setChannel<T extends string, C extends ChannelHandler<T>>(
name: T,
channel: C,
) {
this.registry.set(name, channel); this.registry.set(name, channel);
} }
@ -71,7 +74,9 @@ export class ChannelService {
* @param channelName - The name of the channel (messenger, offline, ...). * @param channelName - The name of the channel (messenger, offline, ...).
* @returns The handler for the specified channel. * @returns The handler for the specified channel.
*/ */
public getChannelHandler<C extends ChannelHandler>(name: string): C { public getChannelHandler<T extends string, C extends ChannelHandler<T>>(
name: T,
): C {
const handler = this.registry.get(name); const handler = this.registry.get(name);
if (!handler) { if (!handler) {
throw new Error(`Channel ${name} not found`); throw new Error(`Channel ${name} not found`);
@ -98,8 +103,8 @@ export class ChannelService {
* @param req - The websocket request object. * @param req - The websocket request object.
* @param res - The websocket response object. * @param res - The websocket response object.
*/ */
@SocketGet('/webhook/offline/') @SocketGet(`/webhook/${OFFLINE_CHANNEL_NAME}/`)
@SocketPost('/webhook/offline/') @SocketPost(`/webhook/${OFFLINE_CHANNEL_NAME}/`)
handleWebsocketForOffline( handleWebsocketForOffline(
@SocketReq() req: SocketRequest, @SocketReq() req: SocketRequest,
@SocketRes() res: SocketResponse, @SocketRes() res: SocketResponse,
@ -116,8 +121,8 @@ export class ChannelService {
* @param req - The websocket request object. * @param req - The websocket request object.
* @param res - The websocket response object. * @param res - The websocket response object.
*/ */
@SocketGet('/webhook/live-chat-tester/') @SocketGet(`/webhook/${LIVE_CHAT_TEST_CHANNEL_NAME}/`)
@SocketPost('/webhook/live-chat-tester/') @SocketPost(`/webhook/${LIVE_CHAT_TEST_CHANNEL_NAME}/`)
async handleWebsocketForLiveChatTester( async handleWebsocketForLiveChatTester(
@SocketReq() req: SocketRequest, @SocketReq() req: SocketRequest,
@SocketRes() res: SocketResponse, @SocketRes() res: SocketResponse,

View File

@ -18,35 +18,57 @@ import {
import { LoggerService } from '@/logger/logger.service'; import { LoggerService } from '@/logger/logger.service';
import BaseNlpHelper from '@/nlp/lib/BaseNlpHelper'; import BaseNlpHelper from '@/nlp/lib/BaseNlpHelper';
import { NlpService } from '@/nlp/services/nlp.service'; import { NlpService } from '@/nlp/services/nlp.service';
import { SettingCreateDto } from '@/setting/dto/setting.dto';
import { SettingService } from '@/setting/services/setting.service'; import { SettingService } from '@/setting/services/setting.service';
import { SocketRequest } from '@/websocket/utils/socket-request'; import { SocketRequest } from '@/websocket/utils/socket-request';
import { SocketResponse } from '@/websocket/utils/socket-response'; import { SocketResponse } from '@/websocket/utils/socket-response';
import { ChannelService } from '../channel.service'; import { ChannelService } from '../channel.service';
import { ChannelSetting } from '../types';
import EventWrapper from './EventWrapper';
import EventWrapper from './EventWrapper'; import EventWrapper from './EventWrapper';
@Injectable() @Injectable()
export default abstract class ChannelHandler { export default abstract class ChannelHandler<N extends string = string> {
protected settings: SettingCreateDto[] = []; private readonly name: N;
private readonly settings: ChannelSetting<N>[];
protected NLP: BaseNlpHelper; protected NLP: BaseNlpHelper;
constructor( constructor(
name: N,
settings: ChannelSetting<N>[],
protected readonly settingService: SettingService, protected readonly settingService: SettingService,
private readonly channelService: ChannelService, private readonly channelService: ChannelService,
protected readonly nlpService: NlpService, protected readonly nlpService: NlpService,
protected readonly logger: LoggerService, protected readonly logger: LoggerService,
) {} ) {
this.name = name;
this.settings = settings;
}
onModuleInit() { onModuleInit() {
this.channelService.setChannel(this.getChannel(), this); this.channelService.setChannel(
this.getChannel(),
this as unknown as ChannelHandler<N>,
);
this.setup(); this.setup();
} }
protected getGroup() {
return this.getChannel().replaceAll('-', '_');
}
async setup() { async setup() {
await this.settingService.seedIfNotExist(this.getChannel(), this.settings); await this.settingService.seedIfNotExist(
this.getChannel(),
this.settings.map((s, i) => ({
...s,
weight: i + 1,
})),
);
const nlp = this.nlpService.getNLP(); const nlp = this.nlpService.getNLP();
this.setNLP(nlp); this.setNLP(nlp);
this.init(); this.init();
@ -61,30 +83,32 @@ export default abstract class ChannelHandler {
} }
/** /**
* Returns the channel specific settings * Returns the channel's name
* @returns Channel's name
*/ */
async getSettings<S>() { getChannel() {
const settings = await this.settingService.getSettings(); return this.name;
return settings[this.getChannel()] as S;
} }
/** /**
* Returns the channel's name * Returns the channel's settings
* @returns {String} * @returns Channel's settings
*/ */
abstract getChannel(): string; async getSettings() {
const settings = await this.settingService.getSettings();
return settings[this.getGroup()];
}
/** /**
* Perform any initialization needed * Perform any initialization needed
* @returns
*/ */
abstract init(): void; abstract init(): void;
/** /**
* Process incoming channel data via POST/GET methods
*
* @param {module:Controller.req} req * @param {module:Controller.req} req
* @param {module:Controller.res} res * @param {module:Controller.res} res
* Process incoming channel data via POST/GET methods
*/ */
abstract handle( abstract handle(
req: Request | SocketRequest, req: Request | SocketRequest,
@ -93,26 +117,28 @@ export default abstract class ChannelHandler {
/** /**
* Format a text message that will be sent to the channel * Format a text message that will be sent to the channel
*
* @param message - A text to be sent to the end user * @param message - A text to be sent to the end user
* @param options - might contain additional settings * @param options - might contain additional settings
* @returns {Object} - A text message in the channel specific format * @returns {Object} - A text message in the channel specific format
*/ */
abstract _textFormat(message: StdOutgoingMessage, options?: any): any; abstract _textFormat(message: StdOutgoingMessage, options?: any): any;
/** /**
* Format a text + quick replies message that can be sent to the channel
*
* @param message - A text + quick replies to be sent to the end user * @param message - A text + quick replies to be sent to the end user
* @param options - might contain additional settings * @param options - might contain additional settings
* @returns {Object} - A quick replies message in the channel specific format * @returns {Object} - A quick replies message in the channel specific format
* Format a text + quick replies message that can be sent to the channel
*/ */
abstract _quickRepliesFormat(message: StdOutgoingMessage, options?: any): any; abstract _quickRepliesFormat(message: StdOutgoingMessage, options?: any): any;
/** /**
* From raw buttons, construct a channel understable message containing those buttons
*
* @param message - A text + buttons to be sent to the end user * @param message - A text + buttons to be sent to the end user
* @param options - Might contain additional settings * @param options - Might contain additional settings
* @returns {Object} - A buttons message in the format required by the channel * @returns {Object} - A buttons message in the format required by the channel
* From raw buttons, construct a channel understable message containing those buttons
*/ */
abstract _buttonsFormat( abstract _buttonsFormat(
message: StdOutgoingMessage, message: StdOutgoingMessage,
@ -121,27 +147,29 @@ export default abstract class ChannelHandler {
): any; ): any;
/** /**
* Format an attachment + quick replies message that can be sent to the channel
*
* @param message - An attachment + quick replies to be sent to the end user * @param message - An attachment + quick replies to be sent to the end user
* @param options - Might contain additional settings * @param options - Might contain additional settings
* @returns {Object} - An attachment message in the format required by the channel * @returns {Object} - An attachment message in the format required by the channel
* Format an attachment + quick replies message that can be sent to the channel
*/ */
abstract _attachmentFormat(message: StdOutgoingMessage, options?: any): any; abstract _attachmentFormat(message: StdOutgoingMessage, options?: any): any;
/** /**
* Format a collection of items to be sent to the channel in carousel/list format
*
* @param data - A list of data items to be sent to the end user * @param data - A list of data items to be sent to the end user
* @param options - Might contain additional settings * @param options - Might contain additional settings
* @returns {Object[]} - An array of element objects * @returns {Object[]} - An array of element objects
* Format a collection of items to be sent to the channel in carousel/list format
*/ */
abstract _formatElements(data: any[], options: any, ...args: any): any[]; abstract _formatElements(data: any[], options: any, ...args: any): any[];
/** /**
* Format a list of elements * Format a list of elements
*
* @param message - Contains elements to be sent to the end user * @param message - Contains elements to be sent to the end user
* @param options - Might contain additional settings * @param options - Might contain additional settings
* @returns {Object} - A ready to be sent list template message in the format required by the channel * @returns {Object} - A ready to be sent list template message in the format required by the channel
*/ */
abstract _listFormat( abstract _listFormat(
message: StdOutgoingMessage, message: StdOutgoingMessage,

8
api/src/channel/types.ts Normal file
View File

@ -0,0 +1,8 @@
import { SettingCreateDto } from '@/setting/dto/setting.dto';
export type ChannelSetting<N extends string = string> = Omit<
SettingCreateDto,
'group' | 'weight'
> & {
group: HyphenToUnderscore<N>;
};

View File

@ -34,7 +34,6 @@ import { I18nService } from '@/i18n/services/i18n.service';
import { LanguageService } from '@/i18n/services/language.service'; import { LanguageService } from '@/i18n/services/language.service';
import { LoggerService } from '@/logger/logger.service'; import { LoggerService } from '@/logger/logger.service';
import { PluginService } from '@/plugins/plugins.service'; import { PluginService } from '@/plugins/plugins.service';
import { Settings } from '@/setting/schemas/types';
import { SettingService } from '@/setting/services/setting.service'; import { SettingService } from '@/setting/services/setting.service';
import { import {
blockFixtures, blockFixtures,
@ -67,6 +66,9 @@ import { FileType } from '../schemas/types/attachment';
import { Context } from '../schemas/types/context'; import { Context } from '../schemas/types/context';
import { PayloadType, StdOutgoingListMessage } from '../schemas/types/message'; import { PayloadType, StdOutgoingListMessage } from '../schemas/types/message';
import { SubscriberContext } from '../schemas/types/subscriberContext'; import { SubscriberContext } from '../schemas/types/subscriberContext';
import { CategoryRepository } from './../repositories/category.repository';
import { BlockService } from './block.service';
import { CategoryService } from './category.service';
import { CategoryRepository } from './../repositories/category.repository'; import { CategoryRepository } from './../repositories/category.repository';
import { BlockService } from './block.service'; import { BlockService } from './block.service';

View File

@ -18,7 +18,6 @@ import { LoggerService } from '@/logger/logger.service';
import { Nlp } from '@/nlp/lib/types'; import { Nlp } from '@/nlp/lib/types';
import { PluginService } from '@/plugins/plugins.service'; import { PluginService } from '@/plugins/plugins.service';
import { PluginType } from '@/plugins/types'; import { PluginType } from '@/plugins/types';
import { Settings } from '@/setting/schemas/types';
import { SettingService } from '@/setting/services/setting.service'; import { SettingService } from '@/setting/services/setting.service';
import { BaseService } from '@/utils/generics/base-service'; import { BaseService } from '@/utils/generics/base-service';
import { getRandom } from '@/utils/helpers/safeRandom'; import { getRandom } from '@/utils/helpers/safeRandom';

View File

@ -11,7 +11,6 @@ import { EventEmitter2 } from '@nestjs/event-emitter';
import EventWrapper from '@/channel/lib/EventWrapper'; import EventWrapper from '@/channel/lib/EventWrapper';
import { LoggerService } from '@/logger/logger.service'; import { LoggerService } from '@/logger/logger.service';
import { Settings } from '@/setting/schemas/types';
import { SettingService } from '@/setting/services/setting.service'; import { SettingService } from '@/setting/services/setting.service';
import { MessageCreateDto } from '../dto/message.dto'; import { MessageCreateDto } from '../dto/message.dto';

View File

@ -0,0 +1,23 @@
{
"verification_token": "Verification Token",
"allowed_domains": "Allowed Domains",
"start_button": "Enable `Get Started`",
"input_disabled": "Disable Input",
"persistent_menu": "Display Persistent Menu",
"greeting_message": "Greeting Message",
"theme_color": "Widget Theme",
"theme_color_options": {
"orange": "Orange",
"red": "Red",
"green": "Green",
"blue": "Blue",
"dark": "Dark"
},
"window_title": "Chat Window Title",
"avatar_url": "Chatbot Avatar URL",
"show_emoji": "Enable Emoji Picker",
"show_file": "Enable Attachment Uploader",
"show_location": "Enable Geolocation Share",
"allowed_upload_size": "Max Upload Size (in bytes)",
"allowed_upload_types": "Allowed Upload Mime Types (comma separated)"
}

View File

@ -0,0 +1,3 @@
{
"live_chat_tester": "Live Chat Tester"
}

View File

@ -0,0 +1,23 @@
{
"verification_token": "Jeton de vérification",
"allowed_domains": "Domaines autorisés",
"start_button": "Activer `Démarrer`",
"input_disabled": "Désactiver la saisie",
"persistent_menu": "Afficher le menu persistent",
"greeting_message": "Message de bienvenue",
"theme_color": "Thème du widget",
"theme_color_options": {
"orange": "Orange",
"red": "Rouge",
"green": "Vert",
"blue": "Bleu",
"dark": "Sombre"
},
"window_title": "Titre de la fenêtre de chat",
"avatar_url": "Avatar du chatbot (URL)",
"show_emoji": "Activer le sélecteur d'Emojis",
"show_file": "Activer l'upload de fichiers",
"show_location": "Activer le partage de géolocalisation",
"allowed_upload_size": "Taille maximale de téléchargement (en octets)",
"allowed_upload_types": "Types MIME autorisés pour le téléchargement (séparés par des virgules)"
}

View File

@ -0,0 +1,3 @@
{
"live_chat_tester": "Testeur Live Chat"
}

View File

@ -17,11 +17,10 @@ import { MenuService } from '@/cms/services/menu.service';
import { I18nService } from '@/i18n/services/i18n.service'; import { I18nService } from '@/i18n/services/i18n.service';
import { LoggerService } from '@/logger/logger.service'; import { LoggerService } from '@/logger/logger.service';
import { NlpService } from '@/nlp/services/nlp.service'; import { NlpService } from '@/nlp/services/nlp.service';
import { SettingCreateDto } from '@/setting/dto/setting.dto';
import { SettingService } from '@/setting/services/setting.service'; import { SettingService } from '@/setting/services/setting.service';
import { WebsocketGateway } from '@/websocket/websocket.gateway'; import { WebsocketGateway } from '@/websocket/websocket.gateway';
import OfflineHandler from '../offline/index.channel'; import BaseWebChannelHandler from '../offline/base-web-channel';
import { import {
DEFAULT_LIVE_CHAT_TEST_SETTINGS, DEFAULT_LIVE_CHAT_TEST_SETTINGS,
@ -29,9 +28,9 @@ import {
} from './settings'; } from './settings';
@Injectable() @Injectable()
export default class LiveChatTesterHandler extends OfflineHandler { export default class LiveChatTesterHandler extends BaseWebChannelHandler<
protected settings: SettingCreateDto[] = DEFAULT_LIVE_CHAT_TEST_SETTINGS; typeof LIVE_CHAT_TEST_CHANNEL_NAME
> {
constructor( constructor(
settingService: SettingService, settingService: SettingService,
channelService: ChannelService, channelService: ChannelService,
@ -46,6 +45,8 @@ export default class LiveChatTesterHandler extends OfflineHandler {
websocketGateway: WebsocketGateway, websocketGateway: WebsocketGateway,
) { ) {
super( super(
LIVE_CHAT_TEST_CHANNEL_NAME,
DEFAULT_LIVE_CHAT_TEST_SETTINGS,
settingService, settingService,
channelService, channelService,
nlpService, nlpService,
@ -59,12 +60,4 @@ export default class LiveChatTesterHandler extends OfflineHandler {
websocketGateway, websocketGateway,
); );
} }
/**
* Returns the channel's name
* @returns {String}
*/
getChannel() {
return LIVE_CHAT_TEST_CHANNEL_NAME;
}
} }

View File

@ -0,0 +1,18 @@
import {
DEFAULT_LIVE_CHAT_TEST_SETTINGS,
LIVE_CHAT_TEST_CHANNEL_NAME,
} from './settings';
declare global {
interface Settings
extends SettingTree<typeof DEFAULT_LIVE_CHAT_TEST_SETTINGS> {}
}
declare module '@nestjs/event-emitter' {
interface IHookSettingsGroupLabelOperationMap {
[name: HyphenToUnderscore<typeof LIVE_CHAT_TEST_CHANNEL_NAME>]: TDefinition<
object,
SettingObject<typeof DEFAULT_LIVE_CHAT_TEST_SETTINGS>
>;
}
}

View File

@ -6,99 +6,89 @@
* 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). * 2. All derivative works must include clear attribution to the original creator and software, Hexastack and Hexabot, in a prominent location (e.g., in the software's "About" section, documentation, and README file).
*/ */
import { ChannelSetting } from '@/channel/types';
import { config } from '@/config'; import { config } from '@/config';
import { SettingCreateDto } from '@/setting/dto/setting.dto';
import { SettingType } from '@/setting/schemas/types'; import { SettingType } from '@/setting/schemas/types';
import { Offline } from '../offline/types'; import { Offline } from '../offline/types';
export const LIVE_CHAT_TEST_CHANNEL_NAME = 'live-chat-tester'; export const LIVE_CHAT_TEST_CHANNEL_NAME = 'live-chat-tester';
export const DEFAULT_LIVE_CHAT_TEST_SETTINGS: SettingCreateDto[] = [ export const LIVE_CHAT_TEST_GROUP_NAME = 'live_chat_tester';
export const DEFAULT_LIVE_CHAT_TEST_SETTINGS = [
{ {
group: LIVE_CHAT_TEST_CHANNEL_NAME, group: LIVE_CHAT_TEST_GROUP_NAME,
label: Offline.SettingLabel.verification_token, label: Offline.SettingLabel.verification_token,
value: 'test', value: 'test',
type: SettingType.text, type: SettingType.text,
weight: 2,
}, },
{ {
group: LIVE_CHAT_TEST_CHANNEL_NAME, group: LIVE_CHAT_TEST_GROUP_NAME,
label: Offline.SettingLabel.allowed_domains, label: Offline.SettingLabel.allowed_domains,
value: config.frontendPath, value: config.frontendPath,
type: SettingType.text, type: SettingType.text,
weight: 3,
}, },
{ {
group: LIVE_CHAT_TEST_CHANNEL_NAME, group: LIVE_CHAT_TEST_GROUP_NAME,
label: Offline.SettingLabel.start_button, label: Offline.SettingLabel.start_button,
value: true, value: true,
type: SettingType.checkbox, type: SettingType.checkbox,
weight: 4,
}, },
{ {
group: LIVE_CHAT_TEST_CHANNEL_NAME, group: LIVE_CHAT_TEST_GROUP_NAME,
label: Offline.SettingLabel.input_disabled, label: Offline.SettingLabel.input_disabled,
value: false, value: false,
type: SettingType.checkbox, type: SettingType.checkbox,
weight: 5,
}, },
{ {
group: LIVE_CHAT_TEST_CHANNEL_NAME, group: LIVE_CHAT_TEST_GROUP_NAME,
label: Offline.SettingLabel.persistent_menu, label: Offline.SettingLabel.persistent_menu,
value: true, value: true,
type: SettingType.checkbox, type: SettingType.checkbox,
weight: 6,
}, },
{ {
group: LIVE_CHAT_TEST_CHANNEL_NAME, group: LIVE_CHAT_TEST_GROUP_NAME,
label: Offline.SettingLabel.greeting_message, label: Offline.SettingLabel.greeting_message,
value: 'Welcome! Ready to start a conversation with our chatbot?', value: 'Welcome! Ready to start a conversation with our chatbot?',
type: SettingType.textarea, type: SettingType.textarea,
weight: 7,
}, },
{ {
group: LIVE_CHAT_TEST_CHANNEL_NAME, group: LIVE_CHAT_TEST_GROUP_NAME,
label: Offline.SettingLabel.theme_color, label: Offline.SettingLabel.theme_color,
value: 'teal', value: 'teal',
type: SettingType.select, type: SettingType.select,
options: ['teal', 'orange', 'red', 'green', 'blue', 'dark'], options: ['teal', 'orange', 'red', 'green', 'blue', 'dark'],
weight: 8,
}, },
{ {
group: LIVE_CHAT_TEST_CHANNEL_NAME, group: LIVE_CHAT_TEST_GROUP_NAME,
label: Offline.SettingLabel.show_emoji, label: Offline.SettingLabel.show_emoji,
value: true, value: true,
type: SettingType.checkbox, type: SettingType.checkbox,
weight: 11,
}, },
{ {
group: LIVE_CHAT_TEST_CHANNEL_NAME, group: LIVE_CHAT_TEST_GROUP_NAME,
label: Offline.SettingLabel.show_file, label: Offline.SettingLabel.show_file,
value: true, value: true,
type: SettingType.checkbox, type: SettingType.checkbox,
weight: 12,
}, },
{ {
group: LIVE_CHAT_TEST_CHANNEL_NAME, group: LIVE_CHAT_TEST_GROUP_NAME,
label: Offline.SettingLabel.show_location, label: Offline.SettingLabel.show_location,
value: true, value: true,
type: SettingType.checkbox, type: SettingType.checkbox,
weight: 13,
}, },
{ {
group: LIVE_CHAT_TEST_CHANNEL_NAME, group: LIVE_CHAT_TEST_GROUP_NAME,
label: Offline.SettingLabel.allowed_upload_size, label: Offline.SettingLabel.allowed_upload_size,
value: 2500000, value: 2500000,
type: SettingType.number, type: SettingType.number,
weight: 14,
}, },
{ {
group: LIVE_CHAT_TEST_CHANNEL_NAME, group: LIVE_CHAT_TEST_GROUP_NAME,
label: Offline.SettingLabel.allowed_upload_types, label: Offline.SettingLabel.allowed_upload_types,
value: value:
'audio/mpeg,audio/x-ms-wma,audio/vnd.rn-realaudio,audio/x-wav,image/gif,image/jpeg,image/png,image/tiff,image/vnd.microsoft.icon,image/vnd.djvu,image/svg+xml,text/css,text/csv,text/html,text/plain,text/xml,video/mpeg,video/mp4,video/quicktime,video/x-ms-wmv,video/x-msvideo,video/x-flv,video/web,application/msword,application/vnd.ms-powerpoint,application/pdf,application/vnd.ms-excel,application/vnd.oasis.opendocument.presentation,application/vnd.oasis.opendocument.tex,application/vnd.oasis.opendocument.spreadsheet,application/vnd.oasis.opendocument.graphics,application/vnd.openxmlformats-officedocument.spreadsheetml.sheet,application/vnd.openxmlformats-officedocument.presentationml.presentation,application/vnd.openxmlformats-officedocument.wordprocessingml.document', 'audio/mpeg,audio/x-ms-wma,audio/vnd.rn-realaudio,audio/x-wav,image/gif,image/jpeg,image/png,image/tiff,image/vnd.microsoft.icon,image/vnd.djvu,image/svg+xml,text/css,text/csv,text/html,text/plain,text/xml,video/mpeg,video/mp4,video/quicktime,video/x-ms-wmv,video/x-msvideo,video/x-flv,video/web,application/msword,application/vnd.ms-powerpoint,application/pdf,application/vnd.ms-excel,application/vnd.oasis.opendocument.presentation,application/vnd.oasis.opendocument.tex,application/vnd.oasis.opendocument.spreadsheet,application/vnd.oasis.opendocument.graphics,application/vnd.openxmlformats-officedocument.spreadsheetml.sheet,application/vnd.openxmlformats-officedocument.presentationml.presentation,application/vnd.openxmlformats-officedocument.wordprocessingml.document',
type: SettingType.textarea, type: SettingType.textarea,
weight: 15,
}, },
]; ] as const satisfies ChannelSetting[];

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,23 @@
{
"verification_token": "Verification Token",
"allowed_domains": "Allowed Domains",
"start_button": "Enable `Get Started`",
"input_disabled": "Disable Input",
"persistent_menu": "Display Persistent Menu",
"greeting_message": "Greeting Message",
"theme_color": "Widget Theme",
"theme_color_options": {
"orange": "Orange",
"red": "Red",
"green": "Green",
"blue": "Blue",
"dark": "Dark"
},
"window_title": "Chat Window Title",
"avatar_url": "Chatbot Avatar URL",
"show_emoji": "Enable Emoji Picker",
"show_file": "Enable Attachment Uploader",
"show_location": "Enable Geolocation Share",
"allowed_upload_size": "Max Upload Size (in bytes)",
"allowed_upload_types": "Allowed Upload Mime Types (comma separated)"
}

View File

@ -0,0 +1,3 @@
{
"offline": "Canal Web"
}

View File

@ -0,0 +1,23 @@
{
"verification_token": "Jeton de vérification",
"allowed_domains": "Domaines autorisés",
"start_button": "Activer `Démarrer`",
"input_disabled": "Désactiver la saisie",
"persistent_menu": "Afficher le menu persistent",
"greeting_message": "Message de bienvenue",
"theme_color": "Thème du widget",
"theme_color_options": {
"orange": "Orange",
"red": "Rouge",
"green": "Vert",
"blue": "Bleu",
"dark": "Sombre"
},
"window_title": "Titre de la fenêtre de chat",
"avatar_url": "Avatar du chatbot (URL)",
"show_emoji": "Activer le sélecteur d'Emojis",
"show_file": "Activer l'upload de fichiers",
"show_location": "Activer le partage de géolocalisation",
"allowed_upload_size": "Taille maximale de téléchargement (en octets)",
"allowed_upload_types": "Types MIME autorisés pour le téléchargement (séparés par des virgules)"
}

View File

@ -0,0 +1,3 @@
{
"live_chat_tester": "Testeur Live Chat"
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,14 @@
import { DEFAULT_OFFLINE_SETTINGS, OFFLINE_CHANNEL_NAME } from './settings';
declare global {
interface Settings extends SettingTree<typeof DEFAULT_OFFLINE_SETTINGS> {}
}
declare module '@nestjs/event-emitter' {
interface IHookSettingsGroupLabelOperationMap {
[key: HyphenToUnderscore<typeof OFFLINE_CHANNEL_NAME>]: TDefinition<
object,
SettingObject<typeof DEFAULT_OFFLINE_SETTINGS>
>;
}
}

View File

@ -6,112 +6,100 @@
* 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). * 2. All derivative works must include clear attribution to the original creator and software, Hexastack and Hexabot, in a prominent location (e.g., in the software's "About" section, documentation, and README file).
*/ */
import { SettingCreateDto } from '@/setting/dto/setting.dto'; import { ChannelSetting } from '@/channel/types';
import { SettingType } from '@/setting/schemas/types'; import { SettingType } from '@/setting/schemas/types';
import { Offline } from './types'; import { Offline } from './types';
export const OFFLINE_CHANNEL_NAME = 'offline'; export const OFFLINE_CHANNEL_NAME = 'offline' as const;
export const DEFAULT_OFFLINE_SETTINGS: SettingCreateDto[] = [ export const OFFLINE_GROUP_NAME = OFFLINE_CHANNEL_NAME;
export const DEFAULT_OFFLINE_SETTINGS = [
{ {
group: OFFLINE_CHANNEL_NAME, group: OFFLINE_GROUP_NAME,
label: Offline.SettingLabel.verification_token, label: Offline.SettingLabel.verification_token,
value: 'token123', value: 'token123',
type: SettingType.secret, type: SettingType.secret,
weight: 2,
}, },
{ {
group: OFFLINE_CHANNEL_NAME, group: OFFLINE_GROUP_NAME,
label: Offline.SettingLabel.allowed_domains, label: Offline.SettingLabel.allowed_domains,
value: 'http://localhost:8080,http://localhost:4000', value: 'http://localhost:8080,http://localhost:4000',
type: SettingType.text, type: SettingType.text,
weight: 3,
}, },
{ {
group: OFFLINE_CHANNEL_NAME, group: OFFLINE_GROUP_NAME,
label: Offline.SettingLabel.start_button, label: Offline.SettingLabel.start_button,
value: true, value: true,
type: SettingType.checkbox, type: SettingType.checkbox,
weight: 4,
}, },
{ {
group: OFFLINE_CHANNEL_NAME, group: OFFLINE_GROUP_NAME,
label: Offline.SettingLabel.input_disabled, label: Offline.SettingLabel.input_disabled,
value: false, value: false,
type: SettingType.checkbox, type: SettingType.checkbox,
weight: 5,
}, },
{ {
group: OFFLINE_CHANNEL_NAME, group: OFFLINE_GROUP_NAME,
label: Offline.SettingLabel.persistent_menu, label: Offline.SettingLabel.persistent_menu,
value: true, value: true,
type: SettingType.checkbox, type: SettingType.checkbox,
weight: 6,
}, },
{ {
group: OFFLINE_CHANNEL_NAME, group: OFFLINE_GROUP_NAME,
label: Offline.SettingLabel.greeting_message, label: Offline.SettingLabel.greeting_message,
value: 'Welcome! Ready to start a conversation with our chatbot?', value: 'Welcome! Ready to start a conversation with our chatbot?',
type: SettingType.textarea, type: SettingType.textarea,
weight: 7,
}, },
{ {
group: OFFLINE_CHANNEL_NAME, group: OFFLINE_GROUP_NAME,
label: Offline.SettingLabel.theme_color, label: Offline.SettingLabel.theme_color,
value: 'teal', value: 'teal',
type: SettingType.select, type: SettingType.select,
options: ['teal', 'orange', 'red', 'green', 'blue', 'dark'], options: ['teal', 'orange', 'red', 'green', 'blue', 'dark'],
weight: 8,
}, },
{ {
group: OFFLINE_CHANNEL_NAME, group: OFFLINE_GROUP_NAME,
label: Offline.SettingLabel.window_title, label: Offline.SettingLabel.window_title,
value: 'Widget Title', value: 'Widget Title',
type: SettingType.text, type: SettingType.text,
weight: 9,
}, },
{ {
group: OFFLINE_CHANNEL_NAME, group: OFFLINE_GROUP_NAME,
label: Offline.SettingLabel.avatar_url, label: Offline.SettingLabel.avatar_url,
value: 'https://eu.ui-avatars.com/api/?name=Hexa+Bot&size=64', value: 'https://eu.ui-avatars.com/api/?name=Hexa+Bot&size=64',
type: SettingType.text, type: SettingType.text,
weight: 10,
}, },
{ {
group: OFFLINE_CHANNEL_NAME, group: OFFLINE_GROUP_NAME,
label: Offline.SettingLabel.show_emoji, label: Offline.SettingLabel.show_emoji,
value: true, value: true,
type: SettingType.checkbox, type: SettingType.checkbox,
weight: 11,
}, },
{ {
group: OFFLINE_CHANNEL_NAME, group: OFFLINE_GROUP_NAME,
label: Offline.SettingLabel.show_file, label: Offline.SettingLabel.show_file,
value: true, value: true,
type: SettingType.checkbox, type: SettingType.checkbox,
weight: 12,
}, },
{ {
group: OFFLINE_CHANNEL_NAME, group: OFFLINE_GROUP_NAME,
label: Offline.SettingLabel.show_location, label: Offline.SettingLabel.show_location,
value: true, value: true,
type: SettingType.checkbox, type: SettingType.checkbox,
weight: 13,
}, },
{ {
group: OFFLINE_CHANNEL_NAME, group: OFFLINE_GROUP_NAME,
label: Offline.SettingLabel.allowed_upload_size, label: Offline.SettingLabel.allowed_upload_size,
value: 2500000, value: 2500000,
type: SettingType.number, type: SettingType.number,
weight: 14,
}, },
{ {
group: OFFLINE_CHANNEL_NAME, group: OFFLINE_GROUP_NAME,
label: Offline.SettingLabel.allowed_upload_types, label: Offline.SettingLabel.allowed_upload_types,
value: value:
'audio/mpeg,audio/x-ms-wma,audio/vnd.rn-realaudio,audio/x-wav,image/gif,image/jpeg,image/png,image/tiff,image/vnd.microsoft.icon,image/vnd.djvu,image/svg+xml,text/css,text/csv,text/html,text/plain,text/xml,video/mpeg,video/mp4,video/quicktime,video/x-ms-wmv,video/x-msvideo,video/x-flv,video/web,application/msword,application/vnd.ms-powerpoint,application/pdf,application/vnd.ms-excel,application/vnd.oasis.opendocument.presentation,application/vnd.oasis.opendocument.tex,application/vnd.oasis.opendocument.spreadsheet,application/vnd.oasis.opendocument.graphics,application/vnd.openxmlformats-officedocument.spreadsheetml.sheet,application/vnd.openxmlformats-officedocument.presentationml.presentation,application/vnd.openxmlformats-officedocument.wordprocessingml.document', 'audio/mpeg,audio/x-ms-wma,audio/vnd.rn-realaudio,audio/x-wav,image/gif,image/jpeg,image/png,image/tiff,image/vnd.microsoft.icon,image/vnd.djvu,image/svg+xml,text/css,text/csv,text/html,text/plain,text/xml,video/mpeg,video/mp4,video/quicktime,video/x-ms-wmv,video/x-msvideo,video/x-flv,video/web,application/msword,application/vnd.ms-powerpoint,application/pdf,application/vnd.ms-excel,application/vnd.oasis.opendocument.presentation,application/vnd.oasis.opendocument.tex,application/vnd.oasis.opendocument.spreadsheet,application/vnd.oasis.opendocument.graphics,application/vnd.openxmlformats-officedocument.spreadsheetml.sheet,application/vnd.openxmlformats-officedocument.presentationml.presentation,application/vnd.openxmlformats-officedocument.wordprocessingml.document',
type: SettingType.textarea, type: SettingType.textarea,
weight: 15,
}, },
]; ] as const satisfies ChannelSetting<typeof OFFLINE_CHANNEL_NAME>[];

View File

@ -19,7 +19,7 @@ import {
} from '@/chat/schemas/types/message'; } from '@/chat/schemas/types/message';
import { Payload } from '@/chat/schemas/types/quick-reply'; import { Payload } from '@/chat/schemas/types/quick-reply';
import OfflineHandler from './index.channel'; import BaseWebChannelHandler from './base-web-channel';
import { Offline } from './types'; import { Offline } from './types';
type OfflineEventAdapter = type OfflineEventAdapter =
@ -66,10 +66,9 @@ type OfflineEventAdapter =
raw: Offline.IncomingMessage<Offline.IncomingAttachmentMessage>; raw: Offline.IncomingMessage<Offline.IncomingAttachmentMessage>;
}; };
export default class OfflineEventWrapper extends EventWrapper< export default class OfflineEventWrapper<
OfflineEventAdapter, T extends BaseWebChannelHandler<string> = BaseWebChannelHandler<string>,
Offline.Event > extends EventWrapper<OfflineEventAdapter, Offline.Event> {
> {
/** /**
* Constructor : channel's event wrapper * Constructor : channel's event wrapper
* *
@ -77,7 +76,7 @@ export default class OfflineEventWrapper extends EventWrapper<
* @param event - The message event received * @param event - The message event received
* @param channelData - Channel's specific extra data {isSocket, ipAddress} * @param channelData - Channel's specific extra data {isSocket, ipAddress}
*/ */
constructor(handler: OfflineHandler, event: Offline.Event, channelData: any) { constructor(handler: T, event: Offline.Event, channelData: any) {
super(handler, event, channelData); super(handler, event, channelData);
} }

View File

@ -152,9 +152,12 @@ describe('NLP Default Helper', () => {
const nlp = nlpService.getNLP(); const nlp = nlpService.getNLP();
const results = nlp.bestGuess(nlpParseResult, true); const results = nlp.bestGuess(nlpParseResult, true);
const settings = await settingService.getSettings(); const settings = await settingService.getSettings();
const threshold = settings.nlp_settings.threshold;
const thresholdGuess = { const thresholdGuess = {
entities: nlpBestGuess.entities.filter( entities: nlpBestGuess.entities.filter(
(g) => g.confidence > parseFloat(settings.nlp_settings.threshold), (g) =>
g.confidence >
(typeof threshold === 'string' ? parseFloat(threshold) : threshold),
), ),
}; };
expect(results).toEqual(thresholdGuess); expect(results).toEqual(thresholdGuess);

View File

@ -135,7 +135,11 @@ export default class DefaultNlpHelper extends BaseNlpHelper {
entities: nlp.entities.slice(), entities: nlp.entities.slice(),
}; };
if (threshold) { if (threshold) {
minConfidence = Number.parseFloat(this.settings.threshold); const threshold = this.settings.threshold;
minConfidence =
typeof threshold === 'string'
? Number.parseFloat(threshold)
: threshold;
guess.entities = guess.entities guess.entities = guess.entities
.map((e) => { .map((e) => {
e.confidence = e.confidence =

View File

@ -0,0 +1,28 @@
/*
* Copyright © 2024 Hexastack. All rights reserved.
*
* Licensed under the GNU Affero General Public License v3.0 (AGPLv3) with the following additional terms:
* 1. The name "Hexabot" is a trademark of Hexastack. You may not use this name in derivative works without express written permission.
* 2. All derivative works must include clear attribution to the original creator and software, Hexastack and Hexabot, in a prominent location (e.g., in the software's "About" section, documentation, and README file).
*/
import { Controller, Get, UseInterceptors } from '@nestjs/common';
import { CsrfInterceptor } from '@/interceptors/csrf.interceptor';
import { I18nService } from '../services/i18n.service';
@UseInterceptors(CsrfInterceptor)
@Controller('i18n')
export class I18nController {
constructor(private readonly i18nService: I18nService) {}
/**
* Retrieves translations of all the installed extensions.
* @returns An nested object that holds the translations grouped by language and extension name.
*/
@Get()
getTranslations() {
return this.i18nService.getExtensionI18nTranslations();
}
}

View File

@ -46,10 +46,10 @@ export class LanguageController extends BaseController<Language> {
} }
/** /**
* Retrieves a paginated list of categories based on provided filters and pagination settings. * Retrieves a paginated list of languages based on provided filters and pagination settings.
* @param pageQuery - The pagination settings. * @param pageQuery - The pagination settings.
* @param filters - The filters to apply to the language search. * @param filters - The filters to apply to the language search.
* @returns A Promise that resolves to a paginated list of categories. * @returns A Promise that resolves to a paginated list of languages.
*/ */
@Get() @Get()
async findPage( async findPage(
@ -61,8 +61,8 @@ export class LanguageController extends BaseController<Language> {
} }
/** /**
* Counts the filtered number of categories. * Counts the filtered number of languages.
* @returns A promise that resolves to an object representing the filtered number of categories. * @returns A promise that resolves to an object representing the filtered number of languages.
*/ */
@Get('count') @Get('count')
async filterCount( async filterCount(

View File

@ -26,6 +26,7 @@ import { Observable } from 'rxjs';
import { ChatModule } from '@/chat/chat.module'; import { ChatModule } from '@/chat/chat.module';
import { I18nController } from './controllers/i18n.controller';
import { LanguageController } from './controllers/language.controller'; import { LanguageController } from './controllers/language.controller';
import { TranslationController } from './controllers/translation.controller'; import { TranslationController } from './controllers/translation.controller';
import { LanguageRepository } from './repositories/language.repository'; import { LanguageRepository } from './repositories/language.repository';
@ -62,6 +63,7 @@ export class I18nModule extends NativeI18nModule {
controllers: (controllers || []).concat([ controllers: (controllers || []).concat([
LanguageController, LanguageController,
TranslationController, TranslationController,
I18nController,
]), ]),
providers: providers.concat([ providers: providers.concat([
I18nService, I18nService,

View File

@ -6,8 +6,13 @@
* 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). * 2. All derivative works must include clear attribution to the original creator and software, Hexastack and Hexabot, in a prominent location (e.g., in the software's "About" section, documentation, and README file).
*/ */
import { Injectable } from '@nestjs/common'; import { promises as fs } from 'fs';
import * as path from 'path';
import { Injectable, OnModuleInit } from '@nestjs/common';
import { import {
I18nJsonLoader,
I18nTranslation,
I18nService as NativeI18nService, I18nService as NativeI18nService,
Path, Path,
PathValue, PathValue,
@ -19,11 +24,22 @@ import { config } from '@/config';
import { Translation } from '@/i18n/schemas/translation.schema'; import { Translation } from '@/i18n/schemas/translation.schema';
@Injectable() @Injectable()
export class I18nService< export class I18nService<K = Record<string, unknown>>
K = Record<string, unknown>, extends NativeI18nService<K>
> extends NativeI18nService<K> { implements OnModuleInit
{
private dynamicTranslations: Record<string, Record<string, string>> = {}; private dynamicTranslations: Record<string, Record<string, string>> = {};
private extensionTranslations: I18nTranslation = {};
onModuleInit() {
this.loadExtensionI18nTranslations();
}
getExtensionI18nTranslations() {
return this.extensionTranslations;
}
t<P extends Path<K> = any, R = PathValue<K, P>>( t<P extends Path<K> = any, R = PathValue<K, P>>(
key: P, key: P,
options?: TranslateOptions, options?: TranslateOptions,
@ -66,4 +82,48 @@ export class I18nService<
return acc; return acc;
}, this.dynamicTranslations); }, this.dynamicTranslations);
} }
async loadExtensionI18nTranslations() {
const extensionsDir = path.join(
__dirname,
'..',
'..',
'extensions',
'channels',
);
try {
const extensionFolders = await fs.readdir(extensionsDir, {
withFileTypes: true,
});
for (const folder of extensionFolders) {
if (folder.isDirectory()) {
const i18nPath = path.join(extensionsDir, folder.name, 'i18n');
const extensionName = folder.name.replaceAll('-', '_');
try {
// Check if the i18n directory exists
await fs.access(i18nPath);
// Load and merge translations
const i18nLoader = new I18nJsonLoader({ path: i18nPath });
const translations = await i18nLoader.load();
for (const lang in translations) {
if (!this.extensionTranslations[lang]) {
this.extensionTranslations[lang] = {
[extensionName]: translations[lang],
};
} else {
this.extensionTranslations[lang][extensionName] =
translations[lang];
}
}
} catch (error) {
// If the i18n folder does not exist or error in reading, skip this folder
}
}
}
} catch (error) {
throw new Error(`Failed to read extensions directory: ${error.message}`);
}
}
} }

View File

@ -10,7 +10,6 @@ import { EventEmitter2 } from '@nestjs/event-emitter';
import { Test, TestingModule } from '@nestjs/testing'; import { Test, TestingModule } from '@nestjs/testing';
import { I18nService } from '@/i18n/services/i18n.service'; import { I18nService } from '@/i18n/services/i18n.service';
import { Settings } from '@/setting/schemas/types';
import { SettingService } from '@/setting/services/setting.service'; import { SettingService } from '@/setting/services/setting.service';
import { Block } from '../../chat/schemas/block.schema'; import { Block } from '../../chat/schemas/block.schema';
@ -64,7 +63,7 @@ describe('TranslationService', () => {
global_fallback: true, global_fallback: true,
fallback_message: ['Global fallback message'], fallback_message: ['Global fallback message'],
}, },
} as Settings), }),
}, },
}, },
{ {

10
api/src/index.d.ts vendored
View File

@ -9,9 +9,9 @@
import 'mongoose'; import 'mongoose';
import { SubscriberStub } from './chat/schemas/subscriber.schema'; import { SubscriberStub } from './chat/schemas/subscriber.schema';
import { import {
WithoutGenericAny,
RecursivePartial,
ObjectWithNestedKeys, ObjectWithNestedKeys,
RecursivePartial,
WithoutGenericAny,
} from './utils/types/filter.types'; } from './utils/types/filter.types';
type TOmitId<T> = Omit<T, 'id'>; type TOmitId<T> = Omit<T, 'id'>;
@ -63,3 +63,9 @@ declare module 'mongoose' {
type THydratedDocument<T> = TOmitId<HydratedDocument<T>>; type THydratedDocument<T> = TOmitId<HydratedDocument<T>>;
} }
declare global {
type HyphenToUnderscore<S extends string> = S extends `${infer P}-${infer Q}`
? `${P}_${HyphenToUnderscore<Q>}`
: S;
}

View File

@ -31,7 +31,6 @@ import {
NlpValueDocument, NlpValueDocument,
NlpValueFull, NlpValueFull,
} from '@/nlp/schemas/nlp-value.schema'; } from '@/nlp/schemas/nlp-value.schema';
import { Settings } from '@/setting/schemas/types';
import { NlpEntityService } from '../services/nlp-entity.service'; import { NlpEntityService } from '../services/nlp-entity.service';
import { NlpSampleService } from '../services/nlp-sample.service'; import { NlpSampleService } from '../services/nlp-sample.service';

View File

@ -9,7 +9,7 @@
import { BlockCreateDto } from '@/chat/dto/block.dto'; import { BlockCreateDto } from '@/chat/dto/block.dto';
import { Block } from '@/chat/schemas/block.schema'; import { Block } from '@/chat/schemas/block.schema';
import { Conversation } from '@/chat/schemas/conversation.schema'; import { Conversation } from '@/chat/schemas/conversation.schema';
import { Setting } from '@/setting/schemas/setting.schema'; import { SettingCreateDto } from '@/setting/dto/setting.dto';
export enum PluginType { export enum PluginType {
event = 'event', event = 'event',
@ -22,7 +22,7 @@ export interface CustomBlocks {}
type ChannelEvent = any; type ChannelEvent = any;
type BlockAttrs = Partial<BlockCreateDto> & { name: string }; type BlockAttrs = Partial<BlockCreateDto> & { name: string };
export type PluginSetting = Omit<Setting, 'id' | 'createdAt' | 'updatedAt'>; export type PluginSetting = SettingCreateDto;
export type PluginBlockTemplate = Omit< export type PluginBlockTemplate = Omit<
BlockAttrs, BlockAttrs,

View File

@ -22,7 +22,7 @@ import { nlpEntityModels } from './nlp/seeds/nlp-entity.seed-model';
import { NlpValueSeeder } from './nlp/seeds/nlp-value.seed'; import { NlpValueSeeder } from './nlp/seeds/nlp-value.seed';
import { nlpValueModels } from './nlp/seeds/nlp-value.seed-model'; import { nlpValueModels } from './nlp/seeds/nlp-value.seed-model';
import { SettingSeeder } from './setting/seeds/setting.seed'; import { SettingSeeder } from './setting/seeds/setting.seed';
import { settingModels } from './setting/seeds/setting.seed-model'; import { DEFAULT_SETTINGS } from './setting/seeds/setting.seed-model';
import { ModelSeeder } from './user/seeds/model.seed'; import { ModelSeeder } from './user/seeds/model.seed';
import { modelModels } from './user/seeds/model.seed-model'; import { modelModels } from './user/seeds/model.seed-model';
import { PermissionSeeder } from './user/seeds/permission.seed'; import { PermissionSeeder } from './user/seeds/permission.seed';
@ -106,7 +106,7 @@ export async function seedDatabase(app: INestApplicationContext) {
} }
// Seed users // Seed users
try { try {
await settingSeeder.seed(settingModels); await settingSeeder.seed(DEFAULT_SETTINGS);
} catch (e) { } catch (e) {
logger.error('Unable to seed the database with settings!'); logger.error('Unable to seed the database with settings!');
throw e; throw e;

View File

@ -11,25 +11,30 @@ import {
IsArray, IsArray,
IsIn, IsIn,
IsNotEmpty, IsNotEmpty,
IsString,
IsOptional, IsOptional,
IsString,
} from 'class-validator'; } from 'class-validator';
import { SettingType } from '../schemas/types'; import { SettingType } from '../schemas/types';
export class SettingCreateDto { export class SettingCreateDto {
@ApiProperty({ description: 'Setting group of setting', type: String }) @ApiProperty({ description: 'Setting group', type: String })
@IsNotEmpty() @IsNotEmpty()
@IsString() @IsString()
group: string; group: string;
@ApiProperty({ description: 'Setting label of setting', type: String }) @ApiProperty({ description: 'Setting subgroup', type: String })
@IsOptional()
@IsString()
subgroup?: string;
@ApiProperty({ description: 'Setting label (system name)', type: String })
@IsNotEmpty() @IsNotEmpty()
@IsString() @IsString()
label: string; label: string;
@ApiProperty({ @ApiProperty({
description: 'Setting type of the setting', description: 'Setting type',
enum: [ enum: [
'text', 'text',
'multiple_text', 'multiple_text',
@ -44,12 +49,12 @@ export class SettingCreateDto {
@IsIn(Object.values(SettingType)) @IsIn(Object.values(SettingType))
type: SettingType; type: SettingType;
@ApiProperty({ description: 'Setting value of the setting' }) @ApiProperty({ description: 'Setting value' })
@IsNotEmpty() @IsNotEmpty()
value: any; value: any;
@ApiPropertyOptional({ @ApiPropertyOptional({
description: 'Setting options', description: 'Setting options (required when type is select)',
isArray: true, isArray: true,
type: Array, type: Array,
}) })

31
api/src/setting/index.d.ts vendored Normal file
View File

@ -0,0 +1,31 @@
import { DEFAULT_SETTINGS } from './seeds/setting.seed-model';
declare global {
type TNativeType<T> = T extends string
? string
: T extends number
? number
: T extends boolean
? boolean
: T extends Array<infer U>
? TNativeType<U>[]
: T extends object
? { [K in keyof T]: TNativeType<T[K]> }
: T;
type SettingObject<
T extends Omit<Setting, 'id' | 'createdAt' | 'updatedAt'>[],
> = {
[K in T[number] as K['label']]: TNativeType<K['value']>;
};
type SettingTree<
T extends Omit<Setting, 'id' | 'createdAt' | 'updatedAt'>[],
> = {
[G in T[number] as G['group']]: {
[K in T[number] as K['label']]: TNativeType<K['value']>;
};
};
interface Settings extends SettingTree<typeof DEFAULT_SETTINGS> {}
}

View File

@ -7,6 +7,7 @@
*/ */
import { ModelDefinition, Prop, Schema, SchemaFactory } from '@nestjs/mongoose'; import { ModelDefinition, Prop, Schema, SchemaFactory } from '@nestjs/mongoose';
import { Transform } from 'class-transformer';
import { IsArray, IsIn } from 'class-validator'; import { IsArray, IsIn } from 'class-validator';
import { BaseSchema } from '@/utils/generics/base-schema'; import { BaseSchema } from '@/utils/generics/base-schema';
@ -22,6 +23,13 @@ export class Setting extends BaseSchema {
}) })
group: string; group: string;
@Prop({
type: String,
default: '',
})
@Transform(({ obj }) => obj.subgroup || undefined)
subgroup?: string;
@Prop({ @Prop({
type: String, type: String,
required: true, required: true,

View File

@ -94,18 +94,3 @@ export type AnySetting =
| MultipleAttachmentSetting; | MultipleAttachmentSetting;
export type SettingDict = { [group: string]: Setting[] }; export type SettingDict = { [group: string]: Setting[] };
export type Settings = {
nlp_settings: {
threshold: string;
provider: string;
endpoint: string;
token: string;
};
contact: { [key: string]: string };
chatbot_settings: {
global_fallback: boolean;
fallback_message: string[];
fallback_block: string;
};
} & Record<string, any>;

View File

@ -9,7 +9,7 @@
import { SettingCreateDto } from '../dto/setting.dto'; import { SettingCreateDto } from '../dto/setting.dto';
import { SettingType } from '../schemas/types'; import { SettingType } from '../schemas/types';
export const settingModels: SettingCreateDto[] = [ export const DEFAULT_SETTINGS = [
{ {
group: 'chatbot_settings', group: 'chatbot_settings',
label: 'global_fallback', label: 'global_fallback',
@ -38,7 +38,7 @@ export const settingModels: SettingCreateDto[] = [
value: [ value: [
"Sorry but i didn't understand your request. Maybe you can check the menu", "Sorry but i didn't understand your request. Maybe you can check the menu",
"I'm really sorry but i don't quite understand what you are saying :(", "I'm really sorry but i don't quite understand what you are saying :(",
], ] as string[],
type: SettingType.multiple_text, type: SettingType.multiple_text,
weight: 3, weight: 3,
}, },
@ -146,4 +146,4 @@ export const settingModels: SettingCreateDto[] = [
type: SettingType.text, type: SettingType.text,
weight: 10, weight: 10,
}, },
]; ] as const satisfies SettingCreateDto[];

View File

@ -21,7 +21,6 @@ import { BaseService } from '@/utils/generics/base-service';
import { SettingCreateDto } from '../dto/setting.dto'; import { SettingCreateDto } from '../dto/setting.dto';
import { SettingRepository } from '../repositories/setting.repository'; import { SettingRepository } from '../repositories/setting.repository';
import { Setting } from '../schemas/setting.schema'; import { Setting } from '../schemas/setting.schema';
import { Settings } from '../schemas/types';
import { SettingSeeder } from '../seeds/setting.seed'; import { SettingSeeder } from '../seeds/setting.seed';
@Injectable() @Injectable()

View File

@ -53,7 +53,7 @@ export class Ability implements CanActivate {
if (user?.roles?.length) { if (user?.roles?.length) {
if ( if (
['/auth/logout', '/logout', '/auth/me', '/channel'].includes( ['/auth/logout', '/logout', '/auth/me', '/channel', '/i18n'].includes(
_parsedUrl.pathname, _parsedUrl.pathname,
) )
) { ) {

View File

@ -17,9 +17,11 @@
"strictBindCallApply": false, "strictBindCallApply": false,
"forceConsistentCasingInFileNames": false, "forceConsistentCasingInFileNames": false,
"noFallthroughCasesInSwitch": false, "noFallthroughCasesInSwitch": false,
"resolveJsonModule": true,
"esModuleInterop": true, "esModuleInterop": true,
"paths": { "paths": {
"@/*": ["src/*"] "@/*": ["src/*"]
} }
} },
"include": ["src/**/*.ts", "src/**/*.json"]
} }

View File

@ -1,28 +0,0 @@
{
"title": {
"live-chat-tester": "Live Chat Tester"
},
"label": {
"verification_token": "Verification Token",
"allowed_domains": "Allowed Domains",
"start_button": "Enable `Get Started`",
"input_disabled": "Disable Input",
"persistent_menu": "Display Persistent Menu",
"greeting_message": "Greeting Message",
"theme_color": "Widget Theme",
"theme_color_options": {
"orange": "Orange",
"red": "Red",
"green": "Green",
"blue": "Blue",
"dark": "Dark"
},
"window_title": "Chat Window Title",
"avatar_url": "Chatbot Avatar URL",
"show_emoji": "Enable Emoji Picker",
"show_file": "Enable Attachment Uploader",
"show_location": "Enable Geolocation Share",
"allowed_upload_size": "Max Upload Size (in bytes)",
"allowed_upload_types": "Allowed Upload Mime Types (comma separated)"
}
}

View File

@ -1,28 +0,0 @@
{
"title": {
"offline": "Web Channel"
},
"label": {
"verification_token": "Verification Token",
"allowed_domains": "Allowed Domains",
"start_button": "Enable `Get Started`",
"input_disabled": "Disable Input",
"persistent_menu": "Display Persistent Menu",
"greeting_message": "Greeting Message",
"theme_color": "Widget Theme",
"theme_color_options": {
"orange": "Orange",
"red": "Red",
"green": "Green",
"blue": "Blue",
"dark": "Dark"
},
"window_title": "Chat Window Title",
"avatar_url": "Chatbot Avatar URL",
"show_emoji": "Enable Emoji Picker",
"show_file": "Enable Attachment Uploader",
"show_location": "Enable Geolocation Share",
"allowed_upload_size": "Max Upload Size (in bytes)",
"allowed_upload_types": "Allowed Upload Mime Types (comma separated)"
}
}

View File

@ -1,28 +0,0 @@
{
"title": {
"live-chat-tester": "Testeur Live Chat"
},
"label": {
"verification_token": "Jeton de vérification",
"allowed_domains": "Domaines autorisés",
"start_button": "Activer `Démarrer`",
"input_disabled": "Désactiver la saisie",
"persistent_menu": "Afficher le menu persistent",
"greeting_message": "Message de bienvenue",
"theme_color": "Thème du widget",
"theme_color_options": {
"orange": "Orange",
"red": "Rouge",
"green": "Vert",
"blue": "Bleu",
"dark": "Sombre"
},
"window_title": "Titre de la fenêtre de chat",
"avatar_url": "Avatar du chatbot (URL)",
"show_emoji": "Activer le sélecteur d'Emojis",
"show_file": "Activer l'upload de fichiers",
"show_location": "Activer le partage de géolocalisation",
"allowed_upload_size": "Taille maximale de téléchargement (en octets)",
"allowed_upload_types": "Types MIME autorisés pour le téléchargement (séparés par des virgules)"
}
}

View File

@ -1,28 +0,0 @@
{
"title": {
"offline": "Canal Web"
},
"label": {
"verification_token": "Jeton de vérification",
"allowed_domains": "Domaines autorisés",
"start_button": "Activer `Démarrer`",
"input_disabled": "Désactiver la saisie",
"persistent_menu": "Afficher le menu persistent",
"greeting_message": "Message de bienvenue",
"theme_color": "Thème du widget",
"theme_color_options": {
"orange": "Orange",
"red": "Rouge",
"green": "Vert",
"blue": "Bleu",
"dark": "Sombre"
},
"window_title": "Titre de la fenêtre de chat",
"avatar_url": "Avatar du chatbot (URL)",
"show_emoji": "Activer le sélecteur d'Emojis",
"show_file": "Activer l'upload de fichiers",
"show_location": "Activer le partage de géolocalisation",
"allowed_upload_size": "Taille maximale de téléchargement (en octets)",
"allowed_upload_types": "Types MIME autorisés pour le téléchargement (séparés par des virgules)"
}
}

View File

@ -10,6 +10,7 @@ import { createContext, ReactNode } from "react";
import { Progress } from "@/app-components/displays/Progress"; import { Progress } from "@/app-components/displays/Progress";
import { useLoadSettings } from "@/hooks/entities/auth-hooks"; import { useLoadSettings } from "@/hooks/entities/auth-hooks";
import { useRemoteI18n } from "@/hooks/useRemoteI18n";
import { ISetting } from "@/types/setting.types"; import { ISetting } from "@/types/setting.types";
export const SettingsContext = createContext<{ export const SettingsContext = createContext<{
@ -27,6 +28,9 @@ export const SettingsProvider = ({
}: SettingsProviderProps): JSX.Element => { }: SettingsProviderProps): JSX.Element => {
const { data, isLoading } = useLoadSettings(); const { data, isLoading } = useLoadSettings();
// Load API i18n Translations (extensions, ...)
useRemoteI18n();
if (isLoading) return <Progress />; if (isLoading) return <Progress />;
return ( return (

View File

@ -0,0 +1,49 @@
/*
* Copyright © 2024 Hexastack. All rights reserved.
*
* Licensed under the GNU Affero General Public License v3.0 (AGPLv3) with the following additional terms:
* 1. The name "Hexabot" is a trademark of Hexastack. You may not use this name in derivative works without express written permission.
* 2. All derivative works must include clear attribution to the original creator and software, Hexastack and Hexabot, in a prominent location (e.g., in the software's "About" section, documentation, and README file).
*/
import { useEffect, useRef } from "react";
import i18n from "@/i18n/config";
import { useApiClient } from "./useApiClient";
import { useAuth } from "./useAuth";
export const useRemoteI18n = () => {
const { isAuthenticated } = useAuth();
const { apiClient } = useApiClient();
const isRemoteI18nLoaded = useRef(false);
useEffect(() => {
const fetchRemoteI18n = async () => {
try {
const additionalTranslations = await apiClient.fetchRemoteI18n();
// Assuming additionalTranslations is an object like { en: { translation: { key: 'value' } } }
Object.keys(additionalTranslations).forEach((lang) => {
Object.keys(additionalTranslations[lang]).forEach((namespace) => {
i18n.addResourceBundle(
lang,
namespace,
additionalTranslations[lang][namespace],
true,
true,
);
});
});
} catch (error) {
// eslint-disable-next-line no-console
console.error("Failed to fetch remote i18n translations:", error);
}
};
if (isAuthenticated && !isRemoteI18nLoaded.current) {
fetchRemoteI18n();
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [isAuthenticated]);
};

View File

@ -26,14 +26,7 @@ i18n
backend: { backend: {
loadPath: "/locales/{{lng}}/{{ns}}.json", loadPath: "/locales/{{lng}}/{{ns}}.json",
}, },
ns: [ ns: ["translation", "chatbot_settings", "contact", "nlp_settings"],
"translation",
"chatbot_settings.json",
"contact",
"nlp_settings",
"offline",
"live-chat-tester",
],
interpolation: { interpolation: {
escapeValue: false, escapeValue: false,
}, },

View File

@ -32,6 +32,7 @@ export const ROUTES = {
CSRF: "/csrftoken", CSRF: "/csrftoken",
BOTSTATS: "/botstats", BOTSTATS: "/botstats",
REFRESH_TRANSLATIONS: "/translation/refresh", REFRESH_TRANSLATIONS: "/translation/refresh",
FETCH_REMOTE_I18N: "/i18n",
RESET: "/user/reset", RESET: "/user/reset",
NLP_SAMPLE_IMPORT: "/nlpsample/import", NLP_SAMPLE_IMPORT: "/nlpsample/import",
NLP_SAMPLE_PREDICT: "/nlpsample/message", NLP_SAMPLE_PREDICT: "/nlpsample/message",
@ -190,6 +191,12 @@ export class ApiClient {
return data; return data;
} }
async fetchRemoteI18n() {
const { data } = await this.request.get(ROUTES.FETCH_REMOTE_I18N);
return data;
}
async reset(token: string, payload: IResetPayload) { async reset(token: string, payload: IResetPayload) {
const { data } = await this.request.post< const { data } = await this.request.post<
IResetPayload, IResetPayload,