Files
whatsapp-bot/templates/dashboard.html
SekiDesu0 e47b12b0d9 modified: bot.db
modified:   handlers/dashboard.go
	modified:   main.go
	modified:   services/openrouter.go
	modified:   templates/dashboard.html
2026-03-01 07:39:40 -03:00

434 lines
19 KiB
HTML

{{ define "dashboard.html" }}
<!DOCTYPE html>
<html lang="en">
<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>
</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>
</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();
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>
<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);
}
// Switch and Load
switchPanel('chat');
loadChat(data.id, data.title);
}
} catch (e) {
console.error(e);
alert("Failed to create chat.");
}
}
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 })
});
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})
});
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>
</body>
</html>
{{ end }}