context extended
maybe need polishing
This commit is contained in:
parent
684481fd2c
commit
28b5b52a8f
Binary file not shown.
49
apps/admin/scripts/create-fitness-goals.js
Normal file
49
apps/admin/scripts/create-fitness-goals.js
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
const Database = require('better-sqlite3');
|
||||||
|
const path = require('path');
|
||||||
|
|
||||||
|
const dbPath = path.join(__dirname, '../data/fitai.db');
|
||||||
|
const db = new Database(dbPath);
|
||||||
|
|
||||||
|
console.log('Creating fitness_goals table...');
|
||||||
|
|
||||||
|
try {
|
||||||
|
db.exec(`
|
||||||
|
CREATE TABLE IF NOT EXISTS fitness_goals (
|
||||||
|
id TEXT PRIMARY KEY,
|
||||||
|
user_id TEXT NOT NULL,
|
||||||
|
fitness_profile_id TEXT,
|
||||||
|
goal_type TEXT NOT NULL CHECK(goal_type IN ('weight_target', 'strength_milestone', 'endurance_target', 'flexibility_goal', 'habit_building', 'custom')),
|
||||||
|
title TEXT NOT NULL,
|
||||||
|
description TEXT,
|
||||||
|
target_value REAL,
|
||||||
|
current_value REAL,
|
||||||
|
unit TEXT,
|
||||||
|
start_date INTEGER NOT NULL,
|
||||||
|
target_date INTEGER,
|
||||||
|
completed_date INTEGER,
|
||||||
|
status TEXT NOT NULL DEFAULT 'active' CHECK(status IN ('active', 'completed', 'abandoned', 'paused')),
|
||||||
|
progress REAL DEFAULT 0,
|
||||||
|
priority TEXT DEFAULT 'medium' CHECK(priority IN ('low', 'medium', 'high')),
|
||||||
|
notes TEXT,
|
||||||
|
created_at INTEGER NOT NULL,
|
||||||
|
updated_at INTEGER NOT NULL,
|
||||||
|
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
|
||||||
|
FOREIGN KEY (fitness_profile_id) REFERENCES fitness_profiles(id) ON DELETE CASCADE
|
||||||
|
)
|
||||||
|
`);
|
||||||
|
|
||||||
|
// Create indexes for better query performance
|
||||||
|
db.exec(`
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_fitness_goals_user_id ON fitness_goals(user_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_fitness_goals_status ON fitness_goals(status);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_fitness_goals_profile_id ON fitness_goals(fitness_profile_id);
|
||||||
|
`);
|
||||||
|
|
||||||
|
console.log('✅ fitness_goals table created successfully');
|
||||||
|
console.log('✅ Indexes created successfully');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ Migration failed:', error);
|
||||||
|
process.exit(1);
|
||||||
|
} finally {
|
||||||
|
db.close();
|
||||||
|
}
|
||||||
39
apps/admin/src/app/api/fitness-goals/[id]/complete/route.ts
Normal file
39
apps/admin/src/app/api/fitness-goals/[id]/complete/route.ts
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
import { NextRequest, NextResponse } from 'next/server';
|
||||||
|
import { auth } from '@clerk/nextjs/server';
|
||||||
|
import { getDatabase } from '@/lib/database';
|
||||||
|
|
||||||
|
// POST - Mark goal as complete
|
||||||
|
export async function POST(
|
||||||
|
req: NextRequest,
|
||||||
|
{ params }: { params: { id: string } }
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
const { userId } = await auth();
|
||||||
|
if (!userId) {
|
||||||
|
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||||
|
}
|
||||||
|
|
||||||
|
const { id } = await params;
|
||||||
|
const db = await getDatabase();
|
||||||
|
|
||||||
|
// Verify goal exists and user owns it
|
||||||
|
const existingGoal = await db.getFitnessGoalById(id);
|
||||||
|
if (!existingGoal) {
|
||||||
|
return NextResponse.json({ error: 'Goal not found' }, { status: 404 });
|
||||||
|
}
|
||||||
|
if (existingGoal.userId !== userId) {
|
||||||
|
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mark as completed
|
||||||
|
const completedGoal = await db.completeGoal(id);
|
||||||
|
|
||||||
|
return NextResponse.json(completedGoal);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error completing fitness goal:', error);
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: 'Internal server error' },
|
||||||
|
{ status: 500 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
119
apps/admin/src/app/api/fitness-goals/[id]/route.ts
Normal file
119
apps/admin/src/app/api/fitness-goals/[id]/route.ts
Normal file
@ -0,0 +1,119 @@
|
|||||||
|
import { NextRequest, NextResponse } from 'next/server';
|
||||||
|
import { auth } from '@clerk/nextjs/server';
|
||||||
|
import { getDatabase } from '@/lib/database';
|
||||||
|
|
||||||
|
// GET - Get specific goal
|
||||||
|
export async function GET(
|
||||||
|
req: NextRequest,
|
||||||
|
{ params }: { params: { id: string } }
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
const { userId } = await auth();
|
||||||
|
if (!userId) {
|
||||||
|
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||||
|
}
|
||||||
|
|
||||||
|
const { id } = await params;
|
||||||
|
const db = await getDatabase();
|
||||||
|
|
||||||
|
const goal = await db.getFitnessGoalById(id);
|
||||||
|
|
||||||
|
if (!goal) {
|
||||||
|
return NextResponse.json({ error: 'Goal not found' }, { status: 404 });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify ownership
|
||||||
|
if (goal.userId !== userId) {
|
||||||
|
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
|
||||||
|
}
|
||||||
|
|
||||||
|
return NextResponse.json(goal);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching fitness goal:', error);
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: 'Internal server error' },
|
||||||
|
{ status: 500 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// PUT - Update goal
|
||||||
|
export async function PUT(
|
||||||
|
req: NextRequest,
|
||||||
|
{ params }: { params: { id: string } }
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
const { userId } = await auth();
|
||||||
|
if (!userId) {
|
||||||
|
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||||
|
}
|
||||||
|
|
||||||
|
const { id } = await params;
|
||||||
|
const db = await getDatabase();
|
||||||
|
|
||||||
|
// Verify goal exists and user owns it
|
||||||
|
const existingGoal = await db.getFitnessGoalById(id);
|
||||||
|
if (!existingGoal) {
|
||||||
|
return NextResponse.json({ error: 'Goal not found' }, { status: 404 });
|
||||||
|
}
|
||||||
|
if (existingGoal.userId !== userId) {
|
||||||
|
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
|
||||||
|
}
|
||||||
|
|
||||||
|
const updates = await req.json();
|
||||||
|
|
||||||
|
// Don't allow changing userId or id
|
||||||
|
delete updates.userId;
|
||||||
|
delete updates.id;
|
||||||
|
delete updates.createdAt;
|
||||||
|
|
||||||
|
const updatedGoal = await db.updateFitnessGoal(id, updates);
|
||||||
|
|
||||||
|
return NextResponse.json(updatedGoal);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error updating fitness goal:', error);
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: 'Internal server error' },
|
||||||
|
{ status: 500 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// DELETE - Delete goal
|
||||||
|
export async function DELETE(
|
||||||
|
req: NextRequest,
|
||||||
|
{ params }: { params: { id: string } }
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
const { userId } = await auth();
|
||||||
|
if (!userId) {
|
||||||
|
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||||
|
}
|
||||||
|
|
||||||
|
const { id } = await params;
|
||||||
|
const db = await getDatabase();
|
||||||
|
|
||||||
|
// Verify goal exists and user owns it
|
||||||
|
const existingGoal = await db.getFitnessGoalById(id);
|
||||||
|
if (!existingGoal) {
|
||||||
|
return NextResponse.json({ error: 'Goal not found' }, { status: 404 });
|
||||||
|
}
|
||||||
|
if (existingGoal.userId !== userId) {
|
||||||
|
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
|
||||||
|
}
|
||||||
|
|
||||||
|
const deleted = await db.deleteFitnessGoal(id);
|
||||||
|
|
||||||
|
if (deleted) {
|
||||||
|
return NextResponse.json({ success: true });
|
||||||
|
} else {
|
||||||
|
return NextResponse.json({ error: 'Failed to delete goal' }, { status: 500 });
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error deleting fitness goal:', error);
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: 'Internal server error' },
|
||||||
|
{ status: 500 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
95
apps/admin/src/app/api/fitness-goals/route.ts
Normal file
95
apps/admin/src/app/api/fitness-goals/route.ts
Normal file
@ -0,0 +1,95 @@
|
|||||||
|
import { NextRequest, NextResponse } from 'next/server';
|
||||||
|
import { auth } from '@clerk/nextjs/server';
|
||||||
|
import { getDatabase } from '@/lib/database';
|
||||||
|
import { randomBytes } from 'crypto';
|
||||||
|
|
||||||
|
// GET - List user's fitness goals
|
||||||
|
export async function GET(req: NextRequest) {
|
||||||
|
try {
|
||||||
|
const { userId } = await auth();
|
||||||
|
if (!userId) {
|
||||||
|
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||||
|
}
|
||||||
|
|
||||||
|
const { searchParams } = new URL(req.url);
|
||||||
|
const targetUserId = searchParams.get('userId') || userId;
|
||||||
|
const status = searchParams.get('status'); // active, completed, all
|
||||||
|
|
||||||
|
const db = await getDatabase();
|
||||||
|
|
||||||
|
// Fetch goals with optional status filter
|
||||||
|
const goals = await db.getFitnessGoalsByUserId(
|
||||||
|
targetUserId,
|
||||||
|
status && status !== 'all' ? status : undefined
|
||||||
|
);
|
||||||
|
|
||||||
|
return NextResponse.json(goals);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching fitness goals:', error);
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: 'Internal server error' },
|
||||||
|
{ status: 500 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// POST - Create new fitness goal
|
||||||
|
export async function POST(req: NextRequest) {
|
||||||
|
try {
|
||||||
|
const { userId } = await auth();
|
||||||
|
if (!userId) {
|
||||||
|
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||||
|
}
|
||||||
|
|
||||||
|
const body = await req.json();
|
||||||
|
const {
|
||||||
|
goalType,
|
||||||
|
title,
|
||||||
|
description,
|
||||||
|
targetValue,
|
||||||
|
currentValue,
|
||||||
|
unit,
|
||||||
|
targetDate,
|
||||||
|
priority,
|
||||||
|
notes,
|
||||||
|
fitnessProfileId
|
||||||
|
} = body;
|
||||||
|
|
||||||
|
// Validation
|
||||||
|
if (!goalType || !title) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: 'goalType and title are required' },
|
||||||
|
{ status: 400 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const db = await getDatabase();
|
||||||
|
|
||||||
|
// Create the goal
|
||||||
|
const goal = await db.createFitnessGoal({
|
||||||
|
id: randomBytes(16).toString('hex'),
|
||||||
|
userId,
|
||||||
|
fitnessProfileId: fitnessProfileId || undefined,
|
||||||
|
goalType,
|
||||||
|
title,
|
||||||
|
description: description || undefined,
|
||||||
|
targetValue: targetValue || undefined,
|
||||||
|
currentValue: currentValue || 0,
|
||||||
|
unit: unit || undefined,
|
||||||
|
startDate: new Date(),
|
||||||
|
targetDate: targetDate ? new Date(targetDate) : undefined,
|
||||||
|
status: 'active',
|
||||||
|
progress: 0,
|
||||||
|
priority: priority || 'medium',
|
||||||
|
notes: notes || undefined
|
||||||
|
});
|
||||||
|
|
||||||
|
return NextResponse.json(goal, { status: 201 });
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error creating fitness goal:', error);
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: 'Internal server error' },
|
||||||
|
{ status: 500 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,5 +1,7 @@
|
|||||||
import { NextResponse } from "next/server";
|
import { NextResponse } from "next/server";
|
||||||
import { getDatabase } from "@/lib/database";
|
import { getDatabase } from "@/lib/database";
|
||||||
|
import { buildAIContext } from "@/lib/ai/ai-context";
|
||||||
|
import { buildEnhancedPrompt, buildBasicPrompt } from "@/lib/ai/prompt-builder";
|
||||||
|
|
||||||
export async function POST(req: Request) {
|
export async function POST(req: Request) {
|
||||||
try {
|
try {
|
||||||
@ -21,26 +23,17 @@ export async function POST(req: Request) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Construct prompt
|
// Build AI context with goals and recommendations
|
||||||
const prompt = `
|
let prompt: string;
|
||||||
You are a professional fitness trainer and nutritionist.
|
try {
|
||||||
Generate a detailed daily recommendation for a user with the following profile:
|
const context = await buildAIContext(userId);
|
||||||
- Height: ${profile.height} cm
|
prompt = buildEnhancedPrompt(context);
|
||||||
- Weight: ${profile.weight} kg
|
console.log('Using enhanced AI context with goals and history');
|
||||||
- Age: ${profile.age}
|
} catch (error) {
|
||||||
- Gender: ${profile.gender}
|
// Fallback to basic prompt if context building fails
|
||||||
- Goal: ${profile.fitnessGoals.join(", ")}
|
console.warn('Failed to build AI context, using basic prompt:', error);
|
||||||
- Activity Level: ${profile.activityLevel}
|
prompt = buildBasicPrompt(profile);
|
||||||
- Medical Conditions: ${profile.medicalConditions || "None"}
|
}
|
||||||
- Injuries: ${profile.injuries || "None"}
|
|
||||||
|
|
||||||
Please provide the response in the following JSON format ONLY, no other text. Do not use markdown formatting or code blocks:
|
|
||||||
{
|
|
||||||
"recommendationText": "General advice and motivation for today.",
|
|
||||||
"activityPlan": "Detailed workout or activity plan for today.",
|
|
||||||
"dietPlan": "Detailed meal plan for today."
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
let parsedResponse;
|
let parsedResponse;
|
||||||
|
|
||||||
|
|||||||
88
apps/admin/src/lib/ai/ai-context.ts
Normal file
88
apps/admin/src/lib/ai/ai-context.ts
Normal file
@ -0,0 +1,88 @@
|
|||||||
|
import { getDatabase } from '@/lib/database';
|
||||||
|
import type { FitnessGoal } from '@/lib/database/types';
|
||||||
|
|
||||||
|
// Import types from database package
|
||||||
|
import type {
|
||||||
|
FitnessProfile as DBFitnessProfile,
|
||||||
|
Recommendation as DBRecommendation,
|
||||||
|
} from '@fitai/database';
|
||||||
|
|
||||||
|
export interface AIContext {
|
||||||
|
profile: DBFitnessProfile;
|
||||||
|
activeGoals: FitnessGoal[];
|
||||||
|
completedGoals: FitnessGoal[];
|
||||||
|
recentRecommendations: DBRecommendation[];
|
||||||
|
progressSummary: {
|
||||||
|
goalsCompleted: number;
|
||||||
|
goalsActive: number;
|
||||||
|
averageProgress: number;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build comprehensive AI context for a user
|
||||||
|
* Aggregates fitness profile, goals, and recommendation history
|
||||||
|
*/
|
||||||
|
export async function buildAIContext(userId: string): Promise<AIContext> {
|
||||||
|
const db = await getDatabase();
|
||||||
|
|
||||||
|
// Fetch fitness profile
|
||||||
|
const profile = await db.getFitnessProfileByUserId(userId);
|
||||||
|
|
||||||
|
if (!profile) {
|
||||||
|
throw new Error(`Fitness profile not found for user ${userId}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch all fitness goals
|
||||||
|
const allGoals = await db.getFitnessGoalsByUserId(userId);
|
||||||
|
|
||||||
|
// Separate active and completed goals
|
||||||
|
const activeGoals = allGoals.filter((g) => g.status === 'active');
|
||||||
|
const completedGoals = allGoals.filter((g) => g.status === 'completed');
|
||||||
|
|
||||||
|
// Get all recommendations
|
||||||
|
const allRecommendations = await db.getRecommendationsByUserId(userId);
|
||||||
|
|
||||||
|
// Filter recent recommendations (last 7 days)
|
||||||
|
const sevenDaysAgo = new Date();
|
||||||
|
sevenDaysAgo.setDate(sevenDaysAgo.getDate() - 7);
|
||||||
|
|
||||||
|
const recentRecommendations = allRecommendations
|
||||||
|
.filter((r) => new Date(r.createdAt) > sevenDaysAgo)
|
||||||
|
.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime())
|
||||||
|
.slice(0, 5);
|
||||||
|
|
||||||
|
// Calculate progress summary
|
||||||
|
const progressSummary = {
|
||||||
|
goalsCompleted: completedGoals.length,
|
||||||
|
goalsActive: activeGoals.length,
|
||||||
|
averageProgress:
|
||||||
|
activeGoals.length > 0
|
||||||
|
? activeGoals.reduce((sum, g) => sum + (g.progress || 0), 0) / activeGoals.length
|
||||||
|
: 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
profile,
|
||||||
|
activeGoals,
|
||||||
|
completedGoals,
|
||||||
|
recentRecommendations,
|
||||||
|
progressSummary,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Format goal for display in AI prompt
|
||||||
|
*/
|
||||||
|
export function formatGoalForPrompt(goal: FitnessGoal): string {
|
||||||
|
const progress = goal.progress || 0;
|
||||||
|
const current = goal.currentValue || 0;
|
||||||
|
const target = goal.targetValue;
|
||||||
|
const unit = goal.unit || '';
|
||||||
|
|
||||||
|
if (target) {
|
||||||
|
return `${goal.title}: ${current}/${target} ${unit} (${progress.toFixed(1)}% complete)`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return `${goal.title}: ${progress.toFixed(1)}% complete`;
|
||||||
|
}
|
||||||
91
apps/admin/src/lib/ai/prompt-builder.ts
Normal file
91
apps/admin/src/lib/ai/prompt-builder.ts
Normal file
@ -0,0 +1,91 @@
|
|||||||
|
import type { AIContext } from './ai-context';
|
||||||
|
import { formatGoalForPrompt } from './ai-context';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build enhanced AI prompt with comprehensive user context
|
||||||
|
* Includes profile, goals, progress, and recommendation history
|
||||||
|
*/
|
||||||
|
export function buildEnhancedPrompt(context: AIContext): string {
|
||||||
|
const { profile, activeGoals, completedGoals, recentRecommendations, progressSummary } = context;
|
||||||
|
|
||||||
|
// Build goals section
|
||||||
|
const activeGoalsText =
|
||||||
|
activeGoals.length > 0
|
||||||
|
? activeGoals.map((g) => `- ${formatGoalForPrompt(g)}`).join('\n')
|
||||||
|
: '- No active goals set';
|
||||||
|
|
||||||
|
const completedGoalsText =
|
||||||
|
completedGoals.length > 0
|
||||||
|
? completedGoals.slice(0, 3).map((g) => g.title).join(', ')
|
||||||
|
: 'None yet';
|
||||||
|
|
||||||
|
// Build context about recent recommendations
|
||||||
|
const recommendationContextText =
|
||||||
|
recentRecommendations.length > 0
|
||||||
|
? `The user has received ${recentRecommendations.length} recommendations in the past week. Consider their progress and avoid repetitive advice.`
|
||||||
|
: 'This is a new user or they haven\'t received recent recommendations. Provide comprehensive guidance.';
|
||||||
|
|
||||||
|
return `You are a professional fitness trainer and nutritionist with access to the user's complete fitness journey.
|
||||||
|
|
||||||
|
## User Profile
|
||||||
|
- Height: ${profile.height || 'Not specified'} cm
|
||||||
|
- Weight: ${profile.weight || 'Not specified'} kg
|
||||||
|
- Age: ${profile.age || 'Not specified'}
|
||||||
|
- Gender: ${profile.gender || 'Not specified'}
|
||||||
|
- Primary Goal: ${profile.fitnessGoal || 'General fitness'}
|
||||||
|
- Activity Level: ${profile.activityLevel || 'Not specified'}
|
||||||
|
- Medical Conditions: ${profile.medicalConditions || 'None'}
|
||||||
|
- Allergies: ${profile.allergies || 'None'}
|
||||||
|
- Injuries: ${profile.injuries || 'None'}
|
||||||
|
|
||||||
|
## Active Goals (${activeGoals.length})
|
||||||
|
${activeGoalsText}
|
||||||
|
|
||||||
|
## Recent Progress
|
||||||
|
- Goals Completed: ${progressSummary.goalsCompleted}
|
||||||
|
- Active Goals: ${progressSummary.goalsActive}
|
||||||
|
- Average Progress on Active Goals: ${progressSummary.averageProgress.toFixed(1)}%
|
||||||
|
${completedGoals.length > 0 ? `- Recently Completed: ${completedGoalsText}` : ''}
|
||||||
|
|
||||||
|
## Recommendation History
|
||||||
|
${recommendationContextText}
|
||||||
|
|
||||||
|
## Task
|
||||||
|
Generate a personalized daily recommendation that:
|
||||||
|
1. Acknowledges their progress on active goals
|
||||||
|
2. Provides specific, actionable steps toward goal completion
|
||||||
|
3. Builds on previous recommendations without being repetitive
|
||||||
|
4. Adapts to their current progress level
|
||||||
|
5. Takes into account their medical conditions, allergies, and injuries
|
||||||
|
|
||||||
|
Respond in the following JSON format ONLY, no other text. Do not use markdown formatting or code blocks:
|
||||||
|
{
|
||||||
|
"recommendationText": "Personalized motivation and progress acknowledgment (2-3 sentences)",
|
||||||
|
"activityPlan": "Specific workout plan aligned with active goals (detailed, 3-5 exercises or activities)",
|
||||||
|
"dietPlan": "Nutrition plan supporting their goals and restrictions (detailed, include meals and portions)"
|
||||||
|
}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build basic prompt for users without complete context
|
||||||
|
* Fallback when goals or other data is missing
|
||||||
|
*/
|
||||||
|
export function buildBasicPrompt(profile: any): string {
|
||||||
|
return `You are a professional fitness trainer and nutritionist.
|
||||||
|
Generate a detailed daily recommendation for a user with the following profile:
|
||||||
|
- Height: ${profile.height} cm
|
||||||
|
- Weight: ${profile.weight} kg
|
||||||
|
- Age: ${profile.age}
|
||||||
|
- Gender: ${profile.gender}
|
||||||
|
- Goal: ${profile.fitnessGoal}
|
||||||
|
- Activity Level: ${profile.activityLevel}
|
||||||
|
- Medical Conditions: ${profile.medicalConditions || "None"}
|
||||||
|
- Injuries: ${profile.injuries || "None"}
|
||||||
|
|
||||||
|
Please provide the response in the following JSON format ONLY, no other text. Do not use markdown formatting or code blocks:
|
||||||
|
{
|
||||||
|
"recommendationText": "General advice and motivation for today.",
|
||||||
|
"activityPlan": "Detailed workout or activity plan for today.",
|
||||||
|
"dietPlan": "Detailed meal plan for today."
|
||||||
|
}`;
|
||||||
|
}
|
||||||
@ -614,6 +614,174 @@ export class SQLiteDatabase implements IDatabase {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Fitness Goals operations
|
||||||
|
async createFitnessGoal(goalData: Omit<import('./types').FitnessGoal, 'createdAt' | 'updatedAt'>): Promise<import('./types').FitnessGoal> {
|
||||||
|
if (!this.db) throw new Error('Database not connected')
|
||||||
|
|
||||||
|
const now = new Date()
|
||||||
|
const goal: import('./types').FitnessGoal = {
|
||||||
|
...goalData,
|
||||||
|
createdAt: now,
|
||||||
|
updatedAt: now
|
||||||
|
}
|
||||||
|
|
||||||
|
const stmt = this.db.prepare(
|
||||||
|
`INSERT INTO fitness_goals (
|
||||||
|
id, user_id, fitness_profile_id, goal_type, title, description,
|
||||||
|
target_value, current_value, unit, start_date, target_date,
|
||||||
|
completed_date, status, progress, priority, notes, created_at, updated_at
|
||||||
|
)
|
||||||
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
|
||||||
|
)
|
||||||
|
|
||||||
|
stmt.run(
|
||||||
|
goal.id,
|
||||||
|
goal.userId,
|
||||||
|
goal.fitnessProfileId || null,
|
||||||
|
goal.goalType,
|
||||||
|
goal.title,
|
||||||
|
goal.description || null,
|
||||||
|
goal.targetValue || null,
|
||||||
|
goal.currentValue || null,
|
||||||
|
goal.unit || null,
|
||||||
|
goal.startDate.toISOString(),
|
||||||
|
goal.targetDate ? goal.targetDate.toISOString() : null,
|
||||||
|
goal.completedDate ? goal.completedDate.toISOString() : null,
|
||||||
|
goal.status,
|
||||||
|
goal.progress,
|
||||||
|
goal.priority,
|
||||||
|
goal.notes || null,
|
||||||
|
goal.createdAt.toISOString(),
|
||||||
|
goal.updatedAt.toISOString()
|
||||||
|
)
|
||||||
|
|
||||||
|
return goal
|
||||||
|
}
|
||||||
|
|
||||||
|
async getFitnessGoalById(id: string): Promise<import('./types').FitnessGoal | null> {
|
||||||
|
if (!this.db) throw new Error('Database not connected')
|
||||||
|
|
||||||
|
const stmt = this.db.prepare('SELECT * FROM fitness_goals WHERE id = ?')
|
||||||
|
const row = stmt.get(id)
|
||||||
|
|
||||||
|
return row ? this.mapRowToFitnessGoal(row) : null
|
||||||
|
}
|
||||||
|
|
||||||
|
async getFitnessGoalsByUserId(userId: string, status?: string): Promise<import('./types').FitnessGoal[]> {
|
||||||
|
if (!this.db) throw new Error('Database not connected')
|
||||||
|
|
||||||
|
let query = 'SELECT * FROM fitness_goals WHERE user_id = ?'
|
||||||
|
const params: any[] = [userId]
|
||||||
|
|
||||||
|
if (status) {
|
||||||
|
query += ' AND status = ?'
|
||||||
|
params.push(status)
|
||||||
|
}
|
||||||
|
|
||||||
|
query += ' ORDER BY created_at DESC'
|
||||||
|
|
||||||
|
const stmt = this.db.prepare(query)
|
||||||
|
const rows = stmt.all(...params)
|
||||||
|
|
||||||
|
return rows.map(row => this.mapRowToFitnessGoal(row))
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateFitnessGoal(id: string, updates: Partial<import('./types').FitnessGoal>): Promise<import('./types').FitnessGoal | null> {
|
||||||
|
if (!this.db) throw new Error('Database not connected')
|
||||||
|
|
||||||
|
const fields = Object.keys(updates).filter(key => key !== 'id' && key !== 'createdAt')
|
||||||
|
if (fields.length === 0) return this.getFitnessGoalById(id)
|
||||||
|
|
||||||
|
// Map camelCase to snake_case for database columns
|
||||||
|
const columnMap: Record<string, string> = {
|
||||||
|
userId: 'user_id',
|
||||||
|
fitnessProfileId: 'fitness_profile_id',
|
||||||
|
goalType: 'goal_type',
|
||||||
|
targetValue: 'target_value',
|
||||||
|
currentValue: 'current_value',
|
||||||
|
startDate: 'start_date',
|
||||||
|
targetDate: 'target_date',
|
||||||
|
completedDate: 'completed_date',
|
||||||
|
updatedAt: 'updated_at'
|
||||||
|
}
|
||||||
|
|
||||||
|
const setClause = fields.map(field => `${columnMap[field] || field} = ?`).join(', ')
|
||||||
|
const values = fields.map(field => {
|
||||||
|
const val = (updates as any)[field]
|
||||||
|
return val instanceof Date ? val.toISOString() : val
|
||||||
|
})
|
||||||
|
values.push(new Date().toISOString()) // updatedAt
|
||||||
|
values.push(id)
|
||||||
|
|
||||||
|
const stmt = this.db.prepare(`UPDATE fitness_goals SET ${setClause}, updated_at = ? WHERE id = ?`)
|
||||||
|
stmt.run(values)
|
||||||
|
|
||||||
|
return this.getFitnessGoalById(id)
|
||||||
|
}
|
||||||
|
|
||||||
|
async deleteFitnessGoal(id: string): Promise<boolean> {
|
||||||
|
if (!this.db) throw new Error('Database not connected')
|
||||||
|
|
||||||
|
const stmt = this.db.prepare('DELETE FROM fitness_goals WHERE id = ?')
|
||||||
|
const result = stmt.run(id)
|
||||||
|
return (result.changes || 0) > 0
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateGoalProgress(id: string, currentValue: number): Promise<import('./types').FitnessGoal | null> {
|
||||||
|
if (!this.db) throw new Error('Database not connected')
|
||||||
|
|
||||||
|
// Get the goal to calculate progress
|
||||||
|
const goal = await this.getFitnessGoalById(id)
|
||||||
|
if (!goal) return null
|
||||||
|
|
||||||
|
let progress = goal.progress
|
||||||
|
if (goal.targetValue && goal.targetValue > 0) {
|
||||||
|
progress = Math.min(100, (currentValue / goal.targetValue) * 100)
|
||||||
|
}
|
||||||
|
|
||||||
|
const stmt = this.db.prepare(
|
||||||
|
'UPDATE fitness_goals SET current_value = ?, progress = ?, updated_at = ? WHERE id = ?'
|
||||||
|
)
|
||||||
|
stmt.run(currentValue, progress, new Date().toISOString(), id)
|
||||||
|
|
||||||
|
return this.getFitnessGoalById(id)
|
||||||
|
}
|
||||||
|
|
||||||
|
async completeGoal(id: string): Promise<import('./types').FitnessGoal | null> {
|
||||||
|
if (!this.db) throw new Error('Database not connected')
|
||||||
|
|
||||||
|
const now = new Date()
|
||||||
|
const stmt = this.db.prepare(
|
||||||
|
'UPDATE fitness_goals SET status = ?, progress = ?, completed_date = ?, updated_at = ? WHERE id = ?'
|
||||||
|
)
|
||||||
|
stmt.run('completed', 100, now.toISOString(), now.toISOString(), id)
|
||||||
|
|
||||||
|
return this.getFitnessGoalById(id)
|
||||||
|
}
|
||||||
|
|
||||||
|
private mapRowToFitnessGoal(row: any): import('./types').FitnessGoal {
|
||||||
|
return {
|
||||||
|
id: row.id,
|
||||||
|
userId: row.user_id,
|
||||||
|
fitnessProfileId: row.fitness_profile_id,
|
||||||
|
goalType: row.goal_type,
|
||||||
|
title: row.title,
|
||||||
|
description: row.description,
|
||||||
|
targetValue: row.target_value,
|
||||||
|
currentValue: row.current_value,
|
||||||
|
unit: row.unit,
|
||||||
|
startDate: new Date(row.start_date),
|
||||||
|
targetDate: row.target_date ? new Date(row.target_date) : undefined,
|
||||||
|
completedDate: row.completed_date ? new Date(row.completed_date) : undefined,
|
||||||
|
status: row.status,
|
||||||
|
progress: row.progress,
|
||||||
|
priority: row.priority,
|
||||||
|
notes: row.notes,
|
||||||
|
createdAt: new Date(row.created_at),
|
||||||
|
updatedAt: new Date(row.updated_at)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async getDashboardStats(): Promise<{
|
async getDashboardStats(): Promise<{
|
||||||
totalUsers: number;
|
totalUsers: number;
|
||||||
activeClients: number;
|
activeClients: number;
|
||||||
|
|||||||
@ -61,6 +61,27 @@ export interface Recommendation {
|
|||||||
approvedBy?: string;
|
approvedBy?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface FitnessGoal {
|
||||||
|
id: string;
|
||||||
|
userId: string;
|
||||||
|
fitnessProfileId?: string;
|
||||||
|
goalType: "weight_target" | "strength_milestone" | "endurance_target" | "flexibility_goal" | "habit_building" | "custom";
|
||||||
|
title: string;
|
||||||
|
description?: string;
|
||||||
|
targetValue?: number;
|
||||||
|
currentValue?: number;
|
||||||
|
unit?: string;
|
||||||
|
startDate: Date;
|
||||||
|
targetDate?: Date;
|
||||||
|
completedDate?: Date;
|
||||||
|
status: "active" | "completed" | "abandoned" | "paused";
|
||||||
|
progress: number;
|
||||||
|
priority: "low" | "medium" | "high";
|
||||||
|
notes?: string;
|
||||||
|
createdAt: Date;
|
||||||
|
updatedAt: Date;
|
||||||
|
}
|
||||||
|
|
||||||
// Database Interface - allows us to swap implementations
|
// Database Interface - allows us to swap implementations
|
||||||
export interface IDatabase {
|
export interface IDatabase {
|
||||||
// Connection management
|
// Connection management
|
||||||
@ -121,6 +142,20 @@ export interface IDatabase {
|
|||||||
): Promise<Recommendation | null>;
|
): Promise<Recommendation | null>;
|
||||||
deleteRecommendation(id: string): Promise<boolean>;
|
deleteRecommendation(id: string): Promise<boolean>;
|
||||||
|
|
||||||
|
// Fitness Goals operations
|
||||||
|
createFitnessGoal(
|
||||||
|
goal: Omit<FitnessGoal, "createdAt" | "updatedAt">
|
||||||
|
): Promise<FitnessGoal>;
|
||||||
|
getFitnessGoalById(id: string): Promise<FitnessGoal | null>;
|
||||||
|
getFitnessGoalsByUserId(userId: string, status?: string): Promise<FitnessGoal[]>;
|
||||||
|
updateFitnessGoal(
|
||||||
|
id: string,
|
||||||
|
updates: Partial<FitnessGoal>
|
||||||
|
): Promise<FitnessGoal | null>;
|
||||||
|
deleteFitnessGoal(id: string): Promise<boolean>;
|
||||||
|
updateGoalProgress(id: string, currentValue: number): Promise<FitnessGoal | null>;
|
||||||
|
completeGoal(id: string): Promise<FitnessGoal | null>;
|
||||||
|
|
||||||
// Dashboard operations
|
// Dashboard operations
|
||||||
getDashboardStats(): Promise<{
|
getDashboardStats(): Promise<{
|
||||||
totalUsers: number;
|
totalUsers: number;
|
||||||
|
|||||||
@ -145,6 +145,67 @@ export const fitnessProfiles = sqliteTable("fitness_profiles", {
|
|||||||
.$defaultFn(() => new Date()),
|
.$defaultFn(() => new Date()),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const fitnessGoals = sqliteTable("fitness_goals", {
|
||||||
|
id: text("id").primaryKey(),
|
||||||
|
userId: text("user_id")
|
||||||
|
.notNull()
|
||||||
|
.references(() => users.id, { onDelete: "cascade" }),
|
||||||
|
fitnessProfileId: text("fitness_profile_id").references(
|
||||||
|
() => fitnessProfiles.id,
|
||||||
|
{ onDelete: "cascade" }
|
||||||
|
),
|
||||||
|
|
||||||
|
// Goal details
|
||||||
|
goalType: text("goal_type", {
|
||||||
|
enum: [
|
||||||
|
"weight_target",
|
||||||
|
"strength_milestone",
|
||||||
|
"endurance_target",
|
||||||
|
"flexibility_goal",
|
||||||
|
"habit_building",
|
||||||
|
"custom",
|
||||||
|
],
|
||||||
|
}).notNull(),
|
||||||
|
|
||||||
|
title: text("title").notNull(),
|
||||||
|
description: text("description"),
|
||||||
|
|
||||||
|
// Measurable targets
|
||||||
|
targetValue: real("target_value"), // e.g., 70 (kg), 100 (kg bench press)
|
||||||
|
currentValue: real("current_value"), // Current progress
|
||||||
|
unit: text("unit"), // kg, km, reps, etc.
|
||||||
|
|
||||||
|
// Timeline
|
||||||
|
startDate: integer("start_date", { mode: "timestamp" })
|
||||||
|
.notNull()
|
||||||
|
.$defaultFn(() => new Date()),
|
||||||
|
targetDate: integer("target_date", { mode: "timestamp" }),
|
||||||
|
completedDate: integer("completed_date", { mode: "timestamp" }),
|
||||||
|
|
||||||
|
// Status tracking
|
||||||
|
status: text("status", {
|
||||||
|
enum: ["active", "completed", "abandoned", "paused"],
|
||||||
|
})
|
||||||
|
.notNull()
|
||||||
|
.default("active"),
|
||||||
|
|
||||||
|
progress: real("progress").default(0), // 0-100 percentage
|
||||||
|
|
||||||
|
// Metadata
|
||||||
|
priority: text("priority", {
|
||||||
|
enum: ["low", "medium", "high"],
|
||||||
|
}).default("medium"),
|
||||||
|
|
||||||
|
notes: text("notes"),
|
||||||
|
|
||||||
|
createdAt: integer("created_at", { mode: "timestamp" })
|
||||||
|
.notNull()
|
||||||
|
.$defaultFn(() => new Date()),
|
||||||
|
updatedAt: integer("updated_at", { mode: "timestamp" })
|
||||||
|
.notNull()
|
||||||
|
.$defaultFn(() => new Date()),
|
||||||
|
});
|
||||||
|
|
||||||
export const recommendations = sqliteTable("recommendations", {
|
export const recommendations = sqliteTable("recommendations", {
|
||||||
id: text("id").primaryKey(),
|
id: text("id").primaryKey(),
|
||||||
userId: text("user_id")
|
userId: text("user_id")
|
||||||
@ -186,5 +247,8 @@ export type Notification = typeof notifications.$inferSelect;
|
|||||||
export type NewNotification = typeof notifications.$inferInsert;
|
export type NewNotification = typeof notifications.$inferInsert;
|
||||||
export type FitnessProfile = typeof fitnessProfiles.$inferSelect;
|
export type FitnessProfile = typeof fitnessProfiles.$inferSelect;
|
||||||
export type NewFitnessProfile = typeof fitnessProfiles.$inferInsert;
|
export type NewFitnessProfile = typeof fitnessProfiles.$inferInsert;
|
||||||
|
export type FitnessGoal = typeof fitnessGoals.$inferSelect;
|
||||||
|
export type NewFitnessGoal = typeof fitnessGoals.$inferInsert;
|
||||||
export type Recommendation = typeof recommendations.$inferSelect;
|
export type Recommendation = typeof recommendations.$inferSelect;
|
||||||
export type NewRecommendation = typeof recommendations.$inferInsert;
|
export type NewRecommendation = typeof recommendations.$inferInsert;
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user