Compare commits

..

5 Commits

Author SHA1 Message Date
42122ac341 Merge branch 'adminRecc' 2026-03-31 20:04:37 +02:00
4dd2ed5839 dbg 2026-03-31 20:04:05 +02:00
f9a588fcd6 db 2026-03-31 20:03:52 +02:00
9330f4fd05 regenerate linked active goals when admin approves ai recommendation 2026-03-31 20:02:28 +02:00
d6683e6e5e dbs 2026-03-31 19:51:44 +02:00
2 changed files with 193 additions and 1 deletions

Binary file not shown.

View File

@ -4,6 +4,101 @@ import { getDatabase } from "@/lib/database";
import log from "@/lib/logger"; import log from "@/lib/logger";
import { ensureUserSynced } from "@/lib/sync-user"; import { ensureUserSynced } from "@/lib/sync-user";
const AI_LINK_PREFIX = "[AI_LINKED]";
type GoalType =
| "weight_target"
| "strength_milestone"
| "endurance_target"
| "flexibility_goal"
| "habit_building"
| "custom";
interface ParsedPlanItem {
id: string;
title: string;
description: string;
goalType: GoalType;
}
function inferGoalType(text: string): GoalType {
const normalized = text.toLowerCase();
if (
normalized.includes("strength") ||
normalized.includes("bench") ||
normalized.includes("squat") ||
normalized.includes("deadlift") ||
normalized.includes("weights")
) {
return "strength_milestone";
}
if (
normalized.includes("run") ||
normalized.includes("cardio") ||
normalized.includes("endurance") ||
normalized.includes("cycle")
) {
return "endurance_target";
}
if (
normalized.includes("stretch") ||
normalized.includes("mobility") ||
normalized.includes("yoga") ||
normalized.includes("flexibility")
) {
return "flexibility_goal";
}
if (
normalized.includes("daily") ||
normalized.includes("routine") ||
normalized.includes("habit")
) {
return "habit_building";
}
return "custom";
}
function parseActivityPlanToItems(activityPlan: string): ParsedPlanItem[] {
const lines = activityPlan
.replace(/\r\n/g, "\n")
.split(/\n+/)
.flatMap((line) => line.split(/(?<=[.!?])\s+(?=[A-Z0-9])/g))
.map((line) => line.trim())
.filter(Boolean)
.map((line) => line.replace(/^[-*•\d.)\s]+/, "").trim())
.filter((line) => line.length > 10)
.slice(0, 8);
const uniqueLines = Array.from(new Set(lines));
return uniqueLines.map((line) => ({
id: crypto.randomUUID(),
title: line.length > 72 ? `${line.slice(0, 69)}...` : line,
description: line,
goalType: inferGoalType(line),
}));
}
function getDefaultPlanItems(): ParsedPlanItem[] {
const defaults = [
"Complete 3 strength sessions this week with progressive overload.",
"Add 2 cardio sessions of 25-30 minutes for endurance.",
"Do a 10-minute mobility routine daily after training.",
];
return defaults.map((line) => ({
id: crypto.randomUUID(),
title: line.length > 72 ? `${line.slice(0, 69)}...` : line,
description: line,
goalType: inferGoalType(line),
}));
}
export async function POST(req: Request) { export async function POST(req: Request) {
try { try {
const { userId: clerkUserId } = await auth(); const { userId: clerkUserId } = await auth();
@ -94,8 +189,103 @@ export async function POST(req: Request) {
); );
} }
// If approved, create a notification for the user let pausedGoalsCount = 0;
let createdGoalsCount = 0;
// If approved, regenerate linked AI goals and create a notification for the user
if (status === "approved") { if (status === "approved") {
try {
const existingActiveGoals = await db.getFitnessGoalsByUserId(
updatedRecommendation.userId,
"active",
);
const linkedGoals = existingActiveGoals.filter((goal) =>
goal.notes?.startsWith(AI_LINK_PREFIX),
);
pausedGoalsCount = linkedGoals.length;
await Promise.all(
linkedGoals.map((goal) =>
db.updateFitnessGoal(goal.id, {
status: "paused",
notes: `${goal.notes || ""}\nPaused due to recommendation approval on ${new Date().toISOString()}`,
}),
),
);
let planItems = parseActivityPlanToItems(
updatedRecommendation.activityPlan || "",
);
if (
planItems.length === 0 &&
updatedRecommendation.recommendationText
) {
planItems = parseActivityPlanToItems(
updatedRecommendation.recommendationText,
);
}
if (planItems.length === 0) {
planItems = getDefaultPlanItems();
}
const fitnessProfileId =
updatedRecommendation.fitnessProfileId ||
(await db.getFitnessProfileByUserId(updatedRecommendation.userId))
?.id;
if (!fitnessProfileId) {
log.warn("No fitness profile available for AI goal creation", {
recommendationId,
userId: updatedRecommendation.userId,
});
} else {
const createdGoals = await Promise.all(
planItems.map((item) =>
db.createFitnessGoal({
id: crypto.randomUUID(),
userId: updatedRecommendation.userId,
fitnessProfileId,
goalType: item.goalType,
title: item.title,
description: item.description,
targetValue: undefined,
currentValue: 0,
unit: undefined,
startDate: new Date(),
targetDate: undefined,
completedDate: undefined,
status: "active",
progress: 0,
priority: "medium",
notes: `${AI_LINK_PREFIX} recommendationId=${updatedRecommendation.id}; itemId=${item.id}`,
}),
),
);
createdGoalsCount = createdGoals.length;
}
log.info("Regenerated linked AI goals from approved recommendation", {
recommendationId: updatedRecommendation.id,
userId: updatedRecommendation.userId,
pausedGoals: pausedGoalsCount,
createdGoals: createdGoalsCount,
});
} catch (goalConversionError) {
log.error(
"Failed to regenerate linked goals for approved recommendation",
goalConversionError,
{
recommendationId,
userId: updatedRecommendation.userId,
},
);
}
try { try {
await db.createNotification({ await db.createNotification({
id: crypto.randomUUID(), id: crypto.randomUUID(),
@ -122,6 +312,8 @@ export async function POST(req: Request) {
data: updatedRecommendation, data: updatedRecommendation,
meta: { meta: {
timestamp: new Date().toISOString(), timestamp: new Date().toISOString(),
pausedGoals: pausedGoalsCount,
createdGoals: createdGoalsCount,
}, },
}); });
} catch (error) { } catch (error) {