v1.1.0:3 — AI upgrades: history context, test connection, cost estimator, streaming preview
Four incremental upgrades to the AI program generator. No schema change, no /data migration.
1. History as context (the killer feature)
- lib/ai/historyContext.ts builds a 90-day per-exercise rollup:
frequency, recent weights, estimated 1RM (Epley), avg RPE,
days-since-last, plus a STAGNANT flag when the heaviest weight in
the new half doesn't beat the old half.
- Generate page surfaces an "Include my workout history as context"
checkbox (default on at >=10 logged workouts). When checked, the
~1-3 KB summary is appended to the system prompt so the model can
recommend things like "you've stalled bench at 245 — try paused reps."
- We deliberately don't ship raw set logs (privacy + token cost).
2. Test connection
- POST /api/ai/test sends a tiny "say hi in 3 words" prompt and
reports latency + first sample, or the error inline.
- "Test connection" button next to "Save AI config" in
Settings -> AI integration. Verifies provider/model/key/baseUrl
without going through full program generation.
3. Cost estimator
- lib/ai/pricing.ts ships a price table for major models
(Claude 3.5/3.7/4/4.5, GPT-4o/5/o1/o3/o4-mini, Gemini 1.5/2.0/2.5).
Ollama always returns 0; openai-compatible returns null.
- Generation history shows per-row cost + a 30-day rolling total
at the top of the page.
4. Streaming preview render
- lib/ai/lenientJson.ts: stack-aware partial-JSON parser that
auto-closes open strings/brackets/braces in reverse-of-opening
order, drops dangling key:value pairs and partial keywords.
Returns a best-effort snapshot of the program-so-far on each chunk.
- Generate UI now renders a live "Building program..." panel that
updates as weeks/days/exercises arrive instead of just showing
raw text and waiting for stream end.
Tests: 26 new (ai-historyContext.test.ts, ai-lenientJson.test.ts,
ai-pricing.test.ts). 161 total pass.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -7,6 +7,10 @@ import {
|
||||
PROGRAM_OUTPUT_SHAPE,
|
||||
parseAIProgram,
|
||||
} from '@/lib/ai/programSchema';
|
||||
import {
|
||||
buildHistorySummary,
|
||||
formatHistoryContext,
|
||||
} from '@/lib/ai/historyContext';
|
||||
|
||||
/**
|
||||
* POST /api/ai/generate
|
||||
@@ -33,6 +37,13 @@ import {
|
||||
const bodySchema = z.object({
|
||||
templateId: z.string().optional().nullable(),
|
||||
userInput: z.string().min(1),
|
||||
/**
|
||||
* When true, build + append a compact summary of the user's
|
||||
* recent (90-day) workout history to the system prompt. Lets the
|
||||
* model design around stagnations, current strength levels, and
|
||||
* actual training frequency.
|
||||
*/
|
||||
includeHistory: z.boolean().optional().default(false),
|
||||
});
|
||||
|
||||
export const dynamic = 'force-dynamic';
|
||||
@@ -135,6 +146,13 @@ export async function POST(request: NextRequest) {
|
||||
})),
|
||||
);
|
||||
|
||||
// If requested, build the workout-history summary block.
|
||||
let historyBlock = '';
|
||||
if (parsed.data.includeHistory) {
|
||||
const summary = await buildHistorySummary(prisma, user.id);
|
||||
historyBlock = formatHistoryContext(summary);
|
||||
}
|
||||
|
||||
// Stitch the final system + user prompts.
|
||||
const baseSystem = template?.systemPrompt ?? DEFAULT_SYSTEM_PROMPT;
|
||||
const systemPrompt = `${baseSystem}
|
||||
@@ -143,7 +161,7 @@ OUTPUT SHAPE — emit ONLY a JSON object matching this shape (no commentary, no
|
||||
${PROGRAM_OUTPUT_SHAPE}
|
||||
|
||||
LIBRARY — pick exerciseId values from this list when possible. If you need an exercise the user doesn't have, set exerciseId to null and put the proposed name in exerciseName; the user will resolve it during preview.
|
||||
${libraryJson}`;
|
||||
${libraryJson}${historyBlock}`;
|
||||
|
||||
const userPromptBody =
|
||||
template?.userPromptTemplate.replace(/{{userInput}}/g, parsed.data.userInput) ??
|
||||
|
||||
Reference in New Issue
Block a user