A production-ready, embeddable AI chatbot widget built with Cloudflare Workers that can be integrated into any website with a single script tag. Features real-time streaming responses, RAG-powered FAQ answers, session persistence, and dark/light mode support.
-
Production-Live: https://arnob-mahmud.vercel.app/
- Features
- Technology Stack
- Project Structure
- Prerequisites
- Installation & Setup
- Configuration
- How It Works
- API Endpoints
- Components & Architecture
- Usage Guide
- Customization
- Deployment
- Reusing Components
- Keywords
- π Real-time Streaming: AI responses stream in real-time for a natural typing effect
- π§ RAG (Retrieval Augmented Generation): Answers questions from your FAQ using semantic search
- πΎ Session Persistence: Remembers conversations across page reloads using Cloudflare KV
- π Dark/Light Mode: Automatic theme detection with manual toggle option
- π± Fully Responsive: Works seamlessly on desktop, tablet, and mobile devices
- π Shadow DOM: Complete CSS isolation prevents conflicts with host website styles
- β‘ Edge Computing: Runs on Cloudflare's global edge network for low latency
- π¨ Customizable: Easy to customize appearance, behavior, and AI personality
- π Free Tier: Generous free tiers from Cloudflare for most use cases
- Cloudflare Workers: Serverless edge computing platform
- Workers AI: Meta's Llama 3-8B model for AI responses
- Vectorize: Vector database for semantic FAQ search
- KV (Key-Value): Distributed storage for session management
- Vanilla JavaScript: No framework dependencies, pure JS
- Shadow DOM: Web Components API for style isolation
- Tailwind CSS: Utility-first CSS framework
- Server-Sent Events (SSE): Real-time streaming protocol
- Wrangler: Cloudflare CLI for development and deployment
- Tailwind CSS: CSS framework with JIT compilation
- Node.js: JavaScript runtime environment
ai-chatbot-widget/
βββ public/ # Static assets served by Worker
β βββ widget.js # Embeddable widget script (frontend)
β βββ styles.css # Compiled Tailwind CSS
β βββ index.html # Demo page with SEO metadata
β βββ logo.svg # Widget favicon/logo
βββ src/ # Source files
β βββ index.js # Cloudflare Worker backend
β βββ input.css # Tailwind source CSS
βββ wrangler.jsonc # Cloudflare Workers configuration
βββ tailwind.config.js # Tailwind CSS configuration
βββ package.json # Node.js dependencies and scripts
βββ README.md # Project documentationsrc/index.js: Main Worker file handling API routes, RAG, streaming, and session managementpublic/widget.js: Self-contained embeddable widget with Shadow DOM implementationpublic/index.html: Demo page showcasing the widget with comprehensive SEO metadatawrangler.jsonc: Configuration for Cloudflare services (AI, Vectorize, KV, Assets)tailwind.config.js: Tailwind configuration for CSS compilation
Before you begin, ensure you have:
- Node.js version 18 or higher installed
- Cloudflare account (free tier works perfectly)
- Basic knowledge of JavaScript
- Git (optional, for version control)
No prior experience with Cloudflare Workers is required!
If starting fresh:
npm create cloudflare@latest ai-chatbot-widget -- --type=hello-world
cd ai-chatbot-widgetnpm install --save-dev tailwindcss autoprefixer postcss wranglernpx wrangler loginThis opens a browser window for authentication.
npx wrangler vectorize create faq-vectors --dimensions=768 --metric=cosineWhat this does: Creates a vector database with 768-dimensional vectors (BGE embedding size) using cosine similarity for semantic search.
npx wrangler kv namespace create CHAT_SESSIONSImportant: Copy the id from the output. You'll need it for wrangler.jsonc.
Update wrangler.jsonc with your KV namespace ID:
npm run build:cssThis compiles Tailwind CSS into public/styles.css.
This file configures Cloudflare Workers bindings:
assets: Serves static files from./publicdirectoryai: Enables Workers AI for LLM and embeddingsvectorize: Links to Vectorize index for RAGkv_namespaces: Connects to KV for session storage
Configure the widget before loading:
<script>
window.CHATBOT_BASE_URL = "https://your-worker.workers.dev";
window.CHATBOT_TITLE = "Support Assistant";
window.CHATBOT_PLACEHOLDER = "Type your message...";
window.CHATBOT_GREETING = "π Hi! How can I help you today?";
</script>
<script src="https://your-worker.workers.dev/widget.js"></script>Note: Cloudflare Workers don't use traditional .env files. Configuration is done through:
wrangler.jsonc: Service bindings (AI, Vectorize, KV)- Wrangler secrets (if needed):
wrangler secret put SECRET_NAME - Widget globals: Set via
window.CHATBOT_*variables
User's Browser
β
ββ> Loads widget.js
β ββ> Creates Shadow DOM
β ββ> Injects chat UI
β
ββ> User sends message
β
ββ> POST /api/chat
β
ββ> Extract session from cookie
ββ> Generate question embedding (BGE model)
ββ> Search Vectorize for similar FAQs (RAG)
ββ> Build AI prompt with FAQ context
ββ> Stream response from Llama 3
ββ> Save session to KV-
Widget Initialization:
- Widget script loads and creates Shadow DOM host
- Injects HTML structure and styles
- Loads chat history from
/api/history - Applies theme based on system preference
-
Message Sending:
- User types message and submits
- Widget sends POST to
/api/chatwith credentials - Backend extracts session ID from cookie
-
RAG Processing:
- Question converted to 768-dim vector using BGE model
- Vectorize searches for top 3 similar FAQ entries
- FAQ context formatted and added to AI prompt
-
AI Response:
- System prompt + FAQ context + conversation history sent to Llama 3
- Response streams back as Server-Sent Events
- Widget updates UI in real-time as tokens arrive
-
Session Persistence:
- Complete conversation saved to KV with 30-day TTL
- Session ID stored in HTTP-only cookie
- History restored on next visit
Handles chat messages and streams AI responses.
Request:
{
"message": "What is your return policy?"
}Response: Server-Sent Events stream
data: {"response": "Our return policy"}
data: {"response": " allows"}
...
data: [DONE]
Features:
- Session management via cookies
- RAG-powered FAQ context
- Streaming responses
- Automatic session creation
Retrieves conversation history for current session.
Response:
{
"messages": [
{
"role": "user",
"content": "Hello",
"timestamp": 1234567890
},
{
"role": "assistant",
"content": "Hi! How can I help?",
"timestamp": 1234567891
}
]
}Uses: Restores chat history on widget load
Populates Vectorize index with FAQ embeddings.
Request: None (uses hardcoded FAQs in src/index.js)
Response:
{
"success": true,
"count": 8
}Usage: Call once after deployment to populate knowledge base
curl -X POST https://your-worker.workers.dev/api/seedHealth check endpoint.
Response:
{
"status": "ok"
}All files in ./public are served automatically:
/widget.js- Embeddable widget script/styles.css- Compiled Tailwind CSS/index.html- Demo page/logo.svg- Favicon/logo
const cookie = (r) =>
r.headers.get("Cookie")?.match(/chatbot_session=([^;]+)/)?.[1];- Extracts session ID from HTTP cookie
- Creates new session if none exists
- Stores session in KV with 30-day expiration
async function faq(env, q) {
// Generate embedding
const e = await env.AI.run("@cf/baai/bge-base-en-v1.5", { text: [q] });
// Search Vectorize
const r = await env.VECTORIZE.query(e.data[0], { topK: 3 });
// Format results
return r.matches
.map((m) => `Q: ${m.metadata?.question}\nA: ${m.metadata?.answer}`)
.join("\n\n");
}How it works:
- Converts question to vector embedding
- Searches Vectorize for semantically similar FAQs
- Returns formatted context for AI prompt
async function chat(req, env) {
// Get/create session
// Add user message
// Get FAQ context (RAG)
// Build AI prompt
// Stream response
// Save to KV
}Key features:
- TransformStream for real-time streaming
- Accumulates full response for KV storage
- Sets HTTP-only cookie for new sessions
const { readable, writable } = new TransformStream({
transform(chunk, ctrl) {
// Parse SSE format
// Accumulate response
ctrl.enqueue(chunk); // Forward to client
},
async flush() {
// Save complete response to KV
},
});const host = document.createElement("div");
host.attachShadow({ mode: "open" });
shadowRoot = host.shadowRoot;Why Shadow DOM?
- CSS Isolation: Prevents Tailwind from affecting host page
- Encapsulation: Widget styles don't leak out
- No Conflicts: Host page styles don't affect widget
let open = 0; // Chat window open/closed
let msgs = []; // Message array
let typing = 0; // Request in progress
let menu = 0; // Menu open/closed
let dark = false; // Theme modefunction draw() {
$('cb-ms').innerHTML = msgs.map((m, i) =>
m.role === 'user'
? /* User message HTML */
: /* Assistant message HTML */
).join('');
}- Maps message array to HTML
- User messages: Right-aligned, black background
- Assistant messages: Left-aligned, includes bot icon
- Auto-scrolls to bottom
const rd = r.body.getReader();
while (1) {
const { done, value } = await rd.read();
// Parse SSE chunks
// Update message in real-time
}Process:
- Reads stream chunk by chunk
- Parses Server-Sent Events format
- Updates message element incrementally
- Creates typing effect
function theme() {
tog($("cb"), "dark", dark);
// Updates icons and text
}- Toggles
darkclass on root element - Tailwind's
dark:variants activate automatically - Persists across page reloads
- Start development server:
npm run devThis runs:
npm run build:css- Compiles Tailwindwrangler dev- Starts local Worker
-
Access locally: Open
http://localhost:8787in your browser -
Test widget:
- Chat with the AI
- Test dark mode toggle
- Clear chat and verify persistence
- Build and deploy:
npm run deployThis:
- Builds CSS
- Deploys Worker to Cloudflare
- Returns deployment URL
- Seed FAQ database:
curl -X POST https://your-worker.workers.dev/api/seed- Test deployment: Visit your Worker URL and test the chatbot
Add these two script tags before </body>:
<script>
window.CHATBOT_BASE_URL = "https://your-worker.workers.dev";
window.CHATBOT_TITLE = "Support";
window.CHATBOT_GREETING = "π How can I help you today?";
</script>
<script src="https://your-worker.workers.dev/widget.js"></script>That's it! The widget will automatically appear in the bottom-right corner.
Edit SYS constant in src/index.js:
const SYS = `You are a friendly assistant for [Your Company].
You help customers with [your services].
Always be helpful and professional.`;Edit the faqs array in seed() function:
const faqs = [
["Your question?", "Your answer."],
["Another question?", "Another answer."],
// Add more...
];Then redeploy and reseed:
npm run deploy
curl -X POST https://your-worker.workers.dev/api/seedAll styles use Tailwind classes in widget.js. Common customizations:
Colors:
// Change button color
class="bg-black" β class="bg-blue-600"Size:
// Chat window dimensions
w-[400px] h-[600px] β w-[500px] h-[700px]Position:
// Button position
bottom-6 right-6 β bottom-4 left-4| Variable | Description | Default |
|---|---|---|
CHATBOT_BASE_URL |
Worker deployment URL | '' (same origin) |
CHATBOT_TITLE |
Header title | 'AI Assistant' |
CHATBOT_PLACEHOLDER |
Input placeholder | 'Message...' |
CHATBOT_GREETING |
Initial greeting | 'π Hi! How can I help you today?' |
- Cloudflare account
- Wrangler CLI installed
- Resources created (Vectorize, KV)
- Build CSS:
npm run build:css- Deploy Worker:
wrangler deployOr use the combined command:
npm run deploy- Verify Deployment:
Visit your Worker URL (e.g., https://ai-chatbot-widget.YOUR-SUBDOMAIN.workers.dev)
- Seed FAQs:
curl -X POST https://your-worker.workers.dev/api/seed- Workers: 100,000 requests/day
- Workers AI: 10,000 neurons/day
- Vectorize: 5M vector operations/month
- KV: 100K reads, 1K writes/day
Most websites can run completely free!
The widget is completely self-contained. To reuse:
- Copy
widget.jsto your project - Host
styles.css(or inline it) - Set configuration globals:
window.CHATBOT_BASE_URL = "https://your-api.com";The faq() function can be reused for any RAG use case:
// In your own Worker
async function searchKnowledgeBase(env, query) {
const embedding = await env.AI.run("@cf/baai/bge-base-en-v1.5", {
text: [query],
});
const results = await env.VECTORIZE.query(embedding.data[0], {
topK: 5,
returnMetadata: "all",
});
return results.matches;
}The Shadow DOM implementation can be adapted for any embeddable widget:
function createIsolatedWidget() {
const host = document.createElement("div");
host.attachShadow({ mode: "open" });
const shadow = host.shadowRoot;
// Inject styles and HTML
shadow.appendChild(style);
shadow.appendChild(content);
document.body.appendChild(host);
return shadow;
}The streaming pattern works for any SSE endpoint:
async function streamResponse(url, onChunk) {
const response = await fetch(url);
const reader = response.body.getReader();
const decoder = new TextDecoder();
while (true) {
const { done, value } = await reader.read();
if (done) break;
const text = decoder.decode(value);
const lines = text.split("\n");
for (const line of lines) {
if (line.startsWith("data: ")) {
const data = JSON.parse(line.slice(6));
onChunk(data);
}
}
}
}Technologies: Cloudflare Workers, Workers AI, Vectorize, KV, RAG, Server-Sent Events, Shadow DOM, Tailwind CSS, Llama 3, BGE embeddings, semantic search
Concepts: Embeddable widget, serverless, edge computing, real-time streaming, session persistence, dark mode, responsive design, CSS isolation, vector database, retrieval augmented generation
Use Cases: Customer support, FAQ chatbot, website assistant, AI assistant, conversational AI, knowledge base search
RAG enhances AI responses by providing relevant context from a knowledge base:
- User asks question β "What is your return policy?"
- Question converted to vector β [0.123, -0.456, ...] (768 dimensions)
- Vectorize searches β Finds similar FAQ entries
- Context added to prompt β "FAQ: Q: What is your return policy? A: 30-day returns..."
- AI generates response β Uses FAQ context for accurate answer
Benefits:
- More accurate than pure AI
- Can be updated without retraining
- Grounded in your data
- Reduces hallucinations
Shadow DOM creates an isolated DOM tree:
// Regular DOM
document.body.appendChild(element); // Styles leak
// Shadow DOM
const shadow = host.attachShadow({ mode: "open" });
shadow.appendChild(element); // Styles isolatedBenefits for widgets:
- No CSS conflicts
- Encapsulated styles
- Reusable components
- Works on any website
Instead of waiting for complete response:
// Traditional (slow)
const response = await fetch("/api/chat");
const data = await response.json();
display(data.message); // User waits
// Streaming (fast)
const stream = await fetch("/api/chat");
const reader = stream.body.getReader();
while ((chunk = await reader.read())) {
display(chunk); // User sees progress
}Benefits:
- Perceived performance
- Natural typing effect
- Better UX
- Lower latency
- Check browser console for errors
- Verify
CHATBOT_BASE_URLis set correctly - Ensure script loads after DOM ready
- Run
npm run build:cssbefore deploying - Check
styles.cssexists inpublic/ - Verify Worker serves static assets
- Ensure Vectorize index is created
- Call
/api/seedendpoint - Check Vectorize bindings in
wrangler.jsonc
- Verify KV namespace ID in
wrangler.jsonc - Check cookies are enabled
- Ensure
credentials: 'include'in fetch calls
This project is open source and available under the MIT License.
Contributions are welcome! Feel free to:
- Report bugs
- Suggest features
- Submit pull requests
- Improve documentation
- Built with Cloudflare Workers
- AI powered by Meta's Llama 3
- Styled with Tailwind CSS
- Inspired by Intercom and Drift chatbots
For questions or issues:
- Open an issue on GitHub
- Visit portfolio: https://arnob-mahmud.vercel.app/
- Email: arnobt78@gmail.com
Feel free to use this project repository and extend this project further!
If you have any questions or want to share your work, reach out via GitHub or my portfolio at https://arnob-mahmud.vercel.app/.
Enjoy building and learning! π
Thank you! π
{ "$schema": "node_modules/wrangler/config-schema.json", "name": "ai-chatbot-widget", "main": "src/index.js", "compatibility_date": "2025-12-23", "observability": { "enabled": true, }, "assets": { "directory": "./public", "binding": "ASSETS", }, "ai": { "binding": "AI", }, "vectorize": [ { "binding": "VECTORIZE", "index_name": "faq-vectors", }, ], "kv_namespaces": [ { "binding": "CHAT_SESSIONS", "id": "YOUR_KV_NAMESPACE_ID", // Replace with your ID }, ], }