Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions backend/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -35,3 +35,7 @@ AWS_DEFAULT_REGION=us-east-1
# ---------------- Other ----------------
LOG_CHANNEL=stack
LOG_LEVEL=debug

# ---------------- AI/OpenRouter ----------------
OPENROUTER_API_KEY=your_openrouter_api_key_here
AI_MODEL=anthropic/claude-3-haiku
296 changes: 296 additions & 0 deletions backend/app/Http/Controllers/AIRecommendationController.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,296 @@
<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;
use App\Services\AIRecommendationService;
use Illuminate\Support\Facades\Validator;

class AIRecommendationController extends Controller
{
/**
* Generate AI-powered movie recommendations
*/
public function generate(Request $request)
{
$user = $request->user();

if (!$user) {
return response()->json(['message' => 'Unauthorized'], 401);
}

// Validate request
$validator = Validator::make($request->all(), [
'mood' => 'nullable|string|max:500',
'genres' => 'nullable|array',
'genres.*' => 'string',
'min_rating' => 'nullable|numeric|min:0|max:10',
'year_range' => 'nullable|array|size:2',
'year_range.*' => 'integer|min:1900|max:2030',
'runtime_preference' => 'nullable|string'
]);

if ($validator->fails()) {
return response()->json([
'message' => 'Validation failed',
'errors' => $validator->errors()
], 422);
}

try {
// Extract preferences from request
$preferences = [
'mood' => $request->input('mood', ''),
'genres' => $request->input('genres', []),
'min_rating' => $request->input('min_rating', 0),
'year_range' => $request->input('year_range', [2000, 2024]),
'runtime_preference' => $request->input('runtime_preference', 'Any Length')
];

// Generate AI recommendations
$result = AIRecommendationService::getAIRecommendations($user, $preferences);

return response()->json([
'success' => true,
'data' => $result,
'message' => 'AI recommendations generated successfully'
]);

} catch (\Exception $e) {
\Log::error('AI Recommendation Controller Error: ' . $e->getMessage());

return response()->json([
'success' => false,
'message' => 'Failed to generate recommendations. Please try again.',
'error' => config('app.debug') ? $e->getMessage() : 'Internal server error'
], 500);
}
}

/**
* Analyze user mood from text input
*/
public function analyzeMood(Request $request)
{
$user = $request->user();

// Validate request
$validator = Validator::make($request->all(), [
'mood_text' => 'required|string|max:1000'
]);

if ($validator->fails()) {
return response()->json([
'message' => 'Validation failed',
'errors' => $validator->errors()
], 422);
}

try {
$moodText = $request->input('mood_text');

// Analyze mood using AI
$moodAnalysis = AIRecommendationService::analyzeMood($moodText, $user);

return response()->json([
'success' => true,
'data' => $moodAnalysis,
'message' => 'Mood analysis completed successfully'
]);

} catch (\Exception $e) {
\Log::error('Mood Analysis Error: ' . $e->getMessage());

return response()->json([
'success' => false,
'message' => 'Failed to analyze mood. Please try again.',
'error' => config('app.debug') ? $e->getMessage() : 'Internal server error'
], 500);
}
}

/**
* Get user insights and preference patterns
*/
public function getUserInsights(Request $request)
{
$user = $request->user();

if (!$user) {
return response()->json(['message' => 'Unauthorized'], 401);
}

try {
// Generate user profile insights
$userProfile = $this->generateUserProfileData($user);

return response()->json([
'success' => true,
'data' => $userProfile,
'message' => 'User insights retrieved successfully'
]);

} catch (\Exception $e) {
\Log::error('User Insights Error: ' . $e->getMessage());

return response()->json([
'success' => false,
'message' => 'Failed to retrieve user insights',
'error' => config('app.debug') ? $e->getMessage() : 'Internal server error'
], 500);
}
}

/**
* Quick AI recommendation based on minimal input
*/
public function quickRecommendation(Request $request)
{
$user = $request->user();

if (!$user) {
return response()->json(['message' => 'Unauthorized'], 401);
}

try {
// Use default preferences for quick recommendation
$preferences = [
'mood' => $request->input('mood', ''),
'genres' => [],
'min_rating' => 7.0,
'year_range' => [2010, 2024],
'runtime_preference' => 'Any Length'
];

$result = AIRecommendationService::getAIRecommendations($user, $preferences);

// Return only top 5 for quick recommendation
$quickResult = [
'recommendations' => $result['recommendations']->take(5),
'confidence_score' => $result['confidence_score']
];

return response()->json([
'success' => true,
'data' => $quickResult,
'message' => 'Quick recommendations generated'
]);

} catch (\Exception $e) {
\Log::error('Quick Recommendation Error: ' . $e->getMessage());

return response()->json([
'success' => false,
'message' => 'Failed to generate quick recommendations',
'error' => config('app.debug') ? $e->getMessage() : 'Internal server error'
], 500);
}
}

/**
* Generate user profile data for insights
*/
private function generateUserProfileData($user)
{
// Get user's movie statistics
$watchedCount = $user->films()->wherePivot('watched', true)->count();
$watchlistCount = $user->films()->wherePivot('watchlist', true)->count();
$reviewsCount = $user->reviews()->count();

// Get favorite genres
$favoriteGenres = $user->films()
->wherePivot('watched', true)
->with('genres')
->get()
->flatMap->genres
->groupBy('name')
->sortByDesc(function($genres) { return $genres->count(); })
->take(5)
->keys()
->toArray();

// Calculate average rating given
$averageRating = $user->reviews()->avg('rating') ?? 0;

// Recent activity
$recentWatchedCount = $user->films()
->wherePivot('watched', true)
->wherePivot('updated_at', '>=', now()->subDays(30))
->count();

return [
'statistics' => [
'movies_watched' => $watchedCount,
'watchlist_items' => $watchlistCount,
'reviews_written' => $reviewsCount,
'average_rating_given' => round($averageRating, 1),
'recent_activity' => $recentWatchedCount
],
'preferences' => [
'favorite_genres' => $favoriteGenres,
'most_active_period' => $this->getMostActivePeriod($user),
'preference_strength' => $this->calculatePreferenceStrength($favoriteGenres, $watchedCount)
],
'insights' => [
'profile_completeness' => $this->calculateProfileCompleteness($user),
'recommendation_confidence' => $this->calculateRecommendationConfidence($watchedCount, $reviewsCount),
'discovery_potential' => $this->calculateDiscoveryPotential($favoriteGenres)
]
];
}

private function getMostActivePeriod($user)
{
// This is a simplified implementation
// You could analyze timestamps to find actual peak activity periods
return 'Evening'; // Default response
}

private function calculatePreferenceStrength($favoriteGenres, $watchedCount)
{
if ($watchedCount == 0) return 'Unknown';
if (count($favoriteGenres) <= 2) return 'Strong';
if (count($favoriteGenres) <= 4) return 'Moderate';
return 'Diverse';
}

private function calculateProfileCompleteness($user)
{
$score = 0;

// Basic profile data
if ($user->name) $score += 20;
if ($user->email) $score += 10;

// Activity data
$watchedCount = $user->films()->wherePivot('watched', true)->count();
$reviewsCount = $user->reviews()->count();

if ($watchedCount > 0) $score += 30;
if ($watchedCount > 10) $score += 20;
if ($reviewsCount > 0) $score += 20;

return min(100, $score);
}

private function calculateRecommendationConfidence($watchedCount, $reviewsCount)
{
$confidence = 50; // Base confidence

if ($watchedCount > 10) $confidence += 20;
if ($watchedCount > 50) $confidence += 15;
if ($reviewsCount > 5) $confidence += 15;

return min(100, $confidence);
}

private function calculateDiscoveryPotential($favoriteGenres)
{
// More diverse preferences = higher discovery potential
$genreCount = count($favoriteGenres);

if ($genreCount >= 5) return 'High';
if ($genreCount >= 3) return 'Medium';
return 'Low';
}
}
Loading
Loading