Files

812 lines
24 KiB
Go

package handlers
import (
"encoding/json"
"fmt"
"net/http"
"os"
"time"
"github.com/gin-gonic/gin"
"github.com/trackeep/backend/models"
"github.com/trackeep/backend/services"
)
// SummarizeContentRequest represents a request to summarize content
type SummarizeContentRequest struct {
ContentType string `json:"content_type" binding:"required"` // "bookmark", "note", "file"
ContentID uint `json:"content_id" binding:"required"`
Provider string `json:"provider"` // "mistral", "longcat", "" for default
ModelType string `json:"model_type"` // "standard", "thinking", "upgraded_thinking"
Options struct {
Length string `json:"length"` // "short", "medium", "long"
Style string `json:"style"` // "bullet", "paragraph", "executive"
IncludeKey bool `json:"include_key"` // Include key points
} `json:"options"`
}
// GenerateTaskSuggestionsRequest represents a request for task suggestions
type GenerateTaskSuggestionsRequest struct {
Context string `json:"context"` // "calendar", "deadlines", "habits", "all"
Timeframe string `json:"timeframe"` // "today", "week", "month"
Limit int `json:"limit"` // Max number of suggestions
Provider string `json:"provider"` // "mistral", "longcat", "" for default
ModelType string `json:"model_type"` // "standard", "thinking", "upgraded_thinking"
}
// GenerateTagsRequest represents a request for tag suggestions
type GenerateTagsRequest struct {
ContentType string `json:"content_type" binding:"required"`
ContentID uint `json:"content_id" binding:"required"`
Content string `json:"content" binding:"required"`
ExistingTag string `json:"existing_tags"`
Provider string `json:"provider"` // "mistral", "longcat", "" for default
ModelType string `json:"model_type"` // "standard", "thinking", "upgraded_thinking"
}
// GenerateContentRequest represents a request for content generation
type GenerateContentRequest struct {
Prompt string `json:"prompt" binding:"required"`
ContentType string `json:"content_type" binding:"required"`
Context string `json:"context"`
Temperature float64 `json:"temperature"`
MaxLength int `json:"max_length"`
Provider string `json:"provider"` // "mistral", "longcat", "" for default
ModelType string `json:"model_type"` // "standard", "thinking", "upgraded_thinking"
}
// SummarizeContent generates AI summary for content
func SummarizeContent(c *gin.Context) {
userID := c.GetUint("user_id")
var req SummarizeContentRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
// Get content based on type
var content string
var title string
switch req.ContentType {
case "bookmark":
var bookmark models.Bookmark
if err := models.DB.Where("id = ? AND user_id = ?", req.ContentID, userID).First(&bookmark).Error; err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "Content not found"})
return
}
content = bookmark.Content
title = bookmark.Title
case "note":
var note models.Note
if err := models.DB.Where("id = ? AND user_id = ?", req.ContentID, userID).First(&note).Error; err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "Content not found"})
return
}
content = note.Content
title = note.Title
default:
c.JSON(http.StatusBadRequest, gin.H{"error": "Unsupported content type"})
return
}
if content == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "No content to summarize"})
return
}
// Check if summary already exists
var existingSummary models.AISummary
if err := models.DB.Where("user_id = ? AND content_type = ? AND content_id = ?", userID, req.ContentType, req.ContentID).First(&existingSummary).Error; err == nil {
c.JSON(http.StatusOK, existingSummary)
return
}
// Generate summary using AI
summary, err := generateAISummary(content, title, req.Options, req.Provider, req.ModelType)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to generate summary: " + err.Error()})
return
}
// Save summary
aiSummary := models.AISummary{
UserID: userID,
ContentType: req.ContentType,
ContentID: req.ContentID,
Title: summary.Title,
Summary: summary.Summary,
KeyPoints: summary.KeyPoints,
Tags: summary.Tags,
ReadTime: summary.ReadTime,
Complexity: summary.Complexity,
ModelUsed: getProviderModel(req.Provider),
Confidence: summary.Confidence,
LastAnalyzed: time.Now(),
}
if err := models.DB.Create(&aiSummary).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to save summary"})
return
}
c.JSON(http.StatusOK, aiSummary)
}
// GetTaskSuggestions generates AI task suggestions
func GetTaskSuggestions(c *gin.Context) {
userID := c.GetUint("user_id")
var req GenerateTaskSuggestionsRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
// Build context from user data
contextData, err := buildTaskContext(userID, req.Context, req.Timeframe)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to build context"})
return
}
// Generate suggestions
suggestions, err := generateTaskSuggestions(contextData, req.Limit, req.Provider, req.ModelType)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to generate suggestions: " + err.Error()})
return
}
// Save suggestions
var aiSuggestions []models.AITaskSuggestion
for _, suggestion := range suggestions {
aiSuggestion := models.AITaskSuggestion{
UserID: userID,
Title: suggestion.Title,
Description: suggestion.Description,
Priority: suggestion.Priority,
Category: suggestion.Category,
Reasoning: suggestion.Reasoning,
ContextType: req.Context,
ContextData: suggestion.ContextData,
Deadline: suggestion.Deadline,
EstimatedTime: suggestion.EstimatedTime,
ModelUsed: getProviderModel(req.Provider),
Confidence: suggestion.Confidence,
}
models.DB.Create(&aiSuggestion)
aiSuggestions = append(aiSuggestions, aiSuggestion)
}
c.JSON(http.StatusOK, aiSuggestions)
}
// GenerateTagSuggestions generates AI tag suggestions
func GenerateTagSuggestions(c *gin.Context) {
userID := c.GetUint("user_id")
var req GenerateTagsRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
// Generate tags
tags, err := generateTagSuggestions(req.Content, req.ExistingTag, req.Provider, req.ModelType)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to generate tags: " + err.Error()})
return
}
// Save suggestion
tagSuggestion := models.AITagSuggestion{
UserID: userID,
ContentType: req.ContentType,
ContentID: req.ContentID,
SuggestedTags: tags.Suggested,
ExistingTags: req.ExistingTag,
Relevance: tags.Relevance,
ModelUsed: getProviderModel(req.Provider),
Confidence: tags.Confidence,
}
if err := models.DB.Create(&tagSuggestion).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to save tag suggestion"})
return
}
c.JSON(http.StatusOK, tagSuggestion)
}
// GenerateContent generates AI content
func GenerateContent(c *gin.Context) {
userID := c.GetUint("user_id")
var req GenerateContentRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
// Generate content
content, err := generateAIContent(req.Prompt, req.ContentType, req.Context, req.Temperature, req.MaxLength, req.Provider, req.ModelType)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to generate content: " + err.Error()})
return
}
// Save generation
aiContent := models.AIContentGeneration{
UserID: userID,
Prompt: req.Prompt,
ContentType: req.ContentType,
Context: req.Context,
Title: content.Title,
Content: content.Content,
WordCount: content.WordCount,
ReadTime: content.ReadTime,
ModelUsed: getProviderModel(req.Provider),
ProcessingMs: content.ProcessingMs,
TokenCount: content.TokenCount,
Confidence: content.Confidence,
Temperature: req.Temperature,
}
if err := models.DB.Create(&aiContent).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to save content"})
return
}
c.JSON(http.StatusOK, aiContent)
}
// GetAIProviders returns available AI providers
func GetAIProviders(c *gin.Context) {
providers := services.GetAvailableProviders()
providerInfo := make([]map[string]interface{}, 0)
for _, provider := range providers {
info := map[string]interface{}{
"id": string(provider),
"name": getProviderDisplayName(provider),
}
// Add model info
switch provider {
case services.ProviderMistral:
standardModel := os.Getenv("MISTRAL_MODEL")
thinkingModel := os.Getenv("MISTRAL_MODEL_THINKING")
info["models"] = []map[string]string{
{"id": "standard", "name": standardModel, "type": "Standard"},
{"id": "thinking", "name": thinkingModel, "type": "Thinking"},
}
info["description"] = "Mistral AI - Fast and efficient European AI"
info["icon"] = "🇪🇺"
case services.ProviderLongCat:
standardModel := os.Getenv("LONGCAT_MODEL")
thinkingModel := os.Getenv("LONGCAT_MODEL_THINKING")
upgradedModel := os.Getenv("LONGCAT_MODEL_THINKING_UPGRADED")
models := []map[string]string{
{"id": "standard", "name": standardModel, "type": "Standard"},
{"id": "thinking", "name": thinkingModel, "type": "Thinking"},
}
if upgradedModel != "" {
models = append(models, map[string]string{"id": "upgraded_thinking", "name": upgradedModel, "type": "Upgraded Thinking"})
}
info["models"] = models
info["description"] = "LongCat AI - High-performance AI models"
info["icon"] = "🐱"
case services.ProviderGrok:
standardModel := os.Getenv("GROK_MODEL")
thinkingModel := os.Getenv("GROK_MODEL_THINKING")
models := []map[string]string{
{"id": "standard", "name": standardModel, "type": "Standard"},
}
if thinkingModel != "" && thinkingModel != standardModel {
models = append(models, map[string]string{"id": "thinking", "name": thinkingModel, "type": "Thinking"})
}
info["models"] = models
info["description"] = "Grok AI - Real-time information from X"
info["icon"] = "🐦"
case services.ProviderDeepSeek:
standardModel := os.Getenv("DEEPSEEK_MODEL")
thinkingModel := os.Getenv("DEEPSEEK_MODEL_THINKING")
models := []map[string]string{
{"id": "standard", "name": standardModel, "type": "Standard"},
}
if thinkingModel != "" && thinkingModel != standardModel {
models = append(models, map[string]string{"id": "thinking", "name": thinkingModel, "type": "Reasoning"})
}
info["models"] = models
info["description"] = "DeepSeek - Advanced reasoning AI"
info["icon"] = "🔍"
case services.ProviderOllama:
standardModel := os.Getenv("OLLAMA_MODEL")
thinkingModel := os.Getenv("OLLAMA_MODEL_THINKING")
models := []map[string]string{
{"id": "standard", "name": standardModel, "type": "Standard"},
}
if thinkingModel != "" && thinkingModel != standardModel {
models = append(models, map[string]string{"id": "thinking", "name": thinkingModel, "type": "Local"})
}
info["models"] = models
info["description"] = "Ollama - Local AI models"
info["icon"] = "🦙"
case services.ProviderOpenRouter:
standardModel := os.Getenv("OPENROUTER_MODEL")
thinkingModel := os.Getenv("OPENROUTER_MODEL_THINKING")
models := []map[string]string{
{"id": "standard", "name": standardModel, "type": "Standard"},
}
if thinkingModel != "" && thinkingModel != standardModel {
models = append(models, map[string]string{"id": "thinking", "name": thinkingModel, "type": "Thinking"})
}
info["models"] = models
info["description"] = "OpenRouter - Unified access to many models"
info["icon"] = "🌀"
}
providerInfo = append(providerInfo, info)
}
c.JSON(http.StatusOK, gin.H{"providers": providerInfo})
}
// Helper function to get display name for provider
func getProviderDisplayName(provider services.AIProvider) string {
switch provider {
case services.ProviderMistral:
return "Mistral AI"
case services.ProviderLongCat:
return "LongCat AI"
case services.ProviderGrok:
return "Grok AI"
case services.ProviderDeepSeek:
return "DeepSeek"
case services.ProviderOllama:
return "Ollama"
case services.ProviderOpenRouter:
return "OpenRouter"
default:
return string(provider)
}
}
// GetAISummaries retrieves AI summaries for user
func GetAISummaries(c *gin.Context) {
userID := c.GetUint("user_id")
var summaries []models.AISummary
models.DB.Where("user_id = ?", userID).Order("created_at desc").Find(&summaries)
c.JSON(http.StatusOK, summaries)
}
// GetTaskSuggestions retrieves task suggestions for user
func GetTaskSuggestionsList(c *gin.Context) {
userID := c.GetUint("user_id")
var suggestions []models.AITaskSuggestion
models.DB.Where("user_id = ? AND accepted = false AND dismissed = false", userID).Order("created_at desc").Find(&suggestions)
c.JSON(http.StatusOK, suggestions)
}
// AcceptTaskSuggestion accepts a task suggestion
func AcceptTaskSuggestion(c *gin.Context) {
userID := c.GetUint("user_id")
suggestionID := c.Param("id")
var suggestion models.AITaskSuggestion
if err := models.DB.Where("id = ? AND user_id = ?", suggestionID, userID).First(&suggestion).Error; err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "Suggestion not found"})
return
}
// Create actual task
task := models.Task{
UserID: userID,
Title: suggestion.Title,
Description: suggestion.Description,
Priority: models.TaskPriority(suggestion.Priority),
Status: models.TaskStatusPending,
DueDate: suggestion.Deadline,
}
if err := models.DB.Create(&task).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create task"})
return
}
// Mark suggestion as accepted
suggestion.Accepted = true
models.DB.Save(&suggestion)
c.JSON(http.StatusOK, gin.H{"message": "Task created successfully", "task_id": task.ID})
}
// DismissTaskSuggestion dismisses a task suggestion
func DismissTaskSuggestion(c *gin.Context) {
userID := c.GetUint("user_id")
suggestionID := c.Param("id")
var suggestion models.AITaskSuggestion
if err := models.DB.Where("id = ? AND user_id = ?", suggestionID, userID).First(&suggestion).Error; err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "Suggestion not found"})
return
}
suggestion.Dismissed = true
models.DB.Save(&suggestion)
c.JSON(http.StatusOK, gin.H{"message": "Suggestion dismissed"})
}
// Helper structs for AI responses
type AISummaryResponse struct {
Title string `json:"title"`
Summary string `json:"summary"`
KeyPoints string `json:"key_points"`
Tags string `json:"tags"`
ReadTime int `json:"read_time"`
Complexity string `json:"complexity"`
Confidence float64 `json:"confidence"`
}
type TaskSuggestionResponse struct {
Title string `json:"title"`
Description string `json:"description"`
Priority string `json:"priority"`
Category string `json:"category"`
Reasoning string `json:"reasoning"`
ContextData string `json:"context_data"`
Deadline *time.Time `json:"deadline"`
EstimatedTime int `json:"estimated_time"`
Confidence float64 `json:"confidence"`
}
type TagSuggestionResponse struct {
Suggested string `json:"suggested"`
Relevance float64 `json:"relevance"`
Confidence float64 `json:"confidence"`
}
type ContentGenerationResponse struct {
Title string `json:"title"`
Content string `json:"content"`
WordCount int `json:"word_count"`
ReadTime int `json:"read_time"`
ProcessingMs int64 `json:"processing_ms"`
TokenCount int `json:"token_count"`
Confidence float64 `json:"confidence"`
}
// AI generation functions (simplified - would call actual AI models)
func generateAISummary(content, title string, options struct {
Length string `json:"length"`
Style string `json:"style"`
IncludeKey bool `json:"include_key"`
}, provider string, modelType string) (*AISummaryResponse, error) {
// Build prompt for summarization
prompt := fmt.Sprintf(`Please summarize the following content:
Title: %s
Content: %s
Length: %s
Style: %s
Include key points: %t
Provide a JSON response with:
- title: Brief title
- summary: Main summary
- key_points: Array of key points (if requested)
- tags: Array of relevant tags
- read_time: Estimated reading time in minutes
- complexity: "low", "medium", or "high"
- confidence: Confidence score 0-1`, title, content, options.Length, options.Style, options.IncludeKey)
messages := []services.Message{
{Role: "system", Content: "You are an expert content summarizer. Always respond with valid JSON."},
{Role: "user", Content: prompt},
}
// Determine provider
aiProvider := services.ProviderMistral // default
if provider == "longcat" {
aiProvider = services.ProviderLongCat
}
aiService := services.NewAIService(aiProvider)
req := services.AIRequest{
Messages: messages,
MaxTokens: 2000,
Temperature: 0.3,
ModelType: modelType,
}
var resp *services.AIResponse
var err error
// Choose the appropriate method based on model type
switch req.ModelType {
case "thinking":
resp, err = aiService.ChatCompletionWithThinking(req)
case "upgraded_thinking":
resp, err = aiService.ChatCompletionWithUpgradedThinking(req)
default:
resp, err = aiService.ChatCompletion(req)
}
if err != nil {
return nil, err
}
// Parse the response content properly for thinking models
actualContent := services.ParseThinkingResponse(resp, aiProvider, modelType)
var summary AISummaryResponse
if err := json.Unmarshal([]byte(actualContent), &summary); err != nil {
return nil, err
}
return &summary, nil
}
func generateTaskSuggestions(contextData map[string]interface{}, limit int, provider string, modelType string) ([]TaskSuggestionResponse, error) {
// Build prompt for task suggestions
prompt := fmt.Sprintf(`Based on the following user context, suggest %d tasks:
Context: %+v
Provide a JSON array of task objects with:
- title: Task title
- description: Task description
- priority: "low", "medium", "high", "urgent"
- category: Task category
- reasoning: Why this task is suggested
- context_data: Additional context
- deadline: Suggested deadline (ISO date or null)
- estimated_time: Estimated time in minutes
- confidence: Confidence score 0-1`, limit, contextData)
messages := []services.Message{
{Role: "system", Content: "You are a productivity assistant. Always respond with valid JSON array."},
{Role: "user", Content: prompt},
}
// Determine provider
aiProvider := services.ProviderMistral // default
if provider == "longcat" {
aiProvider = services.ProviderLongCat
}
aiService := services.NewAIService(aiProvider)
req := services.AIRequest{
Messages: messages,
MaxTokens: 2000,
Temperature: 0.7,
ModelType: modelType,
}
var resp *services.AIResponse
var err error
// Choose the appropriate method based on model type
switch req.ModelType {
case "thinking":
resp, err = aiService.ChatCompletionWithThinking(req)
case "upgraded_thinking":
resp, err = aiService.ChatCompletionWithUpgradedThinking(req)
default:
resp, err = aiService.ChatCompletion(req)
}
if err != nil {
return nil, err
}
// Parse the response content properly for thinking models
actualContent := services.ParseThinkingResponse(resp, aiProvider, modelType)
var suggestions []TaskSuggestionResponse
if err := json.Unmarshal([]byte(actualContent), &suggestions); err != nil {
return nil, err
}
return suggestions, nil
}
func generateTagSuggestions(content, existingTags string, provider string, modelType string) (*TagSuggestionResponse, error) {
prompt := fmt.Sprintf(`Suggest relevant tags for this content:
Content: %s
Existing tags: %s
Provide JSON response with:
- suggested: Array of suggested tags
- relevance: Relevance score 0-1
- confidence: Confidence score 0-1`, content, existingTags)
messages := []services.Message{
{Role: "system", Content: "You are a tagging expert. Always respond with valid JSON."},
{Role: "user", Content: prompt},
}
// Determine provider
aiProvider := services.ProviderMistral // default
if provider == "longcat" {
aiProvider = services.ProviderLongCat
}
aiService := services.NewAIService(aiProvider)
req := services.AIRequest{
Messages: messages,
MaxTokens: 1000,
Temperature: 0.5,
ModelType: modelType,
}
var resp *services.AIResponse
var err error
// Choose the appropriate method based on model type
switch req.ModelType {
case "thinking":
resp, err = aiService.ChatCompletionWithThinking(req)
case "upgraded_thinking":
resp, err = aiService.ChatCompletionWithUpgradedThinking(req)
default:
resp, err = aiService.ChatCompletion(req)
}
if err != nil {
return nil, err
}
// Parse the response content properly for thinking models
actualContent := services.ParseThinkingResponse(resp, aiProvider, modelType)
var tags TagSuggestionResponse
if err := json.Unmarshal([]byte(actualContent), &tags); err != nil {
return nil, err
}
return &tags, nil
}
func generateAIContent(prompt, contentType, context string, temperature float64, maxLength int, provider string, modelType string) (*ContentGenerationResponse, error) {
fullPrompt := fmt.Sprintf(`Generate %s content based on this prompt:
%s
Additional context: %s
Max length: %d words
Provide JSON response with:
- title: Generated title
- content: Generated content
- word_count: Word count
- read_time: Estimated reading time in minutes
- confidence: Confidence score 0-1`, contentType, prompt, context, maxLength)
messages := []services.Message{
{Role: "system", Content: "You are a content generation expert. Always respond with valid JSON."},
{Role: "user", Content: fullPrompt},
}
// Determine provider
aiProvider := services.ProviderMistral // default
if provider == "longcat" {
aiProvider = services.ProviderLongCat
}
aiService := services.NewAIService(aiProvider)
// Adjust temperature if provided
temp := 0.7
if temperature > 0 {
temp = temperature
}
req := services.AIRequest{
Messages: messages,
MaxTokens: maxLength * 2, // Rough estimate
Temperature: temp,
ModelType: modelType,
}
var resp *services.AIResponse
var err error
// Choose the appropriate method based on model type
switch req.ModelType {
case "thinking":
resp, err = aiService.ChatCompletionWithThinking(req)
case "upgraded_thinking":
resp, err = aiService.ChatCompletionWithUpgradedThinking(req)
default:
resp, err = aiService.ChatCompletion(req)
}
if err != nil {
return nil, err
}
// Parse the response content properly for thinking models
actualContent := services.ParseThinkingResponse(resp, aiProvider, modelType)
var content ContentGenerationResponse
if err := json.Unmarshal([]byte(actualContent), &content); err != nil {
return nil, err
}
content.ProcessingMs = 0 // Would track actual processing time
content.TokenCount = resp.Usage.TotalTokens
return &content, nil
}
func buildTaskContext(userID uint, contextType, timeframe string) (map[string]interface{}, error) {
ctx := make(map[string]interface{})
// Get upcoming tasks
var tasks []models.Task
query := models.DB.Where("user_id = ?", userID)
if timeframe == "today" {
query = query.Where("deadline <= ?", time.Now().AddDate(0, 0, 1))
} else if timeframe == "week" {
query = query.Where("deadline <= ?", time.Now().AddDate(0, 0, 7))
}
query.Find(&tasks)
ctx["tasks"] = tasks
// Get calendar events
var events []models.CalendarEvent
models.DB.Where("user_id = ? AND start_time >= ?", userID, time.Now()).Find(&events)
ctx["events"] = events
return ctx, nil
}
// Helper function to get model name based on provider
func getProviderModel(provider string) string {
switch provider {
case "mistral":
return os.Getenv("MISTRAL_MODEL")
case "longcat":
return os.Getenv("LONGCAT_MODEL")
case "grok":
return os.Getenv("GROK_MODEL")
case "deepseek":
return os.Getenv("DEEPSEEK_MODEL")
case "ollama":
return os.Getenv("OLLAMA_MODEL")
case "openrouter":
return os.Getenv("OPENROUTER_MODEL")
default:
return os.Getenv("MISTRAL_MODEL")
}
}