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

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 };
};
};

View File

@@ -0,0 +1,22 @@
/*
* 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 interface IOIncomingMessage<T = unknown> {
statusCode: number;
body: T;
headers: Record<string, string>;
}
export interface IOOutgoingMessage<T = unknown> {
method: 'get' | 'post' | 'put' | 'delete' | 'patch' | 'options' | 'head';
headers: Record<string, string>;
data: T;
// params: Record<string, any>;
url: string;
}

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.
*/
export enum MenuType {
web_url = 'web_url',
postback = 'postback',
nested = 'nested',
}
export interface IMenuNode {
type: MenuType;
url?: string;
title: string;
payload?: string;
_parent?: IMenuNode;
call_to_actions?: IMenuNode[];
}

View File

@@ -0,0 +1,305 @@
/*
* 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 Direction {
sent = 'sent',
received = 'received',
}
export enum QuickReplyType {
text = 'text',
location = 'location',
}
export interface IQuickReply {
title?: string;
payload?: string;
}
export interface IPayload {
text?: string;
payload?: string;
coordinates?: {
lat: number;
lng: number;
};
}
export enum FileType {
image = 'image',
video = 'video',
audio = 'audio',
file = 'file',
unknkown = 'unknown',
}
export interface ISubscriber {
id: string;
first_name: string;
last_name: string;
locale: string;
gender: string;
assignedAt?: Date | null;
lastvisit?: Date;
retainedFrom?: Date;
channel: TChannelData;
timezone?: number;
language: string;
country?: string;
foreign_id: string;
}
export enum ButtonType {
postback = 'postback',
web_url = 'web_url',
}
export type TPostBackButton = {
type: ButtonType.postback;
title: string;
payload: string;
};
export type TWebUrlButton = {
type: ButtonType.web_url;
title: string;
url: string;
messenger_extensions?: boolean;
webview_height_ratio?: 'compact' | 'tall' | 'full';
};
export type TButton = TPostBackButton | TWebUrlButton;
export type TChannelData = {
isSocket: boolean;
ipAddress: string;
agent: string;
};
export type TRequestSession = {
offline?: {
profile: ISubscriber;
isSocket: boolean;
// @TODO : not sure why we added messageQuery (long pooling ?)
messageQueue: never[];
polling: boolean;
};
};
export enum TStatusEventType {
delivery = 'delivery',
read = 'read',
typing = 'typing',
}
export enum TOutgoingMessageType {
text = 'text',
quick_reply = 'quick_reply',
postback = 'postback',
location = 'location',
file = 'file',
}
export type TEventType = TStatusEventType | TOutgoingMessageType;
export enum IncomingMessageType {
text = 'text',
buttons = 'buttons',
quick_replies = 'quick_replies',
file = 'file',
list = 'list',
carousel = 'carousel',
}
export type TOutgoingTextMessageData = { text: string };
export type TOutgoingPayloadMessageData = TOutgoingTextMessageData & {
payload: string; // Quick reply and button payload are the same
};
export type TOutgoingLocationMessageData = {
coordinates: {
lat: number;
lng: number;
};
};
export type TOutgoingAttachmentMessageData = {
type: string; // mime type in a file case
url?: string; // file url
// Only when uploaded
size?: number; // file size
name?: string;
file?: File;
};
export type TOutgoingMessageData =
| TOutgoingTextMessageData
| TOutgoingPayloadMessageData
| TOutgoingLocationMessageData
| TOutgoingAttachmentMessageData;
export type TStatusDeliveryEvent = {
type: TStatusEventType.delivery;
mid: string;
};
export type TStatusReadEvent = {
type: TStatusEventType.read;
watermark: number;
};
export type TStatusTypingEvent = {
type: TStatusEventType.typing;
};
export type TStatusEvent =
| TStatusDeliveryEvent
| TStatusReadEvent
| TStatusTypingEvent;
export type TOutgoingTextMessage = {
type: TOutgoingMessageType.text;
data: TOutgoingTextMessageData;
};
export type TOutgoingPayloadMessage = {
type: TOutgoingMessageType.postback | TOutgoingMessageType.quick_reply;
data: TOutgoingPayloadMessageData;
};
export type TOutgoingLocationMessage = {
type: TOutgoingMessageType.location;
data: TOutgoingLocationMessageData;
};
export type TOutgoingAttachmentMessage = {
type: TOutgoingMessageType.file;
data: TOutgoingAttachmentMessageData;
};
export type TOutgoingMessageBase =
| TOutgoingTextMessage
| TOutgoingPayloadMessage
| TOutgoingLocationMessage
| TOutgoingAttachmentMessage;
export type TOutgoingMessage<
T =
| TOutgoingTextMessage
| TOutgoingPayloadMessage
| TOutgoingLocationMessage
| TOutgoingAttachmentMessage,
> = T & {
mid?: string;
author?: string;
read?: boolean;
delivery?: boolean;
// Whether it's a synchronization
// This is used when message sent by the chatbot from the client side
sync?: boolean;
createdAt: string;
direction: Direction.sent;
};
export type TEvent = TIncomingMessage | TOutgoingMessage | TStatusEvent;
export interface IMessageElement {
title: string;
subtitle?: string;
image_url?: string;
default_action?: Omit<TWebUrlButton, 'title'>;
buttons?: TButton[];
}
export type TIncomingTextMessageData = { text: string };
export type TIncomingQuickRepliesMessageData = TIncomingTextMessageData & {
quick_replies: IQuickReply[];
};
export type TIncomingButtonsMessageData = TIncomingTextMessageData & {
buttons: TButton[];
};
export type TIncomingFileMessageData = {
quick_replies?: IQuickReply[];
type: FileType;
url: string;
};
export type TIncomingCarouselMessageData = {
elements: IMessageElement[];
};
export type TIncomingListMessageData = TIncomingCarouselMessageData & {
top_element_style?: 'large' | 'compact';
buttons: TButton[];
};
export type TIncomingMessageData =
| TIncomingTextMessageData
| TIncomingQuickRepliesMessageData
| TIncomingButtonsMessageData
| TIncomingFileMessageData
| TIncomingCarouselMessageData
| TIncomingListMessageData;
export type TIncomingMessageBase =
| {
type: IncomingMessageType.text;
data: TIncomingTextMessageData;
}
| {
type: IncomingMessageType.quick_replies;
data: TIncomingQuickRepliesMessageData;
}
| {
type: IncomingMessageType.buttons;
data: TIncomingButtonsMessageData;
}
| {
type: IncomingMessageType.file;
data: TIncomingFileMessageData;
}
| {
type: IncomingMessageType.carousel;
data: TIncomingCarouselMessageData;
}
| {
type: IncomingMessageType.list;
data: TIncomingListMessageData;
};
export type TIncomingMessage = TIncomingMessageBase & {
mid: string;
author: string;
read?: boolean;
delivery?: boolean;
createdAt: string;
handover: boolean;
direction: Direction.received;
};
export type TMessage = TIncomingMessage | TOutgoingMessage;
export interface ISuggestion {
text: string;
payload: string;
}
export type TPostMessageEvent<
T =
| TOutgoingTextMessage
| TOutgoingPayloadMessage
| TOutgoingLocationMessage
| TOutgoingAttachmentMessage,
> = T & {
author?: string;
};

View File

@@ -0,0 +1,31 @@
/*
* 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 OutgoingMessageState {
sent = 0,
sending = 1,
uploading = 2,
}
export enum ConnectionState {
disconnected = 0,
wantToConnect = 1,
tryingToConnect = 2,
connected = 3,
}
export type ChatScreen =
// Screen that shows up before the chat (user subscription)
| 'prechat'
// Screen that shows up after the chat is closed (not in use yet)
| 'postchat'
// Screen shows up when user clicks on a url button where there is a webview
| 'webview'
// Screen that shows the messages and text input
| 'chat';

View File

@@ -0,0 +1,194 @@
/*
* 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 { io, Socket, ManagerOptions, SocketOptions } from 'socket.io-client';
import { Config } from '../providers/ConfigProvider';
import {
IOIncomingMessage,
IOOutgoingMessage,
} from '../types/io-message.types';
type SocketIoClientConfig = Partial<ManagerOptions & SocketOptions>;
export class SocketIoClient {
/**
* Default configuration for the socket client
* @static
*/
static defaultConfig: SocketIoClientConfig = {
// Socket options
ackTimeout: 1000,
// auth: undefined,
retries: 3,
// Manager options
autoConnect: true,
// parser: undefined,
// randomizationFactor:0.5,
reconnection: true,
reconnectionAttempts: 100,
reconnectionDelay: 1000,
reconnectionDelayMax: 5000,
timeout: 20000,
// Low Level Options
addTrailingSlash: true, // eg: https://domain.path/ => https://domain.path/
// autoUnref:false, // firefox only option
// path: "/socket.io", // This is the socket path in the server, leave it as default unless changed manually in server
transports: ['websocket', 'polling'], // ["websocket","polling", "websocket"]
upgrade: true,
withCredentials: true,
};
private socket: Socket;
private config: SocketIoClientConfig;
private initialized: boolean = false;
constructor(apiUrl: string, socketConfig?: SocketIoClientConfig) {
this.config = {
...SocketIoClient.defaultConfig,
...socketConfig,
autoConnect: false,
};
const url = new URL(apiUrl);
this.socket = io(url.origin, this.config);
}
/**
* Initializes the socket client and sets up event handlers.
* @param handlers Event handlers for connection, disconnection, and connection errors
*/
public init({
onConnect,
onDisconnect,
onConnectError,
}: {
onConnect?: () => void;
onDisconnect?: (reason: string, details: unknown) => void;
onConnectError?: (error: Error) => void;
}) {
if (!this.initialized) this.socket.connect();
onConnect && this.uniqueOn('connect', onConnect);
onDisconnect && this.uniqueOn('disconnect', onDisconnect);
onConnectError && this.uniqueOn('connect_error', onConnectError);
this.initialized = true;
}
/**
* Registers an event handler for the specified event and removes any existing handlers.
* @param event The event name
* @param callback The callback function to handle the event
*/
//TODO: Fix any type
// eslint-disable-next-line @typescript-eslint/no-explicit-any
private uniqueOn(event: string, callback: (...args: any) => void) {
this.socket.off(event);
this.socket.on(event, callback);
}
/**
* Disconnects the socket client.
*/
public disconnect() {
this.socket.disconnect();
this.initialized = false;
}
/**
* Connects the socket client.
*/
public connect() {
if (!this.socket.active) this.socket.connect();
}
/**
* Registers an event handler for the specified event.
* @param event The event name
* @param callback The callback function to handle the event
*/
public on<T>(event: string, callback: (data: T) => void) {
this.socket.on(event, callback);
}
/**
* Removes an event handler for the specified event.
* @param event The event name
* @param callback The callback function to remove
*/
//TODO: Fix any type
// eslint-disable-next-line @typescript-eslint/no-explicit-any
public off(event: string, callback: (...args: any) => void) {
this.socket.off(event, callback);
}
/**
* Sends a request to the server and waits for an acknowledgment.
* @param options The request options including URL and method
* @returns The response from the server
* @throws Error if the request fails
*/
public async request<T>(
options: Pick<IOOutgoingMessage, 'url' | 'method'> &
Partial<IOOutgoingMessage>,
): Promise<IOIncomingMessage<T>> {
const response: IOIncomingMessage<T> = await this.socket.emitWithAck(
options.method,
options,
);
if (response.statusCode >= 200 && response.statusCode < 300) {
return response;
}
throw new Error(
`Request failed with status code ${response.statusCode}: ${JSON.stringify(
response.body,
)}`,
);
}
/**
* Sends a GET request to the server.
* @param url The URL to send the request to
* @param options Optional request options
* @returns The response from the server
*/
public async get<T>(
url: string,
options?: Partial<Omit<IOOutgoingMessage, 'url' | 'method' | 'body'>>,
): Promise<IOIncomingMessage<T>> {
return this.request({
method: 'get',
url,
...options,
});
}
public async post<T>(
url: string,
options: Partial<Omit<IOOutgoingMessage, 'url' | 'method'>>,
): Promise<IOIncomingMessage<T>> {
return this.request({
method: 'post',
url,
...options,
});
}
}
export const builSocketIoClient = (config: Config) =>
new SocketIoClient(config.apiUrl, {
query: {
channel: config.channel,
verification_token: config.token,
},
});

View File

@@ -0,0 +1,22 @@
/*
* 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 { FileType } from '../types/message.types';
export function getFileType(mimeType: string): FileType {
if (mimeType.startsWith('image/')) {
return FileType.image;
} else if (mimeType.startsWith('video/')) {
return FileType.video;
} else if (mimeType.startsWith('audio/')) {
return FileType.audio;
} else {
return FileType.file;
}
}

View File

@@ -0,0 +1,26 @@
/*
* 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.
*/
function setItem<T>(key: string, value: T) {
if (typeof value === 'undefined')
throw new Error('Value cannot be undefined');
sessionStorage.setItem(key, JSON.stringify(value));
return true;
}
function getItem<T>(key: string) {
const value = sessionStorage.getItem(key);
if (value === null) return null;
return JSON.parse(value) as T;
}
export const SessionStorage = { setItem, getItem };

24
widget/src/utils/text.ts Normal file
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.
*/
export const truncate = (s: string, length = 100) => {
return s.length > length ? s.substr(0, length) + '...' : s;
};
export const linebreak = (s: string) => {
return s.replace(/\n/g, '<br />');
};
export const processContent = (s: string) => {
let result = truncate(s, 50);
result = linebreak(s);
return result;
};

8
widget/src/vite-env.d.ts vendored Normal file
View File

@@ -0,0 +1,8 @@
/*
* 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.
*/