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
|
||||
|
||||
2
main.go
2
main.go
@@ -34,6 +34,8 @@ func main() {
|
||||
r.PUT("/admin/appointment/:id", handlers.UpdateAppointmentHandler)
|
||||
|
||||
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,173 +1,350 @@
|
||||
{{ 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; }
|
||||
.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; }
|
||||
|
||||
/* LAYOUT */
|
||||
.main-content { flex-grow: 1; display: none; flex-direction: column; height: 100vh; }
|
||||
.show { display: flex !important; }
|
||||
.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; }
|
||||
|
||||
/* 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; }
|
||||
.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; }
|
||||
|
||||
.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; }
|
||||
/* 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); }
|
||||
|
||||
/* 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; }
|
||||
/* 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>
|
||||
|
||||
<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>
|
||||
|
||||
<div class="chat-list" id="chat-list-container">
|
||||
{{range .Chats}}
|
||||
<div class="chat-item" onclick="loadChat({{.ID}}, this)" id="chat-link-{{.ID}}">
|
||||
# {{.Title}}
|
||||
<div class="channel-item" id="chat-item-{{.ID}}" onclick="loadChat({{.ID}}, '{{.Title}}')">
|
||||
<div style="display: flex; align-items: center; overflow: hidden;">
|
||||
<span style="color: #72767d; margin-right: 6px;">#</span>
|
||||
<span class="channel-name" id="title-{{.ID}}">{{.Title}}</span>
|
||||
</div>
|
||||
<div class="channel-actions" onclick="event.stopPropagation()">
|
||||
<span class="icon-btn" onclick="openRenameModal({{.ID}})">✎</span>
|
||||
<span class="icon-btn del" onclick="deleteChat({{.ID}})">✖</span>
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<main class="main-panel show" id="panel-chat">
|
||||
<div class="chat-header">
|
||||
<span style="color: #72767d; margin-right: 6px; font-size: 1.2em;">#</span>
|
||||
<span id="current-channel-name">general</span>
|
||||
</div>
|
||||
|
||||
<div class="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="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" id="panel-appt">
|
||||
<div class="panel-container">
|
||||
<div class="input-wrapper">
|
||||
<div class="input-bg">
|
||||
<input type="text" class="chat-input" id="user-input" placeholder="Message #general" onkeypress="handleEnter(event)">
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<main class="main-panel" id="panel-appt">
|
||||
<div class="appt-container">
|
||||
<h1>Appointments Manager</h1>
|
||||
|
||||
<div style="background: var(--bg-sidebar); padding: 20px; border-radius: 8px; margin: 20px 0;">
|
||||
<h3 style="margin-top: 0;">Add Manual Appointment</h3>
|
||||
<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" 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>
|
||||
<input type="text" class="edit-field" id="m-phone" placeholder="+56 9 1234 5678" style="flex: 1;">
|
||||
<input type="datetime-local" class="edit-field" id="m-date" style="flex: 1;">
|
||||
<button class="btn-blurple" style="background: var(--success);" onclick="createMockAppt()">Add Booking</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th style="width: 50px;">ID</th>
|
||||
<th>Phone</th>
|
||||
<th>Date (YYYY-MM-DD HH:MM)</th>
|
||||
<th>Status</th>
|
||||
<th>Actions</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" 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="{{.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>
|
||||
<button class="btn-success" onclick="updateAppt({{.ID}})">💾</button>
|
||||
<button class="btn-danger" onclick="deleteAppt({{.ID}})">🗑️</button>
|
||||
<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>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<script>
|
||||
<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) {
|
||||
// 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) ---
|
||||
// --- CHAT CRUD ---
|
||||
|
||||
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
|
||||
// Create Element
|
||||
const newDiv = document.createElement('div');
|
||||
newDiv.className = 'channel-item active'; // Set active immediately
|
||||
newDiv.id = `chat-item-${data.id}`;
|
||||
newDiv.onclick = () => loadChat(data.id, data.title);
|
||||
|
||||
newDiv.innerHTML = `
|
||||
<div style="display: flex; align-items: center; overflow: hidden;">
|
||||
<span style="color: #72767d; margin-right: 6px;">#</span>
|
||||
<span class="channel-name" id="title-${data.id}">${data.title}</span>
|
||||
</div>
|
||||
<div class="channel-actions" onclick="event.stopPropagation()">
|
||||
<span class="icon-btn" onclick="openRenameModal(${data.id})">✎</span>
|
||||
<span class="icon-btn del" onclick="deleteChat(${data.id})">✖</span>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Insert after the button container
|
||||
const list = document.getElementById('channel-list');
|
||||
const btnContainer = list.firstElementChild; // The div containing the button
|
||||
if (btnContainer.nextSibling) {
|
||||
list.insertBefore(newDiv, btnContainer.nextSibling);
|
||||
} else {
|
||||
list.appendChild(newDiv);
|
||||
}
|
||||
|
||||
// Switch and Load
|
||||
switchPanel('chat');
|
||||
loadChat(data.id, data.title);
|
||||
}
|
||||
} catch (e) {
|
||||
alert("Error creating chat: " + e);
|
||||
console.error(e);
|
||||
alert("Failed to create chat.");
|
||||
}
|
||||
}
|
||||
|
||||
async function loadChat(id, el) {
|
||||
async function deleteChat(id) {
|
||||
if (!confirm("Delete this chat permanently?")) return;
|
||||
await fetch(`/admin/chat/${id}`, { method: 'DELETE' });
|
||||
document.getElementById(`chat-item-${id}`).remove();
|
||||
if (currentChatId === id) {
|
||||
document.getElementById('messages-pane').innerHTML = '<div style="margin: auto; color: var(--text-muted);">Chat deleted.</div>';
|
||||
document.getElementById('current-channel-name').innerText = 'deleted';
|
||||
currentChatId = null;
|
||||
}
|
||||
}
|
||||
|
||||
function openRenameModal(id) {
|
||||
chatToRename = id;
|
||||
document.getElementById('rename-modal').style.display = 'flex';
|
||||
document.getElementById('new-name-input').value = document.getElementById(`title-${id}`).innerText;
|
||||
document.getElementById('new-name-input').focus();
|
||||
}
|
||||
|
||||
function closeModal() {
|
||||
document.getElementById('rename-modal').style.display = 'none';
|
||||
chatToRename = null;
|
||||
}
|
||||
|
||||
async function submitRename() {
|
||||
const newTitle = document.getElementById('new-name-input').value;
|
||||
if (newTitle && chatToRename) {
|
||||
await fetch(`/admin/chat/${chatToRename}/rename`, {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ title: newTitle })
|
||||
});
|
||||
document.getElementById(`title-${chatToRename}`).innerText = newTitle;
|
||||
if (currentChatId === chatToRename) {
|
||||
document.getElementById('current-channel-name').innerText = newTitle;
|
||||
}
|
||||
closeModal();
|
||||
}
|
||||
}
|
||||
|
||||
// --- MESSAGING ---
|
||||
|
||||
async function loadChat(id, title) {
|
||||
currentChatId = id;
|
||||
|
||||
// UI Cleanup
|
||||
document.querySelectorAll('.chat-item').forEach(i => i.classList.remove('active'));
|
||||
if(el) el.classList.add('active');
|
||||
// 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');
|
||||
|
||||
const pane = document.getElementById('message-pane');
|
||||
pane.innerHTML = '<p style="text-align:center; color:#72767d;">Loading messages...</p>';
|
||||
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>';
|
||||
|
||||
// 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>';
|
||||
pane.innerHTML = '';
|
||||
if (messages) {
|
||||
messages.forEach(msg => renderMessage(msg.role, msg.content));
|
||||
} else {
|
||||
messages.forEach(msg => appendMessage(msg.role, msg.content));
|
||||
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!");
|
||||
|
||||
@@ -175,16 +352,11 @@
|
||||
const content = input.value.trim();
|
||||
if (!content) return;
|
||||
|
||||
// 1. Show User Message
|
||||
appendMessage('user', content);
|
||||
input.value = '';
|
||||
renderMessage('user', content);
|
||||
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);
|
||||
const streamContainer = renderMessage('assistant', '');
|
||||
scrollToBottom();
|
||||
|
||||
try {
|
||||
@@ -194,37 +366,23 @@
|
||||
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;
|
||||
const chunk = decoder.decode(value, { stream: true });
|
||||
streamContainer.innerText += chunk;
|
||||
scrollToBottom();
|
||||
}
|
||||
|
||||
} catch (e) {
|
||||
aiMsgDiv.innerText += "\n[Error: Connection lost]";
|
||||
streamContainer.innerText += "\n[Error: Connection Failed]";
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
function scrollToBottom() {
|
||||
const pane = document.getElementById('message-pane');
|
||||
const pane = document.getElementById('messages-pane');
|
||||
pane.scrollTop = pane.scrollHeight;
|
||||
}
|
||||
|
||||
@@ -232,7 +390,7 @@
|
||||
if (e.key === 'Enter') sendMessage();
|
||||
}
|
||||
|
||||
// --- APPOINTMENT LOGIC (KEPT) ---
|
||||
// --- APPOINTMENTS MANAGER ---
|
||||
|
||||
async function createMockAppt() {
|
||||
const phone = document.getElementById('m-phone').value;
|
||||
@@ -244,7 +402,7 @@
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: JSON.stringify({phone, date})
|
||||
});
|
||||
location.reload();
|
||||
location.reload(); // Reload to refresh table
|
||||
}
|
||||
|
||||
async function updateAppt(id) {
|
||||
@@ -257,19 +415,20 @@
|
||||
body: JSON.stringify({phone, date})
|
||||
});
|
||||
|
||||
if(res.ok) alert("Updated successfully.");
|
||||
if(res.ok) alert("Saved.");
|
||||
else alert("Update failed.");
|
||||
}
|
||||
|
||||
async function deleteAppt(id) {
|
||||
if(!confirm("Are you sure you want to delete this appointment?")) return;
|
||||
|
||||
if(!confirm("Are you sure?")) return;
|
||||
const res = await fetch(`/admin/appointment/${id}`, { method: 'DELETE' });
|
||||
if(res.ok) {
|
||||
document.getElementById(`appt-${id}`).remove();
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
{{ end }}
|
||||
Reference in New Issue
Block a user