mirror of
https://github.com/hexastack/hexabot
synced 2025-06-26 18:27:28 +00:00
feat: fetch remote i18n
This commit is contained in:
parent
08b1deae50
commit
85cc85e4db
@ -26,7 +26,7 @@ import ChannelHandler from './lib/Handler';
|
||||
|
||||
@Injectable()
|
||||
export class ChannelService {
|
||||
private registry: Map<string, ChannelHandler> = new Map();
|
||||
private registry: Map<string, ChannelHandler<string>> = new Map();
|
||||
|
||||
constructor(
|
||||
private readonly logger: LoggerService,
|
||||
@ -40,7 +40,10 @@ export class ChannelService {
|
||||
* @param channel - The channel handler associated with the channel name.
|
||||
* @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);
|
||||
}
|
||||
|
||||
@ -71,7 +74,9 @@ export class ChannelService {
|
||||
* @param channelName - The name of the channel (messenger, offline, ...).
|
||||
* @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);
|
||||
if (!handler) {
|
||||
throw new Error(`Channel ${name} not found`);
|
||||
@ -98,8 +103,8 @@ export class ChannelService {
|
||||
* @param req - The websocket request object.
|
||||
* @param res - The websocket response object.
|
||||
*/
|
||||
@SocketGet('/webhook/offline/')
|
||||
@SocketPost('/webhook/offline/')
|
||||
@SocketGet(`/webhook/${OFFLINE_CHANNEL_NAME}/`)
|
||||
@SocketPost(`/webhook/${OFFLINE_CHANNEL_NAME}/`)
|
||||
handleWebsocketForOffline(
|
||||
@SocketReq() req: SocketRequest,
|
||||
@SocketRes() res: SocketResponse,
|
||||
@ -116,8 +121,8 @@ export class ChannelService {
|
||||
* @param req - The websocket request object.
|
||||
* @param res - The websocket response object.
|
||||
*/
|
||||
@SocketGet('/webhook/live-chat-tester/')
|
||||
@SocketPost('/webhook/live-chat-tester/')
|
||||
@SocketGet(`/webhook/${LIVE_CHAT_TEST_CHANNEL_NAME}/`)
|
||||
@SocketPost(`/webhook/${LIVE_CHAT_TEST_CHANNEL_NAME}/`)
|
||||
async handleWebsocketForLiveChatTester(
|
||||
@SocketReq() req: SocketRequest,
|
||||
@SocketRes() res: SocketResponse,
|
||||
|
@ -18,35 +18,57 @@ import {
|
||||
import { LoggerService } from '@/logger/logger.service';
|
||||
import BaseNlpHelper from '@/nlp/lib/BaseNlpHelper';
|
||||
import { NlpService } from '@/nlp/services/nlp.service';
|
||||
import { SettingCreateDto } from '@/setting/dto/setting.dto';
|
||||
import { SettingService } from '@/setting/services/setting.service';
|
||||
import { SocketRequest } from '@/websocket/utils/socket-request';
|
||||
import { SocketResponse } from '@/websocket/utils/socket-response';
|
||||
|
||||
import { ChannelService } from '../channel.service';
|
||||
import { ChannelSetting } from '../types';
|
||||
|
||||
import EventWrapper from './EventWrapper';
|
||||
|
||||
import EventWrapper from './EventWrapper';
|
||||
|
||||
@Injectable()
|
||||
export default abstract class ChannelHandler {
|
||||
protected settings: SettingCreateDto[] = [];
|
||||
export default abstract class ChannelHandler<N extends string = string> {
|
||||
private readonly name: N;
|
||||
|
||||
private readonly settings: ChannelSetting<N>[];
|
||||
|
||||
protected NLP: BaseNlpHelper;
|
||||
|
||||
constructor(
|
||||
name: N,
|
||||
settings: ChannelSetting<N>[],
|
||||
protected readonly settingService: SettingService,
|
||||
private readonly channelService: ChannelService,
|
||||
protected readonly nlpService: NlpService,
|
||||
protected readonly logger: LoggerService,
|
||||
) {}
|
||||
) {
|
||||
this.name = name;
|
||||
this.settings = settings;
|
||||
}
|
||||
|
||||
onModuleInit() {
|
||||
this.channelService.setChannel(this.getChannel(), this);
|
||||
this.channelService.setChannel(
|
||||
this.getChannel(),
|
||||
this as unknown as ChannelHandler<N>,
|
||||
);
|
||||
this.setup();
|
||||
}
|
||||
|
||||
protected getGroup() {
|
||||
return this.getChannel().replaceAll('-', '_');
|
||||
}
|
||||
|
||||
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();
|
||||
this.setNLP(nlp);
|
||||
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>() {
|
||||
const settings = await this.settingService.getSettings();
|
||||
return settings[this.getChannel()] as S;
|
||||
getChannel() {
|
||||
return this.name;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the channel's name
|
||||
* @returns {String}
|
||||
* Returns the channel's settings
|
||||
* @returns Channel's settings
|
||||
*/
|
||||
abstract getChannel(): string;
|
||||
async getSettings() {
|
||||
const settings = await this.settingService.getSettings();
|
||||
return settings[this.getGroup()];
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform any initialization needed
|
||||
* @returns
|
||||
|
||||
*/
|
||||
abstract init(): void;
|
||||
|
||||
/**
|
||||
* Process incoming channel data via POST/GET methods
|
||||
*
|
||||
* @param {module:Controller.req} req
|
||||
* @param {module:Controller.res} res
|
||||
* Process incoming channel data via POST/GET methods
|
||||
*/
|
||||
abstract handle(
|
||||
req: Request | SocketRequest,
|
||||
@ -93,26 +117,28 @@ export default abstract class ChannelHandler {
|
||||
|
||||
/**
|
||||
* Format a text message that will be sent to the channel
|
||||
*
|
||||
* @param message - A text to be sent to the end user
|
||||
* @param options - might contain additional settings
|
||||
* @returns {Object} - A text message in the channel specific format
|
||||
|
||||
*/
|
||||
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 options - might contain additional settings
|
||||
* @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;
|
||||
|
||||
/**
|
||||
* From raw buttons, construct a channel understable message containing those buttons
|
||||
*
|
||||
* @param message - A text + buttons to be sent to the end user
|
||||
* @param options - Might contain additional settings
|
||||
* @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(
|
||||
message: StdOutgoingMessage,
|
||||
@ -121,27 +147,29 @@ export default abstract class ChannelHandler {
|
||||
): 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 options - Might contain additional settings
|
||||
* @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;
|
||||
|
||||
/**
|
||||
* 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 options - Might contain additional settings
|
||||
* @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[];
|
||||
|
||||
/**
|
||||
* Format a list of elements
|
||||
*
|
||||
* @param message - Contains elements to be sent to the end user
|
||||
* @param options - Might contain additional settings
|
||||
* @returns {Object} - A ready to be sent list template message in the format required by the channel
|
||||
|
||||
*/
|
||||
abstract _listFormat(
|
||||
message: StdOutgoingMessage,
|
||||
|
8
api/src/channel/types.ts
Normal file
8
api/src/channel/types.ts
Normal 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>;
|
||||
};
|
@ -34,7 +34,6 @@ 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';
|
||||
import { SettingService } from '@/setting/services/setting.service';
|
||||
import {
|
||||
blockFixtures,
|
||||
@ -67,6 +66,9 @@ import { FileType } from '../schemas/types/attachment';
|
||||
import { Context } from '../schemas/types/context';
|
||||
import { PayloadType, StdOutgoingListMessage } from '../schemas/types/message';
|
||||
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 { BlockService } from './block.service';
|
||||
|
@ -18,7 +18,6 @@ import { LoggerService } from '@/logger/logger.service';
|
||||
import { Nlp } from '@/nlp/lib/types';
|
||||
import { PluginService } from '@/plugins/plugins.service';
|
||||
import { PluginType } from '@/plugins/types';
|
||||
import { Settings } from '@/setting/schemas/types';
|
||||
import { SettingService } from '@/setting/services/setting.service';
|
||||
import { BaseService } from '@/utils/generics/base-service';
|
||||
import { getRandom } from '@/utils/helpers/safeRandom';
|
||||
|
@ -11,7 +11,6 @@ import { EventEmitter2 } from '@nestjs/event-emitter';
|
||||
|
||||
import EventWrapper from '@/channel/lib/EventWrapper';
|
||||
import { LoggerService } from '@/logger/logger.service';
|
||||
import { Settings } from '@/setting/schemas/types';
|
||||
import { SettingService } from '@/setting/services/setting.service';
|
||||
|
||||
import { MessageCreateDto } from '../dto/message.dto';
|
||||
|
@ -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)"
|
||||
}
|
@ -0,0 +1,3 @@
|
||||
{
|
||||
"live_chat_tester": "Live Chat Tester"
|
||||
}
|
@ -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)"
|
||||
}
|
@ -0,0 +1,3 @@
|
||||
{
|
||||
"live_chat_tester": "Testeur Live Chat"
|
||||
}
|
@ -17,11 +17,10 @@ import { MenuService } from '@/cms/services/menu.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';
|
||||
import { SettingService } from '@/setting/services/setting.service';
|
||||
import { WebsocketGateway } from '@/websocket/websocket.gateway';
|
||||
|
||||
import OfflineHandler from '../offline/index.channel';
|
||||
import BaseWebChannelHandler from '../offline/base-web-channel';
|
||||
|
||||
import {
|
||||
DEFAULT_LIVE_CHAT_TEST_SETTINGS,
|
||||
@ -29,9 +28,9 @@ import {
|
||||
} from './settings';
|
||||
|
||||
@Injectable()
|
||||
export default class LiveChatTesterHandler extends OfflineHandler {
|
||||
protected settings: SettingCreateDto[] = DEFAULT_LIVE_CHAT_TEST_SETTINGS;
|
||||
|
||||
export default class LiveChatTesterHandler extends BaseWebChannelHandler<
|
||||
typeof LIVE_CHAT_TEST_CHANNEL_NAME
|
||||
> {
|
||||
constructor(
|
||||
settingService: SettingService,
|
||||
channelService: ChannelService,
|
||||
@ -46,6 +45,8 @@ export default class LiveChatTesterHandler extends OfflineHandler {
|
||||
websocketGateway: WebsocketGateway,
|
||||
) {
|
||||
super(
|
||||
LIVE_CHAT_TEST_CHANNEL_NAME,
|
||||
DEFAULT_LIVE_CHAT_TEST_SETTINGS,
|
||||
settingService,
|
||||
channelService,
|
||||
nlpService,
|
||||
@ -59,12 +60,4 @@ export default class LiveChatTesterHandler extends OfflineHandler {
|
||||
websocketGateway,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the channel's name
|
||||
* @returns {String}
|
||||
*/
|
||||
getChannel() {
|
||||
return LIVE_CHAT_TEST_CHANNEL_NAME;
|
||||
}
|
||||
}
|
||||
|
18
api/src/extensions/channels/live-chat-tester/index.d.ts
vendored
Normal file
18
api/src/extensions/channels/live-chat-tester/index.d.ts
vendored
Normal 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>
|
||||
>;
|
||||
}
|
||||
}
|
@ -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).
|
||||
*/
|
||||
|
||||
import { ChannelSetting } from '@/channel/types';
|
||||
import { config } from '@/config';
|
||||
import { SettingCreateDto } from '@/setting/dto/setting.dto';
|
||||
import { SettingType } from '@/setting/schemas/types';
|
||||
|
||||
import { Offline } from '../offline/types';
|
||||
|
||||
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,
|
||||
value: 'test',
|
||||
type: SettingType.text,
|
||||
weight: 2,
|
||||
},
|
||||
{
|
||||
group: LIVE_CHAT_TEST_CHANNEL_NAME,
|
||||
group: LIVE_CHAT_TEST_GROUP_NAME,
|
||||
label: Offline.SettingLabel.allowed_domains,
|
||||
value: config.frontendPath,
|
||||
type: SettingType.text,
|
||||
weight: 3,
|
||||
},
|
||||
{
|
||||
group: LIVE_CHAT_TEST_CHANNEL_NAME,
|
||||
group: LIVE_CHAT_TEST_GROUP_NAME,
|
||||
label: Offline.SettingLabel.start_button,
|
||||
value: true,
|
||||
type: SettingType.checkbox,
|
||||
weight: 4,
|
||||
},
|
||||
{
|
||||
group: LIVE_CHAT_TEST_CHANNEL_NAME,
|
||||
group: LIVE_CHAT_TEST_GROUP_NAME,
|
||||
label: Offline.SettingLabel.input_disabled,
|
||||
value: false,
|
||||
type: SettingType.checkbox,
|
||||
weight: 5,
|
||||
},
|
||||
{
|
||||
group: LIVE_CHAT_TEST_CHANNEL_NAME,
|
||||
group: LIVE_CHAT_TEST_GROUP_NAME,
|
||||
label: Offline.SettingLabel.persistent_menu,
|
||||
value: true,
|
||||
type: SettingType.checkbox,
|
||||
weight: 6,
|
||||
},
|
||||
{
|
||||
group: LIVE_CHAT_TEST_CHANNEL_NAME,
|
||||
group: LIVE_CHAT_TEST_GROUP_NAME,
|
||||
label: Offline.SettingLabel.greeting_message,
|
||||
value: 'Welcome! Ready to start a conversation with our chatbot?',
|
||||
type: SettingType.textarea,
|
||||
weight: 7,
|
||||
},
|
||||
{
|
||||
group: LIVE_CHAT_TEST_CHANNEL_NAME,
|
||||
group: LIVE_CHAT_TEST_GROUP_NAME,
|
||||
label: Offline.SettingLabel.theme_color,
|
||||
value: 'teal',
|
||||
type: SettingType.select,
|
||||
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,
|
||||
value: true,
|
||||
type: SettingType.checkbox,
|
||||
weight: 11,
|
||||
},
|
||||
{
|
||||
group: LIVE_CHAT_TEST_CHANNEL_NAME,
|
||||
group: LIVE_CHAT_TEST_GROUP_NAME,
|
||||
label: Offline.SettingLabel.show_file,
|
||||
value: true,
|
||||
type: SettingType.checkbox,
|
||||
weight: 12,
|
||||
},
|
||||
{
|
||||
group: LIVE_CHAT_TEST_CHANNEL_NAME,
|
||||
group: LIVE_CHAT_TEST_GROUP_NAME,
|
||||
label: Offline.SettingLabel.show_location,
|
||||
value: true,
|
||||
type: SettingType.checkbox,
|
||||
weight: 13,
|
||||
},
|
||||
{
|
||||
group: LIVE_CHAT_TEST_CHANNEL_NAME,
|
||||
group: LIVE_CHAT_TEST_GROUP_NAME,
|
||||
label: Offline.SettingLabel.allowed_upload_size,
|
||||
value: 2500000,
|
||||
type: SettingType.number,
|
||||
weight: 14,
|
||||
},
|
||||
{
|
||||
group: LIVE_CHAT_TEST_CHANNEL_NAME,
|
||||
group: LIVE_CHAT_TEST_GROUP_NAME,
|
||||
label: Offline.SettingLabel.allowed_upload_types,
|
||||
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',
|
||||
type: SettingType.textarea,
|
||||
weight: 15,
|
||||
},
|
||||
];
|
||||
] as const satisfies ChannelSetting[];
|
||||
|
1301
api/src/extensions/channels/offline/base-web-channel.ts
Normal file
1301
api/src/extensions/channels/offline/base-web-channel.ts
Normal file
File diff suppressed because it is too large
Load Diff
23
api/src/extensions/channels/offline/i18n/en/label.json
Normal file
23
api/src/extensions/channels/offline/i18n/en/label.json
Normal 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)"
|
||||
}
|
3
api/src/extensions/channels/offline/i18n/en/title.json
Normal file
3
api/src/extensions/channels/offline/i18n/en/title.json
Normal file
@ -0,0 +1,3 @@
|
||||
{
|
||||
"offline": "Canal Web"
|
||||
}
|
23
api/src/extensions/channels/offline/i18n/fr/label.json
Normal file
23
api/src/extensions/channels/offline/i18n/fr/label.json
Normal 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)"
|
||||
}
|
3
api/src/extensions/channels/offline/i18n/fr/title.json
Normal file
3
api/src/extensions/channels/offline/i18n/fr/title.json
Normal file
@ -0,0 +1,3 @@
|
||||
{
|
||||
"live_chat_tester": "Testeur Live Chat"
|
||||
}
|
File diff suppressed because it is too large
Load Diff
14
api/src/extensions/channels/offline/index.d.ts
vendored
Normal file
14
api/src/extensions/channels/offline/index.d.ts
vendored
Normal 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>
|
||||
>;
|
||||
}
|
||||
}
|
@ -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).
|
||||
*/
|
||||
|
||||
import { SettingCreateDto } from '@/setting/dto/setting.dto';
|
||||
import { ChannelSetting } from '@/channel/types';
|
||||
import { SettingType } from '@/setting/schemas/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,
|
||||
value: 'token123',
|
||||
type: SettingType.secret,
|
||||
weight: 2,
|
||||
},
|
||||
{
|
||||
group: OFFLINE_CHANNEL_NAME,
|
||||
group: OFFLINE_GROUP_NAME,
|
||||
label: Offline.SettingLabel.allowed_domains,
|
||||
value: 'http://localhost:8080,http://localhost:4000',
|
||||
type: SettingType.text,
|
||||
weight: 3,
|
||||
},
|
||||
{
|
||||
group: OFFLINE_CHANNEL_NAME,
|
||||
group: OFFLINE_GROUP_NAME,
|
||||
label: Offline.SettingLabel.start_button,
|
||||
value: true,
|
||||
type: SettingType.checkbox,
|
||||
weight: 4,
|
||||
},
|
||||
{
|
||||
group: OFFLINE_CHANNEL_NAME,
|
||||
group: OFFLINE_GROUP_NAME,
|
||||
label: Offline.SettingLabel.input_disabled,
|
||||
value: false,
|
||||
type: SettingType.checkbox,
|
||||
weight: 5,
|
||||
},
|
||||
{
|
||||
group: OFFLINE_CHANNEL_NAME,
|
||||
group: OFFLINE_GROUP_NAME,
|
||||
label: Offline.SettingLabel.persistent_menu,
|
||||
value: true,
|
||||
type: SettingType.checkbox,
|
||||
weight: 6,
|
||||
},
|
||||
{
|
||||
group: OFFLINE_CHANNEL_NAME,
|
||||
group: OFFLINE_GROUP_NAME,
|
||||
label: Offline.SettingLabel.greeting_message,
|
||||
value: 'Welcome! Ready to start a conversation with our chatbot?',
|
||||
type: SettingType.textarea,
|
||||
weight: 7,
|
||||
},
|
||||
{
|
||||
group: OFFLINE_CHANNEL_NAME,
|
||||
group: OFFLINE_GROUP_NAME,
|
||||
label: Offline.SettingLabel.theme_color,
|
||||
value: 'teal',
|
||||
type: SettingType.select,
|
||||
options: ['teal', 'orange', 'red', 'green', 'blue', 'dark'],
|
||||
weight: 8,
|
||||
},
|
||||
{
|
||||
group: OFFLINE_CHANNEL_NAME,
|
||||
group: OFFLINE_GROUP_NAME,
|
||||
label: Offline.SettingLabel.window_title,
|
||||
value: 'Widget Title',
|
||||
type: SettingType.text,
|
||||
weight: 9,
|
||||
},
|
||||
{
|
||||
group: OFFLINE_CHANNEL_NAME,
|
||||
group: OFFLINE_GROUP_NAME,
|
||||
label: Offline.SettingLabel.avatar_url,
|
||||
value: 'https://eu.ui-avatars.com/api/?name=Hexa+Bot&size=64',
|
||||
type: SettingType.text,
|
||||
weight: 10,
|
||||
},
|
||||
{
|
||||
group: OFFLINE_CHANNEL_NAME,
|
||||
group: OFFLINE_GROUP_NAME,
|
||||
label: Offline.SettingLabel.show_emoji,
|
||||
value: true,
|
||||
type: SettingType.checkbox,
|
||||
weight: 11,
|
||||
},
|
||||
{
|
||||
group: OFFLINE_CHANNEL_NAME,
|
||||
group: OFFLINE_GROUP_NAME,
|
||||
label: Offline.SettingLabel.show_file,
|
||||
value: true,
|
||||
type: SettingType.checkbox,
|
||||
weight: 12,
|
||||
},
|
||||
{
|
||||
group: OFFLINE_CHANNEL_NAME,
|
||||
group: OFFLINE_GROUP_NAME,
|
||||
label: Offline.SettingLabel.show_location,
|
||||
value: true,
|
||||
type: SettingType.checkbox,
|
||||
weight: 13,
|
||||
},
|
||||
{
|
||||
group: OFFLINE_CHANNEL_NAME,
|
||||
group: OFFLINE_GROUP_NAME,
|
||||
label: Offline.SettingLabel.allowed_upload_size,
|
||||
value: 2500000,
|
||||
type: SettingType.number,
|
||||
weight: 14,
|
||||
},
|
||||
{
|
||||
group: OFFLINE_CHANNEL_NAME,
|
||||
group: OFFLINE_GROUP_NAME,
|
||||
label: Offline.SettingLabel.allowed_upload_types,
|
||||
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',
|
||||
type: SettingType.textarea,
|
||||
weight: 15,
|
||||
},
|
||||
];
|
||||
] as const satisfies ChannelSetting<typeof OFFLINE_CHANNEL_NAME>[];
|
||||
|
@ -19,7 +19,7 @@ import {
|
||||
} from '@/chat/schemas/types/message';
|
||||
import { Payload } from '@/chat/schemas/types/quick-reply';
|
||||
|
||||
import OfflineHandler from './index.channel';
|
||||
import BaseWebChannelHandler from './base-web-channel';
|
||||
import { Offline } from './types';
|
||||
|
||||
type OfflineEventAdapter =
|
||||
@ -66,10 +66,9 @@ type OfflineEventAdapter =
|
||||
raw: Offline.IncomingMessage<Offline.IncomingAttachmentMessage>;
|
||||
};
|
||||
|
||||
export default class OfflineEventWrapper extends EventWrapper<
|
||||
OfflineEventAdapter,
|
||||
Offline.Event
|
||||
> {
|
||||
export default class OfflineEventWrapper<
|
||||
T extends BaseWebChannelHandler<string> = BaseWebChannelHandler<string>,
|
||||
> extends EventWrapper<OfflineEventAdapter, Offline.Event> {
|
||||
/**
|
||||
* Constructor : channel's event wrapper
|
||||
*
|
||||
@ -77,7 +76,7 @@ export default class OfflineEventWrapper extends EventWrapper<
|
||||
* @param event - The message event received
|
||||
* @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);
|
||||
}
|
||||
|
||||
|
@ -152,9 +152,12 @@ describe('NLP Default Helper', () => {
|
||||
const nlp = nlpService.getNLP();
|
||||
const results = nlp.bestGuess(nlpParseResult, true);
|
||||
const settings = await settingService.getSettings();
|
||||
const threshold = settings.nlp_settings.threshold;
|
||||
const thresholdGuess = {
|
||||
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);
|
||||
|
@ -135,7 +135,11 @@ export default class DefaultNlpHelper extends BaseNlpHelper {
|
||||
entities: nlp.entities.slice(),
|
||||
};
|
||||
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
|
||||
.map((e) => {
|
||||
e.confidence =
|
||||
|
28
api/src/i18n/controllers/i18n.controller.ts
Normal file
28
api/src/i18n/controllers/i18n.controller.ts
Normal 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();
|
||||
}
|
||||
}
|
@ -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 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()
|
||||
async findPage(
|
||||
@ -61,8 +61,8 @@ export class LanguageController extends BaseController<Language> {
|
||||
}
|
||||
|
||||
/**
|
||||
* Counts the filtered number of categories.
|
||||
* @returns A promise that resolves to an object representing the filtered number of categories.
|
||||
* Counts the filtered number of languages.
|
||||
* @returns A promise that resolves to an object representing the filtered number of languages.
|
||||
*/
|
||||
@Get('count')
|
||||
async filterCount(
|
||||
|
@ -26,6 +26,7 @@ import { Observable } from 'rxjs';
|
||||
|
||||
import { ChatModule } from '@/chat/chat.module';
|
||||
|
||||
import { I18nController } from './controllers/i18n.controller';
|
||||
import { LanguageController } from './controllers/language.controller';
|
||||
import { TranslationController } from './controllers/translation.controller';
|
||||
import { LanguageRepository } from './repositories/language.repository';
|
||||
@ -62,6 +63,7 @@ export class I18nModule extends NativeI18nModule {
|
||||
controllers: (controllers || []).concat([
|
||||
LanguageController,
|
||||
TranslationController,
|
||||
I18nController,
|
||||
]),
|
||||
providers: providers.concat([
|
||||
I18nService,
|
||||
|
@ -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).
|
||||
*/
|
||||
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { promises as fs } from 'fs';
|
||||
import * as path from 'path';
|
||||
|
||||
import { Injectable, OnModuleInit } from '@nestjs/common';
|
||||
import {
|
||||
I18nJsonLoader,
|
||||
I18nTranslation,
|
||||
I18nService as NativeI18nService,
|
||||
Path,
|
||||
PathValue,
|
||||
@ -19,11 +24,22 @@ import { config } from '@/config';
|
||||
import { Translation } from '@/i18n/schemas/translation.schema';
|
||||
|
||||
@Injectable()
|
||||
export class I18nService<
|
||||
K = Record<string, unknown>,
|
||||
> extends NativeI18nService<K> {
|
||||
export class I18nService<K = Record<string, unknown>>
|
||||
extends NativeI18nService<K>
|
||||
implements OnModuleInit
|
||||
{
|
||||
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>>(
|
||||
key: P,
|
||||
options?: TranslateOptions,
|
||||
@ -66,4 +82,48 @@ export class I18nService<
|
||||
return acc;
|
||||
}, 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}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -10,7 +10,6 @@ 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';
|
||||
@ -64,7 +63,7 @@ describe('TranslationService', () => {
|
||||
global_fallback: true,
|
||||
fallback_message: ['Global fallback message'],
|
||||
},
|
||||
} as Settings),
|
||||
}),
|
||||
},
|
||||
},
|
||||
{
|
||||
|
10
api/src/index.d.ts
vendored
10
api/src/index.d.ts
vendored
@ -9,9 +9,9 @@
|
||||
import 'mongoose';
|
||||
import { SubscriberStub } from './chat/schemas/subscriber.schema';
|
||||
import {
|
||||
WithoutGenericAny,
|
||||
RecursivePartial,
|
||||
ObjectWithNestedKeys,
|
||||
RecursivePartial,
|
||||
WithoutGenericAny,
|
||||
} from './utils/types/filter.types';
|
||||
|
||||
type TOmitId<T> = Omit<T, 'id'>;
|
||||
@ -63,3 +63,9 @@ declare module 'mongoose' {
|
||||
|
||||
type THydratedDocument<T> = TOmitId<HydratedDocument<T>>;
|
||||
}
|
||||
|
||||
declare global {
|
||||
type HyphenToUnderscore<S extends string> = S extends `${infer P}-${infer Q}`
|
||||
? `${P}_${HyphenToUnderscore<Q>}`
|
||||
: S;
|
||||
}
|
||||
|
@ -31,7 +31,6 @@ import {
|
||||
NlpValueDocument,
|
||||
NlpValueFull,
|
||||
} from '@/nlp/schemas/nlp-value.schema';
|
||||
import { Settings } from '@/setting/schemas/types';
|
||||
|
||||
import { NlpEntityService } from '../services/nlp-entity.service';
|
||||
import { NlpSampleService } from '../services/nlp-sample.service';
|
||||
|
@ -9,7 +9,7 @@
|
||||
import { BlockCreateDto } from '@/chat/dto/block.dto';
|
||||
import { Block } from '@/chat/schemas/block.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 {
|
||||
event = 'event',
|
||||
@ -22,7 +22,7 @@ export interface CustomBlocks {}
|
||||
type ChannelEvent = any;
|
||||
type BlockAttrs = Partial<BlockCreateDto> & { name: string };
|
||||
|
||||
export type PluginSetting = Omit<Setting, 'id' | 'createdAt' | 'updatedAt'>;
|
||||
export type PluginSetting = SettingCreateDto;
|
||||
|
||||
export type PluginBlockTemplate = Omit<
|
||||
BlockAttrs,
|
||||
|
@ -22,7 +22,7 @@ import { nlpEntityModels } from './nlp/seeds/nlp-entity.seed-model';
|
||||
import { NlpValueSeeder } from './nlp/seeds/nlp-value.seed';
|
||||
import { nlpValueModels } from './nlp/seeds/nlp-value.seed-model';
|
||||
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 { modelModels } from './user/seeds/model.seed-model';
|
||||
import { PermissionSeeder } from './user/seeds/permission.seed';
|
||||
@ -106,7 +106,7 @@ export async function seedDatabase(app: INestApplicationContext) {
|
||||
}
|
||||
// Seed users
|
||||
try {
|
||||
await settingSeeder.seed(settingModels);
|
||||
await settingSeeder.seed(DEFAULT_SETTINGS);
|
||||
} catch (e) {
|
||||
logger.error('Unable to seed the database with settings!');
|
||||
throw e;
|
||||
|
@ -11,25 +11,30 @@ import {
|
||||
IsArray,
|
||||
IsIn,
|
||||
IsNotEmpty,
|
||||
IsString,
|
||||
IsOptional,
|
||||
IsString,
|
||||
} from 'class-validator';
|
||||
|
||||
import { SettingType } from '../schemas/types';
|
||||
|
||||
export class SettingCreateDto {
|
||||
@ApiProperty({ description: 'Setting group of setting', type: String })
|
||||
@ApiProperty({ description: 'Setting group', type: String })
|
||||
@IsNotEmpty()
|
||||
@IsString()
|
||||
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()
|
||||
@IsString()
|
||||
label: string;
|
||||
|
||||
@ApiProperty({
|
||||
description: 'Setting type of the setting',
|
||||
description: 'Setting type',
|
||||
enum: [
|
||||
'text',
|
||||
'multiple_text',
|
||||
@ -44,12 +49,12 @@ export class SettingCreateDto {
|
||||
@IsIn(Object.values(SettingType))
|
||||
type: SettingType;
|
||||
|
||||
@ApiProperty({ description: 'Setting value of the setting' })
|
||||
@ApiProperty({ description: 'Setting value' })
|
||||
@IsNotEmpty()
|
||||
value: any;
|
||||
|
||||
@ApiPropertyOptional({
|
||||
description: 'Setting options',
|
||||
description: 'Setting options (required when type is select)',
|
||||
isArray: true,
|
||||
type: Array,
|
||||
})
|
||||
|
31
api/src/setting/index.d.ts
vendored
Normal file
31
api/src/setting/index.d.ts
vendored
Normal 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> {}
|
||||
}
|
@ -7,6 +7,7 @@
|
||||
*/
|
||||
|
||||
import { ModelDefinition, Prop, Schema, SchemaFactory } from '@nestjs/mongoose';
|
||||
import { Transform } from 'class-transformer';
|
||||
import { IsArray, IsIn } from 'class-validator';
|
||||
|
||||
import { BaseSchema } from '@/utils/generics/base-schema';
|
||||
@ -22,6 +23,13 @@ export class Setting extends BaseSchema {
|
||||
})
|
||||
group: string;
|
||||
|
||||
@Prop({
|
||||
type: String,
|
||||
default: '',
|
||||
})
|
||||
@Transform(({ obj }) => obj.subgroup || undefined)
|
||||
subgroup?: string;
|
||||
|
||||
@Prop({
|
||||
type: String,
|
||||
required: true,
|
||||
|
@ -94,18 +94,3 @@ export type AnySetting =
|
||||
| MultipleAttachmentSetting;
|
||||
|
||||
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>;
|
||||
|
@ -9,7 +9,7 @@
|
||||
import { SettingCreateDto } from '../dto/setting.dto';
|
||||
import { SettingType } from '../schemas/types';
|
||||
|
||||
export const settingModels: SettingCreateDto[] = [
|
||||
export const DEFAULT_SETTINGS = [
|
||||
{
|
||||
group: 'chatbot_settings',
|
||||
label: 'global_fallback',
|
||||
@ -38,7 +38,7 @@ export const settingModels: SettingCreateDto[] = [
|
||||
value: [
|
||||
"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 :(",
|
||||
],
|
||||
] as string[],
|
||||
type: SettingType.multiple_text,
|
||||
weight: 3,
|
||||
},
|
||||
@ -146,4 +146,4 @@ export const settingModels: SettingCreateDto[] = [
|
||||
type: SettingType.text,
|
||||
weight: 10,
|
||||
},
|
||||
];
|
||||
] as const satisfies SettingCreateDto[];
|
||||
|
@ -21,7 +21,6 @@ import { BaseService } from '@/utils/generics/base-service';
|
||||
import { SettingCreateDto } from '../dto/setting.dto';
|
||||
import { SettingRepository } from '../repositories/setting.repository';
|
||||
import { Setting } from '../schemas/setting.schema';
|
||||
import { Settings } from '../schemas/types';
|
||||
import { SettingSeeder } from '../seeds/setting.seed';
|
||||
|
||||
@Injectable()
|
||||
|
@ -53,7 +53,7 @@ export class Ability implements CanActivate {
|
||||
|
||||
if (user?.roles?.length) {
|
||||
if (
|
||||
['/auth/logout', '/logout', '/auth/me', '/channel'].includes(
|
||||
['/auth/logout', '/logout', '/auth/me', '/channel', '/i18n'].includes(
|
||||
_parsedUrl.pathname,
|
||||
)
|
||||
) {
|
||||
|
@ -17,9 +17,11 @@
|
||||
"strictBindCallApply": false,
|
||||
"forceConsistentCasingInFileNames": false,
|
||||
"noFallthroughCasesInSwitch": false,
|
||||
"resolveJsonModule": true,
|
||||
"esModuleInterop": true,
|
||||
"paths": {
|
||||
"@/*": ["src/*"]
|
||||
}
|
||||
}
|
||||
},
|
||||
"include": ["src/**/*.ts", "src/**/*.json"]
|
||||
}
|
||||
|
@ -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)"
|
||||
}
|
||||
}
|
@ -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)"
|
||||
}
|
||||
}
|
@ -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)"
|
||||
}
|
||||
}
|
@ -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)"
|
||||
}
|
||||
}
|
@ -10,6 +10,7 @@ import { createContext, ReactNode } from "react";
|
||||
|
||||
import { Progress } from "@/app-components/displays/Progress";
|
||||
import { useLoadSettings } from "@/hooks/entities/auth-hooks";
|
||||
import { useRemoteI18n } from "@/hooks/useRemoteI18n";
|
||||
import { ISetting } from "@/types/setting.types";
|
||||
|
||||
export const SettingsContext = createContext<{
|
||||
@ -27,6 +28,9 @@ export const SettingsProvider = ({
|
||||
}: SettingsProviderProps): JSX.Element => {
|
||||
const { data, isLoading } = useLoadSettings();
|
||||
|
||||
// Load API i18n Translations (extensions, ...)
|
||||
useRemoteI18n();
|
||||
|
||||
if (isLoading) return <Progress />;
|
||||
|
||||
return (
|
||||
|
49
frontend/src/hooks/useRemoteI18n.ts
Normal file
49
frontend/src/hooks/useRemoteI18n.ts
Normal 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]);
|
||||
};
|
@ -26,14 +26,7 @@ i18n
|
||||
backend: {
|
||||
loadPath: "/locales/{{lng}}/{{ns}}.json",
|
||||
},
|
||||
ns: [
|
||||
"translation",
|
||||
"chatbot_settings.json",
|
||||
"contact",
|
||||
"nlp_settings",
|
||||
"offline",
|
||||
"live-chat-tester",
|
||||
],
|
||||
ns: ["translation", "chatbot_settings", "contact", "nlp_settings"],
|
||||
interpolation: {
|
||||
escapeValue: false,
|
||||
},
|
||||
|
@ -32,6 +32,7 @@ export const ROUTES = {
|
||||
CSRF: "/csrftoken",
|
||||
BOTSTATS: "/botstats",
|
||||
REFRESH_TRANSLATIONS: "/translation/refresh",
|
||||
FETCH_REMOTE_I18N: "/i18n",
|
||||
RESET: "/user/reset",
|
||||
NLP_SAMPLE_IMPORT: "/nlpsample/import",
|
||||
NLP_SAMPLE_PREDICT: "/nlpsample/message",
|
||||
@ -190,6 +191,12 @@ export class ApiClient {
|
||||
return data;
|
||||
}
|
||||
|
||||
async fetchRemoteI18n() {
|
||||
const { data } = await this.request.get(ROUTES.FETCH_REMOTE_I18N);
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
async reset(token: string, payload: IResetPayload) {
|
||||
const { data } = await this.request.post<
|
||||
IResetPayload,
|
||||
|
Loading…
Reference in New Issue
Block a user