modified: handlers/dashboard.go modified: main.go modified: services/openrouter.go modified: templates/dashboard.html
434 lines
19 KiB
HTML
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 }} |