Three files are enough.
Add the markup, paste the styles and connect the API with JavaScript. The code below is complete, not a visual-only mockup.
Add the chatbot HTML
Place this markup near the end of your page, immediately before the
closing </body> tag.
If you don’t already have the full HTML structure, paste this first:
<!DOCTYPE html>
<html lang="en">
<head>
<link rel="stylesheet" href="style.css">
<!--- Add defer to the script else it will load before html and throw and error or added to the end of the page. --->
<script defer src="chatbot.js"></script>
</head>
<body>
<!---------------------- Then paste the chatbot code here. -------------------------->
</body>
</html>
<!-- Add this near the end of your <body> -->
<button
class="mx-chat-launcher"
id="mxChatLauncher"
type="button"
aria-label="Open AI assistant"
aria-expanded="false"
>
<span class="mx-chat-launcher-icon">M</span>
<span class="mx-chat-launcher-label">Chat with us</span>
</button>
<section
class="mx-chat-widget"
id="mxChatWidget"
aria-label="Mathix AI assistant"
aria-hidden="true"
>
<header class="mx-chat-header">
<div class="mx-chat-agent">
<span class="mx-chat-avatar">M</span>
<div>
<strong>Website Assistant</strong>
<small><span class="mx-chat-status-dot"></span> Powered by Mathix</small>
</div>
</div>
<div class="mx-chat-header-actions">
<button id="mxChatReset" type="button" aria-label="Reset conversation">↻</button>
<button id="mxChatClose" type="button" aria-label="Close chatbot">×</button>
</div>
</header>
<div class="mx-chat-setup" id="mxChatSetup">
<span class="mx-chat-kicker">LIVE SETUP</span>
<h2>Connect your Mathix API key</h2>
<p>Your key stays in this browser tab and is not saved by this demo.</p>
<label for="mxChatApiKey">API key</label>
<input
id="mxChatApiKey"
type="password"
placeholder="mk-your-api-key"
autocomplete="off"
>
<label for="mxChatModel">Model</label>
<select id="mxChatModel">
<option value="g-regular-1.0">g-regular-1.0</option>
<option value="g-fast-1.0">g-fast-1.0</option>
<option value="g-pro-1.0">g-pro-1.0</option>
</select>
<button class="mx-chat-connect" id="mxChatConnect" type="button">
Start chatting
</button>
<p class="mx-chat-error" id="mxChatSetupError" role="alert"></p>
</div>
<div class="mx-chat-conversation" id="mxChatConversation" hidden>
<div
class="mx-chat-messages"
id="mxChatMessages"
aria-live="polite"
aria-label="Chat messages"
></div>
<div class="mx-chat-error mx-chat-request-error" id="mxChatRequestError"></div>
<form class="mx-chat-composer" id="mxChatForm">
<textarea
id="mxChatInput"
rows="1"
placeholder="Ask a question..."
aria-label="Message"
></textarea>
<button id="mxChatSend" type="submit" aria-label="Send message">↑</button>
</form>
<p class="mx-chat-footnote">AI responses may be inaccurate. Verify important information.</p>
</div>
</section>
Style the responsive side widget
These styles position the chatbot in the bottom-right corner and convert it into a near full-width panel on small phones.
:root {
--mx-chat-primary: #0A4D3C;
--mx-chat-primary-dark: #073b2f;
--mx-chat-ink: #17182b;
--mx-chat-muted: #747789;
--mx-chat-line: #e6e7ef;
--mx-chat-soft: #eef7f4;
--mx-chat-success: #0A4D3C;
--mx-chat-danger: #EE4444;
}
.mx-chat-launcher,
.mx-chat-widget,
.mx-chat-widget * {
box-sizing: border-box;
}
.mx-chat-launcher {
position: fixed;
right: 24px;
bottom: 24px;
z-index: 9998;
min-height: 58px;
padding: 8px 18px 8px 8px;
display: inline-flex;
align-items: center;
gap: 10px;
border: 0;
border-radius: 999px;
color: #fff;
background: linear-gradient(135deg, var(--mx-chat-primary), var(--mx-chat-primary-dark));
box-shadow: 0 18px 45px rgba(10, 77, 60, 0.34);
font: 700 14px/1 system-ui, sans-serif;
cursor: pointer;
}
.mx-chat-launcher-icon,
.mx-chat-avatar {
display: grid;
place-items: center;
flex: 0 0 auto;
color: #fff;
background: rgba(255, 255, 255, 0.17);
font-weight: 800;
}
.mx-chat-launcher-icon {
width: 42px;
height: 42px;
border-radius: 50%;
}
.mx-chat-widget {
position: fixed;
right: 24px;
bottom: 96px;
z-index: 9999;
width: min(390px, calc(100vw - 32px));
height: min(650px, calc(100vh - 128px));
display: flex;
flex-direction: column;
overflow: hidden;
border: 1px solid rgba(222, 223, 236, 0.95);
border-radius: 22px;
background: #fff;
box-shadow: 0 30px 90px rgba(27, 25, 57, 0.24);
font-family: Inter, system-ui, sans-serif;
opacity: 0;
visibility: hidden;
transform: translateY(14px) scale(0.98);
transform-origin: bottom right;
transition: opacity 180ms ease, transform 180ms ease, visibility 180ms ease;
}
.mx-chat-widget.is-open {
opacity: 1;
visibility: visible;
transform: none;
}
.mx-chat-header {
min-height: 72px;
padding: 14px 15px;
display: flex;
align-items: center;
justify-content: space-between;
border-bottom: 1px solid var(--mx-chat-line);
background: linear-gradient(135deg, #0A4D3C, #073b2f);
color: #fff;
}
.mx-chat-agent {
display: flex;
align-items: center;
gap: 11px;
}
.mx-chat-avatar {
width: 40px;
height: 40px;
border-radius: 13px;
}
.mx-chat-agent strong,
.mx-chat-agent small {
display: block;
}
.mx-chat-agent strong {
font-size: 14px;
}
.mx-chat-agent small {
margin-top: 5px;
color: rgba(255, 255, 255, 0.76);
font-size: 10px;
}
.mx-chat-status-dot {
width: 7px;
height: 7px;
margin-right: 5px;
display: inline-block;
border-radius: 50%;
background: #74e5ad;
}
.mx-chat-header-actions {
display: flex;
gap: 6px;
}
.mx-chat-header-actions button {
width: 34px;
height: 34px;
border: 0;
border-radius: 10px;
color: #fff;
background: rgba(255, 255, 255, 0.12);
font: 600 19px/1 system-ui, sans-serif;
cursor: pointer;
}
.mx-chat-setup {
padding: 25px;
overflow-y: auto;
}
.mx-chat-kicker {
color: var(--mx-chat-primary);
font-size: 10px;
font-weight: 800;
letter-spacing: 0.14em;
}
.mx-chat-setup h2 {
margin: 9px 0 8px;
color: var(--mx-chat-ink);
font-size: 22px;
line-height: 1.2;
}
.mx-chat-setup > p {
margin: 0 0 18px;
color: var(--mx-chat-muted);
font-size: 13px;
line-height: 1.55;
}
.mx-chat-setup label {
display: block;
margin: 14px 0 7px;
color: var(--mx-chat-ink);
font-size: 12px;
font-weight: 700;
}
.mx-chat-setup input,
.mx-chat-setup select {
width: 100%;
height: 46px;
padding: 0 12px;
border: 1px solid var(--mx-chat-line);
border-radius: 11px;
outline: none;
background: #fbfbfd;
color: var(--mx-chat-ink);
font: 13px system-ui, sans-serif;
}
.mx-chat-setup input:focus,
.mx-chat-setup select:focus {
border-color: #EE4444;
box-shadow: 0 0 0 4px rgba(238, 68, 68, 0.1);
}
.mx-chat-connect {
width: 100%;
min-height: 48px;
margin-top: 18px;
border: 0;
border-radius: 12px;
color: #fff;
background: linear-gradient(135deg, var(--mx-chat-primary), var(--mx-chat-primary-dark));
font: 700 13px system-ui, sans-serif;
cursor: pointer;
}
.mx-chat-conversation {
min-height: 0;
flex: 1;
display: flex;
flex-direction: column;
background: #fff;
}
.mx-chat-messages {
min-height: 0;
flex: 1;
padding: 18px 15px;
display: flex;
flex-direction: column;
gap: 13px;
overflow-y: auto;
background:
radial-gradient(circle at top right, rgba(10, 77, 60, 0.07), transparent 35%),
#fff;
}
.mx-chat-message {
max-width: 84%;
display: flex;
gap: 8px;
align-items: flex-end;
}
.mx-chat-message.is-user {
align-self: flex-end;
justify-content: flex-end;
}
.mx-chat-message.is-assistant {
align-self: flex-start;
}
.mx-chat-message-avatar {
width: 27px;
height: 27px;
display: grid;
place-items: center;
flex: 0 0 auto;
border-radius: 9px;
color: #fff;
background: var(--mx-chat-primary);
font-size: 10px;
font-weight: 800;
}
.mx-chat-bubble {
padding: 11px 13px;
border-radius: 15px;
color: #505365;
background: #f1f2f6;
font-size: 13px;
line-height: 1.55;
white-space: pre-wrap;
overflow-wrap: anywhere;
}
.mx-chat-message.is-user .mx-chat-bubble {
color: #fff;
background: var(--mx-chat-primary);
border-bottom-right-radius: 5px;
}
.mx-chat-message.is-assistant .mx-chat-bubble {
border-bottom-left-radius: 5px;
}
.mx-chat-message.is-loading .mx-chat-bubble {
display: flex;
gap: 4px;
align-items: center;
}
.mx-chat-message.is-loading i {
width: 6px;
height: 6px;
border-radius: 50%;
background: #0A4D3C;
animation: mxChatTyping 900ms infinite ease-in-out;
}
.mx-chat-message.is-loading i:nth-child(2) {
animation-delay: 140ms;
}
.mx-chat-message.is-loading i:nth-child(3) {
animation-delay: 280ms;
}
@keyframes mxChatTyping {
0%, 60%, 100% { transform: translateY(0); opacity: 0.45; }
30% { transform: translateY(-4px); opacity: 1; }
}
.mx-chat-request-error,
.mx-chat-error {
min-height: 18px;
color: var(--mx-chat-danger);
font-size: 11px;
line-height: 1.4;
}
.mx-chat-request-error {
padding: 0 15px 7px;
}
.mx-chat-composer {
margin: 0 12px;
padding: 6px 6px 6px 12px;
display: flex;
align-items: flex-end;
gap: 8px;
border: 1px solid var(--mx-chat-line);
border-radius: 14px;
background: #fff;
box-shadow: 0 10px 30px rgba(26, 24, 58, 0.07);
}
.mx-chat-composer textarea {
min-height: 38px;
max-height: 110px;
padding: 9px 0 7px;
flex: 1;
resize: none;
border: 0;
outline: none;
color: var(--mx-chat-ink);
background: transparent;
font: 13px/1.45 system-ui, sans-serif;
}
.mx-chat-composer button {
width: 38px;
height: 38px;
flex: 0 0 auto;
border: 0;
border-radius: 11px;
color: #fff;
background: var(--mx-chat-primary);
font: 700 18px system-ui, sans-serif;
cursor: pointer;
}
.mx-chat-composer button:disabled {
cursor: not-allowed;
opacity: 0.55;
}
.mx-chat-footnote {
margin: 7px 12px 10px;
color: #9a9cac;
font-size: 9px;
text-align: center;
}
@media (max-width: 560px) {
.mx-chat-launcher {
right: 14px;
bottom: 14px;
}
.mx-chat-launcher-label {
display: none;
}
.mx-chat-launcher {
width: 58px;
padding: 8px;
}
.mx-chat-widget {
right: 8px;
bottom: 82px;
width: calc(100vw - 16px);
height: min(680px, calc(100dvh - 96px));
border-radius: 20px;
}
}
Connect the Mathix API
The script opens the widget, accepts an API key, sends requests, appends user and assistant messages, shows a typing state and keeps the conversation useful by sending earlier messages as context.
(() => {
"use strict";
const API_URL = "https://mathix.co/plain";
const SYSTEM_PROMPT =
"You are a helpful website assistant. Give clear, concise and friendly answers.";
const launcher = document.getElementById("mxChatLauncher");
const widget = document.getElementById("mxChatWidget");
const closeButton = document.getElementById("mxChatClose");
const resetButton = document.getElementById("mxChatReset");
const setup = document.getElementById("mxChatSetup");
const conversation = document.getElementById("mxChatConversation");
const apiKeyInput = document.getElementById("mxChatApiKey");
const modelSelect = document.getElementById("mxChatModel");
const connectButton = document.getElementById("mxChatConnect");
const setupError = document.getElementById("mxChatSetupError");
const messagesElement = document.getElementById("mxChatMessages");
const requestError = document.getElementById("mxChatRequestError");
const form = document.getElementById("mxChatForm");
const input = document.getElementById("mxChatInput");
const sendButton = document.getElementById("mxChatSend");
let apiKey = "";
let model = "g-regular-1.0";
let systemPrompt = SYSTEM_PROMPT;
let isSending = false;
let messages = [];
function openWidget() {
widget.classList.add("is-open");
widget.setAttribute("aria-hidden", "false");
launcher.setAttribute("aria-expanded", "true");
window.setTimeout(() => {
(apiKey ? input : apiKeyInput).focus();
}, 100);
}
function closeWidget() {
widget.classList.remove("is-open");
widget.setAttribute("aria-hidden", "true");
launcher.setAttribute("aria-expanded", "false");
}
function scrollToLatestMessage() {
messagesElement.scrollTop = messagesElement.scrollHeight;
}
function addMessage(role, text, options = {}) {
const message = document.createElement("div");
message.className = `mx-chat-message is-${role}`;
if (options.loading) {
message.classList.add("is-loading");
message.dataset.loading = "true";
}
if (role === "assistant") {
const avatar = document.createElement("span");
avatar.className = "mx-chat-message-avatar";
avatar.textContent = "M";
message.appendChild(avatar);
}
const bubble = document.createElement("div");
bubble.className = "mx-chat-bubble";
if (options.loading) {
bubble.innerHTML = "<i></i><i></i><i></i>";
} else {
bubble.textContent = text;
}
message.appendChild(bubble);
messagesElement.appendChild(message);
scrollToLatestMessage();
return message;
}
function removeLoadingMessage() {
messagesElement.querySelector('[data-loading="true"]')?.remove();
}
function extractAssistantText(data) {
if (typeof data === "string") return data;
const candidates = [
data?.response,
data?.output,
data?.answer,
data?.content,
data?.message,
data?.result,
data?.text,
data?.data?.response,
data?.data?.output,
data?.choices?.[0]?.message?.content,
data?.choices?.[0]?.text
];
const match = candidates.find(
value => typeof value === "string" && value.trim()
);
return match || JSON.stringify(data, null, 2);
}
function buildConversationPrompt() {
const transcript = messages
.slice(-12)
.map(message => `${message.role.toUpperCase()}: ${message.content}`)
.join("\n\n");
return [
"Continue the conversation below.",
"Use the previous messages for context and do not repeat the greeting.",
"",
transcript,
"",
"ASSISTANT:"
].join("\n");
}
function resetConversation() {
messages = [];
messagesElement.innerHTML = "";
requestError.textContent = "";
if (apiKey) {
const welcome =
"Hi! I am your Mathix-powered website assistant. How can I help?";
messages.push({ role: "assistant", content: welcome });
addMessage("assistant", welcome);
}
}
function connectChatbot() {
setupError.textContent = "";
const enteredKey = apiKeyInput.value.trim();
if (!enteredKey) {
setupError.textContent = "Enter your Mathix API key first.";
apiKeyInput.focus();
return;
}
apiKey = enteredKey;
model = modelSelect.value;
setup.hidden = true;
conversation.hidden = false;
resetConversation();
input.focus();
}
async function sendMessage(text) {
if (!text || isSending) return;
requestError.textContent = "";
isSending = true;
sendButton.disabled = true;
messages.push({ role: "user", content: text });
addMessage("user", text);
const loadingMessage = addMessage("assistant", "", { loading: true });
try {
const response = await fetch(API_URL, {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify({
API_KEY: apiKey,
input: buildConversationPrompt(),
model,
system: systemPrompt
})
});
const raw = await response.text();
let data;
try {
data = raw ? JSON.parse(raw) : {};
} catch {
data = { response: raw };
}
if (!response.ok) {
throw new Error(
extractAssistantText(data) || `Request failed with ${response.status}`
);
}
const answer = extractAssistantText(data);
loadingMessage.remove();
messages.push({ role: "assistant", content: answer });
addMessage("assistant", answer);
} catch (error) {
removeLoadingMessage();
requestError.textContent =
error instanceof Error
? error.message
: "The chatbot could not reach the Mathix API.";
} finally {
isSending = false;
sendButton.disabled = false;
input.focus();
}
}
launcher.addEventListener("click", () => {
widget.classList.contains("is-open") ? closeWidget() : openWidget();
});
closeButton.addEventListener("click", closeWidget);
connectButton.addEventListener("click", connectChatbot);
resetButton.addEventListener("click", resetConversation);
form.addEventListener("submit", event => {
event.preventDefault();
const text = input.value.trim();
if (!text) return;
input.value = "";
input.style.height = "";
sendMessage(text);
});
input.addEventListener("keydown", event => {
if (event.key === "Enter" && !event.shiftKey) {
event.preventDefault();
form.requestSubmit();
}
});
input.addEventListener("input", () => {
input.style.height = "auto";
input.style.height = `${Math.min(input.scrollHeight, 110)}px`;
});
window.MathixChatWidget = {
open: openWidget,
close: closeWidget,
reset: resetConversation,
connect(options = {}) {
const nextKey = String(options.apiKey || "").trim();
const nextModel = String(options.model || model).trim();
const nextSystemPrompt = String(options.system || SYSTEM_PROMPT).trim();
if (nextKey) {
apiKeyInput.value = nextKey;
}
if (nextModel) {
modelSelect.value = nextModel;
}
systemPrompt = nextSystemPrompt || SYSTEM_PROMPT;
openWidget();
connectChatbot();
return Boolean(apiKey);
},
setSystemPrompt(value) {
systemPrompt = String(value || "").trim() || SYSTEM_PROMPT;
}
};
})();
New message elements are created with textContent, which
avoids injecting untrusted HTML into the page.
The script converts the message array into a transcript before each API request so replies can follow the conversation.
Invalid keys, endpoint failures and browser network errors appear inside the widget instead of silently failing.