regenerate linked active goals when admin approves ai recommendation
This commit is contained in:
parent
d6683e6e5e
commit
9330f4fd05
@ -4,6 +4,101 @@ import { getDatabase } from "@/lib/database";
|
||||
import log from "@/lib/logger";
|
||||
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) {
|
||||
try {
|
||||
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") {
|
||||
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 {
|
||||
await db.createNotification({
|
||||
id: crypto.randomUUID(),
|
||||
@ -122,6 +312,8 @@ export async function POST(req: Request) {
|
||||
data: updatedRecommendation,
|
||||
meta: {
|
||||
timestamp: new Date().toISOString(),
|
||||
pausedGoals: pausedGoalsCount,
|
||||
createdGoals: createdGoalsCount,
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user