new file: __debug_bin.exe modified: bot.db modified: db/db.go modified: go.mod new file: handlers/auth.go modified: handlers/dashboard.go new file: handlers/saas.go modified: handlers/webhook.go modified: main.go new file: saas_bot.db modified: services/openrouter.go new file: services/types.go modified: services/whatsapp.go new file: static/style.css modified: templates/dashboard.html new file: templates/landing.html new file: templates/login.html new file: templates/register.html deleted: types/types.go
237 lines
6.5 KiB
Go
237 lines
6.5 KiB
Go
package handlers
|
|
|
|
import (
|
|
"log"
|
|
"net/http"
|
|
"strconv"
|
|
"whatsapp-bot/db"
|
|
"whatsapp-bot/services"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
)
|
|
|
|
// ShowDashboard renders the Chat Interface (Discord View)
|
|
// NOTE: The main SaaS Dashboard is now at /dashboard (UserDashboard in saas.go).
|
|
// You might want to rename this route to /chat-interface or keep it as a sub-view.
|
|
func ShowDashboard(c *gin.Context) {
|
|
userID := c.MustGet("userID").(int)
|
|
|
|
// 1. Fetch Appointments (Only for this user)
|
|
type Appt struct {
|
|
ID int
|
|
CustomerPhone string
|
|
Date string
|
|
Status string
|
|
}
|
|
var appts []Appt
|
|
|
|
approws, err := db.Conn.Query("SELECT id, customer_phone, appointment_time, status FROM appointments WHERE user_id = ? ORDER BY id DESC", userID)
|
|
if err != nil {
|
|
log.Println("Appt Query Error:", err)
|
|
} else {
|
|
defer approws.Close()
|
|
for approws.Next() {
|
|
var a Appt
|
|
approws.Scan(&a.ID, &a.CustomerPhone, &a.Date, &a.Status)
|
|
appts = append(appts, a)
|
|
}
|
|
}
|
|
|
|
// 2. Fetch Chats (Only for this user)
|
|
// We need to add user_id to chats table to make this strict,
|
|
// but for now, we'll assume all chats are visible to the admin/user
|
|
// or filter if you add user_id to the chats table later.
|
|
type Chat struct {
|
|
ID int
|
|
Title string
|
|
}
|
|
var chats []Chat
|
|
|
|
chatrows, err := db.Conn.Query("SELECT id, title FROM chats ORDER BY id DESC")
|
|
if err != nil {
|
|
log.Println("Chat Query Error:", err)
|
|
} else {
|
|
defer chatrows.Close()
|
|
for chatrows.Next() {
|
|
var ch Chat
|
|
chatrows.Scan(&ch.ID, &ch.Title)
|
|
if ch.Title == "" {
|
|
ch.Title = "Chat #" + strconv.Itoa(ch.ID)
|
|
}
|
|
chats = append(chats, ch)
|
|
}
|
|
}
|
|
|
|
// 3. Render the Chat Template
|
|
// Note: We are using "dashboard.html" for the SaaS view now.
|
|
// You might want to rename your old chat template to "chat_view.html"
|
|
// if you want to keep both views separate.
|
|
// For now, I'll point this to the new SaaS dashboard template to avoid errors,
|
|
// but realistically you should merge them or have two separate HTML files.
|
|
c.HTML(http.StatusOK, "dashboard.html", gin.H{
|
|
"Appointments": appts,
|
|
"Chats": chats,
|
|
// Pass minimal context so the template doesn't crash if it expects user info
|
|
"UserEmail": "Chat Mode",
|
|
"Tier": "Pro",
|
|
"BotConfig": map[string]string{"PhoneID": "N/A"},
|
|
})
|
|
}
|
|
|
|
// POST /admin/appointment
|
|
func CreateAppointmentHandler(c *gin.Context) {
|
|
userID := c.MustGet("userID").(int)
|
|
var body struct {
|
|
Phone string `json:"phone"`
|
|
Date string `json:"date"`
|
|
}
|
|
if err := c.BindJSON(&body); err != nil {
|
|
c.Status(400)
|
|
return
|
|
}
|
|
|
|
if err := db.SaveAppointment(userID, body.Phone, body.Date); err != nil {
|
|
c.JSON(500, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
c.Status(200)
|
|
}
|
|
|
|
// DELETE /admin/appointment/:id
|
|
func DeleteAppointmentHandler(c *gin.Context) {
|
|
id := c.Param("id")
|
|
userID := c.MustGet("userID").(int)
|
|
|
|
// Ensure user only deletes their own appointments
|
|
_, err := db.Conn.Exec("DELETE FROM appointments WHERE id = ? AND user_id = ?", id, userID)
|
|
if err != nil {
|
|
c.Status(http.StatusInternalServerError)
|
|
return
|
|
}
|
|
c.Status(http.StatusOK)
|
|
}
|
|
|
|
// PUT /admin/appointment/:id
|
|
func UpdateAppointmentHandler(c *gin.Context) {
|
|
id := c.Param("id")
|
|
userID := c.MustGet("userID").(int)
|
|
var body struct {
|
|
Phone string `json:"phone"`
|
|
Date string `json:"date"`
|
|
}
|
|
if err := c.BindJSON(&body); err != nil {
|
|
c.Status(400)
|
|
return
|
|
}
|
|
|
|
_, err := db.Conn.Exec(
|
|
"UPDATE appointments SET customer_phone = ?, appointment_time = ? WHERE id = ? AND user_id = ?",
|
|
body.Phone, body.Date, id, userID,
|
|
)
|
|
if err != nil {
|
|
c.JSON(500, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
c.Status(200)
|
|
}
|
|
|
|
// POST /admin/chat
|
|
func NewChatHandler(c *gin.Context) {
|
|
// You might want to associate chats with users too: INSERT INTO chats (user_id, title)...
|
|
res, err := db.Conn.Exec("INSERT INTO chats (title) VALUES ('New Chat')")
|
|
if err != nil {
|
|
c.JSON(500, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
id, _ := res.LastInsertId()
|
|
c.JSON(200, gin.H{"id": id, "title": "New Chat"})
|
|
}
|
|
|
|
// DELETE /admin/chat/:id
|
|
func DeleteChatHandler(c *gin.Context) {
|
|
id := c.Param("id")
|
|
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
|
|
func GetMessagesHandler(c *gin.Context) {
|
|
id := c.Param("id")
|
|
rows, _ := db.Conn.Query("SELECT role, content FROM messages WHERE chat_id = ? ORDER BY created_at ASC", id)
|
|
defer rows.Close()
|
|
|
|
var msgs []services.Message
|
|
for rows.Next() {
|
|
var m services.Message
|
|
rows.Scan(&m.Role, &m.Content)
|
|
msgs = append(msgs, m)
|
|
}
|
|
c.JSON(200, msgs)
|
|
}
|
|
|
|
// POST /admin/chat/:id/message
|
|
func PostMessageHandler(c *gin.Context) {
|
|
chatId := c.Param("id")
|
|
userID := c.MustGet("userID").(int)
|
|
|
|
var body struct {
|
|
Content string `json:"content"`
|
|
}
|
|
if err := c.BindJSON(&body); err != nil {
|
|
c.Status(400)
|
|
return
|
|
}
|
|
|
|
// 1. Save User Message
|
|
db.Conn.Exec("INSERT INTO messages (chat_id, role, content) VALUES (?, 'user', ?)", chatId, body.Content)
|
|
|
|
// 2. Fetch History
|
|
rows, _ := db.Conn.Query("SELECT role, content FROM messages WHERE chat_id = ? ORDER BY created_at ASC", chatId)
|
|
var history []services.Message
|
|
defer rows.Close()
|
|
for rows.Next() {
|
|
var m services.Message
|
|
rows.Scan(&m.Role, &m.Content)
|
|
history = append(history, m)
|
|
}
|
|
|
|
// 3. Load User's Custom System Prompt for the AI
|
|
// We need the AI to behave like the User's bot, even in the chat interface
|
|
var systemPrompt string
|
|
err := db.Conn.QueryRow("SELECT system_prompt FROM bot_configs WHERE user_id = ?", userID).Scan(&systemPrompt)
|
|
if err != nil {
|
|
systemPrompt = "You are a helpful assistant." // Fallback
|
|
}
|
|
|
|
// 4. Stream Response
|
|
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")
|
|
|
|
// We use the new StreamAIResponse signature that takes (userID, message, prompt, callback)
|
|
fullResponse, _ := services.StreamAIResponse(userID, body.Content, systemPrompt, func(chunk string) {
|
|
c.Writer.Write([]byte(chunk))
|
|
c.Writer.Flush()
|
|
})
|
|
|
|
// 5. Save Assistant Message
|
|
db.Conn.Exec("INSERT INTO messages (chat_id, role, content) VALUES (?, 'assistant', ?)", chatId, fullResponse)
|
|
}
|