feat: initial commit

This commit is contained in:
Mohamed Marrouchi
2024-09-10 10:50:11 +01:00
commit 30e5766487
879 changed files with 122820 additions and 0 deletions

11
widget/.dockerignore Normal file
View File

@@ -0,0 +1,11 @@
.git
.vscode
.dockerignore
.gitignore
.env
config
build
dist
node_modules
Dockerfile
README.md

55
widget/.eslintrc.json Normal file
View File

@@ -0,0 +1,55 @@
{
"plugins": ["@typescript-eslint/eslint-plugin", "import", "react"],
"extends": [
"eslint:recommended",
"plugin:@typescript-eslint/recommended",
"plugin:react-hooks/recommended"
],
"rules": {
"@typescript-eslint/no-unused-vars": [
"error",
{
"argsIgnorePattern": "^_",
"varsIgnorePattern": "^_",
"caughtErrorsIgnorePattern": "^_"
}
],
"import/newline-after-import": "error",
"import/order": [
"error",
{
"groups": [
"builtin", // Built-in imports (come from NodeJS native) go first
"external", // <- External imports
"unknown", // <- unknown
"index", // <- index imports
"internal", // <- Absolute imports
["sibling", "parent"] // <- Relative imports, the sibling and parent types they can be mingled together
],
"newlines-between": "always",
"alphabetize": {
/* sort in ascending order. Options: ["ignore", "asc", "desc"] */
"order": "asc",
/* ignore case. Options: [true, false] */
"caseInsensitive": true
}
}
],
"newline-after-var": "error",
"newline-before-return": "error",
"no-console": "error",
"no-duplicate-imports": "error",
"object-shorthand": "error",
"padding-line-between-statements": [
"error",
{ "blankLine": "never", "prev": ["const"], "next": "const" }
],
"react/jsx-curly-brace-presence": "warn",
"react/self-closing-comp": "error"
},
"settings": {
"react": {
"version": "detect"
}
}
}

24
widget/.gitignore vendored Normal file
View File

@@ -0,0 +1,24 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

38
widget/Dockerfile Normal file
View File

@@ -0,0 +1,38 @@
# Base stage: Base image for all stages
FROM node:18-alpine AS base
WORKDIR /app
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
COPY . .
RUN npm install
# Development stage: Installs development dependencies and serves the app
FROM installer AS development
ENV NODE_ENV=development
EXPOSE 5173
CMD ["npm", "run", "dev"]
# Builder stage: Builds the app
FROM installer AS builder
ENV NODE_ENV=production
RUN npm run build
# Production stage: Serves the app
FROM base AS production
ENV NODE_ENV=production
COPY --from=builder /app/dist /app/dist
RUN npm install -g serve
EXPOSE 5173
CMD ["serve", "-s", "dist", "-l", "5173"]

90
widget/README.md Normal file
View File

@@ -0,0 +1,90 @@
# 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.
- **Multi-Channel Support:** Integrates with multiple messaging platforms through the Hexabot API.
- **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.
- **src/components:** Reusable React components that make up the chat widget interface, such as message bubbles, input fields, and buttons.
- **src/constants:** Hard coded values that are used like colors.
- **src/hooks:** Custom React hooks for managing widget state and handling side effects like API calls or real-time events.
- **src/services:** Handles external services, such as communication with the Hexabot API or other third-party integrations.
- **src/styles:** Contains the styling for the widget, including CSS or SCSS files used to define the look and feel of the chat interface.
- **src/providers:** Context providers for managing global state, such as user session, chat messages, and widget configurations.
- **src/translations:** Contains transalation of a couple of strings.
- **src/types:** Defines the typescript interfaces, types, and enums used.
- **src/utils:** Utility functions and helpers used throughout the widget, such as formatting, validations, or data transformations.
- **/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
To run the widget in development mode, execute the following command at the project root level:
```bash
npm run dev:widget
```
The live chat widget will be accessible at http://localhost:5173.
### Build for Production
To build the widget for production, execute the following command at the widget folder level:
```bash
npm run build
```
This will generate a production-ready build in the dist folder.
## Embed Chat Widget
Once the widget is built, you can easily embed it into any webpage. Here's an example of how to add it to your website:
```js
<script crossorigin src="https://unpkg.com/react@18/umd/react.production.min.js"></script>
<script crossorigin src="https://unpkg.com/react-dom@18/umd/react-dom.production.min.js"></script>
<link rel="stylesheet" href="./style.css">
<script src="<<WIDGET URL>>/hexabot-widget.umd.js"></script>
<div id="hb-chat-widget"></div>
<script>
const el = React.createElement;
const domContainer = document.getElementById('hb-chat-widget');
ReactDOM.render(
el(HexabotWidget, {
apiUrl: 'https://api.yourdomain.com',
channel: 'offline',
token: 'token123',
}),
domContainer,
);
</script>
```
Replace the values in apiUrl and token with your configuration details.
## 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:
- Change colors and fonts to match your website's branding.
- Configure user settings like language and chatbot response preferences.
## 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.
Feel free to join us on [Discord](https://discord.gg/xnpWDYQMAq)
## 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.
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).
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.

13
widget/index.html Normal file
View File

@@ -0,0 +1,13 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vite + React + TS</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>

4305
widget/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

42
widget/package.json Normal file
View File

@@ -0,0 +1,42 @@
{
"name": "hexabot-widget",
"private": true,
"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.",
"author": "Hexastack",
"license": "AGPL-3.0-only",
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc && vite build",
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
"lint:fix": "eslint . --ext ts,tsx --fix",
"preview": "vite preview",
"serve": "npx http-server ./dist/",
"typecheck": "tsc --noEmit"
},
"dependencies": {
"@types/emoji-js": "^3.5.2",
"autolinker": "^4.0.0",
"dayjs": "^1.11.12",
"emoji-js": "^3.8.0",
"normalize.css": "^8.0.1",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"socket.io-client": "^4.7.5"
},
"devDependencies": {
"@types/react": "^18.3.3",
"@types/react-dom": "^18.3.0",
"@typescript-eslint/eslint-plugin": "^7.15.0",
"@typescript-eslint/parser": "^7.15.0",
"@vitejs/plugin-react": "^4.3.1",
"eslint": "^8.57.0",
"eslint-plugin-react-hooks": "^4.6.2",
"eslint-plugin-react-refresh": "^0.4.7",
"sass": "^1.77.8",
"typescript": "^5.2.2",
"vite": "^5.3.4",
"vite-plugin-dts": "^4.0.2"
}
}

BIN
widget/public/chatbot.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

33
widget/public/index.html Normal file
View File

@@ -0,0 +1,33 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Widget Embed</title>
<script
crossorigin
src="https://unpkg.com/react@18/umd/react.production.min.js"
></script>
<script
crossorigin
src="https://unpkg.com/react-dom@18/umd/react-dom.production.min.js"
></script>
<link rel="stylesheet" href="./style.css" />
<script src="./hexabot-widget.umd.js"></script>
</head>
<body>
<div id="hb-chat-widget"></div>
<script>
const el = React.createElement;
const domContainer = document.getElementById('hb-chat-widget');
ReactDOM.render(
el(HexabotWidget, {
apiUrl: 'http://localhost:4000',
channel: 'offline',
token: 'token123',
}),
domContainer,
);
</script>
</body>
</html>

1
widget/public/vite.svg Normal file
View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@@ -0,0 +1,3 @@
#root {
}

45
widget/src/ChatWidget.tsx Normal file
View File

@@ -0,0 +1,45 @@
/*
* 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).
* 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 Launcher from './components/Launcher';
import UserSubscription from './components/UserSubscription';
import ChatProvider from './providers/ChatProvider';
import { ColorProvider } from './providers/ColorProvider';
import { Config, ConfigProvider } from './providers/ConfigProvider';
import { CookieProvider } from './providers/CookieProvider';
import { SettingsProvider } from './providers/SettingsProvider';
import { SocketProvider } from './providers/SocketProvider';
import { TranslationProvider } from './providers/TranslationProvider';
import WidgetProvider from './providers/WidgetProvider';
import 'normalize.css';
import './ChatWidget.css';
function ChatWidget(props: Config) {
return (
<ConfigProvider {...props}>
<TranslationProvider>
<CookieProvider>
<SocketProvider>
<SettingsProvider>
<ColorProvider>
<WidgetProvider>
<ChatProvider>
<Launcher PreChat={UserSubscription} />
</ChatProvider>
</WidgetProvider>
</ColorProvider>
</SettingsProvider>
</SocketProvider>
</CookieProvider>
</TranslationProvider>
</ConfigProvider>
);
}
export default ChatWidget;

View File

@@ -0,0 +1,7 @@
.sc-launcher,
.sc-chat-window {
right: 25px !important;
bottom: 25px !important;
z-index: 999 !important;
box-shadow: 0 0 8px #0003 !important;
}

View File

@@ -0,0 +1,63 @@
/*
* 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).
* 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 { PropsWithChildren } from 'react';
import Launcher from './components/Launcher';
import UserSubscription from './components/UserSubscription';
import ChatProvider from './providers/ChatProvider';
import { ColorProvider } from './providers/ColorProvider';
import { Config, ConfigProvider } from './providers/ConfigProvider';
import { SettingsProvider } from './providers/SettingsProvider';
import { SocketProvider } from './providers/SocketProvider';
import { TranslationProvider } from './providers/TranslationProvider';
import WidgetProvider, { WidgetContextType } from './providers/WidgetProvider';
import './UiChatWidget.css';
import { ConnectionState } from './types/state.types';
type UiChatWidgetProps = PropsWithChildren<{
CustomLauncher?: (props: { widget: WidgetContextType }) => JSX.Element;
CustomHeader?: () => JSX.Element;
CustomAvatar?: () => JSX.Element;
PreChat?: React.FC;
PostChat?: React.FC;
config: Config;
}>;
function UiChatWidget({
CustomHeader,
CustomAvatar,
config,
}: UiChatWidgetProps) {
return (
<ConfigProvider {...config}>
<TranslationProvider>
<SocketProvider>
<SettingsProvider>
<ColorProvider>
<WidgetProvider defaultScreen="chat">
<ChatProvider
defaultConnectionState={ConnectionState.connected}
>
<Launcher
CustomHeader={CustomHeader}
CustomAvatar={CustomAvatar}
PreChat={UserSubscription}
/>
</ChatProvider>
</WidgetProvider>
</ColorProvider>
</SettingsProvider>
</SocketProvider>
</TranslationProvider>
</ConfigProvider>
);
}
export default UiChatWidget;

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="35.93" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 228"><path fill="#00D8FF" d="M210.483 73.824a171.49 171.49 0 0 0-8.24-2.597c.465-1.9.893-3.777 1.273-5.621c6.238-30.281 2.16-54.676-11.769-62.708c-13.355-7.7-35.196.329-57.254 19.526a171.23 171.23 0 0 0-6.375 5.848a155.866 155.866 0 0 0-4.241-3.917C100.759 3.829 77.587-4.822 63.673 3.233C50.33 10.957 46.379 33.89 51.995 62.588a170.974 170.974 0 0 0 1.892 8.48c-3.28.932-6.445 1.924-9.474 2.98C17.309 83.498 0 98.307 0 113.668c0 15.865 18.582 31.778 46.812 41.427a145.52 145.52 0 0 0 6.921 2.165a167.467 167.467 0 0 0-2.01 9.138c-5.354 28.2-1.173 50.591 12.134 58.266c13.744 7.926 36.812-.22 59.273-19.855a145.567 145.567 0 0 0 5.342-4.923a168.064 168.064 0 0 0 6.92 6.314c21.758 18.722 43.246 26.282 56.54 18.586c13.731-7.949 18.194-32.003 12.4-61.268a145.016 145.016 0 0 0-1.535-6.842c1.62-.48 3.21-.974 4.76-1.488c29.348-9.723 48.443-25.443 48.443-41.52c0-15.417-17.868-30.326-45.517-39.844Zm-6.365 70.984c-1.4.463-2.836.91-4.3 1.345c-3.24-10.257-7.612-21.163-12.963-32.432c5.106-11 9.31-21.767 12.459-31.957c2.619.758 5.16 1.557 7.61 2.4c23.69 8.156 38.14 20.213 38.14 29.504c0 9.896-15.606 22.743-40.946 31.14Zm-10.514 20.834c2.562 12.94 2.927 24.64 1.23 33.787c-1.524 8.219-4.59 13.698-8.382 15.893c-8.067 4.67-25.32-1.4-43.927-17.412a156.726 156.726 0 0 1-6.437-5.87c7.214-7.889 14.423-17.06 21.459-27.246c12.376-1.098 24.068-2.894 34.671-5.345a134.17 134.17 0 0 1 1.386 6.193ZM87.276 214.515c-7.882 2.783-14.16 2.863-17.955.675c-8.075-4.657-11.432-22.636-6.853-46.752a156.923 156.923 0 0 1 1.869-8.499c10.486 2.32 22.093 3.988 34.498 4.994c7.084 9.967 14.501 19.128 21.976 27.15a134.668 134.668 0 0 1-4.877 4.492c-9.933 8.682-19.886 14.842-28.658 17.94ZM50.35 144.747c-12.483-4.267-22.792-9.812-29.858-15.863c-6.35-5.437-9.555-10.836-9.555-15.216c0-9.322 13.897-21.212 37.076-29.293c2.813-.98 5.757-1.905 8.812-2.773c3.204 10.42 7.406 21.315 12.477 32.332c-5.137 11.18-9.399 22.249-12.634 32.792a134.718 134.718 0 0 1-6.318-1.979Zm12.378-84.26c-4.811-24.587-1.616-43.134 6.425-47.789c8.564-4.958 27.502 2.111 47.463 19.835a144.318 144.318 0 0 1 3.841 3.545c-7.438 7.987-14.787 17.08-21.808 26.988c-12.04 1.116-23.565 2.908-34.161 5.309a160.342 160.342 0 0 1-1.76-7.887Zm110.427 27.268a347.8 347.8 0 0 0-7.785-12.803c8.168 1.033 15.994 2.404 23.343 4.08c-2.206 7.072-4.956 14.465-8.193 22.045a381.151 381.151 0 0 0-7.365-13.322Zm-45.032-43.861c5.044 5.465 10.096 11.566 15.065 18.186a322.04 322.04 0 0 0-30.257-.006c4.974-6.559 10.069-12.652 15.192-18.18ZM82.802 87.83a323.167 323.167 0 0 0-7.227 13.238c-3.184-7.553-5.909-14.98-8.134-22.152c7.304-1.634 15.093-2.97 23.209-3.984a321.524 321.524 0 0 0-7.848 12.897Zm8.081 65.352c-8.385-.936-16.291-2.203-23.593-3.793c2.26-7.3 5.045-14.885 8.298-22.6a321.187 321.187 0 0 0 7.257 13.246c2.594 4.48 5.28 8.868 8.038 13.147Zm37.542 31.03c-5.184-5.592-10.354-11.779-15.403-18.433c4.902.192 9.899.29 14.978.29c5.218 0 10.376-.117 15.453-.343c-4.985 6.774-10.018 12.97-15.028 18.486Zm52.198-57.817c3.422 7.8 6.306 15.345 8.596 22.52c-7.422 1.694-15.436 3.058-23.88 4.071a382.417 382.417 0 0 0 7.859-13.026a347.403 347.403 0 0 0 7.425-13.565Zm-16.898 8.101a358.557 358.557 0 0 1-12.281 19.815a329.4 329.4 0 0 1-23.444.823c-7.967 0-15.716-.248-23.178-.732a310.202 310.202 0 0 1-12.513-19.846h.001a307.41 307.41 0 0 1-10.923-20.627a310.278 310.278 0 0 1 10.89-20.637l-.001.001a307.318 307.318 0 0 1 12.413-19.761c7.613-.576 15.42-.876 23.31-.876H128c7.926 0 15.743.303 23.354.883a329.357 329.357 0 0 1 12.335 19.695a358.489 358.489 0 0 1 11.036 20.54a329.472 329.472 0 0 1-11 20.722Zm22.56-122.124c8.572 4.944 11.906 24.881 6.52 51.026c-.344 1.668-.73 3.367-1.15 5.09c-10.622-2.452-22.155-4.275-34.23-5.408c-7.034-10.017-14.323-19.124-21.64-27.008a160.789 160.789 0 0 1 5.888-5.4c18.9-16.447 36.564-22.941 44.612-18.3ZM128 90.808c12.625 0 22.86 10.235 22.86 22.86s-10.235 22.86-22.86 22.86s-22.86-10.235-22.86-22.86s10.235-22.86 22.86-22.86Z"></path></svg>

After

Width:  |  Height:  |  Size: 4.0 KiB

View File

@@ -0,0 +1,65 @@
.sc-header {
min-height: 75px;
border-top-left-radius: 9px;
border-top-right-radius: 9px;
padding: 10px;
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.2);
position: relative;
box-sizing: border-box;
display: flex;
}
.sc-header--img {
border-radius: 50%;
align-self: center;
padding: 10px;
max-width: 32px;
max-height: 32px;
box-sizing: content-box;
}
.sc-header--title {
align-self: center;
padding: 10px;
flex: 1;
user-select: none;
font-size: 20px;
}
.sc-header--title.enabled {
cursor: pointer;
border-radius: 5px;
}
.sc-header--title.enabled:hover {
box-shadow: 0px 2px 5px rgba(0.2, 0.2, 0.5, 0.1);
}
.sc-header--close-button {
width: 36px;
height: 36px;
align-self: center;
margin-right: 10px;
box-sizing: border-box;
cursor: pointer;
border-radius: 100%;
margin-left: auto;
}
.sc-header--close-button:hover {
box-shadow: 0px 2px 5px rgba(0.2, 0.2, 0.5, 0.1);
}
.sc-header--close-button svg {
width: 100%;
height: 100%;
padding: 5px;
box-sizing: border-box;
fill: #FFF;
}
@media (max-width: 450px) {
.sc-header {
border-radius: 0px;
}
}

View File

@@ -0,0 +1,54 @@
/*
* 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).
* 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 { FC, PropsWithChildren } from 'react';
import CloseIcon from './icons/CloseIcon';
import { useColors } from '../providers/ColorProvider';
import { useSettings } from '../providers/SettingsProvider';
import { useWidget } from '../providers/WidgetProvider';
import './ChatHeader.scss';
type ChatHeaderProps = PropsWithChildren;
const ChatHeader: FC<ChatHeaderProps> = ({ children }) => {
const settings = useSettings();
const { colors } = useColors();
const widget = useWidget();
return (
<div
className="sc-header"
style={{ background: colors.header.bg, color: colors.header.text }}
>
{children ? (
children
) : (
<>
{settings.titleImageUrl && (
<img
className="sc-header--img"
src={settings.titleImageUrl}
alt=""
/>
)}
<div className="sc-header--title">{settings.title}</div>
</>
)}
<div
className="sc-header--close-button"
onClick={() => widget.setIsOpen(false)}
>
<CloseIcon />
</div>
</div>
);
};
export default ChatHeader;

View File

@@ -0,0 +1,62 @@
.sc-chat-window {
width: 370px;
height: calc(100% - 120px);
max-height: 590px;
position: fixed;
right: 25px;
bottom: 100px;
box-sizing: border-box;
box-shadow: 0px 7px 40px 2px rgba(148, 149, 150, 0.1);
background: white;
display: flex;
flex-direction: column;
justify-content: space-between;
border-radius: 10px;
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
animation: fadeIn;
animation-duration: 0.3s;
animation-timing-function: ease-in-out;
z-index: 10000;
}
.sc-chat-window.closed {
opacity: 0;
display: none;
bottom: 90px;
}
@keyframes fadeIn {
0% {
display: none;
opacity: 0;
}
100% {
display: flex;
opacity: 1;
}
}
.sc-message--me {
text-align: right;
}
.sc-message--them {
text-align: left;
}
@media (max-width: 450px) {
.sc-chat-window {
width: 100%;
height: 100%;
max-height: 100%;
right: 0px;
bottom: 0px;
border-radius: 0px;
}
.sc-chat-window {
transition: 0.1s ease-in-out;
}
.sc-chat-window.closed {
bottom: 0px;
}
}

View File

@@ -0,0 +1,54 @@
/*
* 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).
* 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 React, { PropsWithChildren } from 'react';
import ChatHeader from './ChatHeader';
import ConnectionLost from './ConnectionLost';
import Messages from './Messages';
import UserInput from './UserInput';
import Webview from './Webview';
import { useChat } from '../providers/ChatProvider';
import { useWidget } from '../providers/WidgetProvider';
import './ChatWindow.scss';
type ChatWindowProps = PropsWithChildren<{
CustomHeader?: () => JSX.Element;
CustomAvatar?: () => JSX.Element;
PreChat?: React.FC;
PostChat?: React.FC;
}>;
const ChatWindow: React.FC<ChatWindowProps> = ({
CustomHeader,
CustomAvatar,
PreChat,
PostChat,
}) => {
const { connectionState } = useChat();
const { screen, isOpen } = useWidget();
return (
<div className={`sc-chat-window ${isOpen ? 'opened' : 'closed'}`}>
<ChatHeader>{CustomHeader && <CustomHeader />}</ChatHeader>
{screen === 'prechat' && PreChat && <PreChat />}
{['prechat', 'postchat', 'webview'].indexOf(screen) === -1 &&
connectionState === 3 && <Messages Avatar={CustomAvatar} />}
{screen !== 'prechat' &&
screen !== 'postchat' &&
connectionState !== 3 && <ConnectionLost />}
{screen === 'postchat' && PostChat && <PostChat />}
{['prechat', 'postchat', 'webview'].indexOf(screen) === -1 &&
connectionState === 3 && <UserInput />}
{screen === 'webview' && <Webview />}
</div>
);
};
export default ChatWindow;

View File

@@ -0,0 +1,40 @@
.loading-image {
position: absolute;
top: 50%;
left: 50%;
transform: translateX(-50%) translateY(-50%);
}
.sc-chat--disconnected-icon-wrapper {
position: relative;
height: 100%;
width: 100%;
}
.sc-chat--disconnected-icon {
position: absolute;
top: 50%;
left: 50%;
transform: translateY(-50%) translateX(-50%);
text-align: center;
}
.sc-chat--disconnected-text {
width: 100%;
}
.sc-chat--disconnected-button {
border: 1px solid;
border-radius: 20px;
padding: 5px 10px;
width: 80%;
margin: 2px;
cursor: pointer;
outline: 0;
}
.sc-chat--disconnected-button:active {
content: '';
opacity: 0;
transition: all 0.5s;
}
.sc-chat--disconnected-button:active:after {
opacity: 1;
transition: 0s;
}

View File

@@ -0,0 +1,63 @@
/*
* 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).
* 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 React from 'react';
import ConnectionIcon from './icons/ConnectionIcon';
import LoadingIcon from './icons/LoadingIcon';
import { useTranslation } from '../hooks/useTranslation';
import { useChat } from '../providers/ChatProvider';
import { useColors } from '../providers/ColorProvider';
import './ConnectionLost.scss';
const ConnectionLost: React.FC = () => {
const { t } = useTranslation();
const { connectionState, setConnectionState } = useChat();
const { colors } = useColors();
const handleClick = () => {
if (connectionState === 0) {
setConnectionState(1);
}
};
const loading = connectionState > 0;
return (
<div
className="sc-chat--disconnected-icon-wrapper"
style={{ backgroundColor: colors.messageList.bg }}
>
{loading ? (
<LoadingIcon className="loading-image" />
) : (
<div className="sc-chat--disconnected-icon">
<ConnectionIcon />
<h3
className="sc-chat--disconnected-text"
style={{ color: colors.button.text }}
>
{t('settings.connection_lost')}
</h3>
<button
className="sc-chat--disconnected-button"
style={{
color: colors.button.text,
backgroundColor: colors.button.bg,
borderColor: colors.button.border,
}}
onClick={handleClick}
>
Refresh
</button>
</div>
)}
</div>
);
};
export default ConnectionLost;

View File

@@ -0,0 +1,62 @@
.sc-emoji-picker {
position: absolute;
bottom: 30px;
right: 0px;
min-width: 200px;
max-height: 215px;
box-shadow: 0px 7px 40px 2px rgba(148, 149, 150, 0.3);
background: white;
border-radius: 10px;
outline: none;
}
.sc-emoji-picker:after {
content: "";
width: 14px;
height: 14px;
background: white;
position: absolute;
bottom: -6px;
right: 55px;
transform: rotate(45deg);
border-radius: 2px;
}
.sc-emoji-picker--content {
padding: .5rem;
overflow: auto;
width: 100%;
max-height: 195px;
margin-top: 5px;
box-sizing: border-box;
}
.sc-emoji-picker--category {
display: flex;
flex-direction: row;
flex-wrap: wrap;
}
.sc-emoji-picker--category-title {
min-width: 100%;
color: #b8c3ca;
font-weight: 200;
font-size: 1rem;
margin: 5px;
letter-spacing: 1px;
}
.sc-emoji-picker--emoji {
margin: 5px;
width: 18px;
text-align: center;
cursor: pointer;
vertical-align: middle;
font-size: 1.5rem;
transition: transform 60ms ease-out,-webkit-transform 60ms ease-out;
}
.sc-emoji-picker--emoji:hover {
transform: scale(1.4);
}

View File

@@ -0,0 +1,76 @@
/*
* 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).
* 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 EmojiConvertor from 'emoji-js';
import React, { useEffect, useRef } from 'react';
import emojiData from '../constants/emojiData';
import './EmojiPicker.scss';
interface EmojiPickerProps {
onBlur: (event: React.FocusEvent<HTMLDivElement>) => void;
onSelect: (event: React.MouseEvent<HTMLSpanElement>, emoji: string) => void;
}
const EmojiPicker: React.FC<EmojiPickerProps> = ({ onBlur, onSelect }) => {
const emojiConvertorRef = useRef(new EmojiConvertor());
const domNode = useRef<HTMLDivElement>(null);
useEffect(() => {
const elem = domNode.current;
if (elem) {
elem.style.opacity = '0';
window.requestAnimationFrame(() => {
elem.style.transition = 'opacity 350ms';
elem.style.opacity = '1';
});
elem.focus();
// @ts-expect-error ts error
emojiConvertorRef.current.init_env();
}
}, []);
const emojiClicked = (
_event: React.MouseEvent<HTMLSpanElement>,
emoji: string,
) => {
onSelect(_event, emoji);
// setMessage(message + emoji);
if (domNode.current) {
domNode.current.blur();
}
};
return (
<div tabIndex={0} onBlur={onBlur} className="sc-emoji-picker" ref={domNode}>
<div className="sc-emoji-picker--content">
{emojiData.map((category) => (
<div className="sc-emoji-picker--category" key={category.name}>
<div className="sc-emoji-picker--category-title">
{category.name}
</div>
{category.emojis.map((emoji) => (
<span
key={emoji}
className="sc-emoji-picker--emoji"
onClick={(event) => emojiClicked(event, emoji)}
>
{emoji}
</span>
))}
</div>
))}
</div>
</div>
);
};
export default EmojiPicker;

View File

@@ -0,0 +1,68 @@
.sc-launcher {
width: 60px;
height: 60px;
background-position: center;
background-repeat: no-repeat;
position: fixed;
right: 20px;
bottom: 20px;
border-radius: 50%;
box-shadow: none;
transition: box-shadow 0.2s ease-in-out;
cursor: pointer;
&:before {
content: "";
position: relative;
display: block;
width: 60px;
height: 60px;
border-radius: 50%;
transition: box-shadow 0.2s ease-in-out;
}
.sc-open-icon {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 32px;
height: 32px;
fill: #fff;
}
.sc-closed-icon {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 32px;
height: 32px;
fill: #fff;
}
.sc-new-messages-count {
position: absolute;
top: -3px;
left: 41px;
display: flex;
justify-content: center;
flex-direction: column;
border-radius: 50%;
width: 22px;
height: 22px;
background: #ff4646;
color: white;
text-align: center;
margin: auto;
font-size: 12px;
font-weight: 500;
}
}
.sc-launcher.opened:before {
box-shadow: 0px 0px 400px 250px rgba(148, 149, 150, 0.2);
}
.sc-launcher:hover {
box-shadow: 0 0px 27px 1.5px rgba(0, 0, 0, 0.2);
}

View File

@@ -0,0 +1,89 @@
/*
* 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).
* 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 React, { PropsWithChildren } from 'react';
import ChatWindow from './ChatWindow';
import CloseIcon from './icons/CloseIcon';
import OpenIcon from './icons/OpenIcon';
import { useChat } from '../providers/ChatProvider';
import { useColors } from '../providers/ColorProvider';
import { useSocketLifecycle } from '../providers/SocketProvider';
import { useWidget, WidgetContextType } from '../providers/WidgetProvider';
import './Launcher.scss';
type LauncherProps = PropsWithChildren<{
CustomLauncher?: (props: { widget: WidgetContextType }) => JSX.Element;
CustomHeader?: () => JSX.Element;
CustomAvatar?: () => JSX.Element;
PreChat?: React.FC;
PostChat?: React.FC;
}>;
const Launcher: React.FC<LauncherProps> = ({
CustomLauncher,
CustomHeader,
CustomAvatar,
PreChat,
PostChat,
}) => {
const chat = useChat();
const widget = useWidget();
const { colors } = useColors();
const handleToggle = (e: React.MouseEvent) => {
e.preventDefault();
widget.setIsOpen(!widget.isOpen);
};
useSocketLifecycle();
return (
<div>
<div
className={`sc-launcher-wrapper ${widget.isOpen ? 'opened' : ''}`}
onClick={handleToggle}
>
{CustomLauncher ? (
<CustomLauncher widget={widget} />
) : (
<div
className="sc-launcher"
style={{ backgroundColor: colors.launcher.bg }}
>
{chat.newMessagesCount > 0 && !widget.isOpen && (
<div className="sc-new-messages-count">
{chat.newMessagesCount}
</div>
)}
{widget.isOpen ? (
<CloseIcon
className="sc-closed-icon"
width="16px"
height="16px"
/>
) : (
<OpenIcon className="sc-open-icon" />
)}
</div>
)}
</div>
{widget.isOpen && (
<ChatWindow
CustomHeader={CustomHeader}
CustomAvatar={CustomAvatar}
PreChat={PreChat}
PostChat={PostChat}
/>
)}
</div>
);
};
export default Launcher;

View File

@@ -0,0 +1,21 @@
.sc-menu-element {
border-top: 1px solid #b9b9b9;
position: relative;
font-size: 1rem;
cursor: pointer;
}
.sc-menu-item {
display: inline-block;
padding: 1rem;
text-decoration: none;
width: 100%;
}
.sc-menu-item-button {
display: inline-block;
position: absolute;
right: 0;
top: 50%;
transform: translateY(-50%);
font-size: 1.5rem;
padding: 1rem;
}

View File

@@ -0,0 +1,63 @@
/*
* 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).
* 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 React from 'react';
import { useColors } from '../providers/ColorProvider';
import { IMenuNode } from '../types/menu.type';
import { IPayload } from '../types/message.types';
import './MenuItem.scss';
interface MenuItemProps {
item: IMenuNode;
parent?: IMenuNode;
onOpenSubItems: (item: IMenuNode) => void;
onPostback: (data: IPayload) => void;
}
const MenuItem: React.FC<MenuItemProps> = ({
item,
parent,
onOpenSubItems,
onPostback,
}) => {
const { colors } = useColors();
const handleClick = () => {
switch (item.type) {
case 'web_url':
window.open(item.url, '_blank');
break;
case 'nested':
onOpenSubItems({ ...item, _parent: parent });
break;
case 'postback':
onPostback({ text: item.title, payload: item.payload });
break;
}
};
return (
<div className="sc-menu-element">
<a
className="sc-menu-item"
style={{ color: colors.header.text }}
role="button"
onClick={handleClick}
>
{item.title}
{item.type === 'nested' && (
<span className="sc-menu-item-button">&#10095;</span>
)}
</a>
</div>
);
};
export default MenuItem;

View File

@@ -0,0 +1,102 @@
.sc-message {
margin: auto;
padding: 0 0.5rem;
display: flex;
margin-bottom: 0.25rem;
&:first-child {
margin-top: 50%;
}
&.sent {
flex-direction: row-reverse;
.sc-message--content {
justify-content: flex-end;
.sc-message--avatar {
display: none;
}
.sc-message--wrapper {
align-items: flex-end;
.sc-message--text {
color: white;
background-color: #4e8cff;
word-wrap: break-word;
}
.sc-message--meta {
flex-direction: row-reverse;
}
}
}
}
.sc-message--content {
display: flex;
flex-direction: row;
position: relative;
max-width: calc(100% - 60px);
width: auto;
.sc-message--avatar {
width: 32px;
height: 32px;
margin-right: 4px;
margin-top: 2px;
background-position: 0 0;
flex-shrink: 0;
background-size: cover;
border-radius: 999px;
}
.sc-message--wrapper {
display: flex;
flex-direction: column;
width: 100%;
.sc-message--text {
padding: 10px 20px;
border-radius: 6px;
font-weight: 300;
font-size: .85rem;
max-width: 190px;
line-height: 1.2;
position: relative;
-webkit-font-smoothing: subpixel-antialiased;
color: #263238;
background-color: #f4f7f9;
.sc-message--text-body {
.sc-message--text-content {
white-space: pre-wrap;
}
}
code {
font-family: "Courier New", Courier, monospace !important;
}
}
.sc-message--meta {
display: flex;
justify-content: space-between;
align-items: flex-start;
font-size: 0.65rem;
margin-top: 4px;
padding: 0 4px;
width: 100%;
height: 18px;
box-sizing: border-box;
}
}
}
}
@media (max-width: 450px) {
.sc-message {
width: 80%;
}
}

View File

@@ -0,0 +1,102 @@
/*
* 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).
* 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 dayjs from 'dayjs';
import 'dayjs/locale/en';
import 'dayjs/locale/fr';
import relativeTime from 'dayjs/plugin/relativeTime';
import React, { PropsWithChildren, useState } from 'react';
import ChatIcon from './icons/ChatIcon';
import ButtonsMessage from './messages/ButtonMessage';
import CarouselMessage from './messages/CarouselMessage';
import FileMessage from './messages/FileMessage';
import GeolocationMessage from './messages/GeolocationMessage';
import ListMessage from './messages/ListMessage';
import TextMessage from './messages/TextMessage';
import MessageStatus from './MessageStatus';
import { useChat } from '../providers/ChatProvider';
import { useColors } from '../providers/ColorProvider';
import { TMessage } from '../types/message.types';
import './Message.scss';
dayjs.extend(relativeTime);
type MessageProps = PropsWithChildren<{
Avatar?: () => JSX.Element;
message: TMessage;
}>;
const Message: React.FC<MessageProps> = ({ message, Avatar }) => {
const { participants } = useChat();
const { colors } = useColors();
const [isTimeVisible, setIsTimeVisible] = useState(false);
const user = participants.find(
(participant) => participant.id === message.author,
) || {
id: 'me',
name: 'Anon',
};
const handleTime = () => {
setIsTimeVisible(!isTimeVisible);
};
const fromNow = (time: string) => {
return dayjs(time).fromNow();
};
return (
<div className={`sc-message ${message.direction}`}>
<div className={`sc-message--content ${message.direction}`}>
<div
title={user.name}
className="sc-message--avatar"
style={
user.imageUrl
? { backgroundImage: `url(${user.imageUrl})` }
: undefined
}
>
{Avatar ? (
<Avatar />
) : !user.imageUrl ? (
<ChatIcon width="32px" height="32px" />
) : null}
</div>
<div className="sc-message--wrapper" onClick={handleTime}>
{message.data && 'text' in message.data && (
<TextMessage message={message} />
)}
{message.type === 'file' && <FileMessage message={message} />}
{message.type === 'location' && (
<GeolocationMessage message={message} />
)}
{message.type === 'list' && <ListMessage messageList={message} />}
{message.type === 'carousel' && (
<CarouselMessage messageCarousel={message} />
)}
{message.type === 'buttons' && <ButtonsMessage message={message} />}
<div className="sc-message--meta">
{message.direction === 'sent' && (
<MessageStatus message={message} />
)}
<div
style={{ color: colors.messageTime.text }}
className="sc-message--time"
>
{isTimeVisible && fromNow(message.createdAt)}
</div>
</div>
</div>
</div>
</div>
);
};
export default Message;

View File

@@ -0,0 +1,8 @@
.sc--status {
display: flex;
flex-direction: row;
.sc--status-read {
margin-right: -6px;
}
}

View File

@@ -0,0 +1,57 @@
/*
* 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).
* 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 React from 'react';
import CheckIcon from './icons/CheckIcon';
import { useColors } from '../providers/ColorProvider';
import { TMessage } from '../types/message.types';
import './MessageStatus.scss';
interface MessageStatusProps {
message: TMessage;
}
const MessageStatus: React.FC<MessageStatusProps> = ({ message }) => {
const { colors } = useColors();
if (!('delivery' in message && 'read' in message)) {
throw new Error('Unable to find delivery/read attributes');
}
return (
<div className="sc--status" style={{ color: colors.messageStatus.bg }}>
{message.read && (
<div className="sc--status-wrapper sc--status-read" title="Read">
<CheckIcon
width="16px"
height="16px"
className="read check"
style={{ stroke: colors.messageStatus.bg }}
/>
</div>
)}
{message.delivery && (
<div
className="sc--status-wrapper sc--status-delivery"
title="Delivered"
>
<CheckIcon
width="16px"
height="16px"
className="delivery check"
style={{ stroke: colors.messageStatus.bg }}
/>
</div>
)}
</div>
);
};
export default MessageStatus;

View File

@@ -0,0 +1,6 @@
.sc-message-list {
height: 80%;
overflow-y: auto;
background-size: 100%;
padding: 40px 0px;
}

View File

@@ -0,0 +1,102 @@
/*
* 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).
* 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 React, { PropsWithChildren, useEffect, useRef, useState } from 'react';
import Message from './Message';
import TypingMessage from './messages/TypingMessage';
import { useChat } from '../providers/ChatProvider';
import { useColors } from '../providers/ColorProvider';
import { useWidget } from '../providers/WidgetProvider';
import './Messages.scss';
type MessagesProps = PropsWithChildren<{
Avatar?: () => JSX.Element;
}>;
const Messages: React.FC<MessagesProps> = ({ Avatar }) => {
const { scroll, setScroll, isOpen } = useWidget();
const { messages, showTypingIndicator, setNewIOMessage } = useChat();
const { colors } = useColors();
const scrollListRef = useRef<HTMLDivElement>(null);
const [timeoutId, setTimeoutId] = useState<number | null>(null);
const handleScroll = (e: React.UIEvent<HTMLDivElement>) => {
if (timeoutId) {
clearTimeout(timeoutId);
}
const scrollPercent = Math.round(
(100 * e.currentTarget.scrollTop) / (e.currentTarget.scrollHeight || 1),
);
if (!scroll && scrollPercent) {
setScroll(scrollPercent);
} else if (scrollPercent) {
const id = setTimeout(() => {
setScroll(scrollPercent);
}, 400) as unknown as number;
setTimeoutId(id);
} else if (scroll) {
setScroll(scrollPercent);
}
};
useEffect(() => {
const scrollTo = (scroll: number) => {
if (scrollListRef.current) {
const scrollPercent = Math.round(
(100 * scrollListRef.current.scrollTop) /
(scrollListRef.current.scrollHeight || 1),
);
if (Math.abs(scrollPercent - scroll) > 1 || scroll === 100) {
requestAnimationFrame(() => {
if (scrollListRef.current) {
scrollListRef.current.scrollTo({
top: Math.round(
(scroll * scrollListRef.current.scrollHeight) / 100,
),
behavior: 'instant',
left: 0,
});
}
});
}
}
};
if (isOpen) {
setTimeout(() => {
scrollTo(scroll);
}, 100);
}
}, [scroll, isOpen]);
useEffect(() => {
setNewIOMessage(messages[messages.length - 1]);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
return (
<div
className="sc-message-list"
ref={scrollListRef}
style={{ backgroundColor: colors.messageList.bg }}
onScroll={handleScroll}
>
{messages.map((message) => (
<Message key={message.mid} message={message} Avatar={Avatar} />
))}
{showTypingIndicator && <TypingMessage />}
</div>
);
};
export default Messages;

View File

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

View File

@@ -0,0 +1,60 @@
/*
* 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).
* 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 React from 'react';
import { useChat } from '../providers/ChatProvider';
import { useColors } from '../providers/ColorProvider';
import { useSettings } from '../providers/SettingsProvider';
import { ISuggestion, TOutgoingMessageType } from '../types/message.types';
import './Suggestions.scss';
const Suggestions: React.FC = () => {
const { setPayload, send, suggestions } = useChat();
const settings = useSettings();
const { colors } = useColors();
const sendSuggestion = (
event: React.MouseEvent<HTMLButtonElement>,
suggestion: ISuggestion,
) => {
setPayload(suggestion);
send({
event,
source: 'quick-reply',
data: {
type: TOutgoingMessageType.quick_reply,
data: suggestion,
},
});
if (settings.autoFlush) {
setPayload(null);
}
};
return (
<div
className="sc-suggestions-row"
style={{ background: colors.button.bg }}
>
{suggestions.map((suggestion, idx) => (
<button
key={idx}
className="sc-suggestions-element"
onClick={(event) => sendSuggestion(event, suggestion)}
style={{ borderColor: colors.button.text, color: colors.button.text }}
>
{suggestion.text}
</button>
))}
</div>
);
};
export default Suggestions;

View File

@@ -0,0 +1,123 @@
.sc-user-input {
min-height: 55px;
margin: 0px;
position: relative;
bottom: 0;
display: flex;
background-color: #f4f7f9;
border-bottom-left-radius: 10px;
border-bottom-right-radius: 10px;
transition: background-color 0.2s ease, box-shadow 0.2s ease;
border-top: 1px solid #eaeaea;
}
.sc-user-input--text {
flex-grow: 1;
resize: none;
border: none;
outline: none;
border-bottom-left-radius: 10px;
box-sizing: border-box;
padding: 18px 8px;
font-size: 15px;
font-weight: 400;
line-height: 1.33;
white-space: pre-wrap;
word-wrap: break-word;
color: #565867;
-webkit-font-smoothing: antialiased;
max-height: 200px;
overflow: scroll;
bottom: 0;
overflow-x: hidden;
overflow-y: auto;
}
.sc-user-input--text:empty:before {
content: attr(placeholder);
display: block; /* For Firefox */
color: rgba(86, 88, 103, 0.5);
filter: contrast(15%);
outline: none;
cursor: text;
}
.sc-user-input--text[contenteditable='true']:focus:empty:before {
position: absolute;
}
.sc-user-input--text[contenteditable='true']:focus:empty:after {
content: '\00a0';
}
.sc-user-input--buttons {
display: flex;
gap: 4px;
margin: 0 .5rem;
}
.sc-user-input--button {
display: flex;
flex-direction: column;
justify-content: center;
}
.sc-user-input--button:not(:last-child) {
margin-right: 2px;
}
.sc-user-input.active {
box-shadow: none;
background-color: white;
box-shadow: 0px -5px 20px 0px rgba(150, 165, 190, 0.2);
}
.sc-user-input--button label {
position: relative;
height: 24px;
padding-left: 3px;
cursor: pointer;
}
.sc-user-input--button label:hover path {
fill: rgba(86, 88, 103, 1);
}
.sc-user-input--button input {
position: absolute;
left: 0;
top: 0;
width: 100%;
z-index: 99999;
height: 100%;
opacity: 0;
cursor: pointer;
overflow: hidden;
}
.sc-file-container {
padding: 5px 20px;
display: flex;
align-items: center;
}
.sc-user-input--file-icon {
vertical-align: bottom;
width: 16px;
height: 16px;
}
.delete-file-message {
font-style: normal;
float: right;
cursor: pointer;
color: #c8cad0;
}
.delete-file-message:hover {
color: #5d5e6d;
}
.icon-file-message {
margin-right: 5px;
}

View File

@@ -0,0 +1,231 @@
/*
* 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).
* 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 React, { useEffect, useRef, useState } from 'react';
import EmojiButton from './buttons/EmojiButton';
import FileButton from './buttons/FileButton';
import LocationButton from './buttons/LocationButton';
import MenuButton from './buttons/MenuButton';
import SendButton from './buttons/SendButton';
import CloseIcon from './icons/CloseIcon';
import FileInputIcon from './icons/FileInputIcon';
import Suggestions from './Suggestions';
import { useTranslation } from '../hooks/useTranslation';
import { useChat } from '../providers/ChatProvider';
import { useColors } from '../providers/ColorProvider';
import { useSettings } from '../providers/SettingsProvider';
import { TOutgoingMessageType } from '../types/message.types';
import { OutgoingMessageState } from '../types/state.types';
import './UserInput.scss';
const UserInput: React.FC = () => {
const { t } = useTranslation();
const { colors } = useColors();
const {
suggestions,
message,
setMessage,
file,
setFile,
send,
outgoingMessageState,
} = useChat();
const {
menu,
focusOnOpen,
autoFlush,
allowedUploadTypes,
allowedUploadSize,
showEmoji,
showLocation,
showFile,
placeholder,
} = useSettings();
const userInputRef = useRef<HTMLDivElement>(null);
const [fileError, setFileError] = useState<string | null>(null);
const [inputActive, setInputActive] = useState(false);
useEffect(() => {
// if (userInputRef.current) {
// userInputRef.current.innerHTML = message.current || '';
// }
if (focusOnOpen) {
focusUserInput();
}
}, [message, focusOnOpen]);
useEffect(() => {
if (message === '') {
userInputRef.current!.innerHTML = '';
}
}, [message]);
useEffect(() => {
setFileError(null);
}, [file]);
const cancelFile = () => {
setFile(null);
setFileError(null);
};
const handleInput = () => {
setMessage(
userInputRef.current?.innerText ||
userInputRef.current?.textContent ||
'',
);
};
const sendMessage = (
event: React.MouseEvent | React.KeyboardEvent,
source: string = 'send-button',
) => {
if (message) {
send({
event,
source,
data: {
type: TOutgoingMessageType.text,
data: { text: message },
},
});
if (autoFlush) {
setMessage('');
}
}
if (file) {
setFileError(null);
const typeCheck = allowedUploadTypes.includes(file.type) || false;
if (!typeCheck) {
setFileError(t('messages.file_message.unsupported_file_type'));
} else if (file.size > (allowedUploadSize || 0)) {
setFileError(t('messages.file_message.unsupported_file_size'));
} else {
send({
event,
source,
data: {
type: TOutgoingMessageType.file,
data: {
name: file.name,
size: file.size,
type: file.type,
file,
},
},
});
autoFlush && setFile(null);
}
}
};
const handleKey = (event: React.KeyboardEvent<HTMLDivElement>) => {
if (event.key === 'Enter' && !event.shiftKey) {
sendMessage(event, 'enter-key');
event.preventDefault();
}
};
const focusUserInput = () => {
userInputRef.current?.focus();
};
const uploading = outgoingMessageState === OutgoingMessageState.uploading;
return (
<div
className="sc-user-input-wrapper"
style={{ fill: colors.userInput.text }}
>
{suggestions.length > 0 && <Suggestions />}
{(file || uploading) && (
<div
className="sc-file-container"
style={{
backgroundColor: colors.userInput.text,
color: colors.userInput.bg,
}}
>
<FileInputIcon
width="16px"
height="16px"
className="icon-file-message"
/>
{fileError && <span>{fileError}</span>}
{uploading && <span>Loading...</span>}
{file && file.name && !fileError && (
<span>
{file.name.length > 23
? `${file.name.substring(0, 23)}...`
: file.name}
</span>
)}
<span className="delete-file-message" onClick={cancelFile}>
<CloseIcon height="10" />
</span>
</div>
)}
<form
className={`sc-user-input ${inputActive ? 'active' : ''}`}
style={{ background: colors.userInput.bg }}
>
{menu.length > 0 && <MenuButton />}
<div
role="textbox"
tabIndex={0}
onFocus={() => setInputActive(true)}
onBlur={() => setInputActive(false)}
onKeyDown={handleKey}
onInput={handleInput}
onPaste={(e) => {
e.preventDefault();
(e.target as HTMLInputElement).innerText =
e.clipboardData.getData('text/plain');
handleInput();
}}
contentEditable
suppressContentEditableWarning={true}
spellCheck
aria-autocomplete="list"
// @ts-expect-error to check
placeholder={placeholder} // Adjust for localization
className="sc-user-input--text"
ref={userInputRef}
style={{ color: colors.userInput.text }}
/>
<div className="sc-user-input--buttons">
{showEmoji && (
<div className="sc-user-input--button">
<EmojiButton inputRef={userInputRef} onInput={handleInput} />
</div>
)}
{showLocation && (
<div className="sc-user-input--button">
<LocationButton />
</div>
)}
{showFile && (
<div className="sc-user-input--button">
<FileButton />
</div>
)}
<div className="sc-user-input--button">
<SendButton
disabled={!message}
onClick={(event) => sendMessage(event)}
/>
</div>
</div>
</form>
</div>
);
};
export default UserInput;

View File

@@ -0,0 +1,45 @@
.loading-image {
position: absolute;
top: 50%;
left: 50%;
transform: translateX(-50%) translateY(-50%);
}
.user-subscription-wrapper {
height: 100%;
width: 100%;
position: relative;
text-align: center;
.user-subscription {
position: absolute;
top: 50%;
transform: translateY(-50%);
.user-subscription-title {
margin: 2rem;
}
.user-subscription-form {
.user-subscription-form-input {
border: 0;
background-color: #eeeeee;
outline: 0;
padding: 1rem;
border-radius: 10px;
margin: 1rem;
width: 70%;
}
.user-subscription-form-button-submit {
display: block;
border-radius: 10px;
border: 0;
width: 50%;
margin: 2rem auto;
padding: 1rem;
cursor: pointer;
}
}
}
}

View File

@@ -0,0 +1,162 @@
/*
* 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).
* 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 React, { SyntheticEvent, useCallback, useEffect, useState } from 'react';
import { useTranslation } from '../hooks/useTranslation';
import { useChat } from '../providers/ChatProvider';
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,
ISubscriber,
TMessage,
TOutgoingMessageType,
} from '../types/message.types';
const UserSubscription: React.FC = () => {
const config = useConfig();
const { t } = useTranslation();
const { colors } = useColors();
const { socket } = useSocket();
const settings = useSettings();
const { setScreen } = useWidget();
const {
send,
setMessages,
setConnectionState,
participants,
setParticipants,
} = useChat();
const [firstName, setFirstName] = useState<string>('');
const [lastName, setLastName] = useState<string>('');
const handleSubmit = useCallback(
async (event?: React.FormEvent<HTMLFormElement>) => {
event?.preventDefault();
try {
setConnectionState(2);
const { body } = await socket.get<{
messages: TMessage[];
profile: ISubscriber;
}>(
`/webhook/${config.channel}/?verification_token=${config.token}&first_name=${firstName}&last_name=${lastName}`,
);
const { messages, profile } = body;
localStorage.setItem('profile', JSON.stringify(profile));
messages.forEach((message) => {
const direction =
message.author === profile.foreign_id ||
message.author === profile.id
? Direction.sent
: Direction.received;
message.direction = direction;
if (message.direction === Direction.sent) {
message.read = true;
message.delivery = false;
}
});
setMessages(messages);
setParticipants([
...participants,
{
id: profile.foreign_id,
foreign_id: profile.foreign_id,
name: `${profile.first_name} ${profile.last_name}`,
},
]);
if (messages.length === 0) {
send({
event: event as SyntheticEvent,
source: 'get_started_button',
data: {
type: TOutgoingMessageType.postback,
data: {
text: 'GET_STARTED', //TODO:use translation here?
payload: 'GET_STARTED',
},
author: profile.foreign_id,
},
});
}
setConnectionState(3);
setScreen('chat');
} catch (e) {
// eslint-disable-next-line no-console
console.error('Unable to subscribe user', e);
setScreen('prechat');
setConnectionState(0);
}
},
// eslint-disable-next-line react-hooks/exhaustive-deps
[
firstName,
lastName,
participants,
setConnectionState,
setMessages,
setParticipants,
setScreen,
socket,
],
);
useEffect(() => {
const profile = localStorage.getItem('profile');
if (profile) {
const parsedProfile = JSON.parse(profile);
setFirstName(parsedProfile.first_name);
setLastName(parsedProfile.last_name);
handleSubmit();
}
}, [handleSubmit, setScreen]);
return (
<div className="user-subscription-wrapper">
<form className="user-subscription" onSubmit={handleSubmit}>
<div className="user-subscription-title">
{settings.greetingMessage}
</div>
<div className="user-subscription-form">
<input
className="user-subscription-form-input"
value={firstName}
onChange={(e) => setFirstName(e.target.value)}
placeholder={t('user_subscription.first_name')}
required
/>
<input
className="user-subscription-form-input"
value={lastName}
onChange={(e) => setLastName(e.target.value)}
placeholder={t('user_subscription.last_name')}
required
/>
<button
type="submit"
style={{ background: colors.header.bg, color: colors.header.text }}
className="user-subscription-form-button-submit"
>
{t('user_subscription.get_started')}
</button>
</div>
</form>
</div>
);
};
export default UserSubscription;

View File

@@ -0,0 +1,28 @@
.sc-webview {
border: 0 !important;
position: relative;
width: 100%;
height: 100%;
padding: 0;
margin: 0;
}
.sc-webview iframe {
width: 100%;
height: calc(100% - 35px);
}
.sc-webview--footer {
width: 100%;
height: 35px;
text-align: center;
display: table;
}
.sc-webview--button {
cursor: pointer;
display: table-cell;
vertical-align: middle;
margin: 0;
padding: 0;
}
.sc-webview--button img {
vertical-align: middle;
}

View File

@@ -0,0 +1,50 @@
/*
* 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).
* 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 React, { useEffect, useState } from 'react';
import BackIcon from './icons/BackIcon';
import { useTranslation } from '../hooks/useTranslation';
import { useChat } from '../providers/ChatProvider';
import { useColors } from '../providers/ColorProvider';
import './Webview.scss';
const Webview: React.FC = () => {
const { t } = useTranslation();
const { colors } = useColors();
const { setWebviewUrl, webviewUrl } = useChat();
const [loaded, setLoaded] = useState(false);
useEffect(() => {
setLoaded(true);
}, []);
const close = () => {
setWebviewUrl('');
};
return (
<div className="sc-webview">
{loaded && webviewUrl && (
<iframe src={webviewUrl} title="webview" frameBorder="0" />
)}
<div
className="sc-webview--footer"
style={{ background: colors.header.bg, color: colors.header.text }}
>
<h3 className="sc-webview--button" onClick={close}>
<BackIcon width="16px" height="16px" />
{t('settings.back')}
</h3>
</div>
</div>
);
};
export default Webview;

View File

@@ -0,0 +1,29 @@
.sc-user-input--emoji-icon-wrapper {
background: none;
border: none;
padding: 0px;
margin: 0px;
outline: none;
}
.sc-user-input--emoji-icon-wrapper:focus {
outline: none;
}
.sc-user-input--emoji-icon {
width: 18px;
height: 18px;
cursor: pointer;
align-self: center;
vertical-align: middle;
}
.sc-user-input--emoji-icon-wrapper:focus .sc-user-input--emoji-icon path,
.sc-user-input--emoji-icon-wrapper:focus .sc-user-input--emoji-icon circle,
.sc-user-input--emoji-icon.active path,
.sc-user-input--emoji-icon.active circle,
.sc-user-input--emoji-icon:hover path,
.sc-user-input--emoji-icon:hover circle {
filter: contrast(15%);
}

View File

@@ -0,0 +1,76 @@
/*
* 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).
* 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 React, { RefObject, useRef, useState } from 'react';
import EmojiPicker from '../EmojiPicker';
import './EmojiButton.scss';
import EmojiIcon from '../icons/EmojiIcon';
const EmojiButton: React.FC<{
inputRef: RefObject<HTMLDivElement>;
onInput: () => void;
}> = ({ inputRef, onInput }) => {
const [isActive, setIsActive] = useState(false);
const emojiButtonRef = useRef<HTMLButtonElement>(null);
const togglePicker = () => {
setIsActive(!isActive);
};
const handleBlur = (e: React.FocusEvent) => {
if (!e.relatedTarget || e.relatedTarget !== emojiButtonRef.current) {
togglePicker();
}
// if (inputRef.current) {
// inputRef.current.focus();
// }
};
const handleSelect = (
_event: React.MouseEvent<HTMLSpanElement>,
emoji: string,
) => {
insertEmoji(emoji);
};
const insertEmoji = (emoji: string) => {
if (inputRef.current) {
const textNode = document.createTextNode(emoji);
inputRef.current.appendChild(textNode);
// Place the cursor after the emoji
const range = document.createRange();
const sel = window.getSelection();
range.setStartAfter(textNode);
range.collapse(true);
sel?.removeAllRanges();
sel?.addRange(range);
onInput(); // Call to update the onChange handler
}
};
return (
<div className="sc-user-input--picker-wrapper">
{isActive && <EmojiPicker onBlur={handleBlur} onSelect={handleSelect} />}
<button
onClick={(e) => {
e.preventDefault();
togglePicker();
}}
id="sc-emoji-button"
className="sc-user-input--emoji-icon-wrapper"
ref={emojiButtonRef}
>
<EmojiIcon />
</button>
</div>
);
};
export default EmojiButton;

View File

@@ -0,0 +1,24 @@
.sc-user-input--file-wrapper {
position: relative;
cursor: pointer;
}
.sc-user-input--file-icon-wrapper {
background: none;
border: none;
padding: 0px;
margin: 0px;
outline: none;
cursor: pointer;
}
.sc-user-input--file-icon {
width: 16px;
height: 16px;
align-self: center;
outline: none;
vertical-align: middle;
}
.sc-user-input--file-icon:hover path {
filter: contrast(15%);
}

View File

@@ -0,0 +1,43 @@
/*
* 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).
* 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 React, { ChangeEvent } from 'react';
import { useChat } from '../../providers/ChatProvider';
import FileInputIcon from '../icons/FileInputIcon';
import './FileButton.scss';
const FileButton: React.FC = () => {
const { setFile } = useChat();
const handleClick = (e: React.MouseEvent<HTMLInputElement>) => {
(e.target as HTMLInputElement).value = '';
};
const handleChange = (e: ChangeEvent<HTMLInputElement>) => {
if (e.target.files && e.target.files[0]) {
setFile && setFile(e.target.files[0]);
}
};
return (
<div className="sc-user-input--file-wrapper">
<button className="sc-user-input--file-icon-wrapper" type="button">
<FileInputIcon />
<input
type="file"
id="file-input"
onChange={handleChange}
onClick={handleClick}
/>
</button>
</div>
);
};
export default FileButton;

View File

@@ -0,0 +1,30 @@
.sc-user-input--location-icon-wrapper {
background: none;
border: none;
padding: 0px;
margin: 0px;
outline: none;
}
.sc-user-input--location-icon-wrapper:focus {
outline: none;
}
.sc-user-input--location-icon {
width: 16px;
height: 16px;
cursor: pointer;
align-self: center;
vertical-align: middle;
}
.sc-user-input--location-icon-wrapper:focus .sc-user-input--location-icon path,
.sc-user-input--location-icon-wrapper:focus
.sc-user-input--location-icon
circle,
.sc-user-input--location-icon.active path,
.sc-user-input--location-icon.active circle,
.sc-user-input--location-icon:hover path,
.sc-user-input--location-icon:hover circle {
filter: contrast(15%);
}

View File

@@ -0,0 +1,73 @@
/*
* 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).
* 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 React from 'react';
import { useChat } from '../../providers/ChatProvider';
import { useSettings } from '../../providers/SettingsProvider';
import { TOutgoingMessageType } from '../../types/message.types';
import LocationIcon from '../icons/LocationIcon';
import './LocationButton.scss';
const LocationButton: React.FC = () => {
const { setPayload, send } = useChat();
const settings = useSettings();
const locateMe = (event: React.MouseEvent<HTMLButtonElement>) => {
if (navigator.geolocation) {
navigator.geolocation.getCurrentPosition(
(position) => {
setPayload({
coordinates: {
lat: position.coords.latitude,
lng: position.coords.longitude,
},
});
send({
event,
source: 'geo-location',
data: {
type: TOutgoingMessageType.location,
data: {
coordinates: {
lat: position.coords.latitude,
lng: position.coords.longitude,
},
},
},
});
if (settings.autoFlush) {
setPayload(null);
}
},
(error) => {
// eslint-disable-next-line no-console
console.error('Error getting location', error);
},
);
} else {
// eslint-disable-next-line no-console
console.error('Geolocation is not supported by this browser.');
}
};
return (
<div className="sc-user-input--location-wrapper">
<button
onClick={locateMe}
type="button"
className="sc-user-input--location-icon-wrapper"
>
<LocationIcon />
</button>
</div>
);
};
export default LocationButton;

View File

@@ -0,0 +1,46 @@
.sc-user-input--menu {
align-self: center;
}
.sc-user-input--menu-button {
border: 0;
background-color: transparent;
cursor: pointer;
}
.sc-user-input--menu-img {
max-width: 24px;
}
.sc-menu-content {
display: block;
position: absolute;
box-shadow: 0px -10px 20px 5px rgba(150, 165, 190, 0.2);
width: 60%;
bottom: 55px;
z-index: 3;
&:active,
&:focus {
outline: none;
}
.sc-header-submenu-content {
position: relative;
text-align: center;
.sc-title-submenu-title {
font-weight: 600;
font-size: 1.125rem;
}
}
.sc-return-submenu-content {
display: inline-block;
position: absolute;
left: 0;
top: 50%;
transform: translateY(-50%);
font-size: 1.5rem;
padding: 1rem;
text-decoration: none;
cursor: pointer;
}
}

View File

@@ -0,0 +1,141 @@
/*
* 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).
* 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 React, { useEffect, useRef, useState } from 'react';
import { useChat } from '../../providers/ChatProvider';
import { useColors } from '../../providers/ColorProvider';
import { useSettings } from '../../providers/SettingsProvider';
import { IMenuNode, MenuType } from '../../types/menu.type';
import { IPayload, TOutgoingMessageType } from '../../types/message.types';
import MenuIcon from '../icons/MenuIcon';
import MenuItem from '../MenuItem';
import './MenuButton.scss';
const MenuButton: React.FC = () => {
const { colors } = useColors();
const settings = useSettings();
const { send, setPayload } = useChat();
const [displayMenu, setDisplayMenu] = useState(false);
const [current, setCurrent] = useState<IMenuNode>({
title: 'Menu',
type: MenuType.nested,
call_to_actions: settings?.menu || [],
});
const menuRef = useRef<HTMLDivElement>(null);
useEffect(() => {
setCurrent({
title: 'Menu',
type: MenuType.nested,
call_to_actions: settings?.menu || [],
});
}, [settings]);
const toggleMenu = () => {
setDisplayMenu(!displayMenu);
if (!displayMenu) {
setTimeout(() => {
menuRef.current?.focus();
}, 0);
}
};
const blur = (e: React.FocusEvent<HTMLDivElement>) => {
if (
!e.relatedTarget ||
(e.relatedTarget as HTMLElement).id !== 'sc-menu-button'
) {
setDisplayMenu(false);
}
};
const openSubItems = (item: IMenuNode) => {
setCurrent(item);
};
const handlePostback = (item: IPayload) => {
setPayload(item);
send({
// @ts-expect-error todo
event: new Event('postback'),
source: 'persistent-menu',
data: {
type: TOutgoingMessageType.postback,
data: {
payload: item.payload as string,
text: item.text as string,
},
},
});
if (settings?.autoFlush) {
setPayload(null);
}
menuRef.current?.blur();
};
const previous = (current: IMenuNode) => {
if (current._parent) {
setCurrent(current._parent);
}
};
return (
<div className="sc-user-input--menu sc-user-menu">
<button
onClick={toggleMenu}
type="button"
id="sc-menu-button"
className="sc-user-input--menu-button"
>
<MenuIcon />
</button>
{displayMenu && (
<div
tabIndex={0}
onBlur={blur}
ref={menuRef}
className="sc-menu-content"
style={{
color: colors.header.text,
backgroundColor: colors.header.bg,
}}
>
<div className="sc-header-submenu-content">
{current._parent && (
<a
style={{ color: colors.header.text }}
className="sc-return-submenu-content"
onClick={() => previous(current)}
>
&#10094;
</a>
)}
<h4 className="sc-title-submenu-title">{current.title}</h4>
</div>
{current.call_to_actions && (
<div
className="sc-menu-elements"
style={{ color: colors.header.text }}
>
{current.call_to_actions.map((subitem, index) => (
<MenuItem
key={index}
item={subitem}
parent={current}
onOpenSubItems={openSubItems}
onPostback={handlePostback}
/>
))}
</div>
)}
</div>
)}
</div>
);
};
export default MenuButton;

View File

@@ -0,0 +1,23 @@
.sc-user-input--button-icon-wrapper {
background: none;
border: none;
padding: 0px;
margin: 0px;
outline: none;
cursor: pointer;
}
.sc-user-input--button-icon-wrapper:focus {
outline: none;
}
.sc-user-input--button-icon-wrapper svg {
width: 16px;
height: 16px;
cursor: pointer;
align-self: center;
outline: none;
display: inline-block;
vertical-align: middle;
}
.sc-user-input--button-icon-wrapper svg:hover path {
filter: contrast(15%);
}

View File

@@ -0,0 +1,32 @@
/*
* 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).
* 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 React from 'react';
import SendIcon from '../icons/SendIcon';
import './SendButton.scss';
interface SendButtonProps
extends React.ButtonHTMLAttributes<HTMLButtonElement> {}
const SendButton: React.FC<SendButtonProps> = (props) => {
const { onClick, ...rest } = props;
return (
<button
onClick={onClick}
{...rest}
className="sc-user-input--button-icon-wrapper"
>
<SendIcon />
</button>
);
};
export default SendButton;

View File

@@ -0,0 +1,41 @@
/*
* 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).
* 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 { FC, SVGProps } from 'react';
const BackIcon: FC<SVGProps<SVGSVGElement>> = ({
width = '24',
height = '24',
fill = 'none',
stroke = '#000',
strokeLinecap = 'round',
strokeLinejoin = 'round',
strokeWidth = '2',
viewBox = '0 0 24 24',
...rest
}) => {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
width={width}
height={height}
fill={fill}
stroke={stroke}
strokeLinecap={strokeLinecap}
strokeLinejoin={strokeLinejoin}
strokeWidth={strokeWidth}
viewBox={viewBox}
{...rest}
>
<path stroke="#fff" strokeOpacity="1" d="M15 18L9 12 15 6" />
</svg>
);
};
export default BackIcon;

View File

@@ -0,0 +1,59 @@
/*
* 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).
* 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 { FC, SVGProps } from 'react';
const ChatIcon: FC<SVGProps<SVGSVGElement>> = ({
viewBox = '-4749.48 -5020 35.036 35.036',
...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)"
/>
<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>
</g>
</svg>
);
};
export default ChatIcon;

View File

@@ -0,0 +1,37 @@
/*
* 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).
* 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 { FC, SVGProps } from 'react';
const CheckIcon: FC<SVGProps<SVGSVGElement>> = ({
viewBox = '0 0 24 24',
fill = 'none',
stroke = '',
strokeWidth = '2',
strokeLinecap = 'round',
strokeLinejoin = 'round',
...rest
}) => {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox={viewBox}
fill={fill}
stroke={stroke}
strokeWidth={strokeWidth}
strokeLinecap={strokeLinecap}
strokeLinejoin={strokeLinejoin}
{...rest}
>
<path d="M20 6L9 17l-5-5" />
</svg>
);
};
export default CheckIcon;

View File

@@ -0,0 +1,23 @@
/*
* 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).
* 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 { FC, SVGProps } from 'react';
const CloseIcon: FC<SVGProps<SVGSVGElement>> = ({
viewBox = '0 0 24 24',
...rest
}) => {
return (
<svg xmlns="http://www.w3.org/2000/svg" viewBox={viewBox} {...rest}>
<path d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z" />
</svg>
);
};
export default CloseIcon;

View File

@@ -0,0 +1,39 @@
/*
* 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).
* 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 { FC, SVGProps } from 'react';
const ConnectionIcon: FC<SVGProps<SVGSVGElement>> = ({
width = '100',
height = '100',
x = '0',
y = '0',
viewBox = '0 0 512.115 512.115',
...rest
}) => {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
width={width}
height={height}
x={x}
y={y}
viewBox={viewBox}
{...rest}
>
<circle cx="255.998" cy="374.496" r="32.133" />
<path d="M259.46 342.551c20.068 21.447 2.961 57.089-26.924 53.889 19.876 21.242 55.595 7.028 55.595-21.944 0-16.576-12.553-30.217-28.671-31.945zM346.922 272.908c-14.694-11.793-31.719-20.793-50.261-26.198l28.056-28.056a181.486 181.486 0 0144.53 26.284c8.351 6.698 9.121 19.122 1.552 26.692-6.461 6.46-16.738 7.007-23.877 1.278zm-204.172-27.97c-8.351 6.698-9.122 19.122-1.552 26.692 6.109 6.254 16.525 7.178 23.876 1.278 20.705-16.617 46.037-27.689 73.723-30.964l35.859-35.859c-48.401-4.971-95.296 9.491-131.906 38.853zm157.933-64.88l29.253-29.253c-79.639-25.044-168.342-8.412-234.2 47.892-7.97 6.814-8.541 18.928-1.127 26.343 6.568 6.568 17.121 7.079 24.173 1.035 51.196-43.872 118.577-59.715 181.901-46.017zm116.705 44.982c7.415-7.415 6.844-19.529-1.127-26.343a248.048 248.048 0 00-42.385-29.203l-26.573 26.573a211.854 211.854 0 0145.912 30.008c7.052 6.044 17.605 5.533 24.173-1.035zm-116.359 95.457c6.604 4.525 15.912 4.623 22.908-2.005 7.88-7.88 6.751-21.016-2.422-27.343a114.995 114.995 0 00-49.967-19.326l-37.95 37.95c22.182-6.494 47.03-3.254 67.431 10.724z" />
<path d="M416.261 198.697a248.048 248.048 0 00-42.385-29.203l-7.388 7.388a247.907 247.907 0 0129.774 21.814c9.055 7.741 8.317 21.88-1.529 28.533 6.982 4.781 16.543 3.922 22.655-2.19 7.414-7.413 6.843-19.528-1.127-26.342zM369.247 244.938a181.486 181.486 0 00-44.53-26.284l-6.327 6.327a181.811 181.811 0 0130.857 19.957c9.462 7.589 8.937 22.091-1.089 28.871 7.054 4.744 16.544 3.917 22.64-2.178 7.57-7.571 6.8-19.996-1.551-26.693zM321.515 291.148a114.995 114.995 0 00-49.967-19.326l-3.48 3.48a115.07 115.07 0 0133.447 15.846c10.331 7.126 10.2 22.482-.221 29.513 6.595 4.357 15.745 4.365 22.643-2.17 7.88-7.879 6.751-21.016-2.422-27.343z" />
<path d="M437.077 75.038c-100.046-100.044-261.982-100.057-362.039 0-100.044 100.042-100.058 261.98 0 362.039 100.046 100.044 261.981 100.057 362.04 0 100.043-100.046 100.057-261.981-.001-362.039zm-25.685 67.607c1.856-1.856 4.94-1.582 6.444.568 54.271 77.582 45.249 184.719-22.326 252.297-67.579 67.576-174.715 76.599-252.298 22.326-2.15-1.504-2.423-4.589-.568-6.444l268.748-268.747zM94.274 368.912C5.205 241.953 93.085 58.843 256.057 58.843c40.925 0 79.777 12.225 112.855 35.431 2.147 1.506 2.42 4.588.565 6.443l-268.76 268.761c-1.855 1.854-4.937 1.581-6.443-.566z" />
<path d="M349.926 17.756c141.317 87.019 164.005 282.465 47.152 399.321-74.64 74.638-183.463 93.269-274.888 57.282 97.36 59.953 228.457 49.147 314.888-37.282 131.453-131.456 82.523-352.533-87.152-419.321z" />
</svg>
);
};
export default ConnectionIcon;

View File

@@ -0,0 +1,36 @@
/*
* 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).
* 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 { FC, SVGProps } from 'react';
const EmojiIcon: FC<SVGProps<SVGSVGElement>> = ({
x = '0',
y = '0',
className = 'sc-user-input--emoji-icon',
viewBox = '0 0 37 37',
...rest
}) => {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
x={x}
y={y}
className={className}
viewBox={viewBox}
{...rest}
>
<path d="M18.696 37.393C8.387 37.393 0 29.006 0 18.696 0 8.387 8.387 0 18.696 0c10.31 0 18.696 8.387 18.696 18.696.001 10.31-8.386 18.697-18.696 18.697zm0-35.393C9.49 2 2 9.49 2 18.696c0 9.206 7.49 16.696 16.696 16.696 9.206 0 16.696-7.49 16.696-16.696C35.393 9.49 27.902 2 18.696 2z" />
<circle cx="12.379" cy="14.359" r="1.938" />
<circle cx="24.371" cy="14.414" r="1.992" />
<path d="M18.035 27.453c-5.748 0-8.342-4.18-8.449-4.357a1 1 0 011.71-1.038c.094.151 2.161 3.396 6.74 3.396 4.713 0 7.518-3.462 7.545-3.497a1 1 0 011.566 1.244c-.138.173-3.444 4.252-9.112 4.252z" />
</svg>
);
};
export default EmojiIcon;

View File

@@ -0,0 +1,27 @@
/*
* 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).
* 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 { FC, SVGProps } from 'react';
const FileIcon: FC<SVGProps<SVGSVGElement>> = ({
viewBox = '0 0 512 512',
...rest
}) => {
return (
<svg xmlns="http://www.w3.org/2000/svg" viewBox={viewBox} {...rest}>
<g data-name="1">
<path d="M378.83 450H150a50.17 50.17 0 01-50.11-50.11V98.11A50.17 50.17 0 01150 48h150.1a15 15 0 0110.61 4.39l113.84 113.88a15 15 0 014.39 10.61v223A50.17 50.17 0 01378.83 450zM150 78a20.13 20.13 0 00-20.11 20.11v301.78A20.13 20.13 0 00150 420h228.83a20.13 20.13 0 0020.11-20.11v-216.8L293.85 78z" />
<path d="M413.94 191.88h-78.77a50.17 50.17 0 01-50.11-50.11V63a15 15 0 0130 0v78.77a20.13 20.13 0 0020.11 20.11h78.77a15 15 0 010 30zM264.4 375a15 15 0 01-10.61-4.4l-54.45-54.44a15 15 0 1121.21-21.22l43.85 43.84 43.84-43.84a15 15 0 1121.21 21.22L275 370.55a15 15 0 01-10.6 4.45z" />
<path d="M264.4 365a15 15 0 01-15-15V231a15 15 0 0130 0v119a15 15 0 01-15 15z" />
</g>
</svg>
);
};
export default FileIcon;

View File

@@ -0,0 +1,36 @@
/*
* 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).
* 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 { FC, SVGProps } from 'react';
const FileInputIcon: FC<SVGProps<SVGSVGElement>> = ({
x = '0',
y = '0',
className = 'sc-user-input--file-icon',
viewBox = '0 0 32 32',
...rest
}) => {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
x={x}
y={y}
className={className}
viewBox={viewBox}
{...rest}
>
<path
fill="currentColor"
d="M20.807 10.22l-2.03-2.029-10.15 10.148c-1.682 1.681-1.682 4.408 0 6.089s4.408 1.681 6.09 0l12.18-12.178a7.173 7.173 0 000-10.148 7.176 7.176 0 00-10.149 0L3.96 14.889l-.027.026c-3.909 3.909-3.909 10.245 0 14.153 3.908 3.908 10.246 3.908 14.156 0l.026-.027.001.001 8.729-8.728-2.031-2.029-8.729 8.727-.026.026a7.148 7.148 0 01-10.096 0 7.144 7.144 0 010-10.093l.028-.026-.001-.002L18.78 4.131c1.678-1.679 4.411-1.679 6.09 0s1.678 4.411 0 6.089L12.69 22.398c-.56.56-1.47.56-2.03 0a1.437 1.437 0 010-2.029L20.81 10.22z"
/>
</svg>
);
};
export default FileInputIcon;

View File

@@ -0,0 +1,67 @@
/*
* 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).
* 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 { FC, SVGProps } from 'react';
const LoadingIcon: FC<
SVGProps<SVGSVGElement> & {
size?: number;
color?: string;
}
> = ({ size = 50, color = '#000', ...rest }) => {
return (
<svg
width={size}
height={size}
viewBox="0 0 50 50"
xmlns="http://www.w3.org/2000/svg"
fill={color}
{...rest}
>
<circle cx="25" cy="25" r="20" stroke="none" fill="none">
<animate
attributeName="r"
begin="0s"
dur="1.5s"
values="20; 0"
keyTimes="0; 1"
repeatCount="indefinite"
/>
<animate
attributeName="stroke-opacity"
begin="0s"
dur="1.5s"
values="1; 0"
keyTimes="0; 1"
repeatCount="indefinite"
/>
</circle>
<circle cx="25" cy="25" r="20" stroke="none" fill="none">
<animate
attributeName="r"
begin="0.75s"
dur="1.5s"
values="20; 0"
keyTimes="0; 1"
repeatCount="indefinite"
/>
<animate
attributeName="stroke-opacity"
begin="0.75s"
dur="1.5s"
values="1; 0"
keyTimes="0; 1"
repeatCount="indefinite"
/>
</circle>
</svg>
);
};
export default LoadingIcon;

View File

@@ -0,0 +1,36 @@
/*
* 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).
* 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 { FC, SVGProps } from 'react';
const LocationIcon: FC<SVGProps<SVGSVGElement>> = ({
x = '0',
y = '0',
className = 'sc-user-input--location-icon',
version = '1.1',
viewBox = '0 0 32 32',
...rest
}) => {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
x={x}
y={y}
className={className}
version={version}
viewBox={viewBox}
{...rest}
>
<path d="M16.002 17.746c3.309 0 6-2.692 6-6s-2.691-6-6-6-6 2.691-6 6 2.691 6 6 6zm0-11c2.758 0 5 2.242 5 5s-2.242 5-5 5-5-2.242-5-5 2.242-5 5-5z" />
<path d="M16 0C9.382 0 4 5.316 4 12.001c0 7 6.001 14.161 10.376 19.194.016.02.718.805 1.586.805h.077c.867 0 1.57-.785 1.586-.805 4.377-5.033 10.377-12.193 10.377-19.194A11.971 11.971 0 0016 0zm.117 29.883c-.021.02-.082.064-.135.098-.01-.027-.084-.086-.129-.133C12.188 25.631 6 18.514 6 12.001 6 6.487 10.487 2 16 2c5.516 0 10.002 4.487 10.002 10.002 0 6.512-6.188 13.629-9.885 17.881z" />
</svg>
);
};
export default LocationIcon;

View File

@@ -0,0 +1,37 @@
/*
* 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).
* 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 { FC, SVGProps } from 'react';
const MenuIcon: FC<SVGProps<SVGSVGElement>> = ({
width = '32',
height = '32',
x = '0',
y = '0',
className = 'sc-user-input--menu-img',
viewBox = '0 0 32 32',
...rest
}) => {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
width={width}
height={height}
x={x}
y={y}
className={className}
viewBox={viewBox}
{...rest}
>
<path d="M4 10h24a2 2 0 000-4H4a2 2 0 000 4zm24 4H4a2 2 0 000 4h24a2 2 0 000-4zm0 8H4a2 2 0 000 4h24a2 2 0 000-4z" />
</svg>
);
};
export default MenuIcon;

View File

@@ -0,0 +1,65 @@
/*
* 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).
* 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 { FC, SVGProps } from 'react';
const OpenIcon: FC<SVGProps<SVGSVGElement>> = ({
width = '18',
height = '18',
...rest
}) => {
return (
<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="#fff"
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
fill="#fff"
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>
);
};
export default OpenIcon;

View File

@@ -0,0 +1,24 @@
/*
* 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).
* 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 { FC, SVGProps } from 'react';
const SendIcon: FC<SVGProps<SVGSVGElement>> = ({
viewBox = '0 0 48 48',
...rest
}) => {
return (
<svg xmlns="http://www.w3.org/2000/svg" viewBox={viewBox} {...rest}>
<path d="M4.02 42L46 24 4.02 6 4 20l30 4-30 4z" />
<path fill="none" d="M0 0h48v48H0z" />
</svg>
);
};
export default SendIcon;

View File

@@ -0,0 +1,58 @@
.sc-message--buttons {
color: rgb(34, 34, 34);
max-width: -webkit-fill-available;
padding: 0.25rem 0.5rem;
border-radius: 6px;
font-weight: 300;
font-size: 1.25rem;
line-height: 1.4;
position: relative;
-webkit-font-smoothing: subpixel-antialiased;
text-align: center;
.sc-message--buttons-content {
border: 1px solid;
border-radius: 20px;
padding: 0.25rem;
width: 80%;
margin: 2px;
cursor: pointer;
outline: 0;
font-size: 1rem;
}
}
.sc-message--list-element-bottom {
.sc-message--buttons-content {
width: 100%;
font-weight: 600;
border-radius: 0 0 6px 6px;
padding: 0.5rem;
margin: 0;
}
.sc-message--buttons {
padding: 0;
}
}
.sc-message--list-element {
.sc-message--buttons-content {
padding: 0.5rem;
margin: 0;
}
.sc-message--buttons {
padding: 0;
margin: 1rem;
text-align: center;
}
}
.sc-message--carousel-element {
.sc-message--buttons-content {
padding: 0.5rem;
}
.sc-message--buttons {
margin: 0;
text-align: center;
}
}

View File

@@ -0,0 +1,84 @@
/*
* 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).
* 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 React from 'react';
import { useChat } from '../../providers/ChatProvider';
import { useColors } from '../../providers/ColorProvider';
import { useSettings } from '../../providers/SettingsProvider';
import {
TButton,
TMessage,
TOutgoingMessageType,
} from '../../types/message.types';
import './ButtonMessage.scss';
interface ButtonsMessageProps {
message: TMessage;
}
const ButtonsMessage: React.FC<ButtonsMessageProps> = ({ message }) => {
const { setPayload, send, setWebviewUrl } = useChat();
const settings = useSettings();
const { colors } = useColors();
const handleClick = (
event: React.MouseEvent<HTMLButtonElement>,
button: TButton,
) => {
if (button.type === 'web_url' && button.url) {
if (button.messenger_extensions) {
setWebviewUrl(button.url);
} else {
window.open(button.url, '_blank');
}
} else if (button.type === 'postback') {
setPayload({ text: button.title, payload: button.payload });
send({
event,
source: 'post-back',
data: {
type: TOutgoingMessageType.postback,
data: {
text: button.title,
payload: button.payload,
},
},
});
if (settings.autoFlush) {
setPayload(null);
}
}
};
if (!('buttons' in message.data)) {
throw new Error('Unable to find buttons');
}
return (
<div className="sc-message--buttons">
{message.data.buttons.map((button, index) => (
<button
key={index}
className="sc-message--buttons-content"
onClick={(event) => handleClick(event, button)}
style={{
borderColor: colors.button.border,
color: colors.button.text,
backgroundColor: colors.button.bg,
}}
>
{button.title}
</button>
))}
</div>
);
};
export default ButtonsMessage;

View File

@@ -0,0 +1,65 @@
.sc-message--carousel {
border-radius: 10px;
position: relative;
width: 100%;
overflow: hidden;
.sc-message--carousel-inner {
display: flex;
transition: transform 0.5s ease;
width: 100%;
.sc-message--carousel-element-wrapper {
min-width: 100%;
height: 100%;
display: flex;
align-items: flex-end;
justify-content: center;
.sc-message--carousel-element {
padding: 1rem;
width: 100%;
.sc-message--carousel-element-image {
background-position: center;
background-repeat: no-repeat;
background-size: cover;
position: relative;
width: 100%;
height: 150px;
}
.sc-message--carousel-title {
font-size: 1rem;
font-weight: 600;
padding: 0.5rem 0;
margin-top: 8px;
}
.sc-message--carousel-element-description {
width: 100%;
}
}
}
}
.sc-message--carousel-control {
position: absolute;
top: 50%;
transform: translateY(-50%);
background-color: rgba(0, 0, 0, 0.5);
color: white;
border: none;
cursor: pointer;
padding: 10px;
z-index: 2;
&.prev {
left: 10px;
}
&.next {
right: 10px;
}
}
}

View File

@@ -0,0 +1,114 @@
/*
* 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).
* 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 React, { useState } from 'react';
import ButtonsMessage from './ButtonMessage';
import { useColors } from '../../providers/ColorProvider';
import { TButton, Direction, TMessage } from '../../types/message.types';
import './CarouselMessage.scss';
import { processContent } from '../../utils/text';
interface Element {
title: string;
subtitle?: string;
image_url?: string;
buttons?: TButton[];
}
interface MessageCarousel {
direction?: Direction;
data: {
elements: Element[];
};
}
type CarouselItemProps = {
message: Element;
idx: number;
};
const CarouselItem: React.FC<CarouselItemProps> = ({ message }) => (
<div className="sc-message--carousel-element-wrapper">
<div className="sc-message--carousel-element">
{message.image_url && (
<div
className="sc-message--carousel-element-image"
style={{ backgroundImage: `url('${message.image_url}')` }}
/>
)}
<div className="sc-message--carousel-element-description">
<h3 className="sc-message--carousel-title">{message.title}</h3>
{message.subtitle && (
<p
dangerouslySetInnerHTML={{
__html: processContent(message.subtitle),
}}
/>
)}
</div>
{message.buttons && (
<ButtonsMessage
message={{ data: { buttons: message.buttons } } as TMessage}
/>
)}
</div>
</div>
);
interface CarouselMessageProps {
messageCarousel: MessageCarousel;
}
const CarouselMessage: React.FC<CarouselMessageProps> = ({
messageCarousel,
}) => {
const { colors: allColors } = useColors();
const [activeIndex, setActiveIndex] = useState(0);
const items = messageCarousel.data.elements;
const goToPrevious = () => {
setActiveIndex(
(prevIndex) => (prevIndex + items.length - 1) % items.length,
);
};
const goToNext = () => {
setActiveIndex((prevIndex) => (prevIndex + 1) % items.length);
};
const colors = allColors[messageCarousel.direction || 'received'];
return (
<div
className="sc-message--carousel"
style={{
color: colors.text,
backgroundColor: colors.bg,
}}
>
<div
className="sc-message--carousel-inner"
style={{ transform: `translateX(-${activeIndex * 100}%)` }}
>
{items.map((message, idx) => (
<CarouselItem key={idx} message={message} idx={idx} />
))}
</div>
<button
className="sc-message--carousel-control prev"
onClick={goToPrevious}
>
&#10094;
</button>
<button className="sc-message--carousel-control next" onClick={goToNext}>
&#10095;
</button>
</div>
);
};
export default CarouselMessage;

View File

@@ -0,0 +1,66 @@
.sc-message--file {
background-color: transparent !important;
border-radius: 6px;
font-weight: 300;
font-size: 14px;
line-height: 1.4;
-webkit-font-smoothing: subpixel-antialiased;
audio,
video {
max-width: 100%;
}
}
.sc-message--content.sent .sc-message--file {
word-wrap: break-word;
}
.sc-message--file-icon {
text-align: center;
margin-left: auto;
margin-right: auto;
// margin-top: 15px;
margin-bottom: 0px;
}
.sc-image {
max-width: 100%;
height: auto;
}
.sc-message--file-download {
padding: 10px 20px;
border-radius: 6px;
color: white;
text-align: center;
a {
text-decoration: none;
color: #ece7e7;
img {
vertical-align: middle;
width: 24px;
height: auto;
}
}
}
.sc-message--content.received .sc-message--file {
color: #263238;
background-color: #f4f7f9;
margin-right: 40px;
}
.sc-message--content.received .sc-message--file-download {
color: #000;
}
.sc-message--content.received .sc-message--file a {
color: rgba(43, 40, 40, 0.7);
}
.sc-message--content.received .sc-message--file a:hover {
color: #0c0c0c;
}

View File

@@ -0,0 +1,94 @@
/*
* 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).
* 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 React from 'react';
import { useTranslation } from '../../hooks/useTranslation';
import { useColors } from '../../providers/ColorProvider';
import { TMessage } from '../../types/message.types';
import FileIcon from '../icons/FileIcon';
import './FileMessage.scss';
interface FileMessageProps {
message: TMessage;
}
const FileMessage: React.FC<FileMessageProps> = ({ message }) => {
const { t } = useTranslation();
const { colors: allColors } = useColors();
const colors = allColors[message.direction || 'received'];
if (!('type' in message.data)) {
throw new Error('Unable to detect type for file message');
}
if (
message.data &&
message.data.type !== 'image' &&
message.data.type !== 'audio' &&
message.data.type !== 'video' &&
message.data.type !== 'file'
) {
throw new Error('Uknown type for file message');
}
return (
<div
className="sc-message--file"
style={{
color: colors.text,
backgroundColor: colors.bg,
}}
>
{message.data.type === 'image' && (
<div className="sc-message--file-icon">
<img src={message.data.url || ''} className="sc-image" alt="File" />
</div>
)}
{message.data.type === 'audio' && (
<div className="sc-message--file-audio">
<audio controls>
<source src={message.data.url} />
{t('messages.file_message.browser_audio_unsupport')}
</audio>
</div>
)}
{message.data.type === 'video' && (
<div className="sc-message--file-video">
<video controls width="100%">
<source src={message.data.url} />
{t('messages.file_message.browser_video_unsupport')}
</video>
</div>
)}
{message.data.type === 'file' && (
<div
className="sc-message--file-download"
style={{
color: colors.text,
backgroundColor: colors.bg,
}}
>
<a
href={message.data.url ? message.data.url : '#'}
target="_blank"
rel="noopener noreferrer"
download
>
<FileIcon />
{t('messages.file_message.download')}
</a>
</div>
)}
</div>
);
};
export default FileMessage;

View File

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

View File

@@ -0,0 +1,88 @@
/*
* 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).
* 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 React, { useEffect, useRef, useState } from 'react';
import { useColors } from '../../providers/ColorProvider';
import { useWidget } from '../../providers/WidgetProvider';
import { TMessage } from '../../types/message.types';
import './GeolocationMessage.scss';
interface GeolocationMessageProps {
message: TMessage;
}
const GeolocationMessage: React.FC<GeolocationMessageProps> = ({ message }) => {
const { colors: allColors } = useColors();
const widget = useWidget();
const [isSeen, setIsSeen] = useState(false);
const iframeRef = useRef<HTMLDivElement>(null);
useEffect(() => {
const observer = new IntersectionObserver((entries) => {
if (!isSeen && entries[0].intersectionRatio > 0) {
setIsSeen(true);
}
});
if (iframeRef.current) {
observer.observe(iframeRef.current);
}
return () => {
if (iframeRef.current) {
// eslint-disable-next-line react-hooks/exhaustive-deps
observer.unobserve(iframeRef.current);
}
};
}, [isSeen]);
useEffect(() => {
if (isSeen && widget && widget.scroll > 85) {
widget.scroll = 101;
}
}, [isSeen, widget]);
if (!('coordinates' in message.data)) {
throw new Error('Unable to find coordinates');
}
const coordinates = message.data?.coordinates || { lat: 0.0, lng: 0.0 };
const openStreetMapUrl = `https://www.openstreetmap.org/export/embed.html?bbox=${
coordinates.lng - 0.1
},${coordinates.lat - 0.1},${coordinates.lng + 0.1},${
coordinates.lat + 0.1
}&layer=mapnik&marker=${coordinates.lat},${coordinates.lng}`;
const colors = allColors[message.direction || 'received'];
return (
<div
className="sc-message--location"
style={{
color: colors.text,
backgroundColor: colors.bg,
}}
ref={iframeRef}
>
{isSeen && (
<iframe
loading="lazy"
frameBorder="0"
scrolling="no"
marginHeight={0}
marginWidth={0}
src={openStreetMapUrl}
className="sc-message-map"
/>
)}
</div>
);
};
export default GeolocationMessage;

View File

@@ -0,0 +1,53 @@
.sc-message--list {
border-radius: 10px;
width: 256px;
.sc-message--list-element {
position: relative;
border-bottom: 1px solid;
padding: 1rem;
.sc-message--list-element-content {
width: 100%;
display: block;
&.large {
width: 100%;
display: block;
margin: 0;
.sc-message--list-element-image {
background-size: cover;
height: auto;
border-radius: 10px 10px 0 0;
}
.sc-message--list-element-description {
color: #fff;
border-radius: 10px 10px 0 0;
background: rgba(0, 0, 0, 0.5);
}
}
&.compact {
padding: 0.5rem 0;
}
.sc-message--list-element-title {
font-size: 1.125rem;
font-weight: 600;
margin: 0;
white-space: pre-line;
}
.sc-message--list-element-image {
background-position: center;
background-repeat: no-repeat;
background-size: cover;
position: relative;
width: 100%;
height: 117px;
}
}
}
}

View File

@@ -0,0 +1,118 @@
/*
* 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).
* 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 React from 'react';
import ButtonsMessage from './ButtonMessage';
import { useColors } from '../../providers/ColorProvider';
import { TMessage } from '../../types/message.types';
import './ListMessage.scss';
interface ListMessageProps {
messageList: TMessage;
}
const ListMessage: React.FC<ListMessageProps> = ({ messageList }) => {
const { colors: allColors } = useColors();
const processContent = (string: string) => {
let result = truncate(string, 50);
result = linebreak(string);
return result;
};
const truncate = (string: string, length: number = 100) => {
return string.length > length ? string.substr(0, length) + '...' : string;
};
const linebreak = (string: string) => {
return string.replace(/\n/g, '<br />');
};
if (!('elements' in messageList.data)) {
throw new Error('Unable to find elements');
}
const colors = allColors[messageList.direction || 'received'];
return (
<div
className="sc-message--list"
style={{
color: colors.text,
backgroundColor: colors.bg,
}}
>
{messageList.data.elements.map((message, idx) => {
const mode =
idx === 0 &&
'top_element_style' in messageList.data &&
messageList.data.top_element_style === 'large'
? 'large'
: 'compact';
return (
<div
key={idx}
className="sc-message--list-element"
style={{ borderColor: allColors.messageList.bg }}
>
<div className={`sc-message--list-element-content ${mode}`}>
{message.image_url && (
<div
className="sc-message--list-element-image"
style={{ backgroundImage: `url('${message.image_url}')` }}
>
{mode === 'large' && (
<div className="sc-message--list-element-description">
<h3 className="sc-message--title">{message.title}</h3>
{message.subtitle && (
<p
dangerouslySetInnerHTML={{
__html: processContent(message.subtitle),
}}
/>
)}
</div>
)}
</div>
)}
{mode === 'compact' && (
<div className="sc-message--list-element-description">
<h3 className="sc-message--title">{message.title}</h3>
{message.subtitle && (
<p
dangerouslySetInnerHTML={{
__html: processContent(message.subtitle),
}}
/>
)}
</div>
)}
</div>
{message.buttons && (
<ButtonsMessage
message={{ data: { buttons: message.buttons } } as TMessage}
/>
)}
</div>
);
})}
{'buttons' in messageList.data &&
Array.isArray(messageList.data.buttons) &&
messageList.data.buttons.length > 0 && (
<div className="sc-message--list-element-bottom">
<ButtonsMessage message={messageList as TMessage} />
</div>
)}
</div>
);
};
export default ListMessage;

View File

@@ -0,0 +1,7 @@
a.chatLink {
color: inherit !important;
}
p.sc-message--text-content {
margin: 0 !important;
white-space: pre-line;
}

View File

@@ -0,0 +1,63 @@
/*
* 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).
* 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 Autolinker from 'autolinker';
import React, { useEffect, useRef } from 'react';
import { useColors } from '../../providers/ColorProvider';
import { TMessage } from '../../types/message.types';
import './TextMessage.scss';
interface TextMessageProps {
message: TMessage;
}
const TextMessage: React.FC<TextMessageProps> = ({ message }) => {
const { colors: allColors } = useColors();
const messageTextRef = useRef<HTMLParagraphElement>(null);
useEffect(() => {
autoLink();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [message]);
const autoLink = () => {
if (message.direction === 'received' && messageTextRef.current) {
const text = messageTextRef.current.innerText;
messageTextRef.current.innerHTML = Autolinker.link(text, {
className: 'chatLink',
truncate: { length: 50, location: 'smart' },
});
}
};
if (!('text' in message.data)) {
throw new Error('Unable to find text.');
}
const colors = allColors[message.direction || 'received'];
return (
<div
className="sc-message--text"
style={{
color: colors.text,
backgroundColor: colors.bg,
}}
>
<p className="sc-message--text-content" ref={messageTextRef}>
{message.data.text}
</p>
</div>
);
};
export default TextMessage;

View File

@@ -0,0 +1,39 @@
.sc-typing-indicator {
text-align: center;
padding: 2px 5px;
border-radius: 6px;
width: 50px;
margin-left: 2rem;
}
.sc-typing-indicator span {
display: inline-block;
background-color: #b6b5ba;
width: 5px;
height: 5px;
border-radius: 100%;
margin-right: 3px;
animation: bob 2s infinite;
}
/* SAFARI GLITCH */
.sc-typing-indicator span:nth-child(1) {
animation-delay: -1s;
}
.sc-typing-indicator span:nth-child(2) {
animation-delay: -0.85s;
}
.sc-typing-indicator span:nth-child(3) {
animation-delay: -0.7s;
}
@keyframes bob {
10% {
transform: translateY(-10px);
background-color: #9e9da2;
}
50% {
transform: translateY(0);
background-color: #b6b5ba;
}
}

View File

@@ -0,0 +1,34 @@
/*
* 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).
* 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 React from 'react';
import { useColors } from '../../providers/ColorProvider';
import './TypingMessage.scss';
const TypingMessage: React.FC = () => {
const { colors } = useColors();
return (
<div
className="sc-typing-indicator"
style={{
color: colors.received.text,
backgroundColor: colors.received.bg,
}}
>
<span />
<span />
<span />
</div>
);
};
export default TypingMessage;

View File

@@ -0,0 +1,225 @@
/*
* 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).
* 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 { ColorState } from '../types/colors.types';
const colors: Record<string, ColorState> = {
orange: {
header: {
bg: '#E6A23D',
text: '#fff',
},
launcher: {
bg: '#E6A23D',
},
messageList: {
bg: '#fff',
},
sent: {
bg: '#E6A23D',
text: '#fff',
},
received: {
bg: '#eaeaea',
text: '#222222',
},
userInput: {
bg: '#fff',
text: '#212121',
},
button: {
bg: '#ffffff',
text: '#E6A23D',
border: '#E6A23D',
},
messageStatus: {
bg: '#E6A23D',
},
messageTime: {
text: '#9C9C9C',
},
},
red: {
header: {
bg: '#AB1251',
text: '#fff',
},
launcher: {
bg: '#AB1251',
},
messageList: {
bg: '#fff',
},
sent: {
bg: '#AB1251',
text: '#fff',
},
received: {
bg: '#eaeaea',
text: '#222222',
},
userInput: {
bg: '#fff',
text: '#212121',
},
button: {
bg: '#ffffff',
text: '#AB1251',
border: '#AB1251',
},
messageStatus: {
bg: '#AB1251',
},
messageTime: {
text: '#9C9C9C',
},
},
green: {
header: {
bg: '#ABBD49',
text: '#fff',
},
launcher: {
bg: '#ABBD49',
},
messageList: {
bg: '#fff',
},
sent: {
bg: '#4CAF50',
text: '#fff',
},
received: {
bg: '#eaeaea',
text: '#222222',
},
userInput: {
bg: '#fff',
text: '#212121',
},
button: {
bg: '#ffffff',
text: '#ABBD49',
border: '#ABBD49',
},
messageStatus: {
bg: '#ABBD49',
},
messageTime: {
text: '#9C9C9C',
},
},
blue: {
header: {
bg: '#108AA8',
text: '#ffffff',
},
launcher: {
bg: '#108AA8',
},
messageList: {
bg: '#ffffff',
},
sent: {
bg: '#108AA8',
text: '#ffffff',
},
received: {
bg: '#eaeaea',
text: '#222222',
},
userInput: {
bg: '#f4f7f9',
text: '#565867',
},
button: {
bg: '#ffffff',
text: '#108AA8',
border: '#108AA8',
},
messageStatus: {
bg: '#108AA8',
},
messageTime: {
text: '#9C9C9C',
},
},
teal: {
header: {
bg: '#279084',
text: '#ffffff',
},
launcher: {
bg: '#279084',
},
messageList: {
bg: '#ffffff',
},
sent: {
bg: '#279084',
text: '#ffffff',
},
received: {
bg: '#eaeaea',
text: '#222222',
},
userInput: {
bg: '#f4f7f9',
text: '#565867',
},
button: {
bg: '#ffffff',
text: '#279084',
border: '#279084',
},
messageStatus: {
bg: '#279084',
},
messageTime: {
text: '#9C9C9C',
},
},
dark: {
header: {
bg: '#34495e',
text: '#ecf0f1',
},
launcher: {
bg: '#34495e',
},
messageList: {
bg: '#2c3e50',
},
sent: {
bg: '#7f8c8d',
text: '#ecf0f1',
},
received: {
bg: '#95a5a6',
text: '#ecf0f1',
},
userInput: {
bg: '#34495e',
text: '#ecf0f1',
},
button: {
bg: '#2c3e50',
text: '#ecf0f1',
border: '#34495e',
},
messageStatus: {
bg: '#95a5a6',
},
messageTime: {
text: '#ffffff',
},
},
};
export default colors;

View File

@@ -0,0 +1,15 @@
/*
* 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).
* 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.
*/
export const DEFAULT_CONFIG = {
apiUrl: process.env.REACT_APP_WIDGET_API_URL || 'http://localhost:4000',
channel: process.env.REACT_APP_WIDGET_CHANNEL || 'live-chat-tester',
token: process.env.REACT_APP_WIDGET_TOKEN || 'test',
language: 'en',
};

View File

@@ -0,0 +1,666 @@
/*
* 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).
* 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.
*/
const emojiData = [
{
name: 'People',
emojis: [
'😄',
'😃',
'😀',
'😊',
'😉',
'😍',
'😘',
'😚',
'😗',
'😙',
'😜',
'😝',
'😛',
'😳',
'😁',
'😔',
'😌',
'😒',
'😞',
'😣',
'😢',
'😂',
'😭',
'😪',
'😥',
'😰',
'😅',
'😓',
'😩',
'😫',
'😨',
'😱',
'😠',
'😡',
'😤',
'😖',
'😆',
'😋',
'😷',
'😎',
'😴',
'😵',
'😲',
'😟',
'😦',
'😧',
'👿',
'😮',
'😬',
'😐',
'😕',
'😯',
'😏',
'😑',
'👲',
'👳',
'👮',
'👷',
'💂',
'👶',
'👦',
'👧',
'👨',
'👩',
'👴',
'👵',
'👱',
'👼',
'👸',
'😺',
'😸',
'😻',
'😽',
'😼',
'🙀',
'😿',
'😹',
'😾',
'👹',
'👺',
'🙈',
'🙉',
'🙊',
'💀',
'👽',
'💩',
'🔥',
'✨',
'🌟',
'💫',
'💥',
'💢',
'💦',
'💧',
'💤',
'💨',
'👂',
'👀',
'👃',
'👅',
'👄',
'👍',
'👎',
'👌',
'👊',
'✊',
'👋',
'✋',
'👐',
'👆',
'👇',
'👉',
'👈',
'🙌',
'🙏',
'👏',
'💪',
'🚶',
'🏃',
'💃',
'👫',
'👪',
'💏',
'💑',
'👯',
'🙆',
'🙅',
'💁',
'🙋',
'💆',
'💇',
'💅',
'👰',
'🙎',
'🙍',
'🙇',
'🎩',
'👑',
'👒',
'👟',
'👞',
'👡',
'👠',
'👢',
'👕',
'👔',
'👚',
'👗',
'🎽',
'👖',
'👘',
'👙',
'💼',
'👜',
'👝',
'👛',
'👓',
'🎀',
'🌂',
'💄',
'💛',
'💙',
'💜',
'💚',
'💔',
'💗',
'💓',
'💕',
'💖',
'💞',
'💘',
'💌',
'💋',
'💍',
'💎',
'👤',
'💬',
'👣',
],
},
{
name: 'Nature',
emojis: [
'🐶',
'🐺',
'🐱',
'🐭',
'🐹',
'🐰',
'🐸',
'🐯',
'🐨',
'🐻',
'🐷',
'🐽',
'🐮',
'🐗',
'🐵',
'🐒',
'🐴',
'🐑',
'🐘',
'🐼',
'🐧',
'🐦',
'🐤',
'🐥',
'🐣',
'🐔',
'🐍',
'🐢',
'🐛',
'🐝',
'🐜',
'🐞',
'🐌',
'🐙',
'🐚',
'🐠',
'🐟',
'🐬',
'🐳',
'🐎',
'🐲',
'🐡',
'🐫',
'🐩',
'🐾',
'💐',
'🌸',
'🌷',
'🍀',
'🌹',
'🌻',
'🌺',
'🍁',
'🍃',
'🍂',
'🌿',
'🌾',
'🍄',
'🌵',
'🌴',
'🌰',
'🌱',
'🌼',
'🌑',
'🌓',
'🌔',
'🌕',
'🌛',
'🌙',
'🌏',
'🌋',
'🌌',
'🌠',
'⛅',
'⛄',
'🌀',
'🌁',
'🌈',
'🌊',
],
},
{
name: 'Objects',
emojis: [
'🎍',
'💝',
'🎎',
'🎒',
'🎓',
'🎏',
'🎆',
'🎇',
'🎐',
'🎑',
'🎃',
'👻',
'🎅',
'🎄',
'🎁',
'🎋',
'🎉',
'🎊',
'🎈',
'🎌',
'🔮',
'🎥',
'📷',
'📹',
'📼',
'💿',
'📀',
'💽',
'💾',
'💻',
'📱',
'📞',
'📟',
'📠',
'📡',
'📺',
'📻',
'🔊',
'🔔',
'📢',
'📣',
'⏳',
'⌛',
'⏰',
'⌚',
'🔓',
'🔒',
'🔏',
'🔐',
'🔑',
'🔎',
'💡',
'🔦',
'🔌',
'🔋',
'🔍',
'🛀',
'🚽',
'🔧',
'🔩',
'🔨',
'🚪',
'🚬',
'💣',
'🔫',
'🔪',
'💊',
'💉',
'💰',
'💴',
'💵',
'💳',
'💸',
'📲',
'📧',
'📥',
'📤',
'📩',
'📨',
'📫',
'📪',
'📮',
'📦',
'📝',
'📄',
'📃',
'📑',
'📊',
'📈',
'📉',
'📜',
'📋',
'📅',
'📆',
'📇',
'📁',
'📂',
'📌',
'📎',
'📏',
'📐',
'📕',
'📗',
'📘',
'📙',
'📓',
'📔',
'📒',
'📚',
'📖',
'🔖',
'📛',
'📰',
'🎨',
'🎬',
'🎤',
'🎧',
'🎼',
'🎵',
'🎶',
'🎹',
'🎻',
'🎺',
'🎷',
'🎸',
'👾',
'🎮',
'🃏',
'🎴',
'🀄',
'🎲',
'🎯',
'🏈',
'🏀',
'⚽',
'⚾',
'🎾',
'🎱',
'🎳',
'⛳',
'🏁',
'🏆',
'🎿',
'🏂',
'🏊',
'🏄',
'🎣',
'🍵',
'🍶',
'🍺',
'🍻',
'🍸',
'🍹',
'🍷',
'🍴',
'🍕',
'🍔',
'🍟',
'🍗',
'🍖',
'🍝',
'🍛',
'🍤',
'🍱',
'🍣',
'🍥',
'🍙',
'🍘',
'🍚',
'🍜',
'🍲',
'🍢',
'🍡',
'🍳',
'🍞',
'🍩',
'🍮',
'🍦',
'🍨',
'🍧',
'🎂',
'🍰',
'🍪',
'🍫',
'🍬',
'🍭',
'🍯',
'🍎',
'🍏',
'🍊',
'🍒',
'🍇',
'🍉',
'🍓',
'🍑',
'🍈',
'🍌',
'🍍',
'🍠',
'🍆',
'🍅',
'🌽',
],
},
{
name: 'Places',
emojis: [
'🏠',
'🏡',
'🏫',
'🏢',
'🏣',
'🏥',
'🏦',
'🏪',
'🏩',
'🏨',
'💒',
'⛪',
'🏬',
'🌇',
'🌆',
'🏯',
'🏰',
'⛺',
'🏭',
'🗼',
'🗾',
'🗻',
'🌄',
'🌅',
'🌃',
'🗽',
'🌉',
'🎠',
'🎡',
'⛲',
'🎢',
'🚢',
'⛵',
'🚤',
'🚀',
'💺',
'🚉',
'🚄',
'🚅',
'🚇',
'🚃',
'🚌',
'🚙',
'🚗',
'🚕',
'🚚',
'🚨',
'🚓',
'🚒',
'🚑',
'🚲',
'💈',
'🚏',
'🎫',
'🚥',
'🚧',
'🔰',
'⛽',
'🏮',
'🎰',
'🗿',
'🎪',
'🎭',
'📍',
'🚩',
],
},
{
name: 'Symbols',
emojis: [
'🔟',
'🔢',
'🔣',
'🔠',
'🔡',
'🔤',
'🔼',
'🔽',
'⏪',
'⏩',
'⏫',
'⏬',
'🆗',
'🆕',
'🆙',
'🆒',
'🆓',
'🆖',
'📶',
'🎦',
'🈁',
'🈯',
'🈳',
'🈵',
'🈴',
'🈲',
'🉐',
'🈹',
'🈺',
'🈶',
'🈚',
'🚻',
'🚹',
'🚺',
'🚼',
'🚾',
'🚭',
'🈸',
'🉑',
'🆑',
'🆘',
'🆔',
'🚫',
'🔞',
'⛔',
'❎',
'✅',
'💟',
'🆚',
'📳',
'📴',
'🆎',
'💠',
'⛎',
'🔯',
'🏧',
'💹',
'💲',
'💱',
'❌',
'❗',
'❓',
'❕',
'❔',
'⭕',
'🔝',
'🔚',
'🔙',
'🔛',
'🔜',
'🔃',
'🕛',
'🕐',
'🕑',
'🕒',
'🕓',
'🕔',
'🕕',
'🕖',
'🕗',
'🕘',
'🕙',
'🕚',
'',
'',
'➗',
'💮',
'💯',
'🔘',
'🔗',
'➰',
'🔱',
'🔺',
'🔲',
'🔳',
'🔴',
'🔵',
'🔻',
'⬜',
'⬛',
'🔶',
'🔷',
'🔸',
'🔹',
],
},
];
export default emojiData;

View File

@@ -0,0 +1,53 @@
/*
* 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).
* 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 { useEffect, useState } from 'react';
type UseSocketGetQueryReturnType<T> = {
data: T | null;
isLoading: boolean;
error: string | null;
isError: boolean;
};
function useGetQuery<T>(url: string): UseSocketGetQueryReturnType<T> {
const [data, setData] = useState<T | null>(null);
const [isLoading, setIsLoading] = useState<boolean>(false);
const [error, setError] = useState<string | null>(null);
const [isError, setIsError] = useState<boolean>(false);
useEffect(() => {
const fetchData = async () => {
setIsLoading(true);
setIsError(false);
try {
const response = await fetch(url);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const json = await response.json();
setData(json);
} catch (error) {
setError((error as Error).message);
setIsError(true);
} finally {
setIsLoading(false);
}
};
fetchData();
}, [url]);
return { data, isLoading, error, isError };
}
export default useGetQuery;

View File

@@ -0,0 +1,55 @@
/*
* 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).
* 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 { useEffect, useState } from 'react';
import { useSocket } from '../providers/SocketProvider';
type UseSocketGetQueryReturnType<T> = {
data: T | null;
isLoading: boolean;
error: string | null;
isError: boolean;
};
function useSocketGetQuery<T>(url: string): UseSocketGetQueryReturnType<T> {
const { socket } = useSocket();
const [data, setData] = useState<T | null>(null);
const [isLoading, setIsLoading] = useState<boolean>(false);
const [error, setError] = useState<string | null>(null);
const [isError, setIsError] = useState<boolean>(false);
useEffect(() => {
const fetchData = async () => {
setIsLoading(true);
setIsError(false);
try {
const response = await socket.get<T>(url);
if (response.statusCode < 200 || response.statusCode > 299) {
throw new Error(`HTTP error! status: ${response.statusCode}`);
}
setData(response.body);
} catch (error) {
setError((error as Error).message);
setIsError(true);
} finally {
setIsLoading(false);
}
};
fetchData();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [url]);
return { data, isLoading, error, isError };
}
export default useSocketGetQuery;

View File

@@ -0,0 +1,50 @@
/*
* 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).
* 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 { useCallback } from 'react';
import { useTranslations } from '../providers/TranslationProvider';
// Define a recursive interface for nested objects
interface NestedTranslation {
[key: string]: string | NestedTranslation;
}
const getNestedTranslation = (
obj: NestedTranslation,
path: string,
): string | undefined => {
return path
.split('.')
.reduce((acc: NestedTranslation | string | undefined, part) => {
if (typeof acc === 'object' && acc !== null) {
return acc[part];
}
return undefined;
}, obj) as string | undefined;
};
export const useTranslation = () => {
const { translations, language } = useTranslations();
const t = useCallback(
(key: string, variables: Record<string, string> = {}): string => {
const translation =
getNestedTranslation(translations[language], key) || key;
return translation.replace(
/{(\w+)}/g,
(_, v) => variables[v] || `{${v}}`,
);
},
[language, translations],
);
return { t };
};

3
widget/src/index.css Normal file
View File

@@ -0,0 +1,3 @@
:root {
}

28
widget/src/main.tsx Normal file
View File

@@ -0,0 +1,28 @@
/*
* Copyright © 2024 Hexastack. All rights reserved.
*
* Licensed under the GNU Affero General Public License v3.0 (AGPLv3) with the following additional terms:
* 1. The name "Hexabot" is a trademark of Hexastack. You may not use this name in derivative works without express written permission.
* 2. All derivative works must include clear attribution to the original creator and software, Hexastack and Hexabot, in a prominent location (e.g., in the software's "About" section, documentation, and README file).
* 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 React from 'react';
import ReactDOM from 'react-dom/client';
import ChatWidget from './ChatWidget.tsx';
import './index.css';
ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode>
<ChatWidget
{...{
apiUrl: process.env.REACT_APP_WIDGET_API_URL || 'http://localhost:4000',
channel: process.env.REACT_APP_WIDGET_CHANNEL || 'offline',
token: process.env.REACT_APP_WIDGET_TOKEN || 'token123',
language: 'en',
}}
/>
</React.StrictMode>,
);

View File

@@ -0,0 +1,444 @@
/*
* 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).
* 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 React, {
createContext,
ReactNode,
SyntheticEvent,
useCallback,
useContext,
useEffect,
useState,
} from 'react';
import { useConfig } from './ConfigProvider';
import { useSettings } from './SettingsProvider';
import { useSocket, useSubscribe } from './SocketProvider';
import { useWidget } from './WidgetProvider';
import { StdEventType } from '../types/chat-io-messages.types';
import {
Direction,
IPayload,
ISubscriber,
ISuggestion,
QuickReplyType,
TEvent,
TMessage,
TPostMessageEvent,
} from '../types/message.types';
import { ConnectionState, OutgoingMessageState } from '../types/state.types';
interface Participant {
id: string;
name: string;
foreign_id?: string;
imageUrl?: string;
}
interface ChatContextType {
/**
* List of participants involved in the chat.
*/
participants: Participant[];
setParticipants: (participants: Participant[]) => void;
/**
* Represents the state of an outgoing message.
* 0 = sent
* 1 = sending
* 2 = uploading
*/
outgoingMessageState: OutgoingMessageState;
setOutgoingMessageState: (state: OutgoingMessageState) => void;
/**
* Represents the connection state of the chat.
* 0 = Disconnected
* 1 = Want to connect
* 2 = Trying to connect
* 3 = Connected
*/
connectionState: ConnectionState;
setConnectionState: (state: ConnectionState) => void;
/**
* Array of messages exchanged in the chat.
*/
messages: TMessage[];
setMessages: (messages: TMessage[]) => void;
/**
* List of suggestions available in the chat context.
*/
suggestions: ISuggestion[];
setSuggestions: (suggestions: ISuggestion[]) => void;
/**
* Indicator of whether typing indicators are visible.
*/
showTypingIndicator: boolean;
setShowTypingIndicator: (show: boolean) => void;
/**
* The latest new IO (coming from websocket) message event or null if none.
*/
newIOMessage: TEvent | null;
setNewIOMessage: (IOMessage: TEvent | null) => void;
/**
* The count of new messages since the last read. This is mainly used to show a badge on the chat icon.
*/
newMessagesCount: number;
setNewMessagesCount: (count: number) => void;
/**
* URL for a webview, if applicable in the chat context.
*/
webviewUrl: string;
setWebviewUrl: (url: string) => void;
/**
* The current message being composed.
*/
message: string;
setMessage: (message: string) => void;
/**
* @TODO: Payload is only being set but not read anywhere. Why?
*/
payload: IPayload | null;
setPayload: (p: IPayload | null) => void;
/**
* The file attached to the message, if any.
*/
file: File | null;
setFile: (f: File | null) => void;
/**
* Function to send a message or event in the chat.
* @param event - The synthetic event triggering the send action.
* @param source - The source of the message.
* @param data - The data associated with the message or event.
*/
send: ({
event,
source,
data,
}: {
event: SyntheticEvent;
source: string;
data: TPostMessageEvent;
}) => void;
/**
* Function called to trigger a get request to subscribe :
* 1. Send user informations (firstname and lastname)
* 2. Get messaging history and the full subscriber object
* @param firstName
* @param lastName
*/
handleSubscription: (firstName?: string, lastName?: string) => void;
}
const defaultCtx: ChatContextType = {
participants: [
{
id: 'chatbot',
name: 'Hexabot',
foreign_id: 'chatbot',
imageUrl: '',
},
],
setParticipants: () => {},
outgoingMessageState: 0,
setOutgoingMessageState: () => {},
connectionState: 0,
setConnectionState: () => {},
messages: [],
setMessages: () => {},
suggestions: [],
setSuggestions: () => {},
showTypingIndicator: false,
setShowTypingIndicator: () => {},
newIOMessage: null,
setNewIOMessage: () => {},
newMessagesCount: 0,
setNewMessagesCount: () => {},
webviewUrl: '',
setWebviewUrl: () => {},
message: '',
setMessage: () => {},
payload: null,
setPayload: () => {},
file: null,
setFile: () => {},
send: () => {},
handleSubscription: () => {},
};
const ChatContext = createContext<ChatContextType>(defaultCtx);
const ChatProvider: React.FC<{
wantToConnect?: () => void;
defaultConnectionState?: ConnectionState;
children: ReactNode;
}> = ({ wantToConnect, defaultConnectionState = 0, children }) => {
const config = useConfig();
const settings = useSettings();
const { screen, setScreen } = useWidget();
const { setScroll, syncState, isOpen } = useWidget();
const socketCtx = useSocket();
const [participants, setParticipants] = useState<Participant[]>(
defaultCtx.participants,
);
const [connectionState, setConnectionState] = useState<ConnectionState>(
defaultConnectionState,
);
const [messages, setMessages] = useState<TMessage[]>([]);
const [newMessagesCount, updateNewMessagesCount] = useState<number>(
defaultCtx.newMessagesCount,
);
const [showTypingIndicator, setShowTypingIndicator] = useState(
defaultCtx.showTypingIndicator,
);
const [suggestions, setSuggestions] = useState<ISuggestion[]>(
defaultCtx.suggestions,
);
const [newIOMessage, setNewIOMessage] = useState<TEvent | null>(
defaultCtx.newIOMessage,
);
const [message, setMessage] = useState<string>(defaultCtx.message);
const [outgoingMessageState, setOutgoingMessageState] =
useState<OutgoingMessageState>(defaultCtx.outgoingMessageState);
const [payload, setPayload] = useState<IPayload | null>(defaultCtx.payload);
const [file, setFile] = useState<File | null>(defaultCtx.file);
const [webviewUrl, setWebviewUrl] = useState<string>(defaultCtx.webviewUrl);
const updateConnectionState = (state: ConnectionState) => {
setConnectionState(state);
state === ConnectionState.wantToConnect && wantToConnect && wantToConnect();
state === ConnectionState.connected &&
settings.alwaysScrollToBottom &&
setScroll(101);
};
const handleNewIOMessage = (newIOMessage: TEvent | null) => {
setNewIOMessage(newIOMessage);
if (
newIOMessage &&
'type' in newIOMessage &&
newIOMessage.type === 'typing'
) {
return showTypingIndicator === true;
}
setShowTypingIndicator(false);
if (
newIOMessage &&
'mid' in newIOMessage &&
!messages.find((msg) => newIOMessage.mid === msg.mid)
) {
if ('author' in newIOMessage) {
newIOMessage.direction =
newIOMessage.author === participants[1].foreign_id ||
newIOMessage.author === participants[1].id
? Direction.sent
: Direction.received;
newIOMessage.read = true;
newIOMessage.delivery = true;
}
messages.push(newIOMessage as TMessage);
setScroll(0);
}
if (
newIOMessage &&
'data' in newIOMessage &&
'quick_replies' in newIOMessage.data
) {
setSuggestions(
(newIOMessage.data.quick_replies || []).map(
(qr) =>
({
content_type: QuickReplyType.text,
text: qr.title,
payload: qr.payload,
} as ISuggestion),
),
);
} else {
setSuggestions([]);
}
isOpen || updateNewMessagesCount(newMessagesCount + 1);
settings.alwaysScrollToBottom && setScroll(101); // @hack
setOutgoingMessageState(OutgoingMessageState.sent);
};
const handleSend = async ({
data,
}: {
event: SyntheticEvent;
source: string;
data: TPostMessageEvent;
}) => {
setOutgoingMessageState(
data.type === 'file'
? OutgoingMessageState.uploading
: OutgoingMessageState.sending,
);
setMessage('');
const sentMessage = await socketCtx.socket.post<TMessage>(
`/webhook/${config.channel}/?verification_token=${config.token}`,
{
data: {
...data,
author: data.author ?? participants[1].id,
},
},
);
handleNewIOMessage(sentMessage.body);
};
const handleSubscription = useCallback(
async (firstName?: string, lastName?: string) => {
try {
setConnectionState(2);
const { body } = await socketCtx.socket.get<{
messages: TMessage[];
profile: ISubscriber;
}>(
`/webhook/${config.channel}/?verification_token=${config.token}&first_name=${firstName}&last_name=${lastName}`,
);
const { messages, profile } = body;
localStorage.setItem('profile', JSON.stringify(profile));
// @TODO : condition mix on id VS foreign_id
messages.forEach((message) => {
const direction =
message.author === profile.foreign_id ||
message.author === profile.id
? Direction.sent
: Direction.received;
message.direction = direction;
if (message.direction === Direction.sent) {
message.read = true;
message.delivery = false;
}
});
setMessages(messages);
setParticipants([
...participants,
{
id: profile.foreign_id,
foreign_id: profile.foreign_id,
name: `${profile.first_name} ${profile.last_name}`,
},
]);
setConnectionState(3);
setScreen('chat');
} catch (e) {
// eslint-disable-next-line no-console
console.error('Unable to subscribe user', e);
setScreen('prechat');
setConnectionState(0);
}
},
// eslint-disable-next-line react-hooks/exhaustive-deps
[
participants,
setConnectionState,
setMessages,
setParticipants,
setScreen,
socketCtx,
],
);
useSubscribe<TMessage>(StdEventType.message, handleNewIOMessage);
useSubscribe<boolean>(StdEventType.typing, setShowTypingIndicator);
const updateWebviewUrl = (url: string) => {
if (url) {
setWebviewUrl(url);
setScreen('webview');
} else {
setScreen('chat');
}
};
useEffect(() => {
if (syncState && isOpen) {
updateNewMessagesCount(0);
}
}, [syncState, isOpen]);
useEffect(() => {
if (screen === 'chat' && connectionState === ConnectionState.connected) {
handleSubscription();
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
useEffect(() => {
// @TODO : enhance the participants logic
const newParticipants = [...participants];
newParticipants[0].imageUrl = settings.avatarUrl;
setParticipants(newParticipants);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [settings.avatarUrl]);
const contextValue: ChatContextType = {
participants,
setParticipants,
outgoingMessageState,
setOutgoingMessageState,
connectionState,
setConnectionState: updateConnectionState,
messages: messages.sort((a, b) => {
const aDate = Date.parse(a.createdAt);
const bDate = Date.parse(b.createdAt);
return +new Date(aDate) - +new Date(bDate);
}),
setMessages,
newMessagesCount,
setNewMessagesCount: updateNewMessagesCount,
newIOMessage,
setNewIOMessage,
send: handleSend,
showTypingIndicator: settings.showTypingIndicator && showTypingIndicator,
setShowTypingIndicator,
suggestions,
setSuggestions,
webviewUrl,
setWebviewUrl: updateWebviewUrl,
payload,
setPayload,
file,
setFile,
message,
setMessage,
handleSubscription,
};
return (
<ChatContext.Provider value={contextValue}>{children}</ChatContext.Provider>
);
};
export const useChat = () => {
const context = useContext(ChatContext);
if (!context) {
throw new Error('useChat must be used within a ChatContext');
}
return context;
};
export default ChatProvider;

View File

@@ -0,0 +1,43 @@
/*
* 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).
* 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 React, { createContext, ReactNode, useContext } from 'react';
import { useSettings } from './SettingsProvider';
import colors from '../constants/colors';
import { ColorState } from '../types/colors.types';
const initialState: ColorState = colors['orange'];
const ColorContext = createContext<{
colors: ColorState;
}>({
colors: initialState,
});
export const ColorProvider: React.FC<{ children: ReactNode }> = ({
children,
}) => {
const settings = useSettings();
return (
<ColorContext.Provider value={{ colors: colors[settings.color] }}>
{children}
</ColorContext.Provider>
);
};
export const useColors = () => {
const context = useContext(ColorContext);
if (!context) {
throw new Error('useColors must be used within a ColorProvider');
}
return context;
};

View File

@@ -0,0 +1,52 @@
/*
* 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).
* 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 React, { createContext, ReactNode, useContext, useRef } from 'react';
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;
};
// Create a context with a specific type, providing better type-checking
const ConfigContext = createContext<Config>(DEFAULT_CONFIG);
export const ConfigProvider: React.FC<{
apiUrl?: string;
token?: string;
channel?: string;
language?: string;
children: ReactNode;
}> = ({ children, ...providedConfig }) => {
const config = useRef<Config>({
...DEFAULT_CONFIG,
...providedConfig,
});
return (
<ConfigContext.Provider value={config.current}>
{children}
</ConfigContext.Provider>
);
};
export const useConfig = () => {
const context = useContext(ConfigContext);
if (!context) {
throw new Error('useConfig must be used within a ConfigProvider');
}
return context;
};

View File

@@ -0,0 +1,45 @@
/*
* 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).
* 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 React, { createContext, ReactNode, useEffect, useState } from 'react';
import { useConfig } from './ConfigProvider';
const CookieContext = createContext({});
export const CookieProvider: React.FC<{ children: ReactNode }> = ({
children,
}) => {
const config = useConfig();
const [initialized, setInitialized] = useState(false);
const getCookie = async () => {
try {
await fetch(`${config.apiUrl}/__getcookie`, {
credentials: 'include',
});
setInitialized(true);
} catch (e) {
// eslint-disable-next-line no-console
console.warn('Unable to get cookies ...');
}
};
useEffect(() => {
if (!initialized) {
getCookie();
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
if (!initialized) {
return null;
}
return <CookieContext.Provider value={{}}>{children}</CookieContext.Provider>;
};

View File

@@ -0,0 +1,128 @@
/*
* 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).
* 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 React, {
createContext,
ReactNode,
useCallback,
useContext,
useState,
} from 'react';
import { useSubscribe } from './SocketProvider';
import { useTranslation } from '../hooks/useTranslation';
import { IMenuNode } from '../types/menu.type';
import { SessionStorage } from '../utils/sessionStorage';
type ChannelSettings = {
menu: IMenuNode[];
secret: string;
verification_token: string;
allowed_domains: string;
start_button: boolean;
input_disabled: boolean;
persistent_menu: boolean;
theme_color: string;
window_title: string;
avatar_url: string;
show_emoji: boolean;
show_file: boolean;
show_location: boolean;
allowed_upload_types: string;
greeting_message: string;
allowed_upload_size: number;
};
type ChatSettings = {
showEmoji: boolean;
showFile: boolean;
showLocation: boolean;
showTypingIndicator: boolean;
alwaysScrollToBottom: boolean;
focusOnOpen: boolean;
title: string;
titleImageUrl: string;
inputDisabled: boolean;
placeholder: string;
menu: IMenuNode[];
autoFlush: boolean;
allowedUploadTypes: string[];
allowedUploadSize: number;
color: string;
greetingMessage: string;
avatarUrl: string;
};
const defaultSettings: ChatSettings = {
showEmoji: true,
showFile: true,
showLocation: true,
showTypingIndicator: true,
alwaysScrollToBottom: true,
focusOnOpen: true,
title: 'Hexabot :)',
titleImageUrl: 'https://i.pravatar.cc/300',
inputDisabled: false,
placeholder: 'Write something...',
menu: [],
autoFlush: true,
allowedUploadTypes: ['image/gif', 'image/png', 'image/jpeg'],
allowedUploadSize: 2500000,
color: 'blue',
greetingMessage: 'Welcome !',
avatarUrl: '',
};
const SettingsContext = createContext<ChatSettings>(defaultSettings);
interface ChatSettingsProviderProps {
children: ReactNode;
}
export const SettingsProvider: React.FC<ChatSettingsProviderProps> = ({
children,
}) => {
const { t } = useTranslation();
const defaultOrSavedSettings =
SessionStorage.getItem<ChatSettings>('settings');
const [settings, setSettingsState] = useState(
defaultOrSavedSettings || defaultSettings,
);
const setSettings = useCallback((settings: ChatSettings) => {
SessionStorage.setItem('settings', settings);
setSettingsState(settings);
}, []);
useSubscribe('settings', (settings: ChannelSettings) => {
setSettings({
...defaultSettings,
showEmoji: settings.show_emoji,
showFile: settings.show_file,
showLocation: settings.show_location,
title: settings.window_title,
titleImageUrl: settings.avatar_url,
menu: settings.menu,
allowedUploadTypes: settings.allowed_upload_types.split(','),
allowedUploadSize: settings.allowed_upload_size,
inputDisabled: settings.input_disabled,
color: settings.theme_color,
greetingMessage: t('settings.greeting'),
placeholder: t('settings.placeholder'),
avatarUrl: settings.avatar_url,
});
});
return (
<SettingsContext.Provider value={settings}>
{children}
</SettingsContext.Provider>
);
};
export const useSettings = () => {
return useContext(SettingsContext);
};

View File

@@ -0,0 +1,88 @@
/*
* 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).
* 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 {
createContext,
PropsWithChildren,
useContext,
useEffect,
useRef,
useState,
} from 'react';
import { useConfig } from './ConfigProvider';
import { builSocketIoClient, SocketIoClient } from '../utils/SocketIoClient';
interface socketContext {
socket: SocketIoClient;
connected: boolean;
}
const socketContext = createContext<socketContext>({
socket: {} as SocketIoClient,
connected: false,
});
export const SocketProvider = (props: PropsWithChildren) => {
const config = useConfig();
const socketRef = useRef(builSocketIoClient(config));
const [connected, setConnected] = useState(false);
useEffect(() => {
socketRef.current.init({
onConnect: () => {
setConnected(true);
},
onConnectError: () => {
setConnected(false);
},
onDisconnect: () => {
setConnected(false);
},
});
}, []);
return (
<socketContext.Provider value={{ socket: socketRef.current, connected }}>
{props.children}
</socketContext.Provider>
);
};
export const useSocket = () => {
return useContext(socketContext);
};
export const useSocketConnected = () => {
const { connected } = useSocket();
return connected;
};
export const useSubscribe = <T,>(event: string, callback: (arg: T) => void) => {
const { socket } = useSocket();
useEffect(() => {
socket.on<T>(event, callback);
return () => socket.off(event, callback);
}, [event, callback, socket]);
};
export const useSocketLifecycle = () => {
const { socket } = useSocket();
useEffect(() => {
socket.connect();
return () => {
socket.disconnect();
};
}, [socket]);
};

View File

@@ -0,0 +1,61 @@
/*
* 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).
* 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 React, { createContext, useContext, useState, ReactNode } from 'react';
import { useConfig } from './ConfigProvider';
import { translations } from '../translations';
type Language = keyof typeof translations;
interface TranslationContextProps {
translations: typeof translations;
language: Language;
setLanguage: (language: Language) => void;
}
interface TranslationProviderProps {
children: ReactNode;
}
const TranslationContext = createContext<TranslationContextProps | undefined>(
undefined,
);
export const TranslationProvider: React.FC<TranslationProviderProps> = ({
children,
}) => {
const config = useConfig();
const initialLanguage = config.language;
const isValidLanguage = (lang: string): lang is Language =>
lang in translations;
const [language, setLanguage] = useState<Language>(
isValidLanguage(initialLanguage) ? initialLanguage : 'en',
);
return (
<TranslationContext.Provider
value={{ translations, language, setLanguage }}
>
{children}
</TranslationContext.Provider>
);
};
export const useTranslations = () => {
const context = useContext(TranslationContext);
if (!context) {
throw new Error(
'useTranslationContext must be used within a TranslationProvider',
);
}
return context;
};

View File

@@ -0,0 +1,97 @@
/*
* 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).
* 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 React, { createContext, ReactNode, useContext, useState } from 'react';
import { ChatScreen } from '../types/state.types';
export interface WidgetContextType {
syncState: boolean;
isOpen: boolean;
screen: ChatScreen;
scroll: number;
setSyncState: (syncState: boolean) => void;
setIsOpen: (isOpen: boolean) => void;
setScreen: (screen: ChatScreen) => void;
setScroll: (scroll: number) => void;
}
const WidgetContext = createContext<WidgetContextType | undefined>(undefined);
const WidgetProvider: React.FC<{
onOpen?: () => void;
onClose?: () => void;
onScrollToTop?: () => void;
defaultScreen?: ChatScreen;
children: ReactNode;
}> = ({
onOpen,
onClose,
onScrollToTop,
defaultScreen = 'prechat',
children,
}) => {
const [syncState, setSyncState] = useState<boolean>(true);
const [isOpen, setIsOpen] = useState<boolean>(false);
const [screen, setScreen] = useState<ChatScreen>(defaultScreen);
const [scroll, setScroll] = useState<number>(100);
const handleSetSyncState = (newState: boolean) => {
setSyncState(newState);
};
const handleSetIsOpen = (newState: boolean) => {
setIsOpen(newState);
if (syncState) {
if (newState) {
onOpen && onOpen();
} else {
onClose && onClose();
}
}
};
const handleSetScreen = (newScreen: ChatScreen) => {
setScreen(
['prechat', 'postchat', 'webview'].includes(newScreen)
? newScreen
: 'chat',
);
};
const handleSetScroll = (newScroll: number) => {
setScroll(newScroll);
if (onScrollToTop && syncState && newScroll === 0) {
onScrollToTop();
}
};
const contextValue = {
syncState,
isOpen,
screen,
scroll,
setSyncState: handleSetSyncState,
setIsOpen: handleSetIsOpen,
setScreen: handleSetScreen,
setScroll: handleSetScroll,
};
return (
<WidgetContext.Provider value={contextValue}>
{children}
</WidgetContext.Provider>
);
};
export const useWidget = () => {
const context = useContext(WidgetContext);
if (context === undefined) {
throw new Error('useWidget must be used within a WidgetProvider');
}
return context;
};
export default WidgetProvider;

View File

@@ -0,0 +1,22 @@
{
"user_subscription": {
"get_started": "Get Started",
"first_name": "First Name",
"last_name": "Last Name"
},
"settings": {
"greeting": "Welcome !",
"placeholder": "Write something...",
"connection_lost": "Connection Lost",
"back": "Back"
},
"messages": {
"file_message": {
"browser_audio_unsupport": "Browser does not support the audio element.",
"browser_video_unsupport": "Browser does not support the video element.",
"download": "Download",
"unsupported_file_type": "This file type is not supported.",
"unsupported_file_size": "This file size is not supported."
}
}
}

View File

@@ -0,0 +1,22 @@
{
"user_subscription": {
"get_started": "Commencer",
"first_name": "Prénom",
"last_name": "Nom"
},
"settings": {
"greeting": "Bienvenue !",
"placeholder": "Écrivez quelque chose...",
"connection_lost": "Connexion perdue",
"back": "Retour"
},
"messages": {
"file_message": {
"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.",
"download": "Télécharger",
"unsupported_file_type": "Ce type de fichier n'est pas pris en charge.",
"unsupported_file_size": "Cette taille de fichier n'est pas prise en charge."
}
}
}

View File

@@ -0,0 +1,14 @@
/*
* 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).
* 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 en from './en/translation.json';
import fr from './fr/translation.json';
// TypeScript will infer the types automatically here
export const translations = { en, fr } as const;

View File

@@ -0,0 +1,18 @@
/*
* 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).
* 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.
*/
export enum StdEventType {
message = 'message',
delivery = 'delivery',
read = 'read',
typing = 'typing',
follow = 'follow',
echo = 'echo',
unknown = '',
}

View File

@@ -0,0 +1,33 @@
/*
* 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).
* 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.
*/
export type ColorState = {
header: { bg?: string; text?: string };
launcher: { bg?: string };
messageList: { bg?: string };
sent: { bg?: string; text?: string };
received: { bg?: string; text?: string };
userInput: { bg?: string; text?: string };
button: { bg?: string; text?: string; border?: string };
messageStatus: { bg?: string; text?: string };
messageTime: { text?: string };
};
export type ColorAction = {
type:
| 'setPrimary'
| 'setSecondary'
| 'setText'
| 'setTextSecondary'
| 'updateComponent';
payload: {
component: keyof ColorState;
value: { bg: string; text?: string; border?: string };
};
};

Some files were not shown because too many files have changed in this diff Show More