2b0abad68e
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.
60 lines
1.8 KiB
TypeScript
60 lines
1.8 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 HistoryList from '@/components/ai/HistoryList';
|
|
|
|
export const dynamic = 'force-dynamic';
|
|
|
|
export default async function HistoryPage() {
|
|
const user = await getCurrentUser();
|
|
if (!user) redirect('/auth/login');
|
|
|
|
const rows = await prisma.aIGeneration.findMany({
|
|
// Program history only. Single-workout generations (kind="workout")
|
|
// are ephemeral — the durable record is the saved Workout — so they
|
|
// don't belong in this program-shaped list/detail.
|
|
where: { userId: user.id, kind: 'program' },
|
|
orderBy: { createdAt: 'desc' },
|
|
take: 25,
|
|
select: {
|
|
id: true,
|
|
templateName: true,
|
|
userInput: true,
|
|
provider: true,
|
|
model: true,
|
|
tokensIn: true,
|
|
tokensOut: true,
|
|
durationMs: true,
|
|
status: true,
|
|
errorMessage: true,
|
|
appliedProgramId: true,
|
|
createdAt: true,
|
|
},
|
|
});
|
|
|
|
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">
|
|
<ChevronLeft className="w-5 h-5" />
|
|
</Link>
|
|
<h1 className="text-2xl sm:text-3xl font-bold text-white">
|
|
AI · Generation history
|
|
</h1>
|
|
</div>
|
|
</div>
|
|
<div className="max-w-3xl mx-auto px-4 py-6">
|
|
<HistoryList
|
|
initialRows={rows.map((r) => ({
|
|
...r,
|
|
createdAt: r.createdAt.toISOString(),
|
|
}))}
|
|
/>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|