261 lines
11 KiB
TypeScript
261 lines
11 KiB
TypeScript
"use client";
|
|
|
|
import { useEffect, useState } from "react";
|
|
import { useUser } from "@clerk/nextjs";
|
|
|
|
interface User {
|
|
id: string;
|
|
firstName: string;
|
|
lastName: string;
|
|
email: string;
|
|
}
|
|
|
|
interface Recommendation {
|
|
id: string;
|
|
userId: string;
|
|
content: string;
|
|
activityPlan: string;
|
|
dietPlan: string;
|
|
status: string;
|
|
createdAt: Date;
|
|
}
|
|
|
|
export default function RecommendationsPage() {
|
|
const { user } = useUser();
|
|
const [users, setUsers] = useState<User[]>([]);
|
|
const [pendingRecommendations, setPendingRecommendations] = useState<any[]>([]);
|
|
const [loading, setLoading] = useState(true);
|
|
const [generating, setGenerating] = useState<string | null>(null);
|
|
const [useExternalModel, setUseExternalModel] = useState(false);
|
|
|
|
useEffect(() => {
|
|
fetchData();
|
|
}, []);
|
|
|
|
const fetchData = async () => {
|
|
try {
|
|
// Fetch users
|
|
const usersRes = await fetch("/api/users");
|
|
const usersData = await usersRes.json();
|
|
setUsers(usersData.users || []);
|
|
|
|
// Fetch pending recommendations
|
|
const recsRes = await fetch("/api/recommendations");
|
|
const recsData = await recsRes.json();
|
|
const allRecs = recsData.recommendations || [];
|
|
setPendingRecommendations(allRecs.filter((r: any) => r.status === 'pending'));
|
|
} catch (error) {
|
|
console.error("Error fetching data:", error);
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
const handleGenerate = async (userId: string) => {
|
|
setGenerating(userId);
|
|
try {
|
|
const res = await fetch("/api/recommendations/generate", {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({ userId, useExternalModel }),
|
|
});
|
|
|
|
if (!res.ok) {
|
|
const error = await res.json();
|
|
alert(`Error: ${error.error}`);
|
|
} else {
|
|
alert("Recommendation generated successfully!");
|
|
fetchData(); // Refresh data
|
|
}
|
|
} catch (error) {
|
|
console.error(error);
|
|
alert("Failed to generate recommendation.");
|
|
} finally {
|
|
setGenerating(null);
|
|
}
|
|
};
|
|
|
|
const handleApprove = async (recommendationId: string, status: "approved" | "rejected") => {
|
|
try {
|
|
const res = await fetch("/api/recommendations/approve", {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({
|
|
recommendationId,
|
|
status,
|
|
approvedBy: user?.id || "admin",
|
|
}),
|
|
});
|
|
|
|
if (!res.ok) {
|
|
const errorData = await res.json();
|
|
alert(`Failed to update status: ${errorData.error || 'Unknown error'}`);
|
|
} else {
|
|
fetchData(); // Refresh data
|
|
}
|
|
} catch (error) {
|
|
console.error(error);
|
|
alert("Error processing request");
|
|
}
|
|
};
|
|
|
|
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">
|
|
<div className="text-xl">Loading...</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div className="container mx-auto py-10 px-4">
|
|
<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 */}
|
|
<div>
|
|
<h2 className="text-2xl font-semibold mb-4">Generate Recommendations</h2>
|
|
<div className="bg-white shadow rounded-lg p-6">
|
|
<p className="mb-4 text-gray-600">
|
|
Select a user to generate a new daily recommendation.
|
|
</p>
|
|
<ul className="space-y-4">
|
|
{users.map((user) => (
|
|
<li key={user.id} className="flex items-center justify-between border-b pb-2">
|
|
<div>
|
|
<p className="font-medium">
|
|
{user.firstName} {user.lastName}
|
|
</p>
|
|
<p className="text-sm text-gray-500">{user.email}</p>
|
|
</div>
|
|
<button
|
|
onClick={() => handleGenerate(user.id)}
|
|
disabled={generating === user.id}
|
|
className="bg-blue-600 text-white px-4 py-2 rounded hover:bg-blue-700 disabled:opacity-50"
|
|
>
|
|
{generating === user.id ? "Generating..." : "Generate"}
|
|
</button>
|
|
</li>
|
|
))}
|
|
{users.length === 0 && (
|
|
<p className="text-gray-500 italic">No users found.</p>
|
|
)}
|
|
</ul>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Pending Approvals Section */}
|
|
<div>
|
|
<h2 className="text-2xl font-semibold mb-4">Pending Approvals</h2>
|
|
<div className="bg-white shadow rounded-lg p-6">
|
|
{pendingRecommendations.length === 0 ? (
|
|
<p className="text-gray-500 italic">No pending recommendations.</p>
|
|
) : (
|
|
<ul className="space-y-6">
|
|
{pendingRecommendations.map((rec) => (
|
|
<li key={rec.id} className="border rounded p-4">
|
|
<div className="flex justify-between items-start mb-2">
|
|
<h3 className="font-bold">For: User {rec.userId}</h3>
|
|
<span className="text-xs text-gray-500">
|
|
{new Date(rec.createdAt).toLocaleDateString()}
|
|
</span>
|
|
</div>
|
|
<div className="space-y-2 text-sm mb-4">
|
|
<div>
|
|
<span className="font-semibold">Advice:</span> {rec.content}
|
|
</div>
|
|
<div>
|
|
<span className="font-semibold">Activity:</span> {rec.activityPlan}
|
|
</div>
|
|
<div>
|
|
<span className="font-semibold">Diet:</span> {rec.dietPlan}
|
|
</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"
|
|
>
|
|
Approve
|
|
</button>
|
|
<button
|
|
onClick={() => handleApprove(rec.id, "rejected")}
|
|
className="flex-1 bg-red-600 text-white py-2 rounded hover:bg-red-700"
|
|
>
|
|
Reject
|
|
</button>
|
|
</div>
|
|
</li>
|
|
))}
|
|
</ul>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|