feat: shadow dom + minor css

This commit is contained in:
Mohamed Marrouchi 2024-10-12 10:26:14 +01:00
parent a9070580bd
commit 6669361438
14 changed files with 172 additions and 112 deletions

17
widget/.npmignore Normal file
View File

@ -0,0 +1,17 @@
src
node_modules
build
public
tests
Dockerfile
.dockerignore
.eslintrc.json
index.html
package.json
tsconfig.app.json
tsconfig.json
tsconfig.node.json
vite.config.ts
dist/index.html
dist/shadow.html
vite.svg

View File

@ -1,6 +1,8 @@
# Hexabot Live Chat Widget # Hexabot Live Chat Widget
The [Hexabot](https://hexabot.ai/) Live Chat Widget is a React-based embeddable widget that allows users to integrate real-time chat functionality into their websites. It connects to the Hexabot API and facilitates seamless interaction between end-users and chatbots across multiple channels. The [Hexabot](https://hexabot.ai/) Live Chat Widget is a React-based embeddable widget that allows users to integrate real-time chat functionality into their websites. It connects to the Hexabot API and facilitates seamless interaction between end-users and chatbots across multiple channels.
[Hexabot](https://hexabot.ai/) is an open-source chatbot / agent solution that allows users to create and manage AI-powered, multi-channel, and multilingual chatbots with ease. If you would like to learn more, please visit the [official github repo](https://github.com/Hexastack/Hexabot/).
## Key Features ## Key Features
- **Real-Time Chat:** Engage in real-time conversations with users directly through your website. - **Real-Time Chat:** Engage in real-time conversations with users directly through your website.
- **Customizable:** Easily customize the widget's appearance and behavior to fit your brand and website. - **Customizable:** Easily customize the widget's appearance and behavior to fit your brand and website.
@ -68,6 +70,40 @@ Once the widget is built, you can easily embed it into any webpage. Here's an ex
``` ```
Replace the values in apiUrl and token with your configuration details. Replace the values in apiUrl and token with your configuration details.
To prevent the website css from conflicting with the chat widget css, we can leverage the shadow dom:
```js
<script crossorigin src="https://cdn.jsdelivr.net/npm/react@18/umd/react.production.min.js"></script>
<script crossorigin src="https://cdn.jsdelivr.net/npm/react-dom@18/umd/react-dom.production.min.js"></script>
<script src="<<WIDGET URL>>/hexabot-widget.umd.js"></script>
<div id="hb-chat-widget"></div>
<script>
// Create the shadow root and attach it to the widget container
const createElement = (tag, props = {}) => Object.assign(document.createElement(tag), props);
const shadowContainer = createElement("div");
document
.getElementById('hb-chat-widget')
.attachShadow({ mode: 'open' })
.append(
shadowContainer,
createElement("link", {
rel: "stylesheet",
href: "<<WIDGET URL>>/style.css"
});
);
// Render the widget inside the shadow root
ReactDOM.render(
React.createElement(HexabotWidget, {
apiUrl: 'https://api.yourdomain.com',
channel: 'offline',
token: 'token123',
}),
shadowContainer,
);
</script>
```
## Customization ## Customization
You can customize the look and feel of the chat widget by modifying the widgets scss styles or behavior. The widget allows you to: You can customize the look and feel of the chat widget by modifying the widgets scss styles or behavior. The widget allows you to:

View File

@ -1,11 +1,12 @@
{ {
"name": "hexabot-widget", "name": "hexabot-live-chat-widget",
"private": true, "version": "2.0.0-rc.2",
"version": "2.0.0",
"description": "Hexabot is a solution for creating and managing chatbots across multiple channels, leveraging AI for advanced conversational capabilities. It provides a user-friendly interface for building, training, and deploying chatbots with integrated support for various messaging platforms.", "description": "Hexabot is a solution for creating and managing chatbots across multiple channels, leveraging AI for advanced conversational capabilities. It provides a user-friendly interface for building, training, and deploying chatbots with integrated support for various messaging platforms.",
"author": "Hexastack", "author": "Hexastack",
"license": "AGPL-3.0-only", "license": "AGPL-3.0-only",
"type": "module", "type": "module",
"main": "dist/hexabot-widget.umd.js",
"style": "dist/style.css",
"scripts": { "scripts": {
"dev": "vite", "dev": "vite",
"build": "tsc && vite build", "build": "tsc && vite build",
@ -38,5 +39,9 @@
"typescript": "^5.2.2", "typescript": "^5.2.2",
"vite": "^5.3.4", "vite": "^5.3.4",
"vite-plugin-dts": "^4.0.2" "vite-plugin-dts": "^4.0.2"
},
"optionalDependencies": {
"@rollup/rollup-darwin-arm64": "^4.24.0",
"@esbuild/darwin-arm64": "^0.24.0"
} }
} }

View File

@ -18,15 +18,26 @@
<body> <body>
<div id="hb-chat-widget"></div> <div id="hb-chat-widget"></div>
<script> <script>
const el = React.createElement; // Create the shadow root and attach it to the widget container
const domContainer = document.getElementById('hb-chat-widget'); const createElement = (tag, props = {}) => Object.assign(document.createElement(tag), props);
const shadowContainer = createElement("div");
document
.getElementById('hb-chat-widget')
.attachShadow({ mode: 'open' })
.append(
shadowContainer,
createElement("link", {
rel: "stylesheet",
href: "./style.css"
})
);
ReactDOM.render( ReactDOM.render(
el(HexabotWidget, { React.createElement(HexabotWidget, {
apiUrl: 'http://localhost:4000', apiUrl: 'http://localhost:4000',
channel: 'offline', channel: 'offline',
token: 'token123', token: 'token123',
}), }),
domContainer, shadowContainer,
); );
</script> </script>
</body> </body>

View File

@ -8,7 +8,7 @@
border-bottom-left-radius: 10px; border-bottom-left-radius: 10px;
border-bottom-right-radius: 10px; border-bottom-right-radius: 10px;
transition: background-color 0.2s ease, box-shadow 0.2s ease; transition: background-color 0.2s ease, box-shadow 0.2s ease;
border-top: 1px solid #eaeaea; border: 1px solid #eaeaea;
} }
.sc-user-input--text { .sc-user-input--text {

View File

@ -137,10 +137,7 @@ const UserInput: React.FC = () => {
const uploading = outgoingMessageState === OutgoingMessageState.uploading; const uploading = outgoingMessageState === OutgoingMessageState.uploading;
return ( return (
<div <div className="sc-user-input-wrapper">
className="sc-user-input-wrapper"
style={{ fill: colors.userInput.text }}
>
{suggestions.length > 0 && <Suggestions />} {suggestions.length > 0 && <Suggestions />}
{(file || uploading) && ( {(file || uploading) && (

View File

@ -5,6 +5,7 @@
padding: 0px; padding: 0px;
margin: 0px; margin: 0px;
outline: none; outline: none;
opacity: .5;
} }
.sc-user-input--emoji-icon-wrapper:focus { .sc-user-input--emoji-icon-wrapper:focus {
@ -12,18 +13,14 @@
} }
.sc-user-input--emoji-icon { .sc-user-input--emoji-icon {
width: 18px; width: 20px;
height: 18px; height: 20px;
cursor: pointer; cursor: pointer;
align-self: center; align-self: center;
vertical-align: middle; 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-wrapper:focus .sc-user-input--emoji-icon circle, .sc-user-input--emoji-icon-wrapper:hover {
.sc-user-input--emoji-icon.active path, opacity: 1;
.sc-user-input--emoji-icon.active circle,
.sc-user-input--emoji-icon:hover path,
.sc-user-input--emoji-icon:hover circle {
filter: contrast(15%);
} }

View File

@ -1,6 +1,12 @@
.sc-user-input--file-wrapper { .sc-user-input--file-wrapper {
background: none;
border: none;
padding: 0px;
margin: 0px;
outline: none;
position: relative; position: relative;
cursor: pointer; cursor: pointer;
opacity: .5;
} }
.sc-user-input--file-icon-wrapper { .sc-user-input--file-icon-wrapper {
background: none; background: none;
@ -8,7 +14,7 @@
padding: 0px; padding: 0px;
margin: 0px; margin: 0px;
outline: none; outline: none;
cursor: pointer; cursor: pointer !important;
} }
.sc-user-input--file-icon { .sc-user-input--file-icon {
@ -19,6 +25,6 @@
vertical-align: middle; vertical-align: middle;
} }
.sc-user-input--file-icon:hover path { .sc-user-input--file-wrapper:hover {
filter: contrast(15%); opacity: 1
} }

View File

@ -4,6 +4,7 @@
padding: 0px; padding: 0px;
margin: 0px; margin: 0px;
outline: none; outline: none;
opacity: .5;
} }
.sc-user-input--location-icon-wrapper:focus { .sc-user-input--location-icon-wrapper:focus {
@ -11,20 +12,13 @@
} }
.sc-user-input--location-icon { .sc-user-input--location-icon {
width: 16px; width: 20px;
height: 16px; height: 20px;
cursor: pointer; cursor: pointer;
align-self: center; align-self: center;
vertical-align: middle; vertical-align: middle;
} }
.sc-user-input--location-icon-wrapper:focus .sc-user-input--location-icon path, .sc-user-input--location-icon-wrapper:hover {
.sc-user-input--location-icon-wrapper:focus opacity: 1;
.sc-user-input--location-icon
circle,
.sc-user-input--location-icon.active path,
.sc-user-input--location-icon.active circle,
.sc-user-input--location-icon:hover path,
.sc-user-input--location-icon:hover circle {
filter: contrast(15%);
} }

View File

@ -12,7 +12,7 @@ const EmojiIcon: FC<SVGProps<SVGSVGElement>> = ({
x = '0', x = '0',
y = '0', y = '0',
className = 'sc-user-input--emoji-icon', className = 'sc-user-input--emoji-icon',
viewBox = '0 0 37 37', viewBox = '0 0 24 24',
...rest ...rest
}) => { }) => {
return ( return (
@ -24,10 +24,11 @@ const EmojiIcon: FC<SVGProps<SVGSVGElement>> = ({
viewBox={viewBox} viewBox={viewBox}
{...rest} {...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="15.5" cy="9.5" r="1.5" />
<circle cx="12.379" cy="14.359" r="1.938" /> <circle cx="8.5" cy="9.5" r="1.5" />
<circle cx="24.371" cy="14.414" r="1.992" /> <circle cx="15.5" cy="9.5" r="1.5" />
<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" /> <circle cx="8.5" cy="9.5" r="1.5" />
<path d="M11.99 2C6.47 2 2 6.48 2 12s4.47 10 9.99 10C17.52 22 22 17.52 22 12S17.52 2 11.99 2M12 20c-4.42 0-8-3.58-8-8s3.58-8 8-8 8 3.58 8 8-3.58 8-8 8m0-2.5c2.33 0 4.32-1.45 5.12-3.5h-1.67c-.69 1.19-1.97 2-3.45 2s-2.75-.81-3.45-2H6.88c.8 2.05 2.79 3.5 5.12 3.5" />
</svg> </svg>
); );
}; };

View File

@ -12,7 +12,7 @@ const FileInputIcon: FC<SVGProps<SVGSVGElement>> = ({
x = '0', x = '0',
y = '0', y = '0',
className = 'sc-user-input--file-icon', className = 'sc-user-input--file-icon',
viewBox = '0 0 32 32', viewBox = '0 0 24 24',
...rest ...rest
}) => { }) => {
return ( return (
@ -24,10 +24,7 @@ const FileInputIcon: FC<SVGProps<SVGSVGElement>> = ({
viewBox={viewBox} viewBox={viewBox}
{...rest} {...rest}
> >
<path <path d="M16.5 6v11.5c0 2.21-1.79 4-4 4s-4-1.79-4-4V5c0-1.38 1.12-2.5 2.5-2.5s2.5 1.12 2.5 2.5v10.5c0 .55-.45 1-1 1s-1-.45-1-1V6H10v9.5c0 1.38 1.12 2.5 2.5 2.5s2.5-1.12 2.5-2.5V5c0-2.21-1.79-4-4-4S7 2.79 7 5v12.5c0 3.04 2.46 5.5 5.5 5.5s5.5-2.46 5.5-5.5V6z" />{' '}
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> </svg>
); );
}; };

View File

@ -13,7 +13,7 @@ const LocationIcon: FC<SVGProps<SVGSVGElement>> = ({
y = '0', y = '0',
className = 'sc-user-input--location-icon', className = 'sc-user-input--location-icon',
version = '1.1', version = '1.1',
viewBox = '0 0 32 32', viewBox = '0 0 24 24',
...rest ...rest
}) => { }) => {
return ( return (
@ -26,8 +26,7 @@ const LocationIcon: FC<SVGProps<SVGSVGElement>> = ({
viewBox={viewBox} viewBox={viewBox}
{...rest} {...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="M12 2C8.13 2 5 5.13 5 9c0 5.25 7 13 7 13s7-7.75 7-13c0-3.87-3.13-7-7-7m0 9.5c-1.38 0-2.5-1.12-2.5-2.5s1.12-2.5 2.5-2.5 2.5 1.12 2.5 2.5-1.12 2.5-2.5 2.5" />
<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> </svg>
); );
}; };

View File

@ -18,7 +18,7 @@
margin: 2px; margin: 2px;
cursor: pointer; cursor: pointer;
outline: 0; outline: 0;
font-size: 1rem; font-size: .75rem;
} }
} }

View File

@ -11,34 +11,34 @@ import { ColorState } from '../types/colors.types';
const colors: Record<string, ColorState> = { const colors: Record<string, ColorState> = {
orange: { orange: {
header: { header: {
bg: '#E6A23D', bg: '#FF851B',
text: '#fff', text: '#fff',
}, },
launcher: { launcher: {
bg: '#E6A23D', bg: '#FF851B',
}, },
messageList: { messageList: {
bg: '#fff', bg: '#fff',
}, },
sent: { sent: {
bg: '#E6A23D', bg: '#FF851B',
text: '#fff', text: '#fff',
}, },
received: { received: {
bg: '#eaeaea', bg: '#eaeaea',
text: '#222222', text: '#000',
}, },
userInput: { userInput: {
bg: '#fff', bg: '#fbfbfb',
text: '#212121', text: '#000',
}, },
button: { button: {
bg: '#ffffff', bg: '#ffffff',
text: '#E6A23D', text: '#FF851B',
border: '#E6A23D', border: '#FF851B',
}, },
messageStatus: { messageStatus: {
bg: '#E6A23D', bg: '#FF851B',
}, },
messageTime: { messageTime: {
text: '#9C9C9C', text: '#9C9C9C',
@ -46,139 +46,139 @@ const colors: Record<string, ColorState> = {
}, },
red: { red: {
header: { header: {
bg: '#AB1251', bg: '#FF4136',
text: '#fff', text: '#fff',
}, },
launcher: { launcher: {
bg: '#AB1251', bg: '#FF4136',
}, },
messageList: { messageList: {
bg: '#fff', bg: '#fff',
}, },
sent: { sent: {
bg: '#AB1251', bg: '#FF4136',
text: '#fff', text: '#fff',
}, },
received: { received: {
bg: '#eaeaea', bg: '#eaeaea',
text: '#222222', text: '#000',
}, },
userInput: { userInput: {
bg: '#fff', bg: '#fbfbfb',
text: '#212121', text: '#000',
}, },
button: { button: {
bg: '#ffffff', bg: '#ffffff',
text: '#AB1251', text: '#FF4136',
border: '#AB1251', border: '#FF4136',
}, },
messageStatus: { messageStatus: {
bg: '#AB1251', bg: '#FF4136',
}, },
messageTime: { messageTime: {
text: '#9C9C9C', text: '#FF4136',
}, },
}, },
green: { green: {
header: { header: {
bg: '#ABBD49', bg: '#2ECC40',
text: '#fff', text: '#fff',
}, },
launcher: { launcher: {
bg: '#ABBD49', bg: '#2ECC40',
}, },
messageList: { messageList: {
bg: '#fff', bg: '#fff',
}, },
sent: { sent: {
bg: '#4CAF50', bg: '#2ECC40',
text: '#fff', text: '#fff',
}, },
received: { received: {
bg: '#eaeaea', bg: '#eaeaea',
text: '#222222', text: '#000',
}, },
userInput: { userInput: {
bg: '#fff', bg: '#fbfbfb',
text: '#212121', text: '#000',
}, },
button: { button: {
bg: '#ffffff', bg: '#ffffff',
text: '#ABBD49', text: '#2ECC40',
border: '#ABBD49', border: '#2ECC40',
}, },
messageStatus: { messageStatus: {
bg: '#ABBD49', bg: '#2ECC40',
}, },
messageTime: { messageTime: {
text: '#9C9C9C', text: '#2ECC40',
}, },
}, },
blue: { blue: {
header: { header: {
bg: '#108AA8', bg: '#0074D9',
text: '#ffffff', text: '#fff',
}, },
launcher: { launcher: {
bg: '#108AA8', bg: '#0074D9',
}, },
messageList: { messageList: {
bg: '#ffffff', bg: '#fff',
}, },
sent: { sent: {
bg: '#108AA8', bg: '#0074D9',
text: '#ffffff', text: '#fff',
}, },
received: { received: {
bg: '#eaeaea', bg: '#eaeaea',
text: '#222222', text: '#000',
}, },
userInput: { userInput: {
bg: '#f4f7f9', bg: '#fbfbfb',
text: '#565867', text: '#000',
}, },
button: { button: {
bg: '#ffffff', bg: '#ffffff',
text: '#108AA8', text: '#0074D9',
border: '#108AA8', border: '#0074D9',
}, },
messageStatus: { messageStatus: {
bg: '#108AA8', bg: '#0074D9',
}, },
messageTime: { messageTime: {
text: '#9C9C9C', text: '#0074D9',
}, },
}, },
teal: { teal: {
header: { header: {
bg: '#279084', bg: '#1BA089',
text: '#ffffff', text: '#fff',
}, },
launcher: { launcher: {
bg: '#279084', bg: '#1BA089',
}, },
messageList: { messageList: {
bg: '#ffffff', bg: '#fff',
}, },
sent: { sent: {
bg: '#279084', bg: '#1BA089',
text: '#ffffff', text: '#fff',
}, },
received: { received: {
bg: '#eaeaea', bg: '#eaeaea',
text: '#222222', text: '#000',
}, },
userInput: { userInput: {
bg: '#f4f7f9', bg: '#fbfbfb',
text: '#565867', text: '#000',
}, },
button: { button: {
bg: '#ffffff', bg: '#ffffff',
text: '#279084', text: '#1BA089',
border: '#279084', border: '#1BA089',
}, },
messageStatus: { messageStatus: {
bg: '#279084', bg: '#1BA089',
}, },
messageTime: { messageTime: {
text: '#9C9C9C', text: '#9C9C9C',
@ -186,26 +186,26 @@ const colors: Record<string, ColorState> = {
}, },
dark: { dark: {
header: { header: {
bg: '#34495e', bg: '#000',
text: '#ecf0f1', text: '#ecf0f1',
}, },
launcher: { launcher: {
bg: '#34495e', bg: '#000',
}, },
messageList: { messageList: {
bg: '#2c3e50', bg: '#FFF',
}, },
sent: { sent: {
bg: '#7f8c8d', bg: '#000',
text: '#ecf0f1', text: '#FFF',
}, },
received: { received: {
bg: '#95a5a6', bg: '#F0F0F0',
text: '#ecf0f1', text: '#000',
}, },
userInput: { userInput: {
bg: '#34495e', bg: '#FFF',
text: '#ecf0f1', text: '#000',
}, },
button: { button: {
bg: '#2c3e50', bg: '#2c3e50',