modified: bot.db

modified:   handlers/dashboard.go
	modified:   main.go
	modified:   services/openrouter.go
	modified:   templates/dashboard.html
This commit is contained in:
2026-03-01 07:39:40 -03:00
parent 6f7987c3fe
commit e47b12b0d9
5 changed files with 452 additions and 247 deletions

BIN
bot.db

Binary file not shown.

View File

@@ -116,16 +116,37 @@ func UpdateAppointmentHandler(c *gin.Context) {
// POST /admin/chat // POST /admin/chat
func NewChatHandler(c *gin.Context) { func NewChatHandler(c *gin.Context) {
// Insert a new chat record. In SQLite, this is enough to generate an ID.
res, err := db.Conn.Exec("INSERT INTO chats (title) VALUES ('New Chat')") res, err := db.Conn.Exec("INSERT INTO chats (title) VALUES ('New Chat')")
if err != nil { if err != nil {
log.Println("Database Error in NewChat:", err) c.JSON(500, gin.H{"error": err.Error()})
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create chat"})
return return
} }
id, _ := res.LastInsertId() id, _ := res.LastInsertId()
c.JSON(http.StatusOK, gin.H{"id": id}) // Return the ID so the frontend can select it immediately
c.JSON(200, gin.H{"id": id, "title": "New Chat"})
}
// DELETE /admin/chat/:id
func DeleteChatHandler(c *gin.Context) {
id := c.Param("id")
// Delete messages first (foreign key cleanup usually, but we'll do manual for SQLite safety)
db.Conn.Exec("DELETE FROM messages WHERE chat_id = ?", id)
db.Conn.Exec("DELETE FROM chats WHERE id = ?", id)
c.Status(200)
}
// PUT /admin/chat/:id/rename
func RenameChatHandler(c *gin.Context) {
id := c.Param("id")
var body struct {
Title string `json:"title"`
}
if err := c.BindJSON(&body); err != nil {
c.Status(400)
return
}
db.Conn.Exec("UPDATE chats SET title = ? WHERE id = ?", body.Title, id)
c.Status(200)
} }
// GET /admin/chat/:id/messages // GET /admin/chat/:id/messages

View File

@@ -33,7 +33,9 @@ func main() {
r.POST("/admin/appointment", handlers.CreateAppointmentHandler) r.POST("/admin/appointment", handlers.CreateAppointmentHandler)
r.PUT("/admin/appointment/:id", handlers.UpdateAppointmentHandler) r.PUT("/admin/appointment/:id", handlers.UpdateAppointmentHandler)
r.POST("/admin/chat", handlers.NewChatHandler) // THE BUTTON HITS THIS r.POST("/admin/chat", handlers.NewChatHandler) // THE BUTTON HITS THIS
r.DELETE("/admin/chat/:id", handlers.DeleteChatHandler) // <--- ADD THIS
r.PUT("/admin/chat/:id/rename", handlers.RenameChatHandler) // <--- ADD THIS
r.GET("/admin/chat/:id/messages", handlers.GetMessagesHandler) r.GET("/admin/chat/:id/messages", handlers.GetMessagesHandler)
r.POST("/admin/chat/:id/message", handlers.PostMessageHandler) r.POST("/admin/chat/:id/message", handlers.PostMessageHandler)

View File

@@ -8,6 +8,7 @@ import (
"net/http" "net/http"
"os" "os"
"strings" "strings"
"time"
"whatsapp-bot/db" "whatsapp-bot/db"
) )
@@ -41,28 +42,50 @@ func StreamAIResponse(chatHistory []Message, onToken func(string)) (string, erro
apiKey := os.Getenv("OPENROUTER_API_KEY") apiKey := os.Getenv("OPENROUTER_API_KEY")
url := "https://openrouter.ai/api/v1/chat/completions" url := "https://openrouter.ai/api/v1/chat/completions"
// 1. Get Current Time for the LLM
currentTime := time.Now().Format("Monday, 2006-01-02 15:04")
// 2. Strict System Prompt
systemPrompt := fmt.Sprintf(`
You are a helpful scheduler assistant for a business in Chile.
Current Date/Time: %s
RULES FOR BOOKING:
1. You MUST get three things from the user before booking:
- The Date (day/month)
- The Time (hour)
- The Phone Number
2. If any of these are missing, ASK for them. Do NOT assume or guess.
3. If the user says "tomorrow" or "next Friday", calculate the date based on the Current Date/Time above.
4. Only when you have all details, use the 'create_appointment' tool.
`, currentTime)
// 3. Prepend System Prompt to History
fullMessages := append([]Message{ fullMessages := append([]Message{
{ {Role: "system", Content: systemPrompt},
Role: "system",
Content: "You are a helpful business assistant. You can book appointments. If the user mentions scheduling an appointment always ask for his phone number and the time and date of the appointment, if the user wants to schedule something before doing it, it is requiered to have a phone number and a date, use the create_appointment tool. Be concise and polite.",
},
}, chatHistory...) }, chatHistory...)
payload := map[string]interface{}{ payload := map[string]interface{}{
"model": "arcee-ai/trinity-large-preview:free", // arcee-ai/trinity-large-preview:free, stepfun/step-3.5-flash:free "model": "arcee-ai/trinity-large-preview:free", // stepfun/step-3.5-flash:free, arcee-ai/trinity-large-preview:free
"messages": fullMessages, "messages": fullMessages,
"stream": true, // <--- THIS IS KEY "stream": true,
"tools": []map[string]interface{}{ "tools": []map[string]interface{}{
{ {
"type": "function", "type": "function",
"function": map[string]interface{}{ "function": map[string]interface{}{
"name": "create_appointment", "name": "create_appointment",
"description": "Schedules a new appointment", "description": "Schedules a new appointment. ONLY use this when you have a confirm date, time, and phone number.",
"parameters": map[string]interface{}{ "parameters": map[string]interface{}{
"type": "object", "type": "object",
"properties": map[string]interface{}{ "properties": map[string]interface{}{
"customer_phone": map[string]string{"type": "string"}, "customer_phone": map[string]string{
"date": map[string]string{"type": "string"}, "type": "string",
"description": "The user's phone number (e.g., +569...)",
},
"date": map[string]string{
"type": "string",
"description": "The full date and time in YYYY-MM-DD HH:MM format",
},
}, },
"required": []string{"customer_phone", "date"}, "required": []string{"customer_phone", "date"},
}, },

View File

@@ -1,275 +1,434 @@
{{ define "dashboard.html" }} {{ define "dashboard.html" }}
<!DOCTYPE html> <!DOCTYPE html>
<html> <html lang="en">
<head> <head>
<meta charset="UTF-8">
<title>SekiBot | Dashboard</title> <title>SekiBot | Dashboard</title>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&display=swap" rel="stylesheet">
<style> <style>
/* DISCORD-ISH THEME */ :root {
:root { --blurple: #5865F2; --bg-sidebar: #2f3136; --bg-chat: #36393f; --bg-input: #40444b; --text: #dcddde; --success: #43b581; --danger: #f04747; } --bg-tertiary: #202225;
body { margin: 0; display: flex; height: 100vh; font-family: 'Segoe UI', sans-serif; background: var(--bg-chat); color: var(--text); } --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 */
.sidebar { width: 260px; background: var(--bg-sidebar); display: flex; flex-direction: column; padding: 15px; border-right: 1px solid #202225; } .sidebar { width: 240px; background: var(--bg-secondary); display: flex; flex-direction: column; flex-shrink: 0; }
.chat-list { flex-grow: 1; overflow-y: auto; margin-top: 10px; } .sidebar-header { padding: 16px; border-bottom: 1px solid #202225; font-weight: 600; color: white; display: flex; align-items: center; justify-content: space-between; }
.nav-item, .chat-item { padding: 10px; border-radius: 4px; cursor: pointer; margin-bottom: 5px; color: #8e9297; transition: 0.2s; } .sidebar-nav { padding: 10px; border-bottom: 1px solid #202225; }
.nav-item:hover, .chat-item:hover { background: rgba(79,84,92,0.4); color: var(--text); } .sidebar-scroll { flex: 1; overflow-y: auto; padding: 8px; }
.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; }
/* CHAT AREA */ .nav-item { padding: 10px; border-radius: 4px; cursor: pointer; color: #8e9297; margin-bottom: 2px; font-weight: 500; }
.messages { flex-grow: 1; overflow-y: auto; padding: 20px; display: flex; flex-direction: column; gap: 15px; scroll-behavior: smooth; } .nav-item:hover { background: var(--interactive-hover); color: var(--text-normal); }
.msg { max-width: 80%; padding: 12px 16px; border-radius: 8px; line-height: 1.5; font-size: 0.95rem; white-space: pre-wrap;} .nav-item.active { background: rgba(79,84,92,0.6); color: white; }
.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; }
/* APPOINTMENTS AREA */ .channel-item { display: flex; align-items: center; justify-content: space-between; padding: 8px; border-radius: 4px; cursor: pointer; color: #8e9297; margin-bottom: 2px; }
.panel-container { padding: 40px; overflow-y: auto; } .channel-item:hover { background: var(--interactive-hover); color: var(--text-normal); }
table { width: 100%; border-collapse: collapse; margin-top: 20px; background: var(--bg-sidebar); border-radius: 8px; overflow: hidden; } .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, 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; } th { background: #202225; text-transform: uppercase; font-size: 0.75rem; letter-spacing: 1px; color: var(--text-muted); }
input[type="text"], input[type="datetime-local"] { background: var(--bg-input); border: 1px solid #202225; color: white; padding: 8px; border-radius: 4px; } input.edit-field { background: var(--bg-input); border: 1px solid #202225; color: white; padding: 8px; border-radius: 4px; width: 100%; box-sizing: border-box; }
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); }
.status-pill { background: #faa61a; color: black; padding: 4px 8px; border-radius: 12px; font-size: 0.75rem; font-weight: bold; text-transform: uppercase; } .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> </style>
</head> </head>
<body> <body>
<div class="sidebar"> <nav class="sidebar">
<h2 style="color: white; margin-bottom: 20px;">🤖 SekiBot</h2> <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 active" onclick="switchPanel('chat')" id="nav-chat">💬 Conversations</div>
<div class="nav-item" onclick="switchPanel('appt')" id="nav-appt">📅 Appointments</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;"> {{range .Chats}}
<div class="channel-item" id="chat-item-{{.ID}}" onclick="loadChat({{.ID}}, '{{.Title}}')">
<button class="btn-primary" onclick="createNewChat()">+ New Chat</button> <div style="display: flex; align-items: center; overflow: hidden;">
<span style="color: #72767d; margin-right: 6px;">#</span>
<div class="chat-list" id="chat-list-container"> <span class="channel-name" id="title-{{.ID}}">{{.Title}}</span>
{{range .Chats}}
<div class="chat-item" onclick="loadChat({{.ID}}, this)" id="chat-link-{{.ID}}">
# {{.Title}}
</div> </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> </div>
<div class="main-content show" id="panel-chat"> <div class="input-wrapper">
<div class="messages" id="message-pane"> <div class="input-bg">
<div style="text-align: center; margin-top: 40vh; color: #72767d;"> <input type="text" class="chat-input" id="user-input" placeholder="Message #general" onkeypress="handleEnter(event)">
<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> </div>
</div> </div>
</main>
<div class="main-content" id="panel-appt"> <main class="main-panel" id="panel-appt">
<div class="panel-container"> <div class="appt-container">
<h1>Appointments Manager</h1> <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) ---
async function createNewChat() { <div style="background: var(--bg-secondary); padding: 20px; border-radius: 8px; margin-bottom: 30px;">
try { <h3 style="margin-top: 0; margin-bottom: 15px;">Add Manual Appointment</h3>
const res = await fetch('/admin/chat', { method: 'POST' }); <div style="display: flex; gap: 10px;">
const data = await res.json(); <input type="text" class="edit-field" id="m-phone" placeholder="+56 9 1234 5678" style="flex: 1;">
if (data.id) { <input type="datetime-local" class="edit-field" id="m-date" style="flex: 1;">
location.reload(); // Simple reload to show new chat <button class="btn-blurple" style="background: var(--success);" onclick="createMockAppt()">Add Booking</button>
} </div>
} catch (e) { </div>
alert("Error creating chat: " + e);
}
}
async function loadChat(id, el) { <table>
currentChatId = id; <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 if (data.id) {
document.querySelectorAll('.chat-item').forEach(i => i.classList.remove('active')); // Create Element
if(el) el.classList.add('active'); const newDiv = document.createElement('div');
newDiv.className = 'channel-item active'; // Set active immediately
const pane = document.getElementById('message-pane'); newDiv.id = `chat-item-${data.id}`;
pane.innerHTML = '<p style="text-align:center; color:#72767d;">Loading messages...</p>'; newDiv.onclick = () => loadChat(data.id, data.title);
// Fetch Messages newDiv.innerHTML = `
const res = await fetch(`/admin/chat/${id}/messages`); <div style="display: flex; align-items: center; overflow: hidden;">
const messages = await res.json(); <span style="color: #72767d; margin-right: 6px;">#</span>
<span class="channel-name" id="title-${data.id}">${data.title}</span>
pane.innerHTML = ''; // Clear loading </div>
<div class="channel-actions" onclick="event.stopPropagation()">
if (!messages || messages.length === 0) { <span class="icon-btn" onclick="openRenameModal(${data.id})">✎</span>
pane.innerHTML = '<p style="text-align:center; color:#72767d;">No messages yet. Say hi!</p>'; <span class="icon-btn del" onclick="deleteChat(${data.id})">✖</span>
} else { </div>
messages.forEach(msg => appendMessage(msg.role, msg.content)); `;
}
scrollToBottom(); // Insert after the button container
} const list = document.getElementById('channel-list');
const btnContainer = list.firstElementChild; // The div containing the button
async function sendMessage() { if (btnContainer.nextSibling) {
if (!currentChatId) return alert("Select a chat first!"); list.insertBefore(newDiv, btnContainer.nextSibling);
} else {
const input = document.getElementById('user-input'); list.appendChild(newDiv);
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();
} }
} catch (e) { // Switch and Load
aiMsgDiv.innerText += "\n[Error: Connection lost]"; switchPanel('chat');
loadChat(data.id, data.title);
} }
} catch (e) {
console.error(e);
alert("Failed to create chat.");
} }
}
function appendMessage(role, text) { async function deleteChat(id) {
const pane = document.getElementById('message-pane'); if (!confirm("Delete this chat permanently?")) return;
const div = document.createElement('div'); await fetch(`/admin/chat/${id}`, { method: 'DELETE' });
div.className = `msg ${role}`; document.getElementById(`chat-item-${id}`).remove();
div.innerText = text; if (currentChatId === id) {
pane.appendChild(div); 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() { function openRenameModal(id) {
const pane = document.getElementById('message-pane'); chatToRename = id;
pane.scrollTop = pane.scrollHeight; 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) { function closeModal() {
if (e.key === 'Enter') sendMessage(); document.getElementById('rename-modal').style.display = 'none';
} chatToRename = null;
}
// --- APPOINTMENT LOGIC (KEPT) --- async function submitRename() {
const newTitle = document.getElementById('new-name-input').value;
async function createMockAppt() { if (newTitle && chatToRename) {
const phone = document.getElementById('m-phone').value; await fetch(`/admin/chat/${chatToRename}/rename`, {
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}`, {
method: 'PUT', method: 'PUT',
headers: {'Content-Type': 'application/json'}, headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({phone, date}) body: JSON.stringify({ title: newTitle })
}); });
document.getElementById(`title-${chatToRename}`).innerText = newTitle;
if(res.ok) alert("Updated successfully."); if (currentChatId === chatToRename) {
else alert("Update failed."); document.getElementById('current-channel-name').innerText = newTitle;
}
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();
} }
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> </body>
</html> </html>
{{ end }} {{ end }}