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.
71 lines
2.3 KiB
TypeScript
71 lines
2.3 KiB
TypeScript
'use client';
|
|
|
|
import { useEffect, useState } from 'react';
|
|
import { Exercise } from '@prisma/client';
|
|
import WorkoutForm, { EditWorkoutData } from './WorkoutForm';
|
|
import { AI_WORKOUT_DRAFT_KEY } from '@/components/ai/GenerateWorkoutClient';
|
|
import { buildPrefillExercises, type AiWorkoutDraft } from '@/lib/ai/workoutDraft';
|
|
|
|
/**
|
|
* Reads the ephemeral AI workout draft from sessionStorage (stashed by
|
|
* GenerateWorkoutClient before navigating to /main/workouts/new?from=ai),
|
|
* expands each suggested exercise into N pre-filled SetLogs, and renders
|
|
* the normal WorkoutForm. Nothing is persisted until the user saves
|
|
* through the regular workout path.
|
|
*
|
|
* The draft has no workout id, so WorkoutForm's first save CREATEs.
|
|
* Effort follows the app convention: cardio → gear (1-5), else → rpe.
|
|
*
|
|
* If the draft is missing (e.g. a refresh cleared it), we fall back to a
|
|
* blank form so the page is never broken.
|
|
*/
|
|
export default function AiWorkoutPrefill({
|
|
exercises,
|
|
}: {
|
|
exercises: Exercise[];
|
|
}) {
|
|
// Read + build once on mount via a lazy initializer. This stays PURE
|
|
// (no sessionStorage mutation) so React's StrictMode double-invoke is
|
|
// safe — both passes read the same draft. The one-shot removal happens
|
|
// in the effect below, after the value is captured.
|
|
const [editWorkout] = useState<EditWorkoutData | undefined>(() => {
|
|
if (typeof window === 'undefined') return undefined;
|
|
const raw = sessionStorage.getItem(AI_WORKOUT_DRAFT_KEY);
|
|
if (!raw) return undefined;
|
|
|
|
let draft: AiWorkoutDraft;
|
|
try {
|
|
draft = JSON.parse(raw);
|
|
} catch {
|
|
return undefined;
|
|
}
|
|
|
|
const builtExercises: EditWorkoutData['exercises'] =
|
|
buildPrefillExercises(draft, exercises);
|
|
|
|
if (builtExercises.length === 0) return undefined;
|
|
|
|
return {
|
|
// No id → first save CREATEs a new workout.
|
|
name: draft.name || '',
|
|
date: new Date().toISOString(),
|
|
notes: draft.notes,
|
|
exercises: builtExercises,
|
|
};
|
|
});
|
|
|
|
// Clear the one-shot draft after mount so a manual reload starts blank.
|
|
// removeItem is idempotent, so StrictMode's double-run is harmless.
|
|
useEffect(() => {
|
|
sessionStorage.removeItem(AI_WORKOUT_DRAFT_KEY);
|
|
}, []);
|
|
|
|
return (
|
|
<WorkoutForm
|
|
exercises={exercises}
|
|
recentlyUsedExercises={[]}
|
|
editWorkout={editWorkout}
|
|
/>
|
|
);
|
|
}
|