From 93aad601cfd7586a5580bf6d2b1e5e0456492dec Mon Sep 17 00:00:00 2001 From: Sjoerd de Vries Date: Wed, 20 May 2026 17:47:45 +0200 Subject: [PATCH] feat: AI client and prompt templates --- src/ai/client.ts | 9 +++++ src/ai/prompts.ts | 88 +++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 97 insertions(+) create mode 100644 src/ai/client.ts create mode 100644 src/ai/prompts.ts diff --git a/src/ai/client.ts b/src/ai/client.ts new file mode 100644 index 0000000..58cca25 --- /dev/null +++ b/src/ai/client.ts @@ -0,0 +1,9 @@ +import OpenAI from 'openai'; +import { config } from '../config'; + +export function createAiClient(): OpenAI { + return new OpenAI({ + apiKey: config.openrouterApiKey, + baseURL: 'https://openrouter.ai/api/v1', + }); +} diff --git a/src/ai/prompts.ts b/src/ai/prompts.ts new file mode 100644 index 0000000..a584f4a --- /dev/null +++ b/src/ai/prompts.ts @@ -0,0 +1,88 @@ +import type { AIContext } from './context'; +import { formatDuration } from '../utils/duration'; + +function sessionLines(ctx: AIContext): string { + if (ctx.recentSessions.length === 0) return 'No sessions recorded yet.'; + return ctx.recentSessions + .map(s => ` ${s.date}: ${formatDuration(s.duration_min)}${s.notes ? ` (${s.notes})` : ''}`) + .join('\n'); +} + +function scheduleLines(ctx: AIContext): string { + if (ctx.upcomingSchedule.length === 0) return 'No upcoming schedule.'; + return ctx.upcomingSchedule + .map(e => ` ${e.date}: ${formatDuration(e.planned_min)} [${e.status}]`) + .join('\n'); +} + +function skipLines(ctx: AIContext): string { + if (ctx.recentSkips.length === 0) return 'None.'; + return ctx.recentSkips + .map(s => ` ${s.date}${s.reason ? `: ${s.reason}` : ''}`) + .join('\n'); +} + +function systemBase(ctx: AIContext): string { + return `You are a supportive personal training coach. You help the user track and improve their anal training practice. + +Today: ${ctx.today} +Training goal: ${ctx.goal} +Current streak: ${ctx.currentStreak} sessions + +Recent sessions (newest first): +${sessionLines(ctx)} + +Upcoming schedule: +${scheduleLines(ctx)} + +Recent skips: +${skipLines(ctx)} + +Be encouraging but honest. Always explain your reasoning. Flag if you think the pace is too fast or too slow.`; +} + +export function progressPrompt(ctx: AIContext): { system: string; user: string } { + return { + system: systemBase(ctx), + user: 'Analyse my recent training history. Give me a summary of my progress, highlight any patterns (positive or negative), and tell me how I\'m tracking toward my goal.', + }; +} + +export function nextStepPrompt(ctx: AIContext): { system: string; user: string } { + return { + system: systemBase(ctx), + user: 'Based on my history, what should my next training step be? Should I increase duration, maintain, or take a rest? Explain your reasoning.', + }; +} + +export function reschedulePrompt(ctx: AIContext, note: string | null): { system: string; user: string } { + const extra = note ? `\n\nAdditional context from the user: "${note}"` : ''; + return { + system: systemBase(ctx), + user: `Generate a training schedule for the next 14 days starting from ${ctx.today}.${extra} + +Respond with ONLY a JSON array — no prose, no markdown explanation — in this exact format: +[ + { "date": "YYYY-MM-DD", "planned_min": 60, "rationale": "reason" } +] + +Only include days with planned sessions (rest days are omitted). Space sessions appropriately.`, + }; +} + +export function setupPrompt( + currentLevel: string, availability: string, goal: string +): { system: string; user: string } { + return { + system: 'You are a supportive personal training coach helping a new user set up their anal training plan. Be warm, practical, and non-judgmental.', + user: `New user setup: +- Current level: ${currentLevel} +- Typical availability: ${availability} +- Goal: ${goal} + +Generate a 14-day starter schedule. Respond with ONLY a JSON array in this exact format: +[ + { "date": "YYYY-MM-DD", "planned_min": 30, "rationale": "Starting gently" } +]`, + }; +}