mirror of
https://github.com/Dvorinka/Trackeep.git
synced 2026-06-03 20:12:58 +00:00
402 lines
13 KiB
Go
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})
|
|
}
|