Skip to content
Merged

Dev #97

Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
f4d5d4d
Add AI-powered movie recommendation feature
sakibul-shovon Sep 19, 2025
37a8895
Redesign login page with new background and UI
sakibul-shovon Sep 19, 2025
f31cff0
Merge branch 'dev' into 83-feat-ai-movie-recommendations
sakibul-shovon Sep 20, 2025
ff4a9de
Merge pull request #87 from RedPill-Team/83-feat-ai-movie-recommendat…
sakibul-shovon Sep 20, 2025
afad90e
Refine dark mode UI and improve AI recommendation logic
sakibul-shovon Sep 20, 2025
3a37bbc
done
sakibul-shovon Sep 20, 2025
1b7defc
Merge pull request #88 from RedPill-Team/ui-quiz-fix
sakibul-shovon Sep 20, 2025
f678c60
Add ai based user profile analysis ,info pages, footer,
sakibul-shovon Sep 20, 2025
8146acf
Merge pull request #90 from RedPill-Team/89-feat-profile-personality-…
sakibul-shovon Sep 20, 2025
09dee2a
Update favicon and app title, add top.svg asset
sakibul-shovon Sep 20, 2025
62e118b
Merge pull request #91 from RedPill-Team/89-feat-profile-personality-…
sakibul-shovon Sep 20, 2025
71b2bb2
Add carousel to featured movies on landing page
sakibul-shovon Sep 20, 2025
c697ef5
Merge pull request #92 from RedPill-Team/89-feat-profile-personality-…
sakibul-shovon Sep 20, 2025
d051586
Update Typo
sakibul-shovon Sep 20, 2025
efe6c73
Merge pull request #93 from RedPill-Team/89-feat-profile-personality-…
sakibul-shovon Sep 20, 2025
a2215d3
Update carousel links and login background image
sakibul-shovon Sep 20, 2025
0b4d4e4
Merge pull request #94 from RedPill-Team/89-feat-profile-personality-…
sakibul-shovon Sep 20, 2025
12485aa
Improve dark mode styles for review pages
sakibul-shovon Sep 20, 2025
e66b21e
Add dedicated pages for trending, top rated, and recommended movies
sakibul-shovon Sep 20, 2025
c9804e7
Merge pull request #95 from RedPill-Team/89-feat-profile-personality-…
sakibul-shovon Sep 20, 2025
815d7cf
Increase film list limits and update contact info
plasma-gith Sep 20, 2025
0fe0594
Revamp movie grid UI and improve layout consistency
sakibul-shovon Sep 20, 2025
81c3707
Merge pull request #96 from RedPill-Team/89-feat-profile-personality-…
sakibul-shovon Sep 20, 2025
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';
}
}
4 changes: 2 additions & 2 deletions backend/app/Http/Controllers/UserFilmController.php
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ public function removeFromWatchlist(Film $film, Request $request)

public function trending()
{
$films = Film::orderBy('popularity', 'desc')->limit(10)->get();
$films = Film::orderBy('popularity', 'desc')->limit(50)->get();
return response()->json($films);
}

Expand All @@ -112,7 +112,7 @@ public function topRated()
$films = \App\Models\Film::with('genres', 'people')
->withAvg('reviews', 'rating') // calculates average rating
->orderByDesc('reviews_avg_rating')
->take(20) // limit to top 20
->take(50) // limit to top 20
->get();

return response()->json($films);
Expand Down
Loading