v1.1.0:4 — multi-config AI, background generation, ollama auto-detect, system prompt overhaul

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>
This commit is contained in:
Keysat
2026-05-11 08:09:01 -05:00
parent 8f149d35ab
commit 5e291203a5
35 changed files with 3509 additions and 632 deletions
+76
View File
@@ -172,6 +172,82 @@ if command -v sqlite3 >/dev/null 2>&1 && [ -f "$DB_PATH" ]; then
"
fi
# v1.1.0:4 added AIConfigProfile table (multi-config support) +
# UserPreferences.activeAIConfigId pointer + AIGeneration progress/
# duration columns + ProgramExercise suggested-weight columns.
if ! sqlite3 "$DB_PATH" \
"SELECT name FROM sqlite_master WHERE type='table' AND name='AIConfigProfile';" \
2>/dev/null | grep -q AIConfigProfile; then
log "creating AIConfigProfile table"
sqlite3 "$DB_PATH" "
CREATE TABLE AIConfigProfile (
id TEXT PRIMARY KEY,
userId TEXT NOT NULL,
name TEXT NOT NULL,
provider TEXT NOT NULL,
model TEXT NOT NULL,
baseUrl TEXT,
apiKey TEXT,
createdAt DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
updatedAt DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (userId) REFERENCES User(id) ON DELETE CASCADE
);
CREATE INDEX AIConfigProfile_userId_idx ON AIConfigProfile(userId);
"
fi
if ! sqlite3 "$DB_PATH" "PRAGMA table_info('UserPreferences');" 2>/dev/null | grep -q "|activeAIConfigId|"; then
log "adding UserPreferences.activeAIConfigId"
sqlite3 "$DB_PATH" "ALTER TABLE UserPreferences ADD COLUMN activeAIConfigId TEXT;"
fi
if ! sqlite3 "$DB_PATH" "PRAGMA table_info('AIGeneration');" 2>/dev/null | grep -q "|progressText|"; then
log "adding AIGeneration.progressText"
sqlite3 "$DB_PATH" "ALTER TABLE AIGeneration ADD COLUMN progressText TEXT;"
fi
if ! sqlite3 "$DB_PATH" "PRAGMA table_info('AIGeneration');" 2>/dev/null | grep -q "|durationMs|"; then
log "adding AIGeneration.durationMs"
sqlite3 "$DB_PATH" "ALTER TABLE AIGeneration ADD COLUMN durationMs INTEGER;"
fi
if ! sqlite3 "$DB_PATH" "PRAGMA table_info('ProgramExercise');" 2>/dev/null | grep -q "|suggestedWeight|"; then
log "adding ProgramExercise.suggestedWeight + suggestedWeightUnit"
sqlite3 "$DB_PATH" "ALTER TABLE ProgramExercise ADD COLUMN suggestedWeight REAL;"
sqlite3 "$DB_PATH" "ALTER TABLE ProgramExercise ADD COLUMN suggestedWeightUnit TEXT;"
fi
# v1.1.0:4 one-shot migration: lift each user's legacy single-config
# (UserPreferences.aiProvider/aiModel/...) into a new AIConfigProfile
# row marked active. Idempotent — only runs for users who have a
# configured legacy config but no profiles yet.
log "migrating any legacy single-config to AIConfigProfile (idempotent)"
sqlite3 "$DB_PATH" "
INSERT INTO AIConfigProfile (id, userId, name, provider, model, baseUrl, apiKey)
SELECT
'c' || lower(hex(randomblob(12))),
up.userId,
'Default (' || up.aiProvider || ')',
up.aiProvider,
up.aiModel,
up.aiBaseUrl,
up.aiApiKey
FROM UserPreferences up
WHERE up.aiProvider IS NOT NULL
AND up.aiModel IS NOT NULL
AND NOT EXISTS (SELECT 1 FROM AIConfigProfile p WHERE p.userId = up.userId);
" 2>/dev/null || log "WARN: legacy-config migration skipped"
# Set activeAIConfigId for users who now have exactly one profile.
sqlite3 "$DB_PATH" "
UPDATE UserPreferences
SET activeAIConfigId = (
SELECT id FROM AIConfigProfile WHERE userId = UserPreferences.userId LIMIT 1
)
WHERE activeAIConfigId IS NULL
AND (SELECT COUNT(*) FROM AIConfigProfile WHERE userId = UserPreferences.userId) = 1;
" 2>/dev/null || true
if ! sqlite3 "$DB_PATH" \
"SELECT name FROM sqlite_master WHERE type='table' AND name='InstanceSettings';" \
2>/dev/null | grep -q InstanceSettings; then
+10 -1
View File
@@ -9,6 +9,7 @@ import { v_1_0_0_7 } from './v1.0.0.7'
import { v_1_1_0_1 } from './v1.1.0.1'
import { v_1_1_0_2 } from './v1.1.0.2'
import { v_1_1_0_3 } from './v1.1.0.3'
import { v_1_1_0_4 } from './v1.1.0.4'
/**
* Version graph for the `proof-of-work` package.
@@ -28,9 +29,16 @@ import { v_1_1_0_3 } from './v1.1.0.3'
* OpenAI-compatible / Gemini / Ollama).
* v1.1.0:3 — AI upgrades: history-as-context, test connection,
* cost estimator, streaming preview render.
* v1.1.0:4 — AI integration overhaul: multi-config persistence,
* background generation (survives navigation), Ollama
* auto-detect + installed-model dropdown, curated model
* dropdowns for Claude / OpenAI / Gemini with current
* 2026 model names, system-prompt overhaul forcing library
* exerciseIds + suggested weights, sidebar sub-navigation,
* history detail view.
*/
export const versionGraph = VersionGraph.of({
current: v_1_1_0_3,
current: v_1_1_0_4,
other: [
v_1_0_0_1,
v_1_0_0_2,
@@ -41,5 +49,6 @@ export const versionGraph = VersionGraph.of({
v_1_0_0_7,
v_1_1_0_1,
v_1_1_0_2,
v_1_1_0_3,
],
})
+109
View File
@@ -0,0 +1,109 @@
import { IMPOSSIBLE, VersionInfo } from '@start9labs/start-sdk'
/**
* v1.1.0:4 — multi-config AI integration, background generation,
* ollama auto-detect, system-prompt overhaul, history
* detail view, sidebar sub-nav.
*
* Driven by post-v1.1.0:3 user feedback. The biggest themes:
*
* 1) Multi-config persistence
* - You can save N AI configs (one per provider, or several of the
* same provider with different models/keys), toggle one as the
* "active" config, and per-config "Test connection" to verify
* before activating. Switching providers no longer means losing
* the previous setup.
* - New schema: AIConfigProfile table (per-user). UserPreferences
* grows `activeAIConfigId`; the legacy single-config columns are
* kept and mirrored from the active profile so any old code path
* that reads from prefs continues to work.
* - On boot, any user who already had a single-config setup gets
* that config lifted into a default AIConfigProfile + activated.
* Idempotent.
*
* 2) Ollama auto-detect
* - The "Add AI config" form probes /api/tags on the StartOS
* internal addresses (ollama.startos:11434, ollama.embassy:11434).
* If reachable, the URL field auto-fills and the model field
* becomes a dropdown of installed models. No more memorizing
* "the right URL" or pasting a model tag.
*
* 3) Model dropdowns for the leading providers
* - Settings now offers a curated dropdown of recommended models
* for Claude (Opus 4.7, Sonnet 4.6, Haiku 4.5), OpenAI (GPT-5.5,
* 5.4, 5.4-mini, 5.4-nano), and Gemini (3.1-pro-preview, 2.5-pro,
* 2.5-flash). "Other (type your own)" stays available for power
* users on niche models. Fixes the "I tried gemini-3.0-pro and
* got 404" footgun.
*
* 4) Background generation
* - Generation now runs server-side via a detached runner. Closing
* the page or navigating away no longer kills it — the row keeps
* filling in. The Generate UI surfaces a banner explaining this.
* - The new History detail page polls progress + renders the
* partial JSON live; reload-during-streaming "just works." Useful
* for slow local Ollama runs.
* - New AIGeneration columns: progressText (in-flight stream),
* durationMs (final wall-clock).
*
* 5) System prompt overhaul
* - lib/ai/systemPromptBase.ts: a structural contract prepended to
* every template. Forces JSON-only output (no markdown fences),
* forces use of library exerciseIds (no more "exerciseId doesn't
* belong to this user" on apply), and forces a suggestedWeight
* per resistance exercise — both with-history (relative to user's
* lifts) and without-history (% of typical 1RM) variants.
* - aiExerciseSchema gains suggestedWeight + suggestedWeightUnit.
* ProgramExercise gains the same columns. Starting a workout from
* a ProgramDay now pre-populates SetLog.weight from the suggestion
* so users have a target on day 1 instead of a blank field.
*
* 6) Test connection improvements
* - Latency reported in seconds (was ms — confusing for slow Ollama).
* - Stale "✓ Connected" no longer lingers when you change the form.
* - Gemini surfaces finishReason when the response is empty (e.g.
* "blocked by safety filter") instead of the generic "empty
* response — check the model name."
* - Test ping uses generous maxOutputTokens so thinking models
* (Gemini 2.5/3.x, OpenAI o-series) actually emit text after
* reasoning instead of running out of budget.
* - Per-config Test button (no need to activate first).
*
* 7) History detail view
* - Click any AIGeneration row → full read-only program tree, plus
* the exact prompts sent. Apply from here without re-generating.
* - In-flight rows poll for live progress.
*
* 8) Sidebar sub-navigation
* - "AI" expands to Generate / History / Templates.
* - "Settings" expands to General / Password / Sessions /
* AI integration / Export / Instance / Danger zone, with
* anchor scroll to the matching section.
*
* 9) API key UX
* - "Key saved" indicator on saved configs (was confusing to see
* an empty input field after a successful save).
*
* Migrations:
* - AIConfigProfile table created.
* - UserPreferences.activeAIConfigId added.
* - AIGeneration.progressText + .durationMs added.
* - ProgramExercise.suggestedWeight + .suggestedWeightUnit added.
* - One-shot lift of the legacy single-config row into a default
* AIConfigProfile per user.
*
* /data is unchanged in spirit — all migrations are additive ALTERs
* via the boot entrypoint. Existing programs/workouts/exercises stay
* exactly as they were.
*/
export const v_1_1_0_4 = VersionInfo.of({
version: '1.1.0:4',
releaseNotes: {
en_US:
'AI integration overhauled based on user testing: (1) save MULTIPLE AI configs and switch between them — Claude, OpenAI, Gemini, custom OpenAI-compatible, Ollama all coexist with one toggled active; (2) Ollama auto-detect — the form probes ollama.startos:11434 and shows your installed models in a dropdown, no copy-paste; (3) curated model dropdowns for Claude / OpenAI / Gemini with current 2026 models (Claude Opus 4.7, Sonnet 4.6, GPT-5.5, Gemini 3.1 Pro Preview, etc.); (4) generation now runs in the BACKGROUND — close the page, come back, find your program in History; (5) system prompt rewritten so the model picks library exercises only (no more "exerciseId doesn\'t belong to this user" errors) and suggests starting weights per exercise (which seed your first workout when you start a program day); (6) generation duration shown alongside cost; (7) Gemini "empty response" now reports the actual finishReason (safety filter, max tokens, etc.); (8) sidebar shows sub-navigation for AI + Settings sections; (9) click any History row to see the full program tree without applying it. No data loss; the schema migration runs additively at boot.',
},
migrations: {
up: async () => {},
down: IMPOSSIBLE,
},
})