persona-community-2/services/persona-api/internal/api/handlers/chat.go
2026-02-23 10:54:06 +00:00

95 lines
2.6 KiB
Go

package handlers
import (
"net/http"
"time"
"github.com/google/uuid"
"git.threesix.ai/jordan/persona-community-2/pkg/app"
"git.threesix.ai/jordan/persona-community-2/pkg/auth"
"git.threesix.ai/jordan/persona-community-2/pkg/httpresponse"
"git.threesix.ai/jordan/persona-community-2/pkg/logging"
"git.threesix.ai/jordan/persona-community-2/pkg/queue"
"git.threesix.ai/jordan/persona-community-2/pkg/realtime"
)
// Chat handles HTTP requests for chat messaging with AI responses.
// User messages are broadcast immediately via SSE.
// AI responses are enqueued and processed by the worker with streaming chunks.
type Chat struct {
queue queue.Producer
sseHub *realtime.SSEHub
logger *logging.Logger
}
// NewChat creates a new Chat handler.
func NewChat(q queue.Producer, hub *realtime.SSEHub, logger *logging.Logger) *Chat {
return &Chat{
queue: q,
sseHub: hub,
logger: logger.WithComponent("ChatHandler"),
}
}
// SendMessageRequest is the request body for sending a chat message.
type SendMessageRequest struct {
Content string `json:"content" validate:"required,min=1,max=5000"`
}
// SendMessage broadcasts a chat message to a channel via SSE
// and enqueues an AI response job for the worker.
func (h *Chat) SendMessage(w http.ResponseWriter, r *http.Request) error {
var req SendMessageRequest
if err := app.BindAndValidate(r, &req); err != nil {
return err
}
// Get user info
userID := "anonymous"
userName := "Anonymous"
if user := auth.GetUser(r.Context()); user != nil {
userID = user.ID
if name, ok := user.Metadata["name"].(string); ok && name != "" {
userName = name
} else if user.Email != "" {
userName = user.Email
}
}
msgID := uuid.New().String()
now := time.Now().UTC()
// Broadcast user message to channel:general immediately (synchronous — users
// see their own messages instantly without waiting for the queue)
h.sseHub.SendToChannel("channel:general", &realtime.SSEEvent{
Type: "chat",
Timestamp: now,
JobID: msgID,
Message: req.Content,
Result: map[string]any{
"id": msgID,
"content": req.Content,
"userId": userID,
"userName": userName,
"timestamp": now.Format(time.RFC3339),
},
})
// Enqueue AI response job — worker streams chunks via Redis → SSE
if _, err := h.queue.Enqueue(r.Context(), "ai_chat_response", map[string]any{
"content": req.Content,
"userID": userID,
"channel": "channel:general",
}); err != nil {
h.logger.Error("failed to enqueue AI chat response", "error", err)
// Don't fail the request — user message was already delivered
}
httpresponse.OK(w, r, map[string]string{
"id": msgID,
"status": "sent",
})
return nil
}