modified: bot.db
modified: handlers/dashboard.go modified: main.go modified: services/openrouter.go modified: templates/dashboard.html
This commit is contained in:
@@ -1,275 +1,434 @@
|
||||
{{ define "dashboard.html" }}
|
||||
<!DOCTYPE html>
|
||||
<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>
|
||||
/* DISCORD-ISH THEME */
|
||||
:root { --blurple: #5865F2; --bg-sidebar: #2f3136; --bg-chat: #36393f; --bg-input: #40444b; --text: #dcddde; --success: #43b581; --danger: #f04747; }
|
||||
body { margin: 0; display: flex; height: 100vh; font-family: 'Segoe UI', sans-serif; background: var(--bg-chat); color: var(--text); }
|
||||
|
||||
: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: 260px; background: var(--bg-sidebar); display: flex; flex-direction: column; padding: 15px; border-right: 1px solid #202225; }
|
||||
.chat-list { flex-grow: 1; overflow-y: auto; margin-top: 10px; }
|
||||
.nav-item, .chat-item { padding: 10px; border-radius: 4px; cursor: pointer; margin-bottom: 5px; color: #8e9297; transition: 0.2s; }
|
||||
.nav-item:hover, .chat-item:hover { background: rgba(79,84,92,0.4); color: var(--text); }
|
||||
.nav-item.active, .chat-item.active { background: rgba(79,84,92,0.6); color: white; font-weight: bold; }
|
||||
|
||||
/* LAYOUT */
|
||||
.main-content { flex-grow: 1; display: none; flex-direction: column; height: 100vh; }
|
||||
.show { display: flex !important; }
|
||||
.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; }
|
||||
|
||||
/* CHAT AREA */
|
||||
.messages { flex-grow: 1; overflow-y: auto; padding: 20px; display: flex; flex-direction: column; gap: 15px; scroll-behavior: smooth; }
|
||||
.msg { max-width: 80%; padding: 12px 16px; border-radius: 8px; line-height: 1.5; font-size: 0.95rem; white-space: pre-wrap;}
|
||||
.user { align-self: flex-end; background: var(--blurple); color: white; border-bottom-right-radius: 0; }
|
||||
.assistant { align-self: flex-start; background: var(--bg-input); border-bottom-left-radius: 0; }
|
||||
|
||||
.input-area { padding: 20px; background: var(--bg-chat); margin-bottom: 20px;}
|
||||
#user-input { width: 95%; background: var(--bg-input); border: none; padding: 15px; border-radius: 8px; color: white; outline: none; font-size: 1rem; }
|
||||
.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; }
|
||||
|
||||
/* APPOINTMENTS AREA */
|
||||
.panel-container { padding: 40px; overflow-y: auto; }
|
||||
table { width: 100%; border-collapse: collapse; margin-top: 20px; background: var(--bg-sidebar); border-radius: 8px; overflow: hidden; }
|
||||
.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.8rem; letter-spacing: 1px; }
|
||||
input[type="text"], input[type="datetime-local"] { background: var(--bg-input); border: 1px solid #202225; color: white; padding: 8px; border-radius: 4px; }
|
||||
|
||||
button { cursor: pointer; border: none; padding: 8px 16px; border-radius: 4px; color: white; font-weight: bold; transition: 0.2s; }
|
||||
.btn-primary { background: var(--blurple); width: 100%; padding: 12px; margin-bottom: 10px; }
|
||||
.btn-success { background: var(--success); }
|
||||
.btn-danger { background: var(--danger); }
|
||||
|
||||
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>
|
||||
|
||||
<div class="sidebar">
|
||||
<h2 style="color: white; margin-bottom: 20px;">🤖 SekiBot</h2>
|
||||
|
||||
<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>
|
||||
|
||||
<hr style="border: 0; border-top: 1px solid #42454a; margin: 15px 0;">
|
||||
|
||||
<button class="btn-primary" onclick="createNewChat()">+ New Chat</button>
|
||||
|
||||
<div class="chat-list" id="chat-list-container">
|
||||
{{range .Chats}}
|
||||
<div class="chat-item" onclick="loadChat({{.ID}}, this)" id="chat-link-{{.ID}}">
|
||||
# {{.Title}}
|
||||
{{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>
|
||||
{{end}}
|
||||
<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="main-content show" id="panel-chat">
|
||||
<div class="messages" id="message-pane">
|
||||
<div style="text-align: center; margin-top: 40vh; color: #72767d;">
|
||||
<p>Select a chat from the sidebar to start.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="input-area">
|
||||
<input type="text" id="user-input" placeholder="Message #general..." onkeypress="handleEnter(event)">
|
||||
<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>
|
||||
|
||||
<div class="main-content" id="panel-appt">
|
||||
<div class="panel-container">
|
||||
<h1>Appointments Manager</h1>
|
||||
|
||||
<div style="background: var(--bg-sidebar); padding: 20px; border-radius: 8px; margin: 20px 0;">
|
||||
<h3 style="margin-top: 0;">Add Manual Appointment</h3>
|
||||
<div style="display: flex; gap: 10px;">
|
||||
<input type="text" id="m-phone" placeholder="+56 9 1234 5678" style="flex: 1;">
|
||||
<input type="datetime-local" id="m-date" style="flex: 1;">
|
||||
<button class="btn-success" onclick="createMockAppt()">Add</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>Phone</th>
|
||||
<th>Date (YYYY-MM-DD HH:MM)</th>
|
||||
<th>Status</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{range .Appointments}}
|
||||
<tr id="appt-{{.ID}}">
|
||||
<td>#{{.ID}}</td>
|
||||
<td><input type="text" value="{{.CustomerPhone}}" id="edit-phone-{{.ID}}"></td>
|
||||
<td><input type="text" value="{{.Date}}" id="edit-date-{{.ID}}"></td>
|
||||
<td><span class="status-pill">{{.Status}}</span></td>
|
||||
<td>
|
||||
<button class="btn-success" onclick="updateAppt({{.ID}})">💾</button>
|
||||
<button class="btn-danger" onclick="deleteAppt({{.ID}})">🗑️</button>
|
||||
</td>
|
||||
</tr>
|
||||
{{end}}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
let currentChatId = null;
|
||||
|
||||
// --- NAVIGATION ---
|
||||
function switchPanel(panel) {
|
||||
// Toggle Content
|
||||
document.getElementById('panel-chat').classList.toggle('show', panel === 'chat');
|
||||
document.getElementById('panel-appt').classList.toggle('show', panel === 'appt');
|
||||
|
||||
// Toggle Sidebar Active State
|
||||
document.getElementById('nav-chat').classList.toggle('active', panel === 'chat');
|
||||
document.getElementById('nav-appt').classList.toggle('active', panel === 'appt');
|
||||
}
|
||||
|
||||
// --- CHAT LOGIC (RESTORED) ---
|
||||
<main class="main-panel" id="panel-appt">
|
||||
<div class="appt-container">
|
||||
<h1>Appointments Manager</h1>
|
||||
|
||||
async function createNewChat() {
|
||||
try {
|
||||
const res = await fetch('/admin/chat', { method: 'POST' });
|
||||
const data = await res.json();
|
||||
if (data.id) {
|
||||
location.reload(); // Simple reload to show new chat
|
||||
}
|
||||
} catch (e) {
|
||||
alert("Error creating chat: " + e);
|
||||
}
|
||||
}
|
||||
<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>
|
||||
|
||||
async function loadChat(id, el) {
|
||||
currentChatId = id;
|
||||
<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();
|
||||
|
||||
// UI Cleanup
|
||||
document.querySelectorAll('.chat-item').forEach(i => i.classList.remove('active'));
|
||||
if(el) el.classList.add('active');
|
||||
|
||||
const pane = document.getElementById('message-pane');
|
||||
pane.innerHTML = '<p style="text-align:center; color:#72767d;">Loading messages...</p>';
|
||||
|
||||
// Fetch Messages
|
||||
const res = await fetch(`/admin/chat/${id}/messages`);
|
||||
const messages = await res.json();
|
||||
|
||||
pane.innerHTML = ''; // Clear loading
|
||||
|
||||
if (!messages || messages.length === 0) {
|
||||
pane.innerHTML = '<p style="text-align:center; color:#72767d;">No messages yet. Say hi!</p>';
|
||||
} else {
|
||||
messages.forEach(msg => appendMessage(msg.role, msg.content));
|
||||
}
|
||||
scrollToBottom();
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
// 1. Show User Message
|
||||
appendMessage('user', content);
|
||||
input.value = '';
|
||||
scrollToBottom();
|
||||
|
||||
// 2. Create a placeholder for the AI response
|
||||
// We create the DOM element empty, then fill it as data arrives
|
||||
const aiMsgDiv = document.createElement('div');
|
||||
aiMsgDiv.className = 'msg assistant';
|
||||
document.getElementById('message-pane').appendChild(aiMsgDiv);
|
||||
scrollToBottom();
|
||||
|
||||
try {
|
||||
const response = await fetch(`/admin/chat/${currentChatId}/message`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ content })
|
||||
});
|
||||
|
||||
// 3. Set up the stream reader
|
||||
const reader = response.body.getReader();
|
||||
const decoder = new TextDecoder();
|
||||
|
||||
while (true) {
|
||||
const { done, value } = await reader.read();
|
||||
if (done) break;
|
||||
|
||||
// Decode the chunk (Uint8Array) to string
|
||||
const textChunk = decoder.decode(value, { stream: true });
|
||||
|
||||
// Append to the current message div
|
||||
aiMsgDiv.innerText += textChunk;
|
||||
scrollToBottom();
|
||||
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);
|
||||
}
|
||||
|
||||
} catch (e) {
|
||||
aiMsgDiv.innerText += "\n[Error: Connection lost]";
|
||||
// Switch and Load
|
||||
switchPanel('chat');
|
||||
loadChat(data.id, data.title);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
alert("Failed to create chat.");
|
||||
}
|
||||
}
|
||||
|
||||
function appendMessage(role, text) {
|
||||
const pane = document.getElementById('message-pane');
|
||||
const div = document.createElement('div');
|
||||
div.className = `msg ${role}`;
|
||||
div.innerText = text;
|
||||
pane.appendChild(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 scrollToBottom() {
|
||||
const pane = document.getElementById('message-pane');
|
||||
pane.scrollTop = pane.scrollHeight;
|
||||
}
|
||||
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 handleEnter(e) {
|
||||
if (e.key === 'Enter') sendMessage();
|
||||
}
|
||||
function closeModal() {
|
||||
document.getElementById('rename-modal').style.display = 'none';
|
||||
chatToRename = null;
|
||||
}
|
||||
|
||||
// --- APPOINTMENT LOGIC (KEPT) ---
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
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}`, {
|
||||
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({phone, date})
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ title: newTitle })
|
||||
});
|
||||
|
||||
if(res.ok) alert("Updated successfully.");
|
||||
else alert("Update failed.");
|
||||
}
|
||||
|
||||
async function deleteAppt(id) {
|
||||
if(!confirm("Are you sure you want to delete this appointment?")) return;
|
||||
|
||||
const res = await fetch(`/admin/appointment/${id}`, { method: 'DELETE' });
|
||||
if(res.ok) {
|
||||
document.getElementById(`appt-${id}`).remove();
|
||||
document.getElementById(`title-${chatToRename}`).innerText = newTitle;
|
||||
if (currentChatId === chatToRename) {
|
||||
document.getElementById('current-channel-name').innerText = newTitle;
|
||||
}
|
||||
closeModal();
|
||||
}
|
||||
</script>
|
||||
}
|
||||
|
||||
// --- 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 }}
|
||||
Reference in New Issue
Block a user