Compare commits

...
3 Commits
Author SHA1 Message Date
dimitar bf532ba35d deepseek integrated succesfully 2025-11-24 19:25:34 +01:00
dimitar 0896dd46d1 ai rec refinement 2025-11-24 19:08:58 +01:00
dimitar e9ba9e2700 recommendatio edit added 2025-11-24 18:36:04 +01:00
7 changed files with 457 additions and 68 deletions

No files matched your search

+3
View File
@@ -0,0 +1,3 @@
# DeepSeek AI API Key
# Get your API key from https://platform.deepseek.com/
DEEPSEEK_API_KEY=your_deepseek_api_key_here
Binary file not shown.
@@ -3,7 +3,7 @@ import { getDatabase } from "@/lib/database";
export async function POST(req: Request) {
try {
const { userId } = await req.json();
const { userId, useExternalModel } = await req.json();
if (!userId) {
return NextResponse.json({ error: "User ID is required" }, { status: 400 });
@@ -21,7 +21,7 @@ export async function POST(req: Request) {
);
}
// Construct prompt for Ollama
// Construct prompt
const prompt = `
You are a professional fitness trainer and nutritionist.
Generate a detailed daily recommendation for a user with the following profile:
@@ -42,66 +42,144 @@ export async function POST(req: Request) {
}
`;
// Call Ollama
const ollamaResponse = await fetch("http://localhost:11434/api/generate", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
model: "gemma3:latest", // Make sure this model is pulled
prompt: prompt,
stream: false,
format: "json",
}),
});
if (!ollamaResponse.ok) {
console.error("Ollama API error:", await ollamaResponse.text());
return NextResponse.json(
{ error: "Failed to generate recommendation from AI service" },
{ status: 500 }
);
}
const aiData = await ollamaResponse.json();
console.log("Raw AI Response:", aiData.response);
let parsedResponse;
try {
// Helper to clean up the response
let cleanResponse = aiData.response.trim();
// Remove markdown code blocks if present
if (cleanResponse.startsWith("```json")) {
cleanResponse = cleanResponse.replace(/^```json\s*/, "").replace(/\s*```$/, "");
} else if (cleanResponse.startsWith("```")) {
cleanResponse = cleanResponse.replace(/^```\s*/, "").replace(/\s*```$/, "");
if (useExternalModel) {
// Use DeepSeek AI
const deepseekApiKey = process.env.DEEPSEEK_API_KEY;
if (!deepseekApiKey) {
return NextResponse.json(
{ error: "DeepSeek API key not configured" },
{ status: 500 }
);
}
// Find the first '{' and last '}' to extract the JSON object
const firstBrace = cleanResponse.indexOf("{");
const lastBrace = cleanResponse.lastIndexOf("}");
console.log("Using DeepSeek AI model...");
if (firstBrace !== -1 && lastBrace !== -1) {
cleanResponse = cleanResponse.substring(firstBrace, lastBrace + 1);
const deepseekResponse = await fetch("https://api.deepseek.com/v1/chat/completions", {
method: "POST",
headers: {
"Content-Type": "application/json",
"Authorization": `Bearer ${deepseekApiKey}`,
},
body: JSON.stringify({
model: "deepseek-chat",
messages: [
{
role: "system",
content: "You are a professional fitness trainer and nutritionist. Always respond with valid JSON only, no markdown or code blocks."
},
{
role: "user",
content: prompt
}
],
temperature: 0.7,
max_tokens: 1000,
}),
});
if (!deepseekResponse.ok) {
const errorText = await deepseekResponse.text();
console.error("DeepSeek API error:", errorText);
return NextResponse.json(
{ error: "Failed to generate recommendation from DeepSeek AI" },
{ status: 500 }
);
}
parsedResponse = JSON.parse(cleanResponse);
} catch (e) {
// Fallback if model doesn't return perfect JSON despite instruction
console.error("Failed to parse AI response:", aiData.response);
return NextResponse.json(
{ error: "Invalid response format from AI model" },
{ status: 500 }
);
const deepseekData = await deepseekResponse.json();
console.log("Raw DeepSeek Response:", deepseekData);
try {
const content = deepseekData.choices[0].message.content;
let cleanResponse = content.trim();
// Remove markdown code blocks if present
if (cleanResponse.startsWith("```json")) {
cleanResponse = cleanResponse.replace(/^```json\s*/, "").replace(/\s*```$/, "");
} else if (cleanResponse.startsWith("```")) {
cleanResponse = cleanResponse.replace(/^```\s*/, "").replace(/\s*```$/, "");
}
// Find the first '{' and last '}' to extract the JSON object
const firstBrace = cleanResponse.indexOf("{");
const lastBrace = cleanResponse.lastIndexOf("}");
if (firstBrace !== -1 && lastBrace !== -1) {
cleanResponse = cleanResponse.substring(firstBrace, lastBrace + 1);
}
parsedResponse = JSON.parse(cleanResponse);
} catch (e) {
console.error("Failed to parse DeepSeek response:", deepseekData);
return NextResponse.json(
{ error: "Invalid response format from DeepSeek AI" },
{ status: 500 }
);
}
} else {
// Use local Ollama
console.log("Using local Ollama model...");
const ollamaResponse = await fetch("http://localhost:11434/api/generate", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
model: "gemma3:latest",
prompt: prompt,
stream: false,
format: "json",
}),
});
if (!ollamaResponse.ok) {
console.error("Ollama API error:", await ollamaResponse.text());
return NextResponse.json(
{ error: "Failed to generate recommendation from Ollama" },
{ status: 500 }
);
}
const aiData = await ollamaResponse.json();
console.log("Raw Ollama Response:", aiData.response);
try {
let cleanResponse = aiData.response.trim();
// Remove markdown code blocks if present
if (cleanResponse.startsWith("```json")) {
cleanResponse = cleanResponse.replace(/^```json\s*/, "").replace(/\s*```$/, "");
} else if (cleanResponse.startsWith("```")) {
cleanResponse = cleanResponse.replace(/^```\s*/, "").replace(/\s*```$/, "");
}
// Find the first '{' and last '}' to extract the JSON object
const firstBrace = cleanResponse.indexOf("{");
const lastBrace = cleanResponse.lastIndexOf("}");
if (firstBrace !== -1 && lastBrace !== -1) {
cleanResponse = cleanResponse.substring(firstBrace, lastBrace + 1);
}
parsedResponse = JSON.parse(cleanResponse);
} catch (e) {
console.error("Failed to parse Ollama response:", aiData.response);
return NextResponse.json(
{ error: "Invalid response format from Ollama" },
{ status: 500 }
);
}
}
// Save to database
const recommendation = await db.createRecommendation({
id: crypto.randomUUID(),
userId,
fitnessProfileId: profile.userId, // Using userId as ID for now since it's 1:1
fitnessProfileId: profile.userId,
type: 'ai_plan',
content: parsedResponse.recommendationText,
activityPlan: parsedResponse.activityPlan,
@@ -28,16 +28,22 @@ export async function GET(request: Request) {
}
// Check permissions: Users can view their own, Admins/Trainers can view anyone's
if (currentUserId !== targetUserId) {
const currentUser = await db.getUserById(currentUserId)
const isStaff = currentUser?.role === 'admin' || currentUser?.role === 'superAdmin' || currentUser?.role === 'trainer'
const currentUser = await db.getUserById(currentUserId)
const isStaff = currentUser?.role === 'admin' || currentUser?.role === 'superAdmin' || currentUser?.role === 'trainer'
if (currentUserId !== targetUserId) {
if (!isStaff) {
return new NextResponse('Forbidden', { status: 403 })
}
}
const recommendations = await db.getRecommendationsByUserId(targetUserId)
let recommendations = await db.getRecommendationsByUserId(targetUserId)
// Non-staff users should only see approved recommendations
if (!isStaff) {
recommendations = recommendations.filter((rec: any) => rec.status === 'approved')
}
return NextResponse.json(recommendations)
} catch (error) {
console.error('Error fetching recommendations:', error)
+66 -2
View File
@@ -26,6 +26,7 @@ export default function RecommendationsPage() {
const [pendingRecommendations, setPendingRecommendations] = useState<any[]>([]);
const [loading, setLoading] = useState(true);
const [generating, setGenerating] = useState<string | null>(null);
const [useExternalModel, setUseExternalModel] = useState(false);
useEffect(() => {
fetchData();
@@ -56,7 +57,7 @@ export default function RecommendationsPage() {
const res = await fetch("/api/recommendations/generate", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ userId }),
body: JSON.stringify({ userId, useExternalModel }),
});
if (!res.ok) {
@@ -98,6 +99,41 @@ export default function RecommendationsPage() {
}
};
const handleEdit = async (rec: Recommendation) => {
const newContent = prompt("Edit Advice:", rec.content);
const newActivityPlan = prompt("Edit Activity Plan:", rec.activityPlan);
const newDietPlan = prompt("Edit Diet Plan:", rec.dietPlan);
if (newContent === null || newActivityPlan === null || newDietPlan === null) {
// User cancelled one of the prompts
return;
}
try {
const res = await fetch("/api/recommendations", {
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
id: rec.id,
content: newContent,
activityPlan: newActivityPlan,
dietPlan: newDietPlan,
}),
});
if (!res.ok) {
const errorData = await res.json();
alert(`Failed to update recommendation: ${errorData.error || 'Unknown error'}`);
} else {
alert("Recommendation updated successfully!");
fetchData(); // Refresh data
}
} catch (error) {
console.error("Error updating recommendation:", error);
alert("Failed to update recommendation.");
}
};
if (loading) {
return (
<div className="flex items-center justify-center h-screen">
@@ -108,7 +144,29 @@ export default function RecommendationsPage() {
return (
<div className="container mx-auto py-10 px-4">
<h1 className="text-3xl font-bold mb-8">AI Recommendations</h1>
<div className="flex justify-between items-center mb-8">
<h1 className="text-3xl font-bold">AI Recommendations</h1>
{/* Model Selection Toggle */}
<div className="flex items-center gap-3 bg-white px-4 py-2 rounded-lg shadow">
<span className="text-sm font-medium text-gray-700">
{useExternalModel ? "DeepSeek AI" : "Local Ollama"}
</span>
<button
onClick={() => setUseExternalModel(!useExternalModel)}
className={`relative inline-flex h-6 w-11 items-center rounded-full transition-colors ${useExternalModel ? "bg-blue-600" : "bg-gray-300"
}`}
>
<span
className={`inline-block h-4 w-4 transform rounded-full bg-white transition-transform ${useExternalModel ? "translate-x-6" : "translate-x-1"
}`}
/>
</button>
<span className="text-xs text-gray-500">
{useExternalModel ? "External" : "Local"}
</span>
</div>
</div>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8">
{/* Generate Section */}
@@ -171,6 +229,12 @@ export default function RecommendationsPage() {
</div>
</div>
<div className="flex space-x-2">
<button
onClick={() => handleEdit(rec)}
className="flex-1 bg-blue-500 text-white py-2 rounded hover:bg-blue-600"
>
Edit
</button>
<button
onClick={() => handleApprove(rec.id, "approved")}
className="flex-1 bg-green-600 text-white py-2 rounded hover:bg-green-700"
+50 -13
View File
@@ -2,6 +2,7 @@
import Link from "next/link";
import { usePathname } from "next/navigation";
import { useEffect, useState } from "react";
import {
LayoutDashboard,
Users,
@@ -9,22 +10,59 @@ import {
CreditCard,
Settings,
LogOut,
Brain
Brain,
} from "lucide-react";
import { UserButton, useUser } from "@clerk/nextjs";
const menuItems = [
{ icon: LayoutDashboard, label: "Dashboard", href: "/" },
{ icon: Users, label: "Users", href: "/users" },
{ icon: Brain, label: "AI Recommendations", href: "/recommendations" },
{ icon: CalendarCheck, label: "Attendance", href: "/attendance" },
{ icon: CreditCard, label: "Payments", href: "/payments" },
{ icon: Settings, label: "Settings", href: "/settings" },
];
interface Recommendation {
id: string;
status: string;
}
export function Sidebar() {
const pathname = usePathname();
const { user } = useUser();
const [pendingCount, setPendingCount] = useState(0);
useEffect(() => {
const fetchPending = async () => {
try {
const res = await fetch("/api/recommendations");
if (res.ok) {
const data = await res.json();
const pending = (data.recommendations || []).filter(
(r: Recommendation) => r.status === "pending"
);
setPendingCount(pending.length);
}
} catch (e) {
console.error("Failed to fetch pending recommendations", e);
}
};
fetchPending();
}, []);
const menuItems = [
{ icon: LayoutDashboard, label: "Dashboard", href: "/" },
{ icon: Users, label: "Users", href: "/users" },
{
icon: Brain,
label: (
<span className="flex items-center">
AI Recommendations
{pendingCount > 0 && (
<span className="ml-2 inline-block bg-red-600 text-xs rounded-full px-2 py-0.5">
{pendingCount}
</span>
)}
</span>
),
href: "/recommendations",
},
{ icon: CalendarCheck, label: "Attendance", href: "/attendance" },
{ icon: CreditCard, label: "Payments", href: "/payments" },
{ icon: Settings, label: "Settings", href: "/settings" },
];
return (
<aside className="w-64 bg-slate-900 text-white h-screen fixed left-0 top-0 flex flex-col border-r border-slate-800">
@@ -38,18 +76,17 @@ export function Sidebar() {
{menuItems.map((item) => {
const Icon = item.icon;
const isActive = pathname === item.href;
const label = typeof item.label === "string" ? item.label : item.label;
return (
<Link
key={item.href}
href={item.href}
className={`flex items-center gap-3 px-4 py-3 rounded-lg transition-all duration-200 group ${isActive
? "bg-blue-600 text-white shadow-lg shadow-blue-900/20"
: "text-slate-400 hover:bg-slate-800 hover:text-white"
}`}
: "text-slate-400 hover:bg-slate-800 hover:text-white"}`}
>
<Icon size={20} className={isActive ? "text-white" : "text-slate-500 group-hover:text-white"} />
<span className="font-medium">{item.label}</span>
<span className="font-medium">{label}</span>
</Link>
);
})}
@@ -0,0 +1,201 @@
import { useEffect, useState } from "react";
import { View, Text, FlatList, ActivityIndicator, StyleSheet, RefreshControl } from "react-native";
import { useAuth } from "@clerk/clerk-expo";
import { API_BASE_URL, API_ENDPOINTS } from "../../config/api";
interface Recommendation {
id: string;
userId: string;
content: string;
activityPlan?: string;
dietPlan?: string;
status: string;
createdAt: string;
}
export default function RecommendationsScreen() {
const { getToken, userId } = useAuth();
const [recommendations, setRecommendations] = useState<Recommendation[]>([]);
const [loading, setLoading] = useState(true);
const [refreshing, setRefreshing] = useState(false);
const fetchRecommendations = async () => {
try {
if (!userId) {
console.error('No userId available');
return;
}
const token = await getToken();
const headers: Record<string, string> = {
'Content-Type': 'application/json',
};
if (token) {
headers['Authorization'] = `Bearer ${token}`;
}
const url = `${API_BASE_URL}${API_ENDPOINTS.RECOMMENDATIONS}?userId=${userId}`;
console.log('Fetching recommendations from:', url);
const res = await fetch(url, { headers });
if (!res.ok) {
const errorText = await res.text();
console.error('API Error:', res.status, errorText);
throw new Error(`Network response was not ok: ${res.status}`);
}
const data = await res.json();
console.log('Recommendations data:', data);
setRecommendations(data.recommendations || data || []);
} catch (e) {
console.error('Failed to load recommendations', e);
} finally {
setLoading(false);
setRefreshing(false);
}
};
useEffect(() => {
fetchRecommendations();
}, []);
const onRefresh = () => {
setRefreshing(true);
fetchRecommendations();
};
if (loading) {
return (
<View style={styles.centered}>
<ActivityIndicator size="large" color="#000" />
</View>
);
}
return (
<View style={styles.container}>
<Text style={styles.header}>AI Recommendations</Text>
<FlatList
data={recommendations}
keyExtractor={(item) => item.id}
refreshControl={<RefreshControl refreshing={refreshing} onRefresh={onRefresh} />}
ListEmptyComponent={
<View style={styles.emptyContainer}>
<Text style={styles.empty}>No recommendations available yet.</Text>
<Text style={styles.emptySub}>Pull down to refresh</Text>
</View>
}
renderItem={({ item }) => (
<View style={styles.card}>
<View style={styles.cardHeader}>
<Text style={styles.status}>{item.status.toUpperCase()}</Text>
<Text style={styles.date}>{new Date(item.createdAt).toLocaleDateString()}</Text>
</View>
<Text style={styles.sectionTitle}>Daily Advice</Text>
<Text style={styles.content}>{item.content}</Text>
{item.activityPlan && (
<>
<Text style={styles.sectionTitle}>Activity Plan</Text>
<Text style={styles.content}>{item.activityPlan}</Text>
</>
)}
{item.dietPlan && (
<>
<Text style={styles.sectionTitle}>Diet Plan</Text>
<Text style={styles.content}>{item.dietPlan}</Text>
</>
)}
</View>
)}
/>
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#f5f5f5',
},
header: {
fontSize: 28,
fontWeight: 'bold',
padding: 20,
paddingBottom: 12,
color: '#1a1a1a',
},
centered: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
backgroundColor: '#f5f5f5',
},
card: {
backgroundColor: '#fff',
padding: 16,
marginHorizontal: 16,
marginBottom: 12,
borderRadius: 12,
shadowColor: '#000',
shadowOpacity: 0.1,
shadowRadius: 4,
shadowOffset: { width: 0, height: 2 },
elevation: 3,
},
cardHeader: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
marginBottom: 12,
paddingBottom: 12,
borderBottomWidth: 1,
borderBottomColor: '#e0e0e0',
},
status: {
fontSize: 12,
fontWeight: '600',
color: '#2e7d32',
backgroundColor: '#e8f5e9',
paddingHorizontal: 8,
paddingVertical: 4,
borderRadius: 4,
},
date: {
fontSize: 12,
color: '#666',
},
sectionTitle: {
fontSize: 14,
fontWeight: '600',
color: '#1a1a1a',
marginTop: 12,
marginBottom: 6,
},
content: {
fontSize: 14,
color: '#333',
lineHeight: 20,
},
emptyContainer: {
alignItems: 'center',
justifyContent: 'center',
paddingVertical: 60,
},
empty: {
textAlign: 'center',
fontSize: 16,
color: '#666',
fontWeight: '500',
},
emptySub: {
textAlign: 'center',
fontSize: 14,
color: '#999',
marginTop: 8,
},
});