<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>AI Chat with Usage Billing</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI",
Roboto, sans-serif;
background: #0f0f1e;
background-image: radial-gradient(
at 0% 0%,
rgba(102, 126, 234, 0.15) 0px,
transparent 50%
),
radial-gradient(
at 100% 100%,
rgba(118, 75, 162, 0.15) 0px,
transparent 50%
),
radial-gradient(
at 50% 50%,
rgba(102, 126, 234, 0.05) 0px,
transparent 50%
);
height: 100vh;
display: flex;
justify-content: center;
align-items: center;
padding: 0;
position: relative;
overflow: hidden;
margin: 0;
}
.chat-container {
background: rgba(22, 22, 35, 0.95);
border: 1px solid rgba(102, 126, 234, 0.2);
border-radius: 0;
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.5);
width: 100%;
max-width: 100%;
height: 100vh;
display: flex;
flex-direction: column;
overflow: hidden;
position: relative;
z-index: 1;
}
.chat-header {
background: linear-gradient(
135deg,
rgba(102, 126, 234, 0.15) 0%,
rgba(118, 75, 162, 0.15) 100%
);
border-bottom: 1px solid rgba(102, 126, 234, 0.2);
color: white;
padding: 24px 28px;
position: relative;
overflow: hidden;
}
.chat-header h1 {
font-size: 26px;
margin-bottom: 6px;
font-weight: 700;
letter-spacing: -0.5px;
color: #fff;
}
.chat-header p {
font-size: 13px;
opacity: 0.6;
font-weight: 500;
letter-spacing: 0.3px;
}
.chat-messages {
flex: 1;
overflow-y: auto;
padding: 32px 10%;
background: transparent;
will-change: scroll-position;
scroll-behavior: smooth;
}
.chat-messages::-webkit-scrollbar {
width: 6px;
}
.chat-messages::-webkit-scrollbar-track {
background: rgba(255, 255, 255, 0.05);
}
.chat-messages::-webkit-scrollbar-thumb {
background: rgba(102, 126, 234, 0.3);
border-radius: 3px;
}
.chat-messages::-webkit-scrollbar-thumb:hover {
background: rgba(102, 126, 234, 0.5);
}
.message {
margin-bottom: 20px;
display: flex;
gap: 12px;
animation: slideIn 0.2s ease-out;
}
@keyframes slideIn {
from {
opacity: 0;
transform: translateY(10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.message.user {
flex-direction: row-reverse;
}
.message-avatar {
width: 40px;
height: 40px;
border-radius: 12px;
display: flex;
align-items: center;
justify-content: center;
font-size: 20px;
flex-shrink: 0;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
}
.message.user .message-avatar {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
}
.message.assistant .message-avatar {
background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
}
.message-content {
max-width: 65%;
}
.message-bubble {
padding: 14px 18px;
border-radius: 18px;
line-height: 1.6;
word-wrap: break-word;
font-size: 15px;
position: relative;
}
.message.user .message-bubble {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border-bottom-right-radius: 6px;
box-shadow: 0 2px 8px rgba(102, 126, 234, 0.3);
}
.message.assistant .message-bubble {
background: rgba(255, 255, 255, 0.05);
color: rgba(255, 255, 255, 0.95);
border: 1px solid rgba(255, 255, 255, 0.1);
border-bottom-left-radius: 6px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
}
.message-meta {
display: flex;
gap: 10px;
margin-top: 8px;
font-size: 11px;
color: rgba(255, 255, 255, 0.4);
font-weight: 500;
}
.message.user .message-meta {
justify-content: flex-end;
}
.token-badge {
background: rgba(102, 126, 234, 0.2);
color: #a8b9ff;
padding: 4px 10px;
border-radius: 12px;
font-weight: 600;
border: 1px solid rgba(102, 126, 234, 0.3);
}
.chat-input-area {
padding: 24px 10% 32px;
background: rgba(22, 22, 35, 0.95);
border-top: 1px solid rgba(102, 126, 234, 0.2);
}
.input-wrapper {
display: flex;
gap: 12px;
align-items: flex-end;
}
#messageInput {
flex: 1;
background: rgba(255, 255, 255, 0.05);
border: 2px solid rgba(102, 126, 234, 0.2);
border-radius: 16px;
padding: 14px 20px;
font-size: 15px;
font-family: inherit;
resize: none;
max-height: 120px;
transition: border-color 0.2s ease, background 0.2s ease;
color: white;
will-change: border-color;
overflow: hidden;
scrollbar-width: none;
/* Firefox */
}
#messageInput::-webkit-scrollbar {
display: none;
}
#messageInput::placeholder {
color: rgba(255, 255, 255, 0.3);
}
#messageInput:focus {
outline: none;
border-color: #667eea;
background: rgba(255, 255, 255, 0.08);
}
#sendBtn {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border: none;
width: 52px;
height: 52px;
border-radius: 16px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
font-size: 22px;
transition: transform 0.1s ease, box-shadow 0.1s ease;
flex-shrink: 0;
box-shadow: 0 2px 8px rgba(102, 126, 234, 0.4);
position: relative;
}
#sendBtn:hover:not(:disabled) {
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.5);
}
#sendBtn:active:not(:disabled) {
transform: translateY(0);
}
#sendBtn:disabled {
opacity: 0.4;
cursor: not-allowed;
box-shadow: none;
}
.typing-indicator {
display: none;
padding: 14px 18px;
background: rgba(255, 255, 255, 0.05);
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 18px;
border-bottom-left-radius: 6px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
width: fit-content;
}
.typing-indicator.show {
display: block;
}
.typing-dots {
display: flex;
gap: 6px;
}
.typing-dots span {
width: 10px;
height: 10px;
border-radius: 50%;
background: #667eea;
animation: typing 1.4s infinite ease-in-out;
will-change: transform, opacity;
}
.typing-dots span:nth-child(2) {
animation-delay: 0.2s;
}
.typing-dots span:nth-child(3) {
animation-delay: 0.4s;
}
@keyframes typing {
0%,
60%,
100% {
transform: translateY(0) scale(1);
opacity: 0.6;
}
30% {
transform: translateY(-12px) scale(1.1);
opacity: 1;
}
}
.error-message {
background: rgba(239, 68, 68, 0.15);
color: #fca5a5;
padding: 14px 18px;
border-radius: 12px;
margin-bottom: 12px;
display: none;
border: 1px solid rgba(239, 68, 68, 0.3);
font-size: 14px;
font-weight: 500;
}
.error-message.show {
display: block;
animation: slideIn 0.3s ease;
}
.empty-state {
text-align: center;
padding: 80px 20px;
color: rgba(255, 255, 255, 0.5);
}
.empty-state-icon {
font-size: 72px;
margin-bottom: 20px;
animation: float 3s ease-in-out infinite;
}
@keyframes float {
0%,
100% {
transform: translateY(0px);
}
50% {
transform: translateY(-10px);
}
}
.empty-state h2 {
font-size: 24px;
margin-bottom: 10px;
color: rgba(255, 255, 255, 0.9);
font-weight: 700;
letter-spacing: -0.5px;
}
.empty-state p {
font-size: 15px;
color: rgba(255, 255, 255, 0.4);
font-weight: 500;
}
</style>
</head>
<body>
<div class="chat-container">
<div class="chat-header">
<h1>🤖 AI Chat Assistant</h1>
<p>Powered by AI-SDK & Dodo Payments</p>
</div>
<div class="chat-messages" id="chatMessages">
<div class="empty-state" id="emptyState">
<div class="empty-state-icon">💬</div>
<h2>Start a Conversation</h2>
<p>Ask me anything! Your token usage is automatically tracked.</p>
</div>
</div>
<div class="chat-input-area">
<div class="error-message" id="errorMessage"></div>
<div class="input-wrapper">
<textarea
id="messageInput"
placeholder="Type your message here..."
rows="1"
></textarea>
<button id="sendBtn" onclick="sendMessage()">➤</button>
</div>
</div>
</div>
<script>
let conversationHistory = [];
const messageInput = document.getElementById("messageInput");
let resizeTimeout;
messageInput.addEventListener("input", function () {
clearTimeout(resizeTimeout);
resizeTimeout = setTimeout(() => {
this.style.height = "auto";
this.style.height = Math.min(this.scrollHeight, 120) + "px";
}, 10);
});
// Send message on Enter (Shift+Enter for new line)
messageInput.addEventListener("keydown", function (e) {
if (e.key === "Enter" && !e.shiftKey) {
e.preventDefault();
sendMessage();
}
});
async function sendMessage() {
const input = document.getElementById("messageInput");
const message = input.value.trim();
if (!message) return;
// Hide empty state
document.getElementById("emptyState").style.display = "none";
// Hide error
document.getElementById("errorMessage").classList.remove("show");
// Add user message to UI
addMessage("user", message);
// Add to conversation history
conversationHistory.push({
role: "user",
content: message,
});
// Clear input
input.value = "";
input.style.height = "auto";
// Show typing indicator
showTypingIndicator();
// Disable send button
const sendBtn = document.getElementById("sendBtn");
sendBtn.disabled = true;
try {
const response = await fetch("/chat", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
messages: conversationHistory,
}),
});
const data = await response.json();
if (!response.ok) {
throw new Error(data.error || "Failed to get response");
}
// Hide typing indicator
hideTypingIndicator();
// Add assistant response to UI
addMessage("assistant", data.message, data.usage);
// Add to conversation history
conversationHistory.push({
role: "assistant",
content: data.message,
});
} catch (error) {
hideTypingIndicator();
showError(error.message);
// Remove the last user message from history since it failed
conversationHistory.pop();
} finally {
sendBtn.disabled = false;
}
}
function addMessage(role, content, usage = null) {
const messagesDiv = document.getElementById("chatMessages");
const messageDiv = document.createElement("div");
messageDiv.className = `message ${role}`;
const avatar = role === "user" ? "👤" : "🤖";
let metaHTML = "";
if (usage) {
metaHTML = `
<div class="message-meta">
<span class="token-badge">📊 ${usage.totalTokens} tokens</span>
</div>
`;
}
messageDiv.innerHTML = `
<div class="message-avatar">${avatar}</div>
<div class="message-content">
<div class="message-bubble">${escapeHtml(content)}</div>
${metaHTML}
</div>
`;
messagesDiv.appendChild(messageDiv);
requestAnimationFrame(() => {
messagesDiv.scrollTop = messagesDiv.scrollHeight;
});
}
function showTypingIndicator() {
const messagesDiv = document.getElementById("chatMessages");
const typingDiv = document.createElement("div");
typingDiv.className = "message assistant";
typingDiv.id = "typingIndicator";
typingDiv.innerHTML = `
<div class="message-avatar">🤖</div>
<div class="typing-indicator show">
<div class="typing-dots">
<span></span>
<span></span>
<span></span>
</div>
</div>
`;
messagesDiv.appendChild(typingDiv);
requestAnimationFrame(() => {
messagesDiv.scrollTop = messagesDiv.scrollHeight;
});
}
function hideTypingIndicator() {
const typingIndicator = document.getElementById("typingIndicator");
if (typingIndicator) {
typingIndicator.remove();
}
}
function showError(message) {
const errorDiv = document.getElementById("errorMessage");
errorDiv.textContent = "❌ " + message;
errorDiv.classList.add("show");
}
function escapeHtml(text) {
const div = document.createElement("div");
div.textContent = text;
return div.innerHTML.replace(/\n/g, "<br>");
}
</script>
</body>
</html>