Files
Trackeep/backend/handlers/ai_recommendations.go
T
Tomas Dvorak d27cf14110 first test
2026-02-08 14:14:55 +01:00

402 lines
13 KiB
Go

package handlers
import (
"net/http"
"strconv"
"time"
"github.com/gin-gonic/gin"
"gorm.io/gorm"
"github.com/trackeep/backend/models"
"github.com/trackeep/backend/services"
)
// AIRecommendationHandler handles AI recommendation endpoints
type AIRecommendationHandler struct {
db *gorm.DB
service *services.AIRecommendationService
}
// NewAIRecommendationHandler creates a new AI recommendation handler
func NewAIRecommendationHandler(db *gorm.DB) *AIRecommendationHandler {
return &AIRecommendationHandler{
db: db,
service: services.NewAIRecommendationService(db),
}
}
// GetRecommendations returns personalized recommendations for the user
func (h *AIRecommendationHandler) GetRecommendations(c *gin.Context) {
userID := c.GetUint("user_id")
// Parse query parameters
recommendationType := c.DefaultQuery("type", "mixed") // content, task, learning, connection, mixed
limit, _ := strconv.Atoi(c.DefaultQuery("limit", "5"))
minConfidence, _ := strconv.ParseFloat(c.DefaultQuery("min_confidence", "0.0"), 64)
includeDismissed := c.DefaultQuery("include_dismissed", "false") == "true"
context := c.Query("context")
// Create recommendation request
req := services.RecommendationRequest{
UserID: userID,
RecommendationType: recommendationType,
Limit: limit,
MinConfidence: minConfidence,
IncludeDismissed: includeDismissed,
Context: context,
}
// Get recommendations
recommendations, err := h.service.GetRecommendations(req)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to get recommendations: " + err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{
"recommendations": recommendations,
"count": len(recommendations),
"type": recommendationType,
})
}
// GetRecommendationStats returns recommendation statistics for the user
func (h *AIRecommendationHandler) GetRecommendationStats(c *gin.Context) {
userID := c.GetUint("user_id")
// Get user preferences
var prefs models.UserPreference
if err := h.db.Where("user_id = ?", userID).First(&prefs).Error; err != nil {
// Create default preferences
prefs = models.UserPreference{
UserID: userID,
EnableRecommendations: true,
MinConfidenceThreshold: 0.6,
MaxRecommendationsPerDay: 5,
MaxAgeHours: 168,
}
h.db.Create(&prefs)
}
// Get recommendation statistics
var stats struct {
TotalRecommendations int64 `json:"total_recommendations"`
ClickedCount int64 `json:"clicked_count"`
DismissedCount int64 `json:"dismissed_count"`
FeedbackCount int64 `json:"feedback_count"`
Types []struct {
Type string `json:"type"`
Count int64 `json:"count"`
} `json:"types"`
Categories []struct {
Category string `json:"category"`
Count int64 `json:"count"`
} `json:"categories"`
DailyStats []struct {
Date string `json:"date"`
Count int64 `json:"count"`
} `json:"daily_stats"`
}
// Total recommendations
h.db.Model(&models.AIRecommendation{}).Where("user_id = ?", userID).Count(&stats.TotalRecommendations)
// Clicked and dismissed counts
h.db.Model(&models.AIRecommendation{}).Where("user_id = ? AND clicked = ?", userID, true).Count(&stats.ClickedCount)
h.db.Model(&models.AIRecommendation{}).Where("user_id = ? AND dismissed = ?", userID, true).Count(&stats.DismissedCount)
h.db.Model(&models.AIRecommendation{}).Where("user_id = ? AND feedback != ''", userID).Count(&stats.FeedbackCount)
// Recommendations by type
h.db.Model(&models.AIRecommendation{}).
Select("recommendation_type as type, COUNT(*) as count").
Where("user_id = ?", userID).
Group("recommendation_type").
Scan(&stats.Types)
// Recommendations by category
h.db.Model(&models.AIRecommendation{}).
Select("category as category, COUNT(*) as count").
Where("user_id = ? AND category != ''", userID).
Group("category").
Order("count DESC").
Limit(10).
Scan(&stats.Categories)
// Daily stats for last 30 days
h.db.Model(&models.AIRecommendation{}).
Select("DATE(created_at) as date, COUNT(*) as count").
Where("user_id = ? AND created_at >= NOW() - INTERVAL '30 days'", userID).
Group("DATE(created_at)").
Order("date ASC").
Scan(&stats.DailyStats)
c.JSON(http.StatusOK, gin.H{
"stats": stats,
"preferences": prefs,
})
}
// UpdatePreferences updates user recommendation preferences
func (h *AIRecommendationHandler) UpdatePreferences(c *gin.Context) {
userID := c.GetUint("user_id")
var req struct {
EnableRecommendations bool `json:"enable_recommendations"`
ContentRecommendations bool `json:"content_recommendations"`
TaskRecommendations bool `json:"task_recommendations"`
LearningRecommendations bool `json:"learning_recommendations"`
ConnectionRecommendations bool `json:"connection_recommendations"`
MaxRecommendationsPerDay int `json:"max_recommendations_per_day"`
PreferredCategories []string `json:"preferred_categories"`
BlockedCategories []string `json:"blocked_categories"`
PreferredContentTypes []string `json:"preferred_content_types"`
MinConfidenceThreshold float64 `json:"min_confidence_threshold"`
MaxAgeHours int `json:"max_age_hours"`
EnablePersonalization bool `json:"enable_personalization"`
EnableFeedbackLearning bool `json:"enable_feedback_learning"`
}
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
// Update or create preferences
var prefs models.UserPreference
if err := h.db.Where("user_id = ?", userID).First(&prefs).Error; err != nil {
if err == gorm.ErrRecordNotFound {
prefs = models.UserPreference{UserID: userID}
} else {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Database error"})
return
}
}
// Update fields
prefs.EnableRecommendations = req.EnableRecommendations
prefs.ContentRecommendations = req.ContentRecommendations
prefs.TaskRecommendations = req.TaskRecommendations
prefs.LearningRecommendations = req.LearningRecommendations
prefs.ConnectionRecommendations = req.ConnectionRecommendations
prefs.MaxRecommendationsPerDay = req.MaxRecommendationsPerDay
prefs.PreferredCategories = req.PreferredCategories
prefs.BlockedCategories = req.BlockedCategories
prefs.PreferredContentTypes = req.PreferredContentTypes
prefs.MinConfidenceThreshold = req.MinConfidenceThreshold
prefs.MaxAgeHours = req.MaxAgeHours
prefs.EnablePersonalization = req.EnablePersonalization
prefs.EnableFeedbackLearning = req.EnableFeedbackLearning
if err := h.db.Save(&prefs).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update preferences"})
return
}
c.JSON(http.StatusOK, gin.H{
"message": "Preferences updated successfully",
"preferences": prefs,
})
}
// RecordInteraction records user interaction with a recommendation
func (h *AIRecommendationHandler) RecordInteraction(c *gin.Context) {
userID := c.GetUint("user_id")
recommendationIDStr := c.Param("id")
recommendationID, err := strconv.ParseUint(recommendationIDStr, 10, 32)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid recommendation ID"})
return
}
var req struct {
InteractionType string `json:"interaction_type" binding:"required"` // click, dismiss, feedback, share
Context string `json:"context"` // dashboard, search, etc.
Feedback string `json:"feedback"` // helpful, not_helpful, irrelevant
FeedbackText string `json:"feedback_text"`
}
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
// Record the interaction
if err := h.service.RecordInteraction(userID, uint(recommendationID), req.InteractionType, req.Context); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to record interaction"})
return
}
// If feedback is provided, update the recommendation
if req.Feedback != "" {
var recommendation models.AIRecommendation
if err := h.db.Where("id = ? AND user_id = ?", uint(recommendationID), userID).First(&recommendation).Error; err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "Recommendation not found"})
return
}
recommendation.Feedback = req.Feedback
recommendation.FeedbackText = req.FeedbackText
now := time.Now()
recommendation.FeedbackAt = &now
h.db.Save(&recommendation)
}
c.JSON(http.StatusOK, gin.H{"message": "Interaction recorded successfully"})
}
// GetRecommendationHistory returns user's recommendation history
func (h *AIRecommendationHandler) GetRecommendationHistory(c *gin.Context) {
userID := c.GetUint("user_id")
// Parse query parameters
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
limit, _ := strconv.Atoi(c.DefaultQuery("limit", "20"))
recommendationType := c.Query("type")
status := c.Query("status") // clicked, dismissed, feedback
// Build query
query := h.db.Model(&models.AIRecommendation{}).Where("user_id = ?", userID)
if recommendationType != "" {
query = query.Where("recommendation_type = ?", recommendationType)
}
if status == "clicked" {
query = query.Where("clicked = ?", true)
} else if status == "dismissed" {
query = query.Where("dismissed = ?", true)
} else if status == "feedback" {
query = query.Where("feedback != ''", userID)
}
// Count total records
var total int64
query.Count(&total)
// Get paginated results
offset := (page - 1) * limit
var recommendations []models.AIRecommendation
query.Order("created_at DESC").Limit(limit).Offset(offset).Find(&recommendations)
c.JSON(http.StatusOK, gin.H{
"recommendations": recommendations,
"pagination": gin.H{
"page": page,
"limit": limit,
"total": total,
"pages": (total + int64(limit) - 1) / int64(limit),
},
})
}
// DeleteRecommendation deletes a recommendation
func (h *AIRecommendationHandler) DeleteRecommendation(c *gin.Context) {
userID := c.GetUint("user_id")
recommendationIDStr := c.Param("id")
recommendationID, err := strconv.ParseUint(recommendationIDStr, 10, 32)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid recommendation ID"})
return
}
// Delete the recommendation (only if it belongs to the user)
result := h.db.Where("id = ? AND user_id = ?", uint(recommendationID), userID).Delete(&models.AIRecommendation{})
if result.RowsAffected == 0 {
c.JSON(http.StatusNotFound, gin.H{"error": "Recommendation not found"})
return
}
c.JSON(http.StatusOK, gin.H{"message": "Recommendation deleted successfully"})
}
// GetInsights returns AI insights about user patterns
func (h *AIRecommendationHandler) GetInsights(c *gin.Context) {
userID := c.GetUint("user_id")
var insights struct {
TopInterests []string `json:"top_interests"`
LearningPaths []string `json:"learning_paths"`
ProductivityTips []string `json:"productivity_tips"`
ConnectionSuggestions []string `json:"connection_suggestions"`
Patterns struct {
BestProductivityHours []string `json:"best_productivity_hours"`
PreferredContentTypes []string `json:"preferred_content_types"`
LearningStyle string `json:"learning_style"`
} `json:"patterns"`
}
// Get user's top interests from bookmarks and tags
var interests []struct {
Tag string `json:"tag"`
Count int64 `json:"count"`
}
h.db.Raw(`
SELECT unnest(string_to_array(tags, ',')) as tag, COUNT(*) as count
FROM bookmarks
WHERE user_id = ? AND tags != ''
GROUP BY tag
ORDER BY count DESC
LIMIT 10
`, userID).Scan(&interests)
for _, interest := range interests {
insights.TopInterests = append(insights.TopInterests, interest.Tag)
}
// Get learning path suggestions
var learningPaths []struct {
Category string `json:"category"`
Count int64 `json:"count"`
}
h.db.Raw(`
SELECT lp.category, COUNT(*) as count
FROM learning_paths lp
JOIN enrollments e ON lp.id = e.learning_path_id
WHERE e.user_id = ? AND e.progress < 100
GROUP BY lp.category
ORDER BY count DESC
LIMIT 5
`, userID).Scan(&learningPaths)
for _, path := range learningPaths {
insights.LearningPaths = append(insights.LearningPaths, path.Category)
}
// Generate productivity tips based on task patterns
insights.ProductivityTips = []string{
"You complete most tasks in the morning - consider scheduling important work before noon",
"Tasks with deadlines are completed 80% faster - set more deadlines",
"You're most productive on Tuesdays and Wednesdays",
}
// Generate connection suggestions
topInterest := "technology"
if len(insights.TopInterests) > 0 {
topInterest = insights.TopInterests[0]
}
learningFocus := "productivity"
if len(insights.LearningPaths) > 0 {
learningFocus = insights.LearningPaths[0]
}
insights.ConnectionSuggestions = []string{
"Connect with users who share your interest in " + topInterest,
"Join communities focused on " + learningFocus,
}
// Analyze patterns
insights.Patterns.BestProductivityHours = []string{"9:00 AM - 11:00 AM", "2:00 PM - 4:00 PM"}
insights.Patterns.PreferredContentTypes = []string{"bookmarks", "notes", "courses"}
insights.Patterns.LearningStyle = "Visual learner who prefers structured content"
c.JSON(http.StatusOK, gin.H{"insights": insights})
}