mirror of
https://github.com/hexastack/hexabot
synced 2025-06-26 18:27:28 +00:00
feat: initial commit
This commit is contained in:
3
widget/src/ChatWidget.css
Normal file
3
widget/src/ChatWidget.css
Normal file
@@ -0,0 +1,3 @@
|
||||
#root {
|
||||
|
||||
}
|
||||
45
widget/src/ChatWidget.tsx
Normal file
45
widget/src/ChatWidget.tsx
Normal 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;
|
||||
7
widget/src/UiChatWidget.css
Normal file
7
widget/src/UiChatWidget.css
Normal 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;
|
||||
}
|
||||
63
widget/src/UiChatWidget.tsx
Normal file
63
widget/src/UiChatWidget.tsx
Normal 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;
|
||||
1
widget/src/assets/react.svg
Normal file
1
widget/src/assets/react.svg
Normal 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 |
65
widget/src/components/ChatHeader.scss
Normal file
65
widget/src/components/ChatHeader.scss
Normal 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;
|
||||
}
|
||||
}
|
||||
54
widget/src/components/ChatHeader.tsx
Normal file
54
widget/src/components/ChatHeader.tsx
Normal 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;
|
||||
62
widget/src/components/ChatWindow.scss
Normal file
62
widget/src/components/ChatWindow.scss
Normal 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;
|
||||
}
|
||||
}
|
||||
54
widget/src/components/ChatWindow.tsx
Normal file
54
widget/src/components/ChatWindow.tsx
Normal 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;
|
||||
40
widget/src/components/ConnectionLost.scss
Normal file
40
widget/src/components/ConnectionLost.scss
Normal 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;
|
||||
}
|
||||
63
widget/src/components/ConnectionLost.tsx
Normal file
63
widget/src/components/ConnectionLost.tsx
Normal 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;
|
||||
62
widget/src/components/EmojiPicker.scss
Normal file
62
widget/src/components/EmojiPicker.scss
Normal 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);
|
||||
}
|
||||
76
widget/src/components/EmojiPicker.tsx
Normal file
76
widget/src/components/EmojiPicker.tsx
Normal 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;
|
||||
68
widget/src/components/Launcher.scss
Normal file
68
widget/src/components/Launcher.scss
Normal 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);
|
||||
}
|
||||
89
widget/src/components/Launcher.tsx
Normal file
89
widget/src/components/Launcher.tsx
Normal 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;
|
||||
21
widget/src/components/MenuItem.scss
Normal file
21
widget/src/components/MenuItem.scss
Normal 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;
|
||||
}
|
||||
63
widget/src/components/MenuItem.tsx
Normal file
63
widget/src/components/MenuItem.tsx
Normal 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">❯</span>
|
||||
)}
|
||||
</a>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default MenuItem;
|
||||
102
widget/src/components/Message.scss
Normal file
102
widget/src/components/Message.scss
Normal 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%;
|
||||
}
|
||||
}
|
||||
102
widget/src/components/Message.tsx
Normal file
102
widget/src/components/Message.tsx
Normal 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;
|
||||
8
widget/src/components/MessageStatus.scss
Normal file
8
widget/src/components/MessageStatus.scss
Normal file
@@ -0,0 +1,8 @@
|
||||
.sc--status {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
|
||||
.sc--status-read {
|
||||
margin-right: -6px;
|
||||
}
|
||||
}
|
||||
57
widget/src/components/MessageStatus.tsx
Normal file
57
widget/src/components/MessageStatus.tsx
Normal 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;
|
||||
6
widget/src/components/Messages.scss
Normal file
6
widget/src/components/Messages.scss
Normal file
@@ -0,0 +1,6 @@
|
||||
.sc-message-list {
|
||||
height: 80%;
|
||||
overflow-y: auto;
|
||||
background-size: 100%;
|
||||
padding: 40px 0px;
|
||||
}
|
||||
102
widget/src/components/Messages.tsx
Normal file
102
widget/src/components/Messages.tsx
Normal 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;
|
||||
15
widget/src/components/Suggestions.scss
Normal file
15
widget/src/components/Suggestions.scss
Normal 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;
|
||||
}
|
||||
60
widget/src/components/Suggestions.tsx
Normal file
60
widget/src/components/Suggestions.tsx
Normal 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;
|
||||
123
widget/src/components/UserInput.scss
Normal file
123
widget/src/components/UserInput.scss
Normal 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;
|
||||
}
|
||||
231
widget/src/components/UserInput.tsx
Normal file
231
widget/src/components/UserInput.tsx
Normal 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;
|
||||
45
widget/src/components/UserSubscription.scss
Normal file
45
widget/src/components/UserSubscription.scss
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
162
widget/src/components/UserSubscription.tsx
Normal file
162
widget/src/components/UserSubscription.tsx
Normal 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;
|
||||
28
widget/src/components/Webview.scss
Normal file
28
widget/src/components/Webview.scss
Normal 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;
|
||||
}
|
||||
50
widget/src/components/Webview.tsx
Normal file
50
widget/src/components/Webview.tsx
Normal 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;
|
||||
29
widget/src/components/buttons/EmojiButton.scss
Normal file
29
widget/src/components/buttons/EmojiButton.scss
Normal 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%);
|
||||
}
|
||||
76
widget/src/components/buttons/EmojiButton.tsx
Normal file
76
widget/src/components/buttons/EmojiButton.tsx
Normal 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;
|
||||
24
widget/src/components/buttons/FileButton.scss
Normal file
24
widget/src/components/buttons/FileButton.scss
Normal 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%);
|
||||
}
|
||||
43
widget/src/components/buttons/FileButton.tsx
Normal file
43
widget/src/components/buttons/FileButton.tsx
Normal 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;
|
||||
30
widget/src/components/buttons/LocationButton.scss
Normal file
30
widget/src/components/buttons/LocationButton.scss
Normal 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%);
|
||||
}
|
||||
73
widget/src/components/buttons/LocationButton.tsx
Normal file
73
widget/src/components/buttons/LocationButton.tsx
Normal 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;
|
||||
46
widget/src/components/buttons/MenuButton.scss
Normal file
46
widget/src/components/buttons/MenuButton.scss
Normal 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;
|
||||
}
|
||||
}
|
||||
141
widget/src/components/buttons/MenuButton.tsx
Normal file
141
widget/src/components/buttons/MenuButton.tsx
Normal 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)}
|
||||
>
|
||||
❮
|
||||
</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;
|
||||
23
widget/src/components/buttons/SendButton.scss
Normal file
23
widget/src/components/buttons/SendButton.scss
Normal 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%);
|
||||
}
|
||||
32
widget/src/components/buttons/SendButton.tsx
Normal file
32
widget/src/components/buttons/SendButton.tsx
Normal 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;
|
||||
41
widget/src/components/icons/BackIcon.tsx
Normal file
41
widget/src/components/icons/BackIcon.tsx
Normal 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;
|
||||
59
widget/src/components/icons/ChatIcon.tsx
Normal file
59
widget/src/components/icons/ChatIcon.tsx
Normal 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;
|
||||
37
widget/src/components/icons/CheckIcon.tsx
Normal file
37
widget/src/components/icons/CheckIcon.tsx
Normal 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;
|
||||
23
widget/src/components/icons/CloseIcon.tsx
Normal file
23
widget/src/components/icons/CloseIcon.tsx
Normal 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;
|
||||
39
widget/src/components/icons/ConnectionIcon.tsx
Normal file
39
widget/src/components/icons/ConnectionIcon.tsx
Normal 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;
|
||||
36
widget/src/components/icons/EmojiIcon.tsx
Normal file
36
widget/src/components/icons/EmojiIcon.tsx
Normal 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;
|
||||
27
widget/src/components/icons/FileIcon.tsx
Normal file
27
widget/src/components/icons/FileIcon.tsx
Normal 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;
|
||||
36
widget/src/components/icons/FileInputIcon.tsx
Normal file
36
widget/src/components/icons/FileInputIcon.tsx
Normal 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;
|
||||
67
widget/src/components/icons/LoadingIcon.tsx
Normal file
67
widget/src/components/icons/LoadingIcon.tsx
Normal 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;
|
||||
36
widget/src/components/icons/LocationIcon.tsx
Normal file
36
widget/src/components/icons/LocationIcon.tsx
Normal 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;
|
||||
37
widget/src/components/icons/MenuIcon.tsx
Normal file
37
widget/src/components/icons/MenuIcon.tsx
Normal 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;
|
||||
65
widget/src/components/icons/OpenIcon.tsx
Normal file
65
widget/src/components/icons/OpenIcon.tsx
Normal 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;
|
||||
24
widget/src/components/icons/SendIcon.tsx
Normal file
24
widget/src/components/icons/SendIcon.tsx
Normal 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;
|
||||
58
widget/src/components/messages/ButtonMessage.scss
Normal file
58
widget/src/components/messages/ButtonMessage.scss
Normal 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;
|
||||
}
|
||||
}
|
||||
84
widget/src/components/messages/ButtonMessage.tsx
Normal file
84
widget/src/components/messages/ButtonMessage.tsx
Normal 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;
|
||||
65
widget/src/components/messages/CarouselMessage.scss
Normal file
65
widget/src/components/messages/CarouselMessage.scss
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
114
widget/src/components/messages/CarouselMessage.tsx
Normal file
114
widget/src/components/messages/CarouselMessage.tsx
Normal 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}
|
||||
>
|
||||
❮
|
||||
</button>
|
||||
<button className="sc-message--carousel-control next" onClick={goToNext}>
|
||||
❯
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default CarouselMessage;
|
||||
66
widget/src/components/messages/FileMessage.scss
Normal file
66
widget/src/components/messages/FileMessage.scss
Normal 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;
|
||||
}
|
||||
94
widget/src/components/messages/FileMessage.tsx
Normal file
94
widget/src/components/messages/FileMessage.tsx
Normal 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;
|
||||
8
widget/src/components/messages/GeolocationMessage.scss
Normal file
8
widget/src/components/messages/GeolocationMessage.scss
Normal file
@@ -0,0 +1,8 @@
|
||||
.sc-message--location {
|
||||
border-radius: 6px;
|
||||
}
|
||||
.sc-message-map {
|
||||
width: 200px;
|
||||
height: 150px;
|
||||
border-radius: 6px;
|
||||
}
|
||||
88
widget/src/components/messages/GeolocationMessage.tsx
Normal file
88
widget/src/components/messages/GeolocationMessage.tsx
Normal 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;
|
||||
53
widget/src/components/messages/ListMessage.scss
Normal file
53
widget/src/components/messages/ListMessage.scss
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
118
widget/src/components/messages/ListMessage.tsx
Normal file
118
widget/src/components/messages/ListMessage.tsx
Normal 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;
|
||||
7
widget/src/components/messages/TextMessage.scss
Normal file
7
widget/src/components/messages/TextMessage.scss
Normal file
@@ -0,0 +1,7 @@
|
||||
a.chatLink {
|
||||
color: inherit !important;
|
||||
}
|
||||
p.sc-message--text-content {
|
||||
margin: 0 !important;
|
||||
white-space: pre-line;
|
||||
}
|
||||
63
widget/src/components/messages/TextMessage.tsx
Normal file
63
widget/src/components/messages/TextMessage.tsx
Normal 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;
|
||||
39
widget/src/components/messages/TypingMessage.scss
Normal file
39
widget/src/components/messages/TypingMessage.scss
Normal 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;
|
||||
}
|
||||
}
|
||||
34
widget/src/components/messages/TypingMessage.tsx
Normal file
34
widget/src/components/messages/TypingMessage.tsx
Normal 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;
|
||||
225
widget/src/constants/colors.ts
Normal file
225
widget/src/constants/colors.ts
Normal 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;
|
||||
15
widget/src/constants/defaultConfig.ts
Normal file
15
widget/src/constants/defaultConfig.ts
Normal 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',
|
||||
};
|
||||
666
widget/src/constants/emojiData.ts
Normal file
666
widget/src/constants/emojiData.ts
Normal 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;
|
||||
53
widget/src/hooks/useGetQuery.tsx
Normal file
53
widget/src/hooks/useGetQuery.tsx
Normal 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;
|
||||
55
widget/src/hooks/useSocketGetQuery.tsx
Normal file
55
widget/src/hooks/useSocketGetQuery.tsx
Normal 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;
|
||||
50
widget/src/hooks/useTranslation.tsx
Normal file
50
widget/src/hooks/useTranslation.tsx
Normal 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
3
widget/src/index.css
Normal file
@@ -0,0 +1,3 @@
|
||||
:root {
|
||||
|
||||
}
|
||||
28
widget/src/main.tsx
Normal file
28
widget/src/main.tsx
Normal 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>,
|
||||
);
|
||||
444
widget/src/providers/ChatProvider.tsx
Normal file
444
widget/src/providers/ChatProvider.tsx
Normal 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;
|
||||
43
widget/src/providers/ColorProvider.tsx
Normal file
43
widget/src/providers/ColorProvider.tsx
Normal 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;
|
||||
};
|
||||
52
widget/src/providers/ConfigProvider.tsx
Normal file
52
widget/src/providers/ConfigProvider.tsx
Normal 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;
|
||||
};
|
||||
45
widget/src/providers/CookieProvider.tsx
Normal file
45
widget/src/providers/CookieProvider.tsx
Normal 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>;
|
||||
};
|
||||
128
widget/src/providers/SettingsProvider.tsx
Normal file
128
widget/src/providers/SettingsProvider.tsx
Normal 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);
|
||||
};
|
||||
88
widget/src/providers/SocketProvider.tsx
Normal file
88
widget/src/providers/SocketProvider.tsx
Normal 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]);
|
||||
};
|
||||
61
widget/src/providers/TranslationProvider.tsx
Normal file
61
widget/src/providers/TranslationProvider.tsx
Normal 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;
|
||||
};
|
||||
97
widget/src/providers/WidgetProvider.tsx
Normal file
97
widget/src/providers/WidgetProvider.tsx
Normal 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;
|
||||
22
widget/src/translations/en/translation.json
Normal file
22
widget/src/translations/en/translation.json
Normal 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."
|
||||
}
|
||||
}
|
||||
}
|
||||
22
widget/src/translations/fr/translation.json
Normal file
22
widget/src/translations/fr/translation.json
Normal 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."
|
||||
}
|
||||
}
|
||||
}
|
||||
14
widget/src/translations/index.ts
Normal file
14
widget/src/translations/index.ts
Normal 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;
|
||||
18
widget/src/types/chat-io-messages.types.ts
Normal file
18
widget/src/types/chat-io-messages.types.ts
Normal 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 = '',
|
||||
}
|
||||
33
widget/src/types/colors.types.ts
Normal file
33
widget/src/types/colors.types.ts
Normal 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 };
|
||||
};
|
||||
};
|
||||
22
widget/src/types/io-message.types.ts
Normal file
22
widget/src/types/io-message.types.ts
Normal 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;
|
||||
}
|
||||
23
widget/src/types/menu.type.ts
Normal file
23
widget/src/types/menu.type.ts
Normal 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[];
|
||||
}
|
||||
305
widget/src/types/message.types.ts
Normal file
305
widget/src/types/message.types.ts
Normal 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;
|
||||
};
|
||||
31
widget/src/types/state.types.ts
Normal file
31
widget/src/types/state.types.ts
Normal 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';
|
||||
194
widget/src/utils/SocketIoClient.ts
Normal file
194
widget/src/utils/SocketIoClient.ts
Normal 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,
|
||||
},
|
||||
});
|
||||
22
widget/src/utils/attachment.ts
Normal file
22
widget/src/utils/attachment.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
26
widget/src/utils/sessionStorage.ts
Normal file
26
widget/src/utils/sessionStorage.ts
Normal 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
24
widget/src/utils/text.ts
Normal 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
8
widget/src/vite-env.d.ts
vendored
Normal 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.
|
||||
*/
|
||||
Reference in New Issue
Block a user