mirror of
https://github.com/hexastack/hexabot
synced 2025-06-26 18:27:28 +00:00
feat: initial commit
This commit is contained in:
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;
|
||||
Reference in New Issue
Block a user