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:10:01 -03:00
parent d4d395356b
commit 6f7987c3fe
5 changed files with 328 additions and 151 deletions

BIN
bot.db

Binary file not shown.

View File

@@ -60,29 +60,6 @@ func ShowDashboard(c *gin.Context) {
}) })
} }
// Test OpenRouter via the Dashboard
func TestAIHandler(c *gin.Context) {
var body struct {
Prompt string `json:"prompt"`
}
if err := c.BindJSON(&body); err != nil {
c.JSON(400, gin.H{"response": "Invalid request, dummy."})
return
}
// Calling the service we wrote earlier
response, err := services.GetAIResponse([]services.Message{
{Role: "user", Content: body.Prompt},
})
if err != nil {
c.JSON(500, gin.H{"response": "AI Error: " + err.Error()})
return
}
c.JSON(200, gin.H{"response": response})
}
// Add this to handlers/dashboard.go // Add this to handlers/dashboard.go
func CreateAppointmentHandler(c *gin.Context) { func CreateAppointmentHandler(c *gin.Context) {
var body struct { var body struct {
@@ -172,25 +149,38 @@ func PostMessageHandler(c *gin.Context) {
var body struct { var body struct {
Content string `json:"content"` Content string `json:"content"`
} }
c.BindJSON(&body) if err := c.BindJSON(&body); err != nil {
c.Status(400)
return
}
// 1. Save User Message // 1. Save User Message to DB first
db.Conn.Exec("INSERT INTO messages (chat_id, role, content) VALUES (?, 'user', ?)", chatId, body.Content) db.Conn.Exec("INSERT INTO messages (chat_id, role, content) VALUES (?, 'user', ?)", chatId, body.Content)
// 2. Fetch history for AI // 2. Fetch history
rows, _ := db.Conn.Query("SELECT role, content FROM messages WHERE chat_id = ? ORDER BY created_at ASC", chatId) rows, _ := db.Conn.Query("SELECT role, content FROM messages WHERE chat_id = ? ORDER BY created_at ASC", chatId)
var history []services.Message var history []services.Message
defer rows.Close()
for rows.Next() { for rows.Next() {
var m services.Message var m services.Message
rows.Scan(&m.Role, &m.Content) rows.Scan(&m.Role, &m.Content)
history = append(history, m) history = append(history, m)
} }
// 3. Get AI Response // 3. Set Headers for Streaming
aiResp, _ := services.GetAIResponse(history) c.Writer.Header().Set("Content-Type", "text/event-stream")
c.Writer.Header().Set("Cache-Control", "no-cache")
c.Writer.Header().Set("Connection", "keep-alive")
c.Writer.Header().Set("Transfer-Encoding", "chunked")
// 4. Save Assistant Message // 4. Call the Stream Service
db.Conn.Exec("INSERT INTO messages (chat_id, role, content) VALUES (?, 'assistant', ?)", chatId, aiResp) // We pass a function that writes chunks directly to the HTTP response
fullResponse, _ := services.StreamAIResponse(history, func(chunk string) {
c.Writer.Write([]byte(chunk))
c.Writer.Flush() // Important: Send it NOW, don't buffer
})
c.JSON(200, gin.H{"response": aiResp}) // 5. Save the FULL Assistant Message to DB for history
// We do this AFTER the stream finishes so the next load has the full text
db.Conn.Exec("INSERT INTO messages (chat_id, role, content) VALUES (?, 'assistant', ?)", chatId, fullResponse)
} }

View File

@@ -28,7 +28,7 @@ func main() {
r.GET("/webhook", handlers.VerifyWebhook) r.GET("/webhook", handlers.VerifyWebhook)
r.POST("/webhook", handlers.HandleMessage) r.POST("/webhook", handlers.HandleMessage)
r.GET("/dashboard", handlers.ShowDashboard) r.GET("/dashboard", handlers.ShowDashboard)
r.POST("/admin/test-ai", handlers.TestAIHandler)
r.DELETE("/admin/appointment/:id", handlers.DeleteAppointmentHandler) r.DELETE("/admin/appointment/:id", handlers.DeleteAppointmentHandler)
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)

View File

@@ -1,12 +1,14 @@
package services package services
import ( import (
bytes "bytes" "bufio"
json "encoding/json" "bytes"
io "io" "encoding/json"
http "net/http" "fmt"
os "os" "net/http"
"whatsapp-bot/db" // Ensure this matches your module name "os"
"strings"
"whatsapp-bot/db"
) )
type Message struct { type Message struct {
@@ -14,9 +16,10 @@ type Message struct {
Content string `json:"content"` Content string `json:"content"`
} }
type OpenRouterResponse struct { // Structs to parse the incoming stream from OpenRouter
type StreamResponse struct {
Choices []struct { Choices []struct {
Message struct { Delta struct {
Content string `json:"content"` Content string `json:"content"`
ToolCalls []struct { ToolCalls []struct {
ID string `json:"id"` ID string `json:"id"`
@@ -26,37 +29,40 @@ type OpenRouterResponse struct {
Arguments string `json:"arguments"` Arguments string `json:"arguments"`
} `json:"function"` } `json:"function"`
} `json:"tool_calls"` } `json:"tool_calls"`
} `json:"message"` } `json:"delta"`
FinishReason string `json:"finish_reason"`
} `json:"choices"` } `json:"choices"`
} }
func GetAIResponse(chatHistory []Message) (string, error) { // StreamAIResponse handles the streaming connection
// onToken: a function that gets called every time we get text (we use this to push to the browser)
// Returns: the full final string (so we can save it to the DB)
func StreamAIResponse(chatHistory []Message, onToken func(string)) (string, error) {
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. Build the full message list with a System Prompt
fullMessages := append([]Message{ fullMessages := append([]Message{
{ {
Role: "system", Role: "system",
Content: "You are a helpful Chilean business assistant. You can book appointments. If a user wants to schedule something, use the create_appointment tool. Always be concise and polite.", 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...)
// 2. Define the tools the AI can use
payload := map[string]interface{}{ payload := map[string]interface{}{
"model": "stepfun/step-3.5-flash:free", "model": "arcee-ai/trinity-large-preview:free", // arcee-ai/trinity-large-preview:free, stepfun/step-3.5-flash:free
"messages": fullMessages, "messages": fullMessages,
"stream": true, // <--- THIS IS KEY
"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 in the database", "description": "Schedules a new appointment",
"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", "description": "The user's phone number"}, "customer_phone": map[string]string{"type": "string"},
"date": map[string]string{"type": "string", "description": "The date and time in YYYY-MM-DD HH:MM format"}, "date": map[string]string{"type": "string"},
}, },
"required": []string{"customer_phone", "date"}, "required": []string{"customer_phone", "date"},
}, },
@@ -70,50 +76,82 @@ func GetAIResponse(chatHistory []Message) (string, error) {
req.Header.Set("Authorization", "Bearer "+apiKey) req.Header.Set("Authorization", "Bearer "+apiKey)
req.Header.Set("Content-Type", "application/json") req.Header.Set("Content-Type", "application/json")
resp, err := http.DefaultClient.Do(req) client := &http.Client{}
resp, err := client.Do(req)
if err != nil { if err != nil {
return "", err return "", err
} }
defer resp.Body.Close() defer resp.Body.Close()
bodyBytes, _ := io.ReadAll(resp.Body) // Prepare to read the stream line by line
if resp.StatusCode != http.StatusOK { scanner := bufio.NewScanner(resp.Body)
return "Error from OpenRouter: " + string(bodyBytes), nil
}
var result OpenRouterResponse var fullContentBuffer strings.Builder
if err := json.Unmarshal(bodyBytes, &result); err != nil { var toolCallBuffer strings.Builder
return "", err var toolName string
} isToolCall := false
if len(result.Choices) > 0 { for scanner.Scan() {
aiMsg := result.Choices[0].Message line := scanner.Text()
// 3. Handle Tool Calls (The "Logic" part) // OpenRouter sends "data: {JSON}" lines.
if len(aiMsg.ToolCalls) > 0 { // Use specific string trimming to handle the format.
tc := aiMsg.ToolCalls[0].Function if !strings.HasPrefix(line, "data: ") {
if tc.Name == "create_appointment" { continue
var args struct { }
Phone string `json:"customer_phone"` jsonStr := strings.TrimPrefix(line, "data: ")
Date string `json:"date"`
// The stream ends with "data: [DONE]"
if jsonStr == "[DONE]" {
break
}
var chunk StreamResponse
if err := json.Unmarshal([]byte(jsonStr), &chunk); err != nil {
continue
}
if len(chunk.Choices) > 0 {
delta := chunk.Choices[0].Delta
// 1. Handle Text Content
if delta.Content != "" {
fullContentBuffer.WriteString(delta.Content)
// Send this chunk to the frontend immediately
onToken(delta.Content)
}
// 2. Handle Tool Calls (Accumulate them, don't stream execution yet)
if len(delta.ToolCalls) > 0 {
isToolCall = true
if delta.ToolCalls[0].Function.Name != "" {
toolName = delta.ToolCalls[0].Function.Name
} }
// Unmarshal the AI-generated arguments toolCallBuffer.WriteString(delta.ToolCalls[0].Function.Arguments)
json.Unmarshal([]byte(tc.Arguments), &args)
// Save to DB using your helper
err := db.SaveAppointment(args.Phone, args.Date)
if err != nil {
return "I tried to book it, but the database hates me: " + err.Error(), nil
}
return "✅ [SYSTEM] Appointment automatically booked for " + args.Phone + " at " + args.Date, nil
} }
} }
}
// 4. Return plain text if no tool was called // If it was a tool call, execute it now that the stream is finished
if aiMsg.Content != "" { if isToolCall && toolName == "create_appointment" {
return aiMsg.Content, nil var args struct {
Phone string `json:"customer_phone"`
Date string `json:"date"`
}
// Try to parse the accumulated JSON arguments
if err := json.Unmarshal([]byte(toolCallBuffer.String()), &args); err == nil {
err := db.SaveAppointment(args.Phone, args.Date)
resultMsg := ""
if err != nil {
resultMsg = "\n[System: Failed to book appointment.]"
} else {
resultMsg = fmt.Sprintf("\n✅ Booked for %s at %s", args.Phone, args.Date)
}
// Send the tool result to the frontend
onToken(resultMsg)
fullContentBuffer.WriteString(resultMsg)
} }
} }
return "The AI is giving me the silent treatment, seki.", nil return fullContentBuffer.String(), nil
} }

View File

@@ -2,98 +2,243 @@
<!DOCTYPE html> <!DOCTYPE html>
<html> <html>
<head> <head>
<title>SekiBot | Conversations</title> <title>SekiBot | Dashboard</title>
<style> <style>
/* DISCORD THEME COLORS */ /* DISCORD-ISH THEME */
:root { --blurple: #5865F2; --bg-sidebar: #2f3136; --bg-chat: #36393f; --bg-input: #40444b; --text: #dcddde; } :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: sans-serif; background: var(--bg-chat); color: var(--text); } body { margin: 0; display: flex; height: 100vh; font-family: 'Segoe UI', sans-serif; background: var(--bg-chat); color: var(--text); }
.sidebar { width: 240px; background: var(--bg-sidebar); display: flex; flex-direction: column; padding: 15px; } /* SIDEBAR */
.chat-list { flex-grow: 1; overflow-y: auto; } .sidebar { width: 260px; background: var(--bg-sidebar); display: flex; flex-direction: column; padding: 15px; border-right: 1px solid #202225; }
.chat-item { padding: 10px; border-radius: 4px; cursor: pointer; margin-bottom: 5px; background: rgba(255,255,255,0.05); } .chat-list { flex-grow: 1; overflow-y: auto; margin-top: 10px; }
.chat-item:hover, .active { background: var(--bg-input); color: white; } .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; }
.chat-area { flex-grow: 1; display: flex; flex-direction: column; } /* LAYOUT */
.messages { flex-grow: 1; overflow-y: auto; padding: 20px; display: flex; flex-direction: column; gap: 15px; } .main-content { flex-grow: 1; display: none; flex-direction: column; height: 100vh; }
.msg { max-width: 80%; padding: 10px 15px; border-radius: 8px; line-height: 1.4; } .show { display: flex !important; }
.user { align-self: flex-end; background: var(--blurple); color: white; }
.assistant { align-self: flex-start; background: var(--bg-input); }
.input-container { padding: 20px; background: var(--bg-chat); } /* CHAT AREA */
#user-input { width: 100%; background: var(--bg-input); border: none; padding: 15px; border-radius: 8px; color: white; outline: none; } .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; }
button.new-chat { background: var(--blurple); color: white; border: none; padding: 10px; border-radius: 4px; cursor: pointer; margin-bottom: 20px; } .input-area { padding: 20px; background: var(--bg-chat); margin-bottom: 20px;}
#user-input { width: 95%; background: var(--bg-input); border: none; padding: 15px; border-radius: 8px; color: white; outline: none; font-size: 1rem; }
/* APPOINTMENTS AREA */
.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; }
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); }
.status-pill { background: #faa61a; color: black; padding: 4px 8px; border-radius: 12px; font-size: 0.75rem; font-weight: bold; text-transform: uppercase; }
</style> </style>
</head> </head>
<body> <body>
<div class="sidebar"> <div class="sidebar">
<h2>SekiBot</h2> <h2 style="color: white; margin-bottom: 20px;">🤖 SekiBot</h2>
<nav>
<div class="chat-item active" onclick="showPanel('chat-panel')" id="nav-chat"># conversations</div> <div class="nav-item active" onclick="switchPanel('chat')" id="nav-chat">💬 Conversations</div>
<div class="chat-item" onclick="showPanel('appt-panel')" id="nav-appt"># appointments-manager</div> <div class="nav-item" onclick="switchPanel('appt')" id="nav-appt">📅 Appointments</div>
</nav>
<hr style="border: 0.5px solid #42454a; margin: 15px 0;"> <hr style="border: 0; border-top: 1px solid #42454a; margin: 15px 0;">
<button class="new-chat" onclick="newChat()">+ New Chat</button>
<div class="chat-list"> <button class="btn-primary" onclick="createNewChat()">+ New Chat</button>
<div class="chat-list" id="chat-list-container">
{{range .Chats}} {{range .Chats}}
<div class="chat-item" onclick="loadChat({{.ID}})"># chat-{{.ID}}</div> <div class="chat-item" onclick="loadChat({{.ID}}, this)" id="chat-link-{{.ID}}">
# {{.Title}}
</div>
{{end}} {{end}}
</div> </div>
</div> </div>
<!-- MAIN CHAT PANEL --> <div class="main-content show" id="panel-chat">
<div class="main-content" id="chat-panel"> <div class="messages" id="message-pane">
<div class="messages" id="message-pane" style="height: calc(100vh - 120px); overflow-y: auto; padding: 20px; display: flex; flex-direction: column; gap: 10px;"> <div style="text-align: center; margin-top: 40vh; color: #72767d;">
<p style="text-align: center; color: var(--text-muted);">Select a chat to begin.</p> <p>Select a chat from the sidebar to start.</p>
</div>
</div> </div>
<div class="input-container" style="padding: 20px; background: var(--bg-chat);"> <div class="input-area">
<input type="text" id="user-input" placeholder="Type a message..." onkeypress="if(event.key==='Enter') sendMessage()" style="width: 100%; background: var(--bg-input); border: none; padding: 15px; border-radius: 8px; color: white;"> <input type="text" id="user-input" placeholder="Message #general..." onkeypress="handleEnter(event)">
</div> </div>
</div> </div>
<!-- APPOINTMENTS CRUD PANEL --> <div class="main-content" id="panel-appt">
<div class="main-content" id="appt-panel" style="display: none; padding: 40px;"> <div class="panel-container">
<h1>Appointments Manager</h1> <h1>Appointments Manager</h1>
<div style="background: var(--bg-sidebar); padding: 20px; border-radius: 8px; margin-bottom: 20px;">
<h3>Create Mock Appointment</h3> <div style="background: var(--bg-sidebar); padding: 20px; border-radius: 8px; margin: 20px 0;">
<div style="display: grid; grid-template-columns: 1fr 1fr 100px; gap: 10px;"> <h3 style="margin-top: 0;">Add Manual Appointment</h3>
<input type="text" id="m-phone" placeholder="Phone"> <div style="display: flex; gap: 10px;">
<input type="datetime-local" id="m-date"> <input type="text" id="m-phone" placeholder="+56 9 1234 5678" style="flex: 1;">
<button onclick="createMockAppt()">Add</button> <input type="datetime-local" id="m-date" style="flex: 1;">
<button class="btn-success" onclick="createMockAppt()">Add</button>
</div>
</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>
<table>
<thead>
<tr><th>ID</th><th>Phone</th><th>Date</th><th>Status</th><th>Actions</th></tr>
</thead>
<tbody id="appt-table-body">
{{range .Appointments}}
<tr id="appt-{{.ID}}">
<td>#{{.ID}}</td>
<td><input type="text" value="{{.CustomerPhone}}" id="edit-phone-{{.ID}}" style="width: 120px; margin:0;"></td>
<td><input type="text" value="{{.Date}}" id="edit-date-{{.ID}}" style="width: 160px; margin:0;"></td>
<td><span class="status-pill">{{.Status}}</span></td>
<td>
<button style="background: var(--success);" onclick="updateAppt({{.ID}})">Save</button>
<button class="delete" onclick="deleteAppt({{.ID}})">Del</button>
</td>
</tr>
{{end}}
</tbody>
</table>
</div> </div>
<script> <script>
function showPanel(panelId) { let currentChatId = null;
document.getElementById('chat-panel').style.display = panelId === 'chat-panel' ? 'flex' : 'none';
document.getElementById('appt-panel').style.display = panelId === 'appt-panel' ? 'block' : 'none'; // --- NAVIGATION ---
document.querySelectorAll('nav .chat-item').forEach(el => el.classList.remove('active')); function switchPanel(panel) {
document.getElementById(panelId === 'chat-panel' ? 'nav-chat' : 'nav-appt').classList.add('active'); // Toggle Content
document.getElementById('panel-chat').classList.toggle('show', panel === 'chat');
document.getElementById('panel-appt').classList.toggle('show', panel === 'appt');
// Toggle Sidebar Active State
document.getElementById('nav-chat').classList.toggle('active', panel === 'chat');
document.getElementById('nav-appt').classList.toggle('active', panel === 'appt');
} }
// --- CHAT LOGIC (RESTORED) ---
async function createNewChat() {
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);
}
}
async function loadChat(id, el) {
currentChatId = id;
// 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();
}
} catch (e) {
aiMsgDiv.innerText += "\n[Error: Connection lost]";
}
}
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');
pane.scrollTop = pane.scrollHeight;
}
function handleEnter(e) {
if (e.key === 'Enter') sendMessage();
}
// --- APPOINTMENT LOGIC (KEPT) ---
async function createMockAppt() { async function createMockAppt() {
const phone = document.getElementById('m-phone').value; const phone = document.getElementById('m-phone').value;
const date = document.getElementById('m-date').value; const date = document.getElementById('m-date').value;
if(!phone || !date) return alert("Fill in fields, seki.");
await fetch('/admin/appointment', { await fetch('/admin/appointment', {
method: 'POST', method: 'POST',
headers: {'Content-Type': 'application/json'}, headers: {'Content-Type': 'application/json'},
@@ -105,21 +250,25 @@
async function updateAppt(id) { async function updateAppt(id) {
const phone = document.getElementById(`edit-phone-${id}`).value; const phone = document.getElementById(`edit-phone-${id}`).value;
const date = document.getElementById(`edit-date-${id}`).value; const date = document.getElementById(`edit-date-${id}`).value;
await fetch(`/admin/appointment/${id}`, {
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({phone, date})
}); });
alert("Updated, seki.");
if(res.ok) alert("Updated successfully.");
else alert("Update failed.");
} }
async function deleteAppt(id) { async function deleteAppt(id) {
if(!confirm("Kill it?")) return; if(!confirm("Are you sure you want to delete this appointment?")) return;
await fetch(`/admin/appointment/${id}`, { method: 'DELETE' });
document.getElementById(`appt-${id}`).remove();
}
// ... (Keep your existing newChat, loadChat, and sendMessage scripts here) ... const res = await fetch(`/admin/appointment/${id}`, { method: 'DELETE' });
if(res.ok) {
document.getElementById(`appt-${id}`).remove();
}
}
</script> </script>
</body> </body>
</html> </html>