Files
proof-of-work/proof-of-work/app/main/ai/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

100 lines
3.3 KiB
TypeScript

import { redirect } from 'next/navigation';
import Link from 'next/link';
import { Sparkles, ListChecks, History, Dumbbell } from 'lucide-react';
import { getCurrentUser } from '@/lib/auth';
import { prisma } from '@/lib/prisma';
export const dynamic = 'force-dynamic';
export default async function AIIndexPage() {
const user = await getCurrentUser();
if (!user) redirect('/auth/login');
const prefs = await prisma.userPreferences.findUnique({
where: { userId: user.id },
select: { aiProvider: true, aiModel: true },
});
const aiConfigured = !!prefs?.aiProvider && !!prefs?.aiModel;
const cards = [
{
href: '/main/ai/generate-workout',
icon: Dumbbell,
title: "Today's workout",
blurb:
'Describe today\'s session in plain words and get a ready-to-log workout — exercises with suggested weights and reps from your history. Refine it, then pre-fill the log.',
disabled: !aiConfigured,
},
{
href: '/main/ai/generate',
icon: Sparkles,
title: 'Generate program',
blurb:
'Pick a template, describe what you want, and get a full multi-week program back. Review before applying.',
disabled: !aiConfigured,
},
{
href: '/main/ai/templates',
icon: ListChecks,
title: 'Prompt templates',
blurb:
'Built-in templates ship with the app. Create + save your own for repeated use.',
},
{
href: '/main/ai/history',
icon: History,
title: 'Generation history',
blurb: 'Every prompt you sent, every response, every applied program.',
},
];
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 justify-between">
<h1 className="text-2xl sm:text-3xl font-bold text-white">AI</h1>
<p className="text-xs text-zinc-500">
{aiConfigured ? (
<>
{prefs!.aiProvider} · {prefs!.aiModel}
</>
) : (
<Link href="/main/settings" className="underline">
Configure in Settings
</Link>
)}
</p>
</div>
</div>
<div className="max-w-3xl mx-auto px-4 py-6 grid gap-3">
{cards.map((c) => {
const Icon = c.icon;
const disabled = !!c.disabled;
return (
<Link
key={c.href}
href={disabled ? '/main/settings' : c.href}
className={`block bg-zinc-900 border border-zinc-800 rounded p-5 hover:border-zinc-700 transition ${disabled ? 'opacity-60' : ''}`}
>
<div className="flex items-start gap-3">
<Icon className="w-5 h-5 text-zinc-400 mt-0.5" />
<div>
<h2 className="text-base font-semibold text-white">
{c.title}
{disabled && (
<span className="ml-2 text-[10px] uppercase tracking-wider text-amber-400">
configure first
</span>
)}
</h2>
<p className="text-sm text-zinc-500 mt-1">{c.blurb}</p>
</div>
</div>
</Link>
);
})}
</div>
</div>
);
}