modified: .env

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
This commit is contained in:
2026-03-02 00:38:05 -03:00
parent 9ff021879f
commit e256fcb073
20 changed files with 627 additions and 659 deletions

View File

@@ -10,8 +10,13 @@ import (
"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) {
// 1. Fetch Appointments for the Table
userID := c.MustGet("userID").(int)
// 1. Fetch Appointments (Only for this user)
type Appt struct {
ID int
CustomerPhone string
@@ -20,7 +25,7 @@ func ShowDashboard(c *gin.Context) {
}
var appts []Appt
approws, err := db.Conn.Query("SELECT id, customer_phone, appointment_date, status FROM appointments ORDER BY id DESC")
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 {
@@ -32,36 +37,50 @@ func ShowDashboard(c *gin.Context) {
}
}
// 2. Fetch Chats for the Sidebar
// 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 FROM chats ORDER BY id DESC")
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)
// Give it a default title so the sidebar isn't empty
ch.Title = "Chat #" + strconv.Itoa(ch.ID)
chatrows.Scan(&ch.ID, &ch.Title)
if ch.Title == "" {
ch.Title = "Chat #" + strconv.Itoa(ch.ID)
}
chats = append(chats, ch)
}
}
// 3. Render the Template with BOTH data sets
// 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"},
})
}
// Add this to handlers/dashboard.go
// POST /admin/appointment
func CreateAppointmentHandler(c *gin.Context) {
userID := c.MustGet("userID").(int)
var body struct {
Phone string `json:"phone"`
Date string `json:"date"`
@@ -71,8 +90,7 @@ func CreateAppointmentHandler(c *gin.Context) {
return
}
// Use the helper function instead of raw SQL here
if err := db.SaveAppointment(body.Phone, body.Date); err != nil {
if err := db.SaveAppointment(userID, body.Phone, body.Date); err != nil {
c.JSON(500, gin.H{"error": err.Error()})
return
}
@@ -80,10 +98,13 @@ func CreateAppointmentHandler(c *gin.Context) {
c.Status(200)
}
// Manage/Delete Appointment
// DELETE /admin/appointment/:id
func DeleteAppointmentHandler(c *gin.Context) {
id := c.Param("id")
_, err := db.Conn.Exec("DELETE FROM appointments WHERE id = ?", 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
@@ -94,6 +115,7 @@ func DeleteAppointmentHandler(c *gin.Context) {
// 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"`
@@ -104,8 +126,8 @@ func UpdateAppointmentHandler(c *gin.Context) {
}
_, err := db.Conn.Exec(
"UPDATE appointments SET customer_phone = ?, appointment_date = ? WHERE id = ?",
body.Phone, body.Date, id,
"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()})
@@ -116,20 +138,19 @@ func UpdateAppointmentHandler(c *gin.Context) {
// 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()
// 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)
@@ -167,6 +188,8 @@ func GetMessagesHandler(c *gin.Context) {
// 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"`
}
@@ -175,10 +198,10 @@ func PostMessageHandler(c *gin.Context) {
return
}
// 1. Save User Message to DB first
// 1. Save User Message
db.Conn.Exec("INSERT INTO messages (chat_id, role, content) VALUES (?, 'user', ?)", chatId, body.Content)
// 2. Fetch history
// 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()
@@ -188,20 +211,26 @@ func PostMessageHandler(c *gin.Context) {
history = append(history, m)
}
// 3. Set Headers for Streaming
// 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")
// 4. Call the Stream Service
// We pass a function that writes chunks directly to the HTTP response
fullResponse, _ := services.StreamAIResponse(history, func(chunk string) {
// 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() // Important: Send it NOW, don't buffer
c.Writer.Flush()
})
// 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
// 5. Save Assistant Message
db.Conn.Exec("INSERT INTO messages (chat_id, role, content) VALUES (?, 'assistant', ?)", chatId, fullResponse)
}