Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bf532ba35d | ||
|
|
0896dd46d1 | ||
|
|
e9ba9e2700 |
No files matched your search
@@ -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) {
|
export async function POST(req: Request) {
|
||||||
try {
|
try {
|
||||||
const { userId } = await req.json();
|
const { userId, useExternalModel } = await req.json();
|
||||||
|
|
||||||
if (!userId) {
|
if (!userId) {
|
||||||
return NextResponse.json({ error: "User ID is required" }, { status: 400 });
|
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 = `
|
const prompt = `
|
||||||
You are a professional fitness trainer and nutritionist.
|
You are a professional fitness trainer and nutritionist.
|
||||||
Generate a detailed daily recommendation for a user with the following profile:
|
Generate a detailed daily recommendation for a user with the following profile:
|
||||||
@@ -42,14 +42,94 @@ export async function POST(req: Request) {
|
|||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
// Call Ollama
|
let parsedResponse;
|
||||||
|
|
||||||
|
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 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("Using DeepSeek AI model...");
|
||||||
|
|
||||||
|
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 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
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", {
|
const ollamaResponse = await fetch("http://localhost:11434/api/generate", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
},
|
},
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
model: "gemma3:latest", // Make sure this model is pulled
|
model: "gemma3:latest",
|
||||||
prompt: prompt,
|
prompt: prompt,
|
||||||
stream: false,
|
stream: false,
|
||||||
format: "json",
|
format: "json",
|
||||||
@@ -59,17 +139,15 @@ export async function POST(req: Request) {
|
|||||||
if (!ollamaResponse.ok) {
|
if (!ollamaResponse.ok) {
|
||||||
console.error("Ollama API error:", await ollamaResponse.text());
|
console.error("Ollama API error:", await ollamaResponse.text());
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
{ error: "Failed to generate recommendation from AI service" },
|
{ error: "Failed to generate recommendation from Ollama" },
|
||||||
{ status: 500 }
|
{ status: 500 }
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const aiData = await ollamaResponse.json();
|
const aiData = await ollamaResponse.json();
|
||||||
console.log("Raw AI Response:", aiData.response);
|
console.log("Raw Ollama Response:", aiData.response);
|
||||||
|
|
||||||
let parsedResponse;
|
|
||||||
try {
|
try {
|
||||||
// Helper to clean up the response
|
|
||||||
let cleanResponse = aiData.response.trim();
|
let cleanResponse = aiData.response.trim();
|
||||||
|
|
||||||
// Remove markdown code blocks if present
|
// Remove markdown code blocks if present
|
||||||
@@ -89,19 +167,19 @@ export async function POST(req: Request) {
|
|||||||
|
|
||||||
parsedResponse = JSON.parse(cleanResponse);
|
parsedResponse = JSON.parse(cleanResponse);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// Fallback if model doesn't return perfect JSON despite instruction
|
console.error("Failed to parse Ollama response:", aiData.response);
|
||||||
console.error("Failed to parse AI response:", aiData.response);
|
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
{ error: "Invalid response format from AI model" },
|
{ error: "Invalid response format from Ollama" },
|
||||||
{ status: 500 }
|
{ status: 500 }
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Save to database
|
// Save to database
|
||||||
const recommendation = await db.createRecommendation({
|
const recommendation = await db.createRecommendation({
|
||||||
id: crypto.randomUUID(),
|
id: crypto.randomUUID(),
|
||||||
userId,
|
userId,
|
||||||
fitnessProfileId: profile.userId, // Using userId as ID for now since it's 1:1
|
fitnessProfileId: profile.userId,
|
||||||
type: 'ai_plan',
|
type: 'ai_plan',
|
||||||
content: parsedResponse.recommendationText,
|
content: parsedResponse.recommendationText,
|
||||||
activityPlan: parsedResponse.activityPlan,
|
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
|
// Check permissions: Users can view their own, Admins/Trainers can view anyone's
|
||||||
if (currentUserId !== targetUserId) {
|
|
||||||
const currentUser = await db.getUserById(currentUserId)
|
const currentUser = await db.getUserById(currentUserId)
|
||||||
const isStaff = currentUser?.role === 'admin' || currentUser?.role === 'superAdmin' || currentUser?.role === 'trainer'
|
const isStaff = currentUser?.role === 'admin' || currentUser?.role === 'superAdmin' || currentUser?.role === 'trainer'
|
||||||
|
|
||||||
|
if (currentUserId !== targetUserId) {
|
||||||
if (!isStaff) {
|
if (!isStaff) {
|
||||||
return new NextResponse('Forbidden', { status: 403 })
|
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)
|
return NextResponse.json(recommendations)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error fetching recommendations:', error)
|
console.error('Error fetching recommendations:', error)
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ export default function RecommendationsPage() {
|
|||||||
const [pendingRecommendations, setPendingRecommendations] = useState<any[]>([]);
|
const [pendingRecommendations, setPendingRecommendations] = useState<any[]>([]);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [generating, setGenerating] = useState<string | null>(null);
|
const [generating, setGenerating] = useState<string | null>(null);
|
||||||
|
const [useExternalModel, setUseExternalModel] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchData();
|
fetchData();
|
||||||
@@ -56,7 +57,7 @@ export default function RecommendationsPage() {
|
|||||||
const res = await fetch("/api/recommendations/generate", {
|
const res = await fetch("/api/recommendations/generate", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: { "Content-Type": "application/json" },
|
headers: { "Content-Type": "application/json" },
|
||||||
body: JSON.stringify({ userId }),
|
body: JSON.stringify({ userId, useExternalModel }),
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!res.ok) {
|
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) {
|
if (loading) {
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center justify-center h-screen">
|
<div className="flex items-center justify-center h-screen">
|
||||||
@@ -108,7 +144,29 @@ export default function RecommendationsPage() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="container mx-auto py-10 px-4">
|
<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">
|
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8">
|
||||||
{/* Generate Section */}
|
{/* Generate Section */}
|
||||||
@@ -171,6 +229,12 @@ export default function RecommendationsPage() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex space-x-2">
|
<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
|
<button
|
||||||
onClick={() => handleApprove(rec.id, "approved")}
|
onClick={() => handleApprove(rec.id, "approved")}
|
||||||
className="flex-1 bg-green-600 text-white py-2 rounded hover:bg-green-700"
|
className="flex-1 bg-green-600 text-white py-2 rounded hover:bg-green-700"
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { usePathname } from "next/navigation";
|
import { usePathname } from "next/navigation";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
import {
|
import {
|
||||||
LayoutDashboard,
|
LayoutDashboard,
|
||||||
Users,
|
Users,
|
||||||
@@ -9,22 +10,59 @@ import {
|
|||||||
CreditCard,
|
CreditCard,
|
||||||
Settings,
|
Settings,
|
||||||
LogOut,
|
LogOut,
|
||||||
Brain
|
Brain,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { UserButton, useUser } from "@clerk/nextjs";
|
import { UserButton, useUser } from "@clerk/nextjs";
|
||||||
|
|
||||||
const menuItems = [
|
interface Recommendation {
|
||||||
{ icon: LayoutDashboard, label: "Dashboard", href: "/" },
|
id: string;
|
||||||
{ icon: Users, label: "Users", href: "/users" },
|
status: string;
|
||||||
{ 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" },
|
|
||||||
];
|
|
||||||
|
|
||||||
export function Sidebar() {
|
export function Sidebar() {
|
||||||
const pathname = usePathname();
|
const pathname = usePathname();
|
||||||
const { user } = useUser();
|
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 (
|
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">
|
<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) => {
|
{menuItems.map((item) => {
|
||||||
const Icon = item.icon;
|
const Icon = item.icon;
|
||||||
const isActive = pathname === item.href;
|
const isActive = pathname === item.href;
|
||||||
|
const label = typeof item.label === "string" ? item.label : item.label;
|
||||||
return (
|
return (
|
||||||
<Link
|
<Link
|
||||||
key={item.href}
|
key={item.href}
|
||||||
href={item.href}
|
href={item.href}
|
||||||
className={`flex items-center gap-3 px-4 py-3 rounded-lg transition-all duration-200 group ${isActive
|
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"
|
? "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"} />
|
<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>
|
</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,
|
||||||
|
},
|
||||||
|
});
|
||||||
Reference in new issue
Block a user