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
2 changes: 1 addition & 1 deletion backend/routes/api.php
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
use App\Http\Controllers\ForumPostController;

use App\Http\Controllers\AIRecommendationController;
=======use App\Http\Controllers\FriendActivityController;
use App\Http\Controllers\FriendActivityController;


/*
Expand Down
216 changes: 136 additions & 80 deletions frontend/src/api/ai.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,13 @@
import api from './api';

/**
* AI Recommendation API calls
* AI Recom return data.choices?.[0]?.message?.content;
} finally {
clearTimeout(timeoutId);
}
};

// Generate AI-powered movie recommendations (simplified)ation API calls
*/

// OpenRouter configuration (using same pattern as MovieDetails.jsx)
Expand All @@ -10,27 +16,47 @@ const AI_MODEL = "deepseek/deepseek-chat-v3.1"; // Same model as movie summarize

// Direct OpenRouter API call (frontend approach)
const callOpenRouterAPI = async (prompt, systemMessage = "You are a helpful movie recommendation AI.") => {
const response = await fetch("https://openrouter.ai/api/v1/chat/completions", {
method: "POST",
headers: {
"Content-Type": "application/json",
"Authorization": `Bearer ${OPENROUTER_API_KEY}`,
},
body: JSON.stringify({
model: AI_MODEL,
messages: [
{ role: "system", content: systemMessage },
{ role: "user", content: prompt }
],
max_tokens: 2000,
temperature: 0.8, // Increased for more variety
top_p: 0.9, // Added for more randomness
// Create AbortController for timeout
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 300000); // 5 minute timeout

try {
const response = await fetch("https://openrouter.ai/api/v1/chat/completions", {
method: "POST",
headers: {
"Content-Type": "application/json",
"Authorization": `Bearer ${OPENROUTER_API_KEY}`,
"HTTP-Referer": window.location.origin,
"X-Title": "RedPill Movie Platform"
}
})
});
},
body: JSON.stringify({
model: AI_MODEL,
messages: [
{ role: "system", content: systemMessage },
{ role: "user", content: prompt }
],
max_tokens: 500, // Reduced from 1000 to ensure complete response
temperature: 0.7, // Slightly reduced for faster response
top_p: 0.9,
stream: false // Ensure streaming is disabled
}),
signal: controller.signal // Add abort signal
});

if (!response.ok) {
throw new Error(`OpenRouter API failed: ${response.status}`);
}

const data = await response.json();
return data.choices?.[0]?.message?.content;
} catch (error) {
if (error.name === 'AbortError') {
throw new Error('Request timeout: The AI service took too long to respond');
}
throw error;
} finally {
clearTimeout(timeoutId);
}

if (!response.ok) {
throw new Error(`OpenRouter API failed: ${response.status}`);
Expand Down Expand Up @@ -83,64 +109,37 @@ export const generateAIRecommendations = async (preferences = {}) => {

// Shuffle the movies array to add more randomness
const shuffledMovies = [...movies].sort(() => Math.random() - 0.5);

// Build improved AI prompt with mood analysis

// Reduce the number of movies sent to OpenRouter for faster response

const prompt = `USER REQUEST: "${preferences.prompt || 'good movies'}"

INSTRUCTIONS FOR AI:
1. First analyze the user's emotional state and mood from their text
2. If they express sadness, depression, or bad mood - recommend uplifting, feel-good, or comfort movies
3. If they want specific genres, prioritize those strictly
4. If they're vague, recommend based on their emotional needs
5. Always explain how the movie will help their current mood
6. Generate DIFFERENT recommendations each time, even for similar prompts
7. IMPORTANT: Pick different movies from the list below, avoid repetition

Available movies from database:
${shuffledMovies.slice(0, 40).map(m => {
INSTRUCTIONS:
1. Analyze user's mood
2. Recommend uplifting movies if they're sad
3. Match specific genres if mentioned
4. Pick from movies below, avoid repetition

Available movies:
${shuffledMovies.slice(0, 5).map(m => {
const genres = m.genres?.map(g => g.name).join(', ') || 'Unknown';
return `- ID: ${m.id} | TITLE: ${m.title} | YEAR: ${m.release_date ? new Date(m.release_date).getFullYear() : 'Unknown'} | GENRES: ${genres} | RATING: ${m.vote_average || 'N/A'} | PLOT: ${m.overview?.substring(0, 120) || 'No description'}`;
return `- ID: ${m.id} | TITLE: ${m.title} | YEAR: ${m.release_date ? new Date(m.release_date).getFullYear() : 'Unknown'} | GENRES: ${genres} | RATING: ${m.vote_average || 'N/A'}`;
}).join('\n')}

MOOD-BASED RECOMMENDATIONS:
- If user seems sad/depressed: Recommend Comedy, Feel-good, Uplifting movies
- If user wants distraction: Recommend Action, Adventure, Thriller
- If user wants emotional release: Recommend Drama, but with hopeful endings
- If user is bored: Recommend entertaining, engaging movies
- If user mentions specific genre: Follow that exactly

IMPORTANT RANDOMIZATION RULES:
- NEVER recommend the same movies for similar prompts
- Vary your selections each time, even if the prompt is identical
- Consider different moods, sub-genres, and perspectives
- Mix popular and lesser-known films from the available list
- Generate fresh explanations and insights every time

CURRENT REQUEST ID: ${Date.now()}-${Math.random()}

TASK:
1. Analyze user's emotional state from their Bengali/English text
2. Select TOP 5 DIFFERENT movies that will help improve their mood or match their request
3. For each movie, provide comprehensive information including:
- Why it matches their request/mood
- Key plot details and themes
- Famous quotes from the movie
- Main cast and director information
- Interesting fun facts about the movie
4. Use empathetic language in explanations
5. Ensure variety - avoid repeating same movies from previous requests

Return ONLY valid JSON:
TASK: Select TOP 3 movies and provide basic info.

IMPORTANT: Return ONLY valid JSON. No markdown, no explanation.

{
"recommendations": [
{
"film_id": 123,
"explanation": "This [genre] movie will help because [specific mood benefit]. It's perfect when you're feeling [emotion] as it offers [what it provides]",
"match_percentage": 95,
"details": "Detailed plot summary, themes, and what makes this movie special. Include runtime, director, and key story elements.",
"quotes": ["Famous quote 1 from the movie", "Memorable dialogue 2", "Iconic line 3"],
"cast_info": "Director: [Name]. Stars: [Actor 1] as [Character], [Actor 2] as [Character]. Notable performances and what they're known for.",
"fun_facts": ["Interesting trivia about filming", "Awards or achievements", "Behind-the-scenes facts", "Cultural impact or legacy"]
"explanation": "Brief explanation why this movie helps",
"match_percentage": 90,
"details": "Short plot summary",
"quotes": ["One quote"],
"cast_info": "Director and main stars",
"fun_facts": ["One fact"]
}
]
}`;
Expand Down Expand Up @@ -178,38 +177,95 @@ Return ONLY valid JSON:

const parseAIRecommendations = (aiResponse, originalMovies) => {
try {
const cleanResponse = aiResponse.replace(/```json|```/g, '').trim();
// More aggressive cleaning of the response
let cleanResponse = aiResponse
.replace(/```json|```/g, '')
.replace(/```/g, '')
.trim();

// Remove any text before the first { and after the last }
const firstBrace = cleanResponse.indexOf('{');
const lastBrace = cleanResponse.lastIndexOf('}');

if (firstBrace !== -1 && lastBrace !== -1) {
cleanResponse = cleanResponse.substring(firstBrace, lastBrace + 1);
}

// Check if response is incomplete (missing closing braces)
const openBraces = (cleanResponse.match(/\{/g) || []).length;
const closeBraces = (cleanResponse.match(/\}/g) || []).length;

if (openBraces > closeBraces) {
console.log('Detected incomplete JSON response, attempting to fix...');
// Add missing closing braces
const missingBraces = openBraces - closeBraces;
cleanResponse += '}]}'.repeat(Math.min(missingBraces, 3)); // Limit to prevent infinite braces
}

// Try to fix common JSON issues
cleanResponse = cleanResponse
.replace(/\n/g, ' ') // Replace newlines with spaces
.replace(/\r/g, ' ') // Replace carriage returns
.replace(/\t/g, ' ') // Replace tabs
.replace(/ +/g, ' ') // Replace multiple spaces with single space
.replace(/,(\s*[}\]])/g, '$1') // Remove trailing commas
.replace(/(["\w])\s*\n\s*(["\w])/g, '$1 $2'); // Fix broken strings across lines

console.log('Cleaned AI response:', cleanResponse);

const aiData = JSON.parse(cleanResponse);

if (!aiData.recommendations) return [];
if (!aiData.recommendations) {
console.log('No recommendations found in AI response');
return originalMovies.slice(0, 5).map(movie => ({
...movie,
ai_explanation: 'AI-recommended movie for you',
ai_match_percentage: 80
}));
}

// Create map of AI recommendations
const aiMap = new Map();
aiData.recommendations.forEach(rec => {
aiMap.set(rec.film_id, rec);
});

// Only return movies that have real AI data
return originalMovies
const enhanced = originalMovies
.map(movie => {
const aiData = aiMap.get(movie.id);
if (aiData) {
return {
...movie,
ai_explanation: aiData.explanation,
ai_match_percentage: aiData.match_percentage,
ai_details: aiData.details,
ai_quotes: aiData.quotes,
ai_cast_info: aiData.cast_info,
ai_fun_facts: aiData.fun_facts
ai_explanation: aiData.explanation || 'AI-recommended for you',
ai_match_percentage: aiData.match_percentage || 75,
ai_details: aiData.details || movie.overview,
ai_quotes: aiData.quotes || [],
ai_cast_info: aiData.cast_info || 'Cast information available',
ai_fun_facts: aiData.fun_facts || []
};
}
return null;
})
.filter(Boolean)
.slice(0, 10);
.filter(Boolean);

return enhanced.length > 0 ? enhanced.slice(0, 5) : originalMovies.slice(0, 3).map(movie => ({
...movie,
ai_explanation: 'Recommended movie for your request',
ai_match_percentage: 75
}));

} catch (error) {
console.error('Failed to parse AI response:', error);
return originalMovies.slice(0, 10);
console.log('Original AI response:', aiResponse);

// Return fallback with basic movie data
return originalMovies.slice(0, 3).map(movie => ({
...movie,
ai_explanation: 'Recommended based on your request',
ai_match_percentage: 75,
ai_details: movie.overview || 'Great movie choice for you'
}));
}
};

Expand Down
10 changes: 5 additions & 5 deletions frontend/src/components/PostCard.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ const PostCard = ({ post }) => {
<div
className={`p-6 rounded-2xl border backdrop-blur-md hover:scale-[1.01] transition-transform
${theme === 'dark'
? 'bg-gray-800/80 border-gray-700 text-gray-100 hover:bg-gray-700/60'
? 'bg-white/5 border-white/10 text-white hover:bg-white/10'
: 'bg-gray-100/80 border-gray-300 text-gray-900 hover:bg-gray-200/80'}`}
>
<div className="flex items-start gap-4">
Expand All @@ -22,21 +22,21 @@ const PostCard = ({ post }) => {
{/* Post Content */}
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2 mb-2">
<span className={`font-medium ${theme === 'dark' ? 'text-gray-100' : 'text-gray-900'}`}>
<span className={`font-medium ${theme === 'dark' ? 'text-white' : 'text-gray-900'}`}>
{post.user?.username || 'Unknown'}
</span>
<span className={`text-sm ${theme === 'dark' ? 'text-gray-400' : 'text-gray-500'}`}>
<span className={`text-sm ${theme === 'dark' ? 'text-gray-300' : 'text-gray-500'}`}>
• {new Date(post.created_at).toLocaleDateString()}
</span>
</div>

<Link to={`/forum/${post.id}`} className="block">
<h2 className={`font-semibold text-lg mb-2 hover:underline ${theme === 'dark' ? 'text-gray-100' : 'text-gray-900'}`}>
<h2 className={`font-semibold text-lg mb-2 hover:underline ${theme === 'dark' ? 'text-white' : 'text-gray-900'}`}>
{post.title}
</h2>
</Link>

<p className={`line-clamp-3 mb-4 ${theme === 'dark' ? 'text-gray-300' : 'text-gray-700'}`}>
<p className={`line-clamp-3 mb-4 ${theme === 'dark' ? 'text-gray-200' : 'text-gray-700'}`}>
{post.content}
</p>

Expand Down
10 changes: 5 additions & 5 deletions frontend/src/components/PostForm.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ const PostForm = ({ post, onSubmit, onCancel }) => {
<form
onSubmit={handleSubmit}
className={`w-full h-full flex flex-col justify-between transition-colors duration-300
${theme === 'dark' ? 'bg-gray-800/80 text-white' : 'bg-white/90 text-gray-900'}`}
${theme === 'dark' ? 'bg-white/5 text-white' : 'bg-white/90 text-gray-900'}`}
>
{/* Inputs */}
<div className="flex-1 flex flex-col gap-4">
Expand All @@ -34,7 +34,7 @@ const PostForm = ({ post, onSubmit, onCancel }) => {
required
className={`w-full rounded-none border-b px-4 py-2 text-base transition focus:outline-none focus:ring-2
${theme === 'dark'
? 'border-gray-700 bg-gray-800 text-white placeholder-gray-400 focus:ring-gray-500'
? 'border-white/10 bg-white/5 text-white placeholder-gray-400 focus:ring-gray-500'
: 'border-gray-300 bg-white text-gray-900 placeholder-gray-500 focus:ring-gray-400'
}`}
/>
Expand All @@ -47,7 +47,7 @@ const PostForm = ({ post, onSubmit, onCancel }) => {
required
className={`w-full flex-1 resize-none rounded-none border-b px-4 py-2 text-base transition focus:outline-none focus:ring-2
${theme === 'dark'
? 'border-gray-700 bg-gray-800 text-white placeholder-gray-400 focus:ring-gray-500'
? 'border-white/10 bg-white/5 text-white placeholder-gray-400 focus:ring-gray-500'
: 'border-gray-300 bg-white text-gray-900 placeholder-gray-500 focus:ring-gray-400'
}`}
></textarea>
Expand All @@ -61,7 +61,7 @@ const PostForm = ({ post, onSubmit, onCancel }) => {
onClick={onCancel}
className={`px-4 py-2 rounded-lg font-semibold transition
${theme === 'dark'
? 'bg-gray-700 hover:bg-red-600 text-gray-200'
? 'bg-white/10 hover:bg-red-600 text-gray-200'
: 'bg-gray-100 hover:bg-red-500 text-gray-700'
}`}
>
Expand All @@ -72,7 +72,7 @@ const PostForm = ({ post, onSubmit, onCancel }) => {
type="submit"
className={`px-4 py-2 rounded-lg font-bold transition
${theme === 'dark'
? 'bg-gray-600 hover:bg-red-600 text-white'
? 'bg-white/10 hover:bg-red-600 text-white'
: 'bg-gray-300 hover:bg-red-500 text-gray-900'
}`}
>
Expand Down
Loading