fix: widget re-render issues, ws, build + rename pkg

This commit is contained in:
Mohamed Marrouchi 2024-10-12 19:43:34 +01:00
parent b08ee24fc3
commit 04ce4df348
19 changed files with 152 additions and 129 deletions

View File

@ -63,6 +63,10 @@ export class BlockService extends BaseService<Block, BlockPopulate, BlockFull> {
blocks: BlockFull[], blocks: BlockFull[],
event: EventWrapper<any, any>, event: EventWrapper<any, any>,
): Promise<BlockFull | undefined> { ): Promise<BlockFull | undefined> {
if (!blocks.length) {
return undefined;
}
// Search for block matching a given event // Search for block matching a given event
let block: BlockFull | undefined = undefined; let block: BlockFull | undefined = undefined;
const payload = event.getPayload(); const payload = event.getPayload();

View File

@ -443,7 +443,7 @@ export class BotService {
}); });
if (!blocks.length) { if (!blocks.length) {
return this.logger.debug('No starting message blocks was found'); this.logger.debug('No starting message blocks was found');
} }
// Search for a block match // Search for a block match

View File

@ -67,7 +67,7 @@ export const settingModels: SettingCreateDto[] = [
{ {
group: 'nlp_settings', group: 'nlp_settings',
label: 'threshold', label: 'threshold',
value: 0.9, value: 0.1,
type: SettingType.number, type: SettingType.number,
config: { config: {
min: 0, min: 0,

View File

@ -68,11 +68,8 @@ describe('WebsocketGateway', () => {
it('should connect successfully', async () => { it('should connect successfully', async () => {
ioClient.connect(); ioClient.connect();
await new Promise<void>((resolve) => { await new Promise<void>((resolve) => {
// ioClient.on('connect', () => { ioClient.on('connect', () => {
// console.log('connected'); expect(true).toBe(true);
// });
ioClient.on('message', (data) => {
expect(data.statusCode).toBe(200);
resolve(); resolve();
}); });
}); });

View File

@ -255,22 +255,6 @@ export class WebsocketGateway
this.logger.debug(`Number of connected clients: ${sockets?.size}`); this.logger.debug(`Number of connected clients: ${sockets?.size}`);
this.eventEmitter.emit(`hook:websocket:connection`, client); this.eventEmitter.emit(`hook:websocket:connection`, client);
// @TODO : Revisit once we don't use anymore in frontend
const response = new SocketResponse();
client.send(
response
.setHeaders({
'access-control-allow-origin':
config.security.cors.allowOrigins.join(','),
vary: 'Origin',
'access-control-allow-credentials':
config.security.cors.allowCredentials.toString(),
})
.status(200)
.json({
success: true,
}),
);
} }
async handleDisconnect(client: Socket): Promise<void> { async handleDisconnect(client: Socket): Promise<void> {

View File

@ -1,7 +1,7 @@
/** @type {import('next').NextConfig} */ /** @type {import('next').NextConfig} */
import withTM from "next-transpile-modules"; import withTM from "next-transpile-modules";
const nextConfig = withTM(["hexabot-widget"])({ const nextConfig = withTM(["hexabot-chat-widget"])({
async rewrites() { async rewrites() {
return [ return [
{ {
@ -11,7 +11,7 @@ const nextConfig = withTM(["hexabot-widget"])({
]; ];
}, },
webpack(config) { webpack(config) {
if (process.env.NODE_ENV==="development") { if (process.env.NODE_ENV === "development") {
config.watchOptions = { config.watchOptions = {
poll: 1000, poll: 1000,
aggregateTimeout: 300, aggregateTimeout: 300,

View File

@ -29,7 +29,7 @@
"axios": "^1.7.7", "axios": "^1.7.7",
"eazychart-css": "^0.2.1-alpha.0", "eazychart-css": "^0.2.1-alpha.0",
"eazychart-react": "^0.8.0-alpha.0", "eazychart-react": "^0.8.0-alpha.0",
"hexabot-widget": "*", "hexabot-chat-widget": "*",
"next": "^14.2.13", "next": "^14.2.13",
"next-transpile-modules": "^10.0.1", "next-transpile-modules": "^10.0.1",
"normalizr": "^3.6.2", "normalizr": "^3.6.2",

View File

@ -7,7 +7,7 @@
*/ */
import { Avatar, Box } from "@mui/material"; import { Avatar, Box } from "@mui/material";
import UiChatWidget from "hexabot-widget/src/UiChatWidget"; import UiChatWidget from "hexabot-chat-widget/src/UiChatWidget";
import { usePathname } from "next/navigation"; import { usePathname } from "next/navigation";
import { getAvatarSrc } from "@/components/inbox/helpers/mapMessages"; import { getAvatarSrc } from "@/components/inbox/helpers/mapMessages";

12
package-lock.json generated
View File

@ -63,7 +63,7 @@
"axios": "^1.7.7", "axios": "^1.7.7",
"eazychart-css": "^0.2.1-alpha.0", "eazychart-css": "^0.2.1-alpha.0",
"eazychart-react": "^0.8.0-alpha.0", "eazychart-react": "^0.8.0-alpha.0",
"hexabot-widget": "*", "hexabot-chat-widget": "*",
"next": "^14.2.13", "next": "^14.2.13",
"next-transpile-modules": "^10.0.1", "next-transpile-modules": "^10.0.1",
"normalizr": "^3.6.2", "normalizr": "^3.6.2",
@ -6356,6 +6356,10 @@
"resolved": "https://registry.npmjs.org/heap/-/heap-0.2.5.tgz", "resolved": "https://registry.npmjs.org/heap/-/heap-0.2.5.tgz",
"integrity": "sha512-G7HLD+WKcrOyJP5VQwYZNC3Z6FcQ7YYjEFiFoIj8PfEr73mu421o8B1N5DKUcc8K37EsJ2XXWA8DtrDz/2dReg==" "integrity": "sha512-G7HLD+WKcrOyJP5VQwYZNC3Z6FcQ7YYjEFiFoIj8PfEr73mu421o8B1N5DKUcc8K37EsJ2XXWA8DtrDz/2dReg=="
}, },
"node_modules/hexabot-chat-widget": {
"resolved": "widget",
"link": true
},
"node_modules/hexabot-cli": { "node_modules/hexabot-cli": {
"resolved": "cli", "resolved": "cli",
"link": true "link": true
@ -6364,10 +6368,6 @@
"resolved": "frontend", "resolved": "frontend",
"link": true "link": true
}, },
"node_modules/hexabot-widget": {
"resolved": "widget",
"link": true
},
"node_modules/hoist-non-react-statics": { "node_modules/hoist-non-react-statics": {
"version": "3.3.2", "version": "3.3.2",
"resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz", "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz",
@ -9918,7 +9918,7 @@
} }
}, },
"widget": { "widget": {
"name": "hexabot-widget", "name": "hexabot-chat-widget",
"version": "2.0.0", "version": "2.0.0",
"license": "AGPL-3.0-only", "license": "AGPL-3.0-only",
"dependencies": { "dependencies": {

View File

@ -104,6 +104,17 @@ To prevent the website css from conflicting with the chat widget css, we can lev
</script> </script>
``` ```
If you would like to use the official widget and benefit from updates automatically, you can consider using the cdn url:
`https://cdn.jsdelivr.net/npm/hexabot-chat-widget@2.0.4/dist/`
or lastest from major version:
`https://cdn.jsdelivr.net/npm/hexabot-chat-widget@2/dist/`
JsDelivr uses the package published in the NPM registry : https://www.npmjs.com/package/hexabot-widget
## Examples
As a proof of concept we developed a Wordpress plugin to embed the chat widget in a Wordpress website : [https://github.com/hexastack/hexabot-wordpress-live-chat-widget](https://github.com/hexastack/hexabot-wordpress-live-chat-widget)
## Customization ## Customization
You can customize the look and feel of the chat widget by modifying the widgets scss styles or behavior. The widget allows you to: You can customize the look and feel of the chat widget by modifying the widgets scss styles or behavior. The widget allows you to:

View File

@ -1,6 +1,6 @@
{ {
"name": "hexabot-live-chat-widget", "name": "hexabot-chat-widget",
"version": "2.0.0-rc.2", "version": "2.0.0",
"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.", "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", "author": "Hexastack",
"license": "AGPL-3.0-only", "license": "AGPL-3.0-only",

View File

@ -15,7 +15,6 @@ import { useChat } from '../providers/ChatProvider';
import { useColors } from '../providers/ColorProvider'; import { useColors } from '../providers/ColorProvider';
import { useSocketLifecycle } from '../providers/SocketProvider'; import { useSocketLifecycle } from '../providers/SocketProvider';
import { useWidget, WidgetContextType } from '../providers/WidgetProvider'; import { useWidget, WidgetContextType } from '../providers/WidgetProvider';
import './Launcher.scss'; import './Launcher.scss';
type LauncherProps = PropsWithChildren<{ type LauncherProps = PropsWithChildren<{

View File

@ -6,7 +6,13 @@
* 2. All derivative works must include clear attribution to the original creator and software, Hexastack and Hexabot, in a prominent location (e.g., in the software's "About" section, documentation, and README file). * 2. All derivative works must include clear attribution to the original creator and software, Hexastack and Hexabot, in a prominent location (e.g., in the software's "About" section, documentation, and README file).
*/ */
import React, { SyntheticEvent, useCallback, useEffect, useState } from 'react'; import React, {
SyntheticEvent,
useCallback,
useEffect,
useRef,
useState,
} from 'react';
import { useTranslation } from '../hooks/useTranslation'; import { useTranslation } from '../hooks/useTranslation';
import { useChat } from '../providers/ChatProvider'; import { useChat } from '../providers/ChatProvider';
@ -39,6 +45,7 @@ const UserSubscription: React.FC = () => {
} = useChat(); } = useChat();
const [firstName, setFirstName] = useState<string>(''); const [firstName, setFirstName] = useState<string>('');
const [lastName, setLastName] = useState<string>(''); const [lastName, setLastName] = useState<string>('');
const isInitialized = useRef(false);
const handleSubmit = useCallback( const handleSubmit = useCallback(
async (event?: React.FormEvent<HTMLFormElement>) => { async (event?: React.FormEvent<HTMLFormElement>) => {
event?.preventDefault(); event?.preventDefault();
@ -83,7 +90,7 @@ const UserSubscription: React.FC = () => {
data: { data: {
type: TOutgoingMessageType.postback, type: TOutgoingMessageType.postback,
data: { data: {
text: 'GET_STARTED', //TODO:use translation here? text: t('messages.get_started'),
payload: 'GET_STARTED', payload: 'GET_STARTED',
}, },
author: profile.foreign_id, author: profile.foreign_id,
@ -113,14 +120,18 @@ const UserSubscription: React.FC = () => {
); );
useEffect(() => { useEffect(() => {
const profile = localStorage.getItem('profile'); // User already subscribed ? (example : refreshed the page)
if (!isInitialized.current) {
isInitialized.current = true;
const profile = localStorage.getItem('profile');
if (profile) { if (profile) {
const parsedProfile = JSON.parse(profile); const parsedProfile = JSON.parse(profile);
setFirstName(parsedProfile.first_name); setFirstName(parsedProfile.first_name);
setLastName(parsedProfile.last_name); setLastName(parsedProfile.last_name);
handleSubmit(); handleSubmit();
}
} }
}, [handleSubmit, setScreen]); }, [handleSubmit, setScreen]);

View File

@ -241,15 +241,14 @@ const ChatProvider: React.FC<{
) { ) {
if ('author' in newIOMessage) { if ('author' in newIOMessage) {
newIOMessage.direction = newIOMessage.direction =
newIOMessage.author === participants[1].foreign_id || newIOMessage.author === 'chatbot'
newIOMessage.author === participants[1].id ? Direction.received
? Direction.sent : Direction.sent;
: Direction.received;
newIOMessage.read = true; newIOMessage.read = true;
newIOMessage.delivery = true; newIOMessage.delivery = true;
} }
messages.push(newIOMessage as TMessage); setMessages([...messages, newIOMessage as TMessage]);
setScroll(0); setScroll(0);
} }
@ -310,30 +309,29 @@ const ChatProvider: React.FC<{
}>( }>(
`/webhook/${config.channel}/?verification_token=${config.token}&first_name=${firstName}&last_name=${lastName}`, `/webhook/${config.channel}/?verification_token=${config.token}&first_name=${firstName}&last_name=${lastName}`,
); );
const { messages, profile } = body;
localStorage.setItem('profile', JSON.stringify(profile)); localStorage.setItem('profile', JSON.stringify(body.profile));
// @TODO : condition mix on id VS foreign_id setMessages(
messages.forEach((message) => { body.messages.map((message) => {
const direction = return {
message.author === profile.foreign_id || ...message,
message.author === profile.id direction:
? Direction.sent message.author === body.profile.foreign_id ||
: Direction.received; message.author === body.profile.id
? Direction.sent
message.direction = direction; : Direction.received,
if (message.direction === Direction.sent) { read: message.direction === Direction.sent || message.read,
message.read = true; delivery:
message.delivery = false; message.direction === Direction.sent || message.delivery,
} } as TMessage;
}); }),
setMessages(messages); );
setParticipants([ setParticipants([
...participants, ...participants,
{ {
id: profile.foreign_id, id: body.profile.foreign_id,
foreign_id: profile.foreign_id, foreign_id: body.profile.foreign_id,
name: `${profile.first_name} ${profile.last_name}`, name: `${body.profile.first_name} ${body.profile.last_name}`,
}, },
]); ]);
setConnectionState(3); setConnectionState(3);

View File

@ -12,43 +12,42 @@ import {
useContext, useContext,
useEffect, useEffect,
useRef, useRef,
useState,
} from 'react'; } from 'react';
import { useConfig } from './ConfigProvider'; import { useConfig } from './ConfigProvider';
import { builSocketIoClient, SocketIoClient } from '../utils/SocketIoClient'; import { getSocketIoClient, SocketIoClient } from '../utils/SocketIoClient';
interface socketContext { interface socketContext {
socket: SocketIoClient; socket: SocketIoClient;
connected: boolean;
} }
const socketContext = createContext<socketContext>({ const socketContext = createContext<socketContext>({
socket: {} as SocketIoClient, socket: {} as SocketIoClient,
connected: false,
}); });
export const SocketProvider = (props: PropsWithChildren) => { export const SocketProvider = (props: PropsWithChildren) => {
const config = useConfig(); const config = useConfig();
const socketRef = useRef(builSocketIoClient(config)); const socketRef = useRef(
const [connected, setConnected] = useState(false); getSocketIoClient(config, {
useEffect(() => {
socketRef.current.init({
onConnect: () => { onConnect: () => {
setConnected(true); // eslint-disable-next-line no-console
console.info(
'Hexabot Live Chat : Successfully established WS Connection!',
);
}, },
onConnectError: () => { onConnectError: () => {
setConnected(false); // eslint-disable-next-line no-console
console.error('Hexabot Live Chat : Failed to establish WS Connection!');
}, },
onDisconnect: () => { onDisconnect: () => {
setConnected(false); // eslint-disable-next-line no-console
console.info('Hexabot Live Chat : Disconnected WS.');
}, },
}); }),
}, []); );
return ( return (
<socketContext.Provider value={{ socket: socketRef.current, connected }}> <socketContext.Provider value={{ socket: socketRef.current }}>
{props.children} {props.children}
</socketContext.Provider> </socketContext.Provider>
); );
@ -58,12 +57,6 @@ export const useSocket = () => {
return useContext(socketContext); return useContext(socketContext);
}; };
export const useSocketConnected = () => {
const { connected } = useSocket();
return connected;
};
export const useSubscribe = <T,>(event: string, callback: (arg: T) => void) => { export const useSubscribe = <T,>(event: string, callback: (arg: T) => void) => {
const { socket } = useSocket(); const { socket } = useSocket();
@ -83,5 +76,6 @@ export const useSocketLifecycle = () => {
return () => { return () => {
socket.disconnect(); socket.disconnect();
}; };
}, [socket]); // eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
}; };

View File

@ -11,6 +11,7 @@
"back": "Back" "back": "Back"
}, },
"messages": { "messages": {
"get_started": "Get Started",
"file_message": { "file_message": {
"browser_audio_unsupport": "Browser does not support the audio element.", "browser_audio_unsupport": "Browser does not support the audio element.",
"browser_video_unsupport": "Browser does not support the video element.", "browser_video_unsupport": "Browser does not support the video element.",

View File

@ -11,6 +11,7 @@
"back": "Retour" "back": "Retour"
}, },
"messages": { "messages": {
"get_started": "Démarrer",
"file_message": { "file_message": {
"browser_audio_unsupport": "Le navigateur ne prend pas en charge l'élément audio.", "browser_audio_unsupport": "Le navigateur ne prend pas en charge l'élément audio.",
"browser_video_unsupport": "Le navigateur ne prend pas en charge l'élément vidéo.", "browser_video_unsupport": "Le navigateur ne prend pas en charge l'élément vidéo.",

View File

@ -16,6 +16,12 @@ import {
type SocketIoClientConfig = Partial<ManagerOptions & SocketOptions>; type SocketIoClientConfig = Partial<ManagerOptions & SocketOptions>;
type SocketIoEventHandlers = {
onConnect?: () => void;
onDisconnect?: (reason: string, details: unknown) => void;
onConnectError?: (error: Error) => void;
};
export class SocketIoClient { export class SocketIoClient {
/** /**
* Default configuration for the socket client * Default configuration for the socket client
@ -50,9 +56,11 @@ export class SocketIoClient {
private config: SocketIoClientConfig; private config: SocketIoClientConfig;
private initialized: boolean = false; constructor(
apiUrl: string,
constructor(apiUrl: string, socketConfig?: SocketIoClientConfig) { socketConfig: SocketIoClientConfig,
handlers: SocketIoEventHandlers,
) {
this.config = { this.config = {
...SocketIoClient.defaultConfig, ...SocketIoClient.defaultConfig,
...socketConfig, ...socketConfig,
@ -61,6 +69,7 @@ export class SocketIoClient {
const url = new URL(apiUrl); const url = new URL(apiUrl);
this.socket = io(url.origin, this.config); this.socket = io(url.origin, this.config);
this.init(handlers);
} }
/** /**
@ -71,16 +80,10 @@ export class SocketIoClient {
onConnect, onConnect,
onDisconnect, onDisconnect,
onConnectError, onConnectError,
}: { }: SocketIoEventHandlers) {
onConnect?: () => void;
onDisconnect?: (reason: string, details: unknown) => void;
onConnectError?: (error: Error) => void;
}) {
if (!this.initialized) this.socket.connect();
onConnect && this.uniqueOn('connect', onConnect); onConnect && this.uniqueOn('connect', onConnect);
onDisconnect && this.uniqueOn('disconnect', onDisconnect); onDisconnect && this.uniqueOn('disconnect', onDisconnect);
onConnectError && this.uniqueOn('connect_error', onConnectError); onConnectError && this.uniqueOn('connect_error', onConnectError);
this.initialized = true;
} }
/** /**
@ -100,7 +103,6 @@ export class SocketIoClient {
*/ */
public disconnect() { public disconnect() {
this.socket.disconnect(); this.socket.disconnect();
this.initialized = false;
} }
/** /**
@ -184,10 +186,28 @@ export class SocketIoClient {
} }
} }
export const builSocketIoClient = (config: Config) => let socketIoClient: SocketIoClient;
new SocketIoClient(config.apiUrl, {
query: { /**
channel: config.channel, * Returns a singleton instance of the socket io client
verification_token: config.token, *
}, * @param config The socket connection config
}); * @param handlers Event handlers
* @returns Socket io client instance
*/
export const getSocketIoClient = (config: Config, handlers: SocketIoEventHandlers) => {
if (!socketIoClient) {
socketIoClient = new SocketIoClient(
config.apiUrl,
{
query: {
channel: config.channel,
verification_token: config.token,
},
},
handlers,
);
}
return socketIoClient;
};

View File

@ -5,28 +5,31 @@ import { defineConfig } from "vite";
import dts from "vite-plugin-dts"; import dts from "vite-plugin-dts";
export default defineConfig({ export default defineConfig(({ mode }) => {
plugins: [react(), dts()], return {
server: { plugins: [react(), dts()],
host: "0.0.0.0", server: {
}, host: '0.0.0.0',
define: {
"process.env": process.env,
},
build: {
lib: {
entry: resolve(__dirname, "src/ChatWidget.tsx"),
name: "HexabotWidget",
fileName: (format) => `hexabot-widget.${format}.js`,
}, },
rollupOptions: { define: {
external: ["react", "react-dom"], 'process.env':
output: { mode === 'development' ? { 'process.env': process.env } : {},
globals: { },
react: "React", build: {
"react-dom": "ReactDOM", lib: {
entry: resolve(__dirname, 'src/ChatWidget.tsx'),
name: 'HexabotWidget',
fileName: (format) => `hexabot-widget.${format}.js`,
},
rollupOptions: {
external: ['react', 'react-dom'],
output: {
globals: {
react: 'React',
'react-dom': 'ReactDOM',
},
}, },
}, },
}, },
}, };
}); });