Skip to main content

EmbedChatbot

  • to embed an OpenWebUI chat as a chatbot into a NextJS website

Step 1: Generate an API Key

  1. Open your Open WebUI interface via your public domain.
  2. Navigate to Settings (your profile icon) > Account.
  3. Scroll down to API Keys, click Create Key, and copy the token (it usually starts with sk-).

Step 2: Add Your Secrets to .env

OPEN_WEBUI_API_KEY="sk-your-actual-api-key-here"
OPEN_WEBUI_URL="https://your-public-cloudflare-domain.com"
OPEN_WEBUI_MODEL="your-local-model-name:latest"

Step 3: Create the Secure API Route

  • Create a file at app/api/chat/route.js
import { NextResponse } from 'next/server';

export async function POST(request) {
try {
const { messages } = await request.json();

// 1. Fetch configurations safely from process.env
const targetUrl = `${process.env.OPEN_WEBUI_URL}/api/chat/completions`;
const apiKey = process.env.OPEN_WEBUI_API_KEY;
const modelName = process.env.OPEN_WEBUI_MODEL;

// Generate lightweight unique strings to bypass Open WebUI's structural validation
const sessionChatId = `external-widget-${Date.now()}`;
const syntheticMessageId = `msg-${Math.random().toString(36).substring(2, 11)}`;

// 2. Forward payload to your Cloudflare-tunneled Open WebUI instance
const response = await fetch(targetUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${apiKey}`,
},
body: JSON.stringify({
model: modelName,
messages: messages, // Array of { role: 'user' | 'assistant', content: string }
stream: false, // Can be set to true if you implement a streaming response reader
// 👇 Mandatory parameters to bypass the v0.9.5+ NoneType middleware crash
chat_id: sessionChatId,
id: syntheticMessageId,
parent_id: "root"
}),
});

if (!response.ok) {
const errorData = await response.text();
console.error('Open WebUI Error:', errorData);
return NextResponse.json({ error: 'LLM Server returned an error' }, { status: response.status });
}

const data = await response.json();
return NextResponse.json(data);
} catch (error) {
console.error('Failed to proxy chat route:', error);
return NextResponse.json({ error: 'Internal Server Error' }, { status: 500 });
}
}

Step 4: Build the Floating Chatbot Component

  • Create a component in components folder called ChatWidget.tsx
'use client';

import { useState, useRef, useEffect } from 'react';

export default function ChatWidget() {
const [isOpen, setIsOpen] = useState(false);
const [messages, setMessages] = useState([]);
const [input, setInput] = useState('');
const [isLoading, setIsLoading] = useState(false);
const messagesEndRef = useRef(null);

// Auto-scroll to the bottom when a new message arrives
useEffect(() => {
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
}, [messages, isLoading]);

const handleSendMessage = async (e) => {
e.preventDefault();
if (!input.trim() || isLoading) return;

const userMessage = { role: 'user', content: input.trim() };
setMessages((prev) => [...prev, userMessage]);
setInput('');
setIsLoading(true);

try {
const response = await fetch('/api/chat', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ messages: [...messages, userMessage] }),
});

const data = await response.json();

if (data.choices && data.choices[0]?.message) {
setMessages((prev) => [...prev, data.choices[0].message]);
} else {
setMessages((prev) => [...prev, { role: 'assistant', content: 'Sorry, I couldn\'t fetch a proper response.' }]);
}
} catch (error) {
console.error('Error contacting chat API:', error);
setMessages((prev) => [...prev, { role: 'assistant', content: 'Network error. Please check backend connection.' }]);
} finally {
setIsLoading(false);
}
};

return (
<div style={{ position: 'fixed', bottom: '24px', right: '24px', z-index: 9999, fontFamily: 'sans-serif' }}>
{/* 1. Toggle Button */}
<button
onClick={() => setIsOpen(!isOpen)}
style={{
width: '56px', height: '56px', borderRadius: '50%', backgroundColor: '#2563eb',
color: 'white', border: 'none', cursor: 'pointer', boxShadow: '0 4px 12px rgba(0,0,0,0.15)',
display: 'flex', alignItems: 'center', justifyContent: 'center', fontSize: '24px'
}}
>
{isOpen ? '✕' : '💬'}
</button>

{/* 2. Chat Window Panel */}
{isOpen && (
<div style={{
position: 'absolute', bottom: '72px', right: '0', width: '360px', height: '500px',
backgroundColor: 'white', borderRadius: '12px', boxShadow: '0 8px 24px rgba(0,0,0,0.15)',
display: 'flex', flexDirection: 'column', overflow: 'hidden', border: '1px solid #e5e7eb'
}}>
{/* Header */}
<div style={{ backgroundColor: '#2563eb', color: 'white', padding: '16px', fontWeight: 'bold' }}>
AI Assistant
</div>

{/* Messages Area */}
<div style={{ flex: 1, padding: '16px', overflowY: 'auto', display: 'flex', flexDirection: 'column', gap: '12px' }}>
{messages.length === 0 && (
<p style={{ color: '#6b7280', fontSize: '14px', textAlign: 'center', marginTop: '20px' }}>
Hello! How can I help you today?
</p>
)}
{messages.map((msg, idx) => (
<div
key={idx}
style={{
alignSelf: msg.role === 'user' ? 'flex-end' : 'flex-start',
backgroundColor: msg.role === 'user' ? '#2563eb' : '#f3f4f6',
color: msg.role === 'user' ? 'white' : '#1f2937',
padding: '10px 14px', borderRadius: '8px', maxWidth: '80%',
fontSize: '14px', wordBreak: 'break-word', whiteSpace: 'pre-wrap'
}}
>
{msg.content}
</div>
))}
{isLoading && (
<div style={{ alignSelf: 'flex-start', backgroundColor: '#f3f4f6', color: '#6b7280', padding: '10px 14px', borderRadius: '8px', fontSize: '14px' }}>
Thinking...
</div>
)}
<div ref={messagesEndRef} />
</div>

{/* Input Form */}
<form onSubmit={handleSendMessage} style={{ display: 'flex', borderTop: '1px solid #e5e7eb', padding: '12px' }}>
<input
type="text"
value={input}
onChange={(e) => setInput(e.target.value)}
placeholder="Type your message..."
disabled={isLoading}
style={{ flex: 1, padding: '8px 12px', border: '1px solid #d1d5db', borderRadius: '6px', outline: 'none', color: '#000000' }}
/>
<button
type="submit"
disabled={isLoading}
style={{ marginLeft: '8px', padding: '8px 16px', backgroundColor: '#2563eb', color: 'white', border: 'none', borderRadius: '6px', cursor: 'pointer', opacity: isLoading ? 0.6 : 1 }}
>
Send
</button>
</form>
</div>
)}
</div>
);
}

Step 5: Add the Widget Globally

  • make the chatbot accessible across your entire application, inject it into your root layout file (app/layout.js or app/layout.tsx):
import ChatWidget from '@/components/ChatWidget';

export default function RootLayout({ children }) {
return (
<html lang="en">
<body>
{children}
{/* Rendered at the root layout so it floats across all site views */}
<ChatWidget />
</body>
</html>
);
}