Files
proof-of-work/proof-of-work/app/main/ai/generate-workout/page.tsx
T
Keysat 2b0abad68e
CI / proof-of-work (Next.js app) (push) Waiting to run
CI / start9/0.4 (StartOS package code) (push) Waiting to run
v1.2.0:6 — AI "generate today's workout" from a brain-dump
Add a single-session AI flow alongside program generation: describe a
workout in plain words and get a ready-to-log workout back — exercises
with suggested weights, target reps, and set counts grounded in the
user's recent history. The suggestion can be inline-edited or refined
by sending a follow-up instruction back to the model, then "Use this
workout" pre-fills the normal New Workout form (nothing persists until
the user saves through the regular path).

Why reuse, not fork: the existing program-generation spine (detached
background runner, SSE streaming, lenient-JSON preview, 5 providers,
history context, library name->id mapping) already does the hard parts.
A new AIGeneration.kind discriminant ("program" | "workout", default
"program" via boot-time guarded ALTER) selects the parser and keeps the
ephemeral workout rows out of the program-shaped AI history. Refine is a
fresh generation seeded with the prior suggestion (validated through the
same schema before it re-enters the prompt).

Hand-off is sessionStorage -> /main/workouts/new?from=ai -> AiWorkoutPrefill,
which expands each suggestion into N sets and maps effort by cardio-ness
(Gear for cardio, RPE for strength). EditWorkoutData.id is now optional so
the prefill CREATEs rather than PATCHing a nonexistent id. The AI suggests
each weight in that exercise's effective logging unit (the library JSON
carries a per-exercise unit) so the stored number and unit never diverge.

Built + sideloaded to immense-voyage.local as 1.2.0:6; on-box ALTER and
non-root launch confirmed via start-cli. tsc clean (app + packaging),
251 tests pass, next build + s9pk build succeed.
2026-06-19 10:59:12 -05:00

71 lines
2.3 KiB
TypeScript

import { redirect } from 'next/navigation';
import Link from 'next/link';
import { ChevronLeft } from 'lucide-react';
import { getCurrentUser } from '@/lib/auth';
import { prisma } from '@/lib/prisma';
import GenerateWorkoutClient from '@/components/ai/GenerateWorkoutClient';
export const dynamic = 'force-dynamic';
export default async function GenerateWorkoutPage() {
const user = await getCurrentUser();
if (!user) redirect('/auth/login');
const [exercises, prefs, workoutCount] = await Promise.all([
prisma.exercise.findMany({
where: { userId: user.id },
select: { id: true, name: true, type: true },
orderBy: [{ type: 'asc' }, { name: 'asc' }],
}),
prisma.userPreferences.findUnique({
where: { userId: user.id },
select: { aiProvider: true, aiModel: true },
}),
prisma.workout.count({
where: { userId: user.id, deletedAt: null },
}),
]);
const aiConfigured = !!prefs?.aiProvider && !!prefs?.aiModel;
return (
<div className="min-h-screen bg-[#0A0A0A]">
<div className="border-b border-zinc-800">
<div className="max-w-3xl mx-auto px-4 py-4 sm:py-6 flex items-center gap-3">
<Link
href="/main/ai"
className="text-zinc-400 hover:text-white"
aria-label="Back to AI"
>
<ChevronLeft className="w-5 h-5" />
</Link>
<h1 className="text-2xl sm:text-3xl font-bold text-white">
AI · Today&apos;s workout
</h1>
</div>
</div>
<div className="max-w-3xl mx-auto px-4 py-6">
{!aiConfigured ? (
<div className="bg-amber-950/30 border border-amber-900 rounded p-5 text-sm text-amber-200">
<p className="font-bold text-amber-100 mb-2">AI is not configured.</p>
<p>
Pick a provider, model, and (if needed) API key in{' '}
<Link href="/main/settings" className="underline hover:text-amber-100">
Settings AI integration
</Link>{' '}
before you can generate a workout.
</p>
</div>
) : (
<GenerateWorkoutClient
exercises={exercises}
providerLabel={prefs!.aiProvider!}
modelLabel={prefs!.aiModel!}
workoutCount={workoutCount}
/>
)}
</div>
</div>
);
}