diff --git a/apps/admin/.env.example b/apps/admin/.env.example new file mode 100644 index 0000000..a30effd --- /dev/null +++ b/apps/admin/.env.example @@ -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 diff --git a/apps/admin/data/fitai.db b/apps/admin/data/fitai.db index 2f02afe..640dc8b 100644 Binary files a/apps/admin/data/fitai.db and b/apps/admin/data/fitai.db differ diff --git a/apps/admin/src/app/api/recommendations/generate/route.ts b/apps/admin/src/app/api/recommendations/generate/route.ts index bf209da..cbb6541 100644 --- a/apps/admin/src/app/api/recommendations/generate/route.ts +++ b/apps/admin/src/app/api/recommendations/generate/route.ts @@ -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, diff --git a/apps/admin/src/app/recommendations/page.tsx b/apps/admin/src/app/recommendations/page.tsx index 4cf3c91..dbdf00a 100644 --- a/apps/admin/src/app/recommendations/page.tsx +++ b/apps/admin/src/app/recommendations/page.tsx @@ -26,6 +26,7 @@ export default function RecommendationsPage() { const [pendingRecommendations, setPendingRecommendations] = useState([]); const [loading, setLoading] = useState(true); const [generating, setGenerating] = useState(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) { @@ -143,7 +144,29 @@ export default function RecommendationsPage() { return (
-

AI Recommendations

+
+

AI Recommendations

+ + {/* Model Selection Toggle */} +
+ + {useExternalModel ? "DeepSeek AI" : "Local Ollama"} + + + + {useExternalModel ? "External" : "Local"} + +
+
{/* Generate Section */}