diff --git a/api/.dockerignore b/api/.dockerignore index b55f7aab..1fa84c27 100644 --- a/api/.dockerignore +++ b/api/.dockerignore @@ -14,3 +14,4 @@ test *.mock.ts __mock__ __test__ +.hexabot diff --git a/api/.gitignore b/api/.gitignore index a3d0bf5f..e1fa3152 100644 --- a/api/.gitignore +++ b/api/.gitignore @@ -1,3 +1,4 @@ +.hexabot/ node_modules/ dist/ coverage/ diff --git a/api/package-lock.json b/api/package-lock.json index 4829374f..7486e284 100644 --- a/api/package-lock.json +++ b/api/package-lock.json @@ -11355,6 +11355,11 @@ "he": "bin/he" } }, + "node_modules/hexabot-plugin-medmar": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/hexabot-plugin-medmar/-/hexabot-plugin-medmar-2.0.1.tgz", + "integrity": "sha512-qT2wG+vPjgBD9PZYl1EqzZPqVpoOrpldRUEDiwuzIKkIHEdfL6Q5Tr3rcgnYqFIPimVQhn/0tMgd1yQLD1dCPQ==" + }, "node_modules/hexoid": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/hexoid/-/hexoid-1.0.0.tgz", @@ -19437,4 +19442,4 @@ "dev": true } } -} +} \ No newline at end of file diff --git a/api/package.json b/api/package.json index 67f15f52..55bf1738 100644 --- a/api/package.json +++ b/api/package.json @@ -8,12 +8,18 @@ "scripts": { "preinstall": "node merge-extensions-deps.js", "postinstall": "patch-package", - "build": "nest build", - "format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"", + "build:clean": "rm -rf src/.hexabot", + "build:channels": "mkdir -p src/.hexabot/channels && cp -R node_modules/hexabot-channel-* src/.hexabot/channels/", + "build:helpers": "mkdir -p src/.hexabot/helpers && cp -R node_modules/hexabot-helper-* src/.hexabot/helpers/", + "build:plugins": "mkdir -p src/.hexabot/plugins && cp -R node_modules/hexabot-plugin-* src/.hexabot/plugins/", + "build:extensions": "npm run build:channels && npm run build:helpers && npm run build:plugins", + "build:prepare": "npm run build:clean && npm run build:extensions", + "build": "npm run build:prepare && nest build", + "format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\" \"libs/**/*.ts\"", "start": "nest start", "doc": "npx @compodoc/compodoc --hideGenerator -p tsconfig.doc.json -s -r 9003 -w", - "start:dev": "nest start --watch", - "start:debug": "nest start --debug 0.0.0.0:9229 --watch", + "start:dev": "npm run build:prepare && nest start --watch", + "start:debug": "npm run build:prepare && nest start --debug 0.0.0.0:9229 --watch", "start:prod": "node dist/main", "lint": "eslint \"{src,apps,libs,test}/**/*.ts\"", "lint:fix": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix", diff --git a/api/src/attachment/services/attachment.service.ts b/api/src/attachment/services/attachment.service.ts index 33d6300c..3f6077ce 100644 --- a/api/src/attachment/services/attachment.service.ts +++ b/api/src/attachment/services/attachment.service.ts @@ -108,13 +108,13 @@ export class AttachmentService extends BaseService { await this.getStoragePlugin().uploadAvatar(picture); this.logger.log( `Profile picture uploaded successfully to ${ - this.getStoragePlugin().id + this.getStoragePlugin().name }`, ); } catch (err) { this.logger.error( `Error while uploading profile picture to ${ - this.getStoragePlugin().id + this.getStoragePlugin().name }`, err, ); diff --git a/api/src/channel/channel.controller.ts b/api/src/channel/channel.controller.ts index e2d74f49..0a8d5796 100644 --- a/api/src/channel/channel.controller.ts +++ b/api/src/channel/channel.controller.ts @@ -23,7 +23,7 @@ export class ChannelController { getChannels(): { name: string }[] { return this.channelService.getAll().map((handler) => { return { - name: handler.getChannel(), + name: handler.getName(), }; }); } diff --git a/api/src/channel/channel.middleware.ts b/api/src/channel/channel.middleware.ts index fd694987..f5a49796 100644 --- a/api/src/channel/channel.middleware.ts +++ b/api/src/channel/channel.middleware.ts @@ -7,7 +7,7 @@ */ import { Injectable, NestMiddleware } from '@nestjs/common'; -import { Request, Response, NextFunction } from 'express'; +import { NextFunction, Request, Response } from 'express'; import { ChannelService } from './channel.service'; @@ -20,7 +20,9 @@ export class ChannelMiddleware implements NestMiddleware { try { const [_, path, channelName] = req.path.split('/'); if (path === 'webhook' && channelName) { - const channel = this.channelService.getChannelHandler(channelName); + const channel = this.channelService.getChannelHandler( + `${channelName}-channel`, + ); if (channel) { return await channel.middleware(req, res, next); } diff --git a/api/src/channel/channel.module.ts b/api/src/channel/channel.module.ts index c5732dcb..6fadeb19 100644 --- a/api/src/channel/channel.module.ts +++ b/api/src/channel/channel.module.ts @@ -7,7 +7,12 @@ */ import { HttpModule } from '@nestjs/axios'; -import { MiddlewareConsumer, Module, RequestMethod } from '@nestjs/common'; +import { + Global, + MiddlewareConsumer, + Module, + RequestMethod, +} from '@nestjs/common'; import { InjectDynamicProviders } from 'nestjs-dynamic-providers'; import { AttachmentModule } from '@/attachment/attachment.module'; @@ -23,6 +28,7 @@ export interface ChannelModuleOptions { folder: string; } +@Global() @InjectDynamicProviders('dist/extensions/**/*.channel.js') @Module({ controllers: [WebhookController, ChannelController], diff --git a/api/src/channel/channel.service.ts b/api/src/channel/channel.service.ts index e87d0f3f..62188a05 100644 --- a/api/src/channel/channel.service.ts +++ b/api/src/channel/channel.service.ts @@ -23,10 +23,11 @@ import { SocketRequest } from '@/websocket/utils/socket-request'; import { SocketResponse } from '@/websocket/utils/socket-response'; import ChannelHandler from './lib/Handler'; +import { ChannelName } from './types'; @Injectable() export class ChannelService { - private registry: Map> = new Map(); + private registry: Map> = new Map(); constructor( private readonly logger: LoggerService, @@ -40,7 +41,7 @@ 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>( + public setChannel>( name: T, channel: C, ) { @@ -62,9 +63,9 @@ export class ChannelService { * @param name - The name of the channel to find. * @returns The channel handler associated with the specified name, or undefined if the channel is not found. */ - public findChannel(name: string) { + public findChannel(name: ChannelName) { return this.getAll().find((c) => { - return c.getChannel() === name; + return c.getName() === name; }); } @@ -74,7 +75,7 @@ export class ChannelService { * @param channelName - The name of the channel (messenger, offline, ...). * @returns The handler for the specified channel. */ - public getChannelHandler>( + public getChannelHandler>( name: T, ): C { const handler = this.registry.get(name); @@ -93,7 +94,7 @@ export class ChannelService { * @returns A promise that resolves when the handler has processed the request. */ async handle(channel: string, req: Request, res: Response): Promise { - const handler = this.getChannelHandler(channel); + const handler = this.getChannelHandler(`${channel}-channel`); handler.handle(req, res); } diff --git a/api/src/channel/lib/EventWrapper.ts b/api/src/channel/lib/EventWrapper.ts index 0c7f8f23..9456e200 100644 --- a/api/src/channel/lib/EventWrapper.ts +++ b/api/src/channel/lib/EventWrapper.ts @@ -55,7 +55,7 @@ export default abstract class EventWrapper< toString() { return JSON.stringify( { - handler: this._handler.getChannel(), + handler: this._handler.getName(), channelData: this.getChannelData(), sender: this.getSender(), recipient: this.getRecipientForeignId(), diff --git a/api/src/channel/lib/Handler.ts b/api/src/channel/lib/Handler.ts index 44d13b7e..4b265b23 100644 --- a/api/src/channel/lib/Handler.ts +++ b/api/src/channel/lib/Handler.ts @@ -6,7 +6,9 @@ * 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 path from 'path'; + +import { Injectable, OnModuleInit } from '@nestjs/common'; import { NextFunction, Request, Response } from 'express'; import { Attachment } from '@/attachment/schemas/attachment.schema'; @@ -17,35 +19,39 @@ import { } from '@/chat/schemas/types/message'; import { LoggerService } from '@/logger/logger.service'; import { SettingService } from '@/setting/services/setting.service'; -import { hyphenToUnderscore } from '@/utils/helpers/misc'; +import { Extension } from '@/utils/generics/extension'; import { SocketRequest } from '@/websocket/utils/socket-request'; import { SocketResponse } from '@/websocket/utils/socket-response'; import { ChannelService } from '../channel.service'; -import { ChannelSetting } from '../types'; +import { ChannelName, ChannelSetting } from '../types'; import EventWrapper from './EventWrapper'; @Injectable() -export default abstract class ChannelHandler { - private readonly name: N; - +export default abstract class ChannelHandler< + N extends ChannelName = ChannelName, + > + extends Extension + implements OnModuleInit +{ private readonly settings: ChannelSetting[]; constructor( name: N, - settings: ChannelSetting[], protected readonly settingService: SettingService, private readonly channelService: ChannelService, protected readonly logger: LoggerService, ) { - this.name = name; - this.settings = settings; + super(name); + // eslint-disable-next-line @typescript-eslint/no-var-requires + this.settings = require(path.join(this.getPath(), 'settings')).default; } - onModuleInit() { + async onModuleInit() { + await super.onModuleInit(); this.channelService.setChannel( - this.getChannel(), + this.getName() as ChannelName, this as unknown as ChannelHandler, ); this.setup(); @@ -53,7 +59,7 @@ export default abstract class ChannelHandler { async setup() { await this.settingService.seedIfNotExist( - this.getChannel(), + this.getName(), this.settings.map((s, i) => ({ ...s, weight: i + 1, @@ -62,22 +68,6 @@ export default abstract class ChannelHandler { this.init(); } - /** - * Returns the channel's name - * @returns Channel's name - */ - getChannel() { - return this.name; - } - - /** - * Returns the channel's group - * @returns Channel's group - */ - protected getGroup() { - return hyphenToUnderscore(this.getChannel()) as ChannelSetting['group']; - } - /** * Returns the channel's settings * @returns Channel's settings @@ -85,7 +75,7 @@ export default abstract class ChannelHandler { async getSettings>() { const settings = await this.settingService.getSettings(); // @ts-expect-error workaround typing - return settings[this.getGroup() as keyof Settings] as Settings[S]; + return settings[this.getNamespace() as keyof Settings] as Settings[S]; } /** diff --git a/api/src/channel/lib/__test__/subscriber.mock.ts b/api/src/channel/lib/__test__/subscriber.mock.ts index 5b40ad15..e3c17f22 100644 --- a/api/src/channel/lib/__test__/subscriber.mock.ts +++ b/api/src/channel/lib/__test__/subscriber.mock.ts @@ -25,7 +25,7 @@ export const subscriberInstance: Subscriber = { lastvisit: new Date(), retainedFrom: new Date(), channel: { - name: 'offline', + name: 'offline-channel', }, labels: [], ...modelInstance, diff --git a/api/src/channel/types.ts b/api/src/channel/types.ts index 4b9f3329..9f19d745 100644 --- a/api/src/channel/types.ts +++ b/api/src/channel/types.ts @@ -1,5 +1,7 @@ import { SettingCreateDto } from '@/setting/dto/setting.dto'; +export type ChannelName = `${string}-channel`; + export type ChannelSetting = Omit< SettingCreateDto, 'group' | 'weight' diff --git a/api/src/chat/controllers/block.controller.ts b/api/src/chat/controllers/block.controller.ts index e68e0788..e3145e51 100644 --- a/api/src/chat/controllers/block.controller.ts +++ b/api/src/chat/controllers/block.controller.ts @@ -27,7 +27,7 @@ import { CsrfInterceptor } from '@/interceptors/csrf.interceptor'; import { LoggerService } from '@/logger/logger.service'; import { BaseBlockPlugin } from '@/plugins/base-block-plugin'; import { PluginService } from '@/plugins/plugins.service'; -import { PluginType } from '@/plugins/types'; +import { PluginName, PluginType } from '@/plugins/types'; import { UserService } from '@/user/services/user.service'; import { BaseController } from '@/utils/generics/base-controller'; import { DeleteResult } from '@/utils/generics/base-repository'; @@ -85,20 +85,23 @@ export class BlockController extends BaseController< /** * Retrieves a custom block settings for a specific plugin. * - * @param pluginId - The name of the plugin for which settings are to be retrieved. + * @param pluginName - The name of the plugin for which settings are to be retrieved. * * @returns An array containing the settings of the specified plugin. */ @Get('customBlocks/settings') - findSettings(@Query('plugin') pluginId: string) { + findSettings(@Query('plugin') pluginName: PluginName) { try { - if (!pluginId) { + if (!pluginName) { throw new BadRequestException( 'Plugin id must be supplied as a query param', ); } - const plugin = this.pluginsService.getPlugin(PluginType.block, pluginId); + const plugin = this.pluginsService.getPlugin( + PluginType.block, + pluginName, + ); if (!plugin) { throw new NotFoundException('Plugin Not Found'); @@ -122,11 +125,12 @@ export class BlockController extends BaseController< const plugins = this.pluginsService .getAllByType(PluginType.block) .map((p) => ({ - id: p.id, + id: p.getName(), + namespace: p.getNamespace(), template: { ...p.template, message: { - plugin: p.id, + plugin: p.name, args: p.settings.reduce( (acc, setting) => { acc[setting.label] = setting.value; diff --git a/api/src/chat/schemas/types/channel.ts b/api/src/chat/schemas/types/channel.ts index 306fee5d..38a69083 100644 --- a/api/src/chat/schemas/types/channel.ts +++ b/api/src/chat/schemas/types/channel.ts @@ -6,8 +6,10 @@ * 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 { ChannelName } from '@/channel/types'; + interface BaseChannelData { - name: string; // channel name + name: ChannelName; // channel name isSocket?: boolean; type?: any; //TODO: type has to be checked } diff --git a/api/src/chat/schemas/types/context.ts b/api/src/chat/schemas/types/context.ts index 7fdc08e9..cae1ccf6 100644 --- a/api/src/chat/schemas/types/context.ts +++ b/api/src/chat/schemas/types/context.ts @@ -6,6 +6,7 @@ * 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 { ChannelName } from '@/channel/types'; import { Nlp } from '@/helper/types'; import { Subscriber } from '../subscriber.schema'; @@ -13,7 +14,7 @@ import { Subscriber } from '../subscriber.schema'; import { Payload } from './quick-reply'; export interface Context { - channel?: string; + channel?: ChannelName; text?: string; payload?: Payload | string; nlp?: Nlp.ParseEntities | null; diff --git a/api/src/chat/services/block.service.spec.ts b/api/src/chat/services/block.service.spec.ts index 72fae56f..20fe0e18 100644 --- a/api/src/chat/services/block.service.spec.ts +++ b/api/src/chat/services/block.service.spec.ts @@ -222,7 +222,7 @@ describe('BlockService', () => { describe('match', () => { const handlerMock = { - getChannel: jest.fn(() => OFFLINE_CHANNEL_NAME), + getName: jest.fn(() => OFFLINE_CHANNEL_NAME), } as any as OfflineHandler; const offlineEventGreeting = new OfflineEventWrapper( handlerMock, @@ -502,7 +502,7 @@ describe('BlockService', () => { describe('processText', () => { const context: Context = { ...contextGetStartedInstance, - channel: 'offline', + channel: 'offline-channel', text: '', payload: undefined, nlp: { entities: [] }, diff --git a/api/src/chat/services/block.service.ts b/api/src/chat/services/block.service.ts index 73ffa57a..f7353b73 100644 --- a/api/src/chat/services/block.service.ts +++ b/api/src/chat/services/block.service.ts @@ -17,7 +17,7 @@ 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 { PluginType } from '@/plugins/types'; +import { PluginName, PluginType } from '@/plugins/types'; import { SettingService } from '@/setting/services/setting.service'; import { BaseService } from '@/utils/generics/base-service'; import { getRandom } from '@/utils/helpers/safeRandom'; @@ -71,7 +71,7 @@ export class BlockService extends BaseService { const payload = event.getPayload(); // Perform a filter on the specific channels - const channel = event.getHandler().getChannel(); + const channel = event.getHandler().getName(); blocks = blocks.filter((b) => { return ( !b.trigger_channels || @@ -593,7 +593,7 @@ export class BlockService extends BaseService { } else if (blockMessage && 'plugin' in blockMessage) { const plugin = this.pluginService.findPlugin( PluginType.block, - blockMessage.plugin, + blockMessage.plugin as PluginName, ); // Process custom plugin block try { diff --git a/api/src/chat/services/bot.service.spec.ts b/api/src/chat/services/bot.service.spec.ts index 3134452d..4a864f16 100644 --- a/api/src/chat/services/bot.service.spec.ts +++ b/api/src/chat/services/bot.service.spec.ts @@ -224,7 +224,7 @@ describe('BlockService', () => { nlp: null, payload: null, attempt: 0, - channel: 'offline', + channel: 'offline-channel', text: offlineEventText.data.text, }, }); @@ -283,7 +283,7 @@ describe('BlockService', () => { nlp: null, payload: null, attempt: 0, - channel: 'offline', + channel: 'offline-channel', text: offlineEventText.data.text, }, }); diff --git a/api/src/chat/services/chat.service.ts b/api/src/chat/services/chat.service.ts index 5fdc2d67..6342127b 100644 --- a/api/src/chat/services/chat.service.ts +++ b/api/src/chat/services/chat.service.ts @@ -246,7 +246,7 @@ export class ChatService { this.eventEmitter.emit('hook:stats:entry', 'new_users', 'New users'); subscriberData.channel = { ...event.getChannelData(), - name: handler.getChannel(), + name: handler.getName(), }; subscriber = await this.subscriberService.create(subscriberData); } else { diff --git a/api/src/chat/services/conversation.service.ts b/api/src/chat/services/conversation.service.ts index b6ebb14e..7845427d 100644 --- a/api/src/chat/services/conversation.service.ts +++ b/api/src/chat/services/conversation.service.ts @@ -9,6 +9,7 @@ import { Injectable, Logger } from '@nestjs/common'; import EventWrapper from '@/channel/lib/EventWrapper'; +import { ChannelName } from '@/channel/types'; import { LoggerService } from '@/logger/logger.service'; import { BaseService } from '@/utils/generics/base-service'; @@ -69,7 +70,7 @@ export class ConversationService extends BaseService< const msgType = event.getMessageType(); const profile = event.getSender(); // Capture channel specific context data - convo.context.channel = event.getHandler().getChannel(); + convo.context.channel = event.getHandler().getName() as ChannelName; convo.context.text = event.getText(); convo.context.payload = event.getPayload(); convo.context.nlp = event.getNLP(); diff --git a/api/src/extensions/channels/live-chat-tester/i18n/en/title.json b/api/src/extensions/channels/live-chat-tester/i18n/en/title.json index 67ff4226..41fcdeba 100644 --- a/api/src/extensions/channels/live-chat-tester/i18n/en/title.json +++ b/api/src/extensions/channels/live-chat-tester/i18n/en/title.json @@ -1,3 +1,3 @@ { - "live_chat_tester": "Live Chat Tester" + "live_chat_tester_channel": "Live Chat Tester" } diff --git a/api/src/extensions/channels/live-chat-tester/i18n/fr/title.json b/api/src/extensions/channels/live-chat-tester/i18n/fr/title.json index 6671fd6a..e4f67ea7 100644 --- a/api/src/extensions/channels/live-chat-tester/i18n/fr/title.json +++ b/api/src/extensions/channels/live-chat-tester/i18n/fr/title.json @@ -1,3 +1,3 @@ { - "live_chat_tester": "Testeur Live Chat" + "live_chat_tester_channel": "Testeur Live Chat" } diff --git a/api/src/extensions/channels/live-chat-tester/index.channel.ts b/api/src/extensions/channels/live-chat-tester/index.channel.ts index 0acddbac..6a47a317 100644 --- a/api/src/extensions/channels/live-chat-tester/index.channel.ts +++ b/api/src/extensions/channels/live-chat-tester/index.channel.ts @@ -21,10 +21,7 @@ import { WebsocketGateway } from '@/websocket/websocket.gateway'; import BaseWebChannelHandler from '../offline/base-web-channel'; -import { - DEFAULT_LIVE_CHAT_TEST_SETTINGS, - LIVE_CHAT_TEST_CHANNEL_NAME, -} from './settings'; +import { LIVE_CHAT_TEST_CHANNEL_NAME } from './settings'; @Injectable() export default class LiveChatTesterHandler extends BaseWebChannelHandler< @@ -44,7 +41,6 @@ export default class LiveChatTesterHandler extends BaseWebChannelHandler< ) { super( LIVE_CHAT_TEST_CHANNEL_NAME, - DEFAULT_LIVE_CHAT_TEST_SETTINGS, settingService, channelService, logger, @@ -57,4 +53,8 @@ export default class LiveChatTesterHandler extends BaseWebChannelHandler< websocketGateway, ); } + + getPath(): string { + return __dirname; + } } diff --git a/api/src/extensions/channels/live-chat-tester/index.d.ts b/api/src/extensions/channels/live-chat-tester/index.d.ts index 27efb2d0..f555b69a 100644 --- a/api/src/extensions/channels/live-chat-tester/index.d.ts +++ b/api/src/extensions/channels/live-chat-tester/index.d.ts @@ -1,5 +1,4 @@ -import { - DEFAULT_LIVE_CHAT_TEST_SETTINGS, +import DEFAULT_LIVE_CHAT_TEST_SETTINGS, { LIVE_CHAT_TEST_GROUP_NAME, } from './settings'; diff --git a/api/src/extensions/channels/live-chat-tester/package.json b/api/src/extensions/channels/live-chat-tester/package.json new file mode 100644 index 00000000..dd5dcbf6 --- /dev/null +++ b/api/src/extensions/channels/live-chat-tester/package.json @@ -0,0 +1,7 @@ +{ + "name": "hexabot-channel-live-chat-tester", + "version": "2.0.0", + "description": "The Web Channel Extension for Hexabot Chatbot / Agent Builder for website integration", + "author": "Hexastack", + "license": "AGPL-3.0-only" +} diff --git a/api/src/extensions/channels/live-chat-tester/settings.ts b/api/src/extensions/channels/live-chat-tester/settings.ts index e8afc9ae..71d21622 100644 --- a/api/src/extensions/channels/live-chat-tester/settings.ts +++ b/api/src/extensions/channels/live-chat-tester/settings.ts @@ -12,11 +12,11 @@ import { SettingType } from '@/setting/schemas/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-channel'; -export const LIVE_CHAT_TEST_GROUP_NAME = 'live_chat_tester'; +export const LIVE_CHAT_TEST_GROUP_NAME = 'live_chat_tester_channel'; -export const DEFAULT_LIVE_CHAT_TEST_SETTINGS = [ +export default [ { group: LIVE_CHAT_TEST_GROUP_NAME, label: Offline.SettingLabel.verification_token, diff --git a/api/src/extensions/channels/offline/__test__/index.spec.ts b/api/src/extensions/channels/offline/__test__/index.spec.ts index b93b16cf..a25ed05d 100644 --- a/api/src/extensions/channels/offline/__test__/index.spec.ts +++ b/api/src/extensions/channels/offline/__test__/index.spec.ts @@ -131,7 +131,7 @@ describe('Offline Handler', () => { it('should have correct name', () => { expect(handler).toBeDefined(); - expect(handler.getChannel()).toEqual('offline'); + expect(handler.getName()).toEqual('offline-channel'); }); it('should format text properly', () => { @@ -192,7 +192,7 @@ describe('Offline Handler', () => { agent: req.headers['user-agent'], ipAddress: '0.0.0.0', isSocket: false, - name: 'offline', + name: 'offline-channel', }, country: '', first_name: req.query.first_name, diff --git a/api/src/extensions/channels/offline/base-web-channel.ts b/api/src/extensions/channels/offline/base-web-channel.ts index 65d0c4d0..aefdd4ef 100644 --- a/api/src/extensions/channels/offline/base-web-channel.ts +++ b/api/src/extensions/channels/offline/base-web-channel.ts @@ -22,7 +22,7 @@ import { AttachmentService } from '@/attachment/services/attachment.service'; import { ChannelService } from '@/channel/channel.service'; import EventWrapper from '@/channel/lib/EventWrapper'; import ChannelHandler from '@/channel/lib/Handler'; -import { ChannelSetting } from '@/channel/types'; +import { ChannelName } from '@/channel/types'; import { MessageCreateDto } from '@/chat/dto/message.dto'; import { SubscriberCreateDto } from '@/chat/dto/subscriber.dto'; import { VIEW_MORE_PAYLOAD } from '@/chat/helpers/constants'; @@ -63,12 +63,11 @@ import { Offline } from './types'; import OfflineEventWrapper from './wrapper'; @Injectable() -export default class BaseWebChannelHandler< - N extends string, +export default abstract class BaseWebChannelHandler< + N extends ChannelName, > extends ChannelHandler { constructor( name: N, - settings: ChannelSetting[], settingService: SettingService, channelService: ChannelService, logger: LoggerService, @@ -80,7 +79,7 @@ export default class BaseWebChannelHandler< protected readonly menuService: MenuService, private readonly websocketGateway: WebsocketGateway, ) { - super(name, settings, settingService, channelService, logger); + super(name, settingService, channelService, logger); } /** @@ -102,7 +101,7 @@ export default class BaseWebChannelHandler< const settings = await this.getSettings(); const handshake = client.handshake; const { channel } = handshake.query; - if (channel !== this.getChannel()) { + if (channel !== this.getName()) { return; } try { @@ -464,7 +463,7 @@ export default class BaseWebChannelHandler< retainedFrom: new Date(), channel: { ...channelData, - name: this.getChannel(), + name: this.getName() as ChannelName, }, language: '', locale: '', diff --git a/api/src/extensions/channels/offline/i18n/en/title.json b/api/src/extensions/channels/offline/i18n/en/title.json index 74d381c7..d6bda3c9 100644 --- a/api/src/extensions/channels/offline/i18n/en/title.json +++ b/api/src/extensions/channels/offline/i18n/en/title.json @@ -1,3 +1,3 @@ { - "offline": "Canal Web" + "offline_channel": "Web Channel" } diff --git a/api/src/extensions/channels/offline/i18n/fr/title.json b/api/src/extensions/channels/offline/i18n/fr/title.json index 6671fd6a..d9815c96 100644 --- a/api/src/extensions/channels/offline/i18n/fr/title.json +++ b/api/src/extensions/channels/offline/i18n/fr/title.json @@ -1,3 +1,3 @@ { - "live_chat_tester": "Testeur Live Chat" + "offline_channel": "Canal Web" } diff --git a/api/src/extensions/channels/offline/index.channel.ts b/api/src/extensions/channels/offline/index.channel.ts index e7be89ae..db1073f1 100644 --- a/api/src/extensions/channels/offline/index.channel.ts +++ b/api/src/extensions/channels/offline/index.channel.ts @@ -20,7 +20,7 @@ import { SettingService } from '@/setting/services/setting.service'; import { WebsocketGateway } from '@/websocket/websocket.gateway'; import BaseWebChannelHandler from './base-web-channel'; -import { DEFAULT_OFFLINE_SETTINGS, OFFLINE_CHANNEL_NAME } from './settings'; +import { OFFLINE_CHANNEL_NAME } from './settings'; @Injectable() export default class OfflineHandler extends BaseWebChannelHandler< @@ -40,7 +40,6 @@ export default class OfflineHandler extends BaseWebChannelHandler< ) { super( OFFLINE_CHANNEL_NAME, - DEFAULT_OFFLINE_SETTINGS, settingService, channelService, logger, @@ -53,4 +52,8 @@ export default class OfflineHandler extends BaseWebChannelHandler< websocketGateway, ); } + + getPath(): string { + return __dirname; + } } diff --git a/api/src/extensions/channels/offline/index.d.ts b/api/src/extensions/channels/offline/index.d.ts index 39a1b26a..deede785 100644 --- a/api/src/extensions/channels/offline/index.d.ts +++ b/api/src/extensions/channels/offline/index.d.ts @@ -1,4 +1,4 @@ -import { DEFAULT_OFFLINE_SETTINGS, OFFLINE_GROUP_NAME } from './settings'; +import DEFAULT_OFFLINE_SETTINGS, { OFFLINE_GROUP_NAME } from './settings'; declare global { interface Settings extends SettingTree {} diff --git a/api/src/extensions/channels/offline/package.json b/api/src/extensions/channels/offline/package.json new file mode 100644 index 00000000..7c33567d --- /dev/null +++ b/api/src/extensions/channels/offline/package.json @@ -0,0 +1,7 @@ +{ + "name": "hexabot-channel-offline", + "version": "2.0.0", + "description": "The Web Channel Extension for Hexabot Chatbot / Agent Builder for website integration", + "author": "Hexastack", + "license": "AGPL-3.0-only" +} diff --git a/api/src/extensions/channels/offline/settings.ts b/api/src/extensions/channels/offline/settings.ts index 4d93c56e..3d22d9a1 100644 --- a/api/src/extensions/channels/offline/settings.ts +++ b/api/src/extensions/channels/offline/settings.ts @@ -11,11 +11,11 @@ import { SettingType } from '@/setting/schemas/types'; import { Offline } from './types'; -export const OFFLINE_CHANNEL_NAME = 'offline' as const; +export const OFFLINE_CHANNEL_NAME = 'offline-channel' as const; -export const OFFLINE_GROUP_NAME = OFFLINE_CHANNEL_NAME; +export const OFFLINE_GROUP_NAME = 'offline_channel'; -export const DEFAULT_OFFLINE_SETTINGS = [ +export default [ { group: OFFLINE_GROUP_NAME, label: Offline.SettingLabel.verification_token, diff --git a/api/src/extensions/channels/offline/wrapper.ts b/api/src/extensions/channels/offline/wrapper.ts index 5ab278d0..0331f5a7 100644 --- a/api/src/extensions/channels/offline/wrapper.ts +++ b/api/src/extensions/channels/offline/wrapper.ts @@ -7,6 +7,7 @@ */ import EventWrapper from '@/channel/lib/EventWrapper'; +import { ChannelName } from '@/channel/types'; import { AttachmentForeignKey, AttachmentPayload, @@ -67,7 +68,8 @@ type OfflineEventAdapter = }; export default class OfflineEventWrapper< - T extends BaseWebChannelHandler = BaseWebChannelHandler, + T extends + BaseWebChannelHandler = BaseWebChannelHandler, > extends EventWrapper { /** * Constructor : channel's event wrapper diff --git a/api/src/extensions/helpers/core-nlu/__test__/index.spec.ts b/api/src/extensions/helpers/core-nlu/__test__/index.spec.ts index 9870b8a5..ef5aef23 100644 --- a/api/src/extensions/helpers/core-nlu/__test__/index.spec.ts +++ b/api/src/extensions/helpers/core-nlu/__test__/index.spec.ts @@ -58,7 +58,7 @@ describe('Core NLU Helper', () => { provide: SettingService, useValue: { getSettings: jest.fn(() => ({ - core_nlu: { + core_nlu_helper: { endpoint: 'path', token: 'token', threshold: '0.5', @@ -121,7 +121,7 @@ describe('Core NLU Helper', () => { true, ); const settings = await settingService.getSettings(); - const threshold = settings.core_nlu.threshold; + const threshold = settings.core_nlu_helper.threshold; const thresholdGuess = { entities: nlpBestGuess.entities.filter( (g) => diff --git a/api/src/extensions/helpers/core-nlu/i18n/en/title.json b/api/src/extensions/helpers/core-nlu/i18n/en/title.json index 70534a3d..ded4a46a 100644 --- a/api/src/extensions/helpers/core-nlu/i18n/en/title.json +++ b/api/src/extensions/helpers/core-nlu/i18n/en/title.json @@ -1,3 +1,3 @@ { - "core_nlu": "Core NLU Engine" + "core_nlu_helper": "Core NLU Engine" } diff --git a/api/src/extensions/helpers/core-nlu/i18n/fr/title.json b/api/src/extensions/helpers/core-nlu/i18n/fr/title.json index 70534a3d..ded4a46a 100644 --- a/api/src/extensions/helpers/core-nlu/i18n/fr/title.json +++ b/api/src/extensions/helpers/core-nlu/i18n/fr/title.json @@ -1,3 +1,3 @@ { - "core_nlu": "Core NLU Engine" + "core_nlu_helper": "Core NLU Engine" } diff --git a/api/src/extensions/helpers/core-nlu/index.d.ts b/api/src/extensions/helpers/core-nlu/index.d.ts index 00c0d3c8..3258cffc 100644 --- a/api/src/extensions/helpers/core-nlu/index.d.ts +++ b/api/src/extensions/helpers/core-nlu/index.d.ts @@ -1,4 +1,4 @@ -import { CORE_NLU_HELPER_GROUP, CORE_NLU_HELPER_SETTINGS } from './settings'; +import CORE_NLU_HELPER_SETTINGS, { CORE_NLU_HELPER_GROUP } from './settings'; declare global { interface Settings extends SettingTree {} diff --git a/api/src/extensions/helpers/core-nlu/index.helper.ts b/api/src/extensions/helpers/core-nlu/index.helper.ts index 58327ef1..6a2ebd39 100644 --- a/api/src/extensions/helpers/core-nlu/index.helper.ts +++ b/api/src/extensions/helpers/core-nlu/index.helper.ts @@ -20,7 +20,7 @@ import { NlpValue } from '@/nlp/schemas/nlp-value.schema'; import { SettingService } from '@/setting/services/setting.service'; import { buildURL } from '@/utils/helpers/URL'; -import { CORE_NLU_HELPER_NAME, CORE_NLU_HELPER_SETTINGS } from './settings'; +import { CORE_NLU_HELPER_NAME } from './settings'; import { NlpParseResultType, RasaNlu } from './types'; @Injectable() @@ -34,13 +34,11 @@ export default class CoreNluHelper extends BaseNlpHelper< private readonly httpService: HttpService, private readonly languageService: LanguageService, ) { - super( - CORE_NLU_HELPER_NAME, - CORE_NLU_HELPER_SETTINGS, - settingService, - helperService, - logger, - ); + super(CORE_NLU_HELPER_NAME, settingService, helperService, logger); + } + + getPath() { + return __dirname; } /** diff --git a/api/src/extensions/helpers/core-nlu/package.json b/api/src/extensions/helpers/core-nlu/package.json index 02413a2d..c35cb440 100644 --- a/api/src/extensions/helpers/core-nlu/package.json +++ b/api/src/extensions/helpers/core-nlu/package.json @@ -1,8 +1,8 @@ { - "name": "hexabot-core-nlu", + "name": "hexabot-helper-core-nlu", "version": "2.0.0", "description": "The Core NLU Helper Extension for Hexabot Chatbot / Agent Builder to enable the Intent Classification and Language Detection", "dependencies": {}, "author": "Hexastack", "license": "AGPL-3.0-only" -} +} \ No newline at end of file diff --git a/api/src/extensions/helpers/core-nlu/settings.ts b/api/src/extensions/helpers/core-nlu/settings.ts index 3a5f3d83..f8b11d84 100644 --- a/api/src/extensions/helpers/core-nlu/settings.ts +++ b/api/src/extensions/helpers/core-nlu/settings.ts @@ -1,11 +1,11 @@ import { HelperSetting } from '@/helper/types'; import { SettingType } from '@/setting/schemas/types'; -export const CORE_NLU_HELPER_NAME = 'core-nlu'; +export const CORE_NLU_HELPER_NAME = 'core-nlu-helper'; -export const CORE_NLU_HELPER_GROUP = 'core_nlu'; +export const CORE_NLU_HELPER_GROUP = 'core_nlu_helper'; -export const CORE_NLU_HELPER_SETTINGS = [ +export default [ { group: CORE_NLU_HELPER_GROUP, label: 'endpoint', diff --git a/api/src/extensions/helpers/ollama/i18n/en/title.json b/api/src/extensions/helpers/ollama/i18n/en/title.json index 85109553..a51ce009 100644 --- a/api/src/extensions/helpers/ollama/i18n/en/title.json +++ b/api/src/extensions/helpers/ollama/i18n/en/title.json @@ -1,3 +1,3 @@ { - "ollama": "Ollama" + "ollama_helper": "Ollama" } diff --git a/api/src/extensions/helpers/ollama/i18n/fr/title.json b/api/src/extensions/helpers/ollama/i18n/fr/title.json index 85109553..a51ce009 100644 --- a/api/src/extensions/helpers/ollama/i18n/fr/title.json +++ b/api/src/extensions/helpers/ollama/i18n/fr/title.json @@ -1,3 +1,3 @@ { - "ollama": "Ollama" + "ollama_helper": "Ollama" } diff --git a/api/src/extensions/helpers/ollama/index.d.ts b/api/src/extensions/helpers/ollama/index.d.ts index e922cc4f..80d7e74e 100644 --- a/api/src/extensions/helpers/ollama/index.d.ts +++ b/api/src/extensions/helpers/ollama/index.d.ts @@ -1,4 +1,4 @@ -import { OLLAMA_HELPER_GROUP, OLLAMA_HELPER_SETTINGS } from './settings'; +import OLLAMA_HELPER_SETTINGS, { OLLAMA_HELPER_GROUP } from './settings'; declare global { interface Settings extends SettingTree {} diff --git a/api/src/extensions/helpers/ollama/index.helper.ts b/api/src/extensions/helpers/ollama/index.helper.ts index f1d14fc4..cdc619fd 100644 --- a/api/src/extensions/helpers/ollama/index.helper.ts +++ b/api/src/extensions/helpers/ollama/index.helper.ts @@ -17,7 +17,7 @@ import { LoggerService } from '@/logger/logger.service'; import { Setting } from '@/setting/schemas/setting.schema'; import { SettingService } from '@/setting/services/setting.service'; -import { OLLAMA_HELPER_NAME, OLLAMA_HELPER_SETTINGS } from './settings'; +import { OLLAMA_HELPER_NAME } from './settings'; @Injectable() export default class OllamaLlmHelper @@ -36,13 +36,11 @@ export default class OllamaLlmHelper helperService: HelperService, protected readonly logger: LoggerService, ) { - super( - OLLAMA_HELPER_NAME, - OLLAMA_HELPER_SETTINGS, - settingService, - helperService, - logger, - ); + super('ollama-helper', settingService, helperService, logger); + } + + getPath(): string { + return __dirname; } async onApplicationBootstrap() { @@ -51,7 +49,7 @@ export default class OllamaLlmHelper this.client = new Ollama({ host: settings.api_url }); } - @OnEvent('hook:ollama:api_url') + @OnEvent('hook:ollama_helper:api_url') handleApiUrlChange(setting: Setting) { this.client = new Ollama({ host: setting.value }); } diff --git a/api/src/extensions/helpers/ollama/settings.ts b/api/src/extensions/helpers/ollama/settings.ts index 0d234134..16c61042 100644 --- a/api/src/extensions/helpers/ollama/settings.ts +++ b/api/src/extensions/helpers/ollama/settings.ts @@ -1,11 +1,13 @@ import { HelperSetting } from '@/helper/types'; import { SettingType } from '@/setting/schemas/types'; -export const OLLAMA_HELPER_NAME = 'ollama'; +export const OLLAMA_HELPER_NAME = 'ollama-helper'; -export const OLLAMA_HELPER_GROUP = 'ollama'; +export const OLLAMA_HELPER_GROUP: HyphenToUnderscore< + typeof OLLAMA_HELPER_NAME +> = 'ollama_helper'; -export const OLLAMA_HELPER_SETTINGS = [ +export default [ { label: 'api_url', group: OLLAMA_HELPER_GROUP, diff --git a/api/src/extensions/plugins/ollama/i18n/en/title.json b/api/src/extensions/plugins/ollama/i18n/en/title.json index 85109553..403d3c31 100644 --- a/api/src/extensions/plugins/ollama/i18n/en/title.json +++ b/api/src/extensions/plugins/ollama/i18n/en/title.json @@ -1,3 +1,3 @@ { - "ollama": "Ollama" + "ollama_plugin": "Ollama Plugin" } diff --git a/api/src/extensions/plugins/ollama/i18n/fr/title.json b/api/src/extensions/plugins/ollama/i18n/fr/title.json index 85109553..403d3c31 100644 --- a/api/src/extensions/plugins/ollama/i18n/fr/title.json +++ b/api/src/extensions/plugins/ollama/i18n/fr/title.json @@ -1,3 +1,3 @@ { - "ollama": "Ollama" + "ollama_plugin": "Ollama Plugin" } diff --git a/api/src/extensions/plugins/ollama/index.plugin.ts b/api/src/extensions/plugins/ollama/index.plugin.ts index bcf0ea10..0cfa2a73 100644 --- a/api/src/extensions/plugins/ollama/index.plugin.ts +++ b/api/src/extensions/plugins/ollama/index.plugin.ts @@ -14,14 +14,13 @@ import { HelperType } from '@/helper/types'; import { LoggerService } from '@/logger/logger.service'; import { BaseBlockPlugin } from '@/plugins/base-block-plugin'; import { PluginService } from '@/plugins/plugins.service'; +import { PluginBlockTemplate } from '@/plugins/types'; -import { OLLAMA_PLUGIN_SETTINGS } from './settings'; +import SETTINGS from './settings'; @Injectable() -export class OllamaPlugin extends BaseBlockPlugin< - typeof OLLAMA_PLUGIN_SETTINGS -> { - public readonly settings = OLLAMA_PLUGIN_SETTINGS; +export class OllamaPlugin extends BaseBlockPlugin { + template: PluginBlockTemplate = { name: 'Ollama Plugin' }; constructor( pluginService: PluginService, @@ -30,12 +29,11 @@ export class OllamaPlugin extends BaseBlockPlugin< private contentService: ContentService, private messageService: MessageService, ) { - super('ollama', OLLAMA_PLUGIN_SETTINGS, pluginService); + super('ollama-plugin', pluginService); + } - this.template = { name: 'Ollama Plugin' }; - this.effects = { - onStoreContextData: () => {}, - }; + getPath(): string { + return __dirname; } async process(block: Block, context: Context, _convId: string) { diff --git a/api/src/extensions/plugins/ollama/package.json b/api/src/extensions/plugins/ollama/package.json index 049ce581..a9b0f46b 100644 --- a/api/src/extensions/plugins/ollama/package.json +++ b/api/src/extensions/plugins/ollama/package.json @@ -1,11 +1,10 @@ { - "name": "hexabot-ollama", + "name": "hexabot-plugin-ollama", "version": "2.0.0", "description": "The Ollama Plugin Extension for Hexabot Chatbot / Agent Builder that provides a custom block for Generative AI + RAG", - "dependencies": {}, - "extensions": { + "dependencies": { "hexabot-helper-ollama": "2.0.0" }, "author": "Hexastack", "license": "AGPL-3.0-only" -} +} \ No newline at end of file diff --git a/api/src/extensions/plugins/ollama/settings.ts b/api/src/extensions/plugins/ollama/settings.ts index 4289d1cc..c051597c 100644 --- a/api/src/extensions/plugins/ollama/settings.ts +++ b/api/src/extensions/plugins/ollama/settings.ts @@ -1,7 +1,7 @@ import { PluginSetting } from '@/plugins/types'; import { SettingType } from '@/setting/schemas/types'; -export const OLLAMA_PLUGIN_SETTINGS = [ +export default [ { label: 'model', group: 'default', diff --git a/api/src/helper/helper.service.ts b/api/src/helper/helper.service.ts index ddd07d9d..fb3e6bfb 100644 --- a/api/src/helper/helper.service.ts +++ b/api/src/helper/helper.service.ts @@ -12,7 +12,7 @@ import { LoggerService } from '@/logger/logger.service'; import { SettingService } from '@/setting/services/setting.service'; import BaseHelper from './lib/base-helper'; -import { HelperRegistry, HelperType, TypeOfHelper } from './types'; +import { HelperName, HelperRegistry, HelperType, TypeOfHelper } from './types'; @Injectable() export class HelperService { @@ -33,7 +33,7 @@ export class HelperService { * * @param name - The helper to be registered. */ - public register>(helper: H) { + public register(helper: H) { const helpers = this.registry.get(helper.getType()); helpers.set(helper.getName(), helper); this.logger.log(`Helper "${helper.getName()}" has been registered!`); @@ -47,7 +47,7 @@ export class HelperService { * * @returns - The helper */ - public get(type: T, name: string) { + public get(type: T, name: HelperName) { const helpers = this.registry.get(type); if (!helpers.has(name)) { @@ -67,6 +67,16 @@ export class HelperService { return Array.from(helpers.values()); } + /** + * Retrieves all registered helpers as an array. + * + * @returns An array containing all the registered helpers. + */ + public getAll(): BaseHelper[] { + return Array.from(this.registry.values()) // Get all the inner maps + .flatMap((innerMap) => Array.from(innerMap.values())); // Flatten and get the values from each inner map + } + /** * Get a helper by class. * @@ -100,7 +110,7 @@ export class HelperService { const defaultHelper = this.get( HelperType.NLU, - settings.chatbot_settings.default_nlu_helper, + settings.chatbot_settings.default_nlu_helper as HelperName, ); if (!defaultHelper) { @@ -120,7 +130,7 @@ export class HelperService { const defaultHelper = this.get( HelperType.LLM, - settings.chatbot_settings.default_llm_helper, + settings.chatbot_settings.default_llm_helper as HelperName, ); if (!defaultHelper) { diff --git a/api/src/helper/lib/base-helper.ts b/api/src/helper/lib/base-helper.ts index 2597b175..4ef91ad0 100644 --- a/api/src/helper/lib/base-helper.ts +++ b/api/src/helper/lib/base-helper.ts @@ -6,33 +6,37 @@ * 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 { LoggerService } from '@nestjs/common'; +import path from 'path'; + +import { LoggerService, OnModuleInit } from '@nestjs/common'; import { SettingService } from '@/setting/services/setting.service'; -import { hyphenToUnderscore } from '@/utils/helpers/misc'; +import { Extension } from '@/utils/generics/extension'; import { HelperService } from '../helper.service'; -import { HelperSetting, HelperType } from '../types'; - -export default abstract class BaseHelper { - protected readonly name: N; +import { HelperName, HelperSetting, HelperType } from '../types'; +export default abstract class BaseHelper + extends Extension + implements OnModuleInit +{ protected readonly settings: HelperSetting[] = []; protected abstract type: HelperType; constructor( name: N, - settings: HelperSetting[], protected readonly settingService: SettingService, protected readonly helperService: HelperService, protected readonly logger: LoggerService, ) { - this.name = name; - this.settings = settings; + super(name); + // eslint-disable-next-line @typescript-eslint/no-var-requires + this.settings = require(path.join(this.getPath(), 'settings')).default; } - onModuleInit() { + async onModuleInit() { + await super.onModuleInit(); this.helperService.register(this); this.setup(); } @@ -47,23 +51,6 @@ export default abstract class BaseHelper { ); } - /** - * Returns the helper's name - * - * @returns Helper's name - */ - public getName() { - return this.name; - } - - /** - * Returns the helper's group - * @returns Helper's group - */ - protected getGroup() { - return hyphenToUnderscore(this.getName()) as HelperSetting['group']; - } - /** * Get the helper's type * @@ -81,6 +68,6 @@ export default abstract class BaseHelper { async getSettings>() { const settings = await this.settingService.getSettings(); // @ts-expect-error workaround typing - return settings[this.getGroup() as keyof Settings] as Settings[S]; + return settings[this.getNamespace() as keyof Settings] as Settings[S]; } } diff --git a/api/src/helper/lib/base-llm-helper.ts b/api/src/helper/lib/base-llm-helper.ts index 413e8598..7765738e 100644 --- a/api/src/helper/lib/base-llm-helper.ts +++ b/api/src/helper/lib/base-llm-helper.ts @@ -11,23 +11,22 @@ import { LoggerService } from '@/logger/logger.service'; import { SettingService } from '@/setting/services/setting.service'; import { HelperService } from '../helper.service'; -import { HelperSetting, HelperType } from '../types'; +import { HelperName, HelperType } from '../types'; import BaseHelper from './base-helper'; export default abstract class BaseLlmHelper< - N extends string, + N extends HelperName = HelperName, > extends BaseHelper { protected readonly type: HelperType = HelperType.LLM; constructor( name: N, - settings: HelperSetting[], settingService: SettingService, helperService: HelperService, logger: LoggerService, ) { - super(name, settings, settingService, helperService, logger); + super(name, settingService, helperService, logger); } /** diff --git a/api/src/helper/lib/base-nlp-helper.ts b/api/src/helper/lib/base-nlp-helper.ts index 302cdd17..ae2775b3 100644 --- a/api/src/helper/lib/base-nlp-helper.ts +++ b/api/src/helper/lib/base-nlp-helper.ts @@ -23,23 +23,23 @@ import { import { SettingService } from '@/setting/services/setting.service'; import { HelperService } from '../helper.service'; -import { HelperSetting, HelperType, Nlp } from '../types'; +import { HelperName, HelperType, Nlp } from '../types'; import BaseHelper from './base-helper'; +// eslint-disable-next-line prettier/prettier export default abstract class BaseNlpHelper< - N extends string, + N extends HelperName = HelperName, > extends BaseHelper { protected readonly type: HelperType = HelperType.NLU; constructor( name: N, - settings: HelperSetting[], settingService: SettingService, helperService: HelperService, logger: LoggerService, ) { - super(name, settings, settingService, helperService, logger); + super(name, settingService, helperService, logger); } /** diff --git a/api/src/helper/types.ts b/api/src/helper/types.ts index e3820520..a9edebce 100644 --- a/api/src/helper/types.ts +++ b/api/src/helper/types.ts @@ -29,10 +29,12 @@ export enum HelperType { UTIL = 'util', } +export type HelperName = `${string}-helper`; + export type TypeOfHelper = T extends HelperType.LLM - ? BaseLlmHelper + ? BaseLlmHelper : T extends HelperType.NLU - ? BaseNlpHelper + ? BaseNlpHelper : BaseHelper; export type HelperRegistry = Map< @@ -40,7 +42,7 @@ export type HelperRegistry = Map< Map >; -export type HelperSetting = Omit< +export type HelperSetting = Omit< SettingCreateDto, 'group' | 'weight' > & { diff --git a/api/src/i18n/controllers/i18n.controller.ts b/api/src/i18n/controllers/i18n.controller.ts index be857047..2e2ce965 100644 --- a/api/src/i18n/controllers/i18n.controller.ts +++ b/api/src/i18n/controllers/i18n.controller.ts @@ -8,14 +8,19 @@ import { Controller, Get, UseInterceptors } from '@nestjs/common'; +import { ChannelService } from '@/channel/channel.service'; +import { HelperService } from '@/helper/helper.service'; import { CsrfInterceptor } from '@/interceptors/csrf.interceptor'; - -import { I18nService } from '../services/i18n.service'; +import { PluginService } from '@/plugins/plugins.service'; @UseInterceptors(CsrfInterceptor) @Controller('i18n') export class I18nController { - constructor(private readonly i18nService: I18nService) {} + constructor( + private readonly pluginService: PluginService, + private readonly helperService: HelperService, + private readonly channelService: ChannelService, + ) {} /** * Retrieves translations of all the installed extensions. @@ -23,6 +28,12 @@ export class I18nController { */ @Get() getTranslations() { - return this.i18nService.getExtensionI18nTranslations(); + const plugins = this.pluginService.getAll(); + const helpers = this.helperService.getAll(); + const channels = this.channelService.getAll(); + return [...plugins, ...helpers, ...channels].reduce((acc, curr) => { + acc[curr.getNamespace()] = curr.getTranslations(); + return acc; + }, {}); } } diff --git a/api/src/i18n/services/i18n.service.ts b/api/src/i18n/services/i18n.service.ts index 52d4d063..9e5b93aa 100644 --- a/api/src/i18n/services/i18n.service.ts +++ b/api/src/i18n/services/i18n.service.ts @@ -6,13 +6,8 @@ * 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 { existsSync, promises as fs } from 'fs'; -import * as path from 'path'; - -import { Injectable, OnModuleInit } from '@nestjs/common'; +import { Injectable } from '@nestjs/common'; import { - I18nJsonLoader, - I18nTranslation, I18nService as NativeI18nService, Path, PathValue, @@ -22,25 +17,13 @@ import { IfAnyOrNever } from 'nestjs-i18n/dist/types'; import { config } from '@/config'; import { Translation } from '@/i18n/schemas/translation.schema'; -import { hyphenToUnderscore } from '@/utils/helpers/misc'; @Injectable() -export class I18nService> - extends NativeI18nService - implements OnModuleInit -{ +export class I18nService< + K = Record, +> extends NativeI18nService { private dynamicTranslations: Record> = {}; - private extensionTranslations: I18nTranslation = {}; - - onModuleInit() { - this.loadExtensionI18nTranslations(); - } - - getExtensionI18nTranslations() { - return this.extensionTranslations; - } - t

= any, R = PathValue>( key: P, options?: TranslateOptions, @@ -83,52 +66,4 @@ export class I18nService> return acc; }, this.dynamicTranslations); } - - async loadExtensionI18nTranslations() { - const baseDir = path.join(__dirname, '..', '..', 'extensions'); - const extensionTypes = ['channels', 'helpers', 'plugins']; - - try { - for (const type of extensionTypes) { - const extensionsDir = path.join(baseDir, type); - - if (!existsSync(extensionsDir)) { - continue; - } - - 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 namespace = hyphenToUnderscore(folder.name); - 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] = { - [namespace]: translations[lang], - }; - } else { - this.extensionTranslations[lang][namespace] = - 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}`); - } - } } diff --git a/api/src/plugins/base-block-plugin.ts b/api/src/plugins/base-block-plugin.ts index 47808fa3..1a12ebf9 100644 --- a/api/src/plugins/base-block-plugin.ts +++ b/api/src/plugins/base-block-plugin.ts @@ -6,6 +6,8 @@ * 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 path from 'path'; + import { Injectable } from '@nestjs/common'; import { Block, BlockFull } from '@/chat/schemas/block.schema'; @@ -17,6 +19,7 @@ import { PluginService } from './plugins.service'; import { PluginBlockTemplate, PluginEffects, + PluginName, PluginSetting, PluginType, } from './types'; @@ -29,16 +32,13 @@ export abstract class BaseBlockPlugin< public readonly settings: T; - constructor( - id: string, - settings: T, - pluginService: PluginService, - ) { - super(id, pluginService); - this.settings = settings; + constructor(name: PluginName, pluginService: PluginService) { + super(name, pluginService); + // eslint-disable-next-line @typescript-eslint/no-var-requires + this.settings = require(path.join(this.getPath(), 'settings')).default; } - template: PluginBlockTemplate; + abstract template: PluginBlockTemplate; effects?: PluginEffects; diff --git a/api/src/plugins/base-event-plugin.ts b/api/src/plugins/base-event-plugin.ts index 6a50cdd7..7e80a938 100644 --- a/api/src/plugins/base-event-plugin.ts +++ b/api/src/plugins/base-event-plugin.ts @@ -10,13 +10,13 @@ import { Injectable } from '@nestjs/common'; import { BasePlugin } from './base-plugin.service'; import { PluginService } from './plugins.service'; -import { PluginType } from './types'; +import { PluginName, PluginType } from './types'; @Injectable() export abstract class BaseEventPlugin extends BasePlugin { public readonly type: PluginType = PluginType.event; - constructor(id: string, pluginService: PluginService) { - super(id, pluginService); + constructor(name: PluginName, pluginService: PluginService) { + super(name, pluginService); } } diff --git a/api/src/plugins/base-plugin.service.ts b/api/src/plugins/base-plugin.service.ts index 63c3be10..85986faa 100644 --- a/api/src/plugins/base-plugin.service.ts +++ b/api/src/plugins/base-plugin.service.ts @@ -8,19 +8,24 @@ import { Injectable, OnModuleInit } from '@nestjs/common'; +import { Extension } from '@/utils/generics/extension'; + import { PluginService } from './plugins.service'; -import { PluginType } from './types'; +import { PluginName, PluginType } from './types'; @Injectable() -export abstract class BasePlugin implements OnModuleInit { +export abstract class BasePlugin extends Extension implements OnModuleInit { public readonly type: PluginType; constructor( - public readonly id: string, + public readonly name: PluginName, private pluginService: PluginService, - ) {} + ) { + super(name); + } - onModuleInit() { - this.pluginService.setPlugin(this.type, this.id, this); + async onModuleInit() { + await super.onModuleInit(); + this.pluginService.setPlugin(this.type, this.name, this); } } diff --git a/api/src/plugins/base-storage-plugin.ts b/api/src/plugins/base-storage-plugin.ts index 8b1839c4..7d3a5cfb 100644 --- a/api/src/plugins/base-storage-plugin.ts +++ b/api/src/plugins/base-storage-plugin.ts @@ -13,14 +13,14 @@ import { Attachment } from '@/attachment/schemas/attachment.schema'; import { BasePlugin } from './base-plugin.service'; import { PluginService } from './plugins.service'; -import { PluginType } from './types'; +import { PluginName, PluginType } from './types'; @Injectable() export abstract class BaseStoragePlugin extends BasePlugin { public readonly type: PluginType = PluginType.storage; - constructor(id: string, pluginService: PluginService) { - super(id, pluginService); + constructor(name: PluginName, pluginService: PluginService) { + super(name, pluginService); } abstract fileExists(attachment: Attachment): Promise; diff --git a/api/src/plugins/plugins.module.ts b/api/src/plugins/plugins.module.ts index 2579f06f..4dbf2708 100644 --- a/api/src/plugins/plugins.module.ts +++ b/api/src/plugins/plugins.module.ts @@ -20,7 +20,12 @@ import { ContentModel } from '@/cms/schemas/content.schema'; import { PluginService } from './plugins.service'; -@InjectDynamicProviders('dist/extensions/**/*.plugin.js') +@InjectDynamicProviders( + // Core & under dev plugins + 'dist/extensions/**/*.plugin.js', + // Installed plugins via npm + 'dist/.hexabot/plugins/**/*.plugin.js', +) @Global() @Module({ imports: [ diff --git a/api/src/plugins/plugins.service.spec.ts b/api/src/plugins/plugins.service.spec.ts index 31f15d6b..bb9f42e4 100644 --- a/api/src/plugins/plugins.service.spec.ts +++ b/api/src/plugins/plugins.service.spec.ts @@ -23,7 +23,7 @@ describe('PluginsService', () => { imports: [LoggerModule], }).compile(); pluginsService = module.get(PluginService); - module.get(DummyPlugin).onModuleInit(); + await module.get(DummyPlugin).onModuleInit(); }); afterAll(async () => { jest.clearAllMocks(); @@ -37,7 +37,7 @@ describe('PluginsService', () => { describe('getPlugin', () => { it('should return the required plugin', () => { - const result = pluginsService.getPlugin(PluginType.block, 'dummy'); + const result = pluginsService.getPlugin(PluginType.block, 'dummy-plugin'); expect(result).toBeInstanceOf(DummyPlugin); }); }); diff --git a/api/src/plugins/plugins.service.ts b/api/src/plugins/plugins.service.ts index d9858a9e..1cb8170a 100644 --- a/api/src/plugins/plugins.service.ts +++ b/api/src/plugins/plugins.service.ts @@ -10,7 +10,7 @@ import { Injectable } from '@nestjs/common'; import { BasePlugin } from './base-plugin.service'; import { PluginInstance } from './map-types'; -import { PluginType } from './types'; +import { PluginName, PluginType } from './types'; /** * @summary Service for managing and retrieving plugins. @@ -38,18 +38,18 @@ export class PluginService { constructor() {} /** - * Registers a plugin with a given key. + * Registers a plugin with a given name. * - * @param key The unique identifier for the plugin. + * @param name The unique identifier for the plugin. * @param plugin The plugin instance to register. */ - public setPlugin(type: PluginType, key: string, plugin: T) { + public setPlugin(type: PluginType, name: PluginName, plugin: T) { const registry = this.registry.get(type); - registry.set(key, plugin); + registry.set(name, plugin); } /** - * Retrieves all registered plugins as an array. + * Retrieves all registered plugins by as an array. * * @returns An array containing all the registered plugins. */ @@ -58,29 +58,39 @@ export class PluginService { return Array.from(registry.values()) as PluginInstance[]; } + /** + * Retrieves all registered plugins as an array. + * + * @returns An array containing all the registered plugins. + */ + public getAll(): T[] { + return Array.from(this.registry.values()) // Get all the inner maps + .flatMap((innerMap) => Array.from(innerMap.values())); // Flatten and get the values from each inner map + } + /** * Retrieves a plugin based on its key. * - * @param id The key used to register the plugin. + * @param name The key used to register the plugin. * * @returns The plugin associated with the given key, or `undefined` if not found. */ - public getPlugin(type: PT, id: string) { + public getPlugin(type: PT, name: PluginName) { const registry = this.registry.get(type); - const plugin = registry.get(id); + const plugin = registry.get(name); return plugin ? (plugin as PluginInstance) : undefined; } /** * Finds a plugin by its internal `id` property. * - * @param pluginId The unique `id` of the plugin to find. + * @param name The unique `id` of the plugin to find. * * @returns The plugin with the matching `id`, or `undefined` if no plugin is found. */ - public findPlugin(type: PT, pluginId: string) { + public findPlugin(type: PT, name: PluginName) { return this.getAllByType(type).find((plugin) => { - return plugin.id === pluginId; + return plugin.name === name; }); } } diff --git a/api/src/plugins/types.ts b/api/src/plugins/types.ts index e557c999..2dfae9dc 100644 --- a/api/src/plugins/types.ts +++ b/api/src/plugins/types.ts @@ -6,11 +6,14 @@ * 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 { ChannelEvent } from '@/channel/lib/EventWrapper'; import { BlockCreateDto } from '@/chat/dto/block.dto'; import { Block } from '@/chat/schemas/block.schema'; import { Conversation } from '@/chat/schemas/conversation.schema'; import { SettingCreateDto } from '@/setting/dto/setting.dto'; +export type PluginName = `${string}-plugin`; + export enum PluginType { event = 'event', block = 'block', @@ -19,7 +22,6 @@ export enum PluginType { export interface CustomBlocks {} -type ChannelEvent = any; type BlockAttrs = Partial & { name: string }; export type PluginSetting = Omit; diff --git a/api/src/setting/seeds/setting.seed-model.ts b/api/src/setting/seeds/setting.seed-model.ts index cd999cdf..cb8b4c60 100644 --- a/api/src/setting/seeds/setting.seed-model.ts +++ b/api/src/setting/seeds/setting.seed-model.ts @@ -13,7 +13,7 @@ export const DEFAULT_SETTINGS = [ { group: 'chatbot_settings', label: 'default_nlu_helper', - value: 'core-nlu', + value: 'core-nlu-helper', type: SettingType.select, config: { multiple: false, @@ -27,7 +27,7 @@ export const DEFAULT_SETTINGS = [ { group: 'chatbot_settings', label: 'default_llm_helper', - value: 'ollama', + value: 'ollama-helper', type: SettingType.select, config: { multiple: false, diff --git a/api/src/utils/generics/extension.ts b/api/src/utils/generics/extension.ts new file mode 100644 index 00000000..943dd4f3 --- /dev/null +++ b/api/src/utils/generics/extension.ts @@ -0,0 +1,43 @@ +import { promises as fs } from 'fs'; +import path from 'path'; + +import { OnModuleInit } from '@nestjs/common'; +import { I18nJsonLoader, I18nTranslation } from 'nestjs-i18n'; +import { Observable } from 'rxjs'; + +import { ExtensionName } from '../types/extension'; + +export abstract class Extension implements OnModuleInit { + private translations: I18nTranslation | Observable; + + constructor(public readonly name: ExtensionName) {} + + abstract getPath(): string; + + getName() { + return this.name; + } + + getNamespace() { + return this.name.replaceAll('-', '_') as HyphenToUnderscore; + } + + async onModuleInit() { + // Load i18n + const i18nPath = path.join(this.getPath(), 'i18n'); + try { + // Check if the i18n directory exists + await fs.access(i18nPath); + + // Load and merge translations + const i18nLoader = new I18nJsonLoader({ path: i18nPath }); + this.translations = await i18nLoader.load(); + } catch (error) { + // If the i18n folder does not exist or error in reading, skip this folder + } + } + + getTranslations() { + return this.translations; + } +} diff --git a/api/src/utils/test/dummy/dummy.plugin.ts b/api/src/utils/test/dummy/dummy.plugin.ts index 271343dd..d729530d 100644 --- a/api/src/utils/test/dummy/dummy.plugin.ts +++ b/api/src/utils/test/dummy/dummy.plugin.ts @@ -15,23 +15,27 @@ import { import { LoggerService } from '@/logger/logger.service'; import { BaseBlockPlugin } from '@/plugins/base-block-plugin'; import { PluginService } from '@/plugins/plugins.service'; -import { PluginSetting } from '@/plugins/types'; +import { PluginBlockTemplate, PluginSetting } from '@/plugins/types'; @Injectable() export class DummyPlugin extends BaseBlockPlugin { + template: PluginBlockTemplate = { name: 'Dummy Plugin' }; + constructor( pluginService: PluginService, private logger: LoggerService, ) { - super('dummy', [], pluginService); - - this.template = { name: 'Dummy Plugin' }; + super('dummy-plugin', pluginService); this.effects = { onStoreContextData: () => {}, }; } + getPath(): string { + return __dirname; + } + async process() { const envelope: StdOutgoingTextEnvelope = { format: OutgoingMessageFormat.text, diff --git a/api/src/utils/test/dummy/settings.ts b/api/src/utils/test/dummy/settings.ts new file mode 100644 index 00000000..d6d1738d --- /dev/null +++ b/api/src/utils/test/dummy/settings.ts @@ -0,0 +1 @@ +export default []; diff --git a/api/src/utils/test/fixtures/conversation.ts b/api/src/utils/test/fixtures/conversation.ts index 23b76341..b1770cf0 100644 --- a/api/src/utils/test/fixtures/conversation.ts +++ b/api/src/utils/test/fixtures/conversation.ts @@ -10,8 +10,8 @@ import mongoose from 'mongoose'; import { ConversationCreateDto } from '@/chat/dto/conversation.dto'; import { - ConversationModel, Conversation, + ConversationModel, } from '@/chat/schemas/conversation.schema'; import { getFixturesWithDefaultValues } from '../defaultValues'; @@ -25,7 +25,7 @@ const conversations: ConversationCreateDto[] = [ sender: '0', active: true, context: { - channel: 'messenger', + channel: 'messenger-channel', text: 'Hi', payload: '', nlp: { @@ -60,7 +60,7 @@ const conversations: ConversationCreateDto[] = [ foreign_id: '', labels: [], assignedTo: null, - channel: { name: 'messenger' }, + channel: { name: 'messenger-channel' }, }, skip: {}, attempt: 0, @@ -71,7 +71,7 @@ const conversations: ConversationCreateDto[] = [ { sender: '1', context: { - channel: 'offline', + channel: 'offline-channel', text: 'Hello', payload: '', nlp: { @@ -106,7 +106,7 @@ const conversations: ConversationCreateDto[] = [ foreign_id: '', labels: [], assignedTo: null, - channel: { name: 'offline' }, + channel: { name: 'offline-channel' }, }, skip: {}, attempt: 0, diff --git a/api/src/utils/test/fixtures/subscriber.ts b/api/src/utils/test/fixtures/subscriber.ts index 0df2eba1..62b890ca 100644 --- a/api/src/utils/test/fixtures/subscriber.ts +++ b/api/src/utils/test/fixtures/subscriber.ts @@ -27,7 +27,7 @@ const subscribers: SubscriberCreateDto[] = [ gender: 'male', country: 'FR', channel: { - name: 'messenger', + name: 'messenger-channel', }, labels: [], assignedAt: null, @@ -43,7 +43,7 @@ const subscribers: SubscriberCreateDto[] = [ gender: 'male', country: 'US', channel: { - name: 'offline', + name: 'offline-channel', }, labels: [], assignedAt: null, @@ -59,7 +59,7 @@ const subscribers: SubscriberCreateDto[] = [ gender: 'male', country: 'US', channel: { - name: 'offline', + name: 'offline-channel', }, labels: [], assignedAt: null, @@ -75,7 +75,7 @@ const subscribers: SubscriberCreateDto[] = [ gender: 'male', country: 'US', channel: { - name: 'offline', + name: 'offline-channel', }, labels: [], assignedAt: null, diff --git a/api/src/utils/test/mocks/conversation.ts b/api/src/utils/test/mocks/conversation.ts index b9e4c38b..ab90d76e 100644 --- a/api/src/utils/test/mocks/conversation.ts +++ b/api/src/utils/test/mocks/conversation.ts @@ -16,7 +16,7 @@ import { modelInstance } from './misc'; import { subscriberInstance } from './subscriber'; export const contextBlankInstance: Context = { - channel: 'offline', + channel: 'offline-channel', text: '', payload: undefined, nlp: { entities: [] }, @@ -42,7 +42,7 @@ export const contextEmailVarInstance: Context = { }; export const contextGetStartedInstance: Context = { - channel: 'offline', + channel: 'offline-channel', text: 'Get Started', payload: 'GET_STARTED', nlp: { entities: [] }, diff --git a/api/src/utils/test/mocks/subscriber.ts b/api/src/utils/test/mocks/subscriber.ts index ffe8b324..54aa955d 100644 --- a/api/src/utils/test/mocks/subscriber.ts +++ b/api/src/utils/test/mocks/subscriber.ts @@ -25,7 +25,7 @@ export const subscriberInstance: Subscriber = { lastvisit: new Date(), retainedFrom: new Date(), channel: { - name: 'offline', + name: 'offline-channel', }, labels: [], ...modelInstance, diff --git a/api/src/utils/types/extension.ts b/api/src/utils/types/extension.ts new file mode 100644 index 00000000..09da6f48 --- /dev/null +++ b/api/src/utils/types/extension.ts @@ -0,0 +1,5 @@ +import { ChannelName } from '@/channel/types'; +import { HelperName } from '@/helper/types'; +import { PluginName } from '@/plugins/types'; + +export type ExtensionName = ChannelName | HelperName | PluginName; diff --git a/api/tsconfig.json b/api/tsconfig.json index b1b6499d..12289c34 100644 --- a/api/tsconfig.json +++ b/api/tsconfig.json @@ -23,5 +23,11 @@ "@/*": ["src/*"] } }, - "include": ["src/**/*.ts", "src/**/*.json", "test/**/*.ts"] + "include": [ + "src/**/*.ts", + "src/**/*.json", + "test/**/*.ts", + "src/.hexabot/**/*.ts", + "src/.hexabot/**/*.json" + ] } diff --git a/frontend/src/app-components/widget/ChatWidget.tsx b/frontend/src/app-components/widget/ChatWidget.tsx index e8008c85..f94cd3ae 100644 --- a/frontend/src/app-components/widget/ChatWidget.tsx +++ b/frontend/src/app-components/widget/ChatWidget.tsx @@ -33,7 +33,7 @@ export const ChatWidget = () => { { {customBlocks?.map((customBlock) => ( { )} diff --git a/frontend/src/hooks/useRemoteI18n.ts b/frontend/src/hooks/useRemoteI18n.ts index 407e91bd..c46d6437 100644 --- a/frontend/src/hooks/useRemoteI18n.ts +++ b/frontend/src/hooks/useRemoteI18n.ts @@ -22,14 +22,13 @@ export const useRemoteI18n = () => { 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) => { + Object.keys(additionalTranslations).forEach((namespace) => { + Object.keys(additionalTranslations[namespace]).forEach((lang) => { i18n.addResourceBundle( lang, namespace, - additionalTranslations[lang][namespace], + additionalTranslations[namespace][lang], true, true, ); diff --git a/frontend/src/types/block.types.ts b/frontend/src/types/block.types.ts index 251f35d8..352117bf 100644 --- a/frontend/src/types/block.types.ts +++ b/frontend/src/types/block.types.ts @@ -139,4 +139,6 @@ export interface ICustomBlockTemplateAttributes { // @TODO : templates doe not contain base schema attributes export interface ICustomBlockTemplate extends IBaseSchema, - OmitPopulate {} + OmitPopulate { + namespace: string; +} diff --git a/widget/README.md b/widget/README.md index 20eaeecc..041b9400 100644 --- a/widget/README.md +++ b/widget/README.md @@ -61,7 +61,7 @@ Once the widget is built, you can easily embed it into any webpage. Here's an ex ReactDOM.render( el(HexabotWidget, { apiUrl: 'https://api.yourdomain.com', - channel: 'offline', + channel: 'offline-channel', token: 'token123', }), domContainer, @@ -96,7 +96,7 @@ To prevent the website css from conflicting with the chat widget css, we can lev ReactDOM.render( React.createElement(HexabotWidget, { apiUrl: 'https://api.yourdomain.com', - channel: 'offline', + channel: 'offline-channel', token: 'token123', }), shadowContainer, diff --git a/widget/public/index.html b/widget/public/index.html index 08131b04..00863784 100644 --- a/widget/public/index.html +++ b/widget/public/index.html @@ -34,7 +34,7 @@ ReactDOM.render( React.createElement(HexabotWidget, { apiUrl: 'http://localhost:4000', - channel: 'offline', + channel: 'offline-channel', token: 'token123', }), shadowContainer, diff --git a/widget/src/constants/defaultConfig.ts b/widget/src/constants/defaultConfig.ts index e09133e1..5d189244 100644 --- a/widget/src/constants/defaultConfig.ts +++ b/widget/src/constants/defaultConfig.ts @@ -8,7 +8,7 @@ export const DEFAULT_CONFIG = { apiUrl: process.env.REACT_APP_WIDGET_API_URL || 'http://localhost:4000', - channel: process.env.REACT_APP_WIDGET_CHANNEL || 'live-chat-tester', + channel: process.env.REACT_APP_WIDGET_CHANNEL || 'live-chat-tester-channel', token: process.env.REACT_APP_WIDGET_TOKEN || 'test', language: 'en', }; diff --git a/widget/src/main.tsx b/widget/src/main.tsx index f01b78a1..f07a3165 100644 --- a/widget/src/main.tsx +++ b/widget/src/main.tsx @@ -18,7 +18,7 @@ ReactDOM.createRoot(document.getElementById('root')!).render(