modified: bot.db
modified: handlers/dashboard.go modified: main.go modified: services/openrouter.go modified: templates/dashboard.html
This commit is contained in:
@@ -116,16 +116,37 @@ func UpdateAppointmentHandler(c *gin.Context) {
|
||||
|
||||
// POST /admin/chat
|
||||
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')")
|
||||
if err != nil {
|
||||
log.Println("Database Error in NewChat:", err)
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create chat"})
|
||||
c.JSON(500, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
4
main.go
4
main.go
@@ -33,7 +33,9 @@ func main() {
|
||||
r.POST("/admin/appointment", handlers.CreateAppointmentHandler)
|
||||
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.POST("/admin/chat/:id/message", handlers.PostMessageHandler)
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
"whatsapp-bot/db"
|
||||
)
|
||||
|
||||
@@ -41,28 +42,50 @@ func StreamAIResponse(chatHistory []Message, onToken func(string)) (string, erro
|
||||
apiKey := os.Getenv("OPENROUTER_API_KEY")
|
||||
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{
|
||||
{
|
||||
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.",
|
||||
},
|
||||
{Role: "system", Content: systemPrompt},
|
||||
}, chatHistory...)
|
||||
|
||||
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,
|
||||
"stream": true, // <--- THIS IS KEY
|
||||
"stream": true,
|
||||
"tools": []map[string]interface{}{
|
||||
{
|
||||
"type": "function",
|
||||
"function": map[string]interface{}{
|
||||
"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{}{
|
||||
"type": "object",
|
||||
"properties": map[string]interface{}{
|
||||
"customer_phone": map[string]string{"type": "string"},
|
||||
"date": map[string]string{"type": "string"},
|
||||
"customer_phone": map[string]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"},
|
||||
},
|
||||
|
||||
@@ -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