mirror of
https://github.com/hexastack/hexabot
synced 2025-06-26 18:27:28 +00:00
Merge remote-tracking branch 'upstream/main' into 40-issue-prevent-users-from-deleting-their-own-roles
This commit is contained in:
91
.github/workflows/docker.yml
vendored
Normal file
91
.github/workflows/docker.yml
vendored
Normal file
@@ -0,0 +1,91 @@
|
||||
name: Build and Push Docker Images
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: ["main"]
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
paths-filter:
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
frontend: ${{ steps.filter.outputs.frontend }}
|
||||
api: ${{ steps.filter.outputs.api }}
|
||||
widget: ${{ steps.filter.outputs.widget }}
|
||||
nlu: ${{ steps.filter.outputs.nlu }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Filter paths
|
||||
id: filter
|
||||
uses: dorny/paths-filter@v3
|
||||
with:
|
||||
filters: |
|
||||
frontend:
|
||||
- 'frontend/**'
|
||||
api:
|
||||
- 'api/**'
|
||||
widget:
|
||||
- 'widget/**'
|
||||
nlu:
|
||||
- 'nlu/**'
|
||||
|
||||
build-and-push:
|
||||
runs-on: ubuntu-latest
|
||||
needs:
|
||||
- paths-filter
|
||||
steps:
|
||||
- name: Check out repository code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Login to Docker Hub
|
||||
id: docker_login
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Build and push Frontend Docker image
|
||||
if: ${{ needs.paths-filter.outputs.frontend == 'true' }}
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: ./
|
||||
file: ./frontend/Dockerfile
|
||||
platforms: linux/amd64,linux/arm64
|
||||
push: true
|
||||
tags: hexastack/hexabot-ui:latest
|
||||
|
||||
- name: Build and push API Docker image
|
||||
if: ${{ needs.paths-filter.outputs.api == 'true' }}
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: ./api/
|
||||
file: ./api/Dockerfile
|
||||
platforms: linux/amd64,linux/arm64
|
||||
push: true
|
||||
tags: hexastack/hexabot-api:latest
|
||||
|
||||
- name: Build and push Widget Docker image
|
||||
if: ${{ needs.paths-filter.outputs.widget == 'true' }}
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: ./widget/
|
||||
file: ./widget/Dockerfile
|
||||
platforms: linux/amd64,linux/arm64
|
||||
push: true
|
||||
tags: hexastack/hexabot-widget:latest
|
||||
|
||||
- name: Build and push NLU Docker image
|
||||
if: ${{ needs.paths-filter.outputs.nlu == 'true' }}
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: ./nlu/
|
||||
file: ./nlu/Dockerfile
|
||||
platforms: linux/amd64,linux/arm64
|
||||
push: true
|
||||
tags: hexastack/hexabot-nlu:latest
|
||||
41
Makefile
41
Makefile
@@ -1,5 +1,4 @@
|
||||
# Default path setup
|
||||
COMPOSE_FILES := -f ./docker/docker-compose.yml -f ./docker/docker-compose.dev.yml
|
||||
COMPOSE_FILES := -f ./docker/docker-compose.yml
|
||||
|
||||
# Function to add service files
|
||||
define add_service
|
||||
@@ -8,20 +7,29 @@ define add_service
|
||||
ifneq ($(wildcard ./docker/docker-compose.$(1).prod.yml),)
|
||||
COMPOSE_FILES += -f ./docker/docker-compose.$(1).prod.yml
|
||||
endif
|
||||
else
|
||||
COMPOSE_FILES += -f ./docker/docker-compose.$(1).yml -f ./docker/docker-compose.$(1).dev.yml
|
||||
else ifeq ($(DEV_MODE), true)
|
||||
COMPOSE_FILES += -f ./docker/docker-compose.$(1).yml
|
||||
ifneq ($(wildcard ./docker/docker-compose.$(1).dev.yml),)
|
||||
COMPOSE_FILES += -f ./docker/docker-compose.$(1).dev.yml
|
||||
endif
|
||||
endif
|
||||
endef
|
||||
|
||||
|
||||
# Check if services are specified and add corresponding compose files
|
||||
ifneq ($(NGINX),)
|
||||
$(eval $(call add_service,nginx))
|
||||
endif
|
||||
|
||||
ifneq ($(NLU),)
|
||||
$(eval $(call add_service,nlu))
|
||||
endif
|
||||
# Function to set up COMPOSE_FILES
|
||||
define compose_files
|
||||
ifeq ($(1), true)
|
||||
ifneq ($(wildcard ./docker/docker-compose.dev.yml),)
|
||||
COMPOSE_FILES += -f ./docker/docker-compose.dev.yml
|
||||
endif
|
||||
endif
|
||||
ifneq ($(NGINX),)
|
||||
$(eval $(call add_service,nginx))
|
||||
endif
|
||||
ifneq ($(NLU),)
|
||||
$(eval $(call add_service,nlu))
|
||||
endif
|
||||
endef
|
||||
|
||||
# Ensure .env file exists and matches .env.example
|
||||
check-env:
|
||||
@@ -36,16 +44,21 @@ init:
|
||||
cp ./docker/.env.example ./docker/.env
|
||||
|
||||
dev: check-env
|
||||
$(eval $(call compose_files,true))
|
||||
docker compose $(COMPOSE_FILES) up -d
|
||||
|
||||
start: check-env
|
||||
docker compose $(COMPOSE_FILES) up -d --build
|
||||
$(eval $(call compose_files,false))
|
||||
docker compose $(COMPOSE_FILES) up -d
|
||||
|
||||
stop: check-env
|
||||
$(eval $(call compose_files,true))
|
||||
docker compose $(COMPOSE_FILES) down
|
||||
|
||||
destroy: check-env
|
||||
$(eval $(call compose_files,true))
|
||||
docker compose $(COMPOSE_FILES) down -v
|
||||
|
||||
migrate-up:
|
||||
docker-compose $(COMPOSE_FILES) up --no-deps -d database-init
|
||||
$(eval $(call compose_files,false))
|
||||
docker-compose $(COMPOSE_FILES) up --no-deps -d database-init
|
||||
|
||||
27
README.md
27
README.md
@@ -1,4 +1,5 @@
|
||||
# Hexabot
|
||||
|
||||

|
||||
|
||||
## Description
|
||||
@@ -20,6 +21,7 @@
|
||||
- **Inbox & Handover:** Provides a real-time chat window where conversations can be monitored and handed over to human agents when necessary.
|
||||
|
||||
## Directory Structure
|
||||
|
||||
- **frontend:** The admin panel built with React/Next.js for managing chatbot configurations and flows.
|
||||
- **api:** The backend API built with NestJS and connected to MongoDB for data storage and management.
|
||||
- **widget:** A React-based live chat widget that can be embedded into any website to provide real-time interaction.
|
||||
@@ -27,6 +29,7 @@
|
||||
- **docker:** A set of Docker Compose files for deploying the entire solution, making it easy to run Hexabot in any environment.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
To ensure Hexabot runs smoothly, you'll need the following:
|
||||
|
||||
- **Docker:** We recommend using Docker to start the app since multiple services are required (MongoDB, Redis, Prometheus, etc.). All the necessary Docker Compose files are located in the docker folder.
|
||||
@@ -35,28 +38,37 @@ To ensure Hexabot runs smoothly, you'll need the following:
|
||||
## Installation
|
||||
|
||||
1. **Clone the Repository:**
|
||||
|
||||
```bash
|
||||
$ git clone https://github.com/hexastack/hexabot.git
|
||||
```
|
||||
|
||||
2. **Environment Setup:** To configure the environment variables, use the Makefile at the root folder for initialization:
|
||||
|
||||
```bash
|
||||
$ make init
|
||||
```
|
||||
|
||||
This will copy the `.env.example` file to `.env` in the `./docker` directory if the file does not already exist.
|
||||
|
||||
3. **Running the Application:** Once your environment is set up, you can start the app. Use either of the following commands:
|
||||
|
||||
```bash
|
||||
$ make start
|
||||
```
|
||||
|
||||
or for development mode:
|
||||
|
||||
```bash
|
||||
$ make dev
|
||||
```
|
||||
|
||||
**Note:** The first time you run the app, Docker will take some time to download all the required images.
|
||||
|
||||
## Usage
|
||||
|
||||
UI Admin Panel is accessible via http://localhost:8080, the default credentials are :
|
||||
UI Admin Panel is accessible via http://localhost:8080, the default credentials are :
|
||||
|
||||
- **Username:** admin@admin.admin
|
||||
- **Password:** adminadmin
|
||||
|
||||
@@ -65,18 +77,20 @@ Live Chat Widget is accessible via http://localhost:5173
|
||||
## Commands
|
||||
|
||||
- `make init` : Copies the .env.example file to .env in the ./docker directory if .env does not exist. This is usually used for initial setup.
|
||||
- `make dev` : Starts all configured Docker services in development mode. It first checks the .env file for completeness against .env.example.
|
||||
- `make start` : Similar to dev, but explicitly builds the Docker images before starting the services. This target also checks the .env file for required variables.
|
||||
- `make dev` : Starts all configured Docker services in development mode. It first checks the .env file for completeness against .env.example and builds the docker images locally.
|
||||
- `make start` : Starts all configured Docker services by loading all images from Docker Hub. This target also checks the .env file for required variables.
|
||||
- `make stop` : Stops all running Docker services defined in the compose files.
|
||||
- `make destroy` : Stops all services and removes all volumes associated with the Docker compose setup, ensuring a clean state.
|
||||
- `make check-env` : Checks if the ./docker/.env file exists and contains all the necessary environment variables as defined in ./docker/.env.example. If the file does not exist, it is created from the example. It also lists missing variables if any.
|
||||
|
||||
Example on how to start the stack by adding the Nginx service :
|
||||
Example on how to start the stack by adding the Nginx service :
|
||||
|
||||
```sh
|
||||
make start NGINX=1
|
||||
```
|
||||
|
||||
## Documentation
|
||||
|
||||
For detailed information on how to get started, as well as in-depth user and developer guides, please refer to our full documentation available in the docs folder or visit the [Documentation](https://docs.hexabot.ai).
|
||||
|
||||
You can also find specific documentation for different components of the project in the following locations:
|
||||
@@ -86,16 +100,17 @@ You can also find specific documentation for different components of the project
|
||||
- [Live Chat Widget Documentation](widget/README.md)
|
||||
- [NLU Engine Documentation](nlu/README.md)
|
||||
|
||||
## Contributing
|
||||
## Contributing
|
||||
|
||||
We welcome contributions from the community! Whether you want to report a bug, suggest new features, or submit a pull request, your input is valuable to us.
|
||||
|
||||
Please refer to our contribution policy first : [How to contribute to Hexabot](./CONTRIBUTING.md)
|
||||
[](./CODE_OF_CONDUCT.md)
|
||||
|
||||
|
||||
Feel free to join us on [Discord](https://discord.gg/rNb9t2MFkG)
|
||||
|
||||
## License
|
||||
|
||||
This software is 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.
|
||||
|
||||
@@ -57,24 +57,28 @@ const i18nOptions: I18nOptions = {
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
MailerModule.forRoot({
|
||||
transport: new SMTPTransport({
|
||||
...config.emails.smtp,
|
||||
logger: true,
|
||||
}),
|
||||
template: {
|
||||
adapter: new MjmlAdapter('ejs', { inlineCssEnabled: false }),
|
||||
dir: './src/templates',
|
||||
options: {
|
||||
context: {
|
||||
appName: config.parameters.appName,
|
||||
appUrl: config.parameters.appUrl,
|
||||
// TODO: add i18n support
|
||||
},
|
||||
},
|
||||
},
|
||||
defaults: { from: config.parameters.email.main },
|
||||
}),
|
||||
...(config.emails.isEnabled
|
||||
? [
|
||||
MailerModule.forRoot({
|
||||
transport: new SMTPTransport({
|
||||
...config.emails.smtp,
|
||||
logger: true,
|
||||
debug: false,
|
||||
}),
|
||||
template: {
|
||||
adapter: new MjmlAdapter('ejs', { inlineCssEnabled: false }),
|
||||
dir: './src/templates',
|
||||
options: {
|
||||
context: {
|
||||
appName: config.parameters.appName,
|
||||
appUrl: config.parameters.appUrl,
|
||||
},
|
||||
},
|
||||
},
|
||||
defaults: { from: config.emails.from },
|
||||
}),
|
||||
]
|
||||
: []),
|
||||
MongooseModule.forRoot(config.mongo.uri, {
|
||||
dbName: config.mongo.dbName,
|
||||
connectionFactory: (connection) => {
|
||||
|
||||
@@ -8,19 +8,14 @@
|
||||
*/
|
||||
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { EventEmitter2 } from '@nestjs/event-emitter';
|
||||
|
||||
import { ExtendedI18nService } from './extended-i18n.service';
|
||||
|
||||
@Injectable()
|
||||
export class AppService {
|
||||
constructor(
|
||||
private readonly i18n: ExtendedI18nService,
|
||||
private readonly eventEmitter: EventEmitter2,
|
||||
) {}
|
||||
constructor(private readonly i18n: ExtendedI18nService) {}
|
||||
|
||||
getHello(): string {
|
||||
this.eventEmitter.emit('hook:i18n:refresh', []);
|
||||
return this.i18n.t('Welcome');
|
||||
return this.i18n.t('welcome', { lang: 'en' });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,18 +1,21 @@
|
||||
{
|
||||
"invitation_subject": "[Hexabot] Sign-Up Invitation",
|
||||
"account_confirmation_subject": "[Hexabot] Account Confirmation",
|
||||
"password_reset_subject": "[Hexabot] Password Reset",
|
||||
"welcome": "Welcome",
|
||||
"hi": "Hi",
|
||||
"invitation_to_join": "Invitation to join",
|
||||
"invitation_for_account_creation": "You have been invited to create a",
|
||||
"account": "account",
|
||||
"create_account:": "Click on the button below to create your account:",
|
||||
"create_account": "Click on the button below to create your account:",
|
||||
"registration_failed": "Registration failed! Either email address or invitation token is invalid.",
|
||||
"create_account_button": "Click on the button below to create your account",
|
||||
"join": "Join",
|
||||
"account_successfully_created_confirm_password": "You have successfully created an account but you will need to confirm your email address.",
|
||||
"confirm_account:": "Click on the button below in order to confirm your account:",
|
||||
"confirm_account": "Click on the button below in order to confirm your account:",
|
||||
"password_reset_request": "A request to reset the password for your account has been made.",
|
||||
"reset": "Reset",
|
||||
"click_to_reset_password:": "Click on the button below in order to set a new password:",
|
||||
"click_to_reset_password": "Click on the button below in order to set a new password:",
|
||||
"confirm": "Confirm",
|
||||
"best_regards": "Best Regards,"
|
||||
}
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
{
|
||||
"invitation_subject": "[Hexabot] Invitation à s'inscrire",
|
||||
"account_confirmation_subject": "[Hexabot] Confirmation de compte",
|
||||
"password_reset_subject": "[Hexabot] Réinitialisation du mot de passe",
|
||||
"welcome": "Bienvenue",
|
||||
"hi": "Bonjour",
|
||||
"invitation_to_join": "Invitation",
|
||||
|
||||
@@ -13,7 +13,7 @@ import { Config } from './types';
|
||||
|
||||
export const config: Config = {
|
||||
i18n: {
|
||||
translationFilename: process.env.I18N_TRANSLATION_FILENAME || '',
|
||||
translationFilename: process.env.I18N_TRANSLATION_FILENAME || 'messages',
|
||||
},
|
||||
appPath: process.cwd(),
|
||||
apiPath: process.env.API_ORIGIN,
|
||||
@@ -77,7 +77,7 @@ export const config: Config = {
|
||||
: [undefined], // ['http://example.com', 'https://example.com'],
|
||||
},
|
||||
session: {
|
||||
secret: process.env.SESSION_SECRET || '4fac3596aeb0d048e7b6b38235c29248',
|
||||
secret: process.env.SESSION_SECRET || 'changeme',
|
||||
name: process.env.SESSION_NAME || 'hex.sid',
|
||||
adapter: 'connect-mongo',
|
||||
url: 'mongodb://localhost:27017/hexabot',
|
||||
@@ -90,23 +90,18 @@ export const config: Config = {
|
||||
},
|
||||
},
|
||||
emails: {
|
||||
isEnabled: process.env.EMAIL_SMTP_ENABLED === 'true' || false,
|
||||
smtp: {
|
||||
port: parseInt(process.env.EMAIL_SMTP_PORT) || 25,
|
||||
host: process.env.EMAIL_SMTP_HOST || 'smtp.mailgun.org',
|
||||
host: process.env.EMAIL_SMTP_HOST || 'localhost',
|
||||
ignoreTLS: false,
|
||||
secure: process.env.EMAIL_SMTP_SECURE === 'true' || false,
|
||||
auth: {
|
||||
user:
|
||||
process.env.EMAIL_SMTP_USER ||
|
||||
'postmaster@sandbox9471202ff10448c7ac917618fe94d8e1.mailgun.org',
|
||||
pass: process.env.EMAIL_SMTP_PASS || 'e58526b30ad640394b5c77a211a19c5b',
|
||||
user: process.env.EMAIL_SMTP_USER || '',
|
||||
pass: process.env.EMAIL_SMTP_PASS || '',
|
||||
},
|
||||
},
|
||||
},
|
||||
datastores: {
|
||||
default: {
|
||||
adapter: 'sails-mongo',
|
||||
url: 'mongodb://localhost:27017/hexabot',
|
||||
},
|
||||
from: process.env.EMAIL_SMTP_FROM || 'noreply@example.com',
|
||||
},
|
||||
parameters: {
|
||||
uploadDir:
|
||||
@@ -117,17 +112,9 @@ export const config: Config = {
|
||||
maxUploadSize: process.env.UPLOAD_MAX_SIZE_IN_BYTES
|
||||
? Number(process.env.UPLOAD_MAX_SIZE_IN_BYTES)
|
||||
: 2000000,
|
||||
transport: 'smtp',
|
||||
email: {
|
||||
main: 'postmaster@sandbox9471202ff10448c7ac917618fe94d8e1.mailgun.org',
|
||||
},
|
||||
appName: 'Hexabot.ai',
|
||||
apiUrl: 'http://localhost:4000',
|
||||
appUrl: 'http://localhost:8081',
|
||||
geocoder: {
|
||||
provider: 'opencage',
|
||||
apiKey: 'c2a490d593b14612aefa6ec2e6b77c47',
|
||||
},
|
||||
},
|
||||
pagination: {
|
||||
limit: 10,
|
||||
@@ -135,7 +122,7 @@ export const config: Config = {
|
||||
chatbot: {
|
||||
lang: {
|
||||
default: 'en',
|
||||
available: ['en', 'fr', 'ar', 'tn'],
|
||||
available: ['en', 'fr'],
|
||||
},
|
||||
messages: {
|
||||
track_delivery: false,
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
* 3. SaaS Restriction: This software, or any derivative of it, may not be used to offer a competing product or service (SaaS) without prior written consent from Hexastack. Offering the software as a service or using it in a commercial cloud environment without express permission is strictly prohibited.
|
||||
*/
|
||||
|
||||
import SMTPConnection from 'nodemailer/lib/smtp-connection';
|
||||
import type { ServerOptions, Socket } from 'socket.io';
|
||||
|
||||
type TJwtOptions = {
|
||||
@@ -69,38 +70,18 @@ export type Config = {
|
||||
};
|
||||
};
|
||||
emails: {
|
||||
smtp: {
|
||||
port: number;
|
||||
host: string;
|
||||
secure: boolean;
|
||||
auth: {
|
||||
user: string;
|
||||
pass: string;
|
||||
};
|
||||
};
|
||||
};
|
||||
datastores: {
|
||||
default: {
|
||||
adapter: string;
|
||||
url: string;
|
||||
};
|
||||
isEnabled: boolean;
|
||||
smtp: Partial<SMTPConnection.Options>;
|
||||
from: string;
|
||||
};
|
||||
parameters: {
|
||||
uploadDir: string;
|
||||
avatarDir: string;
|
||||
storageMode: 'disk' | 'memory';
|
||||
maxUploadSize: number;
|
||||
transport: string;
|
||||
email: {
|
||||
main: string;
|
||||
};
|
||||
appName: string;
|
||||
apiUrl: string;
|
||||
appUrl: string;
|
||||
geocoder: {
|
||||
provider: string;
|
||||
apiKey: string;
|
||||
};
|
||||
};
|
||||
pagination: {
|
||||
limit: number;
|
||||
|
||||
@@ -58,9 +58,11 @@ export class ExtendedI18nService<
|
||||
initDynamicTranslations(translations: Translation[]) {
|
||||
this.dynamicTranslations = translations.reduce((acc, curr) => {
|
||||
const { str, translations } = curr;
|
||||
Object.entries(translations).forEach(([lang, t]) => {
|
||||
acc[lang][str] = t;
|
||||
});
|
||||
Object.entries(translations)
|
||||
.filter(([lang]) => lang in acc)
|
||||
.forEach(([lang, t]) => {
|
||||
acc[lang][str] = t;
|
||||
});
|
||||
|
||||
return acc;
|
||||
}, this.dynamicTranslations);
|
||||
|
||||
@@ -111,10 +111,4 @@ export type Settings = {
|
||||
fallback_message: string[];
|
||||
fallback_block: string;
|
||||
};
|
||||
email_settings: {
|
||||
mailer: string;
|
||||
auth_user: string;
|
||||
auth_pass: string;
|
||||
from: string;
|
||||
};
|
||||
} & Record<string, any>;
|
||||
|
||||
@@ -99,56 +99,6 @@ export const settingModels: SettingCreateDto[] = [
|
||||
},
|
||||
weight: 6,
|
||||
},
|
||||
{
|
||||
group: 'email_settings',
|
||||
label: 'from',
|
||||
value: 'no-reply@domain.com',
|
||||
type: SettingType.text,
|
||||
weight: 1,
|
||||
},
|
||||
{
|
||||
group: 'email_settings',
|
||||
label: 'mailer',
|
||||
value: 'sendmail',
|
||||
options: ['sendmail', 'smtp'],
|
||||
type: SettingType.select,
|
||||
weight: 2,
|
||||
},
|
||||
{
|
||||
group: 'email_settings',
|
||||
label: 'host',
|
||||
value: 'localhost',
|
||||
type: SettingType.text,
|
||||
weight: 3,
|
||||
},
|
||||
{
|
||||
group: 'email_settings',
|
||||
label: 'port',
|
||||
value: '25',
|
||||
type: SettingType.text,
|
||||
weight: 4,
|
||||
},
|
||||
{
|
||||
group: 'email_settings',
|
||||
label: 'secure',
|
||||
value: true,
|
||||
type: SettingType.checkbox,
|
||||
weight: 5,
|
||||
},
|
||||
{
|
||||
group: 'email_settings',
|
||||
label: 'auth_user',
|
||||
value: '',
|
||||
type: SettingType.text,
|
||||
weight: 6,
|
||||
},
|
||||
{
|
||||
group: 'email_settings',
|
||||
label: 'auth_pass',
|
||||
value: '',
|
||||
type: SettingType.text,
|
||||
weight: 7,
|
||||
},
|
||||
{
|
||||
group: 'contact',
|
||||
label: 'contact_email_recipient',
|
||||
|
||||
@@ -24,10 +24,10 @@
|
||||
</mj-section>
|
||||
<mj-section>
|
||||
<mj-column>
|
||||
<mj-text font-size="14px" color="#000" font-family="helvetica"
|
||||
><%= t('best_regards') %>,</mj-text
|
||||
<mj-text font-size="16px" color="#000" font-family="helvetica"
|
||||
><%= t('best_regards') %></mj-text
|
||||
>
|
||||
<mj-text font-size="14px" color="#000" font-family="helvetica"
|
||||
<mj-text font-size="16px" color="#000" font-family="helvetica"
|
||||
><%= this.appName %></mj-text
|
||||
>
|
||||
</mj-column>
|
||||
|
||||
@@ -26,11 +26,11 @@
|
||||
</mj-section>
|
||||
<mj-section>
|
||||
<mj-column>
|
||||
<mj-text font-size="14px" color="#000" font-family="helvetica"
|
||||
<mj-text font-size="16px" color="#000" font-family="helvetica"
|
||||
><%= t('best_regards') %></mj-text
|
||||
>
|
||||
|
||||
<mj-text font-size="14px" color="#000" font-family="helvetica"
|
||||
<mj-text font-size="16px" color="#000" font-family="helvetica"
|
||||
><%= this.appName %></mj-text
|
||||
>
|
||||
</mj-column>
|
||||
|
||||
@@ -23,10 +23,10 @@
|
||||
</mj-section>
|
||||
<mj-section>
|
||||
<mj-column>
|
||||
<mj-text font-size="14px" color="#000" font-family="helvetica"
|
||||
><%= t('best_regards') %>,</mj-text
|
||||
<mj-text font-size="16px" color="#000" font-family="helvetica"
|
||||
><%= t('best_regards') %></mj-text
|
||||
>
|
||||
<mj-text font-size="14px" color="#000" font-family="helvetica"
|
||||
<mj-text font-size="16px" color="#000" font-family="helvetica"
|
||||
><%= this.appName %></mj-text
|
||||
>
|
||||
</mj-column>
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
Inject,
|
||||
Injectable,
|
||||
InternalServerErrorException,
|
||||
Optional,
|
||||
} from '@nestjs/common';
|
||||
import { JwtService, JwtSignOptions } from '@nestjs/jwt';
|
||||
import { MailerService } from '@nestjs-modules/mailer';
|
||||
@@ -32,7 +33,7 @@ export class InvitationService extends BaseService<Invitation> {
|
||||
@Inject(InvitationRepository)
|
||||
readonly repository: InvitationRepository,
|
||||
@Inject(JwtService) private readonly jwtService: JwtService,
|
||||
private readonly mailerService: MailerService,
|
||||
@Optional() private readonly mailerService: MailerService | undefined,
|
||||
private logger: LoggerService,
|
||||
protected readonly i18n: ExtendedI18nService,
|
||||
) {
|
||||
@@ -54,24 +55,28 @@ export class InvitationService extends BaseService<Invitation> {
|
||||
*/
|
||||
async create(dto: InvitationCreateDto): Promise<Invitation> {
|
||||
const jwt = await this.sign(dto);
|
||||
try {
|
||||
await this.mailerService.sendMail({
|
||||
to: dto.email,
|
||||
template: 'invitation.mjml',
|
||||
context: {
|
||||
token: jwt,
|
||||
// TODO: Which language should we use?
|
||||
t: (key: string) => this.i18n.t(key),
|
||||
},
|
||||
});
|
||||
} catch (e) {
|
||||
this.logger.error(
|
||||
'Could not send email',
|
||||
e.message,
|
||||
e.stack,
|
||||
'InvitationService',
|
||||
);
|
||||
throw new InternalServerErrorException('Could not send email');
|
||||
if (this.mailerService) {
|
||||
try {
|
||||
await this.mailerService.sendMail({
|
||||
to: dto.email,
|
||||
template: 'invitation.mjml',
|
||||
context: {
|
||||
token: jwt,
|
||||
// TODO: Which language should we use?
|
||||
t: (key: string) =>
|
||||
this.i18n.t(key, { lang: config.chatbot.lang.default }),
|
||||
},
|
||||
subject: this.i18n.t('invitation_subject'),
|
||||
});
|
||||
} catch (e) {
|
||||
this.logger.error(
|
||||
'Could not send email',
|
||||
e.message,
|
||||
e.stack,
|
||||
'InvitationService',
|
||||
);
|
||||
throw new InternalServerErrorException('Could not send email');
|
||||
}
|
||||
}
|
||||
const newInvitation = await super.create({ ...dto, token: jwt });
|
||||
return { ...newInvitation, token: jwt };
|
||||
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
Injectable,
|
||||
InternalServerErrorException,
|
||||
NotFoundException,
|
||||
Optional,
|
||||
UnauthorizedException,
|
||||
} from '@nestjs/common';
|
||||
import { JwtService, JwtSignOptions } from '@nestjs/jwt';
|
||||
@@ -30,7 +31,7 @@ import { UserRequestResetDto, UserResetPasswordDto } from '../dto/user.dto';
|
||||
export class PasswordResetService {
|
||||
constructor(
|
||||
@Inject(JwtService) private readonly jwtService: JwtService,
|
||||
private readonly mailerService: MailerService,
|
||||
@Optional() private readonly mailerService: MailerService | undefined,
|
||||
private logger: LoggerService,
|
||||
private readonly userService: UserService,
|
||||
public readonly i18n: ExtendedI18nService,
|
||||
@@ -55,24 +56,29 @@ export class PasswordResetService {
|
||||
throw new NotFoundException('User not found');
|
||||
}
|
||||
const jwt = await this.sign(dto);
|
||||
try {
|
||||
await this.mailerService.sendMail({
|
||||
to: dto.email,
|
||||
template: 'password_reset.mjml',
|
||||
context: {
|
||||
token: jwt,
|
||||
first_name: user.first_name,
|
||||
t: (key: string) => this.i18n.t(key),
|
||||
},
|
||||
});
|
||||
} catch (e) {
|
||||
this.logger.error(
|
||||
'Could not send email',
|
||||
e.message,
|
||||
e.stack,
|
||||
'InvitationService',
|
||||
);
|
||||
throw new InternalServerErrorException('Could not send email');
|
||||
|
||||
if (this.mailerService) {
|
||||
try {
|
||||
await this.mailerService.sendMail({
|
||||
to: dto.email,
|
||||
template: 'password_reset.mjml',
|
||||
context: {
|
||||
token: jwt,
|
||||
first_name: user.first_name,
|
||||
t: (key: string) =>
|
||||
this.i18n.t(key, { lang: config.chatbot.lang.default }),
|
||||
},
|
||||
subject: this.i18n.t('password_reset_subject'),
|
||||
});
|
||||
} catch (e) {
|
||||
this.logger.error(
|
||||
'Could not send email',
|
||||
e.message,
|
||||
e.stack,
|
||||
'InvitationService',
|
||||
);
|
||||
throw new InternalServerErrorException('Could not send email');
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: hash the token before saving it
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
Inject,
|
||||
Injectable,
|
||||
InternalServerErrorException,
|
||||
Optional,
|
||||
UnauthorizedException,
|
||||
} from '@nestjs/common';
|
||||
import { JwtService, JwtSignOptions } from '@nestjs/jwt';
|
||||
@@ -33,7 +34,7 @@ export class ValidateAccountService {
|
||||
constructor(
|
||||
@Inject(JwtService) private readonly jwtService: JwtService,
|
||||
private readonly userService: UserService,
|
||||
private readonly mailerService: MailerService,
|
||||
@Optional() private readonly mailerService: MailerService | undefined,
|
||||
private readonly i18n: ExtendedI18nService,
|
||||
) {}
|
||||
|
||||
@@ -71,16 +72,19 @@ export class ValidateAccountService {
|
||||
) {
|
||||
const confirmationToken = await this.sign({ email: dto.email });
|
||||
|
||||
await this.mailerService.sendMail({
|
||||
to: dto.email,
|
||||
template: 'account_confirmation.mjml',
|
||||
context: {
|
||||
token: confirmationToken,
|
||||
first_name: dto.first_name,
|
||||
t: (key: string) => this.i18n.t(key),
|
||||
},
|
||||
subject: 'Account confirmation Email',
|
||||
});
|
||||
if (this.mailerService) {
|
||||
await this.mailerService.sendMail({
|
||||
to: dto.email,
|
||||
template: 'account_confirmation.mjml',
|
||||
context: {
|
||||
token: confirmationToken,
|
||||
first_name: dto.first_name,
|
||||
t: (key: string) =>
|
||||
this.i18n.t(key, { lang: config.chatbot.lang.default }),
|
||||
},
|
||||
subject: this.i18n.t('account_confirmation_subject'),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -30,13 +30,15 @@ MONGO_PASSWORD=dev_only
|
||||
MONGO_URI=mongodb://${MONGO_USER}:${MONGO_PASSWORD}@mongo:27017/
|
||||
MONGO_DB=hexabot
|
||||
|
||||
# SMTP Config for local dev env
|
||||
# SMTP Config (for local dev env, use smtp4dev by doing `make start SMTP4DEV=1`)
|
||||
APP_SMTP_4_DEV_PORT=9002
|
||||
EMAIL_SMTP_ENABLED=false
|
||||
EMAIL_SMTP_HOST=smtp4dev
|
||||
EMAIL_SMTP_PORT=25
|
||||
EMAIL_SMTP_SECURE=false
|
||||
EMAIL_SMTP_USER=dev_only
|
||||
EMAIL_SMTP_PASS=dev_only
|
||||
EMAIL_SMTP_FROM=noreply@example.com
|
||||
|
||||
# NLU Server
|
||||
AUTH_TOKEN=token123
|
||||
|
||||
@@ -2,12 +2,18 @@ version: "3.8"
|
||||
|
||||
services:
|
||||
database-init:
|
||||
build:
|
||||
context: ../api
|
||||
pull_policy: build
|
||||
volumes:
|
||||
- ../api/src:/app/src
|
||||
- ../api/migrations:/app/migrations
|
||||
# - ../api/node_modules:/app/node_modules
|
||||
|
||||
api:
|
||||
build:
|
||||
context: ../api
|
||||
pull_policy: build
|
||||
ports:
|
||||
- ${API_PORT}:3000
|
||||
- 9229:9229 # vscode debug port
|
||||
@@ -17,21 +23,6 @@ services:
|
||||
#- ../api/node_modules:/app/node_modules
|
||||
command: ["npm", "run", "start:debug"]
|
||||
|
||||
smtp4dev:
|
||||
image: rnwood/smtp4dev:v3
|
||||
restart: always
|
||||
ports:
|
||||
- ${APP_SMTP_4_DEV_PORT}:80
|
||||
- "25:25"
|
||||
- "143:143"
|
||||
volumes:
|
||||
- smtp4dev-data:/smtp4dev
|
||||
environment:
|
||||
- ServerOptions__HostName=smtp4dev
|
||||
- ServerOptions__LockSettings=true
|
||||
networks:
|
||||
- db-network
|
||||
|
||||
mongo-express:
|
||||
container_name: mongoUi
|
||||
image: mongo-express:1-20
|
||||
@@ -45,13 +36,19 @@ services:
|
||||
ME_CONFIG_MONGODB_ADMINPASSWORD: ${MONGO_PASSWORD}
|
||||
ME_CONFIG_MONGODB_URL: ${MONGO_URI}
|
||||
|
||||
hexabot-frontend:
|
||||
container_name: frontend
|
||||
build:
|
||||
context: ../
|
||||
dockerfile: ./frontend/Dockerfile
|
||||
pull_policy: build
|
||||
|
||||
widget:
|
||||
build:
|
||||
context: ../widget
|
||||
target: development
|
||||
pull_policy: build
|
||||
volumes:
|
||||
- ../widget/src:/app/src
|
||||
ports:
|
||||
- ${APP_WIDGET_PORT}:5173
|
||||
|
||||
volumes:
|
||||
smtp4dev-data:
|
||||
|
||||
@@ -2,5 +2,8 @@ version: "3.9"
|
||||
|
||||
services:
|
||||
nlu-api:
|
||||
build:
|
||||
context: ../nlu
|
||||
dockerfile: Dockerfile
|
||||
ports:
|
||||
- ${NLP_PORT}:5000
|
||||
- ${NLP_PORT}:5000
|
||||
|
||||
@@ -10,9 +10,7 @@ services:
|
||||
|
||||
nlu-api:
|
||||
container_name: nlu-api
|
||||
build:
|
||||
context: ../nlu
|
||||
dockerfile: Dockerfile
|
||||
image: hexastack/hexabot-nlu:latest
|
||||
env_file: .env
|
||||
networks:
|
||||
- nlp-network
|
||||
|
||||
20
docker/docker-compose.smtp4dev.yml
Normal file
20
docker/docker-compose.smtp4dev.yml
Normal file
@@ -0,0 +1,20 @@
|
||||
version: "3.8"
|
||||
|
||||
services:
|
||||
smtp4dev:
|
||||
image: rnwood/smtp4dev:v3
|
||||
restart: always
|
||||
ports:
|
||||
- ${APP_SMTP_4_DEV_PORT}:80
|
||||
- "25:25"
|
||||
- "143:143"
|
||||
volumes:
|
||||
- smtp4dev-data:/smtp4dev
|
||||
environment:
|
||||
- ServerOptions__HostName=smtp4dev
|
||||
- ServerOptions__LockSettings=true
|
||||
networks:
|
||||
- app-network
|
||||
|
||||
volumes:
|
||||
smtp4dev-data:
|
||||
@@ -3,8 +3,7 @@ version: "3.9"
|
||||
services:
|
||||
database-init:
|
||||
container_name: database-init
|
||||
build:
|
||||
context: ../api
|
||||
image: hexastack/hexabot-api:latest
|
||||
command: sh -c "npm run cache:init && npm run migrate prune && npm run migrate up"
|
||||
env_file: .env
|
||||
networks:
|
||||
@@ -15,8 +14,7 @@ services:
|
||||
|
||||
api:
|
||||
container_name: api
|
||||
build:
|
||||
context: ../api
|
||||
image: hexastack/hexabot-api:latest
|
||||
env_file: .env
|
||||
ports:
|
||||
- ${API_PORT}:3000
|
||||
@@ -30,7 +28,6 @@ services:
|
||||
condition: service_healthy
|
||||
database-init:
|
||||
condition: service_completed_successfully
|
||||
|
||||
healthcheck:
|
||||
test: "wget --spider http://localhost:3000"
|
||||
interval: 10s
|
||||
@@ -40,12 +37,7 @@ services:
|
||||
|
||||
hexabot-frontend:
|
||||
container_name: frontend
|
||||
build:
|
||||
context: ../
|
||||
dockerfile: ./frontend/Dockerfile
|
||||
args:
|
||||
- NEXT_PUBLIC_API_ORIGIN=${NEXT_PUBLIC_API_ORIGIN}
|
||||
- NEXT_PUBLIC_SSO_ENABLED=${NEXT_PUBLIC_SSO_ENABLED}
|
||||
image: hexastack/hexabot-ui:latest
|
||||
env_file: .env
|
||||
ports:
|
||||
- ${APP_FRONTEND_PORT}:8080
|
||||
@@ -75,12 +67,7 @@ services:
|
||||
|
||||
widget:
|
||||
container_name: widget
|
||||
build:
|
||||
context: ../widget
|
||||
args:
|
||||
REACT_APP_WIDGET_API_URL: ${REACT_APP_WIDGET_API_URL}
|
||||
REACT_APP_WIDGET_CHANNEL: ${REACT_APP_WIDGET_CHANNEL}
|
||||
REACT_APP_WIDGET_TOKEN: ${REACT_APP_WIDGET_TOKEN}
|
||||
image: hexastack/hexabot-widget:latest
|
||||
networks:
|
||||
- app-network
|
||||
depends_on:
|
||||
|
||||
@@ -21,14 +21,6 @@ RUN \
|
||||
|
||||
# Rebuild the source code only when needed
|
||||
FROM base AS builder
|
||||
ARG NEXT_PUBLIC_API_ORIGIN
|
||||
ENV NEXT_PUBLIC_API_ORIGIN=${NEXT_PUBLIC_API_ORIGIN}
|
||||
ARG NEXT_PUBLIC_SSO_ENABLED
|
||||
ENV NEXT_PUBLIC_SSO_ENABLED=${NEXT_PUBLIC_SSO_ENABLED}
|
||||
|
||||
ENV REACT_APP_WIDGET_API_URL=${NEXT_PUBLIC_API_ORIGIN}
|
||||
ENV REACT_APP_WIDGET_CHANNEL=test
|
||||
ENV REACT_APP_WIDGET_TOKEN=test
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
@@ -57,10 +49,6 @@ ENV NODE_ENV production
|
||||
# Uncomment the following line in case you want to disable telemetry during runtime.
|
||||
ENV NEXT_TELEMETRY_DISABLED 1
|
||||
|
||||
# Set the environment variable API_ORIGIN
|
||||
ENV NEXT_PUBLIC_API_ORIGIN ${NEXT_PUBLIC_API_ORIGIN:-"http://localhost:3000"}
|
||||
ENV NEXT_PUBLIC_SSO_ENABLED ${NEXT_PUBLIC_SSO_ENABLED:-"false"}
|
||||
|
||||
RUN addgroup --system --gid 1001 nodejs
|
||||
RUN adduser --system --uid 1001 nextjs
|
||||
|
||||
|
||||
@@ -1,36 +1,24 @@
|
||||
/** @type {import('next').NextConfig} */
|
||||
import withTM from "next-transpile-modules";
|
||||
|
||||
const apiUrl = process.env.NEXT_PUBLIC_API_ORIGIN || "http://localhost:4000/";
|
||||
const url = new URL(apiUrl);
|
||||
const nextConfig = withTM(["hexabot-widget"])({
|
||||
async rewrites() {
|
||||
return [
|
||||
{
|
||||
source: "/config",
|
||||
destination: "/api/config",
|
||||
},
|
||||
];
|
||||
},
|
||||
webpack(config, _options) {
|
||||
return config;
|
||||
},
|
||||
publicRuntimeConfig: {
|
||||
apiUrl,
|
||||
ssoEnabled: process.env.NEXT_PUBLIC_SSO_ENABLED === "true",
|
||||
lang: {
|
||||
default: "en",
|
||||
},
|
||||
},
|
||||
output: "standalone",
|
||||
images: {
|
||||
remotePatterns: [
|
||||
{
|
||||
protocol: "https",
|
||||
hostname: url.hostname,
|
||||
port: url.port,
|
||||
pathname: "/attachment/**",
|
||||
},
|
||||
{
|
||||
protocol: "http",
|
||||
hostname: url.hostname,
|
||||
port: url.port,
|
||||
pathname: "/attachment/**",
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
export default nextConfig;
|
||||
|
||||
@@ -11,12 +11,15 @@ import { Grid } from "@mui/material";
|
||||
import { GridRenderCellParams } from "@mui/x-data-grid";
|
||||
|
||||
import { getAvatarSrc } from "@/components/inbox/helpers/mapMessages";
|
||||
import { useConfig } from "@/hooks/useConfig";
|
||||
import { EntityType } from "@/services/types";
|
||||
|
||||
export const buildRenderPicture = (
|
||||
entityType: EntityType.USER | EntityType.SUBSCRIBER,
|
||||
) =>
|
||||
function RenderPicture(params: GridRenderCellParams) {
|
||||
const { apiUrl } = useConfig();
|
||||
|
||||
return (
|
||||
<Grid
|
||||
container
|
||||
@@ -28,6 +31,7 @@ export const buildRenderPicture = (
|
||||
>
|
||||
<img
|
||||
src={getAvatarSrc(
|
||||
apiUrl,
|
||||
entityType,
|
||||
entityType === EntityType.USER
|
||||
? params.row.id
|
||||
|
||||
@@ -21,9 +21,9 @@ import { useTranslation } from "react-i18next";
|
||||
|
||||
import { useCreate } from "@/hooks/crud/useCreate";
|
||||
import { useAuth } from "@/hooks/useAuth";
|
||||
import { useConfig } from "@/hooks/useConfig";
|
||||
import { EntityType } from "@/services/types";
|
||||
|
||||
|
||||
import { ChatActions } from "./ChatActions";
|
||||
import { ChatHeader } from "./ChatHeader";
|
||||
import {
|
||||
@@ -35,6 +35,7 @@ import { useChat } from "../hooks/ChatContext";
|
||||
import { useInfinitedLiveMessages } from "../hooks/useInfiniteLiveMessages";
|
||||
|
||||
export function Chat() {
|
||||
const { apiUrl } = useConfig();
|
||||
const { t, i18n } = useTranslation();
|
||||
const { subscriber } = useChat();
|
||||
const { user } = useAuth();
|
||||
@@ -69,7 +70,11 @@ export function Chat() {
|
||||
<ConversationHeader>
|
||||
<Avatar
|
||||
name={subscriber?.first_name}
|
||||
src={getAvatarSrc(EntityType.SUBSCRIBER, subscriber.foreign_id)}
|
||||
src={getAvatarSrc(
|
||||
apiUrl,
|
||||
EntityType.SUBSCRIBER,
|
||||
subscriber.foreign_id,
|
||||
)}
|
||||
/>
|
||||
<ConversationHeader.Content>
|
||||
<ChatHeader />
|
||||
@@ -118,6 +123,7 @@ export function Chat() {
|
||||
i18n.language,
|
||||
)}`}
|
||||
src={getAvatarSrc(
|
||||
apiUrl,
|
||||
message.sender
|
||||
? EntityType.SUBSCRIBER
|
||||
: EntityType.USER,
|
||||
|
||||
@@ -18,12 +18,14 @@ import { Input } from "@/app-components/inputs/Input";
|
||||
import { useFind } from "@/hooks/crud/useFind";
|
||||
import { useUpdate } from "@/hooks/crud/useUpdate";
|
||||
import { useAuth } from "@/hooks/useAuth";
|
||||
import { useConfig } from "@/hooks/useConfig";
|
||||
import { EntityType } from "@/services/types";
|
||||
|
||||
import { getAvatarSrc } from "../helpers/mapMessages";
|
||||
import { useChat } from "../hooks/ChatContext";
|
||||
|
||||
export const ChatActions = () => {
|
||||
const { apiUrl } = useConfig();
|
||||
const { t } = useTranslation();
|
||||
const { subscriber: activeChat } = useChat();
|
||||
const [takeoverBy, setTakeoverBy] = useState<string>(
|
||||
@@ -57,7 +59,7 @@ export const ChatActions = () => {
|
||||
<Avatar
|
||||
size="sm"
|
||||
name={user.first_name}
|
||||
src={getAvatarSrc(EntityType.USER, user.id)}
|
||||
src={getAvatarSrc(apiUrl, EntityType.USER, user.id)}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid>
|
||||
|
||||
@@ -16,6 +16,7 @@ import InboxIcon from "@mui/icons-material/MoveToInbox";
|
||||
import { Chip, debounce, Grid } from "@mui/material";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import { useConfig } from "@/hooks/useConfig";
|
||||
import { Title } from "@/layout/content/Title";
|
||||
import { EntityType } from "@/services/types";
|
||||
|
||||
@@ -29,6 +30,7 @@ export const SubscribersList = (props: {
|
||||
searchPayload: any;
|
||||
assignedTo: AssignedTo;
|
||||
}) => {
|
||||
const { apiUrl } = useConfig();
|
||||
const { t, i18n } = useTranslation();
|
||||
const chat = useChat();
|
||||
const { fetchNextPage, isFetching, subscribers, hasNextPage } =
|
||||
@@ -58,6 +60,7 @@ export const SubscribersList = (props: {
|
||||
>
|
||||
<Avatar
|
||||
src={getAvatarSrc(
|
||||
apiUrl,
|
||||
EntityType.SUBSCRIBER,
|
||||
conversation.foreign_id,
|
||||
)}
|
||||
|
||||
@@ -11,18 +11,16 @@ import { Message, MessageModel } from "@chatscope/chat-ui-kit-react";
|
||||
import MenuRoundedIcon from "@mui/icons-material/MenuRounded";
|
||||
import ReplyIcon from "@mui/icons-material/Reply";
|
||||
import { Chip, Grid } from "@mui/material";
|
||||
import getConfig from "next/config";
|
||||
import { ReactNode } from "react";
|
||||
|
||||
import { ROUTES } from "@/services/api.class";
|
||||
import { EntityType } from "@/services/types";
|
||||
import { IMessage, IMessageFull } from "@/types/message.types";
|
||||
import { buildURL } from "@/utils/URL";
|
||||
|
||||
import { AttachmentViewer } from "../components/AttachmentViewer";
|
||||
import { Carousel } from "../components/Carousel";
|
||||
|
||||
const { publicRuntimeConfig } = getConfig();
|
||||
|
||||
function hasSameSender(
|
||||
m1: IMessage | IMessageFull,
|
||||
m2: IMessage | IMessageFull,
|
||||
@@ -133,13 +131,11 @@ export function getMessageContent(
|
||||
* @description Returns the avatar of the subscriber
|
||||
*/
|
||||
export function getAvatarSrc(
|
||||
apiUrl: string,
|
||||
entity: EntityType.USER | EntityType.SUBSCRIBER,
|
||||
id?: string,
|
||||
) {
|
||||
//remove trailing slash
|
||||
return `${String(publicRuntimeConfig.apiUrl).replace(/\/$/, "")}${
|
||||
ROUTES[entity]
|
||||
}/${id || "bot"}/profile_pic`;
|
||||
return buildURL(apiUrl, `${ROUTES[entity]}/${id || "bot"}/profile_pic`);
|
||||
}
|
||||
|
||||
export function getMessagePosition(
|
||||
@@ -186,4 +182,4 @@ export function getMessagePosition(
|
||||
|
||||
// Default case (should not reach here)
|
||||
return "single";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,7 +21,6 @@ import {
|
||||
MenuItem,
|
||||
} from "@mui/material";
|
||||
import { GridColDef } from "@mui/x-data-grid";
|
||||
import getConfig from "next/config";
|
||||
import { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
@@ -38,6 +37,7 @@ import { DataGrid } from "@/app-components/tables/DataGrid";
|
||||
import { useDelete } from "@/hooks/crud/useDelete";
|
||||
import { useFind } from "@/hooks/crud/useFind";
|
||||
import { useGetFromCache } from "@/hooks/crud/useGet";
|
||||
import { useConfig } from "@/hooks/useConfig";
|
||||
import { getDisplayDialogs, useDialog } from "@/hooks/useDialog";
|
||||
import { useHasPermission } from "@/hooks/useHasPermission";
|
||||
import { useSearch } from "@/hooks/useSearch";
|
||||
@@ -56,7 +56,6 @@ import { buildURL } from "@/utils/URL";
|
||||
import { NlpImportDialog } from "../NlpImportDialog";
|
||||
import { NlpSampleDialog } from "../NlpSampleDialog";
|
||||
|
||||
const { publicRuntimeConfig } = getConfig();
|
||||
const NLP_SAMPLE_TYPE_COLORS = {
|
||||
test: "#e6a23c",
|
||||
train: "#67c23a",
|
||||
@@ -64,6 +63,7 @@ const NLP_SAMPLE_TYPE_COLORS = {
|
||||
};
|
||||
|
||||
export default function NlpSample() {
|
||||
const { apiUrl } = useConfig();
|
||||
const { toast } = useToast();
|
||||
const { t } = useTranslation();
|
||||
const [dataset, setDataSet] = useState("");
|
||||
@@ -287,7 +287,7 @@ export default function NlpSample() {
|
||||
<Button
|
||||
variant="contained"
|
||||
href={buildURL(
|
||||
publicRuntimeConfig.apiUrl,
|
||||
apiUrl,
|
||||
`nlpsample/export${dataset ? `?type=${dataset}` : ""}`,
|
||||
)}
|
||||
startIcon={<DownloadIcon />}
|
||||
|
||||
@@ -11,7 +11,6 @@ import { faUsers } from "@fortawesome/free-solid-svg-icons";
|
||||
import PersonAddAlt1Icon from "@mui/icons-material/PersonAddAlt1";
|
||||
import { Button, Grid, Paper } from "@mui/material";
|
||||
import { GridColDef } from "@mui/x-data-grid";
|
||||
import getConfig from "next/config";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import { ChipEntity } from "@/app-components/displays/ChipEntity";
|
||||
@@ -25,6 +24,7 @@ import { buildRenderPicture } from "@/app-components/tables/columns/renderPictur
|
||||
import { DataGrid } from "@/app-components/tables/DataGrid";
|
||||
import { useFind } from "@/hooks/crud/useFind";
|
||||
import { useUpdate } from "@/hooks/crud/useUpdate";
|
||||
import { useConfig } from "@/hooks/useConfig";
|
||||
import { getDisplayDialogs, useDialog } from "@/hooks/useDialog";
|
||||
import { useHasPermission } from "@/hooks/useHasPermission";
|
||||
import { useSearch } from "@/hooks/useSearch";
|
||||
@@ -39,9 +39,8 @@ import { getDateTimeFormatter } from "@/utils/date";
|
||||
import { EditUserDialog } from "./EditUserDialog";
|
||||
import { InvitationDialog } from "./InvitationDialog";
|
||||
|
||||
const { publicRuntimeConfig } = getConfig();
|
||||
|
||||
export const Users = () => {
|
||||
const { ssoEnabled } = useConfig();
|
||||
const { t } = useTranslation();
|
||||
const { toast } = useToast();
|
||||
const { mutateAsync: updateUser } = useUpdate(EntityType.USER, {
|
||||
@@ -157,7 +156,7 @@ export const Users = () => {
|
||||
},
|
||||
});
|
||||
}}
|
||||
disabled={publicRuntimeConfig.ssoEnabled}
|
||||
disabled={ssoEnabled}
|
||||
>
|
||||
{t(params.row.state ? "label.enabled" : "label.disabled")}
|
||||
</Button>
|
||||
@@ -188,7 +187,7 @@ export const Users = () => {
|
||||
valueGetter: (params) =>
|
||||
t("datetime.updated_at", getDateTimeFormatter(params)),
|
||||
},
|
||||
...(!publicRuntimeConfig.ssoEnabled ? [actionColumns] : []),
|
||||
...(!ssoEnabled ? [actionColumns] : []),
|
||||
];
|
||||
|
||||
return (
|
||||
@@ -207,7 +206,7 @@ export const Users = () => {
|
||||
<Grid item>
|
||||
<FilterTextfield onChange={onSearch} />
|
||||
</Grid>
|
||||
{!publicRuntimeConfig.ssoEnabled &&
|
||||
{!ssoEnabled &&
|
||||
hasPermission(EntityType.USER, PermissionAction.CREATE) ? (
|
||||
<Grid item>
|
||||
<Button
|
||||
|
||||
@@ -273,7 +273,13 @@ const Diagrams = () => {
|
||||
zoomUpdated: debouncedZoomEvent,
|
||||
offsetUpdated: debouncedOffsetEvent,
|
||||
});
|
||||
}, [JSON.stringify(blocks)]);
|
||||
}, [
|
||||
JSON.stringify(
|
||||
blocks.map((b) => {
|
||||
return { ...b, position: undefined, updatedAt: undefined };
|
||||
}),
|
||||
),
|
||||
]);
|
||||
|
||||
const handleDeleteButton = () => {
|
||||
const selectedEntities = engine?.getModel().getSelectedEntities();
|
||||
|
||||
@@ -8,7 +8,6 @@
|
||||
*/
|
||||
|
||||
import axios from "axios";
|
||||
import getConfig from "next/config";
|
||||
import { stringify } from "qs";
|
||||
import React, { createContext, ReactNode, useContext, useMemo } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
@@ -18,17 +17,17 @@ import { EntityType } from "@/services/types";
|
||||
import { IBaseSchema } from "@/types/base.types";
|
||||
|
||||
import { useLogoutRedirection } from "./useAuth";
|
||||
import { useConfig } from "./useConfig";
|
||||
import { useToast } from "./useToast";
|
||||
|
||||
const { publicRuntimeConfig } = getConfig();
|
||||
|
||||
export const useAxiosInstance = () => {
|
||||
const { apiUrl } = useConfig();
|
||||
const { logoutRedirection } = useLogoutRedirection();
|
||||
const { toast } = useToast();
|
||||
const { t } = useTranslation();
|
||||
const axiosInstance = useMemo(() => {
|
||||
const instance = axios.create({
|
||||
baseURL: publicRuntimeConfig.apiUrl,
|
||||
baseURL: apiUrl,
|
||||
withCredentials: true,
|
||||
});
|
||||
// Added the same Query String (de)Serializer as NestJS,
|
||||
|
||||
47
frontend/src/hooks/useConfig.tsx
Normal file
47
frontend/src/hooks/useConfig.tsx
Normal file
@@ -0,0 +1,47 @@
|
||||
import { createContext, useContext, useEffect, useState } from "react";
|
||||
|
||||
const ConfigContext = createContext<IConfig | null>(null);
|
||||
|
||||
export interface IConfig {
|
||||
apiUrl: string;
|
||||
ssoEnabled: boolean;
|
||||
}
|
||||
|
||||
export const ConfigProvider = ({ children }) => {
|
||||
const [config, setConfig] = useState(null);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchConfig = async () => {
|
||||
try {
|
||||
const res = await fetch("/config");
|
||||
const data = await res.json();
|
||||
|
||||
setConfig(data);
|
||||
} catch (error) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error("Failed to fetch configuration:", error);
|
||||
}
|
||||
};
|
||||
|
||||
fetchConfig();
|
||||
}, []);
|
||||
|
||||
if (!config) {
|
||||
// You can return a loader here if you want
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<ConfigContext.Provider value={config}>{children}</ConfigContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export const useConfig = () => {
|
||||
const context = useContext(ConfigContext);
|
||||
|
||||
if (!context) {
|
||||
throw new Error("useConfig must be used within a ConfigProvider");
|
||||
}
|
||||
|
||||
return context;
|
||||
};
|
||||
@@ -40,7 +40,7 @@
|
||||
"edit_account_email": "A valid e-mail address. All e-mails from the system will be sent to this address. The e-mail address is not made public and will only be used if you wish to receive a new password or wish to receive certain news or notifications by e-mail.",
|
||||
"new_password": "To change the current password, enter the new password in both fields.",
|
||||
"account_update_success": "Account has been updated successfully",
|
||||
"account_disabled": "Your account has been disabled!",
|
||||
"account_disabled": "Your account has been either disabled or is pending confirmation.",
|
||||
"success_invitation_sent": "Invitation to join has been successfully sent.",
|
||||
"item_delete_confirm": "Are you sure you want to delete this item?",
|
||||
"item_delete_success": "Item has been deleted successfully",
|
||||
@@ -213,7 +213,6 @@
|
||||
"offline": "Web Channel",
|
||||
"twitter": "Twitter",
|
||||
"dimelo": "Dimelo",
|
||||
"email_settings": "Email",
|
||||
"contact": "Contact Infos",
|
||||
"chatbot_settings": "Chatbot",
|
||||
"nlp_settings": "NLP Provider",
|
||||
|
||||
@@ -40,7 +40,7 @@
|
||||
"edit_account_email": "Une adresse e-mail valide Tous les e-mails du système seront envoyés à cette adresse. L'adresse e-mail n'est pas rendue publique et ne sera utilisée que si vous souhaitez recevoir un nouveau mot de passe ou si vous souhaitez recevoir certaines nouvelles ou notifications par e-mail.",
|
||||
"new_password": "Pour changer le mot de passe actuel, entrez le nouveau mot de passe dans les deux champs.",
|
||||
"account_update_success": "Le compte a été mis à jour avec succès",
|
||||
"account_disabled": "Votre compte a été désactivé!",
|
||||
"account_disabled": "Votre compte a été désactivé ou est en attente de confirmation.",
|
||||
"success_invitation_sent": "L'invitation a été envoyée avec succès.",
|
||||
"item_delete_confirm": "Êtes-vous sûr de bien vouloir supprimer cet élément?",
|
||||
"item_delete_success": "L'élément a été supprimé avec succès",
|
||||
|
||||
@@ -18,7 +18,6 @@ import {
|
||||
styled,
|
||||
} from "@mui/material";
|
||||
import MuiAppBar, { AppBarProps as MuiAppBarProps } from "@mui/material/AppBar";
|
||||
import getConfig from "next/config";
|
||||
import { FC, useEffect, useRef, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
@@ -26,12 +25,11 @@ import { HexabotLogo } from "@/app-components/logos/HexabotLogo";
|
||||
import { PopoverMenu } from "@/app-components/menus/PopoverMenu";
|
||||
import { getAvatarSrc } from "@/components/inbox/helpers/mapMessages";
|
||||
import { useAuth } from "@/hooks/useAuth";
|
||||
import { useConfig } from "@/hooks/useConfig";
|
||||
import { EntityType } from "@/services/types";
|
||||
|
||||
import { borderLine, theme } from "./themes/theme";
|
||||
|
||||
const { publicRuntimeConfig } = getConfig();
|
||||
|
||||
interface AppBarProps extends MuiAppBarProps {
|
||||
open?: boolean;
|
||||
}
|
||||
@@ -75,6 +73,7 @@ export type HeaderProps = {
|
||||
onToggleSidebar: () => void;
|
||||
};
|
||||
export const Header: FC<HeaderProps> = ({ isSideBarOpen, onToggleSidebar }) => {
|
||||
const { apiUrl, ssoEnabled } = useConfig();
|
||||
const { t } = useTranslation();
|
||||
const anchorRef = useRef(null);
|
||||
const [isMenuPopoverOpen, setIsMenuPopoverOpen] = useState(false);
|
||||
@@ -160,7 +159,7 @@ export const Header: FC<HeaderProps> = ({ isSideBarOpen, onToggleSidebar }) => {
|
||||
</Typography>
|
||||
</Box>
|
||||
<Avatar
|
||||
src={getAvatarSrc(EntityType.USER, user?.id).concat(
|
||||
src={getAvatarSrc(apiUrl, EntityType.USER, user?.id).concat(
|
||||
`?${randomSeed}`,
|
||||
)}
|
||||
color={theme.palette.text.secondary}
|
||||
@@ -176,7 +175,7 @@ export const Header: FC<HeaderProps> = ({ isSideBarOpen, onToggleSidebar }) => {
|
||||
last_name: user.last_name,
|
||||
}}
|
||||
links={
|
||||
!publicRuntimeConfig.ssoEnabled
|
||||
!ssoEnabled
|
||||
? [
|
||||
{ text: t("menu.home"), href: "/" },
|
||||
{ text: t("menu.edit_account"), href: "/profile" },
|
||||
|
||||
@@ -31,7 +31,6 @@ import SettingsAccessibilityRoundedIcon from "@mui/icons-material/SettingsAccess
|
||||
import { CSSObject, Grid, IconButton, styled, Theme } from "@mui/material";
|
||||
import MuiDrawer from "@mui/material/Drawer";
|
||||
import { OverridableComponent } from "@mui/material/OverridableComponent";
|
||||
import getConfig from "next/config";
|
||||
import { useRouter } from "next/router";
|
||||
import { FC, useMemo } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
@@ -39,12 +38,12 @@ import { useTranslation } from "react-i18next";
|
||||
import { HexabotLogo } from "@/app-components/logos/HexabotLogo";
|
||||
import { Sidebar } from "@/app-components/menus/Sidebar";
|
||||
import { useAuth } from "@/hooks/useAuth";
|
||||
import { useConfig } from "@/hooks/useConfig";
|
||||
import { useHasPermission } from "@/hooks/useHasPermission";
|
||||
import { EntityType } from "@/services/types";
|
||||
import { PermissionAction } from "@/types/permission.types";
|
||||
import { getLayout } from "@/utils/laylout";
|
||||
|
||||
const { publicRuntimeConfig } = getConfig();
|
||||
const drawerWidth = 280;
|
||||
const openedMixin = (theme: Theme): CSSObject => ({
|
||||
width: drawerWidth,
|
||||
@@ -107,7 +106,7 @@ export type MenuItem = {
|
||||
submenuItems?: MenuItem[];
|
||||
};
|
||||
|
||||
const MENU_ITEMS: MenuItem[] = [
|
||||
const getMenuItems = (ssoEnabled: boolean): MenuItem[] => [
|
||||
{
|
||||
text: "menu.dashboard",
|
||||
href: "/",
|
||||
@@ -236,7 +235,7 @@ const MENU_ITEMS: MenuItem[] = [
|
||||
[EntityType.USER]: [PermissionAction.READ],
|
||||
},
|
||||
},
|
||||
...(!publicRuntimeConfig.ssoEnabled
|
||||
...(!ssoEnabled
|
||||
? [
|
||||
{
|
||||
text: "menu.roles",
|
||||
@@ -271,13 +270,15 @@ export const VerticalMenu: FC<VerticalMenuProps> = ({
|
||||
onToggleIn,
|
||||
onToggleOut,
|
||||
}) => {
|
||||
const { ssoEnabled } = useConfig();
|
||||
const { t } = useTranslation();
|
||||
const { isAuthenticated } = useAuth();
|
||||
const router = useRouter();
|
||||
const hasPermission = useHasPermission();
|
||||
const menuItems = getMenuItems(ssoEnabled);
|
||||
// Filter menu item to which user is allowed access
|
||||
const availableMenuItems = useMemo(() => {
|
||||
return MENU_ITEMS.filter(({ requires: requiredPermissions }) => {
|
||||
return menuItems.filter(({ requires: requiredPermissions }) => {
|
||||
return (
|
||||
!requiredPermissions ||
|
||||
Object.entries(requiredPermissions).every((permission) => {
|
||||
|
||||
@@ -20,6 +20,7 @@ import { ReactQueryDevtools } from "react-query/devtools";
|
||||
import { SnackbarCloseButton } from "@/app-components/displays/Toast/CloseButton";
|
||||
import { ApiClientProvider } from "@/hooks/useApiClient";
|
||||
import { AuthProvider } from "@/hooks/useAuth";
|
||||
import { ConfigProvider } from "@/hooks/useConfig";
|
||||
import { PermissionProvider } from "@/hooks/useHasPermission";
|
||||
import { SettingsProvider } from "@/hooks/useSetting";
|
||||
import { ToastProvider } from "@/hooks/useToast";
|
||||
@@ -69,33 +70,35 @@ const App = ({ Component, pageProps }: TAppPropsWithLayout) => {
|
||||
/>
|
||||
</Head>
|
||||
<main className={roboto.className}>
|
||||
<ThemeProvider theme={theme}>
|
||||
<ToastProvider
|
||||
maxSnack={3}
|
||||
anchorOrigin={{ vertical: "top", horizontal: "center" }}
|
||||
action={(snackbarKey) => (
|
||||
<SnackbarCloseButton snackbarKey={snackbarKey} />
|
||||
)}
|
||||
>
|
||||
<StyledEngineProvider injectFirst>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<CssBaseline />
|
||||
<ApiClientProvider>
|
||||
<AuthProvider>
|
||||
<PermissionProvider>
|
||||
<SettingsProvider>
|
||||
<SocketProvider>
|
||||
{getLayout(<Component {...pageProps} />)}
|
||||
</SocketProvider>
|
||||
</SettingsProvider>
|
||||
</PermissionProvider>
|
||||
</AuthProvider>
|
||||
</ApiClientProvider>
|
||||
<ReactQueryDevtools initialIsOpen={false} />
|
||||
</QueryClientProvider>
|
||||
</StyledEngineProvider>
|
||||
</ToastProvider>
|
||||
</ThemeProvider>
|
||||
<ConfigProvider>
|
||||
<ThemeProvider theme={theme}>
|
||||
<ToastProvider
|
||||
maxSnack={3}
|
||||
anchorOrigin={{ vertical: "top", horizontal: "center" }}
|
||||
action={(snackbarKey) => (
|
||||
<SnackbarCloseButton snackbarKey={snackbarKey} />
|
||||
)}
|
||||
>
|
||||
<StyledEngineProvider injectFirst>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<CssBaseline />
|
||||
<ApiClientProvider>
|
||||
<AuthProvider>
|
||||
<PermissionProvider>
|
||||
<SettingsProvider>
|
||||
<SocketProvider>
|
||||
{getLayout(<Component {...pageProps} />)}
|
||||
</SocketProvider>
|
||||
</SettingsProvider>
|
||||
</PermissionProvider>
|
||||
</AuthProvider>
|
||||
</ApiClientProvider>
|
||||
<ReactQueryDevtools initialIsOpen={false} />
|
||||
</QueryClientProvider>
|
||||
</StyledEngineProvider>
|
||||
</ToastProvider>
|
||||
</ThemeProvider>
|
||||
</ConfigProvider>
|
||||
</main>
|
||||
</>
|
||||
);
|
||||
|
||||
16
frontend/src/pages/api/config.ts
Normal file
16
frontend/src/pages/api/config.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import type { NextApiRequest, NextApiResponse } from "next";
|
||||
|
||||
type ResponseData = {
|
||||
apiUrl: string;
|
||||
ssoEnabled: boolean;
|
||||
};
|
||||
|
||||
export default function handler(
|
||||
req: NextApiRequest,
|
||||
res: NextApiResponse<ResponseData>,
|
||||
) {
|
||||
res.status(200).json({
|
||||
apiUrl: process.env.NEXT_PUBLIC_API_ORIGIN || "http://localhost:4000",
|
||||
ssoEnabled: process.env.NEXT_PUBLIC_SSO_ENABLED === "true" || false,
|
||||
});
|
||||
}
|
||||
@@ -10,17 +10,16 @@
|
||||
import ChatIcon from "@mui/icons-material/Chat";
|
||||
import { Avatar, Box, Typography } from "@mui/material";
|
||||
import UiChatWidget from "hexabot-widget/src/UiChatWidget";
|
||||
import getConfig from "next/config";
|
||||
import { ReactElement } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import { getAvatarSrc } from "@/components/inbox/helpers/mapMessages";
|
||||
import { VisualEditor } from "@/components/visual-editor";
|
||||
import { useConfig } from "@/hooks/useConfig";
|
||||
import i18n from "@/i18n/config";
|
||||
import { Layout } from "@/layout";
|
||||
import { EntityType } from "@/services/types";
|
||||
|
||||
const { publicRuntimeConfig } = getConfig();
|
||||
const CustomWidgetHeader = () => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
@@ -34,12 +33,14 @@ const CustomWidgetHeader = () => {
|
||||
);
|
||||
};
|
||||
const VisualEditorPage = () => {
|
||||
const { apiUrl } = useConfig();
|
||||
|
||||
return (
|
||||
<>
|
||||
<VisualEditor />
|
||||
<UiChatWidget
|
||||
config={{
|
||||
apiUrl: publicRuntimeConfig.apiUrl,
|
||||
apiUrl,
|
||||
channel: "live-chat-tester",
|
||||
token: "test",
|
||||
language: i18n.language,
|
||||
@@ -48,7 +49,7 @@ const VisualEditorPage = () => {
|
||||
CustomAvatar={() => (
|
||||
<Avatar
|
||||
sx={{ width: "32px", height: "32px", fontSize: ".75rem" }}
|
||||
src={getAvatarSrc(EntityType.USER, "bot")}
|
||||
src={getAvatarSrc(apiUrl, EntityType.USER, "bot")}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
|
||||
@@ -7,13 +7,10 @@
|
||||
* 3. SaaS Restriction: This software, or any derivative of it, may not be used to offer a competing product or service (SaaS) without prior written consent from Hexastack. Offering the software as a service or using it in a commercial cloud environment without express permission is strictly prohibited.
|
||||
*/
|
||||
|
||||
import getConfig from "next/config";
|
||||
import { io, Socket, ManagerOptions, SocketOptions } from "socket.io-client";
|
||||
|
||||
import { IOIncomingMessage, IOOutgoingMessage } from "./types/io-message";
|
||||
|
||||
const { publicRuntimeConfig } = getConfig();
|
||||
|
||||
type SocketIoClientConfig = Partial<ManagerOptions & SocketOptions>;
|
||||
|
||||
export class SocketIoClient {
|
||||
@@ -52,13 +49,13 @@ export class SocketIoClient {
|
||||
|
||||
private initialized: boolean = false;
|
||||
|
||||
constructor(socketConfig?: SocketIoClientConfig) {
|
||||
constructor(apiUrl: string, socketConfig?: SocketIoClientConfig) {
|
||||
this.config = {
|
||||
...SocketIoClient.defaultConfig,
|
||||
...socketConfig,
|
||||
autoConnect: false,
|
||||
};
|
||||
const url = new URL(publicRuntimeConfig.apiUrl);
|
||||
const url = new URL(apiUrl);
|
||||
|
||||
this.socket = io(url.origin, this.config);
|
||||
}
|
||||
@@ -160,5 +157,3 @@ export class SocketIoClient {
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export const socketIoClient = new SocketIoClient();
|
||||
|
||||
@@ -12,22 +12,24 @@ import {
|
||||
PropsWithChildren,
|
||||
useContext,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useState,
|
||||
} from "react";
|
||||
import { QueryOptions, useQuery } from "react-query";
|
||||
|
||||
import { useAuth } from "@/hooks/useAuth";
|
||||
import { useConfig } from "@/hooks/useConfig";
|
||||
import { useToast } from "@/hooks/useToast";
|
||||
|
||||
import { SocketIoClient, socketIoClient } from "./SocketIoClient";
|
||||
import { SocketIoClient } from "./SocketIoClient";
|
||||
|
||||
interface socketContext {
|
||||
socket: SocketIoClient;
|
||||
socket: SocketIoClient | null;
|
||||
connected: boolean;
|
||||
}
|
||||
|
||||
const socketContext = createContext<socketContext>({
|
||||
socket: socketIoClient,
|
||||
socket: null,
|
||||
connected: false,
|
||||
});
|
||||
|
||||
@@ -36,13 +38,14 @@ export const useSocket = () => {
|
||||
};
|
||||
|
||||
export const SocketProvider = (props: PropsWithChildren) => {
|
||||
const { socket } = useSocket();
|
||||
const { apiUrl } = useConfig();
|
||||
const [connected, setConnected] = useState(false);
|
||||
const { toast } = useToast();
|
||||
const { user } = useAuth();
|
||||
const socket = useMemo(() => new SocketIoClient(apiUrl), [apiUrl]);
|
||||
|
||||
useEffect(() => {
|
||||
if (user)
|
||||
if (user && apiUrl)
|
||||
socket.init({
|
||||
onConnect: () => {
|
||||
setConnected(true);
|
||||
@@ -54,7 +57,7 @@ export const SocketProvider = (props: PropsWithChildren) => {
|
||||
setConnected(false);
|
||||
},
|
||||
});
|
||||
}, [socket, toast, user]);
|
||||
}, [socket, toast, user, apiUrl]);
|
||||
|
||||
return (
|
||||
<socketContext.Provider value={{ socket, connected }}>
|
||||
@@ -73,9 +76,9 @@ export const useSubscribe = <T,>(event: string, callback: (arg: T) => void) => {
|
||||
const { socket } = useSocket();
|
||||
|
||||
useEffect(() => {
|
||||
socket.on<T>(event, callback);
|
||||
socket?.on<T>(event, callback);
|
||||
|
||||
return () => socket.off(event, callback);
|
||||
return () => socket?.off(event, callback);
|
||||
}, [event, callback, socket]);
|
||||
};
|
||||
|
||||
@@ -83,14 +86,17 @@ export const useSocketGetQuery = <T,>(
|
||||
url: string,
|
||||
options?: Omit<QueryOptions<T, Error, T, string[]>, "queryFn">,
|
||||
) => {
|
||||
const { socket } = useSocket();
|
||||
const query = useQuery({
|
||||
...options,
|
||||
queryKey: ["socket", "get", url],
|
||||
queryFn: async () => {
|
||||
const response = await socketIoClient.get<T>(url);
|
||||
if (!socket) throw new Error("Socket not initialized");
|
||||
const response = await socket.get<T>(url);
|
||||
|
||||
return response.body;
|
||||
},
|
||||
enabled: !!socket,
|
||||
});
|
||||
|
||||
return query;
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
# Hexabot Live Chat Widget
|
||||
The [Hexabot](https://hexabot.ai/) Live Chat Widget is a React-based embeddable widget that allows users to integrate real-time chat functionality into their websites. It connects to the Hexabot API and facilitates seamless interaction between end-users and chatbots across multiple channels.
|
||||
|
||||
|
||||
## Key Features
|
||||
- **Real-Time Chat:** Engage in real-time conversations with users directly through your website.
|
||||
- **Customizable:** Easily customize the widget's appearance and behavior to fit your brand and website.
|
||||
@@ -9,7 +8,6 @@ The [Hexabot](https://hexabot.ai/) Live Chat Widget is a React-based embeddable
|
||||
- **Embeddable:** Simple to embed and integrate into any web page with just a few lines of code.
|
||||
|
||||
## Directory Structure
|
||||
|
||||
The Hexabot Live Chat Widget is organized into the following directory structure, under `src` we have:
|
||||
|
||||
- **src/assets:** Static assets like icons, fonts, and images used in the widget.
|
||||
@@ -26,7 +24,6 @@ The Hexabot Live Chat Widget is organized into the following directory structure
|
||||
|
||||
- **/public:** Contains static files that are publicly accessible. This includes the main HTML template where the widget is embedded for local development.
|
||||
|
||||
|
||||
## Run the Live Chat Widget
|
||||
|
||||
### Dev Mode
|
||||
|
||||
Reference in New Issue
Block a user