modified: .env

new file:   __debug_bin.exe
	modified:   bot.db
	modified:   db/db.go
	modified:   go.mod
	new file:   handlers/auth.go
	modified:   handlers/dashboard.go
	new file:   handlers/saas.go
	modified:   handlers/webhook.go
	modified:   main.go
	new file:   saas_bot.db
	modified:   services/openrouter.go
	new file:   services/types.go
	modified:   services/whatsapp.go
	new file:   static/style.css
	modified:   templates/dashboard.html
	new file:   templates/landing.html
	new file:   templates/login.html
	new file:   templates/register.html
	deleted:    types/types.go
This commit is contained in:
2026-03-02 00:38:05 -03:00
parent 9ff021879f
commit e256fcb073
20 changed files with 627 additions and 659 deletions

View File

@@ -1,434 +1,103 @@
{{ define "dashboard.html" }}
<!DOCTYPE html>
<html lang="en">
<html>
<head>
<meta charset="UTF-8">
<title>SekiBot | Dashboard</title>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&display=swap" rel="stylesheet">
<style>
:root {
--bg-tertiary: #202225;
--bg-secondary: #2f3136;
--bg-primary: #36393f;
--bg-input: #40444b;
--text-normal: #dcddde;
--text-muted: #72767d;
--blurple: #5865F2;
--blurple-hover: #4752c4;
--danger: #ed4245;
--success: #3ba55c;
--interactive-hover: #3b3d42;
}
body { margin: 0; font-family: 'Inter', sans-serif; background: var(--bg-primary); color: var(--text-normal); overflow: hidden; display: flex; height: 100vh; }
/* SCROLLBARS */
::-webkit-scrollbar { width: 8px; height: 8px; background-color: var(--bg-secondary); }
::-webkit-scrollbar-thumb { background-color: #202225; border-radius: 4px; }
/* SIDEBAR */
.sidebar { width: 240px; background: var(--bg-secondary); display: flex; flex-direction: column; flex-shrink: 0; }
.sidebar-header { padding: 16px; border-bottom: 1px solid #202225; font-weight: 600; color: white; display: flex; align-items: center; justify-content: space-between; }
.sidebar-nav { padding: 10px; border-bottom: 1px solid #202225; }
.sidebar-scroll { flex: 1; overflow-y: auto; padding: 8px; }
.nav-item { padding: 10px; border-radius: 4px; cursor: pointer; color: #8e9297; margin-bottom: 2px; font-weight: 500; }
.nav-item:hover { background: var(--interactive-hover); color: var(--text-normal); }
.nav-item.active { background: rgba(79,84,92,0.6); color: white; }
.channel-item { display: flex; align-items: center; justify-content: space-between; padding: 8px; border-radius: 4px; cursor: pointer; color: #8e9297; margin-bottom: 2px; }
.channel-item:hover { background: var(--interactive-hover); color: var(--text-normal); }
.channel-item.active { background: rgba(79,84,92,0.6); color: white; }
.channel-name { flex: 1; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.channel-actions { display: none; gap: 4px; }
.channel-item:hover .channel-actions { display: flex; }
/* ICONS */
.icon-btn { color: var(--text-muted); padding: 2px; cursor: pointer; border-radius: 3px; font-size: 0.8rem; }
.icon-btn:hover { color: white; background: var(--bg-tertiary); }
.icon-btn.del:hover { color: var(--danger); }
/* PANELS */
.main-panel { flex: 1; display: none; flex-direction: column; background: var(--bg-primary); position: relative; height: 100vh; }
.main-panel.show { display: flex; }
/* CHAT STYLES */
.chat-header { height: 48px; border-bottom: 1px solid #26272d; display: flex; align-items: center; padding: 0 16px; font-weight: 600; color: white; box-shadow: 0 1px 0 rgba(4,4,5,0.02); }
.messages-wrapper { flex: 1; overflow-y: scroll; display: flex; flex-direction: column; padding: 0 16px; }
.message-group { margin-top: 16px; display: flex; gap: 16px; }
.avatar { width: 40px; height: 40px; border-radius: 50%; background: var(--blurple); flex-shrink: 0; display: flex; align-items: center; justify-content: center; font-weight: bold; color: white; font-size: 18px; }
.avatar.bot { background: var(--success); }
.msg-content { flex: 1; min-width: 0; }
.msg-header { display: flex; align-items: baseline; gap: 8px; margin-bottom: 4px; }
.username { font-weight: 500; color: white; }
.timestamp { font-size: 0.75rem; color: var(--text-muted); }
.msg-text { white-space: pre-wrap; line-height: 1.375rem; color: var(--text-normal); }
.input-wrapper { padding: 0 16px 24px; flex-shrink: 0; margin-top: 10px; }
.input-bg { background: var(--bg-input); border-radius: 8px; padding: 12px; display: flex; align-items: center; }
.chat-input { background: transparent; border: none; color: white; flex: 1; font-size: 1rem; outline: none; }
/* APPOINTMENTS TABLE STYLES */
.appt-container { padding: 40px; overflow-y: auto; }
table { width: 100%; border-collapse: collapse; margin-top: 20px; background: var(--bg-secondary); border-radius: 8px; overflow: hidden; }
th, td { padding: 15px; text-align: left; border-bottom: 1px solid var(--bg-input); }
th { background: #202225; text-transform: uppercase; font-size: 0.75rem; letter-spacing: 1px; color: var(--text-muted); }
input.edit-field { background: var(--bg-input); border: 1px solid #202225; color: white; padding: 8px; border-radius: 4px; width: 100%; box-sizing: border-box; }
.status-pill { background: #faa61a; color: black; padding: 4px 8px; border-radius: 12px; font-size: 0.75rem; font-weight: bold; text-transform: uppercase; }
/* BUTTONS */
.btn-blurple { background: var(--blurple); color: white; border: none; padding: 8px 16px; border-radius: 4px; font-weight: 500; cursor: pointer; transition: 0.2s; }
.btn-blurple:hover { background: var(--blurple-hover); }
.btn-icon { background: transparent; border: none; cursor: pointer; padding: 5px; color: var(--text-muted); border-radius: 4px; }
.btn-icon:hover { background: var(--bg-tertiary); color: white; }
/* MODAL */
.modal-overlay { display: none; position: fixed; top:0; left:0; width:100%; height:100%; background: rgba(0,0,0,0.7); z-index: 100; align-items: center; justify-content: center;}
.modal { background: var(--bg-primary); padding: 20px; border-radius: 8px; width: 300px; text-align: center; }
</style>
<title>SekiBot Dashboard</title>
<link rel="stylesheet" href="/static/style.css">
<script src='https://cdn.jsdelivr.net/npm/fullcalendar@6.1.8/index.global.min.js'></script>
</head>
<body>
<nav class="sidebar">
<div class="sidebar-header">
<span>SekiBot Server</span>
</div>
<div class="sidebar-nav">
<div class="nav-item active" onclick="switchPanel('chat')" id="nav-chat">💬 Conversations</div>
<div class="nav-item" onclick="switchPanel('appt')" id="nav-appt">📅 Appointments</div>
</div>
<div class="sidebar-scroll" id="channel-list">
<div style="padding: 10px;">
<button class="btn-blurple" style="width: 100%;" onclick="createNewChat()">+ New Chat</button>
</div>
{{range .Chats}}
<div class="channel-item" id="chat-item-{{.ID}}" onclick="loadChat({{.ID}}, '{{.Title}}')">
<div style="display: flex; align-items: center; overflow: hidden;">
<span style="color: #72767d; margin-right: 6px;">#</span>
<span class="channel-name" id="title-{{.ID}}">{{.Title}}</span>
</div>
<div class="channel-actions" onclick="event.stopPropagation()">
<span class="icon-btn" onclick="openRenameModal({{.ID}})"></span>
<span class="icon-btn del" onclick="deleteChat({{.ID}})"></span>
</div>
</div>
{{end}}
</div>
</nav>
<main class="main-panel show" id="panel-chat">
<div class="chat-header">
<span style="color: #72767d; margin-right: 6px; font-size: 1.2em;">#</span>
<span id="current-channel-name">general</span>
</div>
<div class="messages-wrapper" id="messages-pane">
<div style="margin: auto; text-align: center; color: var(--text-muted);">
<h3>Welcome back, Seki.</h3>
<p>Select a chat on the left to start.</p>
</div>
</div>
<div class="input-wrapper">
<div class="input-bg">
<input type="text" class="chat-input" id="user-input" placeholder="Message #general" onkeypress="handleEnter(event)">
</div>
</div>
</main>
<main class="main-panel" id="panel-appt">
<div class="appt-container">
<h1>Appointments Manager</h1>
<div style="background: var(--bg-secondary); padding: 20px; border-radius: 8px; margin-bottom: 30px;">
<h3 style="margin-top: 0; margin-bottom: 15px;">Add Manual Appointment</h3>
<div style="display: flex; gap: 10px;">
<input type="text" class="edit-field" id="m-phone" placeholder="+56 9 1234 5678" style="flex: 1;">
<input type="datetime-local" class="edit-field" id="m-date" style="flex: 1;">
<button class="btn-blurple" style="background: var(--success);" onclick="createMockAppt()">Add Booking</button>
<div class="dashboard-layout">
<div class="sidebar">
<h2 style="color: white;">🤖 SekiBot</h2>
<div style="margin-bottom: 20px; color: var(--text-muted);">
User: {{ .UserEmail }} <br>
<small>Plan: {{ .Tier }}</small>
</div>
<a href="#settings" class="btn-outline" style="text-align:center; border:none; text-align: left;">⚙️ Settings</a>
<a href="#appointments" class="btn-outline" style="text-align:center; border:none; text-align: left;">📅 Appointments</a>
<a href="/logout" style="margin-top: auto; color: var(--danger);">🚪 Logout</a>
</div>
<table>
<thead>
<tr>
<th style="width: 50px;">ID</th>
<th>Phone</th>
<th>Date (YYYY-MM-DD HH:MM)</th>
<th>Status</th>
<th style="width: 100px; text-align: right;">Actions</th>
</tr>
</thead>
<tbody>
{{range .Appointments}}
<tr id="appt-{{.ID}}">
<td>#{{.ID}}</td>
<td><input type="text" class="edit-field" value="{{.CustomerPhone}}" id="edit-phone-{{.ID}}"></td>
<td><input type="text" class="edit-field" value="{{.Date}}" id="edit-date-{{.ID}}"></td>
<td><span class="status-pill">{{.Status}}</span></td>
<td style="text-align: right;">
<button class="btn-icon" title="Save" onclick="updateAppt({{.ID}})">💾</button>
<button class="btn-icon" title="Delete" style="color: var(--danger);" onclick="deleteAppt({{.ID}})">🗑️</button>
</td>
</tr>
{{end}}
</tbody>
</table>
</div>
</main>
<div class="modal-overlay" id="rename-modal">
<div class="modal">
<h3 style="margin-top:0; color:white;">Rename Channel</h3>
<input type="text" id="new-name-input" class="edit-field" style="margin-bottom: 15px;">
<div style="display: flex; justify-content: space-between;">
<button onclick="closeModal()" style="background: transparent; color: white; border: none; cursor: pointer;">Cancel</button>
<button class="btn-blurple" onclick="submitRename()">Save</button>
</div>
</div>
</div>
<script>
let currentChatId = null;
let chatToRename = null;
// --- NAVIGATION ---
function switchPanel(panel) {
document.getElementById('panel-chat').classList.toggle('show', panel === 'chat');
document.getElementById('panel-appt').classList.toggle('show', panel === 'appt');
document.getElementById('nav-chat').classList.toggle('active', panel === 'chat');
document.getElementById('nav-appt').classList.toggle('active', panel === 'appt');
}
// --- CHAT CRUD ---
async function createNewChat() {
try {
const res = await fetch('/admin/chat', { method: 'POST' });
const data = await res.json();
<div class="main-content">
if (data.id) {
// Create Element
const newDiv = document.createElement('div');
newDiv.className = 'channel-item active'; // Set active immediately
newDiv.id = `chat-item-${data.id}`;
newDiv.onclick = () => loadChat(data.id, data.title);
newDiv.innerHTML = `
<div style="display: flex; align-items: center; overflow: hidden;">
<span style="color: #72767d; margin-right: 6px;">#</span>
<span class="channel-name" id="title-${data.id}">${data.title}</span>
<div id="settings" class="card">
<h3 style="color: white;">Business Configuration</h3>
<form action="/update-bot" method="POST">
<label style="color: var(--text-muted);">Bot Name</label>
<input type="text" name="bot_name" value="{{ .BotConfig.Name }}">
<label style="color: var(--text-muted);">System Prompt</label>
<textarea name="system_prompt" rows="3" style="width:100%; background:var(--bg-dark); color:white; border:1px solid #40444b; padding:10px;">{{ .BotConfig.Prompt }}</textarea>
<h4 style="color: white; margin-top: 20px;">Open Hours</h4>
{{ range $day := .Days }}
<div class="hours-grid">
<span style="color:white;">{{ $day }}</span>
<select name="{{$day}}_open">
<option value="">Closed</option>
<option value="08:00">08:00 AM</option>
<option value="09:00" selected>09:00 AM</option>
<option value="10:00">10:00 AM</option>
</select>
<select name="{{$day}}_close">
<option value="">Closed</option>
<option value="17:00" selected>05:00 PM</option>
<option value="18:00">06:00 PM</option>
<option value="19:00">07:00 PM</option>
</select>
</div>
<div class="channel-actions" onclick="event.stopPropagation()">
<span class="icon-btn" onclick="openRenameModal(${data.id})">✎</span>
<span class="icon-btn del" onclick="deleteChat(${data.id})">✖</span>
</div>
`;
// Insert after the button container
const list = document.getElementById('channel-list');
const btnContainer = list.firstElementChild; // The div containing the button
if (btnContainer.nextSibling) {
list.insertBefore(newDiv, btnContainer.nextSibling);
} else {
list.appendChild(newDiv);
}
{{ end }}
<button type="submit" style="margin-top: 20px;">Save Changes</button>
</form>
</div>
// Switch and Load
switchPanel('chat');
loadChat(data.id, data.title);
}
} catch (e) {
console.error(e);
alert("Failed to create chat.");
}
}
<div id="appointments" class="card">
<h3 style="color: white;">Manage Appointments</h3>
<table>
<thead>
<tr><th>Client</th><th>Date</th><th>Status</th><th>Action</th></tr>
</thead>
<tbody>
{{ range .Appointments }}
<tr>
<td>{{ .Phone }}</td>
<td>{{ .Date }}</td>
<td class="{{ if eq .Status "cancelled" }}status-cancelled{{ else }}status-confirmed{{ end }}">
{{ .Status }}
</td>
<td>
{{ if ne .Status "cancelled" }}
<form action="/admin/appointment/{{.ID}}/cancel" method="POST" style="margin:0;">
<button style="background:var(--danger); padding: 5px; font-size: 0.8rem;">Cancel</button>
</form>
{{ end }}
</td>
</tr>
{{ else }}
<tr><td colspan="4" style="text-align:center; color:gray;">No appointments yet.</td></tr>
{{ end }}
</tbody>
</table>
<br>
<div id="calendar" style="background: white; padding: 10px; border-radius: 8px;"></div>
</div>
</div>
</div>
async function deleteChat(id) {
if (!confirm("Delete this chat permanently?")) return;
await fetch(`/admin/chat/${id}`, { method: 'DELETE' });
document.getElementById(`chat-item-${id}`).remove();
if (currentChatId === id) {
document.getElementById('messages-pane').innerHTML = '<div style="margin: auto; color: var(--text-muted);">Chat deleted.</div>';
document.getElementById('current-channel-name').innerText = 'deleted';
currentChatId = null;
}
}
function openRenameModal(id) {
chatToRename = id;
document.getElementById('rename-modal').style.display = 'flex';
document.getElementById('new-name-input').value = document.getElementById(`title-${id}`).innerText;
document.getElementById('new-name-input').focus();
}
function closeModal() {
document.getElementById('rename-modal').style.display = 'none';
chatToRename = null;
}
async function submitRename() {
const newTitle = document.getElementById('new-name-input').value;
if (newTitle && chatToRename) {
await fetch(`/admin/chat/${chatToRename}/rename`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ title: newTitle })
<script>
document.addEventListener('DOMContentLoaded', function() {
var calendarEl = document.getElementById('calendar');
var calendar = new FullCalendar.Calendar(calendarEl, {
initialView: 'dayGridMonth',
events: '/api/my-appointments',
height: 400
});
document.getElementById(`title-${chatToRename}`).innerText = newTitle;
if (currentChatId === chatToRename) {
document.getElementById('current-channel-name').innerText = newTitle;
}
closeModal();
}
}
// --- MESSAGING ---
async function loadChat(id, title) {
currentChatId = id;
// Highlight active channel
document.querySelectorAll('.channel-item').forEach(el => el.classList.remove('active'));
const activeItem = document.getElementById(`chat-item-${id}`);
if(activeItem) activeItem.classList.add('active');
document.getElementById('current-channel-name').innerText = title;
document.getElementById('user-input').placeholder = `Message #${title}`;
document.getElementById('user-input').focus();
const pane = document.getElementById('messages-pane');
pane.innerHTML = '<div style="margin: auto; color: var(--text-muted);">Loading history...</div>';
const res = await fetch(`/admin/chat/${id}/messages`);
const messages = await res.json();
pane.innerHTML = '';
if (messages) {
messages.forEach(msg => renderMessage(msg.role, msg.content));
} else {
pane.innerHTML = '<div style="margin: auto; color: var(--text-muted);">No messages yet. Say hello!</div>';
}
scrollToBottom();
}
function renderMessage(role, text) {
const pane = document.getElementById('messages-pane');
const group = document.createElement('div');
group.className = 'message-group';
const avatar = document.createElement('div');
avatar.className = `avatar ${role === 'assistant' ? 'bot' : ''}`;
avatar.innerText = role === 'user' ? 'U' : 'AI';
const contentDiv = document.createElement('div');
contentDiv.className = 'msg-content';
const header = document.createElement('div');
header.className = 'msg-header';
header.innerHTML = `<span class="username">${role === 'user' ? 'User' : 'SekiBot'}</span> <span class="timestamp">${new Date().toLocaleTimeString()}</span>`;
const body = document.createElement('div');
body.className = 'msg-text';
body.innerText = text;
contentDiv.appendChild(header);
contentDiv.appendChild(body);
group.appendChild(avatar);
group.appendChild(contentDiv);
pane.appendChild(group);
return body;
}
async function sendMessage() {
if (!currentChatId) return alert("Select a chat first!");
const input = document.getElementById('user-input');
const content = input.value.trim();
if (!content) return;
input.value = '';
renderMessage('user', content);
scrollToBottom();
const streamContainer = renderMessage('assistant', '');
scrollToBottom();
try {
const response = await fetch(`/admin/chat/${currentChatId}/message`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ content })
});
const reader = response.body.getReader();
const decoder = new TextDecoder();
while (true) {
const { done, value } = await reader.read();
if (done) break;
const chunk = decoder.decode(value, { stream: true });
streamContainer.innerText += chunk;
scrollToBottom();
}
} catch (e) {
streamContainer.innerText += "\n[Error: Connection Failed]";
}
}
function scrollToBottom() {
const pane = document.getElementById('messages-pane');
pane.scrollTop = pane.scrollHeight;
}
function handleEnter(e) {
if (e.key === 'Enter') sendMessage();
}
// --- APPOINTMENTS MANAGER ---
async function createMockAppt() {
const phone = document.getElementById('m-phone').value;
const date = document.getElementById('m-date').value;
if(!phone || !date) return alert("Fill in fields, seki.");
await fetch('/admin/appointment', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({phone, date})
calendar.render();
});
location.reload(); // Reload to refresh table
}
async function updateAppt(id) {
const phone = document.getElementById(`edit-phone-${id}`).value;
const date = document.getElementById(`edit-date-${id}`).value;
const res = await fetch(`/admin/appointment/${id}`, {
method: 'PUT',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({phone, date})
});
if(res.ok) alert("Saved.");
else alert("Update failed.");
}
async function deleteAppt(id) {
if(!confirm("Are you sure?")) return;
const res = await fetch(`/admin/appointment/${id}`, { method: 'DELETE' });
if(res.ok) {
document.getElementById(`appt-${id}`).remove();
}
}
</script>
</script>
</body>
</html>
{{ end }}

31
templates/landing.html Normal file
View File

@@ -0,0 +1,31 @@
{{ define "landing.html" }}
<!DOCTYPE html>
<html>
<head>
<title>SekiBot - AI Receptionist</title>
<link rel="stylesheet" href="/static/style.css">
</head>
<body>
<div class="center-container">
<h1 class="hero-title">Automate Your Appointments</h1>
<p class="hero-sub">The AI receptionist that lives in WhatsApp.</p>
<div style="display: flex; gap: 20px;">
<a href="/login"><button style="width: 150px;">Login</button></a>
<a href="/register"><button class="btn-outline" style="width: 150px;">Get Started</button></a>
</div>
<div style="margin-top: 50px; display: flex; gap: 20px;">
<div class="auth-box" style="width: 200px;">
<h3>24/7 Booking</h3>
<p style="color: grey;">Never miss a client.</p>
</div>
<div class="auth-box" style="width: 200px;">
<h3>WhatsApp Native</h3>
<p style="color: grey;">No apps to install.</p>
</div>
</div>
</div>
</body>
</html>
{{ end }}

20
templates/login.html Normal file
View File

@@ -0,0 +1,20 @@
{{ define "login.html" }}
<!DOCTYPE html>
<html>
<head><title>Login</title><link rel="stylesheet" href="/static/style.css"></head>
<body>
<div class="center-container">
<div class="auth-box">
<h2 style="color: white;">Welcome Back</h2>
{{ if .Error }}<div style="color: red; margin-bottom: 10px;">{{ .Error }}</div>{{ end }}
<form action="/login" method="POST">
<input type="email" name="email" placeholder="Email" required>
<input type="password" name="password" placeholder="Password" required>
<button type="submit">Login</button>
</form>
<p style="margin-top: 15px;"><a href="/register">Create an account</a></p>
</div>
</div>
</body>
</html>
{{ end }}

20
templates/register.html Normal file
View File

@@ -0,0 +1,20 @@
{{ define "register.html" }}
<!DOCTYPE html>
<html>
<head><title>Register</title><link rel="stylesheet" href="/static/style.css"></head>
<body>
<div class="center-container">
<div class="auth-box">
<h2 style="color: white;">Start Free Trial</h2>
{{ if .Error }}<div style="color: red; margin-bottom: 10px;">{{ .Error }}</div>{{ end }}
<form action="/register" method="POST">
<input type="email" name="email" placeholder="Email" required>
<input type="password" name="password" placeholder="Password" required>
<button type="submit">Create Account</button>
</form>
<p style="margin-top: 15px;"><a href="/login">Already have an account?</a></p>
</div>
</div>
</body>
</html>
{{ end }}