95 lines
2.6 KiB
Go
95 lines
2.6 KiB
Go
package handlers
|
|
|
|
import (
|
|
"net/http"
|
|
"time"
|
|
|
|
"github.com/google/uuid"
|
|
|
|
"git.threesix.ai/jordan/persona-community-1/pkg/app"
|
|
"git.threesix.ai/jordan/persona-community-1/pkg/auth"
|
|
"git.threesix.ai/jordan/persona-community-1/pkg/httpresponse"
|
|
"git.threesix.ai/jordan/persona-community-1/pkg/logging"
|
|
"git.threesix.ai/jordan/persona-community-1/pkg/queue"
|
|
"git.threesix.ai/jordan/persona-community-1/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
|
|
}
|