Step-by-step chatbot tutorial

Add an AI chatbot to any website.

Build a polished floating chatbot with plain HTML, CSS and JavaScript. Connect it to the Mathix API, append every message, preserve conversation context and test the finished widget directly on this page.

now and get $1 in credit and start building with the Mathix API.

No framework required Mobile responsive Conversation memory Copyable source code
YOUR WEBSITE

A helpful AI assistant, available on every page.

The widget stays in the bottom-right corner and expands only when a visitor opens it.

Website Assistant Powered by Mathix API
Hi! How can I help you today?
What services do you offer?
We provide AI API access for chat, content, coding and other application features.
Ask anything...
Floating launcher A compact button that never covers the full website.
Live API responses Visitors send prompts and receive Mathix answers.
Conversation history Earlier messages are included as context.
Responsive design The widget adapts to phones, tablets and desktops.
Build the widget

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.

01

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>

index.html Launcher, setup and chat interface
<!-- 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>
02

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.

chat-widget.css Desktop and mobile styles
: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;
  }
}
03

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.

chat-widget.js Live requests and conversation memory
(() => {
  "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;
    }
  };
})();
Messages are appended

New message elements are created with textContent, which avoids injecting untrusted HTML into the page.

Context is preserved

The script converts the message array into a transcript before each API request so replies can follow the conversation.

Errors stay visible

Invalid keys, endpoint failures and browser network errors appear inside the widget instead of silently failing.

Live Mathix chatbot

Enter your key and test the widget.

The live tester sends requests directly from your browser. Your key is held in memory for this page session and is not stored by this tutorial.

Do not paste a production secret on an untrusted website.

Example business website

Your website stays visible while the chatbot helps.

The assistant opens as a contained side panel instead of taking over the entire page. On mobile, it expands to use the available screen space without breaking the layout.

Production security

Do not publish your private API key in JavaScript.

The browser-key version is useful for this interactive tutorial or for letting each developer test with their own key. For a public production chatbot, send the message to your own backend and keep the Mathix key in an environment variable.

Why a backend matters

Anyone can inspect public JavaScript. A backend proxy prevents visitors from copying your private Mathix key and using your credit.

04

Optional Flask proxy

Store MATHIX_API_KEY in your server environment and let the browser call your own /api/chat route.

app.py Server-side key protection
from flask import Flask, request, jsonify
import os
import requests

app = Flask(__name__)

@app.post("/api/chat")
def chat():
    user_input = request.json.get("input", "")
    model = request.json.get("model", "g-regular-1.0")

    response = requests.post(
        "https://mathgpt.today/plain",
        json={
            "API_KEY": os.environ["MATHIX_API_KEY"],
            "input": user_input,
            "model": model,
            "system": "You are a helpful website assistant."
        },
        timeout=60
    )

    return jsonify(response.json()), response.status_code