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 */ .nav-item { padding: 10px; border-radius: 4px; cursor: pointer; color: #8e9297; margin-bottom: 2px; font-weight: 500; }
.main-content { flex-grow: 1; display: none; flex-direction: column; height: 100vh; } .nav-item:hover { background: var(--interactive-hover); color: var(--text-normal); }
.show { display: flex !important; } .nav-item.active { background: rgba(79,84,92,0.6); color: white; }
/* CHAT AREA */ .channel-item { display: flex; align-items: center; justify-content: space-between; padding: 8px; border-radius: 4px; cursor: pointer; color: #8e9297; margin-bottom: 2px; }
.messages { flex-grow: 1; overflow-y: auto; padding: 20px; display: flex; flex-direction: column; gap: 15px; scroll-behavior: smooth; } .channel-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;} .channel-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; } .channel-name { flex: 1; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.assistant { align-self: flex-start; background: var(--bg-input); border-bottom-left-radius: 0; } .channel-actions { display: none; gap: 4px; }
.channel-item:hover .channel-actions { display: flex; }
.input-area { padding: 20px; background: var(--bg-chat); margin-bottom: 20px;} /* ICONS */
#user-input { width: 95%; background: var(--bg-input); border: none; padding: 15px; border-radius: 8px; color: white; outline: none; font-size: 1rem; } .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); }
/* APPOINTMENTS AREA */ /* PANELS */
.panel-container { padding: 40px; overflow-y: auto; } .main-panel { flex: 1; display: none; flex-direction: column; background: var(--bg-primary); position: relative; height: 100vh; }
table { width: 100%; border-collapse: collapse; margin-top: 20px; background: var(--bg-sidebar); border-radius: 8px; overflow: hidden; } .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>
<hr style="border: 0; border-top: 1px solid #42454a; margin: 15px 0;"> <div class="sidebar-scroll" id="channel-list">
<div style="padding: 10px;">
<button class="btn-blurple" style="width: 100%;" onclick="createNewChat()">+ New Chat</button>
</div>
<button class="btn-primary" onclick="createNewChat()">+ New Chat</button> {{range .Chats}}
<div class="channel-item" id="chat-item-{{.ID}}" onclick="loadChat({{.ID}}, '{{.Title}}')">
<div class="chat-list" id="chat-list-container"> <div style="display: flex; align-items: center; overflow: hidden;">
{{range .Chats}} <span style="color: #72767d; margin-right: 6px;">#</span>
<div class="chat-item" onclick="loadChat({{.ID}}, this)" id="chat-link-{{.ID}}"> <span class="channel-name" id="title-{{.ID}}">{{.Title}}</span>
# {{.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;"> <div style="background: var(--bg-secondary); padding: 20px; border-radius: 8px; margin-bottom: 30px;">
<h3 style="margin-top: 0;">Add Manual Appointment</h3> <h3 style="margin-top: 0; margin-bottom: 15px;">Add Manual Appointment</h3>
<div style="display: flex; gap: 10px;"> <div style="display: flex; gap: 10px;">
<input type="text" id="m-phone" placeholder="+56 9 1234 5678" style="flex: 1;"> <input type="text" class="edit-field" id="m-phone" placeholder="+56 9 1234 5678" style="flex: 1;">
<input type="datetime-local" id="m-date" style="flex: 1;"> <input type="datetime-local" class="edit-field" id="m-date" style="flex: 1;">
<button class="btn-success" onclick="createMockAppt()">Add</button> <button class="btn-blurple" style="background: var(--success);" onclick="createMockAppt()">Add Booking</button>
</div>
</div> </div>
</div>
<table> <table>
<thead> <thead>
<tr> <tr>
<th>ID</th> <th style="width: 50px;">ID</th>
<th>Phone</th> <th>Phone</th>
<th>Date (YYYY-MM-DD HH:MM)</th> <th>Date (YYYY-MM-DD HH:MM)</th>
<th>Status</th> <th>Status</th>
<th>Actions</th> <th style="width: 100px; text-align: right;">Actions</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{{range .Appointments}} {{range .Appointments}}
<tr id="appt-{{.ID}}"> <tr id="appt-{{.ID}}">
<td>#{{.ID}}</td> <td>#{{.ID}}</td>
<td><input type="text" value="{{.CustomerPhone}}" id="edit-phone-{{.ID}}"></td> <td><input type="text" class="edit-field" value="{{.CustomerPhone}}" id="edit-phone-{{.ID}}"></td>
<td><input type="text" value="{{.Date}}" id="edit-date-{{.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><span class="status-pill">{{.Status}}</span></td>
<td> <td style="text-align: right;">
<button class="btn-success" onclick="updateAppt({{.ID}})">💾</button> <button class="btn-icon" title="Save" onclick="updateAppt({{.ID}})">💾</button>
<button class="btn-danger" onclick="deleteAppt({{.ID}})">🗑️</button> <button class="btn-icon" title="Delete" style="color: var(--danger);" onclick="deleteAppt({{.ID}})">🗑️</button>
</td> </td>
</tr> </tr>
{{end}} {{end}}
</tbody> </tbody>
</table> </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> </div>
</div>
<script> <script>
let currentChatId = null; let currentChatId = null;
let chatToRename = null;
// --- NAVIGATION --- // --- NAVIGATION ---
function switchPanel(panel) { function switchPanel(panel) {
// Toggle Content document.getElementById('panel-chat').classList.toggle('show', panel === 'chat');
document.getElementById('panel-chat').classList.toggle('show', panel === 'chat'); document.getElementById('panel-appt').classList.toggle('show', panel === 'appt');
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-chat').classList.toggle('active', panel === 'chat'); document.getElementById('nav-appt').classList.toggle('active', panel === 'appt');
document.getElementById('nav-appt').classList.toggle('active', panel === 'appt'); }
}
// --- CHAT LOGIC (RESTORED) --- // --- CHAT CRUD ---
async function createNewChat() { async function createNewChat() {
try { try {
const res = await fetch('/admin/chat', { method: 'POST' }); const res = await fetch('/admin/chat', { method: 'POST' });
const data = await res.json(); const data = await res.json();
if (data.id) {
location.reload(); // Simple reload to show new chat
}
} catch (e) {
alert("Error creating chat: " + e);
}
}
async function loadChat(id, el) { if (data.id) {
currentChatId = 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);
// UI Cleanup newDiv.innerHTML = `
document.querySelectorAll('.chat-item').forEach(i => i.classList.remove('active')); <div style="display: flex; align-items: center; overflow: hidden;">
if(el) el.classList.add('active'); <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>
`;
const pane = document.getElementById('message-pane'); // Insert after the button container
pane.innerHTML = '<p style="text-align:center; color:#72767d;">Loading messages...</p>'; const list = document.getElementById('channel-list');
const btnContainer = list.firstElementChild; // The div containing the button
// Fetch Messages if (btnContainer.nextSibling) {
const res = await fetch(`/admin/chat/${id}/messages`); list.insertBefore(newDiv, btnContainer.nextSibling);
const messages = await res.json(); } else {
list.appendChild(newDiv);
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();
} }
} 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 (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 })
}); });
if(res.ok) alert("Updated successfully."); const reader = response.body.getReader();
else alert("Update failed."); const decoder = new TextDecoder();
}
async function deleteAppt(id) { while (true) {
if(!confirm("Are you sure you want to delete this appointment?")) return; const { done, value } = await reader.read();
if (done) break;
const res = await fetch(`/admin/appointment/${id}`, { method: 'DELETE' }); const chunk = decoder.decode(value, { stream: true });
if(res.ok) { streamContainer.innerText += chunk;
document.getElementById(`appt-${id}`).remove(); scrollToBottom();
} }
} catch (e) {
streamContainer.innerText += "\n[Error: Connection Failed]";
} }
</script> }
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 }}