From 55c17614b8bfad3ddbe2427a0aec82f86c572233 Mon Sep 17 00:00:00 2001 From: Keysat Date: Sat, 9 May 2026 21:24:00 -0500 Subject: [PATCH] =?UTF-8?q?v1.0.0:7=20=E2=80=94=20exercise=20library=20cle?= =?UTF-8?q?anup,=20photo-import=20removal,=20AI-section=20honesty?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Library JSON cleanup (proof-of-work/prisma/exercises.seed.json) 19 exercises corrected: - Cycling/Jump Rope/Rowing/Running: type=cardio with proper inputFields (duration/distance/calories — no more reps/weight). - Walking Lunge/Wall Sit/Headstand/Hip Extension: reclassified out of cardio into bodyweight. - Plank/Mace warmup/Hollow Body Landmine/Soccer: inputFields fixed. - Descriptions added for ~10 cryptic exercises (Core, Resistance Band, Stir the pot, Slide Board, Neck Circuit, TGU, Captains of Crush, etc.). Reconcile-on-boot (ensureExerciseLibrary.cjs) Changed from INSERT-OR-IGNORE to INSERT-OR-UPDATE keyed on (userId, name). Existing rows where isCustom = 0 get description/type/muscleGroups/inputFields/defaultWeightUnit refreshed from the curated JSON. Rows where isCustom = 1 are skipped — user customizations always win. Verified end-to-end: applied patches propagate to a copy of the user's snapshot DB; manually-tampered isCustom=1 rows survive a second reconcile pass untouched. PATCH /api/exercises/[id] flips isCustom -> true on user edits Once you edit a library exercise via the in-app UI, the row's isCustom flag becomes 1 and the boot-time reconcile leaves it alone forever. Closes the only failure mode where a maintainer curated-library refresh could overwrite user edits. Photo-import (Claude vision) removed - app/api/workouts/import/route.ts deleted. - components/import/WorkoutImportClient.tsx deleted (orphan component — wasn't referenced anywhere by the live UI). - CSV import (app/main/import → page-csv.tsx → /api/workouts/import/save) is unchanged. The save endpoint stays — it's used by the CSV flow too. Settings UI: "Claude AI Integration" section removed The toggle + API key input promised "personalized workout recommendations" that the codebase never delivered (the only actually-wired use was the photo-import we just removed). Schema columns User.enableClaudeAI / User.claudeApiKey stay as harmless dead fields — they'll get cleaned up or repurposed when the model-agnostic AI work lands. The preferences API no longer accepts or returns those fields. No data migration. /data on existing installs is untouched. v1.0.0:7 promoted to current; :1-:6 in other. --- proof-of-work/app/api/exercises/[id]/route.ts | 7 + proof-of-work/app/api/preferences/route.ts | 36 +- .../app/api/workouts/import/route.ts | 216 ---- proof-of-work/app/auth/signup/actions.ts | 1 - .../components/import/WorkoutImportClient.tsx | 1011 ----------------- .../components/settings/SettingsForm.tsx | 96 +- .../prisma/ensureExerciseLibrary.cjs | 67 +- proof-of-work/prisma/exercises.seed.json | 91 +- start9/0.4/docker_entrypoint.sh | 19 +- start9/0.4/startos/versions/index.ts | 14 +- start9/0.4/startos/versions/v1.0.0.7.ts | 58 + 11 files changed, 189 insertions(+), 1427 deletions(-) delete mode 100644 proof-of-work/app/api/workouts/import/route.ts delete mode 100644 proof-of-work/components/import/WorkoutImportClient.tsx create mode 100644 start9/0.4/startos/versions/v1.0.0.7.ts diff --git a/proof-of-work/app/api/exercises/[id]/route.ts b/proof-of-work/app/api/exercises/[id]/route.ts index 33071ff..e1df2e8 100644 --- a/proof-of-work/app/api/exercises/[id]/route.ts +++ b/proof-of-work/app/api/exercises/[id]/route.ts @@ -132,6 +132,13 @@ export async function PATCH( if (validated.defaultWeightUnit !== undefined) data.defaultWeightUnit = validated.defaultWeightUnit; + // Flip isCustom -> true on any user edit. The boot-time + // ensureExerciseLibrary reconciliation only updates rows where + // isCustom = 0, so this preserves the user's intent: once they've + // edited a library exercise, the maintainer can no longer + // overwrite their changes via a curated-library refresh. + data.isCustom = true; + const updated = await prisma.exercise.update({ where: { id: params.id }, data, diff --git a/proof-of-work/app/api/preferences/route.ts b/proof-of-work/app/api/preferences/route.ts index 66f4f0f..6ec25b3 100644 --- a/proof-of-work/app/api/preferences/route.ts +++ b/proof-of-work/app/api/preferences/route.ts @@ -6,13 +6,14 @@ import { z } from "zod"; const PreferencesSchema = z.object({ theme: z.enum(["light", "dark", "system"]).optional(), defaultWeightUnit: z.enum(["lbs", "kg"]).optional(), - enableClaudeAI: z.boolean().optional(), - claudeApiKey: z.string().optional(), }); /** * GET /api/preferences - * Get user preferences + * Get user preferences. Strips the dead Claude AI fields + * (enableClaudeAI / claudeApiKey) — those columns still exist in the + * schema but are slated for replacement by the model-agnostic AI work + * (Option 3 / future). Don't reintroduce them in the response. */ export async function GET(_request: NextRequest) { try { @@ -26,24 +27,18 @@ export async function GET(_request: NextRequest) { }); if (!preferences) { - // Create default preferences preferences = await prisma.userPreferences.create({ data: { userId: user.id, theme: "system", defaultWeightUnit: "lbs", defaultRestSeconds: 90, - enableClaudeAI: false, }, }); } - // Don't return API key in response - const { claudeApiKey, ...safePreferences } = preferences; - return NextResponse.json({ - ...safePreferences, - claudeApiKey: claudeApiKey ? "***" : undefined, - }); + const { claudeApiKey, enableClaudeAI, ...safe } = preferences; + return NextResponse.json(safe); } catch (error) { console.error("GET /api/preferences error:", error); return NextResponse.json( @@ -55,7 +50,9 @@ export async function GET(_request: NextRequest) { /** * POST /api/preferences - * Update user preferences + * Update user preferences. Only the fields in `PreferencesSchema` are + * accepted; anything else (including the dead Claude AI fields) is + * silently dropped at the Zod boundary. */ export async function POST(request: NextRequest) { try { @@ -67,7 +64,6 @@ export async function POST(request: NextRequest) { const body = await request.json(); const validated = PreferencesSchema.parse(body); - // Get or create preferences let preferences = await prisma.userPreferences.findUnique({ where: { userId: user.id }, }); @@ -86,23 +82,15 @@ export async function POST(request: NextRequest) { }); } - // Don't return API key in response - const { claudeApiKey, ...safePreferences } = preferences; - return NextResponse.json({ - ...safePreferences, - claudeApiKey: claudeApiKey ? "***" : undefined, - }); + const { claudeApiKey, enableClaudeAI, ...safe } = preferences; + return NextResponse.json(safe); } catch (error) { if (error instanceof z.ZodError) { return NextResponse.json( - { - error: "Validation error", - details: error.errors, - }, + { error: "Validation error", details: error.errors }, { status: 400 } ); } - console.error("POST /api/preferences error:", error); return NextResponse.json( { error: "Internal server error" }, diff --git a/proof-of-work/app/api/workouts/import/route.ts b/proof-of-work/app/api/workouts/import/route.ts deleted file mode 100644 index e792975..0000000 --- a/proof-of-work/app/api/workouts/import/route.ts +++ /dev/null @@ -1,216 +0,0 @@ -import { NextResponse } from "next/server"; -import { z } from "zod"; -import { getCurrentUser } from "@/lib/auth"; -import { prisma } from "@/lib/prisma"; - -const importSchema = z.object({ - images: z.array(z.string()).min(1, "At least one image is required"), -}); - -const CLAUDE_API_URL = "https://api.anthropic.com/v1/messages"; - -const SYSTEM_PROMPT = `You are analyzing photos of handwritten workout logs, Apple Notes, or other workout records. Extract all workout data you can find. - -IMPORTANT RULES: -- If you can identify a date for the workout, include it as an ISO date string (YYYY-MM-DD) -- If no date is visible, set date to null -- Extract exercise names as closely as written -- For each exercise, extract all sets with whatever data is visible (reps, weight, duration, etc.) -- If you're unsure about an exercise name or value, set "uncertain": true and explain in "uncertainReason" -- Weight units: assume lbs unless kg or kilograms is explicitly written -- For cardio exercises (running, biking, rowing, assault bike, jump rope, etc.), look for duration, distance, and calories -- Be conservative — only include data you can actually read - -Return ONLY valid JSON with this exact structure (no markdown, no code fences): -{ - "workouts": [ - { - "date": "2025-01-15" or null, - "name": "Upper Body" or null, - "notes": "any overall notes" or null, - "exercises": [ - { - "name": "Bench Press", - "type": "barbell" | "dumbbell" | "machine" | "cable" | "bodyweight" | "cardio" | "kettlebell" | "other", - "sets": [ - { - "reps": 8, - "weight": 225, - "weightUnit": "lbs", - "durationSeconds": null, - "distance": null, - "distanceUnit": null, - "calories": null, - "rpe": null, - "notes": null - } - ], - "notes": null, - "uncertain": false, - "uncertainReason": null - } - ] - } - ], - "confidence": "high" | "medium" | "low", - "warnings": ["list any legibility issues or assumptions made"] -}`; - -export async function POST(request: Request) { - try { - const user = await getCurrentUser(); - if (!user) { - return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); - } - - // Get user's Claude API key from preferences - const preferences = await prisma.userPreferences.findUnique({ - where: { userId: user.id }, - }); - - if (!preferences?.enableClaudeAI || !preferences?.claudeApiKey) { - return NextResponse.json( - { - error: "Claude AI is not configured. Please add your API key in Settings.", - code: "NO_API_KEY", - }, - { status: 400 } - ); - } - - const body = await request.json(); - const validated = importSchema.parse(body); - - // Build Claude API request with vision - const content: any[] = [ - { - type: "text", - text: "Please analyze the following workout log image(s) and extract all workout data. Return ONLY valid JSON.", - }, - ]; - - // Add each image - for (const imageData of validated.images) { - // imageData could be a data URL or raw base64 - let base64 = imageData; - let mediaType = "image/jpeg"; - - if (imageData.startsWith("data:")) { - const match = imageData.match(/^data:(image\/\w+);base64,(.+)$/); - if (match) { - mediaType = match[1]; - base64 = match[2]; - } - } - - content.push({ - type: "image", - source: { - type: "base64", - media_type: mediaType, - data: base64, - }, - }); - } - - // Call Claude API - const claudeResponse = await fetch(CLAUDE_API_URL, { - method: "POST", - headers: { - "Content-Type": "application/json", - "x-api-key": preferences.claudeApiKey, - "anthropic-version": "2023-06-01", - }, - body: JSON.stringify({ - model: "claude-sonnet-4-20250514", - max_tokens: 4096, - system: SYSTEM_PROMPT, - messages: [ - { - role: "user", - content, - }, - ], - }), - }); - - if (!claudeResponse.ok) { - const errorBody = await claudeResponse.text(); - console.error("Claude API error:", claudeResponse.status, errorBody); - - if (claudeResponse.status === 401) { - return NextResponse.json( - { error: "Invalid Claude API key. Please check your key in Settings.", code: "INVALID_KEY" }, - { status: 400 } - ); - } - if (claudeResponse.status === 429) { - return NextResponse.json( - { error: "Claude API rate limit reached. Please try again in a moment.", code: "RATE_LIMITED" }, - { status: 429 } - ); - } - - return NextResponse.json( - { error: "Failed to analyze images. Please try again.", code: "API_ERROR" }, - { status: 502 } - ); - } - - const claudeData = await claudeResponse.json(); - - // Extract text content from Claude's response - const textContent = claudeData.content?.find((c: any) => c.type === "text"); - if (!textContent?.text) { - return NextResponse.json( - { error: "No response from Claude. Please try again.", code: "EMPTY_RESPONSE" }, - { status: 502 } - ); - } - - // Parse the JSON response - let parsed; - try { - // Try to extract JSON from the response (Claude might wrap it in code fences) - let jsonText = textContent.text.trim(); - const jsonMatch = jsonText.match(/```(?:json)?\s*([\s\S]*?)```/); - if (jsonMatch) { - jsonText = jsonMatch[1].trim(); - } - parsed = JSON.parse(jsonText); - } catch { - console.error("Failed to parse Claude response:", textContent.text); - return NextResponse.json( - { - error: "Could not parse the workout data. The image may be too unclear.", - code: "PARSE_ERROR", - raw: textContent.text.substring(0, 500), - }, - { status: 422 } - ); - } - - // Validate basic structure - if (!parsed.workouts || !Array.isArray(parsed.workouts)) { - return NextResponse.json( - { error: "Invalid response structure from Claude.", code: "INVALID_STRUCTURE" }, - { status: 422 } - ); - } - - return NextResponse.json(parsed); - } catch (error) { - if (error instanceof z.ZodError) { - return NextResponse.json( - { error: "Invalid request data", details: error.errors }, - { status: 400 } - ); - } - - console.error("POST /api/workouts/import error:", error); - return NextResponse.json( - { error: "Internal server error" }, - { status: 500 } - ); - } -} diff --git a/proof-of-work/app/auth/signup/actions.ts b/proof-of-work/app/auth/signup/actions.ts index 47dfcf2..fe9b3e2 100644 --- a/proof-of-work/app/auth/signup/actions.ts +++ b/proof-of-work/app/auth/signup/actions.ts @@ -62,7 +62,6 @@ export async function signupAction( theme: 'system', defaultWeightUnit: 'lbs', defaultRestSeconds: 90, - enableClaudeAI: false, }, }, }, diff --git a/proof-of-work/components/import/WorkoutImportClient.tsx b/proof-of-work/components/import/WorkoutImportClient.tsx deleted file mode 100644 index 1ca19a0..0000000 --- a/proof-of-work/components/import/WorkoutImportClient.tsx +++ /dev/null @@ -1,1011 +0,0 @@ -'use client'; - -import React, { useState } from 'react'; -import Link from 'next/link'; -import { - Upload, - X, - AlertTriangle, - Check, - Plus, - Trash2, - Loader, - Image, - ChevronDown, - ChevronUp, -} from 'lucide-react'; -import { Exercise } from '@prisma/client'; -import { - ParsedWorkout, - ParsedExercise, - ParsedSet, - ImportParseResponse, - ReviewedWorkout, -} from '@/types'; - -interface WorkoutImportClientProps { - hasApiKey: boolean; - exercises: Exercise[]; -} - -type Phase = 'upload' | 'review' | 'save'; - -interface ImageFile { - id: string; - file: File; - preview: string; -} - -interface EditableSet extends ParsedSet { - id: string; -} - -interface EditableExercise extends ParsedExercise { - id: string; - sets: EditableSet[]; -} - -interface EditableWorkout extends ParsedWorkout { - id: string; - exercises: EditableExercise[]; - overallConfidence?: string; - warnings?: string[]; -} - -const WorkoutImportClient: React.FC = ({ - hasApiKey, - exercises: _exercises, -}) => { - const [phase, setPhase] = useState('upload'); - const [images, setImages] = useState([]); - const [isAnalyzing, setIsAnalyzing] = useState(false); - const [editableWorkouts, setEditableWorkouts] = useState([]); - const [expandedWorkouts, setExpandedWorkouts] = useState>(new Set()); - const [isSaving, setIsSaving] = useState(false); - const [_saveProgress, setSaveProgress] = useState<{ - current: number; - total: number; - } | null>(null); - const [saveSuccess, setSuccess] = useState<{ ids: string[] } | null>(null); - const [errorMessage, setErrorMessage] = useState(null); - - // File upload handlers - const fileToBase64 = (file: File): Promise => { - return new Promise((resolve, reject) => { - const reader = new FileReader(); - reader.onload = () => resolve(reader.result as string); - reader.onerror = reject; - reader.readAsDataURL(file); - }); - }; - - const handleDragOver = (e: React.DragEvent) => { - e.preventDefault(); - e.stopPropagation(); - }; - - const handleDrop = (e: React.DragEvent) => { - e.preventDefault(); - e.stopPropagation(); - const files = Array.from(e.dataTransfer.files).filter((f) => - f.type.startsWith('image/') - ); - addImages(files); - }; - - const handleFileSelect = (e: React.ChangeEvent) => { - const files = Array.from(e.currentTarget.files || []); - addImages(files); - }; - - const addImages = (files: File[]) => { - const newImages: ImageFile[] = files.map((file) => ({ - id: Math.random().toString(36).slice(2), - file, - preview: URL.createObjectURL(file), - })); - setImages((prev) => [...prev, ...newImages]); - }; - - const removeImage = (id: string) => { - setImages((prev) => { - const img = prev.find((i) => i.id === id); - if (img) URL.revokeObjectURL(img.preview); - return prev.filter((i) => i.id !== id); - }); - }; - - const handleAnalyze = async () => { - setIsAnalyzing(true); - setErrorMessage(null); - try { - const base64Images = await Promise.all(images.map((img) => fileToBase64(img.file))); - - const response = await fetch('/api/workouts/import', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ images: base64Images }), - }); - - if (!response.ok) { - throw new Error(`Analysis failed: ${response.statusText}`); - } - - const data: ImportParseResponse = await response.json(); - - // Convert to editable format with unique IDs - const editable: EditableWorkout[] = data.workouts.map((w) => ({ - ...w, - id: Math.random().toString(36).slice(2), - date: w.date || new Date().toISOString().split('T')[0], - exercises: w.exercises.map((e) => ({ - ...e, - id: Math.random().toString(36).slice(2), - sets: e.sets.map((s) => ({ - ...s, - id: Math.random().toString(36).slice(2), - })), - })), - })); - - setEditableWorkouts(editable); - setExpandedWorkouts(new Set(editable.map((w) => w.id))); - setPhase('review'); - } catch (error) { - setErrorMessage(error instanceof Error ? error.message : 'Unknown error'); - } finally { - setIsAnalyzing(false); - } - }; - - const toggleWorkoutExpanded = (id: string) => { - setExpandedWorkouts((prev) => { - const next = new Set(prev); - if (next.has(id)) { - next.delete(id); - } else { - next.add(id); - } - return next; - }); - }; - - const updateWorkoutField = ( - workoutId: string, - field: keyof EditableWorkout, - value: unknown - ) => { - setEditableWorkouts((prev) => - prev.map((w) => - w.id === workoutId ? { ...w, [field]: value } : w - ) - ); - }; - - const updateExerciseField = ( - workoutId: string, - exerciseId: string, - field: keyof EditableExercise, - value: unknown - ) => { - setEditableWorkouts((prev) => - prev.map((w) => - w.id === workoutId - ? { - ...w, - exercises: w.exercises.map((e) => - e.id === exerciseId ? { ...e, [field]: value } : e - ), - } - : w - ) - ); - }; - - const updateSetField = ( - workoutId: string, - exerciseId: string, - setId: string, - field: keyof EditableSet, - value: unknown - ) => { - setEditableWorkouts((prev) => - prev.map((w) => - w.id === workoutId - ? { - ...w, - exercises: w.exercises.map((e) => - e.id === exerciseId - ? { - ...e, - sets: e.sets.map((s) => - s.id === setId ? { ...s, [field]: value } : s - ), - } - : e - ), - } - : w - ) - ); - }; - - const addSet = (workoutId: string, exerciseId: string) => { - const newSet: EditableSet = { - id: Math.random().toString(36).slice(2), - reps: null, - weight: null, - weightUnit: 'lb', - durationSeconds: null, - distance: null, - distanceUnit: 'mi', - calories: null, - rpe: null, - notes: null, - }; - - setEditableWorkouts((prev) => - prev.map((w) => - w.id === workoutId - ? { - ...w, - exercises: w.exercises.map((e) => - e.id === exerciseId - ? { ...e, sets: [...e.sets, newSet] } - : e - ), - } - : w - ) - ); - }; - - const removeSet = (workoutId: string, exerciseId: string, setId: string) => { - setEditableWorkouts((prev) => - prev.map((w) => - w.id === workoutId - ? { - ...w, - exercises: w.exercises.map((e) => - e.id === exerciseId - ? { ...e, sets: e.sets.filter((s) => s.id !== setId) } - : e - ), - } - : w - ) - ); - }; - - const addExercise = (workoutId: string) => { - const newExercise: EditableExercise = { - id: Math.random().toString(36).slice(2), - name: 'New Exercise', - uncertain: false, - uncertainReason: undefined, - sets: [ - { - id: Math.random().toString(36).slice(2), - reps: null, - weight: null, - weightUnit: 'lb', - durationSeconds: null, - distance: null, - distanceUnit: 'mi', - calories: null, - rpe: null, - notes: null, - }, - ], - }; - - setEditableWorkouts((prev) => - prev.map((w) => - w.id === workoutId - ? { ...w, exercises: [...w.exercises, newExercise] } - : w - ) - ); - }; - - const removeExercise = (workoutId: string, exerciseId: string) => { - setEditableWorkouts((prev) => - prev.map((w) => - w.id === workoutId - ? { ...w, exercises: w.exercises.filter((e) => e.id !== exerciseId) } - : w - ) - ); - }; - - const removeWorkout = (workoutId: string) => { - setEditableWorkouts((prev) => prev.filter((w) => w.id !== workoutId)); - }; - - const handleSaveAll = async () => { - setIsSaving(true); - setSaveProgress({ current: 0, total: editableWorkouts.length }); - setErrorMessage(null); - - const createdIds: string[] = []; - - try { - for (let i = 0; i < editableWorkouts.length; i++) { - const workout = editableWorkouts[i]; - setSaveProgress({ current: i + 1, total: editableWorkouts.length }); - - // Prepare reviewed data (remove internal IDs) - const reviewedWorkout: ReviewedWorkout = { - date: workout.date, - name: workout.name, - notes: workout.notes, - exercises: workout.exercises.map((e) => ({ - name: e.name, - sets: e.sets.map((s) => ({ - reps: s.reps, - weight: s.weight, - weightUnit: s.weightUnit, - durationSeconds: s.durationSeconds, - distance: s.distance, - distanceUnit: s.distanceUnit, - calories: s.calories, - rpe: s.rpe, - notes: s.notes, - })), - })), - }; - - const response = await fetch('/api/workouts/import/save', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(reviewedWorkout), - }); - - if (!response.ok) { - throw new Error(`Failed to save workout: ${response.statusText}`); - } - - const result = await response.json(); - if (result.id) { - createdIds.push(result.id); - } - } - - setSuccess({ ids: createdIds }); - setEditableWorkouts([]); - setPhase('save'); - } catch (error) { - setErrorMessage(error instanceof Error ? error.message : 'Unknown error'); - } finally { - setIsSaving(false); - setSaveProgress(null); - } - }; - - const resetToUpload = () => { - setPhase('upload'); - setImages([]); - setEditableWorkouts([]); - setExpandedWorkouts(new Set()); - setSuccess(null); - setErrorMessage(null); - }; - - // Phase 1: Upload - if (phase === 'upload') { - if (!hasApiKey) { - return ( -
-
-
-
- -
-

- Claude API Key Required -

-

- Configure your Claude API key in Settings to use import. -

- - Go to Settings - -
-
-
-
-
- ); - } - - return ( -
-
-
-

Import Workouts

-

- Upload images of your workout to have Claude automatically parse them. -

-
- -
- {/* Drag and Drop Zone */} -
- -

- Drop your workout images here -

-

- or use the button below to select files -

- -
- - {/* Image Previews */} - {images.length > 0 && ( -
-

- Selected Images ({images.length}) -

-
- {images.map((img) => ( -
- Preview - -
- ))} -
-
- )} - - {/* Error Message */} - {errorMessage && ( -
-

{errorMessage}

-
- )} - - {/* Analyze Button */} - -
-
-
- ); - } - - // Phase 2: Review & Edit - if (phase === 'review') { - return ( -
-
-
-

Review Workouts

-

- Verify and edit the parsed workout data before saving. -

-
- - {/* Error Message */} - {errorMessage && ( -
-

{errorMessage}

-
- )} - - {/* Workouts */} -
- {editableWorkouts.map((workout) => { - const isExpanded = expandedWorkouts.has(workout.id); - const confidenceColor = - workout.overallConfidence === 'high' - ? 'bg-green-900/20 border-green-800' - : workout.overallConfidence === 'medium' - ? 'bg-amber-900/20 border-amber-800' - : 'bg-red-900/20 border-red-800'; - - return ( -
- {/* Workout Header */} -
- -
- - {/* Expanded Content */} - {isExpanded && ( -
- {/* Workout Fields */} -
-
- - - updateWorkoutField(workout.id, 'date', e.target.value) - } - className="w-full px-3 py-2 bg-zinc-800 border border-zinc-700 rounded text-white placeholder-zinc-500 focus:outline-none focus:border-zinc-600" - /> -
-
- - - updateWorkoutField(workout.id, 'name', e.target.value) - } - placeholder="e.g., Upper Body Day" - className="w-full px-3 py-2 bg-zinc-800 border border-zinc-700 rounded text-white placeholder-zinc-500 focus:outline-none focus:border-zinc-600" - /> -
-
- - {/* Warnings */} - {workout.warnings && workout.warnings.length > 0 && ( -
-

- Warnings: -

-
    - {workout.warnings.map((warning, idx) => ( -
  • - • {warning} -
  • - ))} -
-
- )} - - {/* Exercises */} -
- {workout.exercises.map((exercise) => ( -
- {/* Exercise Header */} -
-
- - - updateExerciseField( - workout.id, - exercise.id, - 'name', - e.target.value - ) - } - className="w-full px-3 py-2 bg-zinc-700 border border-zinc-600 rounded text-white placeholder-zinc-500 focus:outline-none focus:border-zinc-500" - /> -
- -
- - {/* Uncertain Warning */} - {exercise.uncertain && ( -
- -

- {exercise.uncertainReason || - 'Please verify this exercise name'} -

-
- )} - - {/* Sets */} -
- {exercise.sets.map((set, setIdx) => ( -
-
- - Set {setIdx + 1} - - -
- -
- {/* Reps */} -
- - - updateSetField( - workout.id, - exercise.id, - set.id, - 'reps', - e.target.value ? parseInt(e.target.value) : null - ) - } - className="w-full px-2 py-1 bg-zinc-600 border border-zinc-500 rounded text-white text-sm placeholder-zinc-400 focus:outline-none focus:border-zinc-400" - /> -
- - {/* Weight */} -
- - - updateSetField( - workout.id, - exercise.id, - set.id, - 'weight', - e.target.value ? parseFloat(e.target.value) : null - ) - } - className="w-full px-2 py-1 bg-zinc-600 border border-zinc-500 rounded text-white text-sm placeholder-zinc-400 focus:outline-none focus:border-zinc-400" - /> -
- - {/* Duration */} -
- - - updateSetField( - workout.id, - exercise.id, - set.id, - 'durationSeconds', - seconds - ) - } - /> -
- - {/* Distance */} -
- - - updateSetField( - workout.id, - exercise.id, - set.id, - 'distance', - e.target.value ? parseFloat(e.target.value) : null - ) - } - className="w-full px-2 py-1 bg-zinc-600 border border-zinc-500 rounded text-white text-sm placeholder-zinc-400 focus:outline-none focus:border-zinc-400" - /> -
- - {/* RPE */} -
- - - updateSetField( - workout.id, - exercise.id, - set.id, - 'rpe', - e.target.value ? parseInt(e.target.value) : null - ) - } - className="w-full px-2 py-1 bg-zinc-600 border border-zinc-500 rounded text-white text-sm placeholder-zinc-400 focus:outline-none focus:border-zinc-400" - /> -
- - {/* Calories */} -
- - - updateSetField( - workout.id, - exercise.id, - set.id, - 'calories', - e.target.value ? parseInt(e.target.value) : null - ) - } - className="w-full px-2 py-1 bg-zinc-600 border border-zinc-500 rounded text-white text-sm placeholder-zinc-400 focus:outline-none focus:border-zinc-400" - /> -
- - {/* Notes */} -
- - - updateSetField( - workout.id, - exercise.id, - set.id, - 'notes', - e.target.value || null - ) - } - className="w-full px-2 py-1 bg-zinc-600 border border-zinc-500 rounded text-white text-sm placeholder-zinc-400 focus:outline-none focus:border-zinc-400" - /> -
-
-
- ))} -
- - {/* Add Set Button */} - -
- ))} -
- - {/* Add Exercise Button */} - - - {/* Remove Workout Button */} - -
- )} -
- ); - })} -
- - {/* Action Buttons */} -
- - -
-
-
- ); - } - - // Phase 3: Save Success - return ( -
-
-
- -

Workouts Saved!

-

- Successfully imported {saveSuccess?.ids.length || 0} workout(s). -

- - {saveSuccess?.ids && saveSuccess.ids.length > 0 && ( -
- {saveSuccess.ids.map((id) => ( - - View Workout - - ))} -
- )} - - -
-
-
- ); -}; - -// Helper component for duration input -const DurationInput: React.FC<{ - value: number | null; - onChange: (seconds: number | null) => void; -}> = ({ value, onChange }) => { - const [minutes, setMinutes] = React.useState( - value ? Math.floor(value / 60).toString() : '' - ); - const [seconds, setSeconds] = React.useState( - value ? (value % 60).toString() : '' - ); - - const handleChange = (min: string, sec: string) => { - setMinutes(min); - setSeconds(sec); - - const minNum = min ? parseInt(min) : 0; - const secNum = sec ? parseInt(sec) : 0; - const totalSeconds = minNum * 60 + secNum; - - onChange(totalSeconds || null); - }; - - return ( -
- handleChange(e.target.value, seconds)} - placeholder="0" - className="w-2/3 px-2 py-1 bg-zinc-600 border border-zinc-500 rounded text-white text-sm placeholder-zinc-400 focus:outline-none focus:border-zinc-400" - /> - : - handleChange(minutes, e.target.value)} - placeholder="0" - className="w-2/3 px-2 py-1 bg-zinc-600 border border-zinc-500 rounded text-white text-sm placeholder-zinc-400 focus:outline-none focus:border-zinc-400" - /> -
- ); -}; - -export default WorkoutImportClient; diff --git a/proof-of-work/components/settings/SettingsForm.tsx b/proof-of-work/components/settings/SettingsForm.tsx index 1f804e1..f397fcc 100644 --- a/proof-of-work/components/settings/SettingsForm.tsx +++ b/proof-of-work/components/settings/SettingsForm.tsx @@ -4,8 +4,6 @@ import { useState, useEffect, useRef } from "react"; import { User } from "@prisma/client"; import { Loader2, - Eye, - EyeOff, Upload, Download, AlertTriangle, @@ -17,20 +15,16 @@ interface UserPreferences { theme: string; defaultWeightUnit: string; defaultRestSeconds: number; - enableClaudeAI: boolean; - claudeApiKey?: string; } export default function SettingsForm({ user }: { user: User }) { const [loading, setLoading] = useState(false); const [error, setError] = useState(null); const [success, setSuccess] = useState(false); - const [showApiKey, setShowApiKey] = useState(false); const [preferences, setPreferences] = useState({ theme: "system", defaultWeightUnit: "lbs", defaultRestSeconds: 90, - enableClaudeAI: false, }); useEffect(() => { @@ -179,90 +173,12 @@ export default function SettingsForm({ user }: { user: User }) { - {/* Claude AI Section */} -
-

- Claude AI Integration -

-

- Enable Claude AI to get personalized workout recommendations and - program optimization suggestions. -

- -
- {/* Enable Toggle */} -
- - Enable Claude AI - - -
- - {/* API Key Input - Only show if enabled */} - {preferences.enableClaudeAI && ( -
- -
- - setPreferences((prev) => ({ - ...prev, - claudeApiKey: e.target.value, - })) - } - placeholder="sk-..." - className="w-full px-3 py-2 border border-zinc-700 rounded-lg bg-zinc-800 text-white focus:outline-none focus:ring-2 focus:ring-white/20 pr-10" - /> - -
-

- Get your API key from{" "} - - console.anthropic.com - -

-
- )} -
-
+ {/* AI integration section deliberately removed — was a misleading + placeholder for "Claude AI workout recommendations" that the + codebase never actually delivered. A real model-agnostic AI + integration (Claude / OpenAI / Gemini / self-hosted Ollama) is + on the roadmap; the underlying enableClaudeAI/claudeApiKey + schema columns stay as harmless dead fields until that lands. */} {/* Save Button */}