Backend: Express + Socket.io server with whatsapp-web.js session management, in-memory inbound/outbound queues with idempotency, exponential-backoff retry, and Claude AI draft generation via Anthropic SDK. Frontend: Modern dark-theme single-page dashboard with session status card, real-time inbox, conversation view, AI compose box, and operations panel. Demo mode (DEMO_MODE=true) runs with sample data, no real WhatsApp needed. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
263 lines
11 KiB
HTML
263 lines
11 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8" />
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
<title>WhatsApp AI Agent</title>
|
|
<link rel="stylesheet" href="style.css" />
|
|
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
|
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet" />
|
|
</head>
|
|
<body>
|
|
|
|
<!-- ── Loading Screen ──────────────────────────────────────────────────── -->
|
|
<div id="loading-screen" class="loading-screen">
|
|
<div class="loading-logo">
|
|
<div class="logo-icon">
|
|
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
<path d="M12 2C6.477 2 2 6.477 2 12c0 1.89.525 3.66 1.438 5.168L2 22l4.832-1.438A9.956 9.956 0 0012 22c5.523 0 10-4.477 10-10S17.523 2 12 2z" fill="currentColor"/>
|
|
</svg>
|
|
</div>
|
|
<span>WhatsApp <strong>AI Agent</strong></span>
|
|
</div>
|
|
<div class="loading-spinner"></div>
|
|
</div>
|
|
|
|
<!-- ── QR Screen ───────────────────────────────────────────────────────── -->
|
|
<div id="qr-screen" class="qr-screen hidden">
|
|
<div class="qr-card">
|
|
<div class="qr-header">
|
|
<div class="logo-lockup">
|
|
<div class="logo-icon">
|
|
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
<path d="M12 2C6.477 2 2 6.477 2 12c0 1.89.525 3.66 1.438 5.168L2 22l4.832-1.438A9.956 9.956 0 0012 22c5.523 0 10-4.477 10-10S17.523 2 12 2z" fill="currentColor"/>
|
|
</svg>
|
|
</div>
|
|
<span>WhatsApp <strong>AI Agent</strong></span>
|
|
</div>
|
|
</div>
|
|
<div id="qr-status-badge" class="status-badge status-awaiting">
|
|
<span class="dot"></span>Awaiting Scan
|
|
</div>
|
|
<div class="qr-body">
|
|
<div id="qr-image-wrap" class="qr-image-wrap">
|
|
<img id="qr-img" src="" alt="QR Code" />
|
|
<div id="qr-overlay" class="qr-overlay hidden">
|
|
<div class="qr-refresh-msg">
|
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M1 4v6h6M23 20v-6h-6M20.49 9A9 9 0 005.64 5.64L1 10m22 4l-4.64 4.36A9 9 0 013.51 15"/></svg>
|
|
QR Expired
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<p class="qr-hint">Open <strong>WhatsApp</strong> on your phone → <strong>Linked Devices</strong> → Scan this QR</p>
|
|
<div class="qr-steps">
|
|
<div class="step"><span>1</span>Open WhatsApp on mobile</div>
|
|
<div class="step"><span>2</span>Tap the three dots → Linked Devices</div>
|
|
<div class="step"><span>3</span>Point camera at this screen</div>
|
|
</div>
|
|
</div>
|
|
<div id="qr-connecting" class="qr-connecting hidden">
|
|
<div class="loading-spinner small"></div>
|
|
<span>Authenticating…</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- ── Degraded Screen ─────────────────────────────────────────────────── -->
|
|
<div id="degraded-screen" class="qr-screen hidden">
|
|
<div class="qr-card" style="max-width:420px">
|
|
<div class="qr-header">
|
|
<div class="logo-lockup">
|
|
<div class="logo-icon"><svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M12 2C6.477 2 2 6.477 2 12c0 1.89.525 3.66 1.438 5.168L2 22l4.832-1.438A9.956 9.956 0 0012 22c5.523 0 10-4.477 10-10S17.523 2 12 2z" fill="currentColor"/></svg></div>
|
|
<span>WhatsApp <strong>AI Agent</strong></span>
|
|
</div>
|
|
</div>
|
|
<div class="status-badge status-failed"><span class="dot"></span>Degraded</div>
|
|
<div class="qr-body" style="text-align:center;padding:2rem 1rem">
|
|
<p style="color:var(--text-secondary);margin-bottom:1rem">The WhatsApp session could not be started. This is usually because Chromium is not available in this environment.</p>
|
|
<p style="color:var(--text-muted);font-size:.85rem">Set <code>DEMO_MODE=true</code> in your environment to explore the dashboard with sample data.</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- ── Main Dashboard ──────────────────────────────────────────────────── -->
|
|
<div id="dashboard" class="dashboard hidden">
|
|
|
|
<!-- Sidebar -->
|
|
<aside class="sidebar">
|
|
<div class="sidebar-header">
|
|
<div class="logo-lockup">
|
|
<div class="logo-icon">
|
|
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
<path d="M12 2C6.477 2 2 6.477 2 12c0 1.89.525 3.66 1.438 5.168L2 22l4.832-1.438A9.956 9.956 0 0012 22c5.523 0 10-4.477 10-10S17.523 2 12 2z" fill="currentColor"/>
|
|
</svg>
|
|
</div>
|
|
<span>WA <strong>Agent</strong></span>
|
|
</div>
|
|
<div id="sidebar-session-badge" class="status-badge status-connected">
|
|
<span class="dot"></span><span id="sidebar-session-text">Connected</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="inbox-search-wrap">
|
|
<input id="inbox-search" type="text" class="inbox-search" placeholder="Search conversations…" />
|
|
</div>
|
|
|
|
<div class="inbox-label">
|
|
<span>Inbox</span>
|
|
<span id="total-unread-badge" class="unread-pill hidden">0</span>
|
|
</div>
|
|
|
|
<div id="inbox-list" class="inbox-list">
|
|
<div class="inbox-empty">No conversations yet</div>
|
|
</div>
|
|
</aside>
|
|
|
|
<!-- Conversation Panel -->
|
|
<main class="conv-panel" id="conv-panel">
|
|
<div id="conv-placeholder" class="conv-placeholder">
|
|
<div class="placeholder-icon">
|
|
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
<path d="M21 15a2 2 0 01-2 2H7l-4 4V5a2 2 0 012-2h14a2 2 0 012 2z" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
|
</svg>
|
|
</div>
|
|
<h3>Select a conversation</h3>
|
|
<p>Choose a chat from the inbox to start messaging</p>
|
|
</div>
|
|
|
|
<div id="conv-view" class="conv-view hidden">
|
|
<div class="conv-header">
|
|
<div class="conv-header-info">
|
|
<div id="conv-avatar" class="avatar avatar-lg">R</div>
|
|
<div>
|
|
<div id="conv-name" class="conv-name">Contact Name</div>
|
|
<div id="conv-phone" class="conv-phone">+91 98765 43210</div>
|
|
</div>
|
|
</div>
|
|
<div class="conv-header-actions">
|
|
<button id="btn-ops-toggle" class="icon-btn" title="Operations panel">
|
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="3" width="7" height="7"/><rect x="14" y="3" width="7" height="7"/><rect x="14" y="14" width="7" height="7"/><rect x="3" y="14" width="7" height="7"/></svg>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div id="message-list" class="message-list"></div>
|
|
|
|
<div class="compose-area">
|
|
<div id="ai-draft-bar" class="ai-draft-bar hidden">
|
|
<div class="ai-draft-inner">
|
|
<svg class="ai-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M12 2L2 7l10 5 10-5-10-5zM2 17l10 5 10-5M2 12l10 5 10-5"/></svg>
|
|
<span id="ai-draft-status">Generating draft…</span>
|
|
</div>
|
|
<button id="btn-dismiss-draft" class="icon-btn small">✕</button>
|
|
</div>
|
|
|
|
<textarea
|
|
id="compose-input"
|
|
class="compose-input"
|
|
placeholder="Type a message…"
|
|
rows="1"
|
|
></textarea>
|
|
|
|
<div class="compose-actions">
|
|
<button id="btn-ai-draft" class="btn btn-ai" title="Generate AI reply">
|
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 2L2 7l10 5 10-5-10-5zM2 17l10 5 10-5M2 12l10 5 10-5"/></svg>
|
|
AI Draft
|
|
</button>
|
|
<div class="compose-right">
|
|
<span id="char-count" class="char-count">0</span>
|
|
<button id="btn-send" class="btn btn-send" disabled>
|
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="22" y1="2" x2="11" y2="13"/><polygon points="22 2 15 22 11 13 2 9 22 2"/></svg>
|
|
Send
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</main>
|
|
|
|
<!-- Operations Panel -->
|
|
<aside id="ops-panel" class="ops-panel">
|
|
<div class="ops-section">
|
|
<h4 class="ops-title">Session Health</h4>
|
|
<div class="ops-card">
|
|
<div class="ops-row">
|
|
<span class="ops-label">Status</span>
|
|
<div id="ops-session-badge" class="status-badge status-connected"><span class="dot"></span>Connected</div>
|
|
</div>
|
|
<div class="ops-row">
|
|
<span class="ops-label">Last heartbeat</span>
|
|
<span id="ops-heartbeat" class="ops-value">—</span>
|
|
</div>
|
|
<div class="ops-row">
|
|
<span class="ops-label">Reconnects</span>
|
|
<span id="ops-reconnects" class="ops-value">0</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="ops-section">
|
|
<h4 class="ops-title">Queue Metrics</h4>
|
|
<div class="ops-grid">
|
|
<div class="metric-card">
|
|
<div id="ops-inbound-depth" class="metric-value">0</div>
|
|
<div class="metric-label">Inbound queue</div>
|
|
</div>
|
|
<div class="metric-card">
|
|
<div id="ops-outbound-depth" class="metric-value">0</div>
|
|
<div class="metric-label">Outbound queue</div>
|
|
</div>
|
|
<div class="metric-card success">
|
|
<div id="ops-send-success" class="metric-value">0</div>
|
|
<div class="metric-label">Sent</div>
|
|
</div>
|
|
<div class="metric-card danger">
|
|
<div id="ops-send-failed" class="metric-value">0</div>
|
|
<div class="metric-label">Failed</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="ops-section">
|
|
<h4 class="ops-title">Worker</h4>
|
|
<div class="ops-card">
|
|
<div class="ops-row">
|
|
<span class="ops-label">Status</span>
|
|
<div id="ops-worker-badge" class="status-badge status-idle"><span class="dot"></span>Idle</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="ops-section">
|
|
<h4 class="ops-title">AI Integration</h4>
|
|
<div class="ops-card">
|
|
<div class="ops-row">
|
|
<span class="ops-label">Model</span>
|
|
<span class="ops-value mono">claude-sonnet-4-6</span>
|
|
</div>
|
|
<div class="ops-row">
|
|
<span class="ops-label">Status</span>
|
|
<div id="ops-ai-badge" class="status-badge status-idle"><span class="dot"></span>Ready</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div id="demo-banner" class="demo-banner hidden">
|
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><line x1="12" y1="8" x2="12" y2="12"/><line x1="12" y1="16" x2="12.01" y2="16"/></svg>
|
|
<div>
|
|
<strong>Demo Mode</strong>
|
|
<p>Sample data — no real WhatsApp connection</p>
|
|
</div>
|
|
</div>
|
|
</aside>
|
|
|
|
</div>
|
|
|
|
<!-- Toast container -->
|
|
<div id="toasts" class="toast-container"></div>
|
|
|
|
<script src="/socket.io/socket.io.js"></script>
|
|
<script src="app.js"></script>
|
|
</body>
|
|
</html>
|