Merge branch 'main' of https://github.com/Hexastack/Hexabot into 256-request-move-blocks-between-categories

This commit is contained in:
hexastack
2024-11-14 17:22:42 +01:00
57 changed files with 333 additions and 255 deletions

View File

@@ -16,7 +16,9 @@
<p align="center">
<br />
<a href="https://docs.hexabot.ai" rel="dofollow"><strong>Explore the docs »</strong></a>
<a href="https://hexabot.ai/extensions" rel="dofollow"><strong>Extensions Library</strong></a>
.
<a href="https://docs.hexabot.ai" rel="dofollow"><strong>Documentation</strong></a>
<br />
<br/>
@@ -31,9 +33,7 @@
## Description
[Hexabot](https://hexabot.ai/) is an open-source AI chatbot / agent solution. It allows you to create and manage multi-channel, and multilingual chatbots / agents with ease. Hexabot is designed for flexibility and customization, offering powerful text-to-action capabilities. Originally a closed-source project (version 1), we've now open-sourced version 2 to contribute to the community and enable developers to customize and extend the platform with extensions.
**NOTE:** We are currently working to package it in a way that it would be easy to install and use, hence there's no version release just yet.
[Hexabot](https://hexabot.ai/) is an open-source AI chatbot / agent solution. It allows you to create and manage multi-channel, and multilingual chatbots / agents with ease. Hexabot is designed for flexibility and customization, offering powerful text-to-action capabilities. Originally a closed-source project (version 1), we've now open-sourced version 2 to contribute to the community and enable developers to customize and extend the platform with [extensions](https://hexabot.ai/extensions).
<a href="https://www.producthunt.com/posts/hexabot?embed=true&utm_source=badge-featured&utm_medium=badge&utm_souce=badge-hexabot" target="_blank"><img src="https://api.producthunt.com/widgets/embed-image/v1/featured.svg?post_id=477532&theme=light" alt="Hexabot - Create&#0032;exceptional&#0032;chatbot&#0032;experiences&#0046;&#0032;100&#0037;&#0032;Open&#0032;Source&#0046; | Product Hunt" style="width: 250px; height: 54px;" width="250" height="54" /></a>
## Features
@@ -41,7 +41,7 @@
- **LLMs & NLU Support:** Integrate with your favorite LLM model whether it's by using Ollama, ChatGPT, Mistral or Gemini ... Manage training datasets for machine learning models that detect user intent and language, providing intelligent responses.
- **Multi-Channel Support:** Create consistent chatbot experiences across multiple channels like web, mobile, and social media platforms.
- **Visual Editor:** Design and manage chatbot flows with an intuitive drag-and-drop interface. Supports text messages, quick replies, carousels, and more.
- **Plugin System:** Extend Hexabot's functionality by developing custom plugins. Enable features like text-to-action responses, 3rd party system integrations, and more.
- **Plugin System:** Extend Hexabot's functionality by developing and installing extensions from the [Extension Library](https://hexabot.ai/extensions). Enable features like text-to-action responses, 3rd party system integrations, and more.
- **Multi-lingual Support:** Define multiple languages, allowing the chatbot to interact with users in their preferred language.
- **Knowledge Base:** Seamlessly integrate and manage dynamic content such as product catalogs and store lists for more engaging conversations.
- **User Roles & Permissions:** Granular access control to manage user roles and permissions for different parts of the system.

4
api/package-lock.json generated
View File

@@ -1,12 +1,12 @@
{
"name": "hexabot",
"version": "2.0.9",
"version": "2.0.12",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "hexabot",
"version": "2.0.9",
"version": "2.0.12",
"hasInstallScript": true,
"license": "AGPL-3.0-only",
"dependencies": {

View File

@@ -1,6 +1,6 @@
{
"name": "hexabot",
"version": "2.0.9",
"version": "2.0.12",
"description": "Hexabot is a solution for creating and managing chatbots across multiple channels, leveraging AI for advanced conversational capabilities. It provides a user-friendly interface for building, training, and deploying chatbots with integrated support for various messaging platforms.",
"author": "Hexastack",
"license": "AGPL-3.0-only",

View File

@@ -6,21 +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 {
BadRequestException,
Controller,
Get,
Req,
Res,
Session,
} from '@nestjs/common';
import { Controller, Get, Req, Session } from '@nestjs/common';
import { CsrfCheck, CsrfGenAuth } from '@tekuconcept/nestjs-csrf';
import { CsrfGenerator } from '@tekuconcept/nestjs-csrf/dist/csrf.generator';
import { Request, Response } from 'express';
import { Request } from 'express';
import { Session as ExpressSession } from 'express-session';
import { AppService } from './app.service';
import { config } from './config';
import { LoggerService } from './logger/logger.service';
import { Roles } from './utils/decorators/roles.decorator';
@@ -53,20 +45,6 @@ export class AppController {
@Get('__getcookie')
cookies(@Req() req: Request): string {
req.session.anonymous = true;
return '_sailsIoJSConnect();';
}
// @TODO : remove once old frontend is abandoned
@Get('logout')
logout(@Req() req: Request, @Res({ passthrough: true }) res: Response) {
res.clearCookie(config.session.name);
req.session.destroy((error) => {
if (error) {
this.logger.error(error);
throw new BadRequestException();
}
});
return { status: 'ok' };
return '';
}
}

View File

@@ -1,5 +1,4 @@
{
"verification_token": "Verification Token",
"allowed_domains": "Allowed Domains",
"start_button": "Enable `Get Started`",
"input_disabled": "Disable Input",

View File

@@ -1,5 +1,4 @@
{
"verification_token": "Jeton de vérification",
"allowed_domains": "Domaines autorisés",
"start_button": "Activer `Démarrer`",
"input_disabled": "Désactiver la saisie",

View File

@@ -17,12 +17,6 @@ export const CONSOLE_CHANNEL_NAME = 'console-channel';
export const CONSOLE_CHANNEL_NAMESPACE = 'console_channel';
export default [
{
group: CONSOLE_CHANNEL_NAMESPACE,
label: Web.SettingLabel.verification_token,
value: 'test',
type: SettingType.text,
},
{
group: CONSOLE_CHANNEL_NAMESPACE,
label: Web.SettingLabel.allowed_domains,

View File

@@ -77,7 +77,7 @@ export default abstract class BaseWebChannelHandler<
protected readonly attachmentService: AttachmentService,
protected readonly messageService: MessageService,
protected readonly menuService: MenuService,
private readonly websocketGateway: WebsocketGateway,
protected readonly websocketGateway: WebsocketGateway,
) {
super(name, settingService, channelService, logger);
}
@@ -98,42 +98,32 @@ export default abstract class BaseWebChannelHandler<
*/
@OnEvent('hook:websocket:connection', { async: true })
async onWebSocketConnection(client: Socket) {
const settings = await this.getSettings();
const handshake = client.handshake;
const { channel } = handshake.query;
if (channel !== this.getName()) {
return;
}
try {
const { verification_token } = client.handshake.query;
await this.verifyToken(verification_token.toString());
try {
this.logger.debug(
'Web Channel Handler : WS connected .. sending settings',
);
try {
const menu = await this.menuService.getTree();
return client.emit('settings', { menu, ...settings });
} catch (err) {
this.logger.warn(
'Web Channel Handler : Unable to retrieve menu ',
err,
);
return client.emit('settings', settings);
}
} catch (err) {
this.logger.warn(
'Web Channel Handler : Unable to verify token, disconnecting ...',
err,
);
client.disconnect();
const settings = await this.getSettings();
const handshake = client.handshake;
const { channel } = handshake.query;
if (channel !== this.getName()) {
return;
}
this.logger.debug(
'Web Channel Handler : WS connected .. sending settings',
);
try {
const menu = await this.menuService.getTree();
return client.emit('settings', { menu, ...settings });
} catch (err) {
this.logger.warn('Web Channel Handler : Unable to retrieve menu ', err);
return client.emit('settings', settings);
}
} catch (err) {
this.logger.error(
'Web Channel Handler : Unable to initiate websocket connection',
err,
);
client.disconnect();
}
}
@@ -218,7 +208,7 @@ export default abstract class BaseWebChannelHandler<
*
* @returns Formatted message
*/
private formatHistoryMessages(messages: AnyMessage[]): Web.Message[] {
protected formatMessages(messages: AnyMessage[]): Web.Message[] {
return messages.map((anyMessage: AnyMessage) => {
if ('sender' in anyMessage && anyMessage.sender) {
return {
@@ -262,7 +252,7 @@ export default abstract class BaseWebChannelHandler<
until,
n,
);
return this.formatHistoryMessages(messages.reverse());
return this.formatMessages(messages.reverse());
}
return [];
}
@@ -287,35 +277,11 @@ export default abstract class BaseWebChannelHandler<
since,
n,
);
return this.formatHistoryMessages(messages);
return this.formatMessages(messages);
}
return [];
}
/**
* Verify the received token.
*
* @param verificationToken - Verification Token
*/
private async verifyToken(verificationToken: string) {
const settings =
(await this.getSettings()) as unknown as Settings[typeof WEB_CHANNEL_NAMESPACE];
const verifyToken = settings.verification_token;
if (!verifyToken) {
throw new Error('You need to specify a verifyToken in your config.');
}
if (!verificationToken) {
throw new Error('Did not recieve any verification token.');
}
if (verificationToken !== verifyToken) {
throw new Error('Make sure the validation tokens match.');
}
this.logger.log(
'Web Channel Handler : Token has been verified successfully!',
);
}
/**
* Verify the origin against whitelisted domains.
*
@@ -405,20 +371,12 @@ export default abstract class BaseWebChannelHandler<
* @param req
* @param res
*/
private async checkRequest(
protected async checkRequest(
req: Request | SocketRequest,
res: Response | SocketResponse,
) {
try {
await this.validateCors(req, res);
try {
const { verification_token } =
'verification_token' in req.query ? req.query : req.body;
await this.verifyToken(verification_token);
} catch (err) {
this.logger.warn('Web Channel Handler : Unable to verify token ', err);
throw new Error('Unauthorized, invalid token!');
}
} catch (err) {
this.logger.warn(
'Web Channel Handler : Attempt to access from an unauthorized origin',
@@ -741,7 +699,7 @@ export default abstract class BaseWebChannelHandler<
*
* @returns IP Address
*/
private getIpAddress(req: Request | SocketRequest): string {
protected getIpAddress(req: Request | SocketRequest): string {
if ('isSocket' in req && req.isSocket) {
return req.socket.handshake.address;
} else if (Array.isArray(req.ips) && req.ips.length > 0) {

View File

@@ -1,5 +1,4 @@
{
"verification_token": "Verification Token",
"allowed_domains": "Allowed Domains",
"start_button": "Enable `Get Started`",
"input_disabled": "Disable Input",

View File

@@ -1,5 +1,4 @@
{
"verification_token": "Jeton de vérification",
"allowed_domains": "Domaines autorisés",
"start_button": "Activer `Démarrer`",
"input_disabled": "Désactiver la saisie",

View File

@@ -16,12 +16,6 @@ export const WEB_CHANNEL_NAME = 'web-channel' as const;
export const WEB_CHANNEL_NAMESPACE = 'web_channel';
export default [
{
group: WEB_CHANNEL_NAMESPACE,
label: Web.SettingLabel.verification_token,
value: 'token123',
type: SettingType.secret,
},
{
group: WEB_CHANNEL_NAMESPACE,
label: Web.SettingLabel.allowed_domains,
@@ -68,7 +62,7 @@ export default [
{
group: WEB_CHANNEL_NAMESPACE,
label: Web.SettingLabel.avatar_url,
value: 'https://eu.ui-avatars.com/api/?name=Hexa+Bot&size=64',
value: '',
type: SettingType.text,
},
{

View File

@@ -13,8 +13,6 @@ import { StdQuickReply } from '@/chat/schemas/types/quick-reply';
export namespace Web {
export enum SettingLabel {
secret = 'secret',
verification_token = 'verification_token',
allowed_domains = 'allowed_domains',
start_button = 'start_button',
input_disabled = 'input_disabled',

5
api/src/global.d.ts vendored
View File

@@ -6,6 +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 '../types/event-emitter';
import '../types/express-session';
declare global {
type HyphenToUnderscore<S extends string> = S extends `${infer P}-${infer Q}`
? `${P}_${HyphenToUnderscore<Q>}`
@@ -13,4 +16,4 @@ declare global {
}
// eslint-disable-next-line prettier/prettier
export { };
export {};

View File

@@ -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 { HttpModule } from '@nestjs/axios';
import { Global, Module } from '@nestjs/common';
import { MongooseModule } from '@nestjs/mongoose';
import { InjectDynamicProviders } from 'nestjs-dynamic-providers';
@@ -40,6 +41,7 @@ import { PluginService } from './plugins.service';
CmsModule,
AttachmentModule,
ChatModule,
HttpModule,
],
providers: [PluginService],
exports: [PluginService],

View File

@@ -83,8 +83,8 @@ export class ReadOnlyUserController extends BaseController<
*/
@Roles('public')
@Get('bot/profile_pic')
async botProfilePic() {
return getBotAvatar();
async botProfilePic(@Query('color') color: string) {
return getBotAvatar(color);
}
/**

View File

@@ -39,8 +39,8 @@ export const generateInitialsAvatar = async (name: {
return await generateAvatarSvg(svg);
};
export const getBotAvatar = async () => {
const svg = generateBotAvatarSvg({});
export const getBotAvatar = async (color: string) => {
const svg = generateBotAvatarSvg({ bgColor: color });
return await generateAvatarSvg(svg);
};

View File

@@ -43,17 +43,17 @@ export function generateUIAvatarSvg({
export function generateBotAvatarSvg({
size = 64,
bgColor = '#d1d1d1',
bgColor = '#000',
textColor = '#fff',
}: UIAvatarSvgParams): string {
return `
<svg width="${size}px" height="${size}px" viewBox="-3.36 -3.36 30.72 30.72" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" fill="#4f4f4f" stroke="#4f4f4f" transform="rotate(0)">
<svg width="${size}px" height="${size}px" viewBox="-3.36 -3.36 30.72 30.72" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<g stroke-width="0" transform="translate(0,0), scale(1)">
<rect x="-3.36" y="-3.36" width="30.72" height="30.72" rx="15.36" fill="${bgColor}" strokewidth="0"></rect>
</g>
<g stroke-linecap="round" stroke-linejoin="round" stroke="#CCCCCC" stroke-width="0.336"></g>
<g>
<g stroke-width="0.00024000000000000003" fill="none" fill-rule="evenodd">
<g fill="#000000" fill-rule="nonzero">
<g stroke-width="0.00024000000000000003" fill="${textColor}" fill-rule="evenodd">
<g fill-rule="nonzero">
<path d="M17.7530511,13.999921 C18.9956918,13.999921 20.0030511,15.0072804 20.0030511,16.249921 L20.0030511,17.1550008 C20.0030511,18.2486786 19.5255957,19.2878579 18.6957793,20.0002733 C17.1303315,21.344244 14.8899962,22.0010712 12,22.0010712 C9.11050247,22.0010712 6.87168436,21.3444691 5.30881727,20.0007885 C4.48019625,19.2883988 4.00354153,18.2500002 4.00354153,17.1572408 L4.00354153,16.249921 C4.00354153,15.0072804 5.01090084,13.999921 6.25354153,13.999921 L17.7530511,13.999921 Z M17.7530511,15.499921 L6.25354153,15.499921 C5.83932796,15.499921 5.50354153,15.8357075 5.50354153,16.249921 L5.50354153,17.1572408 C5.50354153,17.8128951 5.78953221,18.4359296 6.28670709,18.8633654 C7.5447918,19.9450082 9.44080155,20.5010712 12,20.5010712 C14.5599799,20.5010712 16.4578003,19.9446634 17.7186879,18.8621641 C18.2165778,18.4347149 18.5030511,17.8112072 18.5030511,17.1550005 L18.5030511,16.249921 C18.5030511,15.8357075 18.1672647,15.499921 17.7530511,15.499921 Z M11.8985607,2.00734093 L12.0003312,2.00049432 C12.380027,2.00049432 12.6938222,2.2826482 12.7434846,2.64872376 L12.7503312,2.75049432 L12.7495415,3.49949432 L16.25,3.5 C17.4926407,3.5 18.5,4.50735931 18.5,5.75 L18.5,10.254591 C18.5,11.4972317 17.4926407,12.504591 16.25,12.504591 L7.75,12.504591 C6.50735931,12.504591 5.5,11.4972317 5.5,10.254591 L5.5,5.75 C5.5,4.50735931 6.50735931,3.5 7.75,3.5 L11.2495415,3.49949432 L11.2503312,2.75049432 C11.2503312,2.37079855 11.5324851,2.05700336 11.8985607,2.00734093 L12.0003312,2.00049432 L11.8985607,2.00734093 Z M16.25,5 L7.75,5 C7.33578644,5 7,5.33578644 7,5.75 L7,10.254591 C7,10.6688046 7.33578644,11.004591 7.75,11.004591 L16.25,11.004591 C16.6642136,11.004591 17,10.6688046 17,10.254591 L17,5.75 C17,5.33578644 16.6642136,5 16.25,5 Z M9.74928905,6.5 C10.4392523,6.5 10.9985781,7.05932576 10.9985781,7.74928905 C10.9985781,8.43925235 10.4392523,8.99857811 9.74928905,8.99857811 C9.05932576,8.99857811 8.5,8.43925235 8.5,7.74928905 C8.5,7.05932576 9.05932576,6.5 9.74928905,6.5 Z M14.2420255,6.5 C14.9319888,6.5 15.4913145,7.05932576 15.4913145,7.74928905 C15.4913145,8.43925235 14.9319888,8.99857811 14.2420255,8.99857811 C13.5520622,8.99857811 12.9927364,8.43925235 12.9927364,7.74928905 C12.9927364,7.05932576 13.5520622,6.5 14.2420255,6.5 Z"></path>
</g>
</g>

View File

@@ -16,11 +16,14 @@ export type TFilterKeysOfNeverType<T> = Omit<T, TFilterKeysOfType<T, []>>;
export type NestedKeys<T> = T extends object
? {
[K in keyof T]: Array<any> extends T[K]
? Exclude<K, symbol>
: K extends symbol
// eslint-disable-next-line @typescript-eslint/ban-types
[K in keyof T]: T[K] extends Function
? never
: Array<any> extends T[K]
? Exclude<K, symbol>
: `${Exclude<K, symbol>}${'' | `.${NestedKeys<T[K]>}`}`;
: K extends symbol
? Exclude<K, symbol>
: `${Exclude<K, symbol>}${'' | `.${NestedKeys<T[K]>}`}`;
}[keyof T]
: never;

View File

@@ -224,7 +224,7 @@ export class WebsocketGateway
'Unable to load session, creating a new one ...',
err,
);
if (searchParams.get('channel') === 'web-channel') {
if (searchParams.get('channel') !== 'console-channel') {
return this.createAndStoreSession(client, next);
} else {
return next(new Error('Unauthorized: Unknown session ID'));

View File

@@ -21,12 +21,11 @@
"resolveJsonModule": true,
"esModuleInterop": true,
"paths": {
"@/*": ["src/*", "src/.hexabot/*"],
"@/*": ["src/*", "src/.hexabot/*", "src/extra/*"],
"hexabot/src/*": ["src/*", "src/.hexabot/*"]
}
},
"include": [
"types/**/*.d.ts",
"src/global.d.ts",
"src/**/*.ts",
"src/**/*.json",

View File

@@ -6,7 +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 { SubscriberStub } from './chat/schemas/subscriber.schema';
import { SubscriberStub } from '@/chat/schemas/subscriber.schema';
declare module 'express-session' {
interface SessionUser {

View File

@@ -5,7 +5,7 @@ SSL_EMAIL=hello@hexabot.ai
API_PORT=4000
APP_SCRIPT_COMPODOC_PORT=9003
API_ORIGIN=http://${APP_DOMAIN}:${API_PORT}
FRONTEND_ORIGIN=http://${APP_DOMAIN}:8080,http://${APP_DOMAIN}:8081,http://${APP_DOMAIN}:5173,http://${APP_DOMAIN},http://${APP_DOMAIN}/*,*
FRONTEND_ORIGIN=http://${APP_DOMAIN},http://${APP_DOMAIN}:8080,http://${APP_DOMAIN}:8081,http://${APP_DOMAIN}:5173,http://${APP_DOMAIN},http://${APP_DOMAIN}/*,*
JWT_SECRET=dev_only
JWT_EXPIRES_IN=60
SALT_LENGTH=12
@@ -58,4 +58,3 @@ NEXT_PUBLIC_SSO_ENABLED=false
APP_WIDGET_PORT=5173
REACT_APP_WIDGET_API_URL=http://${APP_DOMAIN}:${API_PORT}
REACT_APP_WIDGET_CHANNEL=web-channel
REACT_APP_WIDGET_TOKEN=token123

View File

@@ -66,12 +66,14 @@ export type FileUploadProps = {
accept: string;
enableMediaLibrary?: boolean;
onChange?: (data?: IAttachment | null) => void;
onUploadComplete?: () => void;
};
const AttachmentUploader: FC<FileUploadProps> = ({
accept,
enableMediaLibrary,
onChange,
onUploadComplete,
}) => {
const [attachment, setAttachment] = useState<IAttachment | undefined>(
undefined,
@@ -87,6 +89,7 @@ const AttachmentUploader: FC<FileUploadProps> = ({
toast.success(t("message.success_save"));
setAttachment(data);
onChange && onChange(data);
onUploadComplete && onUploadComplete();
},
});
const libraryDialogCtl = useDialog<never>(false);

View File

@@ -0,0 +1,121 @@
/*
* 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 { Box, Button, FormHelperText, FormLabel } from "@mui/material";
import { forwardRef, useState } from "react";
import { useHasPermission } from "@/hooks/useHasPermission";
import { EntityType } from "@/services/types";
import { IAttachment } from "@/types/attachment.types";
import { PermissionAction } from "@/types/permission.types";
import AttachmentThumbnail from "./AttachmentThumbnail";
import AttachmentUploader from "./AttachmentUploader";
type MultipleAttachmentInputProps = {
label: string;
value: string[];
format: "small" | "basic" | "full";
accept: string;
enableMediaLibrary?: boolean;
size?: number;
onChange?: (ids: string[]) => void;
error?: boolean;
helperText?: string;
};
const MultipleAttachmentInput = forwardRef<
HTMLDivElement,
MultipleAttachmentInputProps
>(
(
{
label,
value,
format,
accept,
enableMediaLibrary = true,
size,
onChange,
error,
helperText,
},
ref,
) => {
const [attachments, setAttachments] = useState<string[]>(value);
const [uploadKey, setUploadKey] = useState(Date.now());
const hasPermission = useHasPermission();
const handleChange = (attachment?: IAttachment | null, index?: number) => {
const updatedAttachments = [...attachments];
if (attachment) {
if (index !== undefined) {
updatedAttachments[index] = attachment.id;
} else {
updatedAttachments.push(attachment.id);
}
} else if (index !== undefined) {
updatedAttachments.splice(index, 1);
}
setAttachments(updatedAttachments);
onChange && onChange(updatedAttachments);
setUploadKey(Date.now());
};
const handleRemove = (index: number) => {
handleChange(null, index);
};
return (
<Box ref={ref}>
<FormLabel
component="label"
style={{ display: "inline-block", marginBottom: 8 }}
>
{label}
</FormLabel>
{attachments.map((attachmentId, index) => (
<Box
key={attachmentId}
sx={{ display: "flex", alignItems: "center", mb: 2 }}
>
<AttachmentThumbnail
id={attachmentId}
format={format}
size={size}
onChange={(newAttachment) => handleChange(newAttachment, index)}
/>
<Button
onClick={() => handleRemove(index)}
sx={{ ml: 2 }}
variant="outlined"
color="secondary"
>
Remove
</Button>
</Box>
))}
{hasPermission(EntityType.ATTACHMENT, PermissionAction.CREATE) && (
<AttachmentUploader
key={uploadKey}
accept={accept}
enableMediaLibrary={enableMediaLibrary}
onChange={(attachment) => handleChange(attachment)}
/>
)}
{helperText && (
<FormHelperText error={error}>{helperText}</FormHelperText>
)}
</Box>
);
},
);
MultipleAttachmentInput.displayName = "MultipleAttachmentInput";
export default MultipleAttachmentInput;

View File

@@ -113,7 +113,7 @@ const AutoCompleteEntitySelect = <
return (
<AutoCompleteSelect<Value, Label, Multiple>
value={value}
{...(options.length && { value })}
onChange={onChange}
label={label}
multiple={multiple}

View File

@@ -34,14 +34,15 @@ export const ChatWidget = () => {
config={{
apiUrl,
channel: "console-channel",
token: "test",
language: i18n.language,
}}
CustomHeader={ChatWidgetHeader}
CustomAvatar={() => (
<Avatar
sx={{ width: "32px", height: "32px", fontSize: ".75rem" }}
src={getAvatarSrc(apiUrl, EntityType.USER, "bot")}
sx={{ width: "32px", height: "32px" }}
src={
getAvatarSrc(apiUrl, EntityType.USER, "bot") + "?color=%231ba089"
}
/>
)}
/>

View File

@@ -11,6 +11,7 @@ import { FormControlLabel, MenuItem, Switch } from "@mui/material";
import { ControllerRenderProps } from "react-hook-form";
import AttachmentInput from "@/app-components/attachment/AttachmentInput";
import MultipleAttachmentInput from "@/app-components/attachment/MultipleAttachmentInput";
import { Adornment } from "@/app-components/inputs/Adornment";
import AutoCompleteEntitySelect from "@/app-components/inputs/AutoCompleteEntitySelect";
import { Input } from "@/app-components/inputs/Input";
@@ -20,7 +21,7 @@ import { useTranslate } from "@/hooks/useTranslate";
import { EntityType, Format } from "@/services/types";
import { IBlock } from "@/types/block.types";
import { IHelper } from "@/types/helper.types";
import { ISetting } from "@/types/setting.types";
import { ISetting, SettingType } from "@/types/setting.types";
import { MIME_TYPES } from "@/utils/attachment";
interface RenderSettingInputProps {
@@ -45,7 +46,7 @@ const SettingInput: React.FC<RenderSettingInputProps> = ({
});
switch (setting.type) {
case "text":
case SettingType.text:
case "textarea":
return (
<Input
@@ -185,6 +186,18 @@ const SettingInput: React.FC<RenderSettingInputProps> = ({
size={128}
/>
);
case SettingType.multiple_attachment:
return (
<MultipleAttachmentInput
label={label}
{...field}
value={field.value}
accept={MIME_TYPES["images"].join(",")}
format="full"
size={128}
/>
);
default:
return <Input {...field} />;
}

View File

@@ -18,7 +18,13 @@ const Aside = dynamic(() => import("./Aside"), { ssr: false });
export const VisualEditor = () => {
return (
<VisualEditorProvider>
<Grid container gap={2} flexDirection="column" height="100%" width="100%">
<Grid
container
gap={2}
flexDirection="column"
height="calc(100vh - 64px)"
width="100%"
>
<Grid container height="100%" margin="auto">
<Aside />
<Grid

View File

@@ -21,6 +21,6 @@ export interface ILanguageAttributes {
export interface ILanguageStub
extends IBaseSchema,
OmitPopulate<ILanguageAttributes, EntityType.TRANSLATION> {}
OmitPopulate<ILanguageAttributes, EntityType.LANGUAGE> {}
export interface ILanguage extends ILanguageStub, IFormat<Format.BASIC> {}

2
package-lock.json generated
View File

@@ -9774,7 +9774,7 @@
},
"widget": {
"name": "hexabot-chat-widget",
"version": "2.0.1",
"version": "2.0.3",
"license": "AGPL-3.0-only",
"dependencies": {
"@types/emoji-js": "^3.5.2",

View File

@@ -7,11 +7,9 @@ COPY package*.json ./
# Set the environment variables
ARG REACT_APP_WIDGET_API_URL
ARG REACT_APP_WIDGET_CHANNEL
ARG REACT_APP_WIDGET_TOKEN
ENV REACT_APP_WIDGET_API_URL=${REACT_APP_WIDGET_API_URL}
ENV REACT_APP_WIDGET_CHANNEL=${REACT_APP_WIDGET_CHANNEL}
ENV REACT_APP_WIDGET_TOKEN=${REACT_APP_WIDGET_TOKEN}
# Installer stage: Installs dependencies
FROM base AS installer

View File

@@ -1,6 +1,6 @@
{
"name": "hexabot-chat-widget",
"version": "2.0.1",
"version": "2.0.3",
"description": "Hexabot is a solution for creating and managing chatbots across multiple channels, leveraging AI for advanced conversational capabilities. It provides a user-friendly interface for building, training, and deploying chatbots with integrated support for various messaging platforms.",
"author": "Hexastack",
"license": "AGPL-3.0-only",

View File

@@ -1,7 +1,7 @@
.sc-header {
min-height: 64px;
border-top-left-radius: 9px;
border-top-right-radius: 9px;
border-top-left-radius: .5rem;
border-top-right-radius: .5rem;
padding: 10px;
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.2);
position: relative;

View File

@@ -12,7 +12,7 @@
flex-direction: column;
justify-content: space-between;
border-radius: 10px;
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
animation: fadeIn;
animation-duration: 0.3s;
animation-timing-function: ease-in-out;

View File

@@ -21,7 +21,7 @@
}
.sc-chat--disconnected-button {
border: 1px solid;
border-radius: 20px;
border-radius: 2rem;
padding: 5px 10px;
width: 80%;
margin: 2px;

View File

@@ -38,7 +38,7 @@
display: flex;
flex-direction: row;
position: relative;
max-width: calc(100% - 60px);
max-width: calc(100% - 2rem);
width: auto;
.sc-message--avatar {
@@ -49,7 +49,7 @@
background-position: 0 0;
flex-shrink: 0;
background-size: cover;
border-radius: 999px;
border-radius: 100%;
}
.sc-message--wrapper {
@@ -59,11 +59,10 @@
.sc-message--text {
padding: 10px 20px;
border-radius: 6px;
font-weight: 300;
font-size: .85rem;
max-width: 190px;
line-height: 1.2;
border-radius: .5rem;
font-weight: 400;
font-size: .9rem;
line-height: 1.5;
position: relative;
-webkit-font-smoothing: subpixel-antialiased;
color: #263238;
@@ -74,10 +73,6 @@
white-space: pre-wrap;
}
}
code {
font-family: "Courier New", Courier, monospace !important;
}
}
.sc-message--meta {

View File

@@ -17,6 +17,7 @@ import { useColors } from '../providers/ColorProvider';
import { TMessage } from '../types/message.types';
import ChatIcon from './icons/ChatIcon';
import './Message.scss';
import ButtonsMessage from './messages/ButtonMessage';
import CarouselMessage from './messages/CarouselMessage';
import FileMessage from './messages/FileMessage';
@@ -24,7 +25,6 @@ import GeolocationMessage from './messages/GeolocationMessage';
import ListMessage from './messages/ListMessage';
import TextMessage from './messages/TextMessage';
import MessageStatus from './MessageStatus';
import './Message.scss';
dayjs.extend(relativeTime);
@@ -58,7 +58,11 @@ const Message: React.FC<MessageProps> = ({ message, Avatar }) => {
className="sc-message--avatar"
style={
user.imageUrl
? { backgroundImage: `url(${user.imageUrl})` }
? {
backgroundImage: `url(${
user.imageUrl
}?color=${encodeURIComponent(colors.header.bg)})`,
}
: undefined
}
>

View File

@@ -1,13 +1,13 @@
.sc-suggestions-row {
text-align: center;
padding: 0.25rem 0;
padding: 0.5rem 0;
}
.sc-suggestions-element {
margin: 0 0 0.25rem 0.25rem;
margin: 0 0 0 0.25rem;
padding: 0.25rem 0.5rem;
border: 1px solid;
border-radius: 15px;
border-radius: 2rem;
font-size: 1rem;
background: inherit;
cursor: pointer;

View File

@@ -5,10 +5,10 @@
bottom: 0;
display: flex;
background-color: #f4f7f9;
border-bottom-left-radius: 10px;
border-bottom-right-radius: 10px;
border-bottom-left-radius: .5rem;
border-bottom-right-radius: .5rem;
transition: background-color 0.2s ease, box-shadow 0.2s ease;
border: 1px solid #eaeaea;
border-top: 1px solid #eef2f4;
}
.sc-user-input--text {
@@ -16,7 +16,7 @@
resize: none;
border: none;
outline: none;
border-bottom-left-radius: 10px;
border-bottom-left-radius: .5rem;
box-sizing: border-box;
padding: 18px 8px;
font-size: 15px;

View File

@@ -181,10 +181,20 @@ const UserInput: React.FC = () => {
onBlur={() => setInputActive(false)}
onKeyDown={handleKey}
onInput={handleInput}
onPaste={(e) => {
onPaste={async (e) => {
e.preventDefault();
(e.target as HTMLInputElement).innerText =
e.clipboardData.getData('text/plain');
const text = await navigator.clipboard.readText();
const range = window.getSelection()?.getRangeAt(0);
if (range && text) {
const node = document.createTextNode(text);
range.deleteContents();
range.insertNode(node);
range.collapse(false);
}
handleInput();
}}
contentEditable

View File

@@ -21,17 +21,17 @@
.user-subscription-form {
.user-subscription-form-input {
border: 0;
background-color: #eeeeee;
border: 1px solid #dfe5e8;
outline: 0;
padding: 1rem;
border-radius: 10px;
border-radius: 2rem;
margin: 1rem;
width: 70%;
}
.user-subscription-form-button-submit {
display: block;
border-radius: 10px;
border-radius: 2rem;
border: 0;
width: 50%;
margin: 2rem auto;

View File

@@ -20,7 +20,6 @@ import { useColors } from '../providers/ColorProvider';
import { useConfig } from '../providers/ConfigProvider';
import { useSettings } from '../providers/SettingsProvider';
import { useSocket } from '../providers/SocketProvider';
import './UserSubscription.scss';
import { useWidget } from '../providers/WidgetProvider';
import {
Direction,
@@ -28,6 +27,7 @@ import {
TMessage,
TOutgoingMessageType,
} from '../types/message.types';
import './UserSubscription.scss';
const UserSubscription: React.FC = () => {
const config = useConfig();
@@ -55,7 +55,7 @@ const UserSubscription: React.FC = () => {
messages: TMessage[];
profile: ISubscriber;
}>(
`/webhook/${config.channel}/?verification_token=${config.token}&first_name=${firstName}&last_name=${lastName}`,
`/webhook/${config.channel}/?first_name=${firstName}&last_name=${lastName}`,
);
const { messages, profile } = body;

View File

@@ -9,46 +9,52 @@
import { FC, SVGProps } from 'react';
const ChatIcon: FC<SVGProps<SVGSVGElement>> = ({
viewBox = '-4749.48 -5020 35.036 35.036',
width = '18',
height = '18',
...rest
}) => {
return (
<svg xmlns="http://www.w3.org/2000/svg" viewBox={viewBox} {...rest}>
<defs>
<clipPath id="a">
<path
className="a"
style={{ fill: 'none' }}
d="M0-399.479H17.555v17.555H0Z"
transform="translate(0 399.479)"
/>
</clipPath>
</defs>
<g transform="translate(-4886 -5075)">
<circle
style={{ fill: '#4e8cff' }}
cx="17.518"
cy="17.518"
r="17.518"
transform="translate(136.52 55)"
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 42.555282 47.2949"
width={width}
height={height}
{...rest}
>
<g fillOpacity={1} strokeDasharray="none">
<path
d="M32.756 170.872l-4.26 7.482-2.786-7.494-8.211-.017a4.405 4.405 0 01-3.8-2.191l-6.443-11.087a4.215 4.215 0 01-.011-4.216l6.213-10.833a4.96 4.96 0 014.288-2.492l12.2-.034a4.715 4.715 0 014.09 2.347l6.16 10.602a4.864 4.864 0 01.02 4.855z"
fill="none"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeOpacity={1}
strokeWidth={4.4649702399999995}
paintOrder="normal"
style={{
mixBlendMode: 'normal',
}}
transform="translate(-58.835 -133.808) translate(53.705 -18.313) scale(1.10427)"
/>
<g transform="translate(145.13 64)">
<g style={{ clipPath: "url('#a')" }}>
<g transform="translate(0 0)">
<path
style={{ fill: '#fff' }}
d="M-381.924-190.962a8.778,8.778,0,0,0-8.778-8.778,8.778,8.778,0,0,0-8.778,8.778,8.745,8.745,0,0,0,2.26,5.879v1.442c0,.8.492,1.457,1.1,1.457h5.83a.843.843,0,0,0,.183-.02,8.778,8.778,0,0,0,8.184-8.757"
transform="translate(399.479 199.74)"
/>
</g>
<g transform="translate(0 0)">
<path
style={{ fill: '#eff4f9' }}
d="M-68.763-194.079a9.292,9.292,0,0,1,6.38-8.888c-.252-.022-.506-.033-.763-.033a8.774,8.774,0,0,0-8.778,8.778A9.508,9.508,0,0,0-69.7-188.3c.005,0,0,.009,0,.01-.311.352-1.924,2.849.021,2.849h2.25c-1.23-.022,1.263-2.107.269-3.494a8.225,8.225,0,0,1-1.6-5.141"
transform="translate(71.924 203)"
/>
</g>
</g>
<g
fill="currentColor"
fillRule="nonzero"
stroke="none"
strokeWidth={0.662}
fillOpacity={1}
>
<path
d="M-532.348 630.303a6.253 6.253 0 11-12.506.023 6.253 6.253 0 0112.506-.023"
transform="translate(-58.835 -133.808) translate(53.705 -18.313) scale(1.10427) matrix(.25113 0 0 .25263 -27.6 133.42) matrix(1.51171 0 0 1.50275 1044.41 -860.854)"
/>
<path
d="M-532.348 630.303a6.253 6.253 0 11-12.506.023 6.253 6.253 0 0112.506-.023"
transform="translate(-58.835 -133.808) translate(53.705 -18.313) scale(1.10427) matrix(.25113 0 0 .25263 -27.6 133.42) matrix(1.51171 0 0 1.50275 992.8 -860.854)"
/>
<path
d="M-532.348 630.303a6.253 6.253 0 11-12.506.023 6.253 6.253 0 0112.506-.023"
transform="translate(-58.835 -133.808) translate(53.705 -18.313) scale(1.10427) matrix(.25113 0 0 .25263 -27.6 133.42) matrix(1.51171 0 0 1.50275 1018.605 -860.854)"
/>
</g>
</g>
</svg>

View File

@@ -2,7 +2,7 @@
color: rgb(34, 34, 34);
max-width: -webkit-fill-available;
padding: 0.25rem 0.5rem;
border-radius: 6px;
border-radius: .5rem;
font-weight: 300;
font-size: 1.25rem;
line-height: 1.4;
@@ -12,7 +12,7 @@
.sc-message--buttons-content {
border: 1px solid;
border-radius: 20px;
border-radius: 2rem;
padding: 0.25rem;
width: 80%;
margin: 2px;

View File

@@ -1,5 +1,5 @@
.sc-message--carousel {
border-radius: 10px;
border-radius: .5rem;
position: relative;
width: 100%;
overflow: hidden;

View File

@@ -1,6 +1,6 @@
.sc-message--file {
background-color: transparent !important;
border-radius: 6px;
border-radius: .5rem;
font-weight: 300;
font-size: 14px;
line-height: 1.4;
@@ -31,7 +31,7 @@
.sc-message--file-download {
padding: 10px 20px;
border-radius: 6px;
border-radius: .5rem;
color: white;
text-align: center;

View File

@@ -1,8 +1,8 @@
.sc-message--location {
border-radius: 6px;
border-radius: .5rem;
}
.sc-message-map {
width: 200px;
height: 150px;
border-radius: 6px;
border-radius: .5rem;
}

View File

@@ -1,5 +1,5 @@
.sc-message--list {
border-radius: 10px;
border-radius: .5rem;
width: 256px;
.sc-message--list-element {
@@ -19,12 +19,12 @@
.sc-message--list-element-image {
background-size: cover;
height: auto;
border-radius: 10px 10px 0 0;
border-radius: .5rem .5rem 0 0;
}
.sc-message--list-element-description {
color: #fff;
border-radius: 10px 10px 0 0;
border-radius: .5rem .5rem 0 0;
background: rgba(0, 0, 0, 0.5);
}
}

View File

@@ -1,7 +1,7 @@
.sc-typing-indicator {
text-align: center;
padding: 2px 5px;
border-radius: 6px;
padding: .5rem;
border-radius: 2rem;
width: 50px;
margin-left: 2rem;
}

View File

@@ -25,11 +25,11 @@ const colors: Record<string, ColorState> = {
text: '#fff',
},
received: {
bg: '#eaeaea',
bg: '#f6f8f9',
text: '#000',
},
userInput: {
bg: '#fbfbfb',
bg: '#fff',
text: '#000',
},
button: {
@@ -60,11 +60,11 @@ const colors: Record<string, ColorState> = {
text: '#fff',
},
received: {
bg: '#eaeaea',
bg: '#f6f8f9',
text: '#000',
},
userInput: {
bg: '#fbfbfb',
bg: '#fff',
text: '#000',
},
button: {
@@ -95,11 +95,11 @@ const colors: Record<string, ColorState> = {
text: '#fff',
},
received: {
bg: '#eaeaea',
bg: '#f6f8f9',
text: '#000',
},
userInput: {
bg: '#fbfbfb',
bg: '#fff',
text: '#000',
},
button: {
@@ -130,11 +130,11 @@ const colors: Record<string, ColorState> = {
text: '#fff',
},
received: {
bg: '#eaeaea',
bg: '#f6f8f9',
text: '#000',
},
userInput: {
bg: '#fbfbfb',
bg: '#fff',
text: '#000',
},
button: {
@@ -165,11 +165,11 @@ const colors: Record<string, ColorState> = {
text: '#fff',
},
received: {
bg: '#eaeaea',
bg: '#f6f8f9',
text: '#000',
},
userInput: {
bg: '#fbfbfb',
bg: '#fff',
text: '#000',
},
button: {
@@ -200,15 +200,15 @@ const colors: Record<string, ColorState> = {
text: '#FFF',
},
received: {
bg: '#F0F0F0',
bg: '#f6f8f9',
text: '#000',
},
userInput: {
bg: '#FFF',
bg: '#fff',
text: '#000',
},
button: {
bg: '#2c3e50',
bg: '#000',
text: '#ecf0f1',
border: '#34495e',
},

View File

@@ -9,6 +9,5 @@
export const DEFAULT_CONFIG = {
apiUrl: process.env.REACT_APP_WIDGET_API_URL || 'http://localhost:4000',
channel: process.env.REACT_APP_WIDGET_CHANNEL || 'console-channel',
token: process.env.REACT_APP_WIDGET_TOKEN || 'test',
language: 'en',
};

View File

@@ -19,7 +19,6 @@ ReactDOM.createRoot(document.getElementById('root')!).render(
{...{
apiUrl: process.env.REACT_APP_WIDGET_API_URL || 'http://localhost:4000',
channel: process.env.REACT_APP_WIDGET_CHANNEL || 'web-channel',
token: process.env.REACT_APP_WIDGET_TOKEN || 'token123',
language: 'en',
}}
/>

View File

@@ -289,7 +289,7 @@ const ChatProvider: React.FC<{
);
setMessage('');
const sentMessage = await socketCtx.socket.post<TMessage>(
`/webhook/${config.channel}/?verification_token=${config.token}`,
`/webhook/${config.channel}/`,
{
data: {
...data,
@@ -308,7 +308,7 @@ const ChatProvider: React.FC<{
messages: TMessage[];
profile: ISubscriber;
}>(
`/webhook/${config.channel}/?verification_token=${config.token}&first_name=${firstName}&last_name=${lastName}`,
`/webhook/${config.channel}/?first_name=${firstName}&last_name=${lastName}`,
);
localStorage.setItem('profile', JSON.stringify(body.profile));

View File

@@ -13,7 +13,6 @@ import { DEFAULT_CONFIG } from '../constants/defaultConfig';
// Define the type for your config, including all possible properties
export type Config = {
apiUrl: string;
token: string;
channel: string;
language: string;
};
@@ -23,7 +22,6 @@ const ConfigContext = createContext<Config>(DEFAULT_CONFIG);
export const ConfigProvider: React.FC<{
apiUrl?: string;
token?: string;
channel?: string;
language?: string;
children: ReactNode;

View File

@@ -23,7 +23,6 @@ import { useSubscribe } from './SocketProvider';
type ChannelSettings = {
menu: IMenuNode[];
secret: string;
verification_token: string;
allowed_domains: string;
start_button: boolean;
input_disabled: boolean;
@@ -110,7 +109,7 @@ export const SettingsProvider: React.FC<ChatSettingsProviderProps> = ({
allowedUploadSize: settings.allowed_upload_size,
inputDisabled: settings.input_disabled,
color: settings.theme_color,
greetingMessage: t('settings.greeting'),
greetingMessage: settings.greeting_message,
placeholder: t('settings.placeholder'),
avatarUrl: settings.avatar_url,
});

View File

@@ -7,7 +7,7 @@
*/
export type ColorState = {
header: { bg?: string; text?: string };
header: { bg: string; text: string };
launcher: { bg?: string };
messageList: { bg?: string };
sent: { bg?: string; text?: string };

View File

@@ -6,7 +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 { io, Socket, ManagerOptions, SocketOptions } from 'socket.io-client';
import { io, ManagerOptions, Socket, SocketOptions } from 'socket.io-client';
import { Config } from '../providers/ConfigProvider';
import {
@@ -195,14 +195,16 @@ let socketIoClient: SocketIoClient;
* @param handlers Event handlers
* @returns Socket io client instance
*/
export const getSocketIoClient = (config: Config, handlers: SocketIoEventHandlers) => {
export const getSocketIoClient = (
config: Config,
handlers: SocketIoEventHandlers,
) => {
if (!socketIoClient) {
socketIoClient = new SocketIoClient(
config.apiUrl,
{
query: {
channel: config.channel,
verification_token: config.token,
},
},
handlers,