5e291203a5
User-feedback-driven release after testing v1.1.0:3. Nine themes:
1. Multi-config persistence
- New AIConfigProfile table (per-user). Save N configs, toggle one
active. Switching providers no longer wipes the previous setup.
- UserPreferences gains activeAIConfigId; legacy single-config
columns are mirrored from the active profile so existing reads
keep working without conditional logic.
- Idempotent boot migration lifts any existing single-config row
into a default profile.
2. Ollama auto-detect
- The "Add config" form probes /api/tags on the StartOS internal
addresses (ollama.startos / ollama.embassy on :11434). If
reachable: URL pre-fills, model field becomes a dropdown of
installed models. Fixes the copy-paste UX.
3. Curated model dropdowns for major providers
- Claude: Opus 4.7, Sonnet 4.6 (1M ctx), Haiku 4.5
- OpenAI: GPT-5.5, 5.4, 5.4-mini, 5.4-nano
- Gemini: 3.1-pro-preview, 2.5-pro, 2.5-flash, etc.
- "Other (type your own)" stays for niche models.
- Fixes "I tried gemini-3.0-pro and got 404."
4. Background generation
- lib/ai/generationRunner.ts: detached runner with in-memory
pub/sub bus. POST /api/ai/generate kicks it off and returns
immediately. SSE stream attaches by id. The runner survives
request cancellation; navigating away no longer kills it.
- New AIGeneration columns: progressText (in-flight stream),
durationMs (final wall-clock).
- Generate UI shows a banner explaining background-safety.
- History detail page polls progress + renders partial JSON
live for cross-process resume (page refresh, new tab).
5. System prompt overhaul
- lib/ai/systemPromptBase.ts: structural contract prepended to
every template. Forces JSON-only output, library-exerciseId
usage (kills "exerciseId doesn't belong to this user" errors),
and per-resistance-exercise suggestedWeight (with-history vs
without-history variants).
- aiExerciseSchema + ProgramExercise gain suggestedWeight +
suggestedWeightUnit. Starting a workout from a ProgramDay
pre-populates SetLog.weight from the suggestion.
6. Test connection improvements
- Latency in seconds (was ms — confusing for slow Ollama).
- Stale "✓ Connected" clears on form change.
- Per-config Test (no need to activate first).
- Generous maxOutputTokens for thinking models.
- Gemini surfaces finishReason on empty response (e.g. "blocked
by safety filter") instead of generic "empty response."
- Test endpoint accepts a draft body so you can verify before
saving + before activating.
7. History detail view
- Click row → full program tree + exact prompts sent. Apply from
here without re-generating. Pending rows poll for progress.
8. Sidebar sub-navigation
- AI: Generate / History / Templates
- Settings: General / Password / Sessions / AI integration /
Export / Instance (admin) / Danger zone, with anchor scroll.
9. API key UX
- "Key saved" indicator on saved configs (was confusing to see
an empty input after a successful save).
Schema migrations (additive, idempotent in entrypoint):
- AIConfigProfile table created
- UserPreferences.activeAIConfigId
- AIGeneration.progressText + durationMs
- ProgramExercise.suggestedWeight + suggestedWeightUnit
Tests: 16 new (systemPromptBase, modelMenu, generationRunner). 177
total pass.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
78 lines
4.0 KiB
TypeScript
78 lines
4.0 KiB
TypeScript
/**
|
|
* Base system-prompt rules prepended to every template's prompt before
|
|
* sending to the model. Centralized here so we can tighten output
|
|
* constraints in one place rather than editing every template.
|
|
*
|
|
* Two main jobs:
|
|
* 1. Force the JSON output shape (no prose, no fences, picks library
|
|
* ids only — fixes "exerciseId doesn't belong to this user" errors)
|
|
* 2. Force a suggested starting weight per resistance exercise
|
|
* (the model otherwise tends to leave it null, which leaves the
|
|
* user with no concrete target on day 1)
|
|
*
|
|
* Templates supply their *coaching philosophy* (hypertrophy = volume +
|
|
* progressive overload, conditioning = aerobic base etc); this module
|
|
* supplies the *structural contract*.
|
|
*/
|
|
|
|
export interface BaseSystemPromptOpts {
|
|
/** "lbs" | "kg" — the user's preferred weight unit, used as the default
|
|
* suggestedWeightUnit when the model omits one. */
|
|
weightUnit: 'lbs' | 'kg';
|
|
/** Whether the user's workout history is being included. Toggles a
|
|
* short instruction on how to use it. */
|
|
hasHistoryContext: boolean;
|
|
/** True when the model is local (Ollama). Local models tend to need
|
|
* shorter, blunter rules and benefit from explicit examples. */
|
|
isLocalModel: boolean;
|
|
}
|
|
|
|
export function buildBaseSystemPrompt(opts: BaseSystemPromptOpts): string {
|
|
const lines: string[] = [];
|
|
|
|
lines.push(
|
|
'# OUTPUT CONTRACT (mandatory)',
|
|
'',
|
|
'1. Reply with EXACTLY ONE JSON object. No prose before or after. No ```json fences.',
|
|
'2. Every exercise must use an `exerciseId` from the LIBRARY block at the bottom of this prompt. NEVER invent ids. If nothing in the library matches, pick the closest fit and explain the substitution in `notes`.',
|
|
`3. Every resistance exercise MUST have a \`suggestedWeight\` (a number, in ${opts.weightUnit}). Cardio, stretching, and bodyweight exercises set it to null.`,
|
|
`4. \`suggestedWeightUnit\` should be "${opts.weightUnit}" unless the exercise is conventionally tracked in the other unit (e.g. kettlebells often kg). Omit for non-loaded exercises.`,
|
|
'5. Every exercise needs `sets` and either `repsMin` (with `repsMax` if a range) or a duration note.',
|
|
'6. Use `rpe` (1-10) for working sets to communicate intensity; warmups can be lower or omitted.',
|
|
'7. `restSeconds` is required for compound lifts; optional for accessories.',
|
|
'8. Keep day volumes realistic: 4-7 exercises, 60-75 minutes total. Include warm-up sets only if they belong in the program (don\'t list mobility separately unless the user asked).',
|
|
'9. The `notes` field is for coaching cues, tempo, technique reminders — keep it short, one sentence.',
|
|
);
|
|
|
|
if (opts.hasHistoryContext) {
|
|
lines.push(
|
|
'',
|
|
'# USING THE HISTORY BLOCK',
|
|
'',
|
|
'The HISTORY block below summarizes the user\'s last 90 days. Use it to:',
|
|
'- Pick `suggestedWeight` near their current working weights, NOT round numbers from nowhere.',
|
|
'- Address any STAGNANT lifts: deload, change rep ranges, swap variations, or work at a different RPE.',
|
|
'- Respect their training frequency (don\'t prescribe 5x/week if they\'ve been training 3x).',
|
|
'- Stay in their movement vocabulary unless they asked for variety.',
|
|
);
|
|
} else {
|
|
lines.push(
|
|
'',
|
|
'# WEIGHT GUIDANCE WITHOUT HISTORY',
|
|
'',
|
|
`Without prior performance data, set conservative \`suggestedWeight\` values: 50-65% of typical 1RM for the lift at the user's stated experience level. Use round increments common in commercial gyms (5${opts.weightUnit} jumps; 2.5${opts.weightUnit} for small accessories). Always add a coaching note like "adjust to leave 2-3 reps in reserve" so the user knows it's a starting estimate.`,
|
|
);
|
|
}
|
|
|
|
if (opts.isLocalModel) {
|
|
lines.push(
|
|
'',
|
|
'# LOCAL MODEL REMINDER',
|
|
'',
|
|
'You are running locally with limited reasoning. Stick to the simplest valid program that matches the request. Do not overthink. JSON only.',
|
|
);
|
|
}
|
|
|
|
return lines.join('\n');
|
|
}
|