Strapsr Local host

This commit is contained in:
Gigih_palatangara 2026-04-01 18:24:40 +07:00
commit 8ad58f7beb
103 changed files with 42131 additions and 0 deletions

43
.gitignore vendored Normal file
View File

@ -0,0 +1,43 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.*
.yarn/*
!.yarn/patches
!.yarn/plugins
!.yarn/releases
!.yarn/versions
# testing
/coverage
# next.js
/.next/
/out/
# production
/build
# misc
.DS_Store
*.pem
# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
.pnpm-debug.log*
# env files (can opt-in for committing if needed)
.env*
# vercel
.vercel
# typescript
*.tsbuildinfo
next-env.d.ts
/app/generated/prisma

388
DOKUMENTASI_APP_FOLDER.md Normal file
View File

@ -0,0 +1,388 @@
# Dokumentasi Struktur Folder `app/`
## Gambaran Umum
Folder `app/` adalah direktori utama aplikasi Next.js yang menggunakan App Router. Folder ini berisi semua halaman, API routes, dan komponen utama aplikasi pelatihan kebugaran "Straps".
---
## 📁 Struktur Direktori
### 1. **`api/` - API Routes**
Folder yang berisi semua endpoint backend untuk komunikasi antara client dan server.
#### **`api/auth/[...nextauth]/`**
- **Fungsi**: Menangani autentikasi pengguna menggunakan NextAuth.js
- **Endpoint**: `/api/auth/*`
- **Fitur**: Login, logout, session management
#### **`api/clients/`**
- **Fungsi**: Endpoint untuk mengelola data klien
- **Operasi**: CRUD operations untuk client data
#### **`api/coach/link-client/`**
- **File**: `route.ts`
- **Fungsi**: Menghubungkan pelatih (coach) dengan klien mereka
- **Method**: POST untuk membuat hubungan coach-client
#### **`api/logs/`**
- **File**: `route.ts`
- **Fungsi**: Mencatat dan mengambil log aktivitas training
- **Method**: GET (mengambil logs), POST (membuat log baru)
#### **`api/menus/`**
- **File**: `route.ts`
- **Fungsi**: Mengelola menu latihan (workout programs)
- **Method**: GET (semua menu), POST (buat menu baru)
##### **`api/menus/[id]/`**
- **File**: `route.ts`
- **Fungsi**: Operasi pada menu spesifik berdasarkan ID
- **Method**: GET (detail menu), PUT (update), DELETE (hapus)
#### **`api/recap/`**
- **File**: `route.ts`
- **Fungsi**: Menyimpan dan mengambil ringkasan latihan (training recap)
- **Method**: GET (semua recap), POST (simpan recap baru)
- **Data**: Termasuk skor form, detail per-rep, dan statistik latihan
##### **`api/recap/[id]/`**
- **File**: `route.ts`
- **Fungsi**: Mengakses recap latihan spesifik
- **Method**: GET (detail recap), DELETE (hapus recap)
#### **`api/register/`**
- **File**: `route.ts`
- **Fungsi**: Registrasi pengguna baru
- **Method**: POST untuk membuat akun baru (Coach atau Client)
- **Validasi**: Email unik, password hashing
#### **`api/status/`**
- **Fungsi**: Health check endpoint untuk monitoring aplikasi
- **Method**: GET untuk cek status server
#### **`api/users/`**
- **File**: `route.ts`
- **Fungsi**: Mengelola data pengguna
- **Method**: GET (daftar users), POST (buat user)
##### **`api/users/[id]/`**
- **File**: `route.ts`
- **Fungsi**: Operasi pada user spesifik
- **Method**: GET (detail user), PUT (update), DELETE (hapus)
---
### 2. **`client/` - Halaman Klien**
Folder untuk semua halaman yang diakses oleh pengguna dengan role "Client".
#### **`client/page.tsx`**
- **Route**: `/client`
- **Fungsi**: Dashboard utama klien
- **Fitur**:
- Menampilkan program latihan yang ditugaskan
- Tombol navigasi ke mode Training, Free Mode, dan Monitor
- Informasi pelatih yang ditugaskan
#### **`client/free/`**
##### **`client/free/page.tsx`**
- **Route**: `/client/free`
- **Fungsi**: Mode latihan bebas (Personal Menu)
- **Fitur**:
- Builder untuk membuat menu latihan custom
- Sistem berbasis "Round" (Set grouping)
- Pilihan exercise terbatas ke 7 latihan inti
- Penyimpanan ke localStorage
- Duplicate round untuk efisiensi
#### **`client/monitor/`**
##### **`client/monitor/page.tsx`**
- **Route**: `/client/monitor`
- **Fungsi**: Monitor real-time untuk form exercise
- **Fitur**:
- Live camera feed dengan pose detection
- Visual feedback untuk form quality
- Counter repetisi real-time
- Tidak ada target/assignment
#### **`client/training/`**
##### **`client/training/page.tsx`**
- **Route**: `/client/training`
- **Fungsi**: Mode latihan utama dengan program assigned
- **Fitur**:
- AI-powered form tracking per rep
- Automatic rep counting
- Rest timer antar set
- **Training Recap**:
- Summary statistik (total sets, total reps)
- Table detail per set dengan:
- Form score average
- Expandable per-rep breakdown
- Grade labels (Excellent/Good/Fair/Needs Improvement)
- Feedback text spesifik per rep (e.g., "Knees Inward")
- Toggle antara Assigned Program dan Personal Menu
- Reset functionality
---
### 3. **`coach/` - Halaman Pelatih**
Folder untuk semua halaman yang diakses oleh pengguna dengan role "Coach".
#### **`coach/dashboard/`**
##### **`coach/dashboard/page.tsx`**
- **Route**: `/coach/dashboard`
- **Fungsi**: Dashboard utama pelatih
- **Fitur**:
- Daftar klien yang terhubung
- Link Client functionality
- Navigasi ke menu management
#### **`coach/menu/`**
Folder untuk manajemen menu latihan.
##### **`coach/menu/[id]/`**
- **Route**: `/coach/menu/[id]`
- **Fungsi**: Edit menu latihan yang sudah ada
- **Fitur**:
- Form editor untuk menu details
- Exercise composer dengan round system
- Preview menu structure
- Update dan delete operations
##### **`coach/menu/new/`**
- **File**: `page.tsx`
- **Route**: `/coach/menu/new`
- **Fungsi**: Membuat menu latihan baru
- **Fitur**:
- Round-based exercise builder
- Input untuk:
- Nama menu
- Target reps per exercise
- Weight (beban)
- Rest time
- Pilihan 7 exercise core
- Duplicate round
- Group sets by exercise
- Auto-save to database
#### **`coach/recap/`**
##### **`coach/recap/[id]/`**
- **Route**: `/coach/recap/[id]`
- **Fungsi**: Melihat detail recap latihan klien
- **Fitur**:
- Visualisasi statistik klien
- Form quality analysis
- Historical performance tracking
---
### 4. **`debug/`**
- **Fungsi**: Halaman untuk debugging dan testing
- **Status**: Development/testing only
- **Akses**: Biasanya disabled di production
---
### 5. **`generated/` - Prisma Client**
Folder yang di-generate otomatis oleh Prisma ORM.
#### **`generated/client/`**
- **File-file utama**:
- `client.ts` - Prisma Client instance
- `browser.ts` - Browser-compatible client
- `models.ts` - Type definitions untuk database models
- `enums.ts` - Enum definitions
- `commonInputTypes.ts` - Input type definitions
- `libquery_engine-*.so.node` - Native query engine binary
- **Folder `internal/`**: Internal Prisma helpers
- **Folder `models/`**: Model type definitions
**PENTING**: ❌ Jangan edit manual, akan di-regenerate saat `prisma generate`
---
### 6. **`lib/` (Symlink)**
- **Fungsi**: Symlink ke `/lib` di root project
- **Isi**: Helper functions dan utility libraries
- **Contoh isi**:
- `pose/HARCore.ts` - Human Activity Recognition core
- `pose/RehabCore.ts` - Exercise recognition dan rep counting
- Form analysis algorithms
---
### 7. **`login/`**
- **File**: Kemungkinan `page.tsx` (route file)
- **Route**: `/login`
- **Fungsi**: Halaman login untuk semua pengguna
- **Fitur**:
- Form email dan password
- Authentication via NextAuth
- Redirect ke dashboard sesuai role
---
### 8. **`register/`**
- **File**: `page.tsx`
- **Route**: `/register`
- **Fungsi**: Halaman registrasi pengguna baru
- **Fitur**:
- Form input: nama, email, password
- Pilihan role (Coach/Client)
- Validasi input
- Submit ke `/api/register`
---
## 📄 File Konfigurasi Root
### **`favicon.ico`**
- **Fungsi**: Icon aplikasi yang muncul di browser tab
- **Format**: ICO file
- **Size**: 25.9 KB
### **`globals.css`**
- **Fungsi**: Global stylesheet untuk seluruh aplikasi
- **Isi**:
- Reset CSS
- Tailwind CSS directives (@tailwind base, components, utilities)
- Custom color variables
- Global typography styles
- **Size**: ~773 bytes
### **`layout.tsx`**
- **Fungsi**: Root layout component (App-wide wrapper)
- **Fitur**:
- HTML structure (`<html>`, `<body>`)
- Metadata configuration
- Font loading (biasanya)
- Global providers (Session, Theme, etc.)
- **Applies to**: Semua route di aplikasi
- **Size**: ~757 bytes
### **`page.tsx`**
- **Route**: `/` (Homepage)
- **Fungsi**: Landing page aplikasi
- **Fitur**:
- Welcome screen
- Login/Register CTA
- Informasi aplikasi
- Redirect logic berdasarkan auth status
- **Size**: ~15 KB
---
## 🔑 Poin Penting untuk Dokumentasi
### **Routing Convention (Next.js App Router)**
- `page.tsx` → Halaman yang dapat diakses
- `route.ts` → API endpoint
- `layout.tsx` → Layout wrapper
- `[id]/` → Dynamic route parameter
- `[...slug]/` → Catch-all route
### **Role-Based Access**
- `/client/*` → Hanya untuk role "Client"
- `/coach/*` → Hanya untuk role "Coach"
- `/api/*` → Backend endpoints (internal)
### **Key Features**
1. **AI Form Analysis**: Real-time pose detection dan rep counting
2. **Per-Rep Tracking**: Detail skor form untuk setiap repetisi
3. **Round System**: Exercise grouping untuk efisiensi workout builder
4. **Training Recap**: Dashboard komprehensif pasca-latihan
### **Tech Stack**
- **Framework**: Next.js 16 (App Router)
- **Database**: PostgreSQL via Prisma ORM
- **Auth**: NextAuth.js
- **UI**: TailwindCSS
- **AI**: MediaPipe Pose Landmarker
- **Language**: TypeScript
---
## 📊 Diagram Struktur
```
app/
├── api/ # Backend Endpoints
│ ├── auth/
│ ├── coach/
│ ├── logs/
│ ├── menus/
│ ├── recap/
│ ├── register/
│ └── users/
├── client/ # Client Dashboard & Features
│ ├── free/ # Personal Menu Builder
│ ├── monitor/ # Live Form Monitor
│ ├── training/ # Assigned Training Mode
│ └── page.tsx # Client Dashboard
├── coach/ # Coach Dashboard & Tools
│ ├── dashboard/
│ ├── menu/
│ └── recap/
├── generated/ # Prisma Auto-Generated (DO NOT EDIT)
├── lib/ # Utilities (Symlink)
├── login/ # Login Page
├── register/ # Registration Page
├── favicon.ico
├── globals.css # Global Styles
├── layout.tsx # Root Layout
└── page.tsx # Homepage
```
---
## 💡 Tips Maintenance
1. **Jangan edit** folder `generated/` secara manual
2. **Run** `prisma generate` setelah schema change
3. **API routes** mengikuti REST convention
4. **Per-rep data** disimpan dalam struktur nested di recap
5. **Feedback text** di-capture frame-by-frame lalu dianalisa untuk dominan message

603
DOKUMENTASI_LIB_FOLDER.md Normal file
View File

@ -0,0 +1,603 @@
# Dokumentasi Struktur Folder `lib/`
## Gambaran Umum
Folder `lib/` berisi semua utility libraries, helper functions, dan core logic aplikasi. Ini adalah "otak" dari aplikasi yang menangani AI pose detection, exercise recognition, authentication, dan database operations.
---
## 📁 Struktur Direktori
```
lib/
├── auth.tsx # Authentication Context & Utilities
├── mediapipe-shim.js # MediaPipe Polyfill
├── prisma.ts # Prisma Client Instance
├── pose/ # AI & Exercise Recognition Engine
│ ├── ExerciseRules.ts # Exercise Configuration & Rules
│ ├── HARCore.ts # Human Activity Recognition Core
│ ├── MathUtils.ts # Mathematical Utilities
│ ├── RehabCore.ts # Exercise Recognition & Counting
│ ├── RehabFSM.ts # Finite State Machines for Reps
│ ├── RepetitionCounter.ts # Legacy Rep Counter
│ └── XGBoostPredictor.ts # ML Model Predictor
├── prisma/ # Prisma Generated Client (Symlink)
└── prisma-gen/ # Prisma Generated Types
```
---
## 📄 File Root Level
### **`auth.tsx`**
**Fungsi**: Context Provider untuk authentication dan session management
**Isi Utama**:
- **Interface**:
- `User`: Type definition untuk user object (id, name, role, coach_id)
- `AuthContextType`: Type untuk auth context
- `UserRole`: 'COACH' | 'CLIENT'
- **Context**:
- `AuthContext`: React Context untuk state auth global
- `AuthProvider`: Provider component yang wrap seluruh app
- **Methods**:
- `login(userId: string)`: Login user berdasarkan ID, fetch data dari `/api/users/[id]`
- `logout()`: Clear session, hapus localStorage, reset state
- Auto-load session dari localStorage saat app mount
**Tech**: React Context API, localStorage persistence
**Size**: ~1.8 KB
---
### **`mediapipe-shim.js`**
**Fungsi**: Polyfill/Shim untuk MediaPipe compatibility
**Isi**:
- Menyediakan global objects yang dibutuhkan MediaPipe di browser
- Mengatasi module resolution issues
- Bridge antara MediaPipe WASM dan JavaScript environment
**Use Case**: Diload sebelum MediaPipe library untuk ensure compatibility
**Size**: ~381 bytes
---
### **`prisma.ts`**
**Fungsi**: Singleton instance dari Prisma Client untuk database operations
**Isi**:
```typescript
import { PrismaClient } from "../app/generated/client/client";
const globalForPrisma = global as unknown as { prisma: PrismaClient };
export const prisma = globalForPrisma.prisma || new PrismaClient();
if (process.env.NODE_ENV !== "production") globalForPrisma.prisma = prisma;
```
**Logic**:
- Menggunakan singleton pattern untuk avoid multiple instances
- Di development, menyimpan instance di `global` untuk hot-reload compatibility
- Di production, create new instance sekali saja
**Import Path**: `@/lib/prisma`
**Size**: ~300 bytes
---
## 📁 Folder `pose/` - AI Exercise Recognition Engine
Ini adalah core engine yang mendeteksi dan menganalisis gerakan exercise menggunakan AI dan heuristics.
### **`ExerciseRules.ts`**
**Fungsi**: Konfigurasi dan aturan untuk setiap jenis latihan
**Exports**:
1. **`interface Landmark`**
- Representasi titik pose dari MediaPipe
- Fields: `x`, `y`, `z`, `visibility?`
2. **`interface ExerciseConfig`**
- Schema konfigurasi untuk setiap exercise
- Fields:
- `name`: Nama display
- `detection`: Kriteria untuk mendeteksi exercise type
- `shoulder_static`: Range angle untuk static shoulder
- `shoulder_down`: Range untuk shoulder down
- `hip_static`: Range untuk hip position
- `phase_type`: 'start_down' | 'start_up' (starting position)
- `dynamic_angles`: Range angle untuk setiap fase movement
- Contoh: `elbow_down: [140, 180]`, `elbow_up: [0, 85]`
- `static_angles`: Ideal angle untuk joints yang harus tetap
- `wrist_distance`: Range jarak pergelangan tangan
- `convex_hull`: Area coverage body saat up/down
3. **`EXERCISE_CONFIGS`** (Constant Object)
- Berisi konfigurasi untuk 7 exercise core:
1. **`bicep_curl`**: Bicep Curl
- Phase: start_down (arms extended)
- Dynamic: Elbow flexion 140-180° (down) → 0-85° (up)
- Shoulder harus static (0-30°)
2. **`hammer_curl`**: Hammer Curl
- Similar to bicep curl
- Wrist distance lebih ketat (0-0.2m)
3. **`shoulder_press`**: Overhead Press
- Phase: start_down (shoulders)
- Elbow: 20-100° (down) → 150-180° (up)
- Hip harus tetap 165° (standing straight)
4. **`lateral_raises`**: Lateral Raises
- Phase: start_down (arms at sides)
- Shoulder: 0-30° → 80-110° (T-pose)
- Elbow harus tetap straight (140-180°)
5. **`squat`**: Squat
- Phase: start_up (standing)
- Hip/Knee: 160-180° (up) → 50-100° (down)
6. **`deadlift`**: Deadlift
- Phase: start_down (floor)
- Hip: 45-100° → 160-180°
- Elbow tetap straight (170°)
7. **`lunges`**: Lunges
- Phase: start_up
- Knee: 160-180° → 70-110°
**Use Case**: Digunakan oleh `RehabCore` untuk validasi form dan counting reps
**Size**: ~5.1 KB
---
### **`HARCore.ts`**
**Fungsi**: Human Activity Recognition - mendeteksi aktivitas umum (Standing/Sitting/Fall)
**Class**: `HARCore`
**Fields**:
- `predictor: XGBoostPredictor` - ML model untuk klasifikasi
- `rehab: RehabCore` - Exercise counter instance
- `currentExercise: string | null` - Exercise yang sedang ditrack
**Methods**:
1. **`setExercise(name: string)`**
- Set exercise yang akan ditrack
- Normalize nama dari UI ke config key
- Contoh: "Bicep Curl" → "bicep_curl"
2. **`resetParams()`**
- Reset counter tanpa menghapus exercise selection
- Calls `rehab.reset()`
3. **`async process(landmarks, worldLandmarks)`** ⭐ Main Method
- **Input**:
- `landmarks`: Array 33 pose keypoints (normalized)
- `worldLandmarks`: 3D coordinates in meters
- **Output**:
```typescript
{
status: "Standing" | "Sitting" | "Fall Detected",
confidence: number,
exercise: string | null,
reps: number,
feedback: string,
debug: { angles, scores }
}
```
- **Logic**:
1. Extract 141 features dari landmarks
2. Classify activity menggunakan XGBoost
3. Jika exercise active, process dengan RehabCore
4. Return combined result
4. **`extractFeatures(landmarks): number[]`** (Private)
- Extract 141 features:
- **132 Raw Features**: 33 landmarks × 4 (x, y, z, visibility)
- **9 Derived Features**:
- 6 Angles: Left/Right Elbow, Hip, Knee
- 2 Ratios: Shoulder width, Hip width (relative to torso)
- 1 Alignment: Torso vertical cosine similarity
**Tech Stack**:
- XGBoost model untuk activity classification
- Heuristic rules untuk exercise recognition
- Angle calculation dari Math Utils
**Size**: ~5.9 KB
---
### **`RehabCore.ts`**
**Fungsi**: Exercise Recognition & Rep Counting dengan form validation
**Class**: `RehabCore`
**Fields**:
- `counters: { [key: string]: RepFSM[] }` - FSM instances per exercise
- `worldLandmarksCache: Vec3[]` - Cache untuk 3D coordinates
- `DEVIATION_THRESHOLD = 15.0` - Batas toleransi deviasi form (degrees)
**Counter Map**:
```typescript
{
'bicep_curl': [BicepCurlCounter('left'), BicepCurlCounter('right')],
'hammer_curl': [HammerCurlCounter('left'), HammerCurlCounter('right')],
'shoulder_press': [OverheadPressCounter()],
'lateral_raises': [LateralRaiseCounter()],
'squat': [SquatCounter()],
'deadlift': [DeadliftCounter()],
'lunges': [LungeCounter()]
}
```
**Methods**:
1. **`reset()`**
- Reset semua counters ke initial state
- Clear cache
2. **`validateExerciseType(configKey, features): string | null`**
- ⚠️ **6-Way Wrong Exercise Detection**
- Cek apakah pose sesuai dengan exercise yang diharapkan
- Return error message jika salah exercise
- Contoh: Jika program bicep curl tapi user lakukan squat → "Wrong Exercise: Detected lower body"
3. **`calculateDeviation(configKey, features, fsmState): { mae, isDeviating, details }`**
- ⭐ **Per-Rep Form Scoring**
- Hitung MAE (Mean Absolute Error) dari ideal angles
- Compare current angles dengan config ranges
- Return:
- `mae`: Average deviation dalam degrees
- `isDeviating`: Boolean (> DEVIATION_THRESHOLD)
- `details`: Array pesan koreksi spesifik
4. **`process(exerciseName, landmarks, worldLandmarks, frameTime)`** ⭐ Main Method
- **Core rep counting logic**
- **Workflow**:
1. Normalize exercise name
2. Lazy-load counters jika belum ada
3. Compute features (angles, distances, dll)
4. **Wrong Exercise Detection**
5. Update FSM untuk bilateral sides (left/right)
6. **Form Deviation Analysis** (per frame)
7. Generate composite feedback
- **Output**:
```typescript
{
left: { stage, angle, reps },
right: { stage, angle, reps },
scores: { deviation_mae },
feedback: "L: UP | R: DOWN | Knees Inward 🔴"
}
```
5. **`getReps(exName): number`**
- Get total reps (max dari left/right atau sum)
- Untuk bilateral: ambil yang lebih besar
- Untuk unilateral: langsung return
**Key Features**:
- ✅ Real-time rep detection menggunakan FSM
- ✅ Form quality scoring (MAE calculation)
- ✅ Wrong exercise detection
- ✅ Specific corrective feedback per frame
- ✅ Bilateral tracking (left/right independent)
**Size**: ~16.1 KB
---
### **`RehabFSM.ts`**
**Fungsi**: Finite State Machines untuk setiap jenis exercise
**Exports**:
1. **Helper Functions**:
- `vec3(landmark): Vec3` - Convert Landmark to 3D vector
- `computeFeatures(landmarks, worldLandmarks): PoseFeatures`
- Extract semua angles dan distances
- Return object dengan 20+ features
2. **Base Class**: `RepFSM` (Abstract)
- Fields:
- `state: "LOW" | "HIGH"`
- `reps: number`
- `lastAngle: number`
- Methods:
- `abstract shouldTransition(features): boolean`
- `update(features): void`
- `reset(): void`
3. **Concrete FSMs** (7 Classes):
**`BicepCurlCounter(side: 'left' | 'right')`**
- Track elbow flexion angle
- LOW (140-180°) ↔ HIGH (0-85°)
- +1 rep saat kembali ke LOW
**`HammerCurlCounter(side: 'left' | 'right')`**
- Similar to Bicep, sedikit berbeda thresholds
**`OverheadPressCounter()`**
- Bilateral (min of left/right elbow)
- LOW: hands at shoulders
- HIGH: arms extended overhead
**`LateralRaiseCounter()`**
- Track shoulder abduction (max of left/right)
- LOW: arms at sides → HIGH: T-pose
**`SquatCounter()`**
- Track hip/knee flexion (min)
- HIGH: standing → LOW: squat depth
**`DeadliftCounter()`**
- Track hip extension
- LOW: floor position → HIGH: lockout
**`LungeCounter()`**
- Track knee flexion (min of both)
- Similar to squat
**Logic Pattern**:
```typescript
if (state === "LOW" && angle < UP_THRESHOLD) {
state = "HIGH";
} else if (state === "HIGH" && angle > DOWN_THRESHOLD) {
state = "LOW";
reps++; // Rep completed!
}
```
**Size**: Variable (likely 5-10 KB based on 7 classes)
---
### **`MathUtils.ts`**
**Fungsi**: Mathematical utility functions untuk pose analysis
**Functions**:
1. **`calculateAngle(a, b, c): number`**
- Hitung angle di point B dari triangle ABC
- Input: 3 points dengan {x, y}
- Output: Angle dalam degrees (0-180)
- Formula: `arctangent` menggunakan vectors
2. **`calculateRangeDeviation(value, range, weight = 1.0): number`**
- Hitung deviasi dari ideal range
- Jika value dalam range → return 0
- Jika di luar → return distance × weight
- Contoh: `value=90, range=[80,100]` → return 0
- Contoh: `value=110, range=[80,100]` → return 10
3. **`computeMAE(deviations): number`**
- Mean Absolute Error
- Average dari array deviasi
- Digunakan untuk overall form score
**Use Case**:
- Digunakan di semua modules untuk angle calculation
- Form validation dan scoring
**Size**: ~500 bytes (estimasi)
---
### **`XGBoostPredictor.ts`**
**Fungsi**: XGBoost Machine Learning model predictor untuk activity classification
**Class**: `XGBoostPredictor`
**Purpose**:
- Classify pose sebagai "Standing", "Sitting", atau "Fall Detected"
- Menggunakan pre-trained XGBoost model
**Methods**:
- `predict(features: number[]): number[]`
- Input: 141 features dari `HARCore.extractFeatures()`
- Output: Probability array [standingProb, sittingProb, fallProb]
- Model di-load dari embedded weights
**Tech**:
- XGBoost model serialized ke JavaScript
- Likely menggunakan ONNX atau custom JSON format
**Size**: Likely large (10-50 KB) karena contain model weights
---
### **`RepetitionCounter.ts`**
**Fungsi**: Legacy rep counter (kemungkinan deprecated)
**Status**: ⚠️ Likely tidak digunakan, digantikan oleh `RehabFSM.ts`
**Reason**:
- `RehabCore` menggunakan FSM dari `RehabFSM.ts`
- File ini mungkin versi lama sebelum refactor
---
## 📁 Folder `prisma/` dan `prisma-gen/`
**Status**: Auto-generated by Prisma CLI
### **`prisma/client/`**
- Symlink atau copy dari `/app/generated/client`
- Prisma Client untuk browser compatibility
### **`prisma-gen/`**
- Type definitions dan models
- Files:
- `browser.ts` - Browser-compatible client
- `client.ts` - Main client
- `models.ts` - Type definitions
- `enums.ts` - Enum types
- `models/` - Individual model files:
- `activity_logs.ts`
- `training_menus.ts`
- `user_recaps.ts`
**PENTING**: ❌ **JANGAN EDIT MANUAL** - Will be regenerated by `prisma generate`
---
## 🔑 Key Concepts & Data Flow
### **Complete Exercise Recognition Flow**:
```
1. MediaPipe Pose Detection
└─> 33 Landmarks (x, y, z, visibility)
2. HARCore.process()
├─> Extract 141 Features
├─> XGBoost Prediction (Standing/Sitting/Fall)
└─> RehabCore.process()
├─> Compute Angles & Features
├─> Wrong Exercise Detection
├─> FSM Update (Rep Counting)
├─> Form Deviation Calculation (MAE)
└─> Generate Feedback Text
3. Return to UI
└─> { status, reps, feedback, scores: {deviation_mae} }
```
### **Per-Rep Tracking Flow** (NEW):
```
Training Page (client/training/page.tsx)
├─> Frame Loop: predictWebcam()
│ ├─> Call har.process()
│ ├─> Accumulate MAE to repBuffer
│ └─> Capture feedback to repFeedbackBuffer
├─> Rep Completion Detection (res.reps > lastRepCount)
│ ├─> Calculate avgRepScore
│ ├─> Find dominant feedback
│ └─> Push to currentSetReps[{rep, score, feedback}]
└─> Set Completion
├─> Save to results state
└─> API POST to /api/recap
```
---
## 💡 Important Notes
### **Exercise Configuration**:
- Semua config di `ExerciseRules.ts`
- Untuk tambah exercise baru:
1. Add config di `EXERCISE_CONFIGS`
2. Create FSM class di `RehabFSM.ts`
3. Add entry di `COUNTER_MAP` di `RehabCore.ts`
### **Form Scoring**:
- MAE (Mean Absolute Error) dalam degrees
- Thresholds:
- < 8° = **Excellent**
- < 15° = **Good**
- < 25° = **Fair**
- ≥ 25° = **Needs Improvement**
### **Wrong Exercise Detection**:
- 6-way classification:
1. Upper body dynamic (curls/press)
2. Upper body static T-pose (lateral raises)
3. Lower body squat pattern
4. Lower body hinge pattern (deadlift)
5. Lower body lunge pattern
6. Unrecognized pose
### **Performance**:
- Frame processing: ~10-30ms
- Feature extraction: O(n) dengan n=33 landmarks
- FSM updates: O(1) per exercise
- Real-time capable at 30 FPS
---
## 📊 Dependencies
```
lib/pose/
├── ExerciseRules.ts (Pure config, no deps)
├── MathUtils.ts (Pure math, no deps)
├── RehabFSM.ts
│ └── depends on: ExerciseRules, MathUtils
├── RehabCore.ts
│ └── depends on: ExerciseRules, RehabFSM, MathUtils
├── XGBoostPredictor.ts (Standalone ML model)
└── HARCore.ts
└── depends on: ALL above
```
```
lib/ (Root)
├── auth.tsx (React Context)
├── prisma.ts (Database client)
└── mediapipe-shim.js (Standalone polyfill)
```
---
## 🛠️ Maintenance Tips
1. **Update Exercise Config**: Edit `ExerciseRules.ts` → Test di Monitor mode
2. **Adjust Thresholds**: Tweak angle ranges di config atau FSM classes
3. **Debug Form Scores**: Check `res.debug.scores` di browser console
4. **Add New Exercise**:
- Step 1: Config di `ExerciseRules.ts`
- Step 2: FSM class di `RehabFSM.ts`
- Step 3: Register di `RehabCore.ts` COUNTER_MAP
- Step 4: Update UI exercise dropdown
5. **Performance Tuning**: Optimize `extractFeatures()` di HARCore jika lag
---
## 📈 Future Enhancements
- [ ] Add more exercises (Pull-ups, Push-ups, etc.)
- [ ] Implement velocity-based training (track rep speed)
- [ ] Add form correction animations/overlays
- [ ] Machine learning untuk auto-tune thresholds per user
- [ ] Export workout data ke CSV/PDF

View File

@ -0,0 +1,795 @@
# Dokumentasi Folder `prisma/`
## Gambaran Umum
Folder `prisma/` berisi semua konfigurasi dan migration files untuk database management menggunakan Prisma ORM. Folder ini adalah "blueprint" dari database schema aplikasi.
---
## 📁 Struktur Direktori
```
prisma/
├── migrations/ # Database Migration History
│ ├── 20251228085634_add_users_model/
│ │ └── migration.sql # Initial database schema
│ ├── 20251228091450_add_menu_client_assignment/
│ │ └── migration.sql # Menu assignment feature
│ └── migration_lock.toml # Migration lock file
├── schema.prisma # Database Schema Definition
└── seed.ts # Database Seeder Script
```
---
## 📄 File Utama
### **`schema.prisma`** ⭐
**Fungsi**: Schema definition file - Blueprint utama database
**Location**: `/prisma/schema.prisma`
**Size**: ~2.6 KB
#### **Generator Configuration**:
```prisma
generator client {
provider = "prisma-client"
output = "../app/generated/client"
}
```
- Generate Prisma Client TypeScript
- Output ke `app/generated/client/` untuk import di aplikasi
#### **Datasource Configuration**:
```prisma
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
```
- Database: PostgreSQL
- Connection string dari environment variable `DATABASE_URL`
---
## 📊 Database Models
### **1. Model `users`** 👥
**Fungsi**: Menyimpan data pengguna (Coach & Client)
**Fields**:
| Field | Type | Description | Constraint |
|-------|------|-------------|------------|
| `id` | String | User ID unik | Primary Key, default: cuid() |
| `name` | String | Nama lengkap | VARCHAR, NOT NULL |
| `role` | String | Role pengguna | VARCHAR, "COACH" atau "CLIENT" |
| `coach_id` | String? | ID coach (untuk client) | Foreign Key → users.id |
| `created_at` | DateTime? | Waktu registrasi | Default: now() |
**Relations**:
- `coach` → Self-relation ke `users` (Many-to-One)
- Client memiliki 1 coach
- `clients` → Self-relation ke `users` (One-to-Many)
- Coach memiliki banyak clients
- `created_menus``training_menus[]` (One-to-Many)
- Coach dapat membuat banyak menu
- `assigned_menus``training_menus[]` (One-to-Many)
- Client dapat memiliki banyak assigned menus
- `recaps``user_recaps[]` (One-to-Many)
- User dapat memiliki banyak workout recaps
- `activity_logs``activity_logs[]` (One-to-Many)
- User dapat memiliki banyak activity logs
**Indexes**:
- `ix_users_id` on `id` (Primary)
- `ix_users_coach_id` on `coach_id` (Query optimization)
**ID Format**:
- Coach: `C00001`, `C00002`, etc.
- Client: `U00001`, `U00002`, etc.
**Business Logic**:
```typescript
// Coach can have multiple clients
Coach (C00001)
└─> Clients: [U00001, U00002]
// Client belongs to one coach
Client (U00001)
└─> Coach: C00001
```
---
### **2. Model `activity_logs`** 📝
**Fungsi**: Log aktivitas real-time (Standing/Sitting/Fall detection)
**Fields**:
| Field | Type | Description |
|-------|------|-------------|
| `id` | Int | Auto-increment ID (Primary Key) |
| `timestamp` | DateTime? | Waktu log dibuat |
| `status` | String? | "Standing", "Sitting", "Fall Detected" |
| `confidence` | String? | Confidence score dari XGBoost |
| `details` | Json? | Additional metadata |
| `user_id` | String? | Foreign Key → users.id |
**Relations**:
- `user``users` (Many-to-One)
**Indexes**:
- `ix_activity_logs_id` on `id`
- `ix_activity_logs_user_id` on `user_id`
**Use Case**:
- Real-time monitoring di `/client/monitor`
- Historical activity tracking
- Fall detection alerts
**Example Data**:
```json
{
"id": 1,
"timestamp": "2025-12-28T10:30:00Z",
"status": "Standing",
"confidence": "0.95",
"details": {
"exercise": "bicep_curl",
"reps": 5
},
"user_id": "U00001"
}
```
---
### **3. Model `training_menus`** 📋
**Fungsi**: Menyimpan workout programs/menus yang dibuat coach
**Fields**:
| Field | Type | Description |
|-------|------|-------------|
| `id` | Int | Auto-increment ID (Primary Key) |
| `name` | String? | Nama menu (e.g., "Upper Body Day 1") |
| `exercises` | Json? | Array of exercise objects |
| `created_at` | DateTime? | Waktu pembuatan |
| `author_id` | String? | Foreign Key → users.id (Coach yang buat) |
| `client_id` | String? | Foreign Key → users.id (Client assigned) |
**Relations**:
- `author``users` (Many-to-One, relation: "CreatedMenus")
- Menu dibuat oleh 1 coach
- `assigned_client``users` (Many-to-One, relation: "AssignedMenus")
- Menu ditugaskan kepada 1 client
- `user_recaps``user_recaps[]` (One-to-Many)
- Menu bisa memiliki banyak recap results
**Indexes**:
- `ix_training_menus_id` on `id`
- `ix_training_menus_name` on `name`
- `ix_training_menus_author_id` on `author_id`
**JSON Structure untuk `exercises` field**:
```json
[
{
"name": "Bicep Curl",
"set_index": 1,
"reps": 10,
"weight": 15,
"rest": 60
},
{
"name": "Hammer Curl",
"set_index": 1,
"reps": 12,
"weight": 12,
"rest": 60
}
]
```
**Business Logic**:
```typescript
// Coach creates menu
Coach (C00001)
└─> Creates Menu (id: 1, "Upper Body")
└─> Assigns to Client (U00001)
// Client sees assigned menu
Client (U00001)
└─> Assigned Menu: "Upper Body" (id: 1)
```
---
### **4. Model `user_recaps`** 📈
**Fungsi**: Menyimpan hasil latihan (training recap) setelah workout selesai
**Fields**:
| Field | Type | Description |
|-------|------|-------------|
| `id` | Int | Auto-increment ID (Primary Key) |
| `menu_id` | Int? | Foreign Key → training_menus.id |
| `user_id` | String? | Foreign Key → users.id |
| `summary` | Json? | Workout summary data |
| `completed_at` | DateTime? | Waktu workout selesai |
**Relations**:
- `training_menus``training_menus` (Many-to-One)
- Recap terkait dengan 1 menu
- `user``users` (Many-to-One)
- Recap milik 1 user
**Indexes**:
- `ix_user_recaps_id` on `id`
- `ix_user_recaps_user_id` on `user_id`
**JSON Structure untuk `summary` field**:
```json
{
"completed": true,
"exercises": [
{
"name": "Bicep Curl",
"set_index": 1,
"reps": 10,
"weight": 15,
"rest": 60
}
],
"timestamp": "2025-12-28T11:00:00Z",
"results": [
{
"name": "Bicep Curl",
"set": 1,
"reps": 10,
"weight": 15,
"score": 12.5,
"repDetails": [
{
"rep": 1,
"score": 8.2,
"feedback": "Perfect"
},
{
"rep": 2,
"score": 14.5,
"feedback": "Elbow moving forward"
}
]
}
]
}
```
**Key Features di Summary**:
- ✅ Overall workout stats
- ✅ Per-set average form scores
- ✅ **Per-rep breakdown** dengan:
- Rep number
- MAE score
- Specific feedback text
---
## 🔄 Database Relationships Diagram
```mermaid
erDiagram
users ||--o{ users : "coach-client"
users ||--o{ training_menus : "creates (author)"
users ||--o{ training_menus : "assigned to (client)"
users ||--o{ user_recaps : "performs workout"
users ||--o{ activity_logs : "generates logs"
training_menus ||--o{ user_recaps : "tracked in recap"
users {
string id PK
string name
string role
string coach_id FK
}
training_menus {
int id PK
string name
json exercises
string author_id FK
string client_id FK
}
user_recaps {
int id PK
int menu_id FK
string user_id FK
json summary
}
activity_logs {
int id PK
string status
string user_id FK
}
```
---
## 📁 Folder `migrations/`
### **Gambaran Umum**
Folder yang berisi history semua perubahan database schema.
### **Migration Files**:
#### **1. `20251228085634_add_users_model/migration.sql`**
**Tanggal**: 28 Desember 2025, 08:56:34
**Fungsi**: Initial database schema creation
**Changes**:
- ✅ Create table `users` dengan self-referencing foreign key
- ✅ Create table `activity_logs` dengan JSONB details
- ✅ Create table `training_menus` dengan JSONB exercises
- ✅ Create table `user_recaps` dengan JSONB summary
- ✅ Create indexes untuk optimization:
- User ID index
- Coach ID index
- Menu name index
- Author ID index
- User recap user_id index
- ✅ Add foreign key constraints dengan proper ON DELETE/UPDATE actions
**Key SQL**:
```sql
CREATE TABLE "users" (
"id" TEXT NOT NULL,
"name" VARCHAR NOT NULL,
"role" VARCHAR NOT NULL,
"coach_id" TEXT,
"created_at" TIMESTAMP(6) DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "users_pkey" PRIMARY KEY ("id")
);
-- Self-referencing foreign key
ALTER TABLE "users"
ADD CONSTRAINT "users_coach_id_fkey"
FOREIGN KEY ("coach_id") REFERENCES "users"("id")
ON DELETE SET NULL ON UPDATE CASCADE;
```
**Size**: ~2.7 KB
---
#### **2. `20251228091450_add_menu_client_assignment/migration.sql`**
**Tanggal**: 28 Desember 2025, 09:14:50
**Fungsi**: (Merged into previous migration)
**Status**: ⚠️ Skipped/No-op migration
**Reason**:
- Original purpose: Add `client_id` to `training_menus`
- Already included in first migration to fix type mismatches
- File kept untuk preserve migration history order
**Content**:
```sql
-- This migration is skipped because its changes (adding client_id)
-- were manually merged into the previous migration to fix type mismatches
-- This file is kept to preserve migration history order.
```
---
#### **`migration_lock.toml`**
**Fungsi**: Lock file untuk ensure consistent database provider
**Content**:
```toml
# Please do not edit this file manually
# It should be added in your version-control system (e.g., Git)
provider = "postgresql"
```
**Purpose**:
- Mencegah accidental switch ke database provider lain
- Ensure semua developer/environment menggunakan PostgreSQL
- Auto-generated oleh Prisma CLI
---
## 📄 File `seed.ts`
**Fungsi**: Database seeder untuk populate initial test data
**Location**: `/prisma/seed.ts`
**Size**: ~1.6 KB
### **Seed Data**:
**2 Coaches**:
1. `C00001` - "Coach One"
2. `C00002` - "Coach Two"
**3 Clients**:
1. `U00001` - "Client One" (assigned to Coach One)
2. `U00002` - "Client Two" (assigned to Coach One)
3. `U00003` - "Client Three" (assigned to Coach Two)
### **Script Logic**:
```typescript
import { PrismaClient } from "../app/generated/client/client";
const prisma = new PrismaClient();
async function main() {
// Upsert coaches
const coach1 = await prisma.users.upsert({
where: { id: "C00001" },
update: {},
create: {
id: "C00001",
name: "Coach One",
role: "COACH",
},
});
// Upsert clients with coach_id
const client1 = await prisma.users.upsert({
where: { id: "U00001" },
update: {},
create: {
id: "U00001",
name: "Client One",
role: "CLIENT",
coach_id: coach1.id,
},
});
}
```
### **Run Seeder**:
```bash
npx prisma db seed
```
**Use Case**:
- Development/testing data
- Demo accounts
- Quick reset database dengan data awal
---
## 🛠️ Prisma Commands
### **Essential Commands**:
#### **1. Generate Prisma Client**
```bash
npx prisma generate
```
- Regenerate TypeScript client dari schema
- Run setelah setiap perubahan `schema.prisma`
- Output ke `app/generated/client/`
#### **2. Create Migration**
```bash
npx prisma migrate dev --name migration_name
```
- Create migration file baru
- Apply migration ke database
- Update Prisma Client
#### **3. Apply Migration (Production)**
```bash
npx prisma migrate deploy
```
- Apply pending migrations
- Untuk production environment
- Tidak auto-generate client
#### **4. Reset Database**
```bash
npx prisma migrate reset
```
- Drop database
- Re-run all migrations
- Run seed script
- ⚠️ **DANGER**: Deletes all data!
#### **5. Prisma Studio (GUI)**
```bash
npx prisma studio
```
- Open web GUI untuk browse/edit data
- URL: `http://localhost:5555`
- Visual database management
#### **6. Format Schema**
```bash
npx prisma format
```
- Auto-format `schema.prisma`
- Fix indentation dan spacing
#### **7. Validate Schema**
```bash
npx prisma validate
```
- Check schema for errors
- Verify relations dan syntax
---
## 🔑 Key Concepts
### **Migration Strategy**:
1. **Development**:
- Use `prisma migrate dev`
- Creates migration + applies + generates client
- Safe untuk experiments
2. **Production**:
- Use `prisma migrate deploy`
- Never use `migrate dev` in prod
- Always test migrations di staging first
### **Schema Best Practices**:
1. **Naming Conventions**:
- Tables: `snake_case` (e.g., `training_menus`)
- Fields: `snake_case` (e.g., `coach_id`)
- Relations: `camelCase` di Prisma model (e.g., `assignedClient`)
2. **Indexes**:
- Add index untuk foreign keys
- Add index untuk frequently queried fields
- Consider composite indexes untuk complex queries
3. **JSONB Usage**:
- ✅ Good for: Flexible nested data (exercises, recap summary)
- ❌ Avoid for: Searchable/filterable data
- Use `Json` type di Prisma, becomes `JSONB` di PostgreSQL
4. **ID Strategy**:
- Users: Custom strings (`C00001`, `U00001`)
- Other tables: Auto-increment integers
- Consider UUID untuk distributed systems
---
## 📊 Database Size Estimates
**Assuming Active Usage**:
| Table | Rows/Month | Storage |
| ---------------- | ---------- | --------------------------- |
| `users` | ~10 | < 1 KB |
| `training_menus` | ~50 | ~5 KB |
| `user_recaps` | ~500 | ~500 KB (with per-rep data) |
| `activity_logs` | ~10,000 | ~1 MB |
**Total**: ~2 MB/month dengan moderate usage
**Optimization Tips**:
- Archive old `activity_logs` after 30 days
- Compress old recaps
- Add pagination untuk large queries
---
## 🔐 Security Considerations
### **Implemented**:
- ✅ Foreign key constraints prevent orphaned data
- ✅ Indexes prevent slow queries (avoid DOS)
- ✅ ON DELETE SET NULL untuk soft deletes
### **TODO / Recommendations**:
- [ ] Add `email` field dengan `@unique` constraint
- [ ] Add `password_hash` field (currently not in schema!)
- [ ] Add `role` as enum type instead of string
- [ ] Add `soft_delete` timestamp instead of hard delete
- [ ] Add row-level security (RLS) di PostgreSQL
---
## 🚀 Future Enhancements
### **Planned Schema Changes**:
1. **Add Authentication Fields**:
```prisma
model users {
email String @unique
password_hash String
email_verified Boolean @default(false)
}
```
2. **Add Workout Sessions** (untuk track progress over time):
```prisma
model workout_sessions {
id Int @id @default(autoincrement())
user_id String
menu_id Int
started_at DateTime
ended_at DateTime?
status String // "IN_PROGRESS" | "COMPLETED" | "ABANDONED"
}
```
3. **Add Exercise Library** (normalize exercises):
```prisma
model exercises {
id Int @id @default(autoincrement())
name String @unique
category String // "UPPER" | "LOWER" | "CORE"
instructions Json?
}
```
4. **Add Personal Records**:
```prisma
model personal_records {
id Int @id @default(autoincrement())
user_id String
exercise String
weight_kg Float
reps Int
achieved_at DateTime
}
```
---
## 💡 Troubleshooting
### **Common Issues**:
#### **1. Migration Failed**
```bash
Error: Migration failed to apply
```
**Solution**:
```bash
# Reset database
npx prisma migrate reset
# Or fix manually
npx prisma db push --skip-generate
npx prisma generate
```
#### **2. Client Out of Sync**
```bash
Error: Prisma Client is out of sync with schema
```
**Solution**:
```bash
npx prisma generate
```
#### **3. Connection Error**
```bash
Error: Can't reach database server
```
**Check**:
1. PostgreSQL running? (`sudo systemctl status postgresql`)
2. `DATABASE_URL` correct di `.env`?
3. Network/firewall issues?
#### **4. Seed Script Fails**
```bash
Error: Unique constraint violation
```
**Solution**: Data already exists
```bash
# Clear database first
npx prisma migrate reset
```
---
## 📚 Resources
**Official Docs**:
- [Prisma Schema Reference](https://www.prisma.io/docs/reference/api-reference/prisma-schema-reference)
- [Prisma Migrate](https://www.prisma.io/docs/concepts/components/prisma-migrate)
- [Prisma Client API](https://www.prisma.io/docs/reference/api-reference/prisma-client-reference)
**Tutorials**:
- [Prisma Quickstart](https://www.prisma.io/docs/getting-started/quickstart)
- [PostgreSQL Best Practices](https://wiki.postgresql.org/wiki/Best_practices)
---
## ✅ Checklist Maintenance
- [x] Schema documented
- [x] Migrations tracked in Git
- [x] Seed data available
- [ ] Backup strategy defined
- [ ] Migration rollback tested
- [ ] Production deployment guide
- [ ] Performance benchmarks

View File

@ -0,0 +1,568 @@
# Dokumentasi Folder `public/`
## Gambaran Umum
Folder `public/` berisi semua static assets yang dapat diakses langsung dari browser tanpa processing. File-file di folder ini di-serve oleh Next.js dari root path `/`.
---
## 📁 Struktur Direktori
```
public/
├── models/ # Machine Learning Models
│ └── xgb_activity_model.json # XGBoost activity classifier (2.1 MB)
├── favicon.svg # App icon (browser tab)
├── file.svg # File icon asset
├── globe.svg # Globe icon asset
├── next.svg # Next.js logo
├── vercel.svg # Vercel logo
└── window.svg # Window icon asset
```
---
## 📄 File-File Utama
### **1. `models/xgb_activity_model.json`** 🤖
**Path**: `/public/models/xgb_activity_model.json`
**Fungsi**: XGBoost Machine Learning Model untuk Human Activity Recognition
**Purpose**:
- Classify pose menjadi 3 kategori:
1. **Standing** (Berdiri)
2. **Sitting** (Duduk)
3. **Fall Detected** (Jatuh terdeteksi)
**Size**: **2.1 MB** (Very Large!)
**Format**: JSON serialization dari XGBoost model
- Berisi tree structures, weights, dan thresholds
- trained model dari Python exported ke JavaScript-compatible format
**How It's Used**:
```typescript
// lib/pose/XGBoostPredictor.ts
import modelData from "../public/models/xgb_activity_model.json";
class XGBoostPredictor {
predict(features: number[]): number[] {
// Load model from JSON
// Execute XGBoost inference
// Return [standingProb, sittingProb, fallProb]
}
}
```
**Access URL**: `http://localhost:3000/models/xgb_activity_model.json`
**Tech Details**:
- Input: 141 features (33 landmarks × 4 + 9 derived)
- Output: 3-class probability distribution
- Algorithm: Gradient Boosted Decision Trees
**Performance**:
- Inference: ~5-10ms per frame
- Memory: ~2 MB loaded in browser
- Accuracy: Depends on training data quality
---
### **2. `favicon.svg`** 🎨
**Path**: `/public/favicon.svg`
**Fungsi**: App icon yang muncul di browser tab
**Size**: ~1.1 KB
**Design**:
- SVG icon dengan gambar dumbbells (barbel)
- Color: Blue (`#1E40AF` - blue-700)
- Viewbox: 24×24
- Rendered size: 48×48 pixels
**SVG Structure**:
```svg
<svg viewBox="0 0 24 24" width="48" height="48">
<!-- Left dumbbell weight -->
<path d="M7.4 7H4.6..." stroke="#1E40AF"/>
<!-- Right dumbbell weight -->
<path d="M19.4 7H16.6..." stroke="#1E40AF"/>
<!-- Center bar -->
<path d="M8 12H16" stroke="#1E40AF"/>
</svg>
```
**Visual**: Representing fitness/strength training
**Browser Display**:
- Favicon di tab browser
- Bookmark icon
- Desktop shortcut icon (PWA)
**Access**: Automatically loaded oleh Next.js dari `<link rel="icon">`
---
### **3. Icon Assets** (SVG Icons)
#### **`file.svg`**
**Size**: ~391 bytes
**Fungsi**: Generic file icon
- Bisa digunakan untuk file upload UI
- Document representation
- Attachment icons
**Use Case**: UI components yang butuh file icon
---
#### **`globe.svg`**
**Size**: ~1 KB
**Fungsi**: Globe/world icon
- Public/internet representation
- Language selection
- Global settings
**Possible Uses**:
- Language switcher (future feature)
- Public profile indicators
- Network status
---
#### **`window.svg`**
**Size**: ~385 bytes
**Fungsi**: Window/application icon
- UI element representation
- Modal/dialog indicators
- Application layout icons
**Use Case**:
- Dashboard widgets
- Window management UI
- Layout switching
---
### **4. Brand Logos**
#### **`next.svg`**
**Size**: ~1.4 KB
**Fungsi**: Next.js official logo
- Used in default Next.js templates
- Brand attribution
- Developer credits
**Current Usage**: Likely not displayed in production
- Default Next.js boilerplate file
- Can be removed if not used
---
#### **`vercel.svg`**
**Size**: ~128 bytes
**Fungsi**: Vercel deployment platform logo
- Hosting provider logo
- Deployment attribution
**Current Usage**: Likely not displayed
- Boilerplate file
- Can be removed if deploying elsewhere
---
## 🔗 How Public Files Are Accessed
### **In Code**:
```typescript
// Direct path from root
<img src="/favicon.svg" alt="Logo" />
// Model import (if using import)
import model from '@/public/models/xgb_activity_model.json';
// Or fetch at runtime
const response = await fetch('/models/xgb_activity_model.json');
const model = await response.json();
```
### **In Browser**:
- `http://localhost:3000/favicon.svg`
- `http://localhost:3000/models/xgb_activity_model.json`
- `http://localhost:3000/next.svg`
**Important**:
- ✅ NO `/public` prefix in URL
- ✅ Files served from root `/`
- ❌ Don't use `/public/favicon.svg` (404!)
---
## 📊 File Analysis
| File | Size | Type | Purpose | Status |
| ------------------------- | ------ | -------- | ----------------------- | ---------------- |
| `xgb_activity_model.json` | 2.1 MB | ML Model | Activity classification | ✅ **Critical** |
| `favicon.svg` | 1.1 KB | Icon | App branding | ✅ **Active** |
| `file.svg` | 391 B | Icon | UI asset | ⚠️ Likely unused |
| `globe.svg` | 1 KB | Icon | UI asset | ⚠️ Likely unused |
| `window.svg` | 385 B | Icon | UI asset | ⚠️ Likely unused |
| `next.svg` | 1.4 KB | Logo | Boilerplate | ⚠️ Can remove |
| `vercel.svg` | 128 B | Logo | Boilerplate | ⚠️ Can remove |
**Total Size**: ~2.1 MB
**Largest File**: `xgb_activity_model.json` (99% of total)
---
## 🎯 Optimization Recommendations
### **1. Model Optimization**
**Current**: 2.1 MB JSON file
**Options**:
- ✅ **Lazy Load**: Only load saat dibutuhkan
- ✅ **Compression**: Gzip/Brotli (automatic by Next.js)
- ✅ **CDN**: Host di CDN untuk faster global access
- ⚠️ **Quantization**: Reduce precision (may affect accuracy)
- ⚠️ **ONNX Format**: Convert ke binary format (smaller)
**Lazy Load Example**:
```typescript
// Load only when needed
const loadModel = async () => {
const model = await import("@/public/models/xgb_activity_model.json");
return model;
};
```
---
### **2. Icon Cleanup** 🧹
**Unused Icons**: Remove jika tidak digunakan
```bash
# Check usage across codebase
grep -r "file.svg" app/
grep -r "globe.svg" app/
grep -r "window.svg" app/
# If no results, safe to delete
rm public/file.svg public/globe.svg public/window.svg
rm public/next.svg public/vercel.svg
```
**Benefit**: Reduce deployment size, faster builds
---
### **3. Add More Assets** 📁
**Recommended Additions**:
#### **`robots.txt`**
```txt
# public/robots.txt
User-agent: *
Allow: /
Sitemap: https://yourapp.com/sitemap.xml
```
#### **`manifest.json`** (PWA)
```json
{
"name": "Straps Fitness",
"short_name": "Straps",
"icons": [
{
"src": "/favicon.svg",
"sizes": "48x48",
"type": "image/svg+xml"
}
],
"theme_color": "#1E40AF",
"background_color": "#000000",
"display": "standalone"
}
```
#### **`sitemap.xml`** (SEO)
```xml
<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
<url>
<loc>https://yourapp.com/</loc>
<priority>1.0</priority>
</url>
</urlset>
```
---
## 🔒 Security Considerations
### **Current Status**: ✅ Safe
**Why**:
- ✅ No sensitive data in public files
- ✅ Model file is read-only data (not executable)
- ✅ SVG files are static images
### **Best Practices**:
1. **Never Put Secrets Here**
- No API keys
- No passwords
- No private data
2. **Validate SVG Files**
- Check for XSS vectors
- Sanitize uploaded SVGs
3. **Model Integrity** 🔐
- Verify model hash before use
- Detect tampering
```typescript
const expectedHash = "sha256-abc123...";
const actualHash = await hashFile("/models/xgb_activity_model.json");
if (actualHash !== expectedHash) {
throw new Error("Model tampered!");
}
```
---
## 🚀 Advanced Usage
### **1. Dynamic Model Loading**
```typescript
// Lazy load untuk reduce initial bundle
const HARCore = dynamic(() => import('@/lib/pose/HARCore'), {
ssr: false,
loading: () => <p>Loading AI Model...</p>
});
```
### **2. Service Worker Caching** (PWA)
```javascript
// Cache model untuk offline use
self.addEventListener("install", (event) => {
event.waitUntil(
caches.open("models-v1").then((cache) => {
return cache.add("/models/xgb_activity_model.json");
}),
);
});
```
### **3. Progressive Enhancement**
```typescript
// Fallback jika model load gagal
let classifier;
try {
classifier = await loadXGBoostModel();
} catch (err) {
console.warn("Model failed to load, using heuristics");
classifier = new FallbackHeuristicClassifier();
}
```
---
## 📈 Performance Metrics
### **Model Loading**:
- **Cold Start**: ~200-500ms (first load, no cache)
- **Warm Start**: ~50-100ms (cached)
- **Memory**: ~2-3 MB in-memory after parsing
### **Optimization Impact**:
```typescript
// Before: Load synchronously (blocks rendering)
import model from "@/public/models/xgb_activity_model.json";
// After: Load async (non-blocking)
const model = await fetch("/models/xgb_activity_model.json").then((r) =>
r.json(),
);
```
**Result**:
- ⚡ Faster initial page load
- ⚡ Better Lighthouse scores
- ⚡ Improved user experience
---
## 💡 Maintenance Tips
### **When Adding New Files**:
1. **Images**:
- Use WebP format (smaller than PNG/JPG)
- Optimize dengan tooling (ImageOptim, Squoosh)
- Consider `next/image` untuk auto-optimization
2. **Icons**:
- Prefer SVG (scalable, small)
- Consider icon libraries (react-icons, lucide-react)
- Avoid duplicates
3. **Data Files**:
- Compress large JSON/CSV
- Consider API route instead of static file
- Use CDN untuk large files
### **Regular Cleanup**:
```bash
# Find unused public files
du -sh public/*
# Check git history untuk unused files
git log --all --full-history -- public/
```
---
## 🔧 Troubleshooting
### **Issue**: Model not loading
```
Error: Failed to fetch /models/xgb_activity_model.json
```
**Solutions**:
1. Verify file exists: `ls public/models/`
2. Check Next.js server running
3. Clear browser cache
4. Check CORS headers (if loading from external domain)
---
### **Issue**: Icons not showing
```
404: /public/favicon.svg not found
```
**Solution**: ❌ Remove `/public` prefix
```html
<!-- Wrong -->
<img src="/public/favicon.svg" />
<!-- Correct -->
<img src="/favicon.svg" />
```
---
### **Issue**: Large bundle size
```
Warning: Page size exceeds 300 KB
```
**Solution**: Lazy load model
```typescript
// Lazy load
const model = await import("@/public/models/xgb_activity_model.json");
```
---
## 📚 Next.js Static File Best Practices
### **✅ DO**:
- Keep files small (< 1 MB ideally)
- Use modern formats (WebP, AVIF, SVG)
- Compress large files
- Use meaningful filenames
- Organize in subdirectories
### **❌ DON'T**:
- Put sensitive data
- Store frequently changing data
- Use as database replacement
- Include source files (.psd, .ai)
- Exceed 10 MB total size
---
## 🎓 Summary
**Current Status**: ✅ Well-organized, but can be optimized
**Key Files**:
1. **xgb_activity_model.json** - Critical ML model (2.1 MB)
2. **favicon.svg** - App branding
3. **Other SVGs** - Likely unused (cleanup recommended)
**Recommendations**:
1. ⚡ Lazy load ML model
2. 🧹 Remove unused icons
3. 📁 Add PWA manifest
4. 🔐 Add model integrity check
5. 📊 Monitor bundle size
**Total Size**: ~2.1 MB (99% from model file)
**Performance**: Good, but can be improved with lazy loading

1017
DOKUMENTASI_ROOT_FILES.md Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,779 @@
# Dokumentasi Folder `scripts/`
## Gambaran Umum
Folder `scripts/` berisi utility scripts untuk development, testing, dan database management. Scripts ini membantu developer untuk verify functionality, seed data, dan debug issues.
---
## 📁 Struktur Direktori
```
scripts/
├── check-links.ts # Check user-coach relationships
├── check_logs.ts # View recent activity logs
├── seed_log.ts # Create test activity log
├── test_har_core.ts # Test activity recognition system
└── test_rehab_core.ts # Test exercise recognition system
```
**Total Files**: 5 TypeScript scripts
---
## 📄 Database Management Scripts
### **1. `check-links.ts`** 🔗
**Path**: `/scripts/check-links.ts`
**Fungsi**: Menampilkan semua users dan coach-client relationships
**Size**: ~466 bytes
**Use Case**:
- Verify seeding berhasil
- Debug coach-client assignments
- Quick database health check
**Code**:
```typescript
import "dotenv/config";
import { PrismaClient } from "../app/generated/client/client";
const prisma = new PrismaClient();
async function main() {
const users = await prisma.users.findMany();
console.log("--- All Users ---");
users.forEach((u) =>
console.log(`${u.name} (${u.role}): ID=${u.id}, CoachID=${u.coach_id}`),
);
}
```
**How to Run**:
```bash
npx tsx scripts/check-links.ts
```
**Expected Output**:
```
--- All Users ---
Coach One (COACH): ID=C00001, CoachID=null
Coach Two (COACH): ID=C00002, CoachID=null
Client One (CLIENT): ID=U00001, CoachID=C00001
Client Two (CLIENT): ID=U00002, CoachID=C00001
Client Three (CLIENT): ID=U00003, CoachID=C00002
```
**Use Cases**:
- ✅ Verify after running `prisma db seed`
- ✅ Check migration success
- ✅ Debug role assignments
- ✅ List all registered users
---
### **2. `check_logs.ts`** 📋
**Path**: `/scripts/check_logs.ts`
**Fungsi**: Menampilkan 10 activity logs terbaru dari database
**Size**: ~887 bytes
**Use Case**:
- Monitor real-time activity tracking
- Debug activity logging system
- Verify fall detection alerts
- Inspect log details (JSON)
**Code**:
```typescript
import { PrismaClient } from "../app/generated/client/client";
import * as dotenv from "dotenv";
dotenv.config();
const prisma = new PrismaClient();
async function main() {
console.log("Checking Activity Logs...");
const logs = await prisma.activity_logs.findMany({
take: 10,
orderBy: { timestamp: "desc" },
include: {
user: { select: { name: true } },
},
});
if (logs.length === 0) {
console.log("No logs found.");
} else {
console.log(`Found ${logs.length} logs:`);
logs.forEach((log) => {
console.log(
`[${log.timestamp?.toISOString()}] ` +
`User: ${log.user?.name || log.user_id} | ` +
`Status: ${log.status} | ` +
`Details: ${JSON.stringify(log.details)}`,
);
});
}
}
```
**How to Run**:
```bash
npx tsx scripts/check_logs.ts
```
**Expected Output**:
```
Checking Activity Logs...
Found 3 logs:
[2025-12-28T10:30:00.000Z] User: Client One | Status: Standing | Details: {"exercise":"bicep_curl","reps":5}
[2025-12-28T10:25:00.000Z] User: Client Two | Status: Sitting | Details: {}
[2025-12-28T10:20:00.000Z] User: Client One | Status: Standing | Details: {}
```
**Features**:
- ✅ Shows latest 10 logs (configurable via `take`)
- ✅ Ordered by timestamp (newest first)
- ✅ Includes user name via join
- ✅ Pretty-prints JSON details
**When to Use**:
- After training session untuk verify logs created
- Debug fall detection alerts
- Monitor which users are active
---
### **3. `seed_log.ts`** 🌱
**Path**: `/scripts/seed_log.ts`
**Fungsi**: Membuat test activity log untuk development
**Size**: ~816 bytes
**Use Case**:
- Populate database dengan sample logs
- Test log display UI
- Verify logging system works
**Code**:
```typescript
import { PrismaClient } from "../app/generated/client/client";
import * as dotenv from "dotenv";
dotenv.config();
const prisma = new PrismaClient();
async function main() {
console.log("Seeding Mock Log...");
// Get a client user
const user = await prisma.users.findFirst({
where: { role: "CLIENT" },
});
if (!user) {
console.error("No client user found to attach log to.");
return;
}
const log = await prisma.activity_logs.create({
data: {
user_id: user.id,
timestamp: new Date(),
status: "TEST_LOG",
confidence: "1.0",
details: { message: "Manual verification log" },
},
});
console.log("Created Log ID:", log.id);
}
```
**How to Run**:
```bash
npx tsx scripts/seed_log.ts
```
**Expected Output**:
```
Seeding Mock Log...
Created Log ID: 1
```
**Use Cases**:
- ✅ Quick test untuk UI development
- ✅ Verify database schema works
- ✅ Populate logs tanpa perlu run full app
**Customization**:
```typescript
// Modify untuk create specific log
data: {
status: 'Fall Detected', // Test fall alert
confidence: '0.95',
details: {
coordinates: { x: 100, y: 200 },
severity: 'HIGH'
}
}
```
---
## 🧪 Testing Scripts
### **4. `test_har_core.ts`** 🤖
**Path**: `/scripts/test_har_core.ts`
**Fungsi**: Unit test untuk `HARCore` activity recognition system
**Size**: ~1 KB
**Use Case**:
- Test XGBoost model integration
- Verify exercise detection logic
- Debug HAR system issues
- Ensure setExercise() mapping works
**Code**:
```typescript
import { HARCore } from "../lib/pose/HARCore";
import { Landmark } from "../lib/pose/ExerciseRules";
const har = new HARCore();
const mockLandmarks: Landmark[] = Array(33).fill({
x: 0.5,
y: 0.5,
z: 0,
visibility: 1,
});
console.log("Testing HARCore...");
const inputs = ["Bicep Curl", "Squats", "deadlift", "Unknown Exercise"];
inputs.forEach((input) => {
har.setExercise(input);
console.log(`Set to '${input}'. Invoking process...`);
har
.process(mockLandmarks)
.then((res) => {
console.log(
`[PASS] '${input}' -> Result:`,
res ? `Exercise: ${res.exercise}, Status: ${res.status}` : "NULL",
);
})
.catch((e) => {
console.error(`[FAIL] '${input}' -> Error:`, e);
});
});
setTimeout(() => console.log("Done."), 2000);
```
**How to Run**:
```bash
npx tsx scripts/test_har_core.ts
```
**Expected Output**:
```
Testing HARCore...
Set to 'Bicep Curl'. Invoking process...
Set to 'Squats'. Invoking process...
Set to 'deadlift'. Invoking process...
Set to 'Unknown Exercise'. Invoking process...
[PASS] 'Bicep Curl' -> Result: Exercise: bicep_curl, Status: Standing
[PASS] 'Squats' -> Result: Exercise: squat, Status: Standing
[PASS] 'deadlift' -> Result: Exercise: deadlift, Status: Standing
[PASS] 'Unknown Exercise' -> Result: Exercise: null, Status: Standing
Done.
```
**What It Tests**:
- ✅ Exercise name normalization (UI names → config keys)
- ✅ Activity classification (Standing/Sitting/Fall)
- ✅ Integration HARCore + RehabCore
- ✅ Null handling untuk unknown exercises
**Debug Use**:
```typescript
// Add this untuk see feature extraction
const features = har.extractFeatures(mockLandmarks);
console.log("Extracted Features:", features.length); // Should be 141
```
---
### **5. `test_rehab_core.ts`** 💪
**Path**: `/scripts/test_rehab_core.ts`
**Fungsi**: Unit test untuk `RehabCore` exercise recognition system
**Size**: ~1.1 KB
**Use Case**:
- Test all 7 exercise configs load correctly
- Verify FSM initialization
- Debug rep counting issues
- Ensure config keys match
**Code**:
```typescript
import { RehabCore } from "../lib/pose/RehabCore";
import { Landmark } from "../lib/pose/ExerciseRules";
const core = new RehabCore();
const mockLandmarks: Landmark[] = Array(33).fill({
x: 0.5,
y: 0.5,
z: 0,
visibility: 1,
});
const exercises = [
"bicep_curls",
"hammer_curls",
"shoulder_press",
"lateral_raises",
"squats",
"deadlifts",
"lunges",
];
console.log("Testing RehabCore Config Loading...");
exercises.forEach((name) => {
try {
const result = core.process(name, mockLandmarks);
if (result) {
console.log(`[PASS] ${name} -> Processed successfully.`);
} else {
console.error(`[FAIL] ${name} -> Returned null (Config not found?).`);
}
} catch (e) {
console.error(`[FAIL] ${name} -> Exception:`, e);
}
});
```
**How to Run**:
```bash
npx tsx scripts/test_rehab_core.ts
```
**Expected Output**:
```
Testing RehabCore Config Loading...
[PASS] bicep_curls -> Processed successfully.
[PASS] hammer_curls -> Processed successfully.
[PASS] shoulder_press -> Processed successfully.
[PASS] lateral_raises -> Processed successfully.
[PASS] squats -> Processed successfully.
[PASS] deadlifts -> Processed successfully.
[PASS] lunges -> Processed successfully.
```
**What It Tests**:
- ✅ Exercise name normalization
- ✅ Config lookup dari `EXERCISE_CONFIGS`
- ✅ FSM initialization (`COUNTER_MAP`)
- ✅ Process loop doesn't crash
**Common Failures**:
```
[FAIL] bicep_curls -> Returned null (Config not found?)
```
**Cause**: Typo in exercise name or config key
**Fix**: Check `ExerciseRules.ts` dan `RehabCore.ts` COUNTER_MAP
---
## 🔧 How to Run Scripts
### **Prerequisites**:
```bash
# Install tsx (TypeScript executor)
npm install -g tsx
# Or use npx (no global install)
npx tsx scripts/[script-name].ts
```
### **Environment Setup**:
All scripts load `.env` file:
```bash
# .env
DATABASE_URL="postgresql://user:password@localhost:5432/dbname"
```
### **Package Scripts** (Optional):
Add to `package.json`:
```json
{
"scripts": {
"check:links": "tsx scripts/check-links.ts",
"check:logs": "tsx scripts/check_logs.ts",
"seed:log": "tsx scripts/seed_log.ts",
"test:har": "tsx scripts/test_har_core.ts",
"test:rehab": "tsx scripts/test_rehab_core.ts"
}
}
```
Then run:
```bash
npm run check:links
npm run test:har
```
---
## 📊 Script Usage Matrix
| Script | Purpose | When to Use | Dependencies |
| -------------------- | -------------------------- | -------------------------------- | --------------------------- |
| `check-links.ts` | List users & relationships | After seeding, debug assignments | Prisma, DB |
| `check_logs.ts` | View recent activity logs | After training sessions | Prisma, DB |
| `seed_log.ts` | Create test log | UI development, testing | Prisma, DB |
| `test_har_core.ts` | Test activity recognition | After HAR changes, debug | HARCore, XGBoost model |
| `test_rehab_core.ts` | Test exercise recognition | After config changes, debug | RehabCore, EXERCISE_CONFIGS |
---
## 🎯 Common Workflows
### **Workflow 1: Fresh Database Setup**
```bash
# 1. Reset database
npx prisma migrate reset
# 2. Verify users created
npx tsx scripts/check-links.ts
# 3. Create test log
npx tsx scripts/seed_log.ts
# 4. Verify log saved
npx tsx scripts/check_logs.ts
```
---
### **Workflow 2: Debug Exercise Recognition**
```bash
# 1. Test all exercises load
npx tsx scripts/test_rehab_core.ts
# 2. If FAIL, check config
cat lib/pose/ExerciseRules.ts | grep -A 5 "bicep_curl"
# 3. Fix and re-test
npx tsx scripts/test_rehab_core.ts
```
---
### **Workflow 3: Monitor Production Logs**
```bash
# SSH to server
ssh user@production-server
# Check recent activity
cd /path/to/app
npx tsx scripts/check_logs.ts
# If fall detected, investigate
npx tsx scripts/check_logs.ts | grep "Fall Detected"
```
---
## 💡 Extending Scripts
### **Add New Test Script**:
**Example**: `test_form_scoring.ts`
```typescript
import { RehabCore } from "../lib/pose/RehabCore";
import { Landmark } from "../lib/pose/ExerciseRules";
const core = new RehabCore();
// Create realistic landmarks (proper squat form)
const goodSquatLandmarks: Landmark[] = [
// ... 33 landmarks with correct squat angles
];
const result = core.process("squat", goodSquatLandmarks);
console.log("Form Score:", result?.scores?.deviation_mae);
// Expected: < 8 (Excellent)
```
---
### **Add Data Export Script**:
**Example**: `export_recaps.ts`
```typescript
import { PrismaClient } from "../app/generated/client/client";
import fs from "fs";
const prisma = new PrismaClient();
async function main() {
const recaps = await prisma.user_recaps.findMany({
include: { user: true, training_menus: true },
});
const csv = recaps.map((r) => ({
user: r.user?.name,
menu: r.training_menus?.name,
date: r.completed_at,
// ... more fields
}));
fs.writeFileSync("export.json", JSON.stringify(csv, null, 2));
console.log("Exported to export.json");
}
main();
```
---
## 🔒 Security Notes
### **Safe Scripts**: ✅
- `check-links.ts` - Read-only
- `check_logs.ts` - Read-only
- `test_har_core.ts` - No DB access
- `test_rehab_core.ts` - No DB access
### **Destructive Scripts**: ⚠️
- `seed_log.ts` - **Writes** to database
- Safe in dev, but be careful in production
- Consider adding `--dry-run` flag
### **Best Practice**:
```typescript
// Add environment check
if (process.env.NODE_ENV === "production") {
console.error("This script is not safe to run in production!");
process.exit(1);
}
```
---
## 🚀 Advanced Usage
### **1. Continuous Testing**:
```bash
# Watch mode for TDD
npx tsx watch scripts/test_rehab_core.ts
# Re-runs on file change
```
---
### **2. Automated Checks**:
```bash
# Add to CI/CD pipeline
# .github/workflows/test.yml
- name: Run Unit Tests
run: |
npx tsx scripts/test_har_core.ts
npx tsx scripts/test_rehab_core.ts
```
---
### **3. Data Migration**:
```typescript
// scripts/migrate_old_logs.ts
const oldLogs = await prisma.legacy_logs.findMany();
for (const log of oldLogs) {
await prisma.activity_logs.create({
data: {
user_id: log.userId,
status: transformStatus(log.oldStatus),
// ... transform logic
},
});
}
```
---
## 🛠️ Troubleshooting
### **Issue**: `Cannot find module 'dotenv'`
```bash
Error: Cannot find module 'dotenv'
```
**Solution**:
```bash
npm install dotenv
```
---
### **Issue**: `tsx: command not found`
```bash
bash: tsx: command not found
```
**Solution**:
```bash
# Use npx instead
npx tsx scripts/check-links.ts
# Or install globally
npm install -g tsx
```
---
### **Issue**: Prisma connection error
```bash
Error: Can't reach database server
```
**Solution**:
1. Check PostgreSQL running
2. Verify `DATABASE_URL` in `.env`
3. Test connection:
```bash
npx prisma db pull
```
---
### **Issue**: Scripts hang (promises not resolving)
```typescript
// Add timeout
setTimeout(() => process.exit(0), 5000);
```
---
## 📚 References
**Tools Used**:
- [tsx](https://github.com/esbuild-kit/tsx) - TypeScript executor
- [Prisma Client](https://www.prisma.io/docs/concepts/components/prisma-client) - Database ORM
- [dotenv](https://github.com/motdotla/dotenv) - Environment variables
**Related Docs**:
- `/lib/pose/` - Core AI modules
- `/prisma/` - Database schema
- `package.json` - NPM scripts
---
## ✅ Summary
**Total Scripts**: 5
**Categories**:
- 🗄️ **Database**: 3 scripts (check-links, check_logs, seed_log)
- 🧪 **Testing**: 2 scripts (test_har_core, test_rehab_core)
**When to Use**:
- ✅ After database migrations
- ✅ After modifying exercise configs
- ✅ Debugging recognition issues
- ✅ Verifying production data
**Quick Commands**:
```bash
# Database health check
npx tsx scripts/check-links.ts
# View recent activity
npx tsx scripts/check_logs.ts
# Test AI systems
npx tsx scripts/test_har_core.ts
npx tsx scripts/test_rehab_core.ts
```
Scripts ini adalah **development tools penting** untuk maintain code quality dan debug issues! 🛠️

36
README.md Normal file
View File

@ -0,0 +1,36 @@
This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app).
## Getting Started
First, run the development server:
```bash
npm run dev
# or
yarn dev
# or
pnpm dev
# or
bun dev
```
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.
This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel.
## Learn More
To learn more about Next.js, take a look at the following resources:
- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome!
## Deploy on Vercel
The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details.

View File

@ -0,0 +1,40 @@
import { NextResponse } from 'next/server';
import { prisma } from '@/lib/prisma';
export async function POST(request: Request) {
try {
const body = await request.json();
const { coachId, clientId } = body;
if (!coachId || !clientId) {
return NextResponse.json({ error: 'Coach ID and Client ID are required' }, { status: 400 });
}
// Validate Coach
const coach = await prisma.users.findUnique({ where: { id: String(coachId) } });
if (!coach || coach.role !== 'COACH') {
return NextResponse.json({ error: 'Invalid Coach ID' }, { status: 400 });
}
// Validate Client
const client = await prisma.users.findUnique({ where: { id: String(clientId) } });
if (!client) { // Allow taking over any user as long as they exist? Ideally check if they are CLIENT role.
return NextResponse.json({ error: 'Client not found' }, { status: 404 });
}
if (client.role !== 'CLIENT') {
return NextResponse.json({ error: 'Target user is not a Client' }, { status: 400 });
}
// Update Link
const updatedClient = await prisma.users.update({
where: { id: String(clientId) },
data: { coach_id: String(coachId) }
});
return NextResponse.json(updatedClient);
} catch (error) {
console.error("Link Client Error:", error);
return NextResponse.json({ error: 'Failed to link client' }, { status: 500 });
}
}

48
app/api/logs/route.ts Normal file
View File

@ -0,0 +1,48 @@
import { NextResponse } from 'next/server';
import { prisma } from '@/lib/prisma';
export async function POST(request: Request) {
try {
const userIdHeader = request.headers.get('x-user-id');
if (!userIdHeader) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
}
const body = await request.json();
const { status, confidence, details } = body; // Confidence as string "0.95" etc
const log = await prisma.activity_logs.create({
data: {
user_id: userIdHeader,
timestamp: new Date(),
status: status || 'Unknown',
confidence: String(confidence),
details: details || {}
}
});
return NextResponse.json({ success: true, id: log.id });
} catch (error) {
console.error("Log Error:", error);
return NextResponse.json({ error: 'Failed to log' }, { status: 500 });
}
}
export async function GET(request: Request) {
try {
const userIdHeader = request.headers.get('x-user-id');
if (!userIdHeader) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
}
const logs = await prisma.activity_logs.findMany({
where: { user_id: userIdHeader },
orderBy: { timestamp: 'desc' },
take: 20
});
return NextResponse.json({ logs });
} catch (error) {
return NextResponse.json({ error: 'Failed to fetch' }, { status: 500 });
}
}

View File

@ -0,0 +1,29 @@
import { NextResponse } from 'next/server';
import { prisma } from '@/lib/prisma';
export async function GET(
request: Request,
props: { params: Promise<{ id: string }> } // Change to Promise type
) {
try {
const params = await props.params; // Await the params
const id = parseInt(params.id);
if (isNaN(id)) {
return NextResponse.json({ error: 'Invalid ID' }, { status: 400 });
}
const menu = await prisma.training_menus.findUnique({
where: { id: id }
});
if (!menu) {
return NextResponse.json({ error: 'Menu not found' }, { status: 404 });
}
return NextResponse.json(menu);
} catch (error) {
console.error("GET Menu Detail Error:", error);
return NextResponse.json({ error: 'Failed to fetch menu' }, { status: 500 });
}
}

58
app/api/menus/route.ts Normal file
View File

@ -0,0 +1,58 @@
import { NextResponse } from 'next/server';
import { prisma } from '@/lib/prisma';
export async function GET(request: Request) {
try {
const userIdHeader = request.headers.get('x-user-id');
const userId = userIdHeader || null; // String ID
let whereClause = {};
if (userId) {
const user = await prisma.users.findUnique({ where: { id: userId } });
if (user?.role === 'COACH') {
whereClause = { author_id: userId };
} else if (user?.role === 'CLIENT') {
whereClause = { client_id: userId }; // Only see assigned menus
}
}
const menus = await prisma.training_menus.findMany({
where: whereClause,
include: {
assigned_client: {
select: { name: true, id: true }
}
},
orderBy: { created_at: 'desc' }
});
return NextResponse.json(menus);
} catch (error) {
console.error("GET Error:", error);
return NextResponse.json({ error: 'Failed to fetch menus' }, { status: 500 });
}
}
export async function POST(request: Request) {
try {
const userIdHeader = request.headers.get('x-user-id');
const authorId = userIdHeader || null; // String ID
const body = await request.json();
const { name, exercises, client_id } = body;
const newMenu = await prisma.training_menus.create({
data: {
name,
exercises: exercises,
created_at: new Date(),
author_id: authorId,
client_id: client_id || null // Save assigned client
}
});
return NextResponse.json(newMenu);
} catch (error) {
console.error("POST Error:", error);
return NextResponse.json({ error: 'Failed to create menu' }, { status: 500 });
}
}

View File

@ -0,0 +1,32 @@
import { NextResponse } from 'next/server';
import { prisma } from '@/lib/prisma';
export async function GET(
request: Request,
props: { params: Promise<{ id: string }> }
) {
try {
const params = await props.params;
const id = parseInt(params.id);
if (isNaN(id)) {
return NextResponse.json({ error: 'Invalid ID' }, { status: 400 });
}
const recap = await prisma.user_recaps.findUnique({
where: { id: id },
include: {
training_menus: true // Include menu details
}
});
if (!recap) {
return NextResponse.json({ error: 'Recap not found' }, { status: 404 });
}
return NextResponse.json(recap);
} catch (error) {
console.error("GET Recap Detail Error:", error);
return NextResponse.json({ error: 'Failed to fetch recap' }, { status: 500 });
}
}

62
app/api/recap/route.ts Normal file
View File

@ -0,0 +1,62 @@
import { NextResponse } from 'next/server';
import { prisma } from '@/lib/prisma';
export async function POST(request: Request) {
try {
const body = await request.json();
const { menu_id, user_id, summary } = body;
const recap = await prisma.user_recaps.create({
data: {
menu_id: Number(menu_id), // Menu ID stays Int
user_id: user_id ? String(user_id) : null,
summary: summary,
completed_at: new Date()
}
});
return NextResponse.json(recap);
} catch (error) {
console.error(error);
return NextResponse.json({ error: 'Failed to save recap' }, { status: 500 });
}
}
export async function GET(request: Request) {
try {
const userIdHeader = request.headers.get('x-user-id');
const userId = userIdHeader || null; // String ID
let whereClause = {};
if (userId) {
const user = await prisma.users.findUnique({
where: { id: userId },
include: { clients: true }
});
if (user?.role === 'COACH') {
// Coach sees recaps from their clients
const clientIds = user.clients.map(c => c.id);
whereClause = { user_id: { in: clientIds } };
} else if (user?.role === 'CLIENT') {
// Client sees only their own recaps
whereClause = { user_id: userId };
}
}
const recaps = await prisma.user_recaps.findMany({
where: whereClause,
take: 50,
include: {
user: { select: { name: true, id: true } },
training_menus: { select: { name: true, id: true } }
},
orderBy: { completed_at: 'desc' }
});
return NextResponse.json(recaps);
} catch (error) {
console.error("GET Recap Error:", error);
return NextResponse.json({ error: 'Failed to fetch recaps' }, { status: 500 });
}
}

45
app/api/register/route.ts Normal file
View File

@ -0,0 +1,45 @@
import { NextResponse } from 'next/server';
import { prisma } from '@/lib/prisma';
export async function POST(request: Request) {
try {
const body = await request.json();
const { name, role } = body;
if (!name || !role || !['COACH', 'CLIENT'].includes(role)) {
return NextResponse.json({ error: 'Invalid name or role' }, { status: 400 });
}
// Generate 6-char random ID
const generateId = () => {
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*';
let result = '';
for (let i = 0; i < 6; i++) {
result += chars.charAt(Math.floor(Math.random() * chars.length));
}
return result;
};
let uniqueId = generateId();
let exists = await prisma.users.findUnique({ where: { id: uniqueId } });
while (exists) {
uniqueId = generateId();
exists = await prisma.users.findUnique({ where: { id: uniqueId } });
}
const newUser = await prisma.users.create({
data: {
id: uniqueId,
name,
role,
created_at: new Date()
}
});
return NextResponse.json(newUser);
} catch (error) {
console.error("Register Error:", error);
return NextResponse.json({ error: 'Failed to register user' }, { status: 500 });
}
}

View File

@ -0,0 +1,26 @@
import { NextResponse } from 'next/server';
import { prisma } from '@/lib/prisma';
export async function GET(
request: Request,
props: { params: Promise<{ id: string }> }
) {
try {
const params = await props.params;
const id = params.id; // String ID
const user = await prisma.users.findUnique({
where: { id: id },
include: { coach: true }
});
if (!user) {
return NextResponse.json({ error: 'User not found' }, { status: 404 });
}
return NextResponse.json(user);
} catch (error) {
return NextResponse.json({ error: 'Internal Error' }, { status: 500 });
}
}

30
app/api/users/route.ts Normal file
View File

@ -0,0 +1,30 @@
import { NextResponse } from 'next/server';
import { prisma } from '@/lib/prisma';
export async function GET(request: Request) {
const { searchParams } = new URL(request.url);
const coachId = searchParams.get('coachId');
try {
let whereClause = {};
if (coachId) {
whereClause = { coach_id: coachId };
} else {
// Default? user search? For now just return empty or all clients?
// Let's restrict to only returning if coachId is provided for safety/relevance context
return NextResponse.json([]);
}
const users = await prisma.users.findMany({
where: whereClause,
orderBy: { name: 'asc' }
});
return NextResponse.json(users);
} catch (error) {
console.error("GET Users Error:", error);
return NextResponse.json({ error: 'Failed to fetch users' }, { status: 500 });
}
}

289
app/client/free/page.tsx Normal file
View File

@ -0,0 +1,289 @@
'use client';
import React, { useState } from 'react';
import { useRouter } from 'next/navigation';
import { motion, AnimatePresence } from 'framer-motion';
import { Plus, Trash2, PlayCircle, ArrowLeft, Copy } from 'lucide-react';
import Link from 'next/link';
interface ExerciseItem {
id: string;
name: string;
reps: number;
weight: number;
rest_time_seconds: number;
}
interface RoundData {
id: string;
exercises: ExerciseItem[];
}
export default function FreeModeBuilder() {
const router = useRouter();
// --- Round-Based State ---
const [rounds, setRounds] = useState<RoundData[]>([
{
id: 'round-1',
exercises: [
{ id: 'ex-1', name: 'Squat', reps: 10, weight: 20, rest_time_seconds: 30 }
]
}
]);
// --- Actions ---
const addRound = () => {
setRounds([...rounds, {
id: Math.random().toString(36).substr(2, 9),
exercises: []
}]);
};
const duplicateRound = (sourceIndex: number) => {
const source = rounds[sourceIndex];
const newExercises = source.exercises.map(ex => ({
...ex,
id: Math.random().toString(36).substr(2, 9)
}));
// Insert after the source round
const newRounds = [...rounds];
newRounds.splice(sourceIndex + 1, 0, {
id: Math.random().toString(36).substr(2, 9),
exercises: newExercises
});
setRounds(newRounds);
};
const removeRound = (index: number) => {
setRounds(rounds.filter((_, i) => i !== index));
};
const addExerciseToRound = (roundIndex: number) => {
const newRounds = [...rounds];
newRounds[roundIndex].exercises.push({
id: Math.random().toString(36).substr(2, 9),
name: 'Squat',
reps: 10,
weight: 10,
rest_time_seconds: 30
});
setRounds(newRounds);
};
const removeExerciseFromRound = (roundIndex: number, exIndex: number) => {
const newRounds = [...rounds];
newRounds[roundIndex].exercises = newRounds[roundIndex].exercises.filter((_, i) => i !== exIndex);
setRounds(newRounds);
};
const updateExercise = (roundIndex: number, exIndex: number, field: keyof ExerciseItem, value: any) => {
const newRounds = [...rounds];
newRounds[roundIndex].exercises[exIndex] = {
...newRounds[roundIndex].exercises[exIndex],
[field]: value
};
setRounds(newRounds);
};
const startTraining = () => {
if (rounds.length === 0) return;
if (rounds.every(r => r.exercises.length === 0)) return;
// Flatten Logic: Expand Rounds into Linear List
// Matches Coach App logic exactly
const flatList: any[] = [];
const counts: Record<string, number> = {};
const totals: Record<string, number> = {};
// 1. Calculate Totals (First Pass)
rounds.forEach(round => {
round.exercises.forEach(ex => {
totals[ex.name] = (totals[ex.name] || 0) + 1;
});
});
// 2. Flatten and Assign Indices
rounds.forEach((round) => {
round.exercises.forEach(ex => {
counts[ex.name] = (counts[ex.name] || 0) + 1;
flatList.push({
name: ex.name,
reps: ex.reps,
weight: ex.weight,
rest_time_seconds: ex.rest_time_seconds,
set_index: counts[ex.name],
total_sets: totals[ex.name]
});
});
});
// Save to LocalStorage
const freeMenu = {
id: 'free-mode',
name: 'Free Session',
exercises: flatList
};
localStorage.setItem('straps_free_mode_menu', JSON.stringify(freeMenu));
// Redirect
router.push('/client/training?mode=free');
};
const EXERCISE_OPTIONS = [
"Bicep Curl", "Hammer Curl", "Squat", "Deadlift", "Lunges", "Overhead Press", "Lateral Raises"
];
return (
<div className="min-h-screen bg-background text-foreground p-4 md:p-8 font-sans pb-32">
<header className="max-w-3xl mx-auto mb-10 flex items-center gap-4">
<Link href="/client" className="p-3 bg-white hover:bg-zinc-100 rounded-full transition-colors border border-zinc-200 shadow-sm">
<ArrowLeft className="w-5 h-5 text-zinc-600" />
</Link>
<div>
<h1 className="text-2xl md:text-3xl font-light text-zinc-900 tracking-wide">
Free Style <span className="font-bold text-primary">Composer</span>
</h1>
<p className="text-zinc-500 text-sm mt-1">Design training blocks set-by-set.</p>
</div>
</header>
<div className="max-w-3xl mx-auto space-y-8">
<AnimatePresence>
{rounds.map((round, roundIndex) => (
<motion.div
key={round.id}
initial={{ opacity: 0, scale: 0.95 }}
animate={{ opacity: 1, scale: 1 }}
exit={{ opacity: 0, scale: 0.95 }}
className="bg-zinc-50 border border-zinc-200 rounded-3xl p-6 md:p-8 relative shadow-sm group/round"
>
{/* Round Header */}
<div className="flex justify-between items-center mb-6">
<h3 className="text-xl font-black text-zinc-300 uppercase tracking-tighter flex items-center gap-2">
<span className="text-4xl text-zinc-200">#{ (roundIndex + 1).toString().padStart(2, '0') }</span>
SET/GROUP
</h3>
<div className="flex items-center gap-2">
<button
type="button"
onClick={() => duplicateRound(roundIndex)}
className="p-2 text-zinc-400 hover:text-blue-500 hover:bg-blue-50 rounded-xl transition-all"
title="Duplicate this Round"
>
<Copy className="w-5 h-5" />
</button>
<button
type="button"
onClick={() => removeRound(roundIndex)}
disabled={rounds.length === 1}
className="p-2 text-zinc-300 hover:text-red-500 hover:bg-red-50 rounded-xl transition-all disabled:opacity-0"
>
<Trash2 className="w-5 h-5" />
</button>
</div>
</div>
{/* Exercises List */}
<div className="space-y-4">
{round.exercises.map((ex, exIndex) => (
<div key={ex.id} className="bg-white p-4 rounded-xl shadow-sm border border-zinc-100 grid grid-cols-2 md:grid-cols-4 gap-4 items-center group/ex relative">
{/* Name */}
<div className="col-span-2 md:col-span-1">
<label className="block text-[10px] font-bold text-zinc-300 uppercase mb-1">Exercise</label>
<div className="relative">
<select
value={ex.name}
onChange={(e) => updateExercise(roundIndex, exIndex, 'name', e.target.value)}
className="w-full bg-zinc-50 border border-zinc-100 rounded-lg py-2 px-2 font-bold text-zinc-900 focus:outline-none appearance-none cursor-pointer"
>
{EXERCISE_OPTIONS.map(opt => <option key={opt} value={opt}>{opt}</option>)}
</select>
</div>
</div>
{/* Kg */}
<div>
<label className="block text-[10px] font-bold text-zinc-300 uppercase mb-1 text-center">Kg</label>
<input
type="number"
value={isNaN(ex.weight) ? '' : ex.weight}
onChange={(e) => updateExercise(roundIndex, exIndex, 'weight', parseFloat(e.target.value))}
className="w-full bg-zinc-50 border border-zinc-100 rounded-lg px-2 py-2 text-center font-mono text-sm focus:border-primary outline-none"
/>
</div>
{/* Reps */}
<div>
<label className="block text-[10px] font-bold text-zinc-300 uppercase mb-1 text-center">Reps</label>
<input
type="number"
value={isNaN(ex.reps) ? '' : ex.reps}
onChange={(e) => updateExercise(roundIndex, exIndex, 'reps', parseInt(e.target.value))}
className="w-full bg-zinc-50 border border-zinc-100 rounded-lg px-2 py-2 text-center font-mono text-sm focus:border-primary outline-none"
/>
</div>
{/* Rest */}
<div>
<label className="block text-[10px] font-bold text-zinc-300 uppercase mb-1 text-center">Rest(s)</label>
<input
type="number"
value={isNaN(ex.rest_time_seconds) ? '' : ex.rest_time_seconds}
onChange={(e) => updateExercise(roundIndex, exIndex, 'rest_time_seconds', parseFloat(e.target.value))}
className="w-full bg-zinc-50 border border-zinc-100 rounded-lg px-2 py-2 text-center font-mono text-sm focus:border-primary outline-none"
/>
</div>
{/* Remove Exercise */}
<button
type="button"
onClick={() => removeExerciseFromRound(roundIndex, exIndex)}
className="absolute -top-2 -right-2 p-1.5 bg-white border border-zinc-100 text-zinc-300 hover:text-red-500 rounded-full shadow-sm opacity-0 group-hover/ex:opacity-100 transition-opacity"
>
<Trash2 className="w-3 h-3" />
</button>
</div>
))}
<button
type="button"
onClick={() => addExerciseToRound(roundIndex)}
className="w-full py-3 border border-dashed border-zinc-300 rounded-xl text-zinc-400 text-sm font-bold hover:text-primary hover:border-primary hover:bg-white transition-all flex items-center justify-center gap-2"
>
<Plus className="w-4 h-4" /> Add Exercise
</button>
</div>
</motion.div>
))}
</AnimatePresence>
<div className="flex gap-4 pt-4">
<button
type="button"
onClick={addRound}
className="flex-1 bg-white border border-dashed border-zinc-300 text-zinc-500 hover:text-primary hover:border-primary p-6 rounded-xl flex items-center justify-center gap-2 font-bold transition-all hover:bg-zinc-50 shadow-sm"
>
<Plus className="w-5 h-5" /> Add New Round
</button>
</div>
</div>
<div className="fixed bottom-0 left-0 right-0 p-6 bg-white/80 backdrop-blur-lg border-t border-zinc-200 flex justify-center z-50">
<button
type="button"
onClick={startTraining}
disabled={rounds.length === 0}
className="w-full max-w-md bg-zinc-900 hover:bg-black text-white font-black uppercase tracking-widest py-4 rounded-2xl shadow-xl transform transition-all active:scale-95 disabled:opacity-50 flex items-center justify-center gap-3"
>
<PlayCircle className="w-6 h-6" /> START SESSION
</button>
</div>
</div>
);
}

459
app/client/monitor/page.tsx Normal file
View File

@ -0,0 +1,459 @@
'use client';
import React, { useEffect, useRef, useState } from 'react';
import { HARCore } from '@/lib/pose/HARCore';
// Import from the official tasks-vision package
import { PoseLandmarker, FilesetResolver, DrawingUtils } from '@mediapipe/tasks-vision';
import { ArrowLeft, Activity, ShieldAlert, Ban, CheckCircle, Edit3, Trash2, MousePointerClick, BellRing } from 'lucide-react';
import Link from 'next/link';
import { AuthProvider, useAuth } from '@/lib/auth';
export default function MonitorPageWrap() {
return (
<AuthProvider>
<MonitorPage />
</AuthProvider>
);
}
function MonitorPage() {
const videoRef = useRef<HTMLVideoElement>(null);
const canvasRef = useRef<HTMLCanvasElement>(null);
const [isLoading, setIsLoading] = useState(true);
// State
const [stats, setStats] = useState({ status: 'Initializing...', confidence: 0 });
// Safety Zone State (Normalized 0-1)
const [safetyZone, setSafetyZone] = useState<{x: number, y: number, w: number, h: number} | null>(null);
const [isEditingZone, setIsEditingZone] = useState(false);
const [isDrawing, setIsDrawing] = useState(false);
const [startPoint, setStartPoint] = useState<{x: number, y: number} | null>(null);
const zoneRef = useRef<{x: number, y: number, w: number, h: number} | null>(null);
// Alarm State
const [alarmTriggered, setAlarmTriggered] = useState(false);
const fallStartTimeRef = useRef<number | null>(null);
const [timeToAlarm, setTimeToAlarm] = useState<number | null>(null);
// Sync ref for loop access
useEffect(() => {
zoneRef.current = safetyZone;
}, [safetyZone]);
// Refs
const harRef = useRef<HARCore | null>(null);
const landmarkerRef = useRef<PoseLandmarker | null>(null);
const requestRef = useRef<number | null>(null);
useEffect(() => {
let isMounted = true;
async function init() {
try {
// 1. Init Core
const core = new HARCore();
harRef.current = core;
// 2. Init Vision
const vision = await FilesetResolver.forVisionTasks(
"https://cdn.jsdelivr.net/npm/@mediapipe/tasks-vision@0.10.14/wasm"
);
if (!isMounted) return;
const landmarker = await PoseLandmarker.createFromOptions(vision, {
baseOptions: {
modelAssetPath: `https://storage.googleapis.com/mediapipe-models/pose_landmarker/pose_landmarker_full/float16/1/pose_landmarker_full.task`,
delegate: "GPU"
},
runningMode: "VIDEO",
numPoses: 1
});
landmarkerRef.current = landmarker;
// 3. Init Camera
if (navigator.mediaDevices && navigator.mediaDevices.getUserMedia) {
const stream = await navigator.mediaDevices.getUserMedia({
video: { width: 640, height: 480 }
});
if (videoRef.current) {
videoRef.current.srcObject = stream;
await videoRef.current.play();
setIsLoading(false);
requestRef.current = requestAnimationFrame(predictWebcam);
}
}
} catch (e) {
console.error("Init Error:", e);
setIsLoading(false);
}
}
init();
return () => {
isMounted = false;
if (requestRef.current) cancelAnimationFrame(requestRef.current);
if (videoRef.current && videoRef.current.srcObject) {
(videoRef.current.srcObject as MediaStream).getTracks().forEach(t => t.stop());
}
};
}, []);
// Logging
// Logging
const { user } = useAuth();
const userRef = useRef(user);
// Keep userRef synced
useEffect(() => {
userRef.current = user;
}, [user]);
const lastLogRef = useRef(Date.now());
const alarmLoggedRef = useRef(false);
const sendLog = async (data: any) => {
if (!userRef.current) return;
try {
await fetch('/api/logs', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'x-user-id': userRef.current.id
},
body: JSON.stringify(data)
});
} catch (e) { console.error("Log failed", e); }
};
// Loop
const lastVideoTimeRef = useRef(-1);
const predictWebcam = async () => {
const video = videoRef.current;
const canvas = canvasRef.current;
const landmarker = landmarkerRef.current;
const har = harRef.current;
if (video && canvas && landmarker && har) {
let startTimeMs = performance.now();
if (lastVideoTimeRef.current !== video.currentTime && video.videoWidth > 0 && video.videoHeight > 0) {
lastVideoTimeRef.current = video.currentTime;
const result = landmarker.detectForVideo(video, startTimeMs);
// Draw
const ctx = canvas.getContext('2d');
if (ctx) {
ctx.save();
ctx.clearRect(0, 0, canvas.width, canvas.height);
// Mirror
ctx.scale(-1, 1);
ctx.translate(-canvas.width, 0);
// Draw Video Frame
ctx.drawImage(video, 0, 0, canvas.width, canvas.height);
if (result.landmarks) {
const drawingUtils = new DrawingUtils(ctx);
for (const lm of result.landmarks) {
drawingUtils.drawLandmarks(lm, { radius: 1, color: '#00FF00' });
drawingUtils.drawConnectors(lm, PoseLandmarker.POSE_CONNECTIONS, { color: '#00FF00', lineWidth: 2 });
}
}
ctx.restore();
}
// Process Logic
if (result.landmarks && result.landmarks.length > 0) {
const lm = result.landmarks[0];
// 1. Run HAR first (Always detect status)
const res = await har.process(lm as any);
if (res) {
// 2. Check Safety Zone
let isUnsafe = false;
if (zoneRef.current) {
const z = zoneRef.current;
const inZone = (p: {x:number, y:number}) =>
p.x >= z.x && p.x <= (z.x + z.w) &&
p.y >= z.y && p.y <= (z.y + z.h);
let outsideCount = 0;
for (const point of lm) {
// Convert to Screen Coords (Mirrored)
const screenPoint = { x: 1 - point.x, y: point.y };
if (!inZone(screenPoint)) {
outsideCount++;
}
}
// Threshold: > 70% of points outside triggers Unsafe
if ((outsideCount / lm.length) > 0.7) {
isUnsafe = true;
}
}
// Update Status
setStats({
status: res.status,
confidence: res.confidence || 0
});
// 3. Check Alarm Condition
let currentAlarmState = false;
if (res.status === 'Fall Detected' && isUnsafe) {
const now = Date.now();
if (!fallStartTimeRef.current) {
fallStartTimeRef.current = now;
}
const elapsed = now - fallStartTimeRef.current;
if (elapsed > 10000) {
// Trigger Alarm
setAlarmTriggered(true);
currentAlarmState = true;
} else {
// Update countdown for UI
setTimeToAlarm(Math.ceil((10000 - elapsed) / 1000));
}
} else {
// Reset
fallStartTimeRef.current = null;
setTimeToAlarm(null);
}
// 4. Logging Logic
const now = Date.now();
// A. Check for Alarm Log (Immediate)
// If alarm just triggered (transition) or is buzzing
if (currentAlarmState && !alarmLoggedRef.current) {
sendLog({
status: 'ALARM: Fall Outside Zone',
confidence: '1.0',
details: { reason: 'Fall detected outside safe zone > 10s' }
});
alarmLoggedRef.current = true; // Prevent spamming per frame
}
// B. Periodic Log (Every 1 min)
if (now - lastLogRef.current > 60000) {
sendLog({
status: res.status,
confidence: String(res.confidence),
details: { isUnsafe, zoneConfigured: !!zoneRef.current }
});
lastLogRef.current = now;
}
}
}
}
}
requestRef.current = requestAnimationFrame(predictWebcam);
};
// Drawing Handlers
const handleMouseDown = (e: React.MouseEvent) => {
if (!isEditingZone || !canvasRef.current) return;
const rect = canvasRef.current.getBoundingClientRect();
const x = (e.clientX - rect.left) / rect.width;
const y = (e.clientY - rect.top) / rect.height;
setIsDrawing(true);
setStartPoint({x, y});
setSafetyZone({x, y, w: 0, h: 0});
};
const handleMouseMove = (e: React.MouseEvent) => {
if (!isDrawing || !startPoint || !canvasRef.current) return;
const rect = canvasRef.current.getBoundingClientRect();
const currentX = (e.clientX - rect.left) / rect.width;
const currentY = (e.clientY - rect.top) / rect.height;
const w = Math.abs(currentX - startPoint.x);
const h = Math.abs(currentY - startPoint.y);
const x = Math.min(currentX, startPoint.x);
const y = Math.min(currentY, startPoint.y);
setSafetyZone({x, y, w, h});
};
const handleMouseUp = () => {
setIsDrawing(false);
};
return (
<div className="min-h-screen bg-background text-foreground p-6 font-sans selection:bg-primary/30">
<header className="flex justify-between items-center mb-8">
<div className="flex items-center gap-4">
<Link href="/client" className="p-2 bg-white rounded-full hover:bg-zinc-100 transition-colors border border-zinc-200">
<ArrowLeft className="w-5 h-5 text-zinc-600" />
</Link>
<h1 className="text-3xl font-light tracking-widest text-zinc-800">LIVE<span className="font-bold text-primary">.MONITOR</span></h1>
</div>
<div className="flex items-center gap-2 text-zinc-400 text-xs uppercase tracking-widest">
<Activity className="w-4 h-4 animate-pulse text-green-500" />
System Active
</div>
</header>
<div className="grid grid-cols-1 md:grid-cols-4 gap-6 max-w-6xl mx-auto">
{/* Main Camera View */}
<div className="md:col-span-3 relative border-8 border-white rounded-[2rem] overflow-hidden bg-zinc-100 shadow-2xl group">
{isLoading && <div className="absolute inset-0 flex items-center justify-center text-blue-400 font-mono animate-pulse">Loading AI Engine (WASM)...</div>}
<video ref={videoRef} className="hidden" width="640" height="480" autoPlay playsInline muted />
{/* Interaction Layer */}
<div
onMouseDown={handleMouseDown}
onMouseMove={handleMouseMove}
onMouseUp={handleMouseUp}
onMouseLeave={handleMouseUp}
className={`absolute inset-0 z-30 ${isEditingZone ? 'cursor-crosshair' : 'cursor-default'}`}
>
{/* Render Safety Zone using HTML Overlay */}
{safetyZone && (
<div
className="absolute border-4 duration-300 border-green-500/50 bg-green-500/10"
style={{
left: `${safetyZone.x * 100}%`,
top: `${safetyZone.y * 100}%`,
width: `${safetyZone.w * 100}%`,
height: `${safetyZone.h * 100}%`
}}
>
<div className="absolute top-0 left-0 -translate-y-full px-2 py-1 text-xs font-bold rounded-t-lg bg-green-500 text-white">
SAFE ZONE
</div>
</div>
)}
</div>
<canvas ref={canvasRef} width="640" height="480" className="w-full h-auto object-contain relative z-10" />
{/* Status Overlay */}
<div className="absolute top-6 left-6 flex gap-4 z-10 pointer-events-none">
<div className={`px-6 py-3 rounded-2xl text-lg font-bold tracking-widest backdrop-blur-md border shadow-lg transition-colors duration-300 flex items-center gap-3 ${
stats.status === 'Fall Detected'
? 'bg-red-500/90 border-red-500 text-white animate-pulse'
: 'bg-white/90 border-zinc-200 text-zinc-800'
}`}>
{stats.status === 'Fall Detected' && <Ban className="w-6 h-6" />}
{stats.status}
</div>
</div>
{/* Danger Countdown */}
{timeToAlarm !== null && !alarmTriggered && (
<div className="absolute inset-0 flex items-center justify-center z-40 bg-red-500/20 pointer-events-none">
<div className="bg-red-600 text-white px-8 py-6 rounded-3xl animate-bounce flex flex-col items-center shadow-2xl">
<ShieldAlert className="w-12 h-12 mb-2" />
<div className="text-4xl font-black">{timeToAlarm}</div>
<div className="text-xs font-bold uppercase tracking-widest">Zone Violation Detected</div>
</div>
</div>
)}
{/* ALARM TRIGGERED */}
{alarmTriggered && (
<div className="absolute inset-0 z-50 bg-red-600 animate-pulse flex flex-col items-center justify-center text-white p-8 text-center">
<BellRing className="w-24 h-24 mb-6 animate-bounce" />
<h1 className="text-6xl font-black mb-4 tracking-tighter">EMERGENCY</h1>
<p className="text-xl font-bold uppercase tracking-widest mb-12">Fall Detected Outside Safe Zone</p>
<button
onClick={() => {
setAlarmTriggered(false);
fallStartTimeRef.current = null;
alarmLoggedRef.current = false;
}}
className="bg-white text-red-600 px-10 py-4 rounded-full font-black text-xl hover:scale-105 transition-all shadow-xl uppercase border-4 border-red-800 pointer-events-auto"
>
DISMISS ALARM
</button>
</div>
)}
{isEditingZone && (
<div className="absolute bottom-6 left-0 right-0 flex justify-center z-30 pointer-events-none">
<div className="bg-yellow-100 text-yellow-800 px-4 py-2 rounded-full text-xs font-bold uppercase tracking-wider shadow-md animate-bounce">
Mode: Draw Safety Zone
</div>
</div>
)}
</div>
{/* Sidebar Controls */}
<div className="md:col-span-1 flex flex-col gap-4">
<div className="bg-white p-6 rounded-2xl border border-zinc-200 shadow-sm">
<h3 className="text-zinc-400 text-xs font-bold uppercase tracking-widest mb-4">Safety Controls</h3>
<div className="space-y-3">
<button
onClick={() => setIsEditingZone(!isEditingZone)}
className={`w-full flex items-center gap-3 px-4 py-3 rounded-xl transition-all font-medium text-sm ${
isEditingZone
? 'bg-yellow-50 text-yellow-700 border border-yellow-200 shadow-inner'
: 'bg-zinc-50 text-zinc-700 hover:bg-zinc-100 border border-zinc-100'
}`}
>
<Edit3 className="w-4 h-4" />
{isEditingZone ? 'Done Editing' : 'Edit Safe Zone'}
</button>
{safetyZone && (
<button
onClick={() => setSafetyZone(null)}
className="w-full flex items-center gap-3 px-4 py-3 rounded-xl bg-red-50 text-red-600 hover:bg-red-100 border border-red-100 transition-all font-medium text-sm"
>
<Trash2 className="w-4 h-4" /> Clear Zone
</button>
)}
</div>
{safetyZone ? (
<div className="mt-6 p-4 bg-green-50 rounded-xl border border-green-100">
<div className="flex items-center gap-2 text-green-700 font-bold text-xs uppercase mb-1">
<CheckCircle className="w-4 h-4" /> Zone Active
</div>
<p className="text-green-600 text-xs leading-relaxed">
Alarm triggers if a fall is detected OUTSIDE this zone for &gt; 10 seconds.
</p>
</div>
) : (
<div className="mt-6 p-4 bg-zinc-50 rounded-xl border border-zinc-100">
<div className="flex items-center gap-2 text-zinc-400 font-bold text-xs uppercase mb-1">
<MousePointerClick className="w-4 h-4" /> No Zone
</div>
<p className="text-zinc-400 text-xs leading-relaxed">
Click "Edit Safe Zone" and draw a box on the camera to define the safe area.
</p>
</div>
)}
</div>
<div className="bg-white p-6 rounded-2xl border border-zinc-200 shadow-sm flex-1">
<h3 className="text-zinc-400 text-xs font-bold uppercase tracking-widest mb-4">Stats</h3>
<div className="space-y-4">
<div>
<div className="text-sm text-zinc-500 mb-1">Current State</div>
<div className="text-2xl font-bold text-zinc-800">{stats.status}</div>
</div>
<div>
<div className="text-sm text-zinc-500 mb-1">AI Confidence</div>
<div className="text-xl font-mono text-primary">{(stats.confidence * 100).toFixed(1)}%</div>
</div>
</div>
</div>
</div>
</div>
</div>
);
}

148
app/client/page.tsx Normal file
View File

@ -0,0 +1,148 @@
'use client';
import React from 'react';
import { motion } from 'framer-motion';
import { PlayCircle, Eye, ArrowRight, Activity as ActivityIcon } from 'lucide-react';
import Link from 'next/link';
import { useAuth, AuthProvider } from '@/lib/auth';
export default function ClientHubWrap() {
return (
<AuthProvider>
<ClientHub />
</AuthProvider>
);
}
function ClientHub() {
const { user } = useAuth();
return (
<div className="min-h-screen bg-background text-foreground p-8 font-sans selection:bg-primary/30 flex items-center justify-center">
<div className="max-w-5xl w-full">
<header className="mb-16 text-center">
<div className="flex flex-col items-center gap-2 mb-4">
<h1 className="text-5xl font-light tracking-widest text-zinc-800">
Hello, <span className="font-bold text-primary">{user?.name || 'Client'}</span>.
</h1>
<div className="flex items-center gap-4 text-sm text-zinc-400 mt-2 bg-zinc-50 px-4 py-2 rounded-full border border-zinc-100">
<span>ID: <strong className="text-zinc-600">{user?.id}</strong></span>
{user?.coach && (
<>
<span className="w-1 h-1 bg-zinc-300 rounded-full"></span>
<span>Coach: <strong className="text-zinc-600">{user.coach.name}</strong></span>
</>
)}
</div>
</div>
<p className="text-zinc-500 text-lg">What would you like to focus on today?</p>
</header>
<div className="grid grid-cols-1 md:grid-cols-2 gap-8">
{/* Training Mode Card */}
<Link href="/client/training" className="group">
<motion.div
whileHover={{ y: -5 }}
className="bg-white p-10 rounded-[2rem] border border-zinc-200 shadow-xl group-hover:shadow-2xl group-hover:border-primary/30 transition-all h-full flex flex-col items-start"
>
<div className="w-16 h-16 rounded-2xl bg-blue-50 text-primary flex items-center justify-center mb-8 group-hover:scale-110 transition-transform">
<PlayCircle className="w-8 h-8" />
</div>
<h2 className="text-3xl font-bold text-zinc-900 mb-2">Start Training</h2>
<p className="text-zinc-500 mb-8 flex-1">Execute your assigned rehabilitation program. Follow real-time guidance and track your reps.</p>
<div className="flex items-center gap-2 text-primary font-bold uppercase tracking-widest text-sm group-hover:gap-4 transition-all">
Begin Session <ArrowRight className="w-4 h-4" />
</div>
</motion.div>
</Link>
{/* Live Monitor Card */}
<Link href="/client/monitor" className="group">
<motion.div
whileHover={{ y: -5 }} // Corrected: removed "cursor-pointer" as Link handles it
className="bg-zinc-900 p-10 rounded-[2rem] border border-zinc-800 shadow-xl group-hover:shadow-2xl group-hover:border-zinc-700 transition-all h-full flex flex-col items-start"
>
<div className="w-16 h-16 rounded-2xl bg-zinc-800 text-green-400 flex items-center justify-center mb-8 group-hover:scale-110 transition-transform">
<Eye className="w-8 h-8" />
</div>
<h2 className="text-3xl font-bold text-white mb-2">Live Monitor</h2>
<p className="text-zinc-400 mb-8 flex-1">Continuous activity recognition. Monitors posture (Sitting/Standing) and detects falls in real-time.</p>
<div className="flex items-center gap-2 text-green-400 font-bold uppercase tracking-widest text-sm group-hover:gap-4 transition-all">
Launch Monitor <ArrowRight className="w-4 h-4" />
</div>
</motion.div>
</Link>
</div>
{/* Recent Activity Section */}
<div className="mt-16 bg-white p-8 rounded-[2rem] border border-zinc-200 shadow-sm">
<h3 className="text-xl font-bold text-zinc-800 mb-6 flex items-center gap-2">
<ActivityIcon className="w-5 h-5 text-blue-500" />
Recent Live Activity
</h3>
<ActivityList />
</div>
<footer className="mt-16 text-center">
<Link href="/" className="text-zinc-400 hover:text-zinc-600 text-sm transition-colors">Log Out</Link>
</footer>
</div>
</div>
);
}
function ActivityList() {
const { user } = useAuth();
const [logs, setLogs] = React.useState<any[]>([]);
React.useEffect(() => {
if (user) {
fetch('/api/logs', { headers: { 'x-user-id': user.id } })
.then(res => res.json())
.then(data => {
if (data.logs) setLogs(data.logs);
});
}
}, [user]);
if (logs.length === 0) {
return <div className="text-zinc-400 italic">No recent activity detected.</div>;
}
return (
<div className="overflow-x-auto">
<table className="w-full text-left">
<thead>
<tr className="text-zinc-400 text-xs uppercase tracking-wider border-b border-zinc-100">
<th className="pb-3 font-normal">Time</th>
<th className="pb-3 font-normal">Status</th>
<th className="pb-3 font-normal">Details</th>
</tr>
</thead>
<tbody className="divide-y divide-zinc-50">
{logs.map((log) => (
<tr key={log.id} className="group hover:bg-zinc-50 transition-colors">
<td className="py-3 text-sm text-zinc-500">
{new Date(log.timestamp).toLocaleTimeString()}
</td>
<td className="py-3">
<span className={`inline-flex items-center px-2 py-1 rounded-md text-xs font-medium ${
log.status.includes('Fall') || log.status.includes('ALARM')
? 'bg-red-50 text-red-600 border border-red-100'
: 'bg-green-50 text-green-600 border border-green-100'
}`}>
{log.status}
</span>
</td>
<td className="py-3 text-sm text-zinc-400">
{JSON.stringify(log.details)}
</td>
</tr>
))}
</tbody>
</table>
</div>
);
}

View File

@ -0,0 +1,907 @@
'use client';
import React, { useEffect, useRef, useState } from 'react';
import { HARCore } from '@/lib/pose/HARCore';
// Import from the official tasks-vision package
import { PoseLandmarker, FilesetResolver, DrawingUtils, PoseLandmarkerResult } from '@mediapipe/tasks-vision';
import { RefreshCcw, ArrowLeft, PlayCircle, ChevronDown, ChevronUp } from 'lucide-react';
import Link from 'next/link';
import { useSearchParams } from 'next/navigation';
import { Suspense } from 'react';
import { AuthProvider, useAuth } from '@/lib/auth';
export default function TrainingPageWrap() {
return (
<AuthProvider>
<Suspense fallback={<div className="min-h-screen bg-zinc-900 flex items-center justify-center text-white">Loading...</div>}>
<TrainingPage />
</Suspense>
</AuthProvider>
);
}
function TrainingPage() {
const { user } = useAuth();
const searchParams = useSearchParams();
const mode = searchParams.get('mode');
const videoRef = useRef<HTMLVideoElement>(null);
const canvasRef = useRef<HTMLCanvasElement>(null);
const [isLoading, setIsLoading] = useState(true);
const [isStarted, setIsStarted] = useState(false);
// Workflow State
const [menu, setMenu] = useState<any>(null);
const [currentExerciseIndex, setCurrentExerciseIndex] = useState(0);
// const [currentSet, setCurrentSet] = useState(1); // REMOVED: Linear Progression uses index only
const [repsOffset, setRepsOffset] = useState(0); // Offset for accumulated reps
const [stats, setStats] = useState({ exercise: '', reps: 0, status: 'Idle', feedback: '', mae: 0});
const [isWorkoutComplete, setIsWorkoutComplete] = useState(false);
const [isSaving, setIsSaving] = useState(false);
const [feedbackMsg, setFeedbackMsg] = useState<string>("");
const [isWarning, setIsWarning] = useState<boolean>(false);
// UI State
const [expandedSet, setExpandedSet] = useState<number | null>(null);
// Recap State
const [results, setResults] = useState<any[]>([]);
const maeBuffer = useRef<number[]>([]);
// Per-Rep Tracking
const repBuffer = useRef<number[]>([]);
const repFeedbackBuffer = useRef<string[]>([]); // Buffer for feedback text
const lastRepCount = useRef(0);
const currentSetReps = useRef<{rep: number, score: number, feedback: string}[]>([]);
// Rest Timer State
const [isResting, setIsResting] = useState(false);
const [restTimer, setRestTimer] = useState(0);
// Refs for loop
const harRef = useRef<HARCore | null>(null);
const landmarkerRef = useRef<PoseLandmarker | null>(null);
const requestRef = useRef<number | null>(null);
const isRestingRef = useRef(false);
const isStartedRef = useRef(false);
useEffect(() => {
isRestingRef.current = isResting;
}, [isResting]);
useEffect(() => {
isStartedRef.current = isStarted;
}, [isStarted]);
// API Base URL
const API_BASE = process.env.NEXT_PUBLIC_API_URL || '';
// Fetch Latest Menu
const fetchMenu = async () => {
// Check for Free Mode
if (mode === 'free') {
const local = localStorage.getItem('straps_free_mode_menu');
if (local) {
const menuData = JSON.parse(local);
setMenu(menuData);
setCurrentExerciseIndex(0);
setRepsOffset(0);
return;
}
}
if (!user) return;
const headers = { 'x-user-id': user.id.toString() };
try {
const res = await fetch(`${API_BASE}/api/menus`, { headers });
const data = await res.json();
if (data && data.length > 0) {
const latest = data[0];
if (typeof latest.exercises === 'string') latest.exercises = JSON.parse(latest.exercises);
setMenu(latest);
setCurrentExerciseIndex(0);
setRepsOffset(0);
}
} catch (err) {
console.error("Failed to fetch menu:", err);
}
};
// Init Logic and Load Models
useEffect(() => {
let isMounted = true;
async function init() {
try {
// 1. Fetch Menu
await fetchMenu();
// 2. Init Core
const core = new HARCore();
harRef.current = core;
// 3. Init Vision
const vision = await FilesetResolver.forVisionTasks(
"https://cdn.jsdelivr.net/npm/@mediapipe/tasks-vision@0.10.14/wasm"
);
if (!isMounted) return;
const landmarker = await PoseLandmarker.createFromOptions(vision, {
baseOptions: {
modelAssetPath: `https://storage.googleapis.com/mediapipe-models/pose_landmarker/pose_landmarker_full/float16/1/pose_landmarker_full.task`,
delegate: "GPU"
},
runningMode: "VIDEO",
numPoses: 1
});
landmarkerRef.current = landmarker;
// 4. Init Camera
if (navigator.mediaDevices && navigator.mediaDevices.getUserMedia) {
const stream = await navigator.mediaDevices.getUserMedia({
video: { width: 640, height: 480 }
});
if (videoRef.current) {
videoRef.current.srcObject = stream;
try {
await videoRef.current.play();
} catch (err) {
console.warn("Video play aborted (harmless):", err);
}
setIsLoading(false);
requestRef.current = requestAnimationFrame(predictWebcam);
}
}
} catch (e) {
console.error("Init Error:", e);
setIsLoading(false);
}
}
init();
return () => {
isMounted = false;
if (requestRef.current) cancelAnimationFrame(requestRef.current);
if (videoRef.current && videoRef.current.srcObject) {
(videoRef.current.srcObject as MediaStream).getTracks().forEach(t => t.stop());
}
};
}, [user, mode]); // Trigger init/fetch when mode changes
// Rest Timer Countdown
useEffect(() => {
let interval: NodeJS.Timeout;
if (isResting && restTimer > 0) {
interval = setInterval(() => {
setRestTimer((prev) => prev - 1);
}, 1000);
} else if (isResting && restTimer <= 0) {
// Rest Finished
setIsResting(false);
// Re-sync offset to ignore any movements during rest
setRepsOffset(stats.reps);
}
return () => clearInterval(interval);
}, [isResting, restTimer, stats.reps]);
// Effect: Update Active Exercise in HAR Core
useEffect(() => {
if (menu && harRef.current) {
const range = menu.exercises?.[currentExerciseIndex];
if (range) {
harRef.current.setExercise(range.name);
}
}
}, [menu, currentExerciseIndex]);
// Frame Loop Logic
const lastVideoTimeRef = useRef(-1);
const predictWebcam = async () => {
const video = videoRef.current;
const canvas = canvasRef.current;
const landmarker = landmarkerRef.current;
const har = harRef.current;
if (video && canvas && landmarker && har) {
let startTimeMs = performance.now();
if (lastVideoTimeRef.current !== video.currentTime && video.videoWidth > 0 && video.videoHeight > 0) {
lastVideoTimeRef.current = video.currentTime;
const result = landmarker.detectForVideo(video, startTimeMs);
// Draw
const ctx = canvas.getContext('2d');
if (ctx) {
ctx.save();
ctx.clearRect(0, 0, canvas.width, canvas.height);
// Mirror
ctx.scale(-1, 1);
ctx.translate(-canvas.width, 0);
// Draw Video Frame
ctx.drawImage(video, 0, 0, canvas.width, canvas.height);
if (result.landmarks) {
const drawingUtils = new DrawingUtils(ctx);
for (const lm of result.landmarks) {
// Config
const connectors = PoseLandmarker.POSE_CONNECTIONS;
// Draw Connections
ctx.lineCap = 'round';
ctx.lineJoin = 'round';
for (const { start, end } of connectors) {
const p1 = lm[start];
const p2 = lm[end];
if (!p1 || !p2 || (p1.visibility && p1.visibility < 0.5) || (p2.visibility && p2.visibility < 0.5)) continue;
ctx.beginPath();
ctx.moveTo(p1.x * canvas.width, p1.y * canvas.height);
ctx.lineTo(p2.x * canvas.width, p2.y * canvas.height);
let color = '#00FFFF'; // Cyan
let glow = '#00FFFF'; // Cyan Glow
ctx.shadowColor = glow;
ctx.shadowBlur = 15;
ctx.strokeStyle = color;
ctx.lineWidth = 4;
ctx.stroke();
}
// Draw Joints
for (let i = 0; i < lm.length; i++) {
const p = lm[i];
if (p.visibility && p.visibility < 0.5) continue;
if (i < 11 && i !== 0) continue; // Keep nose (0)
ctx.beginPath();
ctx.arc(p.x * canvas.width, p.y * canvas.height, 5, 0, 2 * Math.PI);
ctx.fillStyle = '#FFFFFF'; // White core
ctx.shadowColor = '#00FFFF'; // Cyan glow
ctx.shadowBlur = 20;
ctx.fill();
}
ctx.shadowBlur = 0;
}
}
ctx.restore();
}
// Process Logic
if (isStartedRef.current && !isRestingRef.current && result.landmarks && result.landmarks.length > 0) {
// Pass normalized landmarks (x,y,z,visibility) AND world landmarks (meters)
const res = await har.process(
result.landmarks[0] as any,
result.worldLandmarks[0] as any
);
if (res) {
// Accumulate Form Score (MAE)
if (res.debug && (res.debug as any).scores && (res.debug as any).scores.deviation_mae) {
const val = (res.debug as any).scores.deviation_mae;
if (val > 0) {
maeBuffer.current.push(val);
repBuffer.current.push(val); // Push to current rep buffer
}
}
// Capture Feedback Text
if (res.feedback && res.feedback.trim() !== "" && !res.feedback.includes("null")) {
// Only push meaningful feedback
repFeedbackBuffer.current.push(res.feedback);
}
// --- Rep Completion Logic ---
if (res.reps > lastRepCount.current) {
// Rep Finished!
const avgRepScore = repBuffer.current.length > 0
? repBuffer.current.reduce((a, b) => a + b, 0) / repBuffer.current.length
: 0;
// Calculate Dominant Feedback
let dominantFeedback = "Perfect";
if (repFeedbackBuffer.current.length > 0) {
// Find most frequent string
const counts: Record<string, number> = {};
let maxCount = 0;
let maxKey = "";
for (const fb of repFeedbackBuffer.current) {
const cleanFb = fb.trim();
counts[cleanFb] = (counts[cleanFb] || 0) + 1;
if (counts[cleanFb] > maxCount) {
maxCount = counts[cleanFb];
maxKey = cleanFb;
}
}
if (maxKey) dominantFeedback = maxKey;
}
currentSetReps.current.push({
rep: res.reps,
score: avgRepScore,
feedback: dominantFeedback
});
// Reset for next rep
repBuffer.current = [];
repFeedbackBuffer.current = [];
lastRepCount.current = res.reps;
}
setStats({
status: res.status,
exercise: res.exercise || 'Unknown',
reps: res.reps, // Reps from RehabCore
feedback: res.feedback,
mae: (res.debug as any)?.scores?.deviation_mae || 0
});
// Update Feedback UI State
if (res.feedback) {
setFeedbackMsg(res.feedback);
setIsWarning(res.feedback.includes("⚠️"));
} else {
setFeedbackMsg("");
setIsWarning(false);
}
}
}
}
}
requestRef.current = requestAnimationFrame(predictWebcam);
};
// Progression Logic
useEffect(() => {
if (!menu || isWorkoutComplete) return;
const currentTarget = menu.exercises[currentExerciseIndex];
if (!currentTarget) {
finishWorkout();
return;
}
// Calculate Reps in Current Set
const currentRepsInSet = Math.max(0, stats.reps - repsOffset);
const isMatchingExercise = stats.exercise && numberSafeMatch(stats.exercise, currentTarget.name);
if (isMatchingExercise) {
if (currentRepsInSet >= currentTarget.reps) {
// --- SET COMPLETE LOGIC ---
// 1. Calculate Average Form Score
const avgMae = maeBuffer.current.length > 0
? maeBuffer.current.reduce((a, b) => a + b, 0) / maeBuffer.current.length
: 0;
// 2. Save Result
setResults(prev => [...prev, {
name: currentTarget.name,
set: currentTarget.set_index || 1,
reps: currentRepsInSet,
weight: currentTarget.weight,
score: avgMae,
repDetails: [...currentSetReps.current] // CAPTURE REP DETAILS
}]);
// 3. Reset Buffers
maeBuffer.current = [];
repBuffer.current = [];
repFeedbackBuffer.current = []; // Reset feedback too
currentSetReps.current = [];
lastRepCount.current = 0; // Reset for next set
// Linear Logic: Next Exercise in List
const nextExIdx = currentExerciseIndex + 1;
const restTime = (currentTarget as any).rest_time_seconds || 0;
if (nextExIdx >= menu.exercises.length) {
finishWorkout();
} else {
setCurrentExerciseIndex(nextExIdx);
setRepsOffset(stats.reps); // Important: Offset total reps
if (restTime > 0) {
setIsResting(true);
setRestTimer(restTime);
}
}
}
}
}, [stats.reps, stats.exercise, menu, currentExerciseIndex, repsOffset]);
const numberSafeMatch = (a: string, b: string) => {
return a.toLowerCase().includes(b.split(' ')[0].toLowerCase());
}
const saveRecap = async (summary: any) => {
if (!menu || !user) return;
try {
await fetch(`${API_BASE}/api/recap`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'x-user-id': user.id
},
body: JSON.stringify({
menu_id: menu.id,
user_id: user.id,
summary: {
...summary,
detailed_results: results // Send detailed results to backend
}
})
});
} catch (e) {
console.error("Failed to save recap:", e);
}
};
const finishWorkout = async () => {
if (isWorkoutComplete || !menu) return;
setIsWorkoutComplete(true);
setIsSaving(true);
try {
await saveRecap({
completed: true,
exercises: menu.exercises,
timestamp: new Date().toISOString()
});
} catch (e) { console.error(e); }
finally { setIsSaving(false); }
};
// Helper for Form Grade
const getGrade = (mae: number) => {
if (mae < 8) return { letter: 'S', color: 'text-purple-400', label: 'Excellent' };
if (mae < 15) return { letter: 'A', color: 'text-green-400', label: 'Good' };
if (mae < 25) return { letter: 'B', color: 'text-yellow-400', label: 'Fair' };
return { letter: 'C', color: 'text-red-400', label: 'Needs Improvement' };
};
if (isWorkoutComplete) {
return (
<div className="min-h-screen bg-zinc-950 text-white flex flex-col items-center justify-center p-4 font-sans">
<div className="max-w-2xl w-full bg-zinc-900 rounded-3xl border border-zinc-800 p-8 shadow-2xl relative overflow-hidden">
{/* Cyberpunk Glow */}
<div className="absolute top-0 left-0 w-full h-1 bg-gradient-to-r from-blue-500 via-primary to-purple-500"></div>
<div className="text-center mb-8">
<div className="inline-block px-4 py-1 rounded-full bg-green-500/10 text-green-400 text-xs font-bold tracking-widest uppercase mb-4 border border-green-500/20">
Session Complete
</div>
<h1 className="text-4xl font-black text-white tracking-tight mb-2">TRAINING RECAP</h1>
<p className="text-zinc-500 text-sm">Excellent work. Here is your performance breakdown.</p>
</div>
{/* Stats Grid */}
<div className="grid grid-cols-2 gap-4 mb-8">
<div className="bg-zinc-950/50 p-6 rounded-2xl border border-zinc-800 text-center">
<div className="text-3xl font-black text-white">{results.length}</div>
<div className="text-xs font-bold text-zinc-500 uppercase tracking-widest mt-1">Sets Completed</div>
</div>
<div className="bg-zinc-950/50 p-6 rounded-2xl border border-zinc-800 text-center">
<div className="text-3xl font-black text-primary">
{results.reduce((a, b) => a + b.reps, 0)}
</div>
<div className="text-xs font-bold text-zinc-500 uppercase tracking-widest mt-1">Total Reps</div>
</div>
</div>
{/* Detailed Results Table */}
<div className="bg-zinc-950/30 rounded-2xl border border-zinc-800 overflow-hidden mb-8 max-h-[40vh] overflow-y-auto">
<table className="w-full text-sm">
<thead className="bg-zinc-900 border-b border-zinc-800">
<tr>
<th className="px-4 py-3 text-left font-bold text-zinc-500 uppercase tracking-wider text-[10px]">Exercise</th>
<th className="px-4 py-3 text-center font-bold text-zinc-500 uppercase tracking-wider text-[10px]">Set</th>
<th className="px-4 py-3 text-center font-bold text-zinc-500 uppercase tracking-wider text-[10px]">Load</th>
<th className="px-4 py-3 text-right font-bold text-zinc-500 uppercase tracking-wider text-[10px]">Form Score</th>
</tr>
</thead>
<tbody className="divide-y divide-zinc-800">
{results.map((res, i) => {
const grade = getGrade(res.score);
const isExpanded = expandedSet === i;
return (
<React.Fragment key={i}>
<tr
onClick={() => setExpandedSet(isExpanded ? null : i)}
className="hover:bg-zinc-800/50 transition-colors cursor-pointer group"
>
<td className="px-4 py-3 font-medium text-white flex items-center gap-2">
{isExpanded ? <ChevronUp size={14} className="text-zinc-500" /> : <ChevronDown size={14} className="text-zinc-500" />}
{res.name}
</td>
<td className="px-4 py-3 text-center text-zinc-400 font-mono">#{res.set}</td>
<td className="px-4 py-3 text-center text-zinc-400">
{res.reps}x <span className="text-zinc-600">@</span> {res.weight}kg
</td>
<td className="px-4 py-3 text-right">
<div className="flex flex-col items-end gap-1">
<div className="flex items-center gap-2">
<span className={`font-black ${grade.color}`}>{grade.label}</span>
<span className="text-[10px] text-zinc-600 font-mono">Avg: {res.score.toFixed(1)}°</span>
</div>
{/* Preview Chips */}
{!isExpanded && res.repDetails && res.repDetails.length > 0 && (
<div className="flex justify-end gap-1">
{res.repDetails.map((r: any, idx: number) => (
<div key={idx} className={`w-1.5 h-1.5 rounded-full ${getGrade(r.score).color.replace('text-','bg-')}`} />
))}
</div>
)}
</div>
</td>
</tr>
{/* Expanded Detail Row */}
{isExpanded && (
<tr className="bg-zinc-900/50">
<td colSpan={4} className="px-4 py-4">
<div className="grid grid-cols-2 sm:grid-cols-3 gap-2">
{res.repDetails?.map((r: any, idx: number) => {
const rGrade = getGrade(r.score);
const isPerfect = rGrade.label === 'Excellent';
const hasFeedback = r.feedback && r.feedback !== 'Perfect';
return (
<div key={idx} className="bg-zinc-950 border border-zinc-800 rounded-lg p-3 flex flex-col gap-1">
<div className="flex justify-between items-center w-full">
<div className="flex items-center gap-2">
<span className="text-xs font-mono text-zinc-500">#{idx + 1}</span>
<span className={`text-xs font-bold ${rGrade.color}`}>{rGrade.label}</span>
</div>
<span className="text-[10px] text-zinc-600 font-mono">{r.score.toFixed(1)}°</span>
</div>
{/* Feedback Text */}
<div className={`text-[10px] uppercase font-bold tracking-wide ${hasFeedback ? 'text-zinc-400' : 'text-zinc-600/50'}`}>
{hasFeedback ? `"${r.feedback}"` : "NO ISSUES DETECTED"}
</div>
</div>
)
})}
{(!res.repDetails || res.repDetails.length === 0) && (
<div className="col-span-3 text-center text-zinc-500 text-xs italic py-2">No individual rep data available.</div>
)}
</div>
</td>
</tr>
)}
</React.Fragment>
);
})}
</tbody>
</table>
</div>
<div className="flex gap-4">
<Link href="/client" className="flex-1 px-6 py-4 bg-white text-black font-bold uppercase tracking-widest rounded-xl hover:bg-zinc-200 transition-colors text-center text-sm">
Back to Dashboard
</Link>
</div>
</div>
</div>
);
}
// ... Render same as before ...
const currentTarget = menu?.exercises?.[currentExerciseIndex];
return (
<div className="min-h-screen bg-background text-foreground p-6 font-sans selection:bg-primary/30">
<header className="flex justify-between items-center mb-8">
<div className="flex items-center gap-4">
<Link href="/client" className="p-2 bg-white rounded-full hover:bg-zinc-100 transition-colors border border-zinc-200">
<ArrowLeft className="w-5 h-5 text-zinc-600" />
</Link>
<h1 className="text-3xl font-light tracking-widest text-zinc-800">TRAINING<span className="font-bold text-primary">.MODE</span></h1>
</div>
<div className="flex items-center gap-4">
<div className="bg-white border border-zinc-200 px-6 py-2 rounded-full text-xs font-medium tracking-wider text-zinc-600 uppercase shadow-sm">
{menu ? menu.name : 'Loading...'}
</div>
<button
onClick={() => {
setMenu(null);
fetchMenu();
}}
className="p-2 bg-white border border-zinc-200 hover:bg-zinc-100 rounded-full transition-colors shadow-sm"
title="Refresh Menu"
>
<RefreshCcw size={18} className="text-zinc-500" />
</button>
<button
onClick={() => {
// Reset Logic
// setCurrentSet(1);
setRepsOffset(0);
setStats(prev => ({ ...prev, reps: 0, status: 'Idle', feedback: 'Reset' }));
if (harRef.current) harRef.current.resetParams();
setIsResting(false);
}}
className="px-4 py-2 bg-red-50 text-red-600 rounded-full text-xs font-bold uppercase tracking-widest border border-red-100 hover:bg-red-100 transition-colors"
>
Reset
</button>
<div className="flex bg-zinc-100 p-1 rounded-full border border-zinc-200">
<Link
href="/client/training"
className={`px-4 py-1.5 rounded-full text-xs font-bold uppercase tracking-widest transition-all ${
!(new URLSearchParams(typeof window !== 'undefined' ? window.location.search : '').get('mode') === 'free')
? 'bg-white text-primary shadow-sm'
: 'text-zinc-400 hover:text-zinc-600'
}`}
>
Assigned
</Link>
<Link
href="/client/training?mode=free"
className={`px-4 py-1.5 rounded-full text-xs font-bold uppercase tracking-widest transition-all ${
(new URLSearchParams(typeof window !== 'undefined' ? window.location.search : '').get('mode') === 'free')
? 'bg-white text-primary shadow-sm'
: 'text-zinc-400 hover:text-zinc-600'
}`}
>
Personal
</Link>
</div>
</div>
</header>
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
<div className="lg:col-span-2 relative border-8 border-white rounded-[2rem] overflow-hidden bg-zinc-100 shadow-2xl">
{isLoading && <div className="absolute inset-0 flex items-center justify-center text-blue-400 font-mono animate-pulse">Loading AI Engine (WASM)...</div>}
<video ref={videoRef} className="hidden" width="640" height="480" autoPlay playsInline muted />
<canvas ref={canvasRef} width="640" height="480" className="w-full h-auto object-contain" />
{/* Status Display Removed as per user request (kept in Live Monitor) */}
{/* <div className="absolute top-6 left-6 flex gap-4">
<div className={`px-4 py-1.5 rounded-full text-xs font-bold tracking-widest backdrop-blur-md border ${stats.status === 'Fall Detected' ? 'bg-red-500/20 border-red-500 text-red-400' : 'bg-primary/10 border-primary/30 text-primary'}`}>
{stats.status.toUpperCase()}
</div>
</div> */}
{/* --- NEW: FEEDBACK OVERLAY WINDOW --- */}
{/* Rest Overlay */}
{isResting && (
<div className="absolute inset-0 bg-black/95 z-50 flex flex-col items-center justify-center animate-in fade-in duration-500">
<div className="text-secondary font-medium tracking-[0.2em] uppercase mb-6 text-sm">Recovery Break</div>
<div className="text-9xl font-light text-highlight mb-10 tabular-nums tracking-tighter">
{Math.floor(restTimer / 60)}:{(restTimer % 60).toString().padStart(2, '0')}
</div>
<button
onClick={() => setIsResting(false)}
className="px-10 py-3 bg-secondary/10 hover:bg-secondary/20 hover:scale-105 text-secondary rounded-full font-medium transition-all text-xs uppercase tracking-widest border border-secondary/30"
>
Resume Workout
</button>
</div>
)}
{/* Start Overlay */}
{!isStarted && !isLoading && (
<div className="absolute inset-0 bg-white/80 backdrop-blur-sm z-40 flex flex-col items-center justify-center p-8 text-center">
<h2 className="text-3xl font-bold text-zinc-900 mb-2">Ready to Train?</h2>
<p className="text-zinc-600 mb-8 max-w-md">
{menu ? `Start your assigned program: ${menu.name}` : `No assigned program found.`}
</p>
<div className="flex flex-col gap-4">
{menu && (
<button
onClick={() => setIsStarted(true)}
className="px-12 py-4 bg-primary text-white text-lg font-bold rounded-full shadow-xl hover:shadow-2xl hover:scale-105 transition-all flex items-center justify-center gap-3 w-64"
>
<PlayCircle className="w-6 h-6" /> START {menu.name ? 'PROGRAM' : 'WORKOUT'}
</button>
)}
<div className="flex items-center gap-4 w-64">
<div className="h-px bg-zinc-300 flex-1"></div>
<span className="text-xs text-zinc-400 font-bold uppercase">OR</span>
<div className="h-px bg-zinc-300 flex-1"></div>
</div>
<Link
href="/client/free"
className="px-12 py-4 bg-white border-2 border-zinc-200 text-zinc-600 text-lg font-bold rounded-full hover:border-primary hover:text-primary transition-all flex items-center justify-center gap-3 w-64 text-center"
>
CREATE PERSONAL MENU
</Link>
{new URLSearchParams(typeof window !== 'undefined' ? window.location.search : '').get('mode') === 'free' && (
<Link
href="/client/training"
className="mt-2 text-zinc-400 text-xs font-bold uppercase tracking-widest hover:text-primary transition-colors"
>
Return to Assigned Program
</Link>
)}
</div>
</div>
)}
</div>
<div className="flex flex-col gap-6">
{/* Workout Menu List */}
<div className="bg-white rounded-2xl border border-zinc-200 overflow-hidden flex flex-col max-h-[40vh] shadow-lg">
<div className="p-4 border-b border-zinc-100 bg-zinc-50">
<h2 className="text-zinc-500 text-xs font-bold uppercase tracking-widest">Workout Plan</h2>
</div>
<div className="overflow-y-auto p-2 space-y-2">
{menu?.exercises?.map((ex: any, idx: number) => {
const isActive = idx === currentExerciseIndex;
const isCompleted = idx < currentExerciseIndex;
return (
<React.Fragment key={idx}>
{(idx === 0 || ex.set_index > (menu.exercises[idx - 1]?.set_index || 0)) && (
<div className="py-4 flex items-center gap-4">
<div className="h-px bg-zinc-200 flex-1"></div>
<span className="text-xs font-black text-zinc-400 uppercase tracking-[0.2em] bg-white px-2 rounded-lg">
SET {ex.set_index || 1}
</span>
<div className="h-px bg-zinc-200 flex-1"></div>
</div>
)}
<div
className={`p-5 rounded-2xl transition-all border ${
isActive
? 'bg-blue-50 border-primary shadow-sm'
: isCompleted
? 'opacity-40 grayscale border-transparent'
: 'bg-white border-zinc-100'
}`}
>
<div className="flex justify-between items-center">
<div>
<div className={`text-lg tracking-wide ${isActive ? 'font-bold text-zinc-900' : 'text-zinc-500 font-medium'}`}>
{ex.name}
</div>
<div className="text-[10px] text-secondary/70 uppercase tracking-widest mt-1">
Target: {ex.reps} reps {ex.weight}kg
</div>
</div>
{isActive && (
<div className="text-right">
<div className="text-3xl font-light text-primary">
{Math.max(0, stats.reps - repsOffset)}<span className="text-sm text-secondary/50 font-normal ml-1">/ {ex.reps}</span>
</div>
<div className="text-[10px] text-blue-300 uppercase tracking-wider font-bold animate-pulse">
Set {ex.set_index || 1}/{ex.total_sets || 1}
</div>
{(ex as any).rest_time_seconds > 0 && (
<div className="text-[10px] text-zinc-500 mt-1">
Rest: {(ex as any).rest_time_seconds}s
</div>
)}
</div>
)}
{isCompleted && (
<div className="text-green-500">
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
</svg>
</div>
)}
</div>
</div>
</React.Fragment>
);
})}
{!menu && (
<div className="p-4 text-center text-zinc-500 italic">No menu loaded</div>
)}
</div>
</div>
<div className="bg-white p-4 rounded-2xl border border-zinc-200 flex flex-col justify-center items-center text-center shadow-lg h-48">
<h2 className="text-zinc-400 text-[10px] font-bold uppercase tracking-widest mb-2">Real-time Counter</h2>
<div className="relative">
<svg className="w-32 h-32 transform -rotate-90">
<circle cx="64" cy="64" r="56" stroke="currentColor" strokeWidth="6" fill="transparent" className="text-zinc-100" />
<circle
cx="64" cy="64" r="56"
stroke="currentColor" strokeWidth="6" fill="transparent"
className="text-primary transition-all duration-500 ease-out drop-shadow-md"
strokeDasharray={2 * Math.PI * 56}
strokeDashoffset={2 * Math.PI * 56 * (1 - (Math.max(0, stats.reps - repsOffset) / (currentTarget?.reps || 1)))}
/>
</svg>
<div className="absolute inset-0 flex items-center justify-center flex-col">
<span className="text-4xl font-black text-zinc-900">{Math.max(0, stats.reps - repsOffset)}</span>
<span className="text-zinc-400 text-[10px] font-medium">REPS</span>
</div>
</div>
</div>
{/* Redesigned Cyberpunk Feedback Card (Expanded) */}
<div className={`p-0.5 rounded-xl flex-1 bg-gradient-to-r ${
isWarning ? 'from-red-500 via-rose-500 to-red-500 animate-pulse' : 'from-cyan-400 via-blue-500 to-cyan-400'
}`}>
<div className="bg-zinc-50 rounded-[10px] p-6 h-full relative overflow-hidden flex flex-col justify-center">
{/* Scanline effect (Subtle Light Mode) */}
<div className="absolute inset-0 bg-[linear-gradient(rgba(0,0,0,0)_50%,rgba(0,0,0,0.02)_50%),linear-gradient(90deg,rgba(0,0,0,0.03),rgba(0,0,0,0.01),rgba(0,0,0,0.03))] z-0 pointer-events-none bg-[length:100%_4px,6px_100%]"></div>
<div className="relative z-10 text-center md:text-left">
<div className="flex justify-between items-start mb-4">
<h3 className={`text-xs font-black uppercase tracking-[0.2em] ${
isWarning ? 'text-red-500 drop-shadow-sm' : 'text-cyan-600 drop-shadow-sm'
}`}>
{isWarning ? 'CRITICAL ERROR' : 'SYSTEM ADVICE'}
</h3>
{isWarning && <div className="w-3 h-3 bg-red-500 rounded-full animate-ping"></div>}
</div>
<p className={`text-2xl font-bold leading-tight uppercase font-mono break-words ${
isWarning ? 'text-red-600' : 'text-zinc-800'
}`}>
{(stats.feedback || "SYSTEM READY").replace(/⚠️|✅|❌/g, '').replace(" | ", "\n").trim() || "WAITING FOR INPUT..."}
</p>
</div>
</div>
</div>
{/* Form Quality (MAE) - Expanded */}
<div className={`rounded-xl border-l-8 overflow-hidden shadow-lg bg-white h-32 flex flex-col justify-center ${
stats.mae > 15
? 'border-red-500'
: stats.mae > 5
? 'border-yellow-400'
: 'border-emerald-500'
}`}>
<div className="px-6 py-2 flex justify-between items-center h-full">
<div className="flex flex-col text-left">
<span className="text-xs font-black uppercase tracking-widest text-zinc-400">Form Quality</span>
<span className={`text-2xl uppercase font-black mt-1 ${
stats.mae > 15 ? 'text-red-600' : stats.mae > 5 ? 'text-yellow-600' : 'text-emerald-600'
}`}>
{stats.mae > 15 ? 'Needs Improvement' : stats.mae > 5 ? 'Fair' : 'Excellent'}
</span>
</div>
<div className="text-right">
<div className="text-5xl font-black tabular-nums leading-none text-zinc-900 tracking-tighter">
{stats.mae.toFixed(1)}°
</div>
<span className="text-[10px] uppercase text-zinc-400 tracking-wider font-bold">Deviation</span>
</div>
</div>
{/* Mini Graph Bar */}
<div className="h-2 w-full bg-zinc-100 flex mt-auto">
<div
className={`h-full transition-all duration-500 ${stats.mae > 15 ? 'bg-red-500' : stats.mae > 5 ? 'bg-yellow-400' : 'bg-emerald-500'}`}
style={{ width: `${Math.min(100, (stats.mae / 30) * 100)}%` }}
></div>
</div>
</div>
</div>
</div>
</div>
);
}

View File

@ -0,0 +1,380 @@
'use client';
import React, { useEffect, useState } from 'react';
import { motion } from 'framer-motion';
import { Activity, User, Clock, ShieldAlert, Plus, Users, UserPlus } from 'lucide-react';
import Link from 'next/link';
import { AuthProvider, useAuth } from '@/lib/auth';
export default function DashboardPageWrap() {
return (
<AuthProvider>
<DashboardPage />
</AuthProvider>
);
}
function DashboardPage() {
const { user } = useAuth();
const [greeting, setGreeting] = useState('');
const [stats, setStats] = useState({
totalMenus: 0,
totalSessions: 0, // All time
sessionsToday: 0,
recentMenus: [] as any[],
recentRecaps: [] as any[],
linkedClients: [] as any[]
});
// Add Client State
const [isAddingClient, setIsAddingClient] = useState(false);
const [clientIdToAdd, setClientIdToAdd] = useState('');
const [addClientStatus, setAddClientStatus] = useState('');
const loadData = React.useCallback(async () => {
if (!user) return;
// Security Check
if (user.role !== 'COACH') {
window.location.href = '/';
return;
}
try {
const headers = { 'x-user-id': user.id };
// Fetch Recaps
const resRecaps = await fetch('/api/recap', { headers });
const recaps = await resRecaps.json();
// Fetch Menus
const resMenus = await fetch('/api/menus', { headers });
const menus = await resMenus.json();
// Fetch Linked Clients
const resClients = await fetch(`/api/users?coachId=${user.id}`);
const clients = await resClients.json();
if (Array.isArray(recaps) && Array.isArray(menus)) {
const today = new Date().toDateString();
const todaysRecaps = recaps.filter((r: any) => new Date(r.completed_at).toDateString() === today);
setStats({
totalMenus: menus.length,
totalSessions: recaps.length,
sessionsToday: todaysRecaps.length,
recentMenus: menus.sort((a: any, b: any) => new Date(b.created_at).getTime() - new Date(a.created_at).getTime()).slice(0, 5),
recentRecaps: recaps.sort((a: any, b: any) => new Date(b.completed_at).getTime() - new Date(a.completed_at).getTime()).slice(0, 5),
linkedClients: Array.isArray(clients) ? clients : []
});
}
} catch (e) {
console.error("Failed to load dashboard stats", e);
}
}, [user]);
useEffect(() => {
const hour = new Date().getHours();
if (hour < 12) setGreeting('Good Morning');
else if (hour < 18) setGreeting('Good Afternoon');
else setGreeting('Good Evening');
loadData();
}, [user, loadData]);
const handleAddClient = async (e: React.FormEvent) => {
e.preventDefault();
if (!user || !clientIdToAdd) return;
setAddClientStatus('Linking...');
try {
const res = await fetch('/api/coach/link-client', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ coachId: user.id, clientId: clientIdToAdd })
});
const data = await res.json();
if (res.ok) {
setAddClientStatus('Client Linked!');
setClientIdToAdd('');
setIsAddingClient(false);
// Refresh data
await loadData();
} else {
setAddClientStatus(data.error || 'Failed to link');
}
} catch (e) {
setAddClientStatus('Error linking client');
}
};
// Staggered animation variants
const container = {
hidden: { opacity: 0 },
show: {
opacity: 1,
transition: { staggerChildren: 0.1 }
}
};
const item = {
hidden: { opacity: 0, y: 20 },
show: { opacity: 1, y: 0 }
};
return (
<div className="min-h-screen bg-background text-foreground p-10 font-sans selection:bg-primary/30">
<header className="mb-16 border-b border-zinc-200 pb-8 flex flex-col md:flex-row justify-between items-end gap-6">
<div>
<h1 className="text-5xl font-light tracking-tight text-zinc-800 mb-2">
{greeting}, <span className="font-bold text-primary">{user?.name || 'Coach'}</span>.
</h1>
<p className="text-secondary text-sm tracking-wide mt-2 flex items-center gap-3">
<span className="w-2 h-2 rounded-full bg-green-500 shadow-[0_0_8px_rgba(34,197,94,0.5)]"></span>
SYSTEM ACTIVE
</p>
</div>
<Link href="/coach/menu/new" className="bg-primary hover:bg-primary/90 text-black px-8 py-3 rounded-full font-bold transition-all hover:scale-105 shadow-[0_0_20px_-5px_var(--color-primary)] flex items-center gap-2 text-sm tracking-wide">
<Plus className="w-4 h-4" /> NEW PROGRAM
</Link>
</header>
<motion.div
className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6"
variants={container}
initial="hidden"
animate="show"
>
<StatsCard
title="Total Training Menus"
value={stats.totalMenus.toString()}
icon={<Activity className="text-blue-400" />}
variant={item}
/>
<StatsCard
title="Total Sessions (All Time)"
value={stats.totalSessions.toString()}
icon={<User className="text-purple-400" />}
variant={item}
/>
<StatsCard
title="Sessions Today"
value={stats.sessionsToday.toString()}
icon={<ShieldAlert className="text-green-400" />}
variant={item}
/>
</motion.div>
<motion.div
className="mt-12 bg-white border border-zinc-200 rounded-xl p-6 shadow-sm"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ delay: 0.5 }}
>
<h2 className="text-xl font-bold mb-4">Live Activity Feed</h2>
<div className="space-y-4">
<div className="flex items-center gap-4 text-sm text-gray-400">
<span className="w-16">Now</span>
<span className="text-zinc-900">System monitoring active...</span>
</div>
</div>
</motion.div>
{/* My Clients Section */}
<motion.div
className="mt-6 bg-white border border-zinc-200 rounded-xl p-6 shadow-sm"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ delay: 0.55 }}
>
<div className="flex justify-between items-center mb-4">
<h2 className="text-xl font-bold text-zinc-900">My Clients</h2>
<button
onClick={() => setIsAddingClient(!isAddingClient)}
className="text-primary hover:text-blue-700 text-sm font-bold flex items-center gap-1 bg-blue-50 px-3 py-1 rounded-full transition-colors"
>
<UserPlus className="w-4 h-4" /> Add Client
</button>
</div>
{isAddingClient && (
<form onSubmit={handleAddClient} className="mb-6 bg-zinc-50 p-4 rounded-xl border border-zinc-200 animate-in fade-in slide-in-from-top-2 flex items-center gap-4">
<input
type="text"
placeholder="Enter Client ID"
value={clientIdToAdd}
onChange={(e) => setClientIdToAdd(e.target.value)}
className="bg-white border border-zinc-200 rounded-lg px-4 py-2 text-sm w-48 font-mono"
/>
<button type="submit" className="bg-zinc-900 text-white px-4 py-2 rounded-lg text-sm font-bold hover:bg-black">
Link
</button>
{addClientStatus && <span className="text-xs font-bold text-primary">{addClientStatus}</span>}
</form>
)}
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
{stats.linkedClients.map((client: any) => (
<div key={client.id} className="p-4 rounded-xl border border-zinc-100 bg-zinc-50 flex items-center gap-4">
<div className="w-10 h-10 rounded-full bg-blue-100 flex items-center justify-center text-primary font-bold">
{client.name.charAt(0)}
</div>
<div>
<div className="font-bold text-zinc-900">{client.name}</div>
<div className="text-xs text-zinc-500">ID: {client.id}</div>
</div>
</div>
))}
{stats.linkedClients.length === 0 && (
<div className="col-span-3 text-center py-8 text-zinc-400 italic">No clients linked yet. Add one above.</div>
)}
</div>
</motion.div>
{/* Recent Menus List */}
<motion.div
className="mt-6 bg-white border border-zinc-200 rounded-xl p-6 shadow-sm"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ delay: 0.6 }}
>
<h2 className="text-xl font-bold mb-4 text-zinc-900">Recent Programs</h2>
<div className="overflow-x-auto">
<table className="w-full text-left">
<thead>
<tr className="border-b border-zinc-100 text-zinc-500 text-sm uppercasetracking-wider">
<th className="pb-3 font-medium">Program Name</th>
<th className="pb-3 font-medium">Client</th>
<th className="pb-3 font-medium">Created At</th>
<th className="pb-3 font-medium">Exercises</th>
<th className="pb-3 font-medium">Action</th>
</tr>
</thead>
<tbody className="divide-y divide-zinc-100">
{stats.recentMenus.map((menu: any) => (
<tr key={menu.id} className="group hover:bg-zinc-50 transition-colors">
<td className="py-4 font-medium text-zinc-900">{menu.name}</td>
<td className="py-4 text-zinc-600 font-medium">
{menu.assigned_client ? (
<span className="bg-blue-50 text-blue-700 px-2 py-1 rounded-md text-xs border border-blue-100">
{menu.assigned_client.name}
</span>
) : (
<span className="text-zinc-400 text-xs italic">Unassigned</span>
)}
</td>
<td className="py-4 text-zinc-500 text-sm">
{new Date(menu.created_at).toLocaleDateString()}
</td>
<td className="py-4 text-zinc-500 text-sm">
{(() => {
if (!menu.exercises) return 0;
if (Array.isArray(menu.exercises)) return menu.exercises.length;
try { return JSON.parse(menu.exercises as string).length; } catch { return 0; }
})()} exercises
</td>
<td className="py-4">
<Link
href={`/coach/menu/${menu.id}`}
className="text-primary hover:text-blue-700 text-sm font-bold flex items-center gap-1"
>
View Details
</Link>
</td>
</tr>
))}
{stats.recentMenus.length === 0 && (
<tr>
<td colSpan={4} className="py-8 text-center text-zinc-400 italic">No programs created yet.</td>
</tr>
)}
</tbody>
</table>
</div>
</motion.div>
{/* Recent Recaps List */}
<motion.div
className="mt-6 bg-white border border-zinc-200 rounded-xl p-6 shadow-sm"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ delay: 0.7 }}
>
<h2 className="text-xl font-bold mb-4 text-zinc-900">Recent Activity Reports</h2>
<div className="overflow-x-auto">
<table className="w-full text-left">
<thead>
<tr className="border-b border-zinc-100 text-zinc-500 text-sm uppercasetracking-wider">
<th className="pb-3 font-medium">Date</th>
<th className="pb-3 font-medium">Client</th>
<th className="pb-3 font-medium">Program</th>
<th className="pb-3 font-medium">Status</th>
<th className="pb-3 font-medium">Action</th>
</tr>
</thead>
<tbody className="divide-y divide-zinc-100">
{stats.recentRecaps.map((recap: any) => (
<tr key={recap.id} className="group hover:bg-zinc-50 transition-colors">
<td className="py-4 text-zinc-500 text-sm">
{new Date(recap.completed_at).toLocaleString()}
</td>
<td className="py-4 font-medium text-zinc-900">
<div className="flex items-center gap-2">
<div className="w-6 h-6 rounded-full bg-blue-100 flex items-center justify-center text-blue-600 text-xs font-bold">
{recap.user?.name ? recap.user.name[0] : '?'}
</div>
{recap.user?.name || 'Unknown'}
</div>
</td>
<td className="py-4 text-zinc-600 text-sm">
{recap.training_menus?.name || `Menu #${recap.menu_id}`}
</td>
<td className="py-4">
<span className="bg-green-100 text-green-700 px-3 py-1 rounded-full text-xs font-bold">
COMPLETED
</span>
</td>
<td className="py-4">
<Link
href={`/coach/recap/${recap.id}`}
className="text-primary hover:text-blue-700 text-sm font-bold flex items-center gap-1"
>
View Report
</Link>
</td>
</tr>
))}
{stats.recentRecaps.length === 0 && (
<tr>
<td colSpan={4} className="py-8 text-center text-zinc-400 italic">No activity recorded yet.</td>
</tr>
)}
</tbody>
</table>
</div>
</motion.div>
</div>
);
}
function StatsCard({ title, value, icon, variant }: any) {
return (
<motion.div
variants={variant}
className="p-8 rounded-3xl bg-white border border-zinc-200 hover:border-primary/50 transition-all group relative overflow-hidden shadow-sm hover:shadow-md"
>
<div className="absolute top-0 right-0 p-8 opacity-10 group-hover:opacity-100 group-hover:scale-110 transition-all duration-500 grayscale group-hover:grayscale-0">
{icon}
</div>
<div className="relative z-10">
<h3 className="text-zinc-500 text-xs font-bold uppercase tracking-[0.2em] mb-3">{title}</h3>
<div className="text-5xl font-light text-zinc-900 tracking-tighter">{value}</div>
</div>
</motion.div>
);
}

View File

@ -0,0 +1,150 @@
'use client';
import React, { useEffect, useState } from 'react';
import { useParams, useRouter } from 'next/navigation';
import { ArrowLeft, Calendar, Dumbbell, Clock, Repeat } from 'lucide-react';
import Link from 'next/link';
export default function MenuDetailPage() {
const params = useParams();
const router = useRouter();
const [menu, setMenu] = useState<any>(null);
const [isLoading, setIsLoading] = useState(true);
useEffect(() => {
if (!params.id) return;
async function fetchMenu() {
try {
const res = await fetch(`/api/menus/${params.id}`);
if (!res.ok) throw new Error('Failed to fetch');
const data = await res.json();
// Parse exercises if string
if (typeof data.exercises === 'string') {
data.exercises = JSON.parse(data.exercises);
}
setMenu(data);
} catch (err) {
console.error(err);
// Optionally redirect or show error
} finally {
setIsLoading(false);
}
}
fetchMenu();
}, [params.id]);
if (isLoading) {
return (
<div className="min-h-screen bg-zinc-50 flex items-center justify-center">
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-primary"></div>
</div>
);
}
if (!menu) {
return (
<div className="min-h-screen bg-zinc-50 flex flex-col items-center justify-center text-zinc-500">
<p className="mb-4">Menu not found.</p>
<Link href="/coach/dashboard" className="text-primary hover:underline">Return to Dashboard</Link>
</div>
);
}
return (
<div className="min-h-screen bg-background text-foreground p-8 font-sans">
<header className="max-w-4xl mx-auto mb-10">
<Link
href="/coach/dashboard"
className="inline-flex items-center gap-2 text-zinc-500 hover:text-primary transition-colors mb-6 group"
>
<ArrowLeft className="w-4 h-4 group-hover:-translate-x-1 transition-transform" />
Back to Dashboard
</Link>
<div className="flex flex-col md:flex-row md:items-center justify-between gap-4">
<div>
<h1 className="text-4xl font-light text-zinc-900 tracking-tight mb-2">
{menu.name}
</h1>
<div className="flex items-center gap-4 text-sm text-zinc-500">
<span className="flex items-center gap-1">
<Calendar className="w-4 h-4" />
{new Date(menu.created_at).toLocaleDateString()}
</span>
<span className="flex items-center gap-1">
<Dumbbell className="w-4 h-4" />
{menu.exercises?.length || 0} Exercises
</span>
</div>
</div>
<button
onClick={() => window.print()}
className="px-6 py-2 bg-white border border-zinc-200 text-zinc-700 rounded-lg hover:bg-zinc-50 text-sm font-medium shadow-sm transition-all"
>
Print Program
</button>
</div>
</header>
<main className="max-w-4xl mx-auto">
<div className="bg-white rounded-2xl shadow-sm border border-zinc-200 overflow-hidden">
<div className="p-6 border-b border-zinc-100 bg-zinc-50/50 flex justify-between items-center">
<h2 className="font-bold text-zinc-700 uppercase text-xs tracking-widest">Exercise Schedule</h2>
<span className="bg-green-100 text-green-700 px-3 py-1 rounded-full text-xs font-bold">ACTIVE</span>
</div>
<div className="divide-y divide-zinc-100">
{menu.exercises?.map((ex: any, idx: number) => (
<div key={idx} className="p-6 hover:bg-zinc-50 transition-colors flex flex-col md:flex-row md:items-center justify-between gap-4">
<div className="flex items-start gap-4">
<div className="w-10 h-10 rounded-full bg-blue-50 text-primary flex items-center justify-center font-bold text-lg shrink-0">
{idx + 1}
</div>
<div>
<h3 className="text-lg font-bold text-zinc-900">{ex.name}</h3>
<p className="text-sm text-zinc-500 mt-1">Target Muscles: General</p>
</div>
</div>
<div className="flex items-center gap-8 text-sm">
<div className="flex flex-col items-center">
<span className="text-zinc-400 text-xs uppercase tracking-wider font-bold mb-1">Sets</span>
<span className="text-xl font-light text-zinc-900">{ex.sets}</span>
</div>
<div className="flex flex-col items-center">
<span className="text-zinc-400 text-xs uppercase tracking-wider font-bold mb-1">Reps</span>
<span className="text-xl font-light text-zinc-900">{ex.reps}</span>
</div>
<div className="flex flex-col items-center">
<span className="text-zinc-400 text-xs uppercase tracking-wider font-bold mb-1">Weight</span>
<span className="text-xl font-light text-zinc-900">{ex.weight}kg</span>
</div>
<div className="flex flex-col items-center w-20">
<span className="text-zinc-400 text-xs uppercase tracking-wider font-bold mb-1 flex items-center gap-1"><Clock className="w-3 h-3"/> Rest</span>
<span className="text-xl font-light text-zinc-900">{ex.rest_time_seconds || 0}s</span>
</div>
</div>
</div>
))}
</div>
</div>
<div className="mt-8 grid grid-cols-1 md:grid-cols-3 gap-6">
<div className="p-6 bg-blue-50 rounded-xl border border-blue-100">
<h4 className="text-blue-900 font-bold mb-2">Coach Notes</h4>
<p className="text-blue-700/80 text-sm">Ensure client maintains proper form during the eccentric phase of each movement.</p>
</div>
<div className="p-6 bg-orange-50 rounded-xl border border-orange-100">
<h4 className="text-orange-900 font-bold mb-2">Safety Protocols</h4>
<p className="text-orange-700/80 text-sm">Stop immediately if pain is reported in joints. Monitor heart rate variations.</p>
</div>
</div>
</main>
</div>
);
}

350
app/coach/menu/new/page.tsx Normal file
View File

@ -0,0 +1,350 @@
'use client';
import React, { useState, useEffect } from 'react';
import { useRouter } from 'next/navigation';
import { motion } from 'framer-motion';
import { Plus, Trash2, Save, ArrowLeft, Copy, Layers, GripVertical } from 'lucide-react';
import Link from 'next/link';
import { useAuth, AuthProvider } from '@/lib/auth';
interface ExerciseItem {
id: string;
name: string;
reps: number;
weight: number;
rest_time_seconds: number;
}
interface RoundData {
id: string;
exercises: ExerciseItem[];
}
export default function CreateMenuPageWrap() {
return (
<AuthProvider>
<CreateMenuPage />
</AuthProvider>
);
}
function CreateMenuPage() {
const router = useRouter();
const { user } = useAuth();
const [isSubmitting, setIsSubmitting] = useState(false);
const [menuName, setMenuName] = useState('');
// Round-Based State
const [rounds, setRounds] = useState<RoundData[]>([
{
id: 'round-1',
exercises: [
{ id: 'ex-1', name: 'Squat', reps: 10, weight: 20, rest_time_seconds: 30 }
]
}
]);
const [clients, setClients] = useState<any[]>([]);
const [selectedClient, setSelectedClient] = useState('');
useEffect(() => {
if (user) {
fetch(`/api/users?coachId=${encodeURIComponent(user.id)}`)
.then(res => res.json())
.then(data => {
if (Array.isArray(data)) setClients(data);
});
}
}, [user]);
// --- Actions ---
const addRound = () => {
setRounds([...rounds, {
id: Math.random().toString(36).substr(2, 9),
exercises: []
}]);
};
const duplicateRound = (sourceIndex: number) => {
const source = rounds[sourceIndex];
const newExercises = source.exercises.map(ex => ({
...ex,
id: Math.random().toString(36).substr(2, 9)
}));
setRounds([...rounds, {
id: Math.random().toString(36).substr(2, 9),
exercises: newExercises
}]);
};
const removeRound = (index: number) => {
setRounds(rounds.filter((_, i) => i !== index));
};
const addExerciseToRound = (roundIndex: number) => {
const newRounds = [...rounds];
newRounds[roundIndex].exercises.push({
id: Math.random().toString(36).substr(2, 9),
name: 'Bicep Curl',
reps: 10,
weight: 10,
rest_time_seconds: 30
});
setRounds(newRounds);
};
const removeExerciseFromRound = (roundIndex: number, exIndex: number) => {
const newRounds = [...rounds];
newRounds[roundIndex].exercises = newRounds[roundIndex].exercises.filter((_, i) => i !== exIndex);
setRounds(newRounds);
};
const updateExercise = (roundIndex: number, exIndex: number, field: keyof ExerciseItem, value: any) => {
const newRounds = [...rounds];
newRounds[roundIndex].exercises[exIndex] = {
...newRounds[roundIndex].exercises[exIndex],
[field]: value
};
setRounds(newRounds);
};
// --- Submit Logic (Flattening) ---
// We assume the user creates rounds sequentially: Set 1, Set 2.
// So distinct Sets of "Squat" will imply Set 1, Set 2 logic naturally in the list.
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setIsSubmitting(true);
try {
if (!selectedClient) {
alert("Please select a client.");
setIsSubmitting(false);
return;
}
// FLATTEN ROUNDS
const flatList: any[] = [];
const counts: Record<string, number> = {};
const totals: Record<string, number> = {};
// 1. Calculate Totals (First Pass)
rounds.forEach(round => {
round.exercises.forEach(ex => {
totals[ex.name] = (totals[ex.name] || 0) + 1;
});
});
// 2. Flatten and Assign Indices
rounds.forEach((round, roundIndex) => {
round.exercises.forEach(ex => {
counts[ex.name] = (counts[ex.name] || 0) + 1;
flatList.push({
name: ex.name,
reps: ex.reps,
weight: ex.weight,
rest_time_seconds: ex.rest_time_seconds,
// This corresponds to "Which instance of Squat is this?" -> Set Number
set_index: counts[ex.name],
total_sets: totals[ex.name]
});
});
});
const res = await fetch('/api/menus', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'x-user-id': user?.id || ''
},
body: JSON.stringify({
name: menuName,
exercises: flatList,
client_id: selectedClient
})
});
if (!res.ok) throw new Error('Failed');
router.push('/coach/dashboard');
} catch (error) {
alert('Error creating menu');
} finally {
setIsSubmitting(false);
}
};
const EXERCISE_OPTIONS = [
"Bicep Curl", "Hammer Curl", "Squat", "Deadlift", "Lunges", "Overhead Press", "Lateral Raises"
];
return (
<div className="min-h-screen bg-background text-foreground p-8 font-sans pb-32">
<header className="max-w-4xl mx-auto mb-10 flex items-center gap-6">
<Link href="/coach/dashboard" className="p-3 bg-white hover:bg-zinc-100 rounded-full transition-colors border border-zinc-200 shadow-sm">
<ArrowLeft className="w-5 h-5 text-zinc-600" />
</Link>
<div>
<h1 className="text-3xl font-light text-zinc-900 tracking-wide">
Program <span className="font-bold text-primary">Composer</span>
</h1>
<p className="text-zinc-500 text-sm mt-1">Design training blocks set-by-set.</p>
</div>
</header>
<form onSubmit={handleSubmit} className="max-w-4xl mx-auto space-y-8">
{/* Meta Info */}
<div className="bg-white p-6 rounded-2xl border border-zinc-200 shadow-sm">
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div>
<label className="block text-xs font-bold text-zinc-400 uppercase tracking-widest mb-2">Program Name</label>
<input
type="text"
placeholder="e.g. Hypertrophy A"
value={menuName}
onChange={(e) => setMenuName(e.target.value)}
className="w-full text-xl font-bold border-b-2 border-zinc-100 focus:border-primary outline-none py-2 transition-colors placeholder:font-normal"
/>
</div>
<div>
<label className="block text-xs font-bold text-zinc-400 uppercase tracking-widest mb-2">Assign To Client</label>
<select
value={selectedClient}
onChange={(e) => setSelectedClient(e.target.value)}
className="w-full text-lg border-b-2 border-zinc-100 focus:border-primary outline-none py-2 bg-transparent transition-colors"
>
<option value="" disabled>Select a client...</option>
{clients.map(c => (
<option key={c.id} value={c.id}>{c.name}</option>
))}
</select>
</div>
</div>
</div>
{/* Rounds */}
<div className="grid grid-cols-1 gap-6">
{rounds.map((round, roundIndex) => (
<motion.div
key={round.id}
initial={{ opacity: 0, scale: 0.95 }}
animate={{ opacity: 1, scale: 1 }}
className="bg-zinc-50 border border-zinc-200 rounded-3xl p-6 md:p-8 relative shadow-sm group/round"
>
<div className="flex justify-between items-center mb-6">
<h3 className="text-xl font-black text-zinc-300 uppercase tracking-tighter flex items-center gap-2">
<span className="text-4xl text-zinc-200">#{ (roundIndex + 1).toString().padStart(2, '0') }</span>
SET
</h3>
<div className="flex items-center gap-2">
<button
type="button"
onClick={() => duplicateRound(roundIndex)}
className="p-2 text-zinc-400 hover:text-blue-500 hover:bg-blue-50 rounded-xl transition-all"
title="Duplicate this Set"
>
<Copy className="w-5 h-5" />
</button>
<button
type="button"
onClick={() => removeRound(roundIndex)}
disabled={rounds.length === 1}
className="p-2 text-zinc-300 hover:text-red-500 hover:bg-red-50 rounded-xl transition-all disabled:opacity-0"
>
<Trash2 className="w-5 h-5" />
</button>
</div>
</div>
{/* Exercises in Round */}
<div className="space-y-3">
{round.exercises.map((ex, exIndex) => (
<div key={ex.id} className="bg-white p-4 rounded-xl shadow-sm border border-zinc-100 flex flex-col md:flex-row gap-4 items-center group/ex">
<div className="flex-1 w-full">
<label className="block text-[10px] font-bold text-zinc-300 uppercase mb-1 md:hidden">Exercise</label>
<select
value={ex.name}
onChange={(e) => updateExercise(roundIndex, exIndex, 'name', e.target.value)}
className="w-full bg-transparent font-bold text-zinc-900 focus:outline-none cursor-pointer"
>
{EXERCISE_OPTIONS.map(opt => <option key={opt} value={opt}>{opt}</option>)}
</select>
</div>
<div className="flex gap-2 w-full md:w-auto">
<div>
<label className="block text-[10px] font-bold text-zinc-300 uppercase mb-1 text-center">Reps</label>
<input
type="number"
value={isNaN(ex.reps) ? '' : ex.reps}
onChange={(e) => updateExercise(roundIndex, exIndex, 'reps', parseInt(e.target.value))}
className="w-full md:w-16 bg-zinc-50 border border-zinc-100 rounded-lg px-2 py-1.5 text-center font-mono text-sm focus:border-primary outline-none"
/>
</div>
<div>
<label className="block text-[10px] font-bold text-zinc-300 uppercase mb-1 text-center">Kg</label>
<input
type="number"
value={isNaN(ex.weight) ? '' : ex.weight}
onChange={(e) => updateExercise(roundIndex, exIndex, 'weight', parseFloat(e.target.value))}
className="w-full md:w-16 bg-zinc-50 border border-zinc-100 rounded-lg px-2 py-1.5 text-center font-mono text-sm focus:border-primary outline-none"
/>
</div>
<div>
<label className="block text-[10px] font-bold text-zinc-300 uppercase mb-1 text-center">Rest</label>
<input
type="number"
value={isNaN(ex.rest_time_seconds) ? '' : ex.rest_time_seconds}
onChange={(e) => updateExercise(roundIndex, exIndex, 'rest_time_seconds', parseFloat(e.target.value))}
className="w-full md:w-16 bg-zinc-50 border border-zinc-100 rounded-lg px-2 py-1.5 text-center font-mono text-sm focus:border-primary outline-none"
/>
</div>
</div>
<button
type="button"
onClick={() => removeExerciseFromRound(roundIndex, exIndex)}
className="p-2 text-zinc-300 hover:text-red-500 transition-colors"
>
<Trash2 className="w-4 h-4" />
</button>
</div>
))}
<button
type="button"
onClick={() => addExerciseToRound(roundIndex)}
className="w-full py-3 border-2 border-dashed border-zinc-200 rounded-xl text-zinc-400 hover:text-primary hover:border-primary hover:bg-blue-50/50 transition-all font-bold text-xs uppercase tracking-widest flex items-center justify-center gap-2"
>
<Plus className="w-4 h-4" /> Add Exercise
</button>
</div>
</motion.div>
))}
</div>
<div className="flex justify-center py-6">
<button
type="button"
onClick={addRound}
className="bg-zinc-100 hover:bg-zinc-200 text-zinc-800 px-8 py-3 rounded-full font-bold shadow-sm hover:scale-105 transition-all flex items-center gap-2"
>
<Plus className="w-5 h-5" /> ADD NEW SET
</button>
</div>
<div className="fixed bottom-0 left-0 right-0 p-6 bg-white/80 backdrop-blur-lg border-t border-zinc-200 flex justify-center z-50">
<button
type="submit"
disabled={isSubmitting}
className="w-full max-w-md bg-primary hover:bg-primary/90 text-black font-black uppercase tracking-widest py-4 rounded-2xl shadow-[0_0_30px_-5px_var(--color-primary)] transform transition-all active:scale-95 disabled:opacity-50 flex items-center justify-center gap-3"
>
{isSubmitting ? 'Saving...' : 'Deploy Program'}
</button>
</div>
</form>
</div>
);
}

View File

@ -0,0 +1,156 @@
'use client';
import React, { useEffect, useState } from 'react';
import { useParams, useRouter } from 'next/navigation';
import { ArrowLeft, Calendar, User, Clock, CheckCircle } from 'lucide-react';
import Link from 'next/link';
export default function RecapDetailPage() {
const params = useParams();
const router = useRouter();
const [recap, setRecap] = useState<any>(null);
const [isLoading, setIsLoading] = useState(true);
useEffect(() => {
if (!params.id) return;
async function fetchRecap() {
try {
const res = await fetch(`/api/recap/${params.id}`);
if (!res.ok) throw new Error('Failed to fetch');
const data = await res.json();
// Parse summary/exercises if string (though usually JSON in Prisma)
// Assuming Prisma handles JSON parsing automatically for `details` field
setRecap(data);
} catch (err) {
console.error(err);
} finally {
setIsLoading(false);
}
}
fetchRecap();
}, [params.id]);
if (isLoading) {
return (
<div className="min-h-screen bg-zinc-50 flex items-center justify-center">
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-primary"></div>
</div>
);
}
if (!recap) {
return (
<div className="min-h-screen bg-zinc-50 flex flex-col items-center justify-center text-zinc-500">
<p className="mb-4">Report not found.</p>
<Link href="/coach/dashboard" className="text-primary hover:underline">Return to Dashboard</Link>
</div>
);
}
const { summary, training_menus } = recap;
const exercises = summary?.exercises || [];
return (
<div className="min-h-screen bg-background text-foreground p-8 font-sans">
<header className="max-w-4xl mx-auto mb-10">
<Link
href="/coach/dashboard"
className="inline-flex items-center gap-2 text-zinc-500 hover:text-primary transition-colors mb-6 group"
>
<ArrowLeft className="w-4 h-4 group-hover:-translate-x-1 transition-transform" />
Back to Dashboard
</Link>
<div className="flex flex-col md:flex-row md:items-center justify-between gap-4">
<div>
<h1 className="text-4xl font-light text-zinc-900 tracking-tight mb-2">
Session Report <span className="text-zinc-400">#{recap.id}</span>
</h1>
<div className="flex items-center gap-4 text-sm text-zinc-500">
<span className="flex items-center gap-1">
<Calendar className="w-4 h-4" />
{new Date(recap.completed_at).toLocaleDateString()}
</span>
<span className="flex items-center gap-1">
<User className="w-4 h-4" />
{training_menus ? training_menus.name : `Menu #${recap.menu_id}`}
</span>
<span className="flex items-center gap-1">
<Clock className="w-4 h-4" />
{new Date(recap.completed_at).toLocaleTimeString()}
</span>
</div>
</div>
</div>
</header>
<main className="max-w-4xl mx-auto space-y-8">
{/* Performance Summary */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div className="bg-white p-6 rounded-2xl shadow-sm border border-zinc-200">
<h2 className="text-zinc-400 text-xs font-bold uppercase tracking-widest mb-4">Completion Status</h2>
<div className="flex items-center gap-3">
<CheckCircle className="w-10 h-10 text-green-500" />
<div>
<div className="text-2xl font-bold text-zinc-900">Completed</div>
<div className="text-sm text-zinc-500">All exercises finished</div>
</div>
</div>
</div>
{/* Placeholder for future detailed analysis */}
<div className="bg-white p-6 rounded-2xl shadow-sm border border-zinc-200 opacity-60 grayscale">
<h2 className="text-zinc-400 text-xs font-bold uppercase tracking-widest mb-4">Total Load</h2>
<div className="text-3xl font-light text-zinc-800">-- kg</div>
<div className="text-xs text-blue-500 font-bold uppercase mt-2">Analysis Coming Soon</div>
</div>
</div>
<div className="bg-white rounded-2xl shadow-sm border border-zinc-200 overflow-hidden">
<div className="p-6 border-b border-zinc-100 bg-zinc-50/50">
<h2 className="font-bold text-zinc-700 uppercase text-xs tracking-widest">Exercise Log</h2>
</div>
<div className="divide-y divide-zinc-100">
{exercises.map((ex: any, idx: number) => (
<div key={idx} className="p-6 flex flex-col md:flex-row md:items-center justify-between gap-4 group hover:bg-zinc-50 transition-colors">
<div className="flex items-start gap-4">
<div className="w-8 h-8 rounded-full bg-zinc-100 text-zinc-400 flex items-center justify-center font-bold text-sm shrink-0">
{idx + 1}
</div>
<div>
<div className="flex items-center gap-2">
<h3 className="text-lg font-bold text-zinc-900">{ex.name}</h3>
<span className="px-2 py-0.5 rounded text-[10px] font-bold bg-blue-50 text-blue-600 border border-blue-100 uppercase tracking-wide">
Set {ex.set_index || '?'}/{ex.total_sets || '?'}
</span>
</div>
<div className="flex gap-2 text-xs font-medium uppercase tracking-wider text-zinc-400 mt-1">
<span>{ex.reps} Reps</span>
<span></span>
<span>{ex.weight} kg</span>
</div>
</div>
</div>
<div className="flex items-center gap-2">
<span className="px-3 py-1 bg-green-50 text-green-700 rounded-lg text-xs font-bold uppercase border border-green-100">
Target Met
</span>
</div>
</div>
))}
{exercises.length === 0 && (
<div className="p-8 text-center text-zinc-400 italic">
No exercise details available for this session.
</div>
)}
</div>
</div>
</main>
</div>
);
}

BIN
app/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

View File

@ -0,0 +1,39 @@
/* !!! This is code generated by Prisma. Do not edit directly. !!! */
/* eslint-disable */
// biome-ignore-all lint: generated file
// @ts-nocheck
/*
* This file should be your main import to use Prisma-related types and utilities in a browser.
* Use it to get access to models, enums, and input types.
*
* This file does not contain a `PrismaClient` class, nor several other helpers that are intended as server-side only.
* See `client.ts` for the standard, server-side entry point.
*
* 🟢 You can import this file directly.
*/
import * as Prisma from './internal/prismaNamespaceBrowser.js'
export { Prisma }
export * as $Enums from './enums.js'
export * from './enums.js';
/**
* Model users
*
*/
export type users = Prisma.usersModel
/**
* Model activity_logs
*
*/
export type activity_logs = Prisma.activity_logsModel
/**
* Model training_menus
*
*/
export type training_menus = Prisma.training_menusModel
/**
* Model user_recaps
*
*/
export type user_recaps = Prisma.user_recapsModel

View File

@ -0,0 +1,66 @@
/* !!! This is code generated by Prisma. Do not edit directly. !!! */
/* eslint-disable */
// biome-ignore-all lint: generated file
// @ts-nocheck
/*
* This file should be your main import to use Prisma. Through it you get access to all the models, enums, and input types.
* If you're looking for something you can import in the client-side of your application, please refer to the `browser.ts` file instead.
*
* 🟢 You can import this file directly.
*/
import * as process from 'node:process'
import * as path from 'node:path'
import { fileURLToPath } from 'node:url'
globalThis['__dirname'] = path.dirname(fileURLToPath(import.meta.url))
import * as runtime from "@prisma/client/runtime/library"
import * as $Enums from "./enums.js"
import * as $Class from "./internal/class.js"
import * as Prisma from "./internal/prismaNamespace.js"
export * as $Enums from './enums.js'
export * from "./enums.js"
/**
* ## Prisma Client
*
* Type-safe database client for TypeScript
* @example
* ```
* const prisma = new PrismaClient()
* // Fetch zero or more Users
* const users = await prisma.users.findMany()
* ```
*
* Read more in our [docs](https://www.prisma.io/docs/reference/tools-and-interfaces/prisma-client).
*/
export const PrismaClient = $Class.getPrismaClientClass(__dirname)
export type PrismaClient<LogOpts extends Prisma.LogLevel = never, OmitOpts extends Prisma.PrismaClientOptions["omit"] = Prisma.PrismaClientOptions["omit"], ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = $Class.PrismaClient<LogOpts, OmitOpts, ExtArgs>
export { Prisma }
// file annotations for bundling tools to include these files
path.join(__dirname, "query_engine-windows.dll.node")
path.join(process.cwd(), "app/generated/client/query_engine-windows.dll.node")
/**
* Model users
*
*/
export type users = Prisma.usersModel
/**
* Model activity_logs
*
*/
export type activity_logs = Prisma.activity_logsModel
/**
* Model training_menus
*
*/
export type training_menus = Prisma.training_menusModel
/**
* Model user_recaps
*
*/
export type user_recaps = Prisma.user_recapsModel

View File

@ -0,0 +1,405 @@
/* !!! This is code generated by Prisma. Do not edit directly. !!! */
/* eslint-disable */
// biome-ignore-all lint: generated file
// @ts-nocheck
/*
* This file exports various common sort, input & filter types that are not directly linked to a particular model.
*
* 🟢 You can import this file directly.
*/
import type * as runtime from "@prisma/client/runtime/library"
import * as $Enums from "./enums.js"
import type * as Prisma from "./internal/prismaNamespace.js"
export type StringFilter<$PrismaModel = never> = {
equals?: string | Prisma.StringFieldRefInput<$PrismaModel>
in?: string[] | Prisma.ListStringFieldRefInput<$PrismaModel>
notIn?: string[] | Prisma.ListStringFieldRefInput<$PrismaModel>
lt?: string | Prisma.StringFieldRefInput<$PrismaModel>
lte?: string | Prisma.StringFieldRefInput<$PrismaModel>
gt?: string | Prisma.StringFieldRefInput<$PrismaModel>
gte?: string | Prisma.StringFieldRefInput<$PrismaModel>
contains?: string | Prisma.StringFieldRefInput<$PrismaModel>
startsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>
endsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>
mode?: Prisma.QueryMode
not?: Prisma.NestedStringFilter<$PrismaModel> | string
}
export type StringNullableFilter<$PrismaModel = never> = {
equals?: string | Prisma.StringFieldRefInput<$PrismaModel> | null
in?: string[] | Prisma.ListStringFieldRefInput<$PrismaModel> | null
notIn?: string[] | Prisma.ListStringFieldRefInput<$PrismaModel> | null
lt?: string | Prisma.StringFieldRefInput<$PrismaModel>
lte?: string | Prisma.StringFieldRefInput<$PrismaModel>
gt?: string | Prisma.StringFieldRefInput<$PrismaModel>
gte?: string | Prisma.StringFieldRefInput<$PrismaModel>
contains?: string | Prisma.StringFieldRefInput<$PrismaModel>
startsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>
endsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>
mode?: Prisma.QueryMode
not?: Prisma.NestedStringNullableFilter<$PrismaModel> | string | null
}
export type DateTimeNullableFilter<$PrismaModel = never> = {
equals?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel> | null
in?: Date[] | string[] | Prisma.ListDateTimeFieldRefInput<$PrismaModel> | null
notIn?: Date[] | string[] | Prisma.ListDateTimeFieldRefInput<$PrismaModel> | null
lt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
lte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
gt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
gte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
not?: Prisma.NestedDateTimeNullableFilter<$PrismaModel> | Date | string | null
}
export type SortOrderInput = {
sort: Prisma.SortOrder
nulls?: Prisma.NullsOrder
}
export type StringWithAggregatesFilter<$PrismaModel = never> = {
equals?: string | Prisma.StringFieldRefInput<$PrismaModel>
in?: string[] | Prisma.ListStringFieldRefInput<$PrismaModel>
notIn?: string[] | Prisma.ListStringFieldRefInput<$PrismaModel>
lt?: string | Prisma.StringFieldRefInput<$PrismaModel>
lte?: string | Prisma.StringFieldRefInput<$PrismaModel>
gt?: string | Prisma.StringFieldRefInput<$PrismaModel>
gte?: string | Prisma.StringFieldRefInput<$PrismaModel>
contains?: string | Prisma.StringFieldRefInput<$PrismaModel>
startsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>
endsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>
mode?: Prisma.QueryMode
not?: Prisma.NestedStringWithAggregatesFilter<$PrismaModel> | string
_count?: Prisma.NestedIntFilter<$PrismaModel>
_min?: Prisma.NestedStringFilter<$PrismaModel>
_max?: Prisma.NestedStringFilter<$PrismaModel>
}
export type StringNullableWithAggregatesFilter<$PrismaModel = never> = {
equals?: string | Prisma.StringFieldRefInput<$PrismaModel> | null
in?: string[] | Prisma.ListStringFieldRefInput<$PrismaModel> | null
notIn?: string[] | Prisma.ListStringFieldRefInput<$PrismaModel> | null
lt?: string | Prisma.StringFieldRefInput<$PrismaModel>
lte?: string | Prisma.StringFieldRefInput<$PrismaModel>
gt?: string | Prisma.StringFieldRefInput<$PrismaModel>
gte?: string | Prisma.StringFieldRefInput<$PrismaModel>
contains?: string | Prisma.StringFieldRefInput<$PrismaModel>
startsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>
endsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>
mode?: Prisma.QueryMode
not?: Prisma.NestedStringNullableWithAggregatesFilter<$PrismaModel> | string | null
_count?: Prisma.NestedIntNullableFilter<$PrismaModel>
_min?: Prisma.NestedStringNullableFilter<$PrismaModel>
_max?: Prisma.NestedStringNullableFilter<$PrismaModel>
}
export type DateTimeNullableWithAggregatesFilter<$PrismaModel = never> = {
equals?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel> | null
in?: Date[] | string[] | Prisma.ListDateTimeFieldRefInput<$PrismaModel> | null
notIn?: Date[] | string[] | Prisma.ListDateTimeFieldRefInput<$PrismaModel> | null
lt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
lte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
gt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
gte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
not?: Prisma.NestedDateTimeNullableWithAggregatesFilter<$PrismaModel> | Date | string | null
_count?: Prisma.NestedIntNullableFilter<$PrismaModel>
_min?: Prisma.NestedDateTimeNullableFilter<$PrismaModel>
_max?: Prisma.NestedDateTimeNullableFilter<$PrismaModel>
}
export type IntFilter<$PrismaModel = never> = {
equals?: number | Prisma.IntFieldRefInput<$PrismaModel>
in?: number[] | Prisma.ListIntFieldRefInput<$PrismaModel>
notIn?: number[] | Prisma.ListIntFieldRefInput<$PrismaModel>
lt?: number | Prisma.IntFieldRefInput<$PrismaModel>
lte?: number | Prisma.IntFieldRefInput<$PrismaModel>
gt?: number | Prisma.IntFieldRefInput<$PrismaModel>
gte?: number | Prisma.IntFieldRefInput<$PrismaModel>
not?: Prisma.NestedIntFilter<$PrismaModel> | number
}
export type JsonNullableFilter<$PrismaModel = never> =
| Prisma.PatchUndefined<
Prisma.Either<Required<JsonNullableFilterBase<$PrismaModel>>, Exclude<keyof Required<JsonNullableFilterBase<$PrismaModel>>, 'path'>>,
Required<JsonNullableFilterBase<$PrismaModel>>
>
| Prisma.OptionalFlat<Omit<Required<JsonNullableFilterBase<$PrismaModel>>, 'path'>>
export type JsonNullableFilterBase<$PrismaModel = never> = {
equals?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel> | Prisma.JsonNullValueFilter
path?: string[]
mode?: Prisma.QueryMode | Prisma.EnumQueryModeFieldRefInput<$PrismaModel>
string_contains?: string | Prisma.StringFieldRefInput<$PrismaModel>
string_starts_with?: string | Prisma.StringFieldRefInput<$PrismaModel>
string_ends_with?: string | Prisma.StringFieldRefInput<$PrismaModel>
array_starts_with?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel> | null
array_ends_with?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel> | null
array_contains?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel> | null
lt?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel>
lte?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel>
gt?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel>
gte?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel>
not?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel> | Prisma.JsonNullValueFilter
}
export type IntWithAggregatesFilter<$PrismaModel = never> = {
equals?: number | Prisma.IntFieldRefInput<$PrismaModel>
in?: number[] | Prisma.ListIntFieldRefInput<$PrismaModel>
notIn?: number[] | Prisma.ListIntFieldRefInput<$PrismaModel>
lt?: number | Prisma.IntFieldRefInput<$PrismaModel>
lte?: number | Prisma.IntFieldRefInput<$PrismaModel>
gt?: number | Prisma.IntFieldRefInput<$PrismaModel>
gte?: number | Prisma.IntFieldRefInput<$PrismaModel>
not?: Prisma.NestedIntWithAggregatesFilter<$PrismaModel> | number
_count?: Prisma.NestedIntFilter<$PrismaModel>
_avg?: Prisma.NestedFloatFilter<$PrismaModel>
_sum?: Prisma.NestedIntFilter<$PrismaModel>
_min?: Prisma.NestedIntFilter<$PrismaModel>
_max?: Prisma.NestedIntFilter<$PrismaModel>
}
export type JsonNullableWithAggregatesFilter<$PrismaModel = never> =
| Prisma.PatchUndefined<
Prisma.Either<Required<JsonNullableWithAggregatesFilterBase<$PrismaModel>>, Exclude<keyof Required<JsonNullableWithAggregatesFilterBase<$PrismaModel>>, 'path'>>,
Required<JsonNullableWithAggregatesFilterBase<$PrismaModel>>
>
| Prisma.OptionalFlat<Omit<Required<JsonNullableWithAggregatesFilterBase<$PrismaModel>>, 'path'>>
export type JsonNullableWithAggregatesFilterBase<$PrismaModel = never> = {
equals?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel> | Prisma.JsonNullValueFilter
path?: string[]
mode?: Prisma.QueryMode | Prisma.EnumQueryModeFieldRefInput<$PrismaModel>
string_contains?: string | Prisma.StringFieldRefInput<$PrismaModel>
string_starts_with?: string | Prisma.StringFieldRefInput<$PrismaModel>
string_ends_with?: string | Prisma.StringFieldRefInput<$PrismaModel>
array_starts_with?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel> | null
array_ends_with?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel> | null
array_contains?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel> | null
lt?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel>
lte?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel>
gt?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel>
gte?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel>
not?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel> | Prisma.JsonNullValueFilter
_count?: Prisma.NestedIntNullableFilter<$PrismaModel>
_min?: Prisma.NestedJsonNullableFilter<$PrismaModel>
_max?: Prisma.NestedJsonNullableFilter<$PrismaModel>
}
export type IntNullableFilter<$PrismaModel = never> = {
equals?: number | Prisma.IntFieldRefInput<$PrismaModel> | null
in?: number[] | Prisma.ListIntFieldRefInput<$PrismaModel> | null
notIn?: number[] | Prisma.ListIntFieldRefInput<$PrismaModel> | null
lt?: number | Prisma.IntFieldRefInput<$PrismaModel>
lte?: number | Prisma.IntFieldRefInput<$PrismaModel>
gt?: number | Prisma.IntFieldRefInput<$PrismaModel>
gte?: number | Prisma.IntFieldRefInput<$PrismaModel>
not?: Prisma.NestedIntNullableFilter<$PrismaModel> | number | null
}
export type IntNullableWithAggregatesFilter<$PrismaModel = never> = {
equals?: number | Prisma.IntFieldRefInput<$PrismaModel> | null
in?: number[] | Prisma.ListIntFieldRefInput<$PrismaModel> | null
notIn?: number[] | Prisma.ListIntFieldRefInput<$PrismaModel> | null
lt?: number | Prisma.IntFieldRefInput<$PrismaModel>
lte?: number | Prisma.IntFieldRefInput<$PrismaModel>
gt?: number | Prisma.IntFieldRefInput<$PrismaModel>
gte?: number | Prisma.IntFieldRefInput<$PrismaModel>
not?: Prisma.NestedIntNullableWithAggregatesFilter<$PrismaModel> | number | null
_count?: Prisma.NestedIntNullableFilter<$PrismaModel>
_avg?: Prisma.NestedFloatNullableFilter<$PrismaModel>
_sum?: Prisma.NestedIntNullableFilter<$PrismaModel>
_min?: Prisma.NestedIntNullableFilter<$PrismaModel>
_max?: Prisma.NestedIntNullableFilter<$PrismaModel>
}
export type NestedStringFilter<$PrismaModel = never> = {
equals?: string | Prisma.StringFieldRefInput<$PrismaModel>
in?: string[] | Prisma.ListStringFieldRefInput<$PrismaModel>
notIn?: string[] | Prisma.ListStringFieldRefInput<$PrismaModel>
lt?: string | Prisma.StringFieldRefInput<$PrismaModel>
lte?: string | Prisma.StringFieldRefInput<$PrismaModel>
gt?: string | Prisma.StringFieldRefInput<$PrismaModel>
gte?: string | Prisma.StringFieldRefInput<$PrismaModel>
contains?: string | Prisma.StringFieldRefInput<$PrismaModel>
startsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>
endsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>
not?: Prisma.NestedStringFilter<$PrismaModel> | string
}
export type NestedStringNullableFilter<$PrismaModel = never> = {
equals?: string | Prisma.StringFieldRefInput<$PrismaModel> | null
in?: string[] | Prisma.ListStringFieldRefInput<$PrismaModel> | null
notIn?: string[] | Prisma.ListStringFieldRefInput<$PrismaModel> | null
lt?: string | Prisma.StringFieldRefInput<$PrismaModel>
lte?: string | Prisma.StringFieldRefInput<$PrismaModel>
gt?: string | Prisma.StringFieldRefInput<$PrismaModel>
gte?: string | Prisma.StringFieldRefInput<$PrismaModel>
contains?: string | Prisma.StringFieldRefInput<$PrismaModel>
startsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>
endsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>
not?: Prisma.NestedStringNullableFilter<$PrismaModel> | string | null
}
export type NestedDateTimeNullableFilter<$PrismaModel = never> = {
equals?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel> | null
in?: Date[] | string[] | Prisma.ListDateTimeFieldRefInput<$PrismaModel> | null
notIn?: Date[] | string[] | Prisma.ListDateTimeFieldRefInput<$PrismaModel> | null
lt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
lte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
gt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
gte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
not?: Prisma.NestedDateTimeNullableFilter<$PrismaModel> | Date | string | null
}
export type NestedStringWithAggregatesFilter<$PrismaModel = never> = {
equals?: string | Prisma.StringFieldRefInput<$PrismaModel>
in?: string[] | Prisma.ListStringFieldRefInput<$PrismaModel>
notIn?: string[] | Prisma.ListStringFieldRefInput<$PrismaModel>
lt?: string | Prisma.StringFieldRefInput<$PrismaModel>
lte?: string | Prisma.StringFieldRefInput<$PrismaModel>
gt?: string | Prisma.StringFieldRefInput<$PrismaModel>
gte?: string | Prisma.StringFieldRefInput<$PrismaModel>
contains?: string | Prisma.StringFieldRefInput<$PrismaModel>
startsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>
endsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>
not?: Prisma.NestedStringWithAggregatesFilter<$PrismaModel> | string
_count?: Prisma.NestedIntFilter<$PrismaModel>
_min?: Prisma.NestedStringFilter<$PrismaModel>
_max?: Prisma.NestedStringFilter<$PrismaModel>
}
export type NestedIntFilter<$PrismaModel = never> = {
equals?: number | Prisma.IntFieldRefInput<$PrismaModel>
in?: number[] | Prisma.ListIntFieldRefInput<$PrismaModel>
notIn?: number[] | Prisma.ListIntFieldRefInput<$PrismaModel>
lt?: number | Prisma.IntFieldRefInput<$PrismaModel>
lte?: number | Prisma.IntFieldRefInput<$PrismaModel>
gt?: number | Prisma.IntFieldRefInput<$PrismaModel>
gte?: number | Prisma.IntFieldRefInput<$PrismaModel>
not?: Prisma.NestedIntFilter<$PrismaModel> | number
}
export type NestedStringNullableWithAggregatesFilter<$PrismaModel = never> = {
equals?: string | Prisma.StringFieldRefInput<$PrismaModel> | null
in?: string[] | Prisma.ListStringFieldRefInput<$PrismaModel> | null
notIn?: string[] | Prisma.ListStringFieldRefInput<$PrismaModel> | null
lt?: string | Prisma.StringFieldRefInput<$PrismaModel>
lte?: string | Prisma.StringFieldRefInput<$PrismaModel>
gt?: string | Prisma.StringFieldRefInput<$PrismaModel>
gte?: string | Prisma.StringFieldRefInput<$PrismaModel>
contains?: string | Prisma.StringFieldRefInput<$PrismaModel>
startsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>
endsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>
not?: Prisma.NestedStringNullableWithAggregatesFilter<$PrismaModel> | string | null
_count?: Prisma.NestedIntNullableFilter<$PrismaModel>
_min?: Prisma.NestedStringNullableFilter<$PrismaModel>
_max?: Prisma.NestedStringNullableFilter<$PrismaModel>
}
export type NestedIntNullableFilter<$PrismaModel = never> = {
equals?: number | Prisma.IntFieldRefInput<$PrismaModel> | null
in?: number[] | Prisma.ListIntFieldRefInput<$PrismaModel> | null
notIn?: number[] | Prisma.ListIntFieldRefInput<$PrismaModel> | null
lt?: number | Prisma.IntFieldRefInput<$PrismaModel>
lte?: number | Prisma.IntFieldRefInput<$PrismaModel>
gt?: number | Prisma.IntFieldRefInput<$PrismaModel>
gte?: number | Prisma.IntFieldRefInput<$PrismaModel>
not?: Prisma.NestedIntNullableFilter<$PrismaModel> | number | null
}
export type NestedDateTimeNullableWithAggregatesFilter<$PrismaModel = never> = {
equals?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel> | null
in?: Date[] | string[] | Prisma.ListDateTimeFieldRefInput<$PrismaModel> | null
notIn?: Date[] | string[] | Prisma.ListDateTimeFieldRefInput<$PrismaModel> | null
lt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
lte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
gt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
gte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
not?: Prisma.NestedDateTimeNullableWithAggregatesFilter<$PrismaModel> | Date | string | null
_count?: Prisma.NestedIntNullableFilter<$PrismaModel>
_min?: Prisma.NestedDateTimeNullableFilter<$PrismaModel>
_max?: Prisma.NestedDateTimeNullableFilter<$PrismaModel>
}
export type NestedIntWithAggregatesFilter<$PrismaModel = never> = {
equals?: number | Prisma.IntFieldRefInput<$PrismaModel>
in?: number[] | Prisma.ListIntFieldRefInput<$PrismaModel>
notIn?: number[] | Prisma.ListIntFieldRefInput<$PrismaModel>
lt?: number | Prisma.IntFieldRefInput<$PrismaModel>
lte?: number | Prisma.IntFieldRefInput<$PrismaModel>
gt?: number | Prisma.IntFieldRefInput<$PrismaModel>
gte?: number | Prisma.IntFieldRefInput<$PrismaModel>
not?: Prisma.NestedIntWithAggregatesFilter<$PrismaModel> | number
_count?: Prisma.NestedIntFilter<$PrismaModel>
_avg?: Prisma.NestedFloatFilter<$PrismaModel>
_sum?: Prisma.NestedIntFilter<$PrismaModel>
_min?: Prisma.NestedIntFilter<$PrismaModel>
_max?: Prisma.NestedIntFilter<$PrismaModel>
}
export type NestedFloatFilter<$PrismaModel = never> = {
equals?: number | Prisma.FloatFieldRefInput<$PrismaModel>
in?: number[] | Prisma.ListFloatFieldRefInput<$PrismaModel>
notIn?: number[] | Prisma.ListFloatFieldRefInput<$PrismaModel>
lt?: number | Prisma.FloatFieldRefInput<$PrismaModel>
lte?: number | Prisma.FloatFieldRefInput<$PrismaModel>
gt?: number | Prisma.FloatFieldRefInput<$PrismaModel>
gte?: number | Prisma.FloatFieldRefInput<$PrismaModel>
not?: Prisma.NestedFloatFilter<$PrismaModel> | number
}
export type NestedJsonNullableFilter<$PrismaModel = never> =
| Prisma.PatchUndefined<
Prisma.Either<Required<NestedJsonNullableFilterBase<$PrismaModel>>, Exclude<keyof Required<NestedJsonNullableFilterBase<$PrismaModel>>, 'path'>>,
Required<NestedJsonNullableFilterBase<$PrismaModel>>
>
| Prisma.OptionalFlat<Omit<Required<NestedJsonNullableFilterBase<$PrismaModel>>, 'path'>>
export type NestedJsonNullableFilterBase<$PrismaModel = never> = {
equals?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel> | Prisma.JsonNullValueFilter
path?: string[]
mode?: Prisma.QueryMode | Prisma.EnumQueryModeFieldRefInput<$PrismaModel>
string_contains?: string | Prisma.StringFieldRefInput<$PrismaModel>
string_starts_with?: string | Prisma.StringFieldRefInput<$PrismaModel>
string_ends_with?: string | Prisma.StringFieldRefInput<$PrismaModel>
array_starts_with?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel> | null
array_ends_with?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel> | null
array_contains?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel> | null
lt?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel>
lte?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel>
gt?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel>
gte?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel>
not?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel> | Prisma.JsonNullValueFilter
}
export type NestedIntNullableWithAggregatesFilter<$PrismaModel = never> = {
equals?: number | Prisma.IntFieldRefInput<$PrismaModel> | null
in?: number[] | Prisma.ListIntFieldRefInput<$PrismaModel> | null
notIn?: number[] | Prisma.ListIntFieldRefInput<$PrismaModel> | null
lt?: number | Prisma.IntFieldRefInput<$PrismaModel>
lte?: number | Prisma.IntFieldRefInput<$PrismaModel>
gt?: number | Prisma.IntFieldRefInput<$PrismaModel>
gte?: number | Prisma.IntFieldRefInput<$PrismaModel>
not?: Prisma.NestedIntNullableWithAggregatesFilter<$PrismaModel> | number | null
_count?: Prisma.NestedIntNullableFilter<$PrismaModel>
_avg?: Prisma.NestedFloatNullableFilter<$PrismaModel>
_sum?: Prisma.NestedIntNullableFilter<$PrismaModel>
_min?: Prisma.NestedIntNullableFilter<$PrismaModel>
_max?: Prisma.NestedIntNullableFilter<$PrismaModel>
}
export type NestedFloatNullableFilter<$PrismaModel = never> = {
equals?: number | Prisma.FloatFieldRefInput<$PrismaModel> | null
in?: number[] | Prisma.ListFloatFieldRefInput<$PrismaModel> | null
notIn?: number[] | Prisma.ListFloatFieldRefInput<$PrismaModel> | null
lt?: number | Prisma.FloatFieldRefInput<$PrismaModel>
lte?: number | Prisma.FloatFieldRefInput<$PrismaModel>
gt?: number | Prisma.FloatFieldRefInput<$PrismaModel>
gte?: number | Prisma.FloatFieldRefInput<$PrismaModel>
not?: Prisma.NestedFloatNullableFilter<$PrismaModel> | number | null
}

View File

@ -0,0 +1,15 @@
/* !!! This is code generated by Prisma. Do not edit directly. !!! */
/* eslint-disable */
// biome-ignore-all lint: generated file
// @ts-nocheck
/*
* This file exports all enum related types from the schema.
*
* 🟢 You can import this file directly.
*/
// This file is empty because there are no enums in the schema.
export {}

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,159 @@
/* !!! This is code generated by Prisma. Do not edit directly. !!! */
/* eslint-disable */
// biome-ignore-all lint: generated file
// @ts-nocheck
/*
* WARNING: This is an internal file that is subject to change!
*
* 🛑 Under no circumstances should you import this file directly! 🛑
*
* All exports from this file are wrapped under a `Prisma` namespace object in the browser.ts file.
* While this enables partial backward compatibility, it is not part of the stable public API.
*
* If you are looking for your Models, Enums, and Input Types, please import them from the respective
* model files in the `model` directory!
*/
import * as runtime from "@prisma/client/runtime/index-browser"
export type * from '../models.js'
export type * from './prismaNamespace.js'
export const Decimal = runtime.Decimal
export const NullTypes = {
DbNull: runtime.objectEnumValues.classes.DbNull as (new (secret: never) => typeof runtime.objectEnumValues.instances.DbNull),
JsonNull: runtime.objectEnumValues.classes.JsonNull as (new (secret: never) => typeof runtime.objectEnumValues.instances.JsonNull),
AnyNull: runtime.objectEnumValues.classes.AnyNull as (new (secret: never) => typeof runtime.objectEnumValues.instances.AnyNull),
}
/**
* Helper for filtering JSON entries that have `null` on the database (empty on the db)
*
* @see https://www.prisma.io/docs/concepts/components/prisma-client/working-with-fields/working-with-json-fields#filtering-on-a-json-field
*/
export const DbNull = runtime.objectEnumValues.instances.DbNull
/**
* Helper for filtering JSON entries that have JSON `null` values (not empty on the db)
*
* @see https://www.prisma.io/docs/concepts/components/prisma-client/working-with-fields/working-with-json-fields#filtering-on-a-json-field
*/
export const JsonNull = runtime.objectEnumValues.instances.JsonNull
/**
* Helper for filtering JSON entries that are `Prisma.DbNull` or `Prisma.JsonNull`
*
* @see https://www.prisma.io/docs/concepts/components/prisma-client/working-with-fields/working-with-json-fields#filtering-on-a-json-field
*/
export const AnyNull = runtime.objectEnumValues.instances.AnyNull
export const ModelName = {
users: 'users',
activity_logs: 'activity_logs',
training_menus: 'training_menus',
user_recaps: 'user_recaps'
} as const
export type ModelName = (typeof ModelName)[keyof typeof ModelName]
/*
* Enums
*/
export const TransactionIsolationLevel = runtime.makeStrictEnum({
ReadUncommitted: 'ReadUncommitted',
ReadCommitted: 'ReadCommitted',
RepeatableRead: 'RepeatableRead',
Serializable: 'Serializable'
} as const)
export type TransactionIsolationLevel = (typeof TransactionIsolationLevel)[keyof typeof TransactionIsolationLevel]
export const UsersScalarFieldEnum = {
id: 'id',
name: 'name',
role: 'role',
coach_id: 'coach_id',
created_at: 'created_at'
} as const
export type UsersScalarFieldEnum = (typeof UsersScalarFieldEnum)[keyof typeof UsersScalarFieldEnum]
export const Activity_logsScalarFieldEnum = {
id: 'id',
timestamp: 'timestamp',
status: 'status',
confidence: 'confidence',
details: 'details',
user_id: 'user_id'
} as const
export type Activity_logsScalarFieldEnum = (typeof Activity_logsScalarFieldEnum)[keyof typeof Activity_logsScalarFieldEnum]
export const Training_menusScalarFieldEnum = {
id: 'id',
name: 'name',
exercises: 'exercises',
created_at: 'created_at',
author_id: 'author_id',
client_id: 'client_id'
} as const
export type Training_menusScalarFieldEnum = (typeof Training_menusScalarFieldEnum)[keyof typeof Training_menusScalarFieldEnum]
export const User_recapsScalarFieldEnum = {
id: 'id',
menu_id: 'menu_id',
user_id: 'user_id',
summary: 'summary',
completed_at: 'completed_at'
} as const
export type User_recapsScalarFieldEnum = (typeof User_recapsScalarFieldEnum)[keyof typeof User_recapsScalarFieldEnum]
export const SortOrder = {
asc: 'asc',
desc: 'desc'
} as const
export type SortOrder = (typeof SortOrder)[keyof typeof SortOrder]
export const NullableJsonNullValueInput = {
DbNull: DbNull,
JsonNull: JsonNull
} as const
export type NullableJsonNullValueInput = (typeof NullableJsonNullValueInput)[keyof typeof NullableJsonNullValueInput]
export const QueryMode = {
default: 'default',
insensitive: 'insensitive'
} as const
export type QueryMode = (typeof QueryMode)[keyof typeof QueryMode]
export const NullsOrder = {
first: 'first',
last: 'last'
} as const
export type NullsOrder = (typeof NullsOrder)[keyof typeof NullsOrder]
export const JsonNullValueFilter = {
DbNull: DbNull,
JsonNull: JsonNull,
AnyNull: AnyNull
} as const
export type JsonNullValueFilter = (typeof JsonNullValueFilter)[keyof typeof JsonNullValueFilter]

View File

@ -0,0 +1,15 @@
/* !!! This is code generated by Prisma. Do not edit directly. !!! */
/* eslint-disable */
// biome-ignore-all lint: generated file
// @ts-nocheck
/*
* This is a barrel export file for all models and their related types.
*
* 🟢 You can import this file directly.
*/
export type * from './models/users.js'
export type * from './models/activity_logs.js'
export type * from './models/training_menus.js'
export type * from './models/user_recaps.js'
export type * from './commonInputTypes.js'

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

Binary file not shown.

27
app/globals.css Normal file
View File

@ -0,0 +1,27 @@
@import "tailwindcss";
@theme inline {
--color-background: var(--background);
--color-foreground: var(--foreground);
--font-sans: var(--font-geist-sans);
--font-mono: var(--font-geist-mono);
/* Friendly Palette */
--color-primary: #5682B1; /* Steel Blue */
--color-secondary: #739EC9; /* Soft Blue */
--color-highlight: #404040; /* Dark Grey for text contrast, replacing peach which is too light for text */
--color-accent: #FFE8DB; /* Peach (New variable for backgrounds) */
}
/* Friendly Light Theme Default */
:root {
--background: #FFFFFF;
--foreground: #171717;
}
/* Dark mode removed to force Friendly Light Theme */
body {
background: var(--background);
color: var(--foreground);
font-family: Arial, Helvetica, sans-serif;
}

37
app/layout.tsx Normal file
View File

@ -0,0 +1,37 @@
import type { Metadata } from "next";
import { Geist, Geist_Mono } from "next/font/google";
import "./globals.css";
const geistSans = Geist({
variable: "--font-geist-sans",
subsets: ["latin"],
});
const geistMono = Geist_Mono({
variable: "--font-geist-mono",
subsets: ["latin"],
});
export const metadata: Metadata = {
title: "STRAPS",
description: "Smart Training & Rehab System",
icons: {
icon: [{ url: "/favicon.svg", type: "image/svg+xml" }],
},
};
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<html lang="en">
<body
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
>
{children}
</body>
</html>
);
}

310
app/page.tsx Normal file
View File

@ -0,0 +1,310 @@
'use client';
import React, { useState } from 'react';
import Link from 'next/link';
import { motion } from 'framer-motion';
import { User, Shield, ArrowRight, Activity } from 'lucide-react';
import { useRouter } from 'next/navigation';
import { AuthProvider, useAuth } from '@/lib/auth';
export default function LandingPageWrap() {
return (
<AuthProvider>
<LandingPage />
</AuthProvider>
);
}
function LandingPage() {
const router = useRouter();
const { login } = useAuth();
const [role, setRole] = useState<'coach' | 'client' | null>(null);
const [userId, setUserId] = useState('');
const [status, setStatus] = useState('');
const [isLoading, setIsLoading] = useState(false);
// Registration State
const [isRegistering, setIsRegistering] = useState(false);
const [regName, setRegName] = useState('');
const [regRole, setRegRole] = useState<'COACH' | 'CLIENT' | null>(null);
const [generatedUser, setGeneratedUser] = useState<any>(null);
const handleRegister = async (e: React.FormEvent) => {
e.preventDefault();
if (!regName || !regRole) return;
setIsLoading(true);
setStatus('Creating Account...');
try {
const res = await fetch('/api/register', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name: regName, role: regRole })
});
if (res.ok) {
const newUser = await res.json();
setGeneratedUser(newUser);
setStatus('Account Created!');
} else {
setStatus('Registration Failed');
}
} catch (err) {
setStatus('Error connecting to server');
} finally {
setIsLoading(false);
}
};
const handleLogin = async (e: React.FormEvent) => {
e.preventDefault();
if (!role || !userId) return;
setIsLoading(true);
setStatus('Validating credentials...');
try {
const user = await login(userId);
if (user) {
// Role Validation
if (role === 'coach') {
if (user.role !== 'COACH') {
setStatus('Access Denied: You are not a Coach');
setIsLoading(false);
return;
}
setStatus('Authenticated. Redirecting...');
router.push('/coach/dashboard');
} else {
if (user.role !== 'CLIENT') {
setStatus('Access Denied: You are not a Client');
setIsLoading(false);
return;
}
setStatus('Authenticated. Redirecting...');
router.push('/client');
}
} else {
setStatus('Invalid User ID');
setIsLoading(false);
}
} catch (err) {
setStatus('Connection Error');
setIsLoading(false);
}
};
return (
<div className="min-h-screen bg-white text-zinc-900 font-sans selection:bg-blue-100 flex flex-col">
{/* Decorative Background Elements */}
<div className="fixed inset-0 overflow-hidden pointer-events-none">
<div className="absolute top-0 left-0 w-[500px] h-[500px] bg-blue-50 rounded-full blur-3xl -translate-x-1/2 -translate-y-1/2 opacity-70"></div>
<div className="absolute bottom-0 right-0 w-[500px] h-[500px] bg-indigo-50 rounded-full blur-3xl translate-x-1/2 translate-y-1/2 opacity-70"></div>
</div>
<div className="relative z-10 flex-1 flex flex-col items-center justify-center p-6 md:p-12">
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.8 }}
className="max-w-md w-full"
>
{/* Logo / Header */}
<div className="text-center mb-10">
<h1 className="text-5xl font-light tracking-tight text-zinc-900 mb-2">
STRAPS<span className="font-bold text-primary">-R</span>
</h1>
<p className="text-zinc-500 text-lg tracking-wide">Strength Training Pose Recognition and Patient Rehabilitation</p>
</div>
{/* Login / Register Card */}
<div className="bg-white/80 backdrop-blur-xl border border-white/50 shadow-xl rounded-3xl p-8 md:p-10 relative overflow-hidden transition-all">
{/* Mode Toggle */}
<div className="flex justify-center mb-8">
<div className="bg-zinc-100 p-1 rounded-full flex">
<button
onClick={() => { setIsRegistering(false); setGeneratedUser(null); setStatus(''); }}
className={`px-6 py-2 rounded-full text-sm font-bold transition-all ${!isRegistering ? 'bg-white shadow text-zinc-900' : 'text-zinc-500'}`}
>
LOGIN
</button>
<button
onClick={() => { setIsRegistering(true); setGeneratedUser(null); setStatus(''); }}
className={`px-6 py-2 rounded-full text-sm font-bold transition-all ${isRegistering ? 'bg-white shadow text-zinc-900' : 'text-zinc-500'}`}
>
REGISTER
</button>
</div>
</div>
{isRegistering ? (
generatedUser ? (
<div className="text-center space-y-6 animate-in fade-in slide-in-from-bottom-4">
<div className="w-16 h-16 bg-green-100 rounded-full flex items-center justify-center mx-auto text-green-600">
<Shield className="w-8 h-8" />
</div>
<div>
<h3 className="text-xl font-bold text-zinc-900">Welcome, {generatedUser.name}!</h3>
<p className="text-zinc-500 text-sm mt-1">Your account has been created.</p>
</div>
<div className="bg-zinc-50 border-2 border-dashed border-zinc-200 p-6 rounded-2xl">
<p className="text-xs font-bold text-zinc-400 uppercase tracking-widest mb-2">Your Unique User ID</p>
<p className="text-4xl font-black text-primary tracking-tighter">{generatedUser.id}</p>
</div>
<p className="text-xs text-red-400">Please save this ID. You will need it to login.</p>
<button
onClick={() => {
setIsRegistering(false);
setRole(generatedUser.role === 'COACH' ? 'coach' : 'client');
setUserId(generatedUser.id.toString());
setGeneratedUser(null);
}}
className="w-full py-4 bg-zinc-900 text-white rounded-xl font-bold tracking-widest uppercase hover:bg-black transition-all"
>
Go to Login
</button>
</div>
) : (
<form onSubmit={handleRegister} className="space-y-6 animate-in fade-in slide-in-from-bottom-4">
<div className="space-y-4">
<div>
<label className="text-xs font-bold text-zinc-400 uppercase tracking-widest ml-1">Full Name</label>
<input
required
type="text"
placeholder="e.g. John Doe"
value={regName}
onChange={(e) => setRegName(e.target.value)}
className="w-full bg-zinc-50 border border-zinc-200 rounded-xl px-4 py-3 text-lg focus:outline-none focus:border-zinc-400 transition-colors text-zinc-900"
/>
</div>
<div>
<label className="text-xs font-bold text-zinc-400 uppercase tracking-widest ml-1">Role</label>
<div className="grid grid-cols-2 gap-3 mt-1">
<button
type="button"
onClick={() => setRegRole('COACH')}
className={`p-3 rounded-xl border-2 transition-all flex flex-col items-center gap-2 ${
regRole === 'COACH'
? 'border-primary bg-blue-50 text-primary'
: 'border-zinc-100 bg-zinc-50 text-zinc-400'
}`}
>
<Shield className="w-5 h-5" />
<span className="font-bold text-xs">COACH</span>
</button>
<button
type="button"
onClick={() => setRegRole('CLIENT')}
className={`p-3 rounded-xl border-2 transition-all flex flex-col items-center gap-2 ${
regRole === 'CLIENT'
? 'border-emerald-500 bg-emerald-50 text-emerald-600'
: 'border-zinc-100 bg-zinc-50 text-zinc-400'
}`}
>
<User className="w-5 h-5" />
<span className="font-bold text-xs">CLIENT</span>
</button>
</div>
</div>
</div>
<button
type="submit"
disabled={!regName || !regRole || isLoading}
className={`w-full py-4 rounded-xl font-bold tracking-widest uppercase transition-all flex items-center justify-center gap-2 ${
regName && regRole && !isLoading
? 'bg-zinc-900 text-white hover:bg-black shadow-lg'
: 'bg-zinc-100 text-zinc-300 cursor-not-allowed'
}`}
>
{isLoading ? 'Creating...' : 'Create Account'}
</button>
</form>
)
) : (
<>
{/* Role Selection */}
<div className="grid grid-cols-2 gap-4 mb-8">
<button
onClick={() => setRole('coach')}
className={`p-4 rounded-xl border-2 transition-all flex flex-col items-center gap-3 ${
role === 'coach'
? 'border-primary bg-blue-50/50 text-primary shadow-sm'
: 'border-zinc-100 bg-zinc-50 text-zinc-400 hover:border-zinc-200 hover:bg-zinc-100'
}`}
>
<Shield className={`w-8 h-8 ${role === 'coach' ? 'fill-current' : ''}`} />
<span className="font-bold tracking-wider text-xs uppercase">Coach</span>
</button>
<button
onClick={() => setRole('client')}
className={`p-4 rounded-xl border-2 transition-all flex flex-col items-center gap-3 ${
role === 'client'
? 'border-emerald-500 bg-emerald-50/50 text-emerald-600 shadow-sm'
: 'border-zinc-100 bg-zinc-50 text-zinc-400 hover:border-zinc-200 hover:bg-zinc-100'
}`}
>
<User className={`w-8 h-8 ${role === 'client' ? 'fill-current' : ''}`} />
<span className="font-bold tracking-wider text-xs uppercase">Client</span>
</button>
</div>
{/* Login Form */}
<form onSubmit={handleLogin} className="space-y-6">
<div className="space-y-2">
<label className="text-xs font-bold text-zinc-400 uppercase tracking-widest ml-1">
{role === 'coach' ? 'Coach Identifier' : 'Client Identifier'}
</label>
<input
type="text"
disabled={!role || isLoading}
placeholder={!role ? "Select a role above" : "Enter ID (e.g. A8#k9P)"}
value={userId}
onChange={(e) => setUserId(e.target.value)}
className="w-full bg-zinc-50 border border-zinc-200 rounded-xl px-4 py-4 text-lg text-center tracking-widest focus:outline-none focus:border-zinc-400 transition-colors disabled:opacity-50 disabled:cursor-not-allowed text-zinc-900 placeholder:text-zinc-300 font-mono"
/>
{status && <p className="text-center text-xs font-bold text-primary animate-pulse">{status}</p>}
</div>
<button
type="submit"
disabled={!role || isLoading}
className={`w-full py-4 rounded-xl font-bold tracking-widest uppercase transition-all flex items-center justify-center gap-2 ${
role
? 'bg-zinc-900 text-white hover:bg-black shadow-lg hover:shadow-xl hover:-translate-y-0.5'
: 'bg-zinc-100 text-zinc-300 cursor-not-allowed'
}`}
>
{isLoading ? (
<span className="w-5 h-5 border-2 border-white/30 border-t-white rounded-full animate-spin" />
) : (
<>
Enter Platform <ArrowRight className="w-4 h-4" />
</>
)}
</button>
</form>
</>
)}
{/* Active Status Indicator */}
<div className="absolute top-6 right-6 flex items-center gap-2">
<span className="relative flex h-2 w-2">
<span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-green-400 opacity-75"></span>
<span className="relative inline-flex rounded-full h-2 w-2 bg-green-500"></span>
</span>
</div>
</div>
<p className="text-center text-zinc-400 text-xs mt-8 tracking-widest">
SECURE ACCESS v2.0.4
</p>
</motion.div>
</div>
</div>
);
}

26
debug_menu.js Normal file
View File

@ -0,0 +1,26 @@
const { PrismaClient } = require('./app/generated/client');
const prisma = new PrismaClient();
async function main() {
const menus = await prisma.training_menus.findMany({
orderBy: { id: 'desc' },
take: 1
});
if (menus.length > 0) {
console.log("Latest Menu:", JSON.stringify(menus[0], null, 2));
const ex = menus[0].exercises;
if (typeof ex === 'string') {
console.log("Exercises (parsed):", JSON.stringify(JSON.parse(ex), null, 2));
} else {
console.log("Exercises (raw):", JSON.stringify(ex, null, 2));
}
} else {
console.log("No menus found.");
}
}
main()
.catch(e => console.error(e))
.finally(async () => await prisma.$disconnect());

18
eslint.config.mjs Normal file
View File

@ -0,0 +1,18 @@
import { defineConfig, globalIgnores } from "eslint/config";
import nextVitals from "eslint-config-next/core-web-vitals";
import nextTs from "eslint-config-next/typescript";
const eslintConfig = defineConfig([
...nextVitals,
...nextTs,
// Override default ignores of eslint-config-next.
globalIgnores([
// Default ignores of eslint-config-next:
".next/**",
"out/**",
"build/**",
"next-env.d.ts",
]),
]);
export default eslintConfig;

BIN
exercise_rules.pdf Normal file

Binary file not shown.

3380
integrated.js Normal file

File diff suppressed because it is too large Load Diff

75
lib/auth.tsx Normal file
View File

@ -0,0 +1,75 @@
import React, { createContext, useContext, useEffect, useState } from 'react';
type UserRole = 'COACH' | 'CLIENT';
interface User {
id: string; // Changed to string
name: string;
role: UserRole;
coach_id?: string | null;
coach?: {
id: string;
name: string;
} | null;
}
interface AuthContextType {
user: User | null;
login: (userId: string) => Promise<User | null>; // Changed to string
logout: () => void;
isLoading: boolean;
}
const AuthContext = createContext<AuthContextType>({
user: null,
login: async () => null,
logout: () => {},
isLoading: true,
});
export function AuthProvider({ children }: { children: React.ReactNode }) {
const [user, setUser] = useState<User | null>(null);
const [isLoading, setIsLoading] = useState(true);
useEffect(() => {
// Check sessionStorage on mount
const storedUser = sessionStorage.getItem('straps_user');
if (storedUser) {
setUser(JSON.parse(storedUser));
}
setIsLoading(false);
}, []);
const login = async (id: string): Promise<User | null> => {
setIsLoading(true);
try {
const res = await fetch(`/api/users/${id}`);
if (res.ok) {
const userData = await res.json();
setUser(userData);
sessionStorage.setItem('straps_user', JSON.stringify(userData)); // Store full object in Session
return userData;
}
return null;
} catch (error) {
console.error("Login failed", error);
return null;
} finally {
setIsLoading(false);
}
};
const logout = () => {
setUser(null);
sessionStorage.removeItem('straps_user');
};
return (
<AuthContext.Provider value={{ user, login, logout, isLoading }}>
{children}
</AuthContext.Provider>
);
}
export const useAuth = () => useContext(AuthContext);

7
lib/mediapipe-shim.js Normal file
View File

@ -0,0 +1,7 @@
// This shim bridges the gap between Webpack and the MediaPipe global script
module.exports = {
get Pose() { return (typeof window !== 'undefined' ? window.Pose : undefined); },
get POSE_CONNECTIONS() { return (typeof window !== 'undefined' ? window.POSE_CONNECTIONS : undefined); },
get VERSION() { return (typeof window !== 'undefined' ? window.VERSION : undefined); }
};

141
lib/pose/ExerciseRules.ts Normal file
View File

@ -0,0 +1,141 @@
export interface Landmark {
x: number;
y: number;
z: number;
visibility?: number;
}
export interface AnglesDict {
[key: string]: number;
}
export type FormValidationResult = {
valid: boolean;
feedback: string[];
};
// Expanded Configuration Interface to match Python
export interface ExerciseConfig {
name: string;
// Core Identification
detection: {
shoulder_static?: [number, number];
shoulder_down?: [number, number];
hip_static?: [number, number];
};
// Counting Logic
phase_type: 'start_down' | 'start_up';
dynamic_angles: {
[key: string]: [number, number]; // e.g., elbow_up: [0, 60]
};
// Scoring & Validation
static_angles?: { [key: string]: number }; // Ideal static angle
wrist_distance?: [number, number];
convex_hull?: {
up?: [number, number];
down?: [number, number];
};
// Legacy support (optional)
form_rules?: Array<(landmarks: Landmark[], angles: AnglesDict, side?: 'left' | 'right') => FormValidationResult>;
}
export const EXERCISE_CONFIGS: { [key: string]: ExerciseConfig } = {
'bicep_curl': {
name: "Bicep Curl",
phase_type: 'start_down',
detection: { shoulder_static: [0, 30] },
dynamic_angles: {
'elbow_down': [140, 180],
'elbow_up': [0, 85],
'shoulder_down': [0, 30],
'shoulder_up': [0, 60]
},
static_angles: { 'shoulder_r': 15, 'shoulder_l': 15 },
wrist_distance: [0, 0.3],
convex_hull: { down: [0, 0.05], up: [0.05, 0.2] }
},
'hammer_curl': {
name: "Hammer Curl",
phase_type: 'start_down',
detection: { shoulder_static: [0, 30] },
dynamic_angles: {
'elbow_down': [120, 180],
'elbow_up': [0, 85], // Similar to bicep, maybe slightly different in 3D but same in 2D
'shoulder_down': [0, 30],
'shoulder_up': [0, 60]
},
static_angles: { 'shoulder_r': 15, 'shoulder_l': 15 },
wrist_distance: [0, 0.2], // Hammer curl usually keeps weights closer?
convex_hull: { down: [0, 0.05], up: [0.05, 0.2] }
},
'shoulder_press': { // Overhead Press
name: "Overhead Press",
phase_type: 'start_down', // Starts at shoulders, goes UP. Actually "Down" state is hands at shoulders. "Up" is hands in air.
detection: { shoulder_down: [50, 120] }, // Relaxed detection
dynamic_angles: {
'elbow_down': [20, 100], // Relaxed bottom position (can stop at chin level)
'elbow_up': [150, 180], // Relaxed lockout (sometimes 140 is enough)
'shoulder_down': [40, 110], // Relaxed shoulder range
'shoulder_up': [130, 180] // Relaxed top range
},
static_angles: { 'hip_r': 165, 'hip_l': 165 }, // Standing straight
wrist_distance: [0, 0.3],
convex_hull: { down: [0.05, 0.15], up: [0.15, 0.3] }
},
'lateral_raises': {
name: "Lateral Raises",
phase_type: 'start_down', // Arms at sides
detection: {},
dynamic_angles: {
'shoulder_down': [0, 30],
'shoulder_up': [80, 110], // T-pose
'elbow_down': [140, 180], // Straight arm
'elbow_up': [140, 180] // Keep arms straight
},
static_angles: { 'elbow_r': 160, 'elbow_l': 160 },
convex_hull: { down: [0, 0.1], up: [0.2, 0.4] } // Wide hull when arms up
},
'squat': {
name: "Squat",
phase_type: 'start_up', // Standing -> Squat -> Standing
detection: {},
dynamic_angles: {
'hip_up': [160, 180], // Standing
'hip_down': [50, 100], // Squat depth
'knee_up': [160, 180],
'knee_down': [50, 100]
},
static_angles: { 'shoulder_r': 20, 'shoulder_l': 20 }, // Torso relatively upright
convex_hull: { up: [0.1, 0.2], down: [0.05, 0.15] } // Hull shrinks when squatting? Or stays same?
},
'deadlift': {
name: "Deadlift",
// Actually deadlift starts on floor. So 'start_down' (hips low) -> 'up' (hips high/standing).
phase_type: 'start_down', // Down (at floor) -> Up (Standing). Warning: Logic usually assumes "Down" means "Rest/Start".
detection: {},
dynamic_angles: {
'hip_down': [45, 100], // Hips flexed at bottom
'hip_up': [160, 180], // Hips extended at top
'knee_down': [60, 120], // Knees bent
'knee_up': [160, 180] // Knees locked
},
static_angles: { 'elbow_r': 170, 'elbow_l': 170 }, // Arms straight
convex_hull: { down: [0.1, 0.2], up: [0.1, 0.2] }
},
'lunges': {
name: "Lunges",
phase_type: 'start_up', // Standing -> Lunge -> Standing
detection: {},
dynamic_angles: {
'knee_up': [160, 180], // Standing
'knee_down': [70, 110], // Lunge depth
'hip_up': [160, 180],
'hip_down': [70, 110]
},
static_angles: {},
convex_hull: {}
}
};

151
lib/pose/HARCore.ts Normal file
View File

@ -0,0 +1,151 @@
import { XGBoostPredictor } from './XGBoostPredictor';
import { Landmark, EXERCISE_CONFIGS } from './ExerciseRules';
import { RehabCore } from './RehabCore';
import { calculateAngle } from './MathUtils';
// Label Encoder mapping from python: Classes: ['berdiri' 'duduk' 'jatuh']
const LABELS = ['Standing', 'Sitting', 'Fall Detected'];
export class HARCore {
private predictor: XGBoostPredictor;
private rehab: RehabCore;
private currentExercise: string | null = null;
constructor() {
this.predictor = new XGBoostPredictor();
this.rehab = new RehabCore();
}
// Set the active exercise to track (from menu)
public setExercise(name: string) {
// Map UI name to internal config name if needed
// For now assume direct match e.g. "Bicep Curl" -> "bicep_curls"
// Simple normalizer
const lowerName = name.toLowerCase();
// Find matching key in EXERCISE_CONFIGS
// EXERCISE_CONFIGS keys are like 'bicep_curl', 'squat'
// UI names might be "Bicep Curl", "Squats"
const key = Object.keys(EXERCISE_CONFIGS).find(k =>
lowerName.includes(k.replace('_', ' ')) ||
k.replace('_', ' ').includes(lowerName.split(' ')[0])
);
this.currentExercise = key || null;
}
public resetParams() {
this.rehab.reset();
// this.currentExercise = null; // Don't nullify exercise, just counters
}
public async process(landmarks: Landmark[], worldLandmarks: Landmark[] = []) {
if (!landmarks || landmarks.length === 0) return null;
// 1. Activity Recognition (HAR) - XGBoost
const features = this.extractFeatures(landmarks);
const probs = this.predictor.predict(features);
const maxIdx = probs.indexOf(Math.max(...probs));
const status = LABELS[maxIdx];
const confidence = probs[maxIdx];
// 2. Exercise Counting (Rehab) - Heuristic
let reps = 0;
let feedback = "";
let debug = {};
if (this.currentExercise) {
const result = this.rehab.process(this.currentExercise, landmarks, worldLandmarks);
if (result) {
// Combine left/right reps for total or max?
// Usually we want total completed reps.
reps = this.rehab.getReps(this.currentExercise);
// Construct feedback
const stateL = result.left.stage;
const stateR = result.right.stage;
feedback = `L: ${stateL || '-'} | R: ${stateR || '-'}`;
if (result.feedback && result.feedback.length > 0) {
feedback += ` | ${result.feedback}`; // Add generic feedback
}
debug = {
angles: { l: result.left.angle, r: result.right.angle },
scores: result.scores
};
}
}
return {
status,
confidence,
exercise: this.currentExercise,
reps,
feedback,
debug
};
}
private extractFeatures(landmarks: Landmark[]): number[] {
// 1. Flatten Raw Keypoints (33 * 4 = 132 features)
const raw: number[] = [];
landmarks.forEach(lm => {
raw.push(lm.x, lm.y, lm.z, lm.visibility || 0);
});
// 2. Derived Features
// Helper to get landmark
const getLm = (idx: number) => landmarks[idx];
// Helper to flatten {x,y} to point for calculateAngle
const pt = (lm: Landmark) => ({x: lm.x, y: lm.y});
const calcAng = (a: Landmark, b: Landmark, c: Landmark) => calculateAngle(pt(a), pt(b), pt(c));
const derived: number[] = [];
// Angles
// 0: Left Elbow (11-13-15)
derived.push(calcAng(getLm(11), getLm(13), getLm(15)));
// 1: Right Elbow (12-14-16)
derived.push(calcAng(getLm(12), getLm(14), getLm(16)));
// 2: Left Hip (11-23-25)
derived.push(calcAng(getLm(11), getLm(23), getLm(25)));
// 3: Right Hip (12-24-26)
derived.push(calcAng(getLm(12), getLm(24), getLm(26)));
// 4: Left Knee (23-25-27)
derived.push(calcAng(getLm(23), getLm(25), getLm(27)));
// 5: Right Knee (24-26-28)
derived.push(calcAng(getLm(24), getLm(26), getLm(28)));
// Distances & Ratios
const dist = (a: Landmark, b: Landmark) => Math.sqrt(Math.pow(a.x - b.x, 2) + Math.pow(a.y - b.y, 2));
const shoulderWidth = dist(getLm(11), getLm(12));
const hipWidth = dist(getLm(23), getLm(24));
const midShoulder = { x: (getLm(11).x + getLm(12).x)/2, y: (getLm(11).y + getLm(12).y)/2 };
const midHip = { x: (getLm(23).x + getLm(24).x)/2, y: (getLm(23).y + getLm(24).y)/2 };
const torsoHeight = dist(midShoulder as Landmark, midHip as Landmark);
const eps = 1e-6;
// 6: Shoulder Width Ratio
derived.push(shoulderWidth / (torsoHeight + eps));
// 7: Hip Width Ratio
derived.push(hipWidth / (torsoHeight + eps));
// 8: Torso Vertical Alignment (Cosine Similarity with [0, -1])
// Vector from Hip to Shoulder (Upwards)
const torsoVec = { x: midShoulder.x - midHip.x, y: midShoulder.y - midHip.y };
const verticalVec = { x: 0, y: -1 }; // Up in image coordinates (y is down)?
// Python: vertical_vector = np.array([0, -1])
// In python/opencv y is down. So [0, -1] is UP vector.
const dot = (torsoVec.x * verticalVec.x) + (torsoVec.y * verticalVec.y);
const norm = Math.sqrt(torsoVec.x * torsoVec.x + torsoVec.y * torsoVec.y);
derived.push(norm > 0 ? dot / norm : 0);
return [...raw, ...derived];
}
}

176
lib/pose/MathUtils.ts Normal file
View File

@ -0,0 +1,176 @@
import { Landmark } from './ExerciseRules';
// --- Types ---
export type Point = { x: number; y: number };
// --- Basic Geometry ---
export function computeDistance(p1: Point, p2: Point): number {
return Math.sqrt(Math.pow(p1.x - p2.x, 2) + Math.pow(p1.y - p2.y, 2));
}
export function calculateAngle(a: Point, b: Point, c: Point): number {
const radians = Math.atan2(c.y - b.y, c.x - b.x) - Math.atan2(a.y - b.y, a.x - b.x);
let angle = Math.abs((radians * 180.0) / Math.PI);
if (angle > 180.0) angle = 360 - angle;
return angle;
}
export function inRange(val: number, low: number, high: number): boolean {
return val >= low && val <= high;
}
// --- Normalization (Port of normalize_v2) ---
function formatLandmark(lm: Landmark): Point {
return { x: lm.x, y: lm.y };
}
export function normalizeLandmarks(landmarks: Landmark[]): Point[] {
// Indices for torso: 11(sho_l), 12(sho_r), 23(hip_l), 24(hip_r)
// Note: Python mediapipe indices match JS.
const indices = [11, 12, 23, 24];
// Prepare points for least squares (Torso alignment)
const pts = indices.map(i => ({ x: landmarks[i].x, y: landmarks[i].y }));
// Linear Regression (Least Squares) to find torso centerline angle
// We want line y = mx + c. But vertical lines fail, so we often do PCA or simple regression.
// Python code uses np.linalg.lstsq on X to predict Y.
let sumX = 0, sumY = 0, sumXY = 0, sumXX = 0;
const n = pts.length;
for (const p of pts) {
sumX += p.x;
sumY += p.y;
sumXY += p.x * p.y;
sumXX += p.x * p.x;
}
const m = (n * sumXY - sumX * sumY) / (n * sumXX - sumX * sumX);
// If undefined (perfect vertical), theta is 90 deg.
const theta = !isFinite(m) ? Math.PI / 2 : Math.atan(m);
const cos_t = Math.cos(-theta);
const sin_t = Math.sin(-theta);
// Centers
const sho_l = landmarks[11];
const sho_r = landmarks[12];
const hip_l = landmarks[23];
const hip_r = landmarks[24];
const shoulder_center = { x: (sho_l.x + sho_r.x) / 2, y: (sho_l.y + sho_r.y) / 2 };
const hip_center = { x: (hip_l.x + hip_r.x) / 2, y: (hip_l.y + hip_r.y) / 2 };
const scale_factor = computeDistance(shoulder_center, hip_center);
// Normalize logic from Python:
// 1. Shift by hip_center
// 2. Rotate by theta
// 3. Scale by scale_factor
return landmarks.map(lm => {
const x = lm.x - hip_center.x;
const y = lm.y - hip_center.y;
const x_rot = (x * cos_t - y * sin_t) / scale_factor;
const y_rot = (x * sin_t + y * cos_t) / scale_factor;
return { x: x_rot, y: y_rot };
});
}
// --- Convex Hull (Monotone Chain Algorithm) ---
function crossProduct(o: Point, a: Point, b: Point): number {
return (a.x - o.x) * (b.y - o.y) - (a.y - o.y) * (b.x - o.x);
}
export function computeConvexHullArea(points: Point[]): number {
const n = points.length;
if (n <= 2) return 0;
// Sort points by x, then y
const sorted = [...points].sort((a, b) => a.x === b.x ? a.y - b.y : a.x - b.x);
// Build lower hull
const lower: Point[] = [];
for (const p of sorted) {
while (lower.length >= 2 && crossProduct(lower[lower.length - 2], lower[lower.length - 1], p) <= 0) {
lower.pop();
}
lower.push(p);
}
// Build upper hull
const upper: Point[] = [];
for (let i = n - 1; i >= 0; i--) {
const p = sorted[i];
while (upper.length >= 2 && crossProduct(upper[upper.length - 2], upper[upper.length - 1], p) <= 0) {
upper.pop();
}
upper.push(p);
}
// Concatenate (remove last point of lower and upper as they are duplicates of start/end)
const hull = [...lower.slice(0, -1), ...upper.slice(0, -1)];
// Shoelace Formula for Area
let area = 0;
for (let i = 0; i < hull.length; i++) {
const j = (i + 1) % hull.length;
area += hull[i].x * hull[j].y;
area -= hull[j].x * hull[i].y;
}
return Math.abs(area) / 2;
}
// --- Scoring Utilities ---
export function calculateContainmentScore(userRange: [number, number], refRange: [number, number]): number {
const [user_min, user_max] = userRange;
const [ref_min, ref_max] = refRange;
if (user_min === user_max) {
return (user_min >= ref_min && user_min <= ref_max) ? 1.0 : 0.0;
}
const user_length = user_max - user_min;
if (user_length <= 0) return 1.0;
const intersection_min = Math.max(user_min, ref_min);
const intersection_max = Math.min(user_max, ref_max);
const intersection_length = Math.max(0, intersection_max - intersection_min);
return intersection_length / user_length;
}
/*
* Calculates the Mean Absolute Error (MAE) for a value against a target range.
* If the value is within range, error is 0.
* If outside, error is distance to the nearest bound.
*/
export function calculateRangeDeviation(value: number, range: [number, number]): number {
const [min, max] = range;
// If value is smaller than min, return difference
if (value < min) return Math.abs(min - value);
// If value is larger than max, return difference
if (value > max) return Math.abs(value - max);
// Within range, perfect score (0 deviation)
return 0;
}
/**
* Computes average deviation across multiple joints.
*/
export function computeMAE(errors: number[]): number {
if (errors.length === 0) return 0;
const sum = errors.reduce((a, b) => a + b, 0);
return sum / errors.length;
}
// End of file

348
lib/pose/RehabCore.ts Normal file
View File

@ -0,0 +1,348 @@
import { Landmark, EXERCISE_CONFIGS } from './ExerciseRules';
import {
computeFeatures, RepFSM, Vec3, PoseFeatures,
BicepCurlCounter, HammerCurlCounter, OverheadPressCounter,
LateralRaiseCounter, SquatCounter, DeadliftCounter, LungeCounter
} from './RehabFSM';
import { calculateRangeDeviation, computeMAE } from './MathUtils';
const normalizeExerciseName = (input: string): string => {
if (!input) return '';
const clean = input.toLowerCase().trim().replace(/\s+/g, '_'); // "Overhead Press" -> "overhead_press"
// Map common variations to internal keys
if (clean.includes('bicep')) return 'bicep_curl';
if (clean.includes('hammer')) return 'hammer_curl';
if (clean.includes('overhead') || clean.includes('shoulder_press')) return 'shoulder_press';
if (clean.includes('lateral')) return 'lateral_raises';
if (clean.includes('squat')) return 'squat';
if (clean.includes('deadlift')) return 'deadlift';
if (clean.includes('lunge')) return 'lunges';
return clean; // Fallback
};
// Map UI names to Counter Classes
const COUNTER_MAP: { [key: string]: () => RepFSM[] } = {
'bicep_curl': () => [new BicepCurlCounter('left'), new BicepCurlCounter('right')],
'hammer_curl': () => [new HammerCurlCounter('left'), new HammerCurlCounter('right')],
'shoulder_press': () => [new OverheadPressCounter()], // Bilateral logic inside? No, it's single counter based on avg/both
'lateral_raises': () => [new LateralRaiseCounter()],
'squat': () => [new SquatCounter()],
'deadlift': () => [new DeadliftCounter()],
'lunges': () => [new LungeCounter()] // Bilateral or unified? FSM seems unified (min of both knees)
};
export class RehabCore {
private counters: { [key: string]: RepFSM[] } = {};
private worldLandmarksCache: Vec3[] = []; // If we had world landmarks, for now we might approximate or expect them passed
// NEW: Threshold for warning (degrees)
private readonly DEVIATION_THRESHOLD = 15.0;
constructor() {
// Initialize all counters? Or lazy load?
// Let's lazy load or init on reset.
}
public reset() {
this.counters = {};
console.log("RehabCore: Counters reset");
}
// --- UPDATED: Comprehensive 6-Way Wrong Exercise Detection ---
private validateExerciseType(
configKey: string,
features: PoseFeatures
): string | null {
// Feature Extraction
const minKneeAngle = Math.min(features.leftKnee, features.rightKnee);
const isLegsBent = minKneeAngle < 130;
const minElbowAngle = Math.min(features.leftElbow, features.rightElbow);
const isElbowsBent = minElbowAngle < 110;
const isArmsStraight = minElbowAngle > 140;
const isHandsOverhead = (features.leftWristY < features.noseY) || (features.rightWristY < features.noseY);
const isHandsLow = (features.leftWristY > features.leftShoulderY) && (features.rightWristY > features.rightShoulderY);
const diffElbow = Math.abs(features.leftElbow - features.rightElbow);
const isAlternating = diffElbow > 40;
const isSimultaneous = diffElbow < 20;
if (configKey === 'bicep_curl') {
if (isLegsBent) return "Detected: Squat/Lunge. Stand straight for Curls.";
if (isHandsOverhead) return "Detected: Overhead Press. Keep elbows down.";
if (isArmsStraight && !isHandsLow) return "Detected: Lateral Raise. Bend your elbows.";
if (isAlternating) return "Detected: Hammer Curl (Alternating). Move both arms together.";
return null;
}
if (configKey === 'hammer_curl') {
if (isLegsBent) return "Detected: Squat/Lunge. Stand straight.";
if (isHandsOverhead) return "Detected: Overhead Press. Keep elbows down.";
if (isArmsStraight && !isHandsLow) return "Detected: Lateral Raise. Bend your elbows.";
if (isSimultaneous && isElbowsBent) return "Detected: Bicep Curl (Simultaneous). Alternate arms.";
return null;
}
if (configKey === 'shoulder_press') {
if (isLegsBent) return "Detected: Squat/Lunge. Focus on upper body.";
if (isHandsLow && isElbowsBent) return "Detected: Bicep Curl. Push weight UP, not curl.";
if (isArmsStraight && !isHandsOverhead) return "Detected: Lateral Raise/Deadlift. Press overhead.";
if (isAlternating) return "Detected: Alternating Press. Push both arms together.";
return null;
}
if (configKey === 'lateral_raises') {
if (isLegsBent) return "Detected: Squat/Lunge. Stand straight.";
if (isHandsOverhead) return "Detected: Overhead Press. Stop at shoulder height.";
if (isElbowsBent) return "Detected: Bicep/Hammer Curl. Keep arms straight (T-pose).";
return null;
}
if (configKey === 'squat') {
if (!isLegsBent && isElbowsBent) return "Detected: Bicep/Hammer Curl. Bend your knees!";
if (!isLegsBent && isHandsOverhead) return "Detected: Overhead Press. Focus on legs.";
const diffKnee = Math.abs(features.leftKnee - features.rightKnee);
if (isLegsBent && diffKnee > 30) return "Detected: Lunge. Keep knees symmetrical for Squat.";
return null;
}
if (configKey === 'lunges') {
if (!isLegsBent && isElbowsBent) return "Detected: Curl. Focus on legs.";
if (!isLegsBent && isHandsOverhead) return "Detected: Press. Focus on legs.";
const diffKnee = Math.abs(features.leftKnee - features.rightKnee);
if (isLegsBent && diffKnee < 15) return "Detected: Squat. Step one foot back for Lunge.";
return null;
}
if (configKey === 'deadlift') {
const isHipsBent = features.leftHip < 140 || features.rightHip < 140;
if (!isHipsBent && isElbowsBent) return "Detected: Curl. Keep arms straight/locked.";
if (!isHipsBent && isHandsOverhead) return "Detected: Press. Keep bar low.";
if (!isHipsBent && isLegsBent) return "Detected: Squat (Too much knee). Hinge at hips more.";
return null;
}
return null;
}
private calculateDeviation(
configKey: string,
features: any,
fsmState: "LOW" | "HIGH"
): { mae: number; isDeviating: boolean; details: string[] } {
const config = EXERCISE_CONFIGS[configKey];
if (!config || !config.dynamic_angles) {
return { mae: 0, isDeviating: false, details: [] };
}
let targetSuffix = '';
if (config.phase_type === 'start_down') {
targetSuffix = (fsmState === 'HIGH') ? '_up' : '_down';
} else {
targetSuffix = (fsmState === 'HIGH') ? '_down' : '_up';
}
const errors: number[] = [];
const details: string[] = [];
Object.keys(config.dynamic_angles).forEach(key => {
if (key.endsWith(targetSuffix)) {
const prefix = key.replace(targetSuffix, '');
let val = 0;
// --- FIX: Specific Handling for Overhead Press & Curls (Dual Arm Checks) ---
if ((configKey === 'bicep_curl' || configKey === 'hammer_curl' || configKey === 'shoulder_press') && prefix === 'elbow') {
const errL = calculateRangeDeviation(features.leftElbow, config.dynamic_angles[key]);
const errR = calculateRangeDeviation(features.rightElbow, config.dynamic_angles[key]);
errors.push(errL, errR);
if(errL > 0) details.push(`L_${key} dev ${errL.toFixed(0)}`);
if(errR > 0) details.push(`R_${key} dev ${errR.toFixed(0)}`);
return;
}
// Standard Averaging Logic (As requested)
if (prefix.includes('elbow')) val = (features.leftElbow + features.rightElbow) / 2;
else if (prefix.includes('knee')) val = (features.leftKnee + features.rightKnee) / 2;
else if (prefix.includes('hip')) val = (features.leftHip + features.rightHip) / 2;
else if (prefix.includes('shoulder')) val = (features.leftShoulderY * 180);
if(val > 0) {
const err = calculateRangeDeviation(val, config.dynamic_angles[key]);
errors.push(err);
if(err > 0) details.push(`${key} dev ${err.toFixed(0)}`);
}
}
});
const mae = computeMAE(errors);
return {
mae,
isDeviating: mae > this.DEVIATION_THRESHOLD,
details
};
}
public process(exerciseName: string, landmarks: Landmark[], worldLandmarks: Landmark[] = [], frameTime: number = 0) {
// // Normalize exercise name
// const KEY_MAP: {[key:string]: string} = {
// 'bicep_curls': 'bicep_curl',
// 'shoulder_press': 'shoulder_press',
// 'hammer_curls': 'hammer_curl',
// 'lateral_raises': 'lateral_raises',
// 'squats': 'squat',
// 'deadlifts': 'deadlift',
// 'lunges': 'lunges'
// };
const configKey = normalizeExerciseName(exerciseName);
// Init counters if not exists
if (!this.counters[configKey]) {
const factory = COUNTER_MAP[configKey];
if (factory) {
console.log(`RehabCore: Initialized counter for ${configKey}`);
this.counters[configKey] = factory();
} else {
console.warn(`RehabCore: No factory found for exercise "${configKey}" (Raw: ${exerciseName})`);
return null; // Unknown exercise
}
}
const activeCounters = this.counters[configKey];
if (!activeCounters) return null;
// Data Conversion
// We usually need World Landmarks for accurate angles (meters).
// MediaPipe Pose returns:
// 1. poseLandmarks (normalized x,y,z)
// 2. poseWorldLandmarks (meters x,y,z)
//
// The current `landmarks` input in Straps usually comes from `poseLandmarks` (normalized).
// The new algorithm expects `normalized` AND `world`.
// If we only have normalized, we can pass normalized as world, but angles might be skewed by perspective.
// HOWEVER, `angleDeg` uses `sub` and `dot`. If z is normalized (0..1 scale relative to image width), it's roughly ok for basic 2D-ish angles.
// Ideally we update `HARCore` to pass world landmarks too.
// For now, I will use `landmarks` for BOTH, assuming the user is aware or `z` is roughly scaled.
// Actually `HARCore` sees `Landmark` interface which has x,y,z.
const vecLandmarks: Vec3[] = landmarks.map(l => ({ x: l.x, y: l.y, z: l.z || 0, visibility: l.visibility }));
const vecWorld: Vec3[] = (worldLandmarks && worldLandmarks.length > 0)
? worldLandmarks.map(l => ({ x: l.x, y: l.y, z: l.z || 0, visibility: l.visibility }))
: vecLandmarks; // Fallback
// Compute Features
const features = computeFeatures(vecLandmarks, vecWorld, frameTime || Date.now());
// Update Counters
const results = activeCounters.map(c => c.update(features));
// Determine dominant state (Use the first counter as primary reference)
const mainCounter = this.counters[configKey]?.[0];
const fsmState = mainCounter ? mainCounter.state : "LOW";
// Calculate Deviation
const deviationAnalysis = this.calculateDeviation(exerciseName, features, fsmState);
const wrongExerciseWarning = this.validateExerciseType(configKey, features);
// Format Output for HARCore
// Old format: { left: { stage, reps, angle }, right: { stage, reps, angle }, feedback, scores }
// Determine Left/Right results
// If we have 2 counters, usually [0]=Left, [1]=Right (based on my factory above)
// Wait, BicepCurlCounter('left') is first?
// Let's look at factory:
// 'bicep_curl': () => [new BicepCurlCounter('left'), new BicepCurlCounter('right')],
let leftRes = { stage: 'REST', reps: 0, angle: 0 };
let rightRes = { stage: 'REST', reps: 0, angle: 0 };
let feedback = "";
if (configKey === 'bicep_curl' || configKey === 'hammer_curl') {
const lCounter = activeCounters[0];
const rCounter = activeCounters[1];
leftRes = {
stage: lCounter.state === 'HIGH' ? 'UP' : 'DOWN',
reps: lCounter.reps,
angle: features.leftElbow
};
rightRes = {
stage: rCounter.state === 'HIGH' ? 'UP' : 'DOWN',
reps: rCounter.reps,
angle: features.rightElbow
};
} else {
// Unified counters (Squat, Press, etc)
// We apply result to "Both" or just map to nice UI
const main = activeCounters[0];
const stage = main.state === 'HIGH' ? 'UP' : 'DOWN';
const reps = main.reps;
leftRes = { stage, reps, angle: 0 }; // Angle 0 for now as main metric might be diff
rightRes = { stage, reps, angle: 0 };
// Populate specific angles for UI if needed
if (configKey === 'squat') { leftRes.angle = features.leftKnee; rightRes.angle = features.rightKnee; }
if (configKey === 'shoulder_press') { leftRes.angle = features.leftElbow; rightRes.angle = features.rightElbow; } // Approx
}
if (wrongExerciseWarning) {
feedback = `⚠️ ${wrongExerciseWarning}`;
}
// Append Deviation info to feedback
else if (deviationAnalysis.isDeviating) {
const detailText = deviationAnalysis.details.join(", ");
feedback += ` | Fix Form: ${detailText}`;
}
// Accumulate feedback? FSM has `debug`
// results.forEach(r => if(r.debug.note) feedback += r.debug.note + " ");
return {
left: leftRes,
right: rightRes,
feedback: feedback.trim(),
scores: {deviation_mae: deviationAnalysis.mae} // No scores in new FSM yet
};
}
public getReps(exName: string) {
// // Normalized key
// const KEY_MAP: {[key:string]: string} = {
// 'bicep_curls': 'bicep_curl',
// 'shoulder_press': 'shoulder_press',
// 'hammer_curls': 'hammer_curl',
// 'lateral_raises': 'lateral_raises',
// 'squats': 'squat',
// 'deadlifts': 'deadlift',
// 'lunges': 'lunges'
// };
const configKey = normalizeExerciseName(exName);
const counters = this.counters[configKey];
if (!counters || counters.length === 0) return 0;
if (configKey === 'hammer_curl') {
return Math.min(...counters.map(c => c.reps));
}
// If multiple counters (bilateral), usually we return the SUM or MAX or MIN?
// Old logic was: wait for both to complete -> increment total.
// New FSM logic tracks reps independently.
// For Curls, it likely makes sense to show Total Reps (L+R) or Max?
// Usually "1 Rep" means both arms if simultaneous, or 1 each.
// For now, let's return the AVG or MAX.
// If unilateral exercise mode?
// Straps usually assumes bilateral simultaneous.
// If I do 10 left and 10 right = 20 total? or 10 sets?
// Let's return MAX for now (assuming users try to keep sync).
// Actually, if I do alternate curls, I want sum?
// Let's stick to MAX for synchronized exercises.
return Math.max(...counters.map(c => c.reps));
}
}

373
lib/pose/RehabFSM.ts Normal file
View File

@ -0,0 +1,373 @@
export type Vec3 = { x: number; y: number; z: number; visibility?: number };
export const LM = {
NOSE: 0,
LEFT_SHOULDER: 11,
RIGHT_SHOULDER: 12,
LEFT_ELBOW: 13,
RIGHT_ELBOW: 14,
LEFT_WRIST: 15,
RIGHT_WRIST: 16,
LEFT_PINKY: 17, // NEW
RIGHT_PINKY: 18, // NEW
LEFT_INDEX: 19,
RIGHT_INDEX: 20,
LEFT_THUMB: 21, // NEW
RIGHT_THUMB: 22, // NEW
LEFT_HIP: 23,
RIGHT_HIP: 24,
LEFT_KNEE: 25,
RIGHT_KNEE: 26,
LEFT_ANKLE: 27,
RIGHT_ANKLE: 28,
} as const;
function clamp(x: number, a: number, b: number) { return Math.max(a, Math.min(b, x)); }
function sub(a: Vec3, b: Vec3) { return { x: a.x - b.x, y: a.y - b.y, z: a.z - b.z }; }
function dot(a: Vec3, b: Vec3) { return a.x*b.x + a.y*b.y + a.z*b.z; }
function norm(a: Vec3) { return Math.sqrt(dot(a,a)) + 1e-8; }
export function angleDeg(A: Vec3, B: Vec3, C: Vec3): number {
const BA = sub(A,B);
const BC = sub(C,B);
const cos = clamp(dot(BA,BC) / (norm(BA)*norm(BC)), -1, 1);
return Math.acos(cos) * 180 / Math.PI;
}
function ema(prev: number | null, x: number, alpha: number): number {
return prev === null ? x : alpha*x + (1-alpha)*prev;
}
function meanVisibility(lms: Vec3[], idxs: number[]): number {
const v = idxs.map(i => (lms[i]?.visibility ?? 1.0));
const s = v.reduce((a,b)=>a+b, 0);
return v.length ? s / v.length : 0;
}
export type PoseFeatures = {
tMs: number;
leftElbow: number; rightElbow: number;
leftKnee: number; rightKnee: number;
leftHip: number; rightHip: number;
// normalized coords (0..1), y lebih besar = lebih bawah
leftWristY: number; rightWristY: number;
leftShoulderY: number; rightShoulderY: number;
noseY: number;
// NEW: Hand Orientation Data
leftThumbY: number; leftPinkyY: number;
rightThumbY: number; rightPinkyY: number;
// NEW: X Coordinates for Width/Rotation Check
leftThumbX: number; leftPinkyX: number;
rightThumbX: number; rightPinkyX: number;
visArms: number; visLegs: number;
};
export function computeFeatures(
normalized: Vec3[], // landmarks (image coords normalized)
world: Vec3[], // worldLandmarks (meters)
tMs: number
): PoseFeatures {
const A = (i: number) => world[i];
const N = (i: number) => normalized[i];
const leftElbow = angleDeg(A(LM.LEFT_SHOULDER), A(LM.LEFT_ELBOW), A(LM.LEFT_WRIST));
const rightElbow = angleDeg(A(LM.RIGHT_SHOULDER), A(LM.RIGHT_ELBOW), A(LM.RIGHT_WRIST));
const leftKnee = angleDeg(A(LM.LEFT_HIP), A(LM.LEFT_KNEE), A(LM.LEFT_ANKLE));
const rightKnee = angleDeg(A(LM.RIGHT_HIP), A(LM.RIGHT_KNEE), A(LM.RIGHT_ANKLE));
const leftHip = angleDeg(A(LM.LEFT_SHOULDER), A(LM.LEFT_HIP), A(LM.LEFT_KNEE));
const rightHip = angleDeg(A(LM.RIGHT_SHOULDER), A(LM.RIGHT_HIP), A(LM.RIGHT_KNEE));
const armsIdx = [LM.LEFT_SHOULDER, LM.LEFT_ELBOW, LM.LEFT_WRIST, LM.RIGHT_SHOULDER, LM.RIGHT_ELBOW, LM.RIGHT_WRIST];
const legsIdx = [LM.LEFT_HIP, LM.LEFT_KNEE, LM.LEFT_ANKLE, LM.RIGHT_HIP, LM.RIGHT_KNEE, LM.RIGHT_ANKLE];
return {
tMs,
leftElbow, rightElbow,
leftKnee, rightKnee,
leftHip, rightHip,
leftWristY: N(LM.LEFT_WRIST).y,
rightWristY: N(LM.RIGHT_WRIST).y,
leftShoulderY: N(LM.LEFT_SHOULDER).y,
rightShoulderY: N(LM.RIGHT_SHOULDER).y,
noseY: N(LM.NOSE).y,
// Capture Thumb/Pinky Y for grip check
leftThumbY: N(LM.LEFT_THUMB).y,
leftPinkyY: N(LM.LEFT_PINKY).y,
rightThumbY: N(LM.RIGHT_THUMB).y,
rightPinkyY: N(LM.RIGHT_PINKY).y,
// NEW: Capture X coordinates
leftThumbX: N(LM.LEFT_THUMB).x,
leftPinkyX: N(LM.LEFT_PINKY).x,
rightThumbX: N(LM.RIGHT_THUMB).x,
rightPinkyX: N(LM.RIGHT_PINKY).x,
visArms: meanVisibility(world, armsIdx),
visLegs: meanVisibility(world, legsIdx),
};
}
// =======================
// Robust FSM base
// =======================
export class RepFSM {
public state: "LOW" | "HIGH" = "LOW";
public reps = 0;
private metricS: number | null = null;
private metricPrev: number | null = null;
private lastMotionT: number | null = null;
private enteredHighT: number | null = null;
private cycleStartT: number | null = null;
private cycleMin: number | null = null;
private cycleMax: number | null = null;
constructor(
public name: string,
public minVis = 0.6,
public emaAlpha = 0.25,
public idleVelTh = 0.8,
public idleMs = 900,
public highHoldMs = 120,
public minRepMs = 500,
public maxRepMs = 12000,
public minRomDeg = 60,
) {}
visibilityOk(_f: PoseFeatures): boolean { return true; }
metric(_f: PoseFeatures): number { throw new Error("metric not implemented"); }
isLow(_m: number, _f: PoseFeatures): boolean { throw new Error("isLow not implemented"); }
isHigh(_m: number, _f: PoseFeatures): boolean { throw new Error("isHigh not implemented"); }
extraValid(_f: PoseFeatures): boolean { return true; }
private updateRom(m: number) {
this.cycleMin = this.cycleMin === null ? m : Math.min(this.cycleMin, m);
this.cycleMax = this.cycleMax === null ? m : Math.max(this.cycleMax, m);
}
update(f: PoseFeatures): { delta: number; debug: any } {
const t = f.tMs;
if (!this.visibilityOk(f)) {
return { delta: 0, debug: { name: this.name, state: this.state, note: "visibility_fail" } };
}
const mRaw = this.metric(f);
this.metricS = ema(this.metricS, mRaw, this.emaAlpha);
if (this.metricPrev === null) {
this.metricPrev = this.metricS;
this.lastMotionT = t;
return { delta: 0, debug: { name: this.name, state: this.state, m: this.metricS } };
}
const vel = Math.abs(this.metricS - this.metricPrev);
this.metricPrev = this.metricS;
if (vel >= this.idleVelTh) this.lastMotionT = t;
if (this.lastMotionT !== null && (t - this.lastMotionT) > this.idleMs) {
// idle -> reset to safe state
this.state = "LOW";
this.enteredHighT = null;
this.cycleStartT = null;
this.cycleMin = null;
this.cycleMax = null;
return { delta: 0, debug: { name: this.name, state: this.state, note: "idle" } };
}
this.updateRom(this.metricS);
if (this.state === "LOW") {
if (this.isHigh(this.metricS, f) && this.extraValid(f)) {
this.state = "HIGH";
this.enteredHighT = t;
if (this.cycleStartT === null) this.cycleStartT = t;
}
return { delta: 0, debug: { name: this.name, state: this.state, m: this.metricS } };
}
// state HIGH
if (this.enteredHighT !== null && (t - this.enteredHighT) < this.highHoldMs) {
return { delta: 0, debug: { name: this.name, state: this.state, m: this.metricS, note: "hold_high" } };
}
if (this.isLow(this.metricS, f)) {
const dur = this.cycleStartT ? (t - this.cycleStartT) : 0;
const rom = (this.cycleMax !== null && this.cycleMin !== null) ? (this.cycleMax - this.cycleMin) : 0;
const okDur = dur >= this.minRepMs && dur <= this.maxRepMs;
const okRom = rom >= this.minRomDeg;
let delta = 0;
if (okDur && okRom) {
this.reps += 1;
delta = 1;
}
// reset
this.state = "LOW";
this.enteredHighT = null;
this.cycleStartT = null;
this.cycleMin = null;
this.cycleMax = null;
return { delta, debug: { name: this.name, state: this.state, m: this.metricS, dur, rom, okDur, okRom } };
}
return { delta: 0, debug: { name: this.name, state: this.state, m: this.metricS } };
}
}
// =======================
// Exercise counters
// =======================
// --- Base Class for Curls ---
class BaseCurlCounter extends RepFSM {
constructor(public side: "left" | "right", name: string) {
super(name, 0.6, 0.25, 0.8, 900, 120, 500, 12000, 70);
}
private highTh = 85;
private lowTh = 140;
visibilityOk(f: PoseFeatures) { return f.visArms >= this.minVis; }
metric(f: PoseFeatures) { return this.side === "right" ? f.rightElbow : f.leftElbow; }
isLow(m: number) { return m >= this.lowTh; }
isHigh(m: number) { return m <= this.highTh; }
}
export class BicepCurlCounter extends BaseCurlCounter {
constructor(side: "left" | "right" = "right") {
super(side, "bicep_curl");
}
// LOGIC: Simultaneous Lift
// Valid ONLY if the OTHER arm is also active (Bent).
extraValid(f: PoseFeatures) {
const otherArmAngle = this.side === "right" ? f.leftElbow : f.rightElbow;
// Check: Is the other arm bent? (< 120 degrees)
// If the other arm is straight (> 120), then we are NOT doing simultaneous curls.
if (otherArmAngle > 120) {
return false; // Reject: Other arm is lazy/resting
}
return true; // Accept: Both arms are working
}
}
export class HammerCurlCounter extends BaseCurlCounter {
constructor(side: "left" | "right" = "right") {
super(side, "hammer_curl");
}
// LOGIC: Alternating Lift
// Valid ONLY if the OTHER arm is resting (Straight).
extraValid(f: PoseFeatures) {
const otherArmAngle = this.side === "right" ? f.leftElbow : f.rightElbow;
// Check: Is the other arm straight? (> 130 degrees)
// If the other arm is bent (< 130), it means we are moving both (Simultaneous).
if (otherArmAngle < 130) {
return false; // Reject: Both arms are moving
}
return true; // Accept: Only this arm is moving
}
}
export class OverheadPressCounter extends RepFSM {
constructor() {
// Using relaxed Min ROM (30) to ensuring counting
super("overhead_press", 0.6, 0.25, 0.8, 900, 120, 500, 12000, 30);
}
visibilityOk(f: PoseFeatures) { return f.visArms >= this.minVis; }
metric(f: PoseFeatures) {
// Use MIN elbow angle to require at least one arm to lock out
return Math.min(f.leftElbow, f.rightElbow);
}
// High State (Lockout) > 140 deg
isHigh(m: number) {
return m > 150;
}
// Low State (Start/Chin level) < 120 deg
isLow(m: number) {
return m < 140;
}
// Validation: Ensure hands are ABOVE shoulders
extraValid(f: PoseFeatures) {
const isHandsUpL = f.leftWristY < f.leftShoulderY;
const isHandsUpR = f.rightWristY < f.rightShoulderY;
return isHandsUpL && isHandsUpR;
}
}
export class LateralRaiseCounter extends RepFSM {
private highMargin = 0.05;
private lowMargin = 0.12;
constructor() {
super("lateral_raise", 0.6, 0.25, 0.003, 900, 120, 500, 12000, 0);
}
visibilityOk(f: PoseFeatures) { return f.visArms >= this.minVis; }
metric(f: PoseFeatures) {
const dl = Math.abs(f.leftWristY - f.leftShoulderY);
const dr = Math.abs(f.rightWristY - f.rightShoulderY);
return 0.5*(dl+dr);
}
isHigh(m: number) { return m < this.highMargin; }
isLow(m: number) { return m > this.lowMargin; }
extraValid(f: PoseFeatures) { return f.leftElbow > 120 && f.rightElbow > 120; }
}
export class SquatCounter extends RepFSM {
private topTh = 165;
private bottomTh = 100;
constructor() { super("squat", 0.6, 0.25, 0.8, 900, 120, 600, 12000, 40); }
visibilityOk(f: PoseFeatures) { return f.visLegs >= this.minVis; }
metric(f: PoseFeatures) { return Math.min(f.leftKnee, f.rightKnee); }
isLow(m: number) { return m >= this.topTh; } // standing
isHigh(m: number) { return m <= this.bottomTh; } // bottom
extraValid(f: PoseFeatures) { return Math.min(f.leftHip, f.rightHip) < 140; }
}
export class DeadliftCounter extends RepFSM {
private topHipTh = 165;
private bottomHipTh = 120;
constructor() { super("deadlift", 0.6, 0.25, 0.8, 900, 120, 600, 12000, 35); }
visibilityOk(f: PoseFeatures) { return f.visLegs >= this.minVis && f.visArms >= 0.4; }
metric(f: PoseFeatures) { return Math.min(f.leftHip, f.rightHip); }
isLow(m: number) { return m >= this.topHipTh; }
isHigh(m: number, f: PoseFeatures) {
const knee = Math.min(f.leftKnee, f.rightKnee);
return m <= this.bottomHipTh && knee > 110;
}
}
export class LungeCounter extends RepFSM {
private topKneeTh = 165;
private bottomFrontTh = 105;
private bottomBackTh = 130;
constructor() { super("lunge", 0.6, 0.25, 0.8, 900, 120, 600, 12000, 25); }
visibilityOk(f: PoseFeatures) { return f.visLegs >= this.minVis; }
metric(f: PoseFeatures) { return Math.min(f.leftKnee, f.rightKnee); }
isLow(_m: number, f: PoseFeatures) {
return f.leftKnee > this.topKneeTh && f.rightKnee > this.topKneeTh;
}
isHigh(_m: number, f: PoseFeatures) {
const front = Math.min(f.leftKnee, f.rightKnee);
const back = Math.max(f.leftKnee, f.rightKnee);
return front < this.bottomFrontTh && back < this.bottomBackTh;
}
}

View File

@ -0,0 +1,392 @@
import { Landmark, AnglesDict } from './ExerciseRules';
import {
computeConvexHullArea,
normalizeLandmarks,
inRange,
calculateContainmentScore
} from './MathUtils';
// --- Types ---
export type RepData = {
elbow_r: { up: number[], down: number[] };
elbow_l: { up: number[], down: number[] };
shoulder_r: { up: number[], down: number[] };
shoulder_l: { up: number[], down: number[] };
hull_area: { up: number[], down: number[] };
wrist_dist: number[];
static_angles: { [key: string]: number[] };
feedback: string[];
frame_times: number[];
};
export type RepetitionSummary = {
scores: any;
feedback: string;
fps: number;
count?: number;
};
export class RepetitionCounter {
// Buffers (Max len 15)
private elbow_hist_r: number[] = [];
private elbow_hist_l: number[] = [];
private shoulder_hist_r: number[] = [];
private shoulder_hist_l: number[] = [];
// State
public current_exercise: string = "unknown";
public stage_right: string | null = null;
public stage_left: string | null = null;
// Phase State Machine
private right_phase: "idle" | "down_prep" | "up" | "done" = "idle";
private left_phase: "idle" | "down_prep" | "up" | "done" = "idle";
private hull_phase: "idle" | "down" | "up" = "idle";
// Raw Reps
private raw_reps: { [key: string]: number } = {
"hammer_curl": 0,
"overhead_press": 0
};
private raw_right_phase: "idle" | "down_prep" | "up" | "done" = "idle";
private raw_left_phase: "idle" | "down_prep" | "up" | "done" = "idle";
// Debounce / Bad Frame Tolerance
private bad_frame_count_r = 0;
private bad_frame_count_l = 0;
// Data Collection
private rep_data: RepData;
public all_scores: any[] = [];
public last_score: any = {};
constructor() {
this.rep_data = this._reset_rep_data();
}
private _reset_rep_data(): RepData {
return {
elbow_r: { up: [], down: [] }, elbow_l: { up: [], down: [] },
shoulder_r: { up: [], down: [] }, shoulder_l: { up: [], down: [] },
hull_area: { up: [], down: [] },
wrist_dist: [],
static_angles: {
knee_r: [], knee_l: [],
hip_r: [], hip_l: [],
shoulder_r: [], shoulder_l: []
},
feedback: [],
frame_times: []
};
}
private updateBuffer(buffer: number[], val: number) {
buffer.push(val);
if (buffer.length > 15) buffer.shift();
}
public update_angles(elbow_r: number, elbow_l: number, shoulder_r: number, shoulder_l: number) {
this.updateBuffer(this.elbow_hist_r, elbow_r);
this.updateBuffer(this.shoulder_hist_r, shoulder_r);
this.updateBuffer(this.elbow_hist_l, elbow_l);
this.updateBuffer(this.shoulder_hist_l, shoulder_l);
}
public get_raw_reps(exercise_name: string): number {
return this.raw_reps[exercise_name] || 0;
}
// --- Scoring Logic (Ported) ---
private _calculate_dynamic_angle_score(
dynamic_thresholds: any,
relevant_joints: string[],
buffer: number
): { [key: string]: number } {
const joint_scores: { [key: string]: number } = {};
for (const joint of relevant_joints) {
const joint_base = joint.split('_')[0]; // e.g., elbow_r -> elbow
for (const stage of ['up', 'down'] as const) {
// Access rep_data dynamically.
// Note: Typescript dynamic access requires careful typing or casting.
const user_angles = (this.rep_data as any)[joint]?.[stage] as number[];
const score_key = `${joint}_${stage}`;
if (user_angles && user_angles.length > 0) {
const user_min = Math.min(...user_angles);
const user_max = Math.max(...user_angles);
const ref_key = `${joint_base}_${stage}`; // e.g. elbow_up
const ref_range = dynamic_thresholds[ref_key] || [0, 0];
const [ref_min, ref_max] = ref_range;
const out_low = Math.max(0, ref_min - user_min);
const out_high = Math.max(0, user_max - ref_max);
const pen_low = Math.max(0, out_low - buffer);
const pen_high = Math.max(0, out_high - buffer);
const total_penalty = pen_low + pen_high;
const user_length = user_max - user_min;
let score = 0.0;
if (user_min >= ref_min && user_max <= ref_max) {
score = 1.0;
} else if (user_length > 0) {
score = Math.max(0, (user_length - total_penalty) / user_length);
}
joint_scores[score_key] = score * 100;
}
}
}
return joint_scores;
}
private calculate_repetition_score(
config: any,
dynamic_thresholds: any,
current_exercise: string
): any {
const gender_thresholds = config; // Assuming full config passed
const global_config = { static_angle_tolerance: 12, dynamic_angle_buffer: 10 }; // Defaults
// 1. Convex Hull
const hull_scores: number[] = [];
for (const stage of ['up', 'down'] as const) {
const user_vals = this.rep_data.hull_area[stage];
if (user_vals && user_vals.length > 0) {
const user_range: [number, number] = [Math.min(...user_vals), Math.max(...user_vals)];
const ref_range = gender_thresholds.convex_hull?.[stage];
if (ref_range) {
hull_scores.push(calculateContainmentScore(user_range, ref_range));
}
}
}
const avg_hull_score = hull_scores.length > 0
? hull_scores.reduce((a, b) => a + b, 0) / hull_scores.length
: 0;
// 2. Dynamic Angles
const exercise_joints_map: {[key: string]: string[]} = {
"hammer_curl": ["elbow_r", "elbow_l"],
"overhead_press": ["elbow_r", "elbow_l", "shoulder_r", "shoulder_l"]
};
const relevant = exercise_joints_map[current_exercise] || [];
const dynamic_scores = this._calculate_dynamic_angle_score(
dynamic_thresholds,
relevant,
global_config.dynamic_angle_buffer
);
// 3. Static Angles
const static_scores: {[key: string]: number} = {};
const ref_static = gender_thresholds.static_angles || {};
for (const [joint, ref_val] of Object.entries(ref_static)) {
const user_vals = this.rep_data.static_angles[joint];
if (user_vals && user_vals.length > 0) {
const user_range: [number, number] = [Math.min(...user_vals), Math.max(...user_vals)];
const tolerance = global_config.static_angle_tolerance;
// ref_val is number, need range [val, val+tol]
const r_val = ref_val as number;
static_scores[joint] = calculateContainmentScore(user_range, [r_val, r_val + tolerance]) * 100;
}
}
// 4. Wrist Distance
let wrist_score = 0;
if (this.rep_data.wrist_dist.length > 0) {
const user_vals = this.rep_data.wrist_dist;
const user_range: [number, number] = [Math.min(...user_vals), Math.max(...user_vals)];
const ref_range = gender_thresholds.wrist_distance;
if (ref_range) {
wrist_score = calculateContainmentScore(user_range, ref_range);
}
}
return {
"Hull Score": avg_hull_score * 100,
"Dynamic Angle Score": dynamic_scores,
"Static Angle Score": static_scores,
"Wrist Distance Score": wrist_score * 100
};
}
// --- Main Logic ---
public count_repetitions(
angles: AnglesDict,
wrist_dist: number,
hull_area: number,
exercise_config: any,
frame_time: number
): [string | null, string | null, boolean, RepetitionSummary] {
let completed = false;
let rep_summary: RepetitionSummary = { scores: {}, feedback: '', fps: 0 };
const thresholds = exercise_config;
const dynamic_thresholds = thresholds.dynamic_angles;
const phase_type = thresholds.phase_type || 'start_down'; // Default to curl behavior
if (!dynamic_thresholds) return [this.stage_right, this.stage_left, false, rep_summary];
// 1. Detect Stages for EACH joint involved
// Helper to detect generic stage for a value against a config key
const detect_raw = (val: number, key: string) => {
// key e.g., 'elbow' -> checks 'elbow_up' and 'elbow_down'
const up = dynamic_thresholds[`${key}_up`];
const down = dynamic_thresholds[`${key}_down`];
if (down && inRange(val, down[0], down[1])) return "down";
if (up && inRange(val, up[0], up[1])) return "up";
return null;
};
const detect_joint_stage = (joint_prefix: string, side_suffix: string) => {
// e.g. joint_prefix='elbow', side_suffix='_r' -> angle 'elbow_r'
// check thresholds 'elbow_up', 'elbow_down'
const angle = angles[`${joint_prefix}${side_suffix}`];
if (angle === undefined) return null;
return detect_raw(angle, joint_prefix);
};
// Determine relevant joints from the config keys
// e.g. keys: 'elbow_up', 'shoulder_down' -> joints: ['elbow', 'shoulder']
const keys = Object.keys(dynamic_thresholds);
const joint_prefixes = Array.from(new Set(keys.map(k => k.split('_')[0])));
// Determine Composite Stage for Right and Left
// Logic: All relevant joints must match the target stage to trigger that stage?
// OR: At least one matches?
// Python logic for HC: "up" if elbow OR shoulder is up. "down" if elbow AND shoulder is down.
// Python logic for OV: "up" if shoulder IS up AND matches elbow.
// Revised Generic Logic:
// "UP" = Primary Mover is UP.
// "DOWN" = Primary Mover is DOWN.
// We will define specific "Primary Joints" logic or simple dominant logic.
// For simplicity and robustness across these 7 exercises:
// - Bicep/Hammer: Elbow is primary.
// - Press/Raise: Shoulder & Elbow.
// - Squat/Dead/Lunge: Hip & Knee.
// Let's iterate all tracked joints.
// If ANY tracked joint is "up", we lean towards "up".
// If ALL tracked joints are "down", we are "down".
// (This matches the loose Python logic for 'up' and strict for 'down' in Curls)
const get_side_stage = (suffix: string) => {
const stages = joint_prefixes.map(j => detect_joint_stage(j, suffix));
// Special overrides based on exercise type if needed, but trying to be generic:
if (phase_type === 'start_down') {
// E.g. Curl: Start Down.
// Up if ANY is Up (e.g. slight shoulder raise + full curl = Up)
// Down if ALL are Down (Full extension)
if (stages.some(s => s === 'up')) return 'up';
if (stages.every(s => s === 'down')) return 'down';
} else {
// E.g. Squat: Start Up (Standing).
// Down (Squatting) if ANY is Down (e.g. hip OR knee flexes deep) -> Actually usually both flex.
// Up (Standing) if ALL are Up (Full extension)
// Let's invert:
if (stages.some(s => s === 'down')) return 'down'; // Dipping
if (stages.every(s => s === 'up')) return 'up'; // Standing tall
}
return null;
};
let stage_r = get_side_stage('_r');
let stage_l = get_side_stage('_l');
// Convex Hull Stage (Override/Filter)
const ch = thresholds.convex_hull || {};
const ch_up = ch.up;
const ch_down = ch.down;
if (inRange(hull_area, ch_down?.[0] || 0, ch_down?.[1] || 0)) this.hull_phase = "down";
else if (this.hull_phase === "down" && inRange(hull_area, ch_up?.[0] || 0, ch_up?.[1] || 999)) this.hull_phase = "up";
// Data Collection
const current_stage = this.stage_right || this.stage_left;
if (current_stage && (current_stage === 'up' || current_stage === 'down')) {
// ... (Data collection remains same) ...
this.rep_data.elbow_r[current_stage].push(angles['elbow_r']);
this.rep_data.elbow_l[current_stage].push(angles['elbow_l']);
this.rep_data.shoulder_r[current_stage].push(angles['shoulder_r']);
this.rep_data.shoulder_l[current_stage].push(angles['shoulder_l']);
this.rep_data.hull_area[current_stage].push(hull_area);
this.rep_data.wrist_dist.push(wrist_dist);
this.rep_data.frame_times.push(frame_time);
const ref_static = thresholds.static_angles || {};
for (const joint of Object.keys(ref_static)) {
if (this.rep_data.static_angles[joint]) {
this.rep_data.static_angles[joint].push(angles[joint] || 0);
}
}
}
// Debounce / Smoothing
const bad_limit = 5;
if (stage_r) { this.stage_right = stage_r; this.bad_frame_count_r = 0; }
else { this.bad_frame_count_r++; if (this.bad_frame_count_r >= bad_limit) this.stage_right = null; }
if (stage_l) { this.stage_left = stage_l; this.bad_frame_count_l = 0; }
else { this.bad_frame_count_l++; if (this.bad_frame_count_l >= bad_limit) this.stage_left = null; }
// --- State Machine (Generic) ---
// We track "Right" and "Left" independently for completion, but increment one counter.
// Logic: Both sides must complete the rep cycle? Or just one?
// Python logic: "if self.raw_right_phase == 'done' and self.raw_left_phase == 'done'" -> REQUIRES BOTH.
const update_phase = (current_phase: string, stage: string | null) => {
if (phase_type === 'start_down') {
// Idle -> Down (Start) -> Up (Peak) -> Down (Done)
// Note: "Down" is the REST state. "Up" is the ACTIVE state.
// If we are IDLE, we wait for DOWN (Prep).
// Actually, usually you start "Down".
if (current_phase === 'idle' && stage === 'down') return 'down_prep';
if (current_phase === 'down_prep' && stage === 'up') return 'up';
if (current_phase === 'up' && stage === 'down') return 'done';
} else {
// Start Up (Squat).
// Idle -> Up (Start) -> Down (Peak) -> Up (Done).
if (current_phase === 'idle' && stage === 'up') return 'up_prep';
if (current_phase === 'up_prep' && stage === 'down') return 'down'; // Peak
if (current_phase === 'down' && stage === 'up') return 'done';
}
return current_phase;
};
this.raw_right_phase = update_phase(this.raw_right_phase, this.stage_right) as any;
this.raw_left_phase = update_phase(this.raw_left_phase, this.stage_left) as any;
// Completion Check
// If bilateral (trackBothSides), wait for both.
// We will assume bilateral for now as Python did.
if (this.raw_right_phase === "done" && this.raw_left_phase === "done") {
this.raw_reps[this.current_exercise] = (this.raw_reps[this.current_exercise] || 0) + 1;
completed = true;
this.last_score = this.calculate_repetition_score(thresholds, dynamic_thresholds, this.current_exercise);
// FPS
const fps = this.rep_data.frame_times.length > 0
? 1000 / (this.rep_data.frame_times.reduce((a,b)=>a+b,0) / this.rep_data.frame_times.length || 1)
: 0;
this.all_scores.push({ exercise: this.current_exercise, scores: this.last_score });
rep_summary = { scores: this.last_score, feedback: "Rep Completed", fps: fps, count: this.raw_reps[this.current_exercise] };
this.rep_data = this._reset_rep_data();
this.raw_right_phase = "idle";
this.raw_left_phase = "idle";
}
return [this.stage_right, this.stage_left, completed, rep_summary];
}
}

View File

@ -0,0 +1,163 @@
import modelData from '@/public/models/xgb_activity_model.json';
type TreeNode = {
split_indices?: number;
split_conditions?: number;
yes?: number;
no?: number;
missing?: number;
split_type?: number;
leaf?: number;
children?: TreeNode[];
};
// The raw JSON structure from XGBoost dump
interface XGBModelDump {
learner: {
gradient_booster: {
model: {
trees: Array<{
base_weights: number[];
default_left: number[];
id: number;
left_children: number[];
loss_changes: number[];
parents: number[];
right_children: number[];
split_conditions: number[];
split_indices: number[];
split_type: number[];
sum_hessian: number[];
tree_param: {
num_deleted: string;
num_feature: string;
num_nodes: string;
size_leaf_vector: string;
};
}>;
gbtree_model_param: {
num_parallel_tree: string;
num_trees: string;
};
};
};
learner_model_param: {
base_score: string; // e.g. "[0.2, 0.3, 0.5]" or single float
num_class: string;
}
};
}
export class XGBoostPredictor {
private model: XGBModelDump;
private numTrees: number;
private numClass: number;
private baseScores: number[];
constructor() {
this.model = modelData as unknown as XGBModelDump;
this.numTrees = parseInt(this.model.learner.gradient_booster.model.gbtree_model_param.num_trees);
this.numClass = parseInt(this.model.learner.learner_model_param.num_class);
// Parse base score (often represented as an array string or a single float string)
const baseScoreRaw = this.model.learner.learner_model_param.base_score;
if (baseScoreRaw.startsWith('[')) {
try {
this.baseScores = JSON.parse(baseScoreRaw);
} catch (e) {
// Fallback manually parsing if JSON.parse fails on some formats
console.error("Error parsing base_score", e);
this.baseScores = Array(this.numClass).fill(0.5);
}
} else {
this.baseScores = Array(this.numClass).fill(parseFloat(baseScoreRaw));
}
}
public predict(features: number[]): number[] {
// Initialize scores with base_margin (inverse link of base_score usually, but for XGBoost multi-class
// with 'multi:softprob', it usually starts at 0.5 before the tree sums if using raw margin,
// but let's assume we sum the raw tree outputs).
// Actually, XGBoost stores the raw margins.
const rawScores = new Array(this.numClass).fill(0.5);
// NOTE: In strict XGBoost implementation, the initial prediction is 0.5 (logit)
// if base_score is 0.5. For accurate results, we should check `base_score` parameter.
// If base_scores are provided, we should convert them to margins if boosting starts from them.
// Usually, sum = base_margin + sum(tree_outputs)
// Convert base scores to margins (logit)
// margin = ln(p / (1-p)) is for binary. For multiclass, it's more complex.
// Let's rely on standard additive behavior: rawScores starts at 0?
// Or starts at the initial margin.
// Let's use 0.0 effectively and rely on Trees
// (This might require tuning, but standard dump execution typically sums weights)
const treeScores = new Array(this.numClass).fill(0);
const trees = this.model.learner.gradient_booster.model.trees;
for (let i = 0; i < this.numTrees; i++) {
const tree = trees[i];
const classIdx = i % this.numClass; // Trees are interleaved for classes 0, 1, 2, 0, 1, 2...
let nodeId = 0; // Start at root
// Traverse
while (true) {
// Check if leaf
// In this JSON format, children arrays contain -1 for no child.
// But we must check if the current node is a split or leaf.
// The arrays (split_indices, etc.) are indexed by node ID.
// Wait, the JSON format provided is aggressive: "left_children", "right_children" are arrays.
const leftChild = tree.left_children[nodeId];
const rightChild = tree.right_children[nodeId];
// If leaf, left child is usually -1 (or similar indicator)
// However, look at the values.
// If index is valid split, proceed.
if (leftChild === -1 && rightChild === -1) {
// Leaf node
// Weight is in base_weights[nodeId]
treeScores[classIdx] += tree.base_weights[nodeId];
break;
}
// Split
const featureIdx = tree.split_indices[nodeId];
const threshold = tree.split_conditions[nodeId];
const defaultLeft = tree.default_left[nodeId] === 1;
const featureVal = features[featureIdx];
// Missing value handling (if feature is NaN, go default)
if (featureVal === undefined || isNaN(featureVal)) {
nodeId = defaultLeft ? leftChild : rightChild;
} else {
if (featureVal < threshold) {
nodeId = leftChild;
} else {
nodeId = rightChild;
}
}
}
}
// Softmax
// First add base margin?
// For 'multi:softprob', output is softmax(raw_score + base_margin)
// If base_score=[0.5, 0.5, 0.5], base_margin ~ 0.
return this.softmax(treeScores);
}
private softmax(logits: number[]): number[] {
const maxLogit = Math.max(...logits);
const scores = logits.map(l => Math.exp(l - maxLogit));
const sumScores = scores.reduce((a, b) => a + b, 0);
return scores.map(s => s / sumScores);
}
}

34
lib/prisma-gen/browser.ts Normal file
View File

@ -0,0 +1,34 @@
/* !!! This is code generated by Prisma. Do not edit directly. !!! */
/* eslint-disable */
// biome-ignore-all lint: generated file
// @ts-nocheck
/*
* This file should be your main import to use Prisma-related types and utilities in a browser.
* Use it to get access to models, enums, and input types.
*
* This file does not contain a `PrismaClient` class, nor several other helpers that are intended as server-side only.
* See `client.ts` for the standard, server-side entry point.
*
* 🟢 You can import this file directly.
*/
import * as Prisma from './internal/prismaNamespaceBrowser.js'
export { Prisma }
export * as $Enums from './enums.js'
export * from './enums.js';
/**
* Model activity_logs
*
*/
export type activity_logs = Prisma.activity_logsModel
/**
* Model training_menus
*
*/
export type training_menus = Prisma.training_menusModel
/**
* Model user_recaps
*
*/
export type user_recaps = Prisma.user_recapsModel

56
lib/prisma-gen/client.ts Normal file
View File

@ -0,0 +1,56 @@
/* !!! This is code generated by Prisma. Do not edit directly. !!! */
/* eslint-disable */
// biome-ignore-all lint: generated file
// @ts-nocheck
/*
* This file should be your main import to use Prisma. Through it you get access to all the models, enums, and input types.
* If you're looking for something you can import in the client-side of your application, please refer to the `browser.ts` file instead.
*
* 🟢 You can import this file directly.
*/
import * as process from 'node:process'
import * as path from 'node:path'
import { fileURLToPath } from 'node:url'
globalThis['__dirname'] = path.dirname(fileURLToPath(import.meta.url))
import * as runtime from "@prisma/client/runtime/client"
import * as $Enums from "./enums.js"
import * as $Class from "./internal/class.js"
import * as Prisma from "./internal/prismaNamespace.js"
export * as $Enums from './enums.js'
export * from "./enums.js"
/**
* ## Prisma Client
*
* Type-safe database client for TypeScript
* @example
* ```
* const prisma = new PrismaClient()
* // Fetch zero or more Activity_logs
* const activity_logs = await prisma.activity_logs.findMany()
* ```
*
* Read more in our [docs](https://pris.ly/d/client).
*/
export const PrismaClient = $Class.getPrismaClientClass()
export type PrismaClient<LogOpts extends Prisma.LogLevel = never, OmitOpts extends Prisma.PrismaClientOptions["omit"] = Prisma.PrismaClientOptions["omit"], ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = $Class.PrismaClient<LogOpts, OmitOpts, ExtArgs>
export { Prisma }
/**
* Model activity_logs
*
*/
export type activity_logs = Prisma.activity_logsModel
/**
* Model training_menus
*
*/
export type training_menus = Prisma.training_menusModel
/**
* Model user_recaps
*
*/
export type user_recaps = Prisma.user_recapsModel

View File

@ -0,0 +1,341 @@
/* !!! This is code generated by Prisma. Do not edit directly. !!! */
/* eslint-disable */
// biome-ignore-all lint: generated file
// @ts-nocheck
/*
* This file exports various common sort, input & filter types that are not directly linked to a particular model.
*
* 🟢 You can import this file directly.
*/
import type * as runtime from "@prisma/client/runtime/client"
import * as $Enums from "./enums.js"
import type * as Prisma from "./internal/prismaNamespace.js"
export type IntFilter<$PrismaModel = never> = {
equals?: number | Prisma.IntFieldRefInput<$PrismaModel>
in?: number[] | Prisma.ListIntFieldRefInput<$PrismaModel>
notIn?: number[] | Prisma.ListIntFieldRefInput<$PrismaModel>
lt?: number | Prisma.IntFieldRefInput<$PrismaModel>
lte?: number | Prisma.IntFieldRefInput<$PrismaModel>
gt?: number | Prisma.IntFieldRefInput<$PrismaModel>
gte?: number | Prisma.IntFieldRefInput<$PrismaModel>
not?: Prisma.NestedIntFilter<$PrismaModel> | number
}
export type DateTimeNullableFilter<$PrismaModel = never> = {
equals?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel> | null
in?: Date[] | string[] | Prisma.ListDateTimeFieldRefInput<$PrismaModel> | null
notIn?: Date[] | string[] | Prisma.ListDateTimeFieldRefInput<$PrismaModel> | null
lt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
lte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
gt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
gte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
not?: Prisma.NestedDateTimeNullableFilter<$PrismaModel> | Date | string | null
}
export type StringNullableFilter<$PrismaModel = never> = {
equals?: string | Prisma.StringFieldRefInput<$PrismaModel> | null
in?: string[] | Prisma.ListStringFieldRefInput<$PrismaModel> | null
notIn?: string[] | Prisma.ListStringFieldRefInput<$PrismaModel> | null
lt?: string | Prisma.StringFieldRefInput<$PrismaModel>
lte?: string | Prisma.StringFieldRefInput<$PrismaModel>
gt?: string | Prisma.StringFieldRefInput<$PrismaModel>
gte?: string | Prisma.StringFieldRefInput<$PrismaModel>
contains?: string | Prisma.StringFieldRefInput<$PrismaModel>
startsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>
endsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>
mode?: Prisma.QueryMode
not?: Prisma.NestedStringNullableFilter<$PrismaModel> | string | null
}
export type JsonNullableFilter<$PrismaModel = never> =
| Prisma.PatchUndefined<
Prisma.Either<Required<JsonNullableFilterBase<$PrismaModel>>, Exclude<keyof Required<JsonNullableFilterBase<$PrismaModel>>, 'path'>>,
Required<JsonNullableFilterBase<$PrismaModel>>
>
| Prisma.OptionalFlat<Omit<Required<JsonNullableFilterBase<$PrismaModel>>, 'path'>>
export type JsonNullableFilterBase<$PrismaModel = never> = {
equals?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel> | Prisma.JsonNullValueFilter
path?: string[]
mode?: Prisma.QueryMode | Prisma.EnumQueryModeFieldRefInput<$PrismaModel>
string_contains?: string | Prisma.StringFieldRefInput<$PrismaModel>
string_starts_with?: string | Prisma.StringFieldRefInput<$PrismaModel>
string_ends_with?: string | Prisma.StringFieldRefInput<$PrismaModel>
array_starts_with?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel> | null
array_ends_with?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel> | null
array_contains?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel> | null
lt?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel>
lte?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel>
gt?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel>
gte?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel>
not?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel> | Prisma.JsonNullValueFilter
}
export type SortOrderInput = {
sort: Prisma.SortOrder
nulls?: Prisma.NullsOrder
}
export type IntWithAggregatesFilter<$PrismaModel = never> = {
equals?: number | Prisma.IntFieldRefInput<$PrismaModel>
in?: number[] | Prisma.ListIntFieldRefInput<$PrismaModel>
notIn?: number[] | Prisma.ListIntFieldRefInput<$PrismaModel>
lt?: number | Prisma.IntFieldRefInput<$PrismaModel>
lte?: number | Prisma.IntFieldRefInput<$PrismaModel>
gt?: number | Prisma.IntFieldRefInput<$PrismaModel>
gte?: number | Prisma.IntFieldRefInput<$PrismaModel>
not?: Prisma.NestedIntWithAggregatesFilter<$PrismaModel> | number
_count?: Prisma.NestedIntFilter<$PrismaModel>
_avg?: Prisma.NestedFloatFilter<$PrismaModel>
_sum?: Prisma.NestedIntFilter<$PrismaModel>
_min?: Prisma.NestedIntFilter<$PrismaModel>
_max?: Prisma.NestedIntFilter<$PrismaModel>
}
export type DateTimeNullableWithAggregatesFilter<$PrismaModel = never> = {
equals?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel> | null
in?: Date[] | string[] | Prisma.ListDateTimeFieldRefInput<$PrismaModel> | null
notIn?: Date[] | string[] | Prisma.ListDateTimeFieldRefInput<$PrismaModel> | null
lt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
lte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
gt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
gte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
not?: Prisma.NestedDateTimeNullableWithAggregatesFilter<$PrismaModel> | Date | string | null
_count?: Prisma.NestedIntNullableFilter<$PrismaModel>
_min?: Prisma.NestedDateTimeNullableFilter<$PrismaModel>
_max?: Prisma.NestedDateTimeNullableFilter<$PrismaModel>
}
export type StringNullableWithAggregatesFilter<$PrismaModel = never> = {
equals?: string | Prisma.StringFieldRefInput<$PrismaModel> | null
in?: string[] | Prisma.ListStringFieldRefInput<$PrismaModel> | null
notIn?: string[] | Prisma.ListStringFieldRefInput<$PrismaModel> | null
lt?: string | Prisma.StringFieldRefInput<$PrismaModel>
lte?: string | Prisma.StringFieldRefInput<$PrismaModel>
gt?: string | Prisma.StringFieldRefInput<$PrismaModel>
gte?: string | Prisma.StringFieldRefInput<$PrismaModel>
contains?: string | Prisma.StringFieldRefInput<$PrismaModel>
startsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>
endsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>
mode?: Prisma.QueryMode
not?: Prisma.NestedStringNullableWithAggregatesFilter<$PrismaModel> | string | null
_count?: Prisma.NestedIntNullableFilter<$PrismaModel>
_min?: Prisma.NestedStringNullableFilter<$PrismaModel>
_max?: Prisma.NestedStringNullableFilter<$PrismaModel>
}
export type JsonNullableWithAggregatesFilter<$PrismaModel = never> =
| Prisma.PatchUndefined<
Prisma.Either<Required<JsonNullableWithAggregatesFilterBase<$PrismaModel>>, Exclude<keyof Required<JsonNullableWithAggregatesFilterBase<$PrismaModel>>, 'path'>>,
Required<JsonNullableWithAggregatesFilterBase<$PrismaModel>>
>
| Prisma.OptionalFlat<Omit<Required<JsonNullableWithAggregatesFilterBase<$PrismaModel>>, 'path'>>
export type JsonNullableWithAggregatesFilterBase<$PrismaModel = never> = {
equals?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel> | Prisma.JsonNullValueFilter
path?: string[]
mode?: Prisma.QueryMode | Prisma.EnumQueryModeFieldRefInput<$PrismaModel>
string_contains?: string | Prisma.StringFieldRefInput<$PrismaModel>
string_starts_with?: string | Prisma.StringFieldRefInput<$PrismaModel>
string_ends_with?: string | Prisma.StringFieldRefInput<$PrismaModel>
array_starts_with?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel> | null
array_ends_with?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel> | null
array_contains?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel> | null
lt?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel>
lte?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel>
gt?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel>
gte?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel>
not?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel> | Prisma.JsonNullValueFilter
_count?: Prisma.NestedIntNullableFilter<$PrismaModel>
_min?: Prisma.NestedJsonNullableFilter<$PrismaModel>
_max?: Prisma.NestedJsonNullableFilter<$PrismaModel>
}
export type IntNullableFilter<$PrismaModel = never> = {
equals?: number | Prisma.IntFieldRefInput<$PrismaModel> | null
in?: number[] | Prisma.ListIntFieldRefInput<$PrismaModel> | null
notIn?: number[] | Prisma.ListIntFieldRefInput<$PrismaModel> | null
lt?: number | Prisma.IntFieldRefInput<$PrismaModel>
lte?: number | Prisma.IntFieldRefInput<$PrismaModel>
gt?: number | Prisma.IntFieldRefInput<$PrismaModel>
gte?: number | Prisma.IntFieldRefInput<$PrismaModel>
not?: Prisma.NestedIntNullableFilter<$PrismaModel> | number | null
}
export type IntNullableWithAggregatesFilter<$PrismaModel = never> = {
equals?: number | Prisma.IntFieldRefInput<$PrismaModel> | null
in?: number[] | Prisma.ListIntFieldRefInput<$PrismaModel> | null
notIn?: number[] | Prisma.ListIntFieldRefInput<$PrismaModel> | null
lt?: number | Prisma.IntFieldRefInput<$PrismaModel>
lte?: number | Prisma.IntFieldRefInput<$PrismaModel>
gt?: number | Prisma.IntFieldRefInput<$PrismaModel>
gte?: number | Prisma.IntFieldRefInput<$PrismaModel>
not?: Prisma.NestedIntNullableWithAggregatesFilter<$PrismaModel> | number | null
_count?: Prisma.NestedIntNullableFilter<$PrismaModel>
_avg?: Prisma.NestedFloatNullableFilter<$PrismaModel>
_sum?: Prisma.NestedIntNullableFilter<$PrismaModel>
_min?: Prisma.NestedIntNullableFilter<$PrismaModel>
_max?: Prisma.NestedIntNullableFilter<$PrismaModel>
}
export type NestedIntFilter<$PrismaModel = never> = {
equals?: number | Prisma.IntFieldRefInput<$PrismaModel>
in?: number[] | Prisma.ListIntFieldRefInput<$PrismaModel>
notIn?: number[] | Prisma.ListIntFieldRefInput<$PrismaModel>
lt?: number | Prisma.IntFieldRefInput<$PrismaModel>
lte?: number | Prisma.IntFieldRefInput<$PrismaModel>
gt?: number | Prisma.IntFieldRefInput<$PrismaModel>
gte?: number | Prisma.IntFieldRefInput<$PrismaModel>
not?: Prisma.NestedIntFilter<$PrismaModel> | number
}
export type NestedDateTimeNullableFilter<$PrismaModel = never> = {
equals?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel> | null
in?: Date[] | string[] | Prisma.ListDateTimeFieldRefInput<$PrismaModel> | null
notIn?: Date[] | string[] | Prisma.ListDateTimeFieldRefInput<$PrismaModel> | null
lt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
lte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
gt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
gte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
not?: Prisma.NestedDateTimeNullableFilter<$PrismaModel> | Date | string | null
}
export type NestedStringNullableFilter<$PrismaModel = never> = {
equals?: string | Prisma.StringFieldRefInput<$PrismaModel> | null
in?: string[] | Prisma.ListStringFieldRefInput<$PrismaModel> | null
notIn?: string[] | Prisma.ListStringFieldRefInput<$PrismaModel> | null
lt?: string | Prisma.StringFieldRefInput<$PrismaModel>
lte?: string | Prisma.StringFieldRefInput<$PrismaModel>
gt?: string | Prisma.StringFieldRefInput<$PrismaModel>
gte?: string | Prisma.StringFieldRefInput<$PrismaModel>
contains?: string | Prisma.StringFieldRefInput<$PrismaModel>
startsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>
endsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>
not?: Prisma.NestedStringNullableFilter<$PrismaModel> | string | null
}
export type NestedIntWithAggregatesFilter<$PrismaModel = never> = {
equals?: number | Prisma.IntFieldRefInput<$PrismaModel>
in?: number[] | Prisma.ListIntFieldRefInput<$PrismaModel>
notIn?: number[] | Prisma.ListIntFieldRefInput<$PrismaModel>
lt?: number | Prisma.IntFieldRefInput<$PrismaModel>
lte?: number | Prisma.IntFieldRefInput<$PrismaModel>
gt?: number | Prisma.IntFieldRefInput<$PrismaModel>
gte?: number | Prisma.IntFieldRefInput<$PrismaModel>
not?: Prisma.NestedIntWithAggregatesFilter<$PrismaModel> | number
_count?: Prisma.NestedIntFilter<$PrismaModel>
_avg?: Prisma.NestedFloatFilter<$PrismaModel>
_sum?: Prisma.NestedIntFilter<$PrismaModel>
_min?: Prisma.NestedIntFilter<$PrismaModel>
_max?: Prisma.NestedIntFilter<$PrismaModel>
}
export type NestedFloatFilter<$PrismaModel = never> = {
equals?: number | Prisma.FloatFieldRefInput<$PrismaModel>
in?: number[] | Prisma.ListFloatFieldRefInput<$PrismaModel>
notIn?: number[] | Prisma.ListFloatFieldRefInput<$PrismaModel>
lt?: number | Prisma.FloatFieldRefInput<$PrismaModel>
lte?: number | Prisma.FloatFieldRefInput<$PrismaModel>
gt?: number | Prisma.FloatFieldRefInput<$PrismaModel>
gte?: number | Prisma.FloatFieldRefInput<$PrismaModel>
not?: Prisma.NestedFloatFilter<$PrismaModel> | number
}
export type NestedDateTimeNullableWithAggregatesFilter<$PrismaModel = never> = {
equals?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel> | null
in?: Date[] | string[] | Prisma.ListDateTimeFieldRefInput<$PrismaModel> | null
notIn?: Date[] | string[] | Prisma.ListDateTimeFieldRefInput<$PrismaModel> | null
lt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
lte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
gt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
gte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
not?: Prisma.NestedDateTimeNullableWithAggregatesFilter<$PrismaModel> | Date | string | null
_count?: Prisma.NestedIntNullableFilter<$PrismaModel>
_min?: Prisma.NestedDateTimeNullableFilter<$PrismaModel>
_max?: Prisma.NestedDateTimeNullableFilter<$PrismaModel>
}
export type NestedIntNullableFilter<$PrismaModel = never> = {
equals?: number | Prisma.IntFieldRefInput<$PrismaModel> | null
in?: number[] | Prisma.ListIntFieldRefInput<$PrismaModel> | null
notIn?: number[] | Prisma.ListIntFieldRefInput<$PrismaModel> | null
lt?: number | Prisma.IntFieldRefInput<$PrismaModel>
lte?: number | Prisma.IntFieldRefInput<$PrismaModel>
gt?: number | Prisma.IntFieldRefInput<$PrismaModel>
gte?: number | Prisma.IntFieldRefInput<$PrismaModel>
not?: Prisma.NestedIntNullableFilter<$PrismaModel> | number | null
}
export type NestedStringNullableWithAggregatesFilter<$PrismaModel = never> = {
equals?: string | Prisma.StringFieldRefInput<$PrismaModel> | null
in?: string[] | Prisma.ListStringFieldRefInput<$PrismaModel> | null
notIn?: string[] | Prisma.ListStringFieldRefInput<$PrismaModel> | null
lt?: string | Prisma.StringFieldRefInput<$PrismaModel>
lte?: string | Prisma.StringFieldRefInput<$PrismaModel>
gt?: string | Prisma.StringFieldRefInput<$PrismaModel>
gte?: string | Prisma.StringFieldRefInput<$PrismaModel>
contains?: string | Prisma.StringFieldRefInput<$PrismaModel>
startsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>
endsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>
not?: Prisma.NestedStringNullableWithAggregatesFilter<$PrismaModel> | string | null
_count?: Prisma.NestedIntNullableFilter<$PrismaModel>
_min?: Prisma.NestedStringNullableFilter<$PrismaModel>
_max?: Prisma.NestedStringNullableFilter<$PrismaModel>
}
export type NestedJsonNullableFilter<$PrismaModel = never> =
| Prisma.PatchUndefined<
Prisma.Either<Required<NestedJsonNullableFilterBase<$PrismaModel>>, Exclude<keyof Required<NestedJsonNullableFilterBase<$PrismaModel>>, 'path'>>,
Required<NestedJsonNullableFilterBase<$PrismaModel>>
>
| Prisma.OptionalFlat<Omit<Required<NestedJsonNullableFilterBase<$PrismaModel>>, 'path'>>
export type NestedJsonNullableFilterBase<$PrismaModel = never> = {
equals?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel> | Prisma.JsonNullValueFilter
path?: string[]
mode?: Prisma.QueryMode | Prisma.EnumQueryModeFieldRefInput<$PrismaModel>
string_contains?: string | Prisma.StringFieldRefInput<$PrismaModel>
string_starts_with?: string | Prisma.StringFieldRefInput<$PrismaModel>
string_ends_with?: string | Prisma.StringFieldRefInput<$PrismaModel>
array_starts_with?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel> | null
array_ends_with?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel> | null
array_contains?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel> | null
lt?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel>
lte?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel>
gt?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel>
gte?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel>
not?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel> | Prisma.JsonNullValueFilter
}
export type NestedIntNullableWithAggregatesFilter<$PrismaModel = never> = {
equals?: number | Prisma.IntFieldRefInput<$PrismaModel> | null
in?: number[] | Prisma.ListIntFieldRefInput<$PrismaModel> | null
notIn?: number[] | Prisma.ListIntFieldRefInput<$PrismaModel> | null
lt?: number | Prisma.IntFieldRefInput<$PrismaModel>
lte?: number | Prisma.IntFieldRefInput<$PrismaModel>
gt?: number | Prisma.IntFieldRefInput<$PrismaModel>
gte?: number | Prisma.IntFieldRefInput<$PrismaModel>
not?: Prisma.NestedIntNullableWithAggregatesFilter<$PrismaModel> | number | null
_count?: Prisma.NestedIntNullableFilter<$PrismaModel>
_avg?: Prisma.NestedFloatNullableFilter<$PrismaModel>
_sum?: Prisma.NestedIntNullableFilter<$PrismaModel>
_min?: Prisma.NestedIntNullableFilter<$PrismaModel>
_max?: Prisma.NestedIntNullableFilter<$PrismaModel>
}
export type NestedFloatNullableFilter<$PrismaModel = never> = {
equals?: number | Prisma.FloatFieldRefInput<$PrismaModel> | null
in?: number[] | Prisma.ListFloatFieldRefInput<$PrismaModel> | null
notIn?: number[] | Prisma.ListFloatFieldRefInput<$PrismaModel> | null
lt?: number | Prisma.FloatFieldRefInput<$PrismaModel>
lte?: number | Prisma.FloatFieldRefInput<$PrismaModel>
gt?: number | Prisma.FloatFieldRefInput<$PrismaModel>
gte?: number | Prisma.FloatFieldRefInput<$PrismaModel>
not?: Prisma.NestedFloatNullableFilter<$PrismaModel> | number | null
}

15
lib/prisma-gen/enums.ts Normal file
View File

@ -0,0 +1,15 @@
/* !!! This is code generated by Prisma. Do not edit directly. !!! */
/* eslint-disable */
// biome-ignore-all lint: generated file
// @ts-nocheck
/*
* This file exports all enum related types from the schema.
*
* 🟢 You can import this file directly.
*/
// This file is empty because there are no enums in the schema.
export {}

View File

@ -0,0 +1,210 @@
/* !!! This is code generated by Prisma. Do not edit directly. !!! */
/* eslint-disable */
// biome-ignore-all lint: generated file
// @ts-nocheck
/*
* WARNING: This is an internal file that is subject to change!
*
* 🛑 Under no circumstances should you import this file directly! 🛑
*
* Please import the `PrismaClient` class from the `client.ts` file instead.
*/
import * as runtime from "@prisma/client/runtime/client"
import type * as Prisma from "./prismaNamespace.js"
const config: runtime.GetPrismaClientConfig = {
"previewFeatures": [],
"clientVersion": "7.2.0",
"engineVersion": "0c8ef2ce45c83248ab3df073180d5eda9e8be7a3",
"activeProvider": "postgresql",
"inlineSchema": "generator client {\n provider = \"prisma-client\"\n output = \"../lib/prisma-gen\"\n engineType = \"binary\"\n}\n\ndatasource db {\n provider = \"postgresql\"\n}\n\nmodel activity_logs {\n id Int @id @default(autoincrement())\n timestamp DateTime? @db.Timestamp(6)\n status String? @db.VarChar\n confidence String? @db.VarChar\n details Json?\n\n @@index([id], map: \"ix_activity_logs_id\")\n}\n\nmodel training_menus {\n id Int @id @default(autoincrement())\n name String? @db.VarChar\n exercises Json?\n created_at DateTime? @db.Timestamp(6)\n user_recaps user_recaps[]\n\n @@index([id], map: \"ix_training_menus_id\")\n @@index([name], map: \"ix_training_menus_name\")\n}\n\nmodel user_recaps {\n id Int @id @default(autoincrement())\n menu_id Int?\n summary Json?\n completed_at DateTime? @db.Timestamp(6)\n training_menus training_menus? @relation(fields: [menu_id], references: [id], onDelete: NoAction, onUpdate: NoAction)\n\n @@index([id], map: \"ix_user_recaps_id\")\n}\n",
"runtimeDataModel": {
"models": {},
"enums": {},
"types": {}
}
}
config.runtimeDataModel = JSON.parse("{\"models\":{\"activity_logs\":{\"fields\":[{\"name\":\"id\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"timestamp\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"status\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"confidence\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"details\",\"kind\":\"scalar\",\"type\":\"Json\"}],\"dbName\":null},\"training_menus\":{\"fields\":[{\"name\":\"id\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"name\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"exercises\",\"kind\":\"scalar\",\"type\":\"Json\"},{\"name\":\"created_at\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"user_recaps\",\"kind\":\"object\",\"type\":\"user_recaps\",\"relationName\":\"training_menusTouser_recaps\"}],\"dbName\":null},\"user_recaps\":{\"fields\":[{\"name\":\"id\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"menu_id\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"summary\",\"kind\":\"scalar\",\"type\":\"Json\"},{\"name\":\"completed_at\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"training_menus\",\"kind\":\"object\",\"type\":\"training_menus\",\"relationName\":\"training_menusTouser_recaps\"}],\"dbName\":null}},\"enums\":{},\"types\":{}}")
async function decodeBase64AsWasm(wasmBase64: string): Promise<WebAssembly.Module> {
const { Buffer } = await import('node:buffer')
const wasmArray = Buffer.from(wasmBase64, 'base64')
return new WebAssembly.Module(wasmArray)
}
config.compilerWasm = {
getRuntime: async () => await import("@prisma/client/runtime/query_compiler_bg.postgresql.mjs"),
getQueryCompilerWasmModule: async () => {
const { wasm } = await import("@prisma/client/runtime/query_compiler_bg.postgresql.wasm-base64.mjs")
return await decodeBase64AsWasm(wasm)
}
}
export type LogOptions<ClientOptions extends Prisma.PrismaClientOptions> =
'log' extends keyof ClientOptions ? ClientOptions['log'] extends Array<Prisma.LogLevel | Prisma.LogDefinition> ? Prisma.GetEvents<ClientOptions['log']> : never : never
export interface PrismaClientConstructor {
/**
* ## Prisma Client
*
* Type-safe database client for TypeScript
* @example
* ```
* const prisma = new PrismaClient()
* // Fetch zero or more Activity_logs
* const activity_logs = await prisma.activity_logs.findMany()
* ```
*
* Read more in our [docs](https://pris.ly/d/client).
*/
new <
Options extends Prisma.PrismaClientOptions = Prisma.PrismaClientOptions,
LogOpts extends LogOptions<Options> = LogOptions<Options>,
OmitOpts extends Prisma.PrismaClientOptions['omit'] = Options extends { omit: infer U } ? U : Prisma.PrismaClientOptions['omit'],
ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs
>(options: Prisma.Subset<Options, Prisma.PrismaClientOptions> ): PrismaClient<LogOpts, OmitOpts, ExtArgs>
}
/**
* ## Prisma Client
*
* Type-safe database client for TypeScript
* @example
* ```
* const prisma = new PrismaClient()
* // Fetch zero or more Activity_logs
* const activity_logs = await prisma.activity_logs.findMany()
* ```
*
* Read more in our [docs](https://pris.ly/d/client).
*/
export interface PrismaClient<
in LogOpts extends Prisma.LogLevel = never,
in out OmitOpts extends Prisma.PrismaClientOptions['omit'] = undefined,
in out ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs
> {
[K: symbol]: { types: Prisma.TypeMap<ExtArgs>['other'] }
$on<V extends LogOpts>(eventType: V, callback: (event: V extends 'query' ? Prisma.QueryEvent : Prisma.LogEvent) => void): PrismaClient;
/**
* Connect with the database
*/
$connect(): runtime.Types.Utils.JsPromise<void>;
/**
* Disconnect from the database
*/
$disconnect(): runtime.Types.Utils.JsPromise<void>;
/**
* Executes a prepared raw query and returns the number of affected rows.
* @example
* ```
* const result = await prisma.$executeRaw`UPDATE User SET cool = ${true} WHERE email = ${'user@email.com'};`
* ```
*
* Read more in our [docs](https://pris.ly/d/raw-queries).
*/
$executeRaw<T = unknown>(query: TemplateStringsArray | Prisma.Sql, ...values: any[]): Prisma.PrismaPromise<number>;
/**
* Executes a raw query and returns the number of affected rows.
* Susceptible to SQL injections, see documentation.
* @example
* ```
* const result = await prisma.$executeRawUnsafe('UPDATE User SET cool = $1 WHERE email = $2 ;', true, 'user@email.com')
* ```
*
* Read more in our [docs](https://pris.ly/d/raw-queries).
*/
$executeRawUnsafe<T = unknown>(query: string, ...values: any[]): Prisma.PrismaPromise<number>;
/**
* Performs a prepared raw query and returns the `SELECT` data.
* @example
* ```
* const result = await prisma.$queryRaw`SELECT * FROM User WHERE id = ${1} OR email = ${'user@email.com'};`
* ```
*
* Read more in our [docs](https://pris.ly/d/raw-queries).
*/
$queryRaw<T = unknown>(query: TemplateStringsArray | Prisma.Sql, ...values: any[]): Prisma.PrismaPromise<T>;
/**
* Performs a raw query and returns the `SELECT` data.
* Susceptible to SQL injections, see documentation.
* @example
* ```
* const result = await prisma.$queryRawUnsafe('SELECT * FROM User WHERE id = $1 OR email = $2;', 1, 'user@email.com')
* ```
*
* Read more in our [docs](https://pris.ly/d/raw-queries).
*/
$queryRawUnsafe<T = unknown>(query: string, ...values: any[]): Prisma.PrismaPromise<T>;
/**
* Allows the running of a sequence of read/write operations that are guaranteed to either succeed or fail as a whole.
* @example
* ```
* const [george, bob, alice] = await prisma.$transaction([
* prisma.user.create({ data: { name: 'George' } }),
* prisma.user.create({ data: { name: 'Bob' } }),
* prisma.user.create({ data: { name: 'Alice' } }),
* ])
* ```
*
* Read more in our [docs](https://www.prisma.io/docs/concepts/components/prisma-client/transactions).
*/
$transaction<P extends Prisma.PrismaPromise<any>[]>(arg: [...P], options?: { isolationLevel?: Prisma.TransactionIsolationLevel }): runtime.Types.Utils.JsPromise<runtime.Types.Utils.UnwrapTuple<P>>
$transaction<R>(fn: (prisma: Omit<PrismaClient, runtime.ITXClientDenyList>) => runtime.Types.Utils.JsPromise<R>, options?: { maxWait?: number, timeout?: number, isolationLevel?: Prisma.TransactionIsolationLevel }): runtime.Types.Utils.JsPromise<R>
$extends: runtime.Types.Extensions.ExtendsHook<"extends", Prisma.TypeMapCb<OmitOpts>, ExtArgs, runtime.Types.Utils.Call<Prisma.TypeMapCb<OmitOpts>, {
extArgs: ExtArgs
}>>
/**
* `prisma.activity_logs`: Exposes CRUD operations for the **activity_logs** model.
* Example usage:
* ```ts
* // Fetch zero or more Activity_logs
* const activity_logs = await prisma.activity_logs.findMany()
* ```
*/
get activity_logs(): Prisma.activity_logsDelegate<ExtArgs, { omit: OmitOpts }>;
/**
* `prisma.training_menus`: Exposes CRUD operations for the **training_menus** model.
* Example usage:
* ```ts
* // Fetch zero or more Training_menus
* const training_menus = await prisma.training_menus.findMany()
* ```
*/
get training_menus(): Prisma.training_menusDelegate<ExtArgs, { omit: OmitOpts }>;
/**
* `prisma.user_recaps`: Exposes CRUD operations for the **user_recaps** model.
* Example usage:
* ```ts
* // Fetch zero or more User_recaps
* const user_recaps = await prisma.user_recaps.findMany()
* ```
*/
get user_recaps(): Prisma.user_recapsDelegate<ExtArgs, { omit: OmitOpts }>;
}
export function getPrismaClientClass(): PrismaClientConstructor {
return runtime.getPrismaClient(config) as unknown as PrismaClientConstructor
}

View File

@ -0,0 +1,977 @@
/* !!! This is code generated by Prisma. Do not edit directly. !!! */
/* eslint-disable */
// biome-ignore-all lint: generated file
// @ts-nocheck
/*
* WARNING: This is an internal file that is subject to change!
*
* 🛑 Under no circumstances should you import this file directly! 🛑
*
* All exports from this file are wrapped under a `Prisma` namespace object in the client.ts file.
* While this enables partial backward compatibility, it is not part of the stable public API.
*
* If you are looking for your Models, Enums, and Input Types, please import them from the respective
* model files in the `model` directory!
*/
import * as runtime from "@prisma/client/runtime/client"
import type * as Prisma from "../models.js"
import { type PrismaClient } from "./class.js"
export type * from '../models.js'
export type DMMF = typeof runtime.DMMF
export type PrismaPromise<T> = runtime.Types.Public.PrismaPromise<T>
/**
* Prisma Errors
*/
export const PrismaClientKnownRequestError = runtime.PrismaClientKnownRequestError
export type PrismaClientKnownRequestError = runtime.PrismaClientKnownRequestError
export const PrismaClientUnknownRequestError = runtime.PrismaClientUnknownRequestError
export type PrismaClientUnknownRequestError = runtime.PrismaClientUnknownRequestError
export const PrismaClientRustPanicError = runtime.PrismaClientRustPanicError
export type PrismaClientRustPanicError = runtime.PrismaClientRustPanicError
export const PrismaClientInitializationError = runtime.PrismaClientInitializationError
export type PrismaClientInitializationError = runtime.PrismaClientInitializationError
export const PrismaClientValidationError = runtime.PrismaClientValidationError
export type PrismaClientValidationError = runtime.PrismaClientValidationError
/**
* Re-export of sql-template-tag
*/
export const sql = runtime.sqltag
export const empty = runtime.empty
export const join = runtime.join
export const raw = runtime.raw
export const Sql = runtime.Sql
export type Sql = runtime.Sql
/**
* Decimal.js
*/
export const Decimal = runtime.Decimal
export type Decimal = runtime.Decimal
export type DecimalJsLike = runtime.DecimalJsLike
/**
* Extensions
*/
export type Extension = runtime.Types.Extensions.UserArgs
export const getExtensionContext = runtime.Extensions.getExtensionContext
export type Args<T, F extends runtime.Operation> = runtime.Types.Public.Args<T, F>
export type Payload<T, F extends runtime.Operation = never> = runtime.Types.Public.Payload<T, F>
export type Result<T, A, F extends runtime.Operation> = runtime.Types.Public.Result<T, A, F>
export type Exact<A, W> = runtime.Types.Public.Exact<A, W>
export type PrismaVersion = {
client: string
engine: string
}
/**
* Prisma Client JS version: 7.2.0
* Query Engine version: 0c8ef2ce45c83248ab3df073180d5eda9e8be7a3
*/
export const prismaVersion: PrismaVersion = {
client: "7.2.0",
engine: "0c8ef2ce45c83248ab3df073180d5eda9e8be7a3"
}
/**
* Utility Types
*/
export type Bytes = runtime.Bytes
export type JsonObject = runtime.JsonObject
export type JsonArray = runtime.JsonArray
export type JsonValue = runtime.JsonValue
export type InputJsonObject = runtime.InputJsonObject
export type InputJsonArray = runtime.InputJsonArray
export type InputJsonValue = runtime.InputJsonValue
export const NullTypes = {
DbNull: runtime.NullTypes.DbNull as (new (secret: never) => typeof runtime.DbNull),
JsonNull: runtime.NullTypes.JsonNull as (new (secret: never) => typeof runtime.JsonNull),
AnyNull: runtime.NullTypes.AnyNull as (new (secret: never) => typeof runtime.AnyNull),
}
/**
* Helper for filtering JSON entries that have `null` on the database (empty on the db)
*
* @see https://www.prisma.io/docs/concepts/components/prisma-client/working-with-fields/working-with-json-fields#filtering-on-a-json-field
*/
export const DbNull = runtime.DbNull
/**
* Helper for filtering JSON entries that have JSON `null` values (not empty on the db)
*
* @see https://www.prisma.io/docs/concepts/components/prisma-client/working-with-fields/working-with-json-fields#filtering-on-a-json-field
*/
export const JsonNull = runtime.JsonNull
/**
* Helper for filtering JSON entries that are `Prisma.DbNull` or `Prisma.JsonNull`
*
* @see https://www.prisma.io/docs/concepts/components/prisma-client/working-with-fields/working-with-json-fields#filtering-on-a-json-field
*/
export const AnyNull = runtime.AnyNull
type SelectAndInclude = {
select: any
include: any
}
type SelectAndOmit = {
select: any
omit: any
}
/**
* From T, pick a set of properties whose keys are in the union K
*/
type Prisma__Pick<T, K extends keyof T> = {
[P in K]: T[P];
};
export type Enumerable<T> = T | Array<T>;
/**
* Subset
* @desc From `T` pick properties that exist in `U`. Simple version of Intersection
*/
export type Subset<T, U> = {
[key in keyof T]: key extends keyof U ? T[key] : never;
};
/**
* SelectSubset
* @desc From `T` pick properties that exist in `U`. Simple version of Intersection.
* Additionally, it validates, if both select and include are present. If the case, it errors.
*/
export type SelectSubset<T, U> = {
[key in keyof T]: key extends keyof U ? T[key] : never
} &
(T extends SelectAndInclude
? 'Please either choose `select` or `include`.'
: T extends SelectAndOmit
? 'Please either choose `select` or `omit`.'
: {})
/**
* Subset + Intersection
* @desc From `T` pick properties that exist in `U` and intersect `K`
*/
export type SubsetIntersection<T, U, K> = {
[key in keyof T]: key extends keyof U ? T[key] : never
} &
K
type Without<T, U> = { [P in Exclude<keyof T, keyof U>]?: never };
/**
* XOR is needed to have a real mutually exclusive union type
* https://stackoverflow.com/questions/42123407/does-typescript-support-mutually-exclusive-types
*/
export type XOR<T, U> =
T extends object ?
U extends object ?
(Without<T, U> & U) | (Without<U, T> & T)
: U : T
/**
* Is T a Record?
*/
type IsObject<T extends any> = T extends Array<any>
? False
: T extends Date
? False
: T extends Uint8Array
? False
: T extends BigInt
? False
: T extends object
? True
: False
/**
* If it's T[], return T
*/
export type UnEnumerate<T extends unknown> = T extends Array<infer U> ? U : T
/**
* From ts-toolbelt
*/
type __Either<O extends object, K extends Key> = Omit<O, K> &
{
// Merge all but K
[P in K]: Prisma__Pick<O, P & keyof O> // With K possibilities
}[K]
type EitherStrict<O extends object, K extends Key> = Strict<__Either<O, K>>
type EitherLoose<O extends object, K extends Key> = ComputeRaw<__Either<O, K>>
type _Either<
O extends object,
K extends Key,
strict extends Boolean
> = {
1: EitherStrict<O, K>
0: EitherLoose<O, K>
}[strict]
export type Either<
O extends object,
K extends Key,
strict extends Boolean = 1
> = O extends unknown ? _Either<O, K, strict> : never
export type Union = any
export type PatchUndefined<O extends object, O1 extends object> = {
[K in keyof O]: O[K] extends undefined ? At<O1, K> : O[K]
} & {}
/** Helper Types for "Merge" **/
export type IntersectOf<U extends Union> = (
U extends unknown ? (k: U) => void : never
) extends (k: infer I) => void
? I
: never
export type Overwrite<O extends object, O1 extends object> = {
[K in keyof O]: K extends keyof O1 ? O1[K] : O[K];
} & {};
type _Merge<U extends object> = IntersectOf<Overwrite<U, {
[K in keyof U]-?: At<U, K>;
}>>;
type Key = string | number | symbol;
type AtStrict<O extends object, K extends Key> = O[K & keyof O];
type AtLoose<O extends object, K extends Key> = O extends unknown ? AtStrict<O, K> : never;
export type At<O extends object, K extends Key, strict extends Boolean = 1> = {
1: AtStrict<O, K>;
0: AtLoose<O, K>;
}[strict];
export type ComputeRaw<A extends any> = A extends Function ? A : {
[K in keyof A]: A[K];
} & {};
export type OptionalFlat<O> = {
[K in keyof O]?: O[K];
} & {};
type _Record<K extends keyof any, T> = {
[P in K]: T;
};
// cause typescript not to expand types and preserve names
type NoExpand<T> = T extends unknown ? T : never;
// this type assumes the passed object is entirely optional
export type AtLeast<O extends object, K extends string> = NoExpand<
O extends unknown
? | (K extends keyof O ? { [P in K]: O[P] } & O : O)
| {[P in keyof O as P extends K ? P : never]-?: O[P]} & O
: never>;
type _Strict<U, _U = U> = U extends unknown ? U & OptionalFlat<_Record<Exclude<Keys<_U>, keyof U>, never>> : never;
export type Strict<U extends object> = ComputeRaw<_Strict<U>>;
/** End Helper Types for "Merge" **/
export type Merge<U extends object> = ComputeRaw<_Merge<Strict<U>>>;
export type Boolean = True | False
export type True = 1
export type False = 0
export type Not<B extends Boolean> = {
0: 1
1: 0
}[B]
export type Extends<A1 extends any, A2 extends any> = [A1] extends [never]
? 0 // anything `never` is false
: A1 extends A2
? 1
: 0
export type Has<U extends Union, U1 extends Union> = Not<
Extends<Exclude<U1, U>, U1>
>
export type Or<B1 extends Boolean, B2 extends Boolean> = {
0: {
0: 0
1: 1
}
1: {
0: 1
1: 1
}
}[B1][B2]
export type Keys<U extends Union> = U extends unknown ? keyof U : never
export type GetScalarType<T, O> = O extends object ? {
[P in keyof T]: P extends keyof O
? O[P]
: never
} : never
type FieldPaths<
T,
U = Omit<T, '_avg' | '_sum' | '_count' | '_min' | '_max'>
> = IsObject<T> extends True ? U : T
export type GetHavingFields<T> = {
[K in keyof T]: Or<
Or<Extends<'OR', K>, Extends<'AND', K>>,
Extends<'NOT', K>
> extends True
? // infer is only needed to not hit TS limit
// based on the brilliant idea of Pierre-Antoine Mills
// https://github.com/microsoft/TypeScript/issues/30188#issuecomment-478938437
T[K] extends infer TK
? GetHavingFields<UnEnumerate<TK> extends object ? Merge<UnEnumerate<TK>> : never>
: never
: {} extends FieldPaths<T[K]>
? never
: K
}[keyof T]
/**
* Convert tuple to union
*/
type _TupleToUnion<T> = T extends (infer E)[] ? E : never
type TupleToUnion<K extends readonly any[]> = _TupleToUnion<K>
export type MaybeTupleToUnion<T> = T extends any[] ? TupleToUnion<T> : T
/**
* Like `Pick`, but additionally can also accept an array of keys
*/
export type PickEnumerable<T, K extends Enumerable<keyof T> | keyof T> = Prisma__Pick<T, MaybeTupleToUnion<K>>
/**
* Exclude all keys with underscores
*/
export type ExcludeUnderscoreKeys<T extends string> = T extends `_${string}` ? never : T
export type FieldRef<Model, FieldType> = runtime.FieldRef<Model, FieldType>
type FieldRefInputType<Model, FieldType> = Model extends never ? never : FieldRef<Model, FieldType>
export const ModelName = {
activity_logs: 'activity_logs',
training_menus: 'training_menus',
user_recaps: 'user_recaps'
} as const
export type ModelName = (typeof ModelName)[keyof typeof ModelName]
export interface TypeMapCb<GlobalOmitOptions = {}> extends runtime.Types.Utils.Fn<{extArgs: runtime.Types.Extensions.InternalArgs }, runtime.Types.Utils.Record<string, any>> {
returns: TypeMap<this['params']['extArgs'], GlobalOmitOptions>
}
export type TypeMap<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs, GlobalOmitOptions = {}> = {
globalOmitOptions: {
omit: GlobalOmitOptions
}
meta: {
modelProps: "activity_logs" | "training_menus" | "user_recaps"
txIsolationLevel: TransactionIsolationLevel
}
model: {
activity_logs: {
payload: Prisma.$activity_logsPayload<ExtArgs>
fields: Prisma.activity_logsFieldRefs
operations: {
findUnique: {
args: Prisma.activity_logsFindUniqueArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$activity_logsPayload> | null
}
findUniqueOrThrow: {
args: Prisma.activity_logsFindUniqueOrThrowArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$activity_logsPayload>
}
findFirst: {
args: Prisma.activity_logsFindFirstArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$activity_logsPayload> | null
}
findFirstOrThrow: {
args: Prisma.activity_logsFindFirstOrThrowArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$activity_logsPayload>
}
findMany: {
args: Prisma.activity_logsFindManyArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$activity_logsPayload>[]
}
create: {
args: Prisma.activity_logsCreateArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$activity_logsPayload>
}
createMany: {
args: Prisma.activity_logsCreateManyArgs<ExtArgs>
result: BatchPayload
}
createManyAndReturn: {
args: Prisma.activity_logsCreateManyAndReturnArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$activity_logsPayload>[]
}
delete: {
args: Prisma.activity_logsDeleteArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$activity_logsPayload>
}
update: {
args: Prisma.activity_logsUpdateArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$activity_logsPayload>
}
deleteMany: {
args: Prisma.activity_logsDeleteManyArgs<ExtArgs>
result: BatchPayload
}
updateMany: {
args: Prisma.activity_logsUpdateManyArgs<ExtArgs>
result: BatchPayload
}
updateManyAndReturn: {
args: Prisma.activity_logsUpdateManyAndReturnArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$activity_logsPayload>[]
}
upsert: {
args: Prisma.activity_logsUpsertArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$activity_logsPayload>
}
aggregate: {
args: Prisma.Activity_logsAggregateArgs<ExtArgs>
result: runtime.Types.Utils.Optional<Prisma.AggregateActivity_logs>
}
groupBy: {
args: Prisma.activity_logsGroupByArgs<ExtArgs>
result: runtime.Types.Utils.Optional<Prisma.Activity_logsGroupByOutputType>[]
}
count: {
args: Prisma.activity_logsCountArgs<ExtArgs>
result: runtime.Types.Utils.Optional<Prisma.Activity_logsCountAggregateOutputType> | number
}
}
}
training_menus: {
payload: Prisma.$training_menusPayload<ExtArgs>
fields: Prisma.training_menusFieldRefs
operations: {
findUnique: {
args: Prisma.training_menusFindUniqueArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$training_menusPayload> | null
}
findUniqueOrThrow: {
args: Prisma.training_menusFindUniqueOrThrowArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$training_menusPayload>
}
findFirst: {
args: Prisma.training_menusFindFirstArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$training_menusPayload> | null
}
findFirstOrThrow: {
args: Prisma.training_menusFindFirstOrThrowArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$training_menusPayload>
}
findMany: {
args: Prisma.training_menusFindManyArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$training_menusPayload>[]
}
create: {
args: Prisma.training_menusCreateArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$training_menusPayload>
}
createMany: {
args: Prisma.training_menusCreateManyArgs<ExtArgs>
result: BatchPayload
}
createManyAndReturn: {
args: Prisma.training_menusCreateManyAndReturnArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$training_menusPayload>[]
}
delete: {
args: Prisma.training_menusDeleteArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$training_menusPayload>
}
update: {
args: Prisma.training_menusUpdateArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$training_menusPayload>
}
deleteMany: {
args: Prisma.training_menusDeleteManyArgs<ExtArgs>
result: BatchPayload
}
updateMany: {
args: Prisma.training_menusUpdateManyArgs<ExtArgs>
result: BatchPayload
}
updateManyAndReturn: {
args: Prisma.training_menusUpdateManyAndReturnArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$training_menusPayload>[]
}
upsert: {
args: Prisma.training_menusUpsertArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$training_menusPayload>
}
aggregate: {
args: Prisma.Training_menusAggregateArgs<ExtArgs>
result: runtime.Types.Utils.Optional<Prisma.AggregateTraining_menus>
}
groupBy: {
args: Prisma.training_menusGroupByArgs<ExtArgs>
result: runtime.Types.Utils.Optional<Prisma.Training_menusGroupByOutputType>[]
}
count: {
args: Prisma.training_menusCountArgs<ExtArgs>
result: runtime.Types.Utils.Optional<Prisma.Training_menusCountAggregateOutputType> | number
}
}
}
user_recaps: {
payload: Prisma.$user_recapsPayload<ExtArgs>
fields: Prisma.user_recapsFieldRefs
operations: {
findUnique: {
args: Prisma.user_recapsFindUniqueArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$user_recapsPayload> | null
}
findUniqueOrThrow: {
args: Prisma.user_recapsFindUniqueOrThrowArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$user_recapsPayload>
}
findFirst: {
args: Prisma.user_recapsFindFirstArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$user_recapsPayload> | null
}
findFirstOrThrow: {
args: Prisma.user_recapsFindFirstOrThrowArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$user_recapsPayload>
}
findMany: {
args: Prisma.user_recapsFindManyArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$user_recapsPayload>[]
}
create: {
args: Prisma.user_recapsCreateArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$user_recapsPayload>
}
createMany: {
args: Prisma.user_recapsCreateManyArgs<ExtArgs>
result: BatchPayload
}
createManyAndReturn: {
args: Prisma.user_recapsCreateManyAndReturnArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$user_recapsPayload>[]
}
delete: {
args: Prisma.user_recapsDeleteArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$user_recapsPayload>
}
update: {
args: Prisma.user_recapsUpdateArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$user_recapsPayload>
}
deleteMany: {
args: Prisma.user_recapsDeleteManyArgs<ExtArgs>
result: BatchPayload
}
updateMany: {
args: Prisma.user_recapsUpdateManyArgs<ExtArgs>
result: BatchPayload
}
updateManyAndReturn: {
args: Prisma.user_recapsUpdateManyAndReturnArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$user_recapsPayload>[]
}
upsert: {
args: Prisma.user_recapsUpsertArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$user_recapsPayload>
}
aggregate: {
args: Prisma.User_recapsAggregateArgs<ExtArgs>
result: runtime.Types.Utils.Optional<Prisma.AggregateUser_recaps>
}
groupBy: {
args: Prisma.user_recapsGroupByArgs<ExtArgs>
result: runtime.Types.Utils.Optional<Prisma.User_recapsGroupByOutputType>[]
}
count: {
args: Prisma.user_recapsCountArgs<ExtArgs>
result: runtime.Types.Utils.Optional<Prisma.User_recapsCountAggregateOutputType> | number
}
}
}
}
} & {
other: {
payload: any
operations: {
$executeRaw: {
args: [query: TemplateStringsArray | Sql, ...values: any[]],
result: any
}
$executeRawUnsafe: {
args: [query: string, ...values: any[]],
result: any
}
$queryRaw: {
args: [query: TemplateStringsArray | Sql, ...values: any[]],
result: any
}
$queryRawUnsafe: {
args: [query: string, ...values: any[]],
result: any
}
}
}
}
/**
* Enums
*/
export const TransactionIsolationLevel = runtime.makeStrictEnum({
ReadUncommitted: 'ReadUncommitted',
ReadCommitted: 'ReadCommitted',
RepeatableRead: 'RepeatableRead',
Serializable: 'Serializable'
} as const)
export type TransactionIsolationLevel = (typeof TransactionIsolationLevel)[keyof typeof TransactionIsolationLevel]
export const Activity_logsScalarFieldEnum = {
id: 'id',
timestamp: 'timestamp',
status: 'status',
confidence: 'confidence',
details: 'details'
} as const
export type Activity_logsScalarFieldEnum = (typeof Activity_logsScalarFieldEnum)[keyof typeof Activity_logsScalarFieldEnum]
export const Training_menusScalarFieldEnum = {
id: 'id',
name: 'name',
exercises: 'exercises',
created_at: 'created_at'
} as const
export type Training_menusScalarFieldEnum = (typeof Training_menusScalarFieldEnum)[keyof typeof Training_menusScalarFieldEnum]
export const User_recapsScalarFieldEnum = {
id: 'id',
menu_id: 'menu_id',
summary: 'summary',
completed_at: 'completed_at'
} as const
export type User_recapsScalarFieldEnum = (typeof User_recapsScalarFieldEnum)[keyof typeof User_recapsScalarFieldEnum]
export const SortOrder = {
asc: 'asc',
desc: 'desc'
} as const
export type SortOrder = (typeof SortOrder)[keyof typeof SortOrder]
export const NullableJsonNullValueInput = {
DbNull: DbNull,
JsonNull: JsonNull
} as const
export type NullableJsonNullValueInput = (typeof NullableJsonNullValueInput)[keyof typeof NullableJsonNullValueInput]
export const QueryMode = {
default: 'default',
insensitive: 'insensitive'
} as const
export type QueryMode = (typeof QueryMode)[keyof typeof QueryMode]
export const JsonNullValueFilter = {
DbNull: DbNull,
JsonNull: JsonNull,
AnyNull: AnyNull
} as const
export type JsonNullValueFilter = (typeof JsonNullValueFilter)[keyof typeof JsonNullValueFilter]
export const NullsOrder = {
first: 'first',
last: 'last'
} as const
export type NullsOrder = (typeof NullsOrder)[keyof typeof NullsOrder]
/**
* Field references
*/
/**
* Reference to a field of type 'Int'
*/
export type IntFieldRefInput<$PrismaModel> = FieldRefInputType<$PrismaModel, 'Int'>
/**
* Reference to a field of type 'Int[]'
*/
export type ListIntFieldRefInput<$PrismaModel> = FieldRefInputType<$PrismaModel, 'Int[]'>
/**
* Reference to a field of type 'DateTime'
*/
export type DateTimeFieldRefInput<$PrismaModel> = FieldRefInputType<$PrismaModel, 'DateTime'>
/**
* Reference to a field of type 'DateTime[]'
*/
export type ListDateTimeFieldRefInput<$PrismaModel> = FieldRefInputType<$PrismaModel, 'DateTime[]'>
/**
* Reference to a field of type 'String'
*/
export type StringFieldRefInput<$PrismaModel> = FieldRefInputType<$PrismaModel, 'String'>
/**
* Reference to a field of type 'String[]'
*/
export type ListStringFieldRefInput<$PrismaModel> = FieldRefInputType<$PrismaModel, 'String[]'>
/**
* Reference to a field of type 'Json'
*/
export type JsonFieldRefInput<$PrismaModel> = FieldRefInputType<$PrismaModel, 'Json'>
/**
* Reference to a field of type 'QueryMode'
*/
export type EnumQueryModeFieldRefInput<$PrismaModel> = FieldRefInputType<$PrismaModel, 'QueryMode'>
/**
* Reference to a field of type 'Float'
*/
export type FloatFieldRefInput<$PrismaModel> = FieldRefInputType<$PrismaModel, 'Float'>
/**
* Reference to a field of type 'Float[]'
*/
export type ListFloatFieldRefInput<$PrismaModel> = FieldRefInputType<$PrismaModel, 'Float[]'>
/**
* Batch Payload for updateMany & deleteMany & createMany
*/
export type BatchPayload = {
count: number
}
export const defineExtension = runtime.Extensions.defineExtension as unknown as runtime.Types.Extensions.ExtendsHook<"define", TypeMapCb, runtime.Types.Extensions.DefaultArgs>
export type DefaultPrismaClient = PrismaClient
export type ErrorFormat = 'pretty' | 'colorless' | 'minimal'
export type PrismaClientOptions = ({
/**
* Instance of a Driver Adapter, e.g., like one provided by `@prisma/adapter-pg`.
*/
adapter: runtime.SqlDriverAdapterFactory
accelerateUrl?: never
} | {
/**
* Prisma Accelerate URL allowing the client to connect through Accelerate instead of a direct database.
*/
accelerateUrl: string
adapter?: never
}) & {
/**
* @default "colorless"
*/
errorFormat?: ErrorFormat
/**
* @example
* ```
* // Shorthand for `emit: 'stdout'`
* log: ['query', 'info', 'warn', 'error']
*
* // Emit as events only
* log: [
* { emit: 'event', level: 'query' },
* { emit: 'event', level: 'info' },
* { emit: 'event', level: 'warn' }
* { emit: 'event', level: 'error' }
* ]
*
* / Emit as events and log to stdout
* og: [
* { emit: 'stdout', level: 'query' },
* { emit: 'stdout', level: 'info' },
* { emit: 'stdout', level: 'warn' }
* { emit: 'stdout', level: 'error' }
*
* ```
* Read more in our [docs](https://pris.ly/d/logging).
*/
log?: (LogLevel | LogDefinition)[]
/**
* The default values for transactionOptions
* maxWait ?= 2000
* timeout ?= 5000
*/
transactionOptions?: {
maxWait?: number
timeout?: number
isolationLevel?: TransactionIsolationLevel
}
/**
* Global configuration for omitting model fields by default.
*
* @example
* ```
* const prisma = new PrismaClient({
* omit: {
* user: {
* password: true
* }
* }
* })
* ```
*/
omit?: GlobalOmitConfig
/**
* SQL commenter plugins that add metadata to SQL queries as comments.
* Comments follow the sqlcommenter format: https://google.github.io/sqlcommenter/
*
* @example
* ```
* const prisma = new PrismaClient({
* adapter,
* comments: [
* traceContext(),
* queryInsights(),
* ],
* })
* ```
*/
comments?: runtime.SqlCommenterPlugin[]
}
export type GlobalOmitConfig = {
activity_logs?: Prisma.activity_logsOmit
training_menus?: Prisma.training_menusOmit
user_recaps?: Prisma.user_recapsOmit
}
/* Types for Logging */
export type LogLevel = 'info' | 'query' | 'warn' | 'error'
export type LogDefinition = {
level: LogLevel
emit: 'stdout' | 'event'
}
export type CheckIsLogLevel<T> = T extends LogLevel ? T : never;
export type GetLogType<T> = CheckIsLogLevel<
T extends LogDefinition ? T['level'] : T
>;
export type GetEvents<T extends any[]> = T extends Array<LogLevel | LogDefinition>
? GetLogType<T[number]>
: never;
export type QueryEvent = {
timestamp: Date
query: string
params: string
duration: number
target: string
}
export type LogEvent = {
timestamp: Date
message: string
target: string
}
/* End Types for Logging */
export type PrismaAction =
| 'findUnique'
| 'findUniqueOrThrow'
| 'findMany'
| 'findFirst'
| 'findFirstOrThrow'
| 'create'
| 'createMany'
| 'createManyAndReturn'
| 'update'
| 'updateMany'
| 'updateManyAndReturn'
| 'upsert'
| 'delete'
| 'deleteMany'
| 'executeRaw'
| 'queryRaw'
| 'aggregate'
| 'count'
| 'runCommandRaw'
| 'findRaw'
| 'groupBy'
/**
* `PrismaClient` proxy available in interactive transactions.
*/
export type TransactionClient = Omit<DefaultPrismaClient, runtime.ITXClientDenyList>

View File

@ -0,0 +1,145 @@
/* !!! This is code generated by Prisma. Do not edit directly. !!! */
/* eslint-disable */
// biome-ignore-all lint: generated file
// @ts-nocheck
/*
* WARNING: This is an internal file that is subject to change!
*
* 🛑 Under no circumstances should you import this file directly! 🛑
*
* All exports from this file are wrapped under a `Prisma` namespace object in the browser.ts file.
* While this enables partial backward compatibility, it is not part of the stable public API.
*
* If you are looking for your Models, Enums, and Input Types, please import them from the respective
* model files in the `model` directory!
*/
import * as runtime from "@prisma/client/runtime/index-browser"
export type * from '../models.js'
export type * from './prismaNamespace.js'
export const Decimal = runtime.Decimal
export const NullTypes = {
DbNull: runtime.NullTypes.DbNull as (new (secret: never) => typeof runtime.DbNull),
JsonNull: runtime.NullTypes.JsonNull as (new (secret: never) => typeof runtime.JsonNull),
AnyNull: runtime.NullTypes.AnyNull as (new (secret: never) => typeof runtime.AnyNull),
}
/**
* Helper for filtering JSON entries that have `null` on the database (empty on the db)
*
* @see https://www.prisma.io/docs/concepts/components/prisma-client/working-with-fields/working-with-json-fields#filtering-on-a-json-field
*/
export const DbNull = runtime.DbNull
/**
* Helper for filtering JSON entries that have JSON `null` values (not empty on the db)
*
* @see https://www.prisma.io/docs/concepts/components/prisma-client/working-with-fields/working-with-json-fields#filtering-on-a-json-field
*/
export const JsonNull = runtime.JsonNull
/**
* Helper for filtering JSON entries that are `Prisma.DbNull` or `Prisma.JsonNull`
*
* @see https://www.prisma.io/docs/concepts/components/prisma-client/working-with-fields/working-with-json-fields#filtering-on-a-json-field
*/
export const AnyNull = runtime.AnyNull
export const ModelName = {
activity_logs: 'activity_logs',
training_menus: 'training_menus',
user_recaps: 'user_recaps'
} as const
export type ModelName = (typeof ModelName)[keyof typeof ModelName]
/*
* Enums
*/
export const TransactionIsolationLevel = {
ReadUncommitted: 'ReadUncommitted',
ReadCommitted: 'ReadCommitted',
RepeatableRead: 'RepeatableRead',
Serializable: 'Serializable'
} as const
export type TransactionIsolationLevel = (typeof TransactionIsolationLevel)[keyof typeof TransactionIsolationLevel]
export const Activity_logsScalarFieldEnum = {
id: 'id',
timestamp: 'timestamp',
status: 'status',
confidence: 'confidence',
details: 'details'
} as const
export type Activity_logsScalarFieldEnum = (typeof Activity_logsScalarFieldEnum)[keyof typeof Activity_logsScalarFieldEnum]
export const Training_menusScalarFieldEnum = {
id: 'id',
name: 'name',
exercises: 'exercises',
created_at: 'created_at'
} as const
export type Training_menusScalarFieldEnum = (typeof Training_menusScalarFieldEnum)[keyof typeof Training_menusScalarFieldEnum]
export const User_recapsScalarFieldEnum = {
id: 'id',
menu_id: 'menu_id',
summary: 'summary',
completed_at: 'completed_at'
} as const
export type User_recapsScalarFieldEnum = (typeof User_recapsScalarFieldEnum)[keyof typeof User_recapsScalarFieldEnum]
export const SortOrder = {
asc: 'asc',
desc: 'desc'
} as const
export type SortOrder = (typeof SortOrder)[keyof typeof SortOrder]
export const NullableJsonNullValueInput = {
DbNull: 'DbNull',
JsonNull: 'JsonNull'
} as const
export type NullableJsonNullValueInput = (typeof NullableJsonNullValueInput)[keyof typeof NullableJsonNullValueInput]
export const QueryMode = {
default: 'default',
insensitive: 'insensitive'
} as const
export type QueryMode = (typeof QueryMode)[keyof typeof QueryMode]
export const JsonNullValueFilter = {
DbNull: 'DbNull',
JsonNull: 'JsonNull',
AnyNull: 'AnyNull'
} as const
export type JsonNullValueFilter = (typeof JsonNullValueFilter)[keyof typeof JsonNullValueFilter]
export const NullsOrder = {
first: 'first',
last: 'last'
} as const
export type NullsOrder = (typeof NullsOrder)[keyof typeof NullsOrder]

14
lib/prisma-gen/models.ts Normal file
View File

@ -0,0 +1,14 @@
/* !!! This is code generated by Prisma. Do not edit directly. !!! */
/* eslint-disable */
// biome-ignore-all lint: generated file
// @ts-nocheck
/*
* This is a barrel export file for all models and their related types.
*
* 🟢 You can import this file directly.
*/
export type * from './models/activity_logs.js'
export type * from './models/training_menus.js'
export type * from './models/user_recaps.js'
export type * from './commonInputTypes.js'

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

10
lib/prisma.ts Normal file
View File

@ -0,0 +1,10 @@
// @ts-nocheck
import { PrismaClient } from '../app/generated/client/client';
const globalForPrisma = global as unknown as { prisma: PrismaClient };
export const prisma =
globalForPrisma.prisma ||
new PrismaClient();
if (process.env.NODE_ENV !== 'production') globalForPrisma.prisma = prisma;

View File

@ -0,0 +1,34 @@
/* !!! This is code generated by Prisma. Do not edit directly. !!! */
/* eslint-disable */
// biome-ignore-all lint: generated file
// @ts-nocheck
/*
* This file should be your main import to use Prisma-related types and utilities in a browser.
* Use it to get access to models, enums, and input types.
*
* This file does not contain a `PrismaClient` class, nor several other helpers that are intended as server-side only.
* See `client.ts` for the standard, server-side entry point.
*
* 🟢 You can import this file directly.
*/
import * as Prisma from './internal/prismaNamespaceBrowser'
export { Prisma }
export * as $Enums from './enums'
export * from './enums';
/**
* Model activity_logs
*
*/
export type activity_logs = Prisma.activity_logsModel
/**
* Model training_menus
*
*/
export type training_menus = Prisma.training_menusModel
/**
* Model user_recaps
*
*/
export type user_recaps = Prisma.user_recapsModel

View File

@ -0,0 +1,56 @@
/* !!! This is code generated by Prisma. Do not edit directly. !!! */
/* eslint-disable */
// biome-ignore-all lint: generated file
// @ts-nocheck
/*
* This file should be your main import to use Prisma. Through it you get access to all the models, enums, and input types.
* If you're looking for something you can import in the client-side of your application, please refer to the `browser.ts` file instead.
*
* 🟢 You can import this file directly.
*/
import * as process from 'node:process'
import * as path from 'node:path'
import { fileURLToPath } from 'node:url'
globalThis['__dirname'] = path.dirname(fileURLToPath(import.meta.url))
import * as runtime from "@prisma/client/runtime/client"
import * as $Enums from "./enums"
import * as $Class from "./internal/class"
import * as Prisma from "./internal/prismaNamespace"
export * as $Enums from './enums'
export * from "./enums"
/**
* ## Prisma Client
*
* Type-safe database client for TypeScript
* @example
* ```
* const prisma = new PrismaClient()
* // Fetch zero or more Activity_logs
* const activity_logs = await prisma.activity_logs.findMany()
* ```
*
* Read more in our [docs](https://pris.ly/d/client).
*/
export const PrismaClient = $Class.getPrismaClientClass()
export type PrismaClient<LogOpts extends Prisma.LogLevel = never, OmitOpts extends Prisma.PrismaClientOptions["omit"] = Prisma.PrismaClientOptions["omit"], ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = $Class.PrismaClient<LogOpts, OmitOpts, ExtArgs>
export { Prisma }
/**
* Model activity_logs
*
*/
export type activity_logs = Prisma.activity_logsModel
/**
* Model training_menus
*
*/
export type training_menus = Prisma.training_menusModel
/**
* Model user_recaps
*
*/
export type user_recaps = Prisma.user_recapsModel

View File

@ -0,0 +1,341 @@
/* !!! This is code generated by Prisma. Do not edit directly. !!! */
/* eslint-disable */
// biome-ignore-all lint: generated file
// @ts-nocheck
/*
* This file exports various common sort, input & filter types that are not directly linked to a particular model.
*
* 🟢 You can import this file directly.
*/
import type * as runtime from "@prisma/client/runtime/client"
import * as $Enums from "./enums"
import type * as Prisma from "./internal/prismaNamespace"
export type IntFilter<$PrismaModel = never> = {
equals?: number | Prisma.IntFieldRefInput<$PrismaModel>
in?: number[] | Prisma.ListIntFieldRefInput<$PrismaModel>
notIn?: number[] | Prisma.ListIntFieldRefInput<$PrismaModel>
lt?: number | Prisma.IntFieldRefInput<$PrismaModel>
lte?: number | Prisma.IntFieldRefInput<$PrismaModel>
gt?: number | Prisma.IntFieldRefInput<$PrismaModel>
gte?: number | Prisma.IntFieldRefInput<$PrismaModel>
not?: Prisma.NestedIntFilter<$PrismaModel> | number
}
export type DateTimeNullableFilter<$PrismaModel = never> = {
equals?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel> | null
in?: Date[] | string[] | Prisma.ListDateTimeFieldRefInput<$PrismaModel> | null
notIn?: Date[] | string[] | Prisma.ListDateTimeFieldRefInput<$PrismaModel> | null
lt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
lte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
gt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
gte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
not?: Prisma.NestedDateTimeNullableFilter<$PrismaModel> | Date | string | null
}
export type StringNullableFilter<$PrismaModel = never> = {
equals?: string | Prisma.StringFieldRefInput<$PrismaModel> | null
in?: string[] | Prisma.ListStringFieldRefInput<$PrismaModel> | null
notIn?: string[] | Prisma.ListStringFieldRefInput<$PrismaModel> | null
lt?: string | Prisma.StringFieldRefInput<$PrismaModel>
lte?: string | Prisma.StringFieldRefInput<$PrismaModel>
gt?: string | Prisma.StringFieldRefInput<$PrismaModel>
gte?: string | Prisma.StringFieldRefInput<$PrismaModel>
contains?: string | Prisma.StringFieldRefInput<$PrismaModel>
startsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>
endsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>
mode?: Prisma.QueryMode
not?: Prisma.NestedStringNullableFilter<$PrismaModel> | string | null
}
export type JsonNullableFilter<$PrismaModel = never> =
| Prisma.PatchUndefined<
Prisma.Either<Required<JsonNullableFilterBase<$PrismaModel>>, Exclude<keyof Required<JsonNullableFilterBase<$PrismaModel>>, 'path'>>,
Required<JsonNullableFilterBase<$PrismaModel>>
>
| Prisma.OptionalFlat<Omit<Required<JsonNullableFilterBase<$PrismaModel>>, 'path'>>
export type JsonNullableFilterBase<$PrismaModel = never> = {
equals?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel> | Prisma.JsonNullValueFilter
path?: string[]
mode?: Prisma.QueryMode | Prisma.EnumQueryModeFieldRefInput<$PrismaModel>
string_contains?: string | Prisma.StringFieldRefInput<$PrismaModel>
string_starts_with?: string | Prisma.StringFieldRefInput<$PrismaModel>
string_ends_with?: string | Prisma.StringFieldRefInput<$PrismaModel>
array_starts_with?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel> | null
array_ends_with?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel> | null
array_contains?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel> | null
lt?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel>
lte?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel>
gt?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel>
gte?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel>
not?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel> | Prisma.JsonNullValueFilter
}
export type SortOrderInput = {
sort: Prisma.SortOrder
nulls?: Prisma.NullsOrder
}
export type IntWithAggregatesFilter<$PrismaModel = never> = {
equals?: number | Prisma.IntFieldRefInput<$PrismaModel>
in?: number[] | Prisma.ListIntFieldRefInput<$PrismaModel>
notIn?: number[] | Prisma.ListIntFieldRefInput<$PrismaModel>
lt?: number | Prisma.IntFieldRefInput<$PrismaModel>
lte?: number | Prisma.IntFieldRefInput<$PrismaModel>
gt?: number | Prisma.IntFieldRefInput<$PrismaModel>
gte?: number | Prisma.IntFieldRefInput<$PrismaModel>
not?: Prisma.NestedIntWithAggregatesFilter<$PrismaModel> | number
_count?: Prisma.NestedIntFilter<$PrismaModel>
_avg?: Prisma.NestedFloatFilter<$PrismaModel>
_sum?: Prisma.NestedIntFilter<$PrismaModel>
_min?: Prisma.NestedIntFilter<$PrismaModel>
_max?: Prisma.NestedIntFilter<$PrismaModel>
}
export type DateTimeNullableWithAggregatesFilter<$PrismaModel = never> = {
equals?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel> | null
in?: Date[] | string[] | Prisma.ListDateTimeFieldRefInput<$PrismaModel> | null
notIn?: Date[] | string[] | Prisma.ListDateTimeFieldRefInput<$PrismaModel> | null
lt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
lte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
gt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
gte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
not?: Prisma.NestedDateTimeNullableWithAggregatesFilter<$PrismaModel> | Date | string | null
_count?: Prisma.NestedIntNullableFilter<$PrismaModel>
_min?: Prisma.NestedDateTimeNullableFilter<$PrismaModel>
_max?: Prisma.NestedDateTimeNullableFilter<$PrismaModel>
}
export type StringNullableWithAggregatesFilter<$PrismaModel = never> = {
equals?: string | Prisma.StringFieldRefInput<$PrismaModel> | null
in?: string[] | Prisma.ListStringFieldRefInput<$PrismaModel> | null
notIn?: string[] | Prisma.ListStringFieldRefInput<$PrismaModel> | null
lt?: string | Prisma.StringFieldRefInput<$PrismaModel>
lte?: string | Prisma.StringFieldRefInput<$PrismaModel>
gt?: string | Prisma.StringFieldRefInput<$PrismaModel>
gte?: string | Prisma.StringFieldRefInput<$PrismaModel>
contains?: string | Prisma.StringFieldRefInput<$PrismaModel>
startsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>
endsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>
mode?: Prisma.QueryMode
not?: Prisma.NestedStringNullableWithAggregatesFilter<$PrismaModel> | string | null
_count?: Prisma.NestedIntNullableFilter<$PrismaModel>
_min?: Prisma.NestedStringNullableFilter<$PrismaModel>
_max?: Prisma.NestedStringNullableFilter<$PrismaModel>
}
export type JsonNullableWithAggregatesFilter<$PrismaModel = never> =
| Prisma.PatchUndefined<
Prisma.Either<Required<JsonNullableWithAggregatesFilterBase<$PrismaModel>>, Exclude<keyof Required<JsonNullableWithAggregatesFilterBase<$PrismaModel>>, 'path'>>,
Required<JsonNullableWithAggregatesFilterBase<$PrismaModel>>
>
| Prisma.OptionalFlat<Omit<Required<JsonNullableWithAggregatesFilterBase<$PrismaModel>>, 'path'>>
export type JsonNullableWithAggregatesFilterBase<$PrismaModel = never> = {
equals?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel> | Prisma.JsonNullValueFilter
path?: string[]
mode?: Prisma.QueryMode | Prisma.EnumQueryModeFieldRefInput<$PrismaModel>
string_contains?: string | Prisma.StringFieldRefInput<$PrismaModel>
string_starts_with?: string | Prisma.StringFieldRefInput<$PrismaModel>
string_ends_with?: string | Prisma.StringFieldRefInput<$PrismaModel>
array_starts_with?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel> | null
array_ends_with?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel> | null
array_contains?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel> | null
lt?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel>
lte?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel>
gt?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel>
gte?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel>
not?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel> | Prisma.JsonNullValueFilter
_count?: Prisma.NestedIntNullableFilter<$PrismaModel>
_min?: Prisma.NestedJsonNullableFilter<$PrismaModel>
_max?: Prisma.NestedJsonNullableFilter<$PrismaModel>
}
export type IntNullableFilter<$PrismaModel = never> = {
equals?: number | Prisma.IntFieldRefInput<$PrismaModel> | null
in?: number[] | Prisma.ListIntFieldRefInput<$PrismaModel> | null
notIn?: number[] | Prisma.ListIntFieldRefInput<$PrismaModel> | null
lt?: number | Prisma.IntFieldRefInput<$PrismaModel>
lte?: number | Prisma.IntFieldRefInput<$PrismaModel>
gt?: number | Prisma.IntFieldRefInput<$PrismaModel>
gte?: number | Prisma.IntFieldRefInput<$PrismaModel>
not?: Prisma.NestedIntNullableFilter<$PrismaModel> | number | null
}
export type IntNullableWithAggregatesFilter<$PrismaModel = never> = {
equals?: number | Prisma.IntFieldRefInput<$PrismaModel> | null
in?: number[] | Prisma.ListIntFieldRefInput<$PrismaModel> | null
notIn?: number[] | Prisma.ListIntFieldRefInput<$PrismaModel> | null
lt?: number | Prisma.IntFieldRefInput<$PrismaModel>
lte?: number | Prisma.IntFieldRefInput<$PrismaModel>
gt?: number | Prisma.IntFieldRefInput<$PrismaModel>
gte?: number | Prisma.IntFieldRefInput<$PrismaModel>
not?: Prisma.NestedIntNullableWithAggregatesFilter<$PrismaModel> | number | null
_count?: Prisma.NestedIntNullableFilter<$PrismaModel>
_avg?: Prisma.NestedFloatNullableFilter<$PrismaModel>
_sum?: Prisma.NestedIntNullableFilter<$PrismaModel>
_min?: Prisma.NestedIntNullableFilter<$PrismaModel>
_max?: Prisma.NestedIntNullableFilter<$PrismaModel>
}
export type NestedIntFilter<$PrismaModel = never> = {
equals?: number | Prisma.IntFieldRefInput<$PrismaModel>
in?: number[] | Prisma.ListIntFieldRefInput<$PrismaModel>
notIn?: number[] | Prisma.ListIntFieldRefInput<$PrismaModel>
lt?: number | Prisma.IntFieldRefInput<$PrismaModel>
lte?: number | Prisma.IntFieldRefInput<$PrismaModel>
gt?: number | Prisma.IntFieldRefInput<$PrismaModel>
gte?: number | Prisma.IntFieldRefInput<$PrismaModel>
not?: Prisma.NestedIntFilter<$PrismaModel> | number
}
export type NestedDateTimeNullableFilter<$PrismaModel = never> = {
equals?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel> | null
in?: Date[] | string[] | Prisma.ListDateTimeFieldRefInput<$PrismaModel> | null
notIn?: Date[] | string[] | Prisma.ListDateTimeFieldRefInput<$PrismaModel> | null
lt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
lte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
gt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
gte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
not?: Prisma.NestedDateTimeNullableFilter<$PrismaModel> | Date | string | null
}
export type NestedStringNullableFilter<$PrismaModel = never> = {
equals?: string | Prisma.StringFieldRefInput<$PrismaModel> | null
in?: string[] | Prisma.ListStringFieldRefInput<$PrismaModel> | null
notIn?: string[] | Prisma.ListStringFieldRefInput<$PrismaModel> | null
lt?: string | Prisma.StringFieldRefInput<$PrismaModel>
lte?: string | Prisma.StringFieldRefInput<$PrismaModel>
gt?: string | Prisma.StringFieldRefInput<$PrismaModel>
gte?: string | Prisma.StringFieldRefInput<$PrismaModel>
contains?: string | Prisma.StringFieldRefInput<$PrismaModel>
startsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>
endsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>
not?: Prisma.NestedStringNullableFilter<$PrismaModel> | string | null
}
export type NestedIntWithAggregatesFilter<$PrismaModel = never> = {
equals?: number | Prisma.IntFieldRefInput<$PrismaModel>
in?: number[] | Prisma.ListIntFieldRefInput<$PrismaModel>
notIn?: number[] | Prisma.ListIntFieldRefInput<$PrismaModel>
lt?: number | Prisma.IntFieldRefInput<$PrismaModel>
lte?: number | Prisma.IntFieldRefInput<$PrismaModel>
gt?: number | Prisma.IntFieldRefInput<$PrismaModel>
gte?: number | Prisma.IntFieldRefInput<$PrismaModel>
not?: Prisma.NestedIntWithAggregatesFilter<$PrismaModel> | number
_count?: Prisma.NestedIntFilter<$PrismaModel>
_avg?: Prisma.NestedFloatFilter<$PrismaModel>
_sum?: Prisma.NestedIntFilter<$PrismaModel>
_min?: Prisma.NestedIntFilter<$PrismaModel>
_max?: Prisma.NestedIntFilter<$PrismaModel>
}
export type NestedFloatFilter<$PrismaModel = never> = {
equals?: number | Prisma.FloatFieldRefInput<$PrismaModel>
in?: number[] | Prisma.ListFloatFieldRefInput<$PrismaModel>
notIn?: number[] | Prisma.ListFloatFieldRefInput<$PrismaModel>
lt?: number | Prisma.FloatFieldRefInput<$PrismaModel>
lte?: number | Prisma.FloatFieldRefInput<$PrismaModel>
gt?: number | Prisma.FloatFieldRefInput<$PrismaModel>
gte?: number | Prisma.FloatFieldRefInput<$PrismaModel>
not?: Prisma.NestedFloatFilter<$PrismaModel> | number
}
export type NestedDateTimeNullableWithAggregatesFilter<$PrismaModel = never> = {
equals?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel> | null
in?: Date[] | string[] | Prisma.ListDateTimeFieldRefInput<$PrismaModel> | null
notIn?: Date[] | string[] | Prisma.ListDateTimeFieldRefInput<$PrismaModel> | null
lt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
lte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
gt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
gte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
not?: Prisma.NestedDateTimeNullableWithAggregatesFilter<$PrismaModel> | Date | string | null
_count?: Prisma.NestedIntNullableFilter<$PrismaModel>
_min?: Prisma.NestedDateTimeNullableFilter<$PrismaModel>
_max?: Prisma.NestedDateTimeNullableFilter<$PrismaModel>
}
export type NestedIntNullableFilter<$PrismaModel = never> = {
equals?: number | Prisma.IntFieldRefInput<$PrismaModel> | null
in?: number[] | Prisma.ListIntFieldRefInput<$PrismaModel> | null
notIn?: number[] | Prisma.ListIntFieldRefInput<$PrismaModel> | null
lt?: number | Prisma.IntFieldRefInput<$PrismaModel>
lte?: number | Prisma.IntFieldRefInput<$PrismaModel>
gt?: number | Prisma.IntFieldRefInput<$PrismaModel>
gte?: number | Prisma.IntFieldRefInput<$PrismaModel>
not?: Prisma.NestedIntNullableFilter<$PrismaModel> | number | null
}
export type NestedStringNullableWithAggregatesFilter<$PrismaModel = never> = {
equals?: string | Prisma.StringFieldRefInput<$PrismaModel> | null
in?: string[] | Prisma.ListStringFieldRefInput<$PrismaModel> | null
notIn?: string[] | Prisma.ListStringFieldRefInput<$PrismaModel> | null
lt?: string | Prisma.StringFieldRefInput<$PrismaModel>
lte?: string | Prisma.StringFieldRefInput<$PrismaModel>
gt?: string | Prisma.StringFieldRefInput<$PrismaModel>
gte?: string | Prisma.StringFieldRefInput<$PrismaModel>
contains?: string | Prisma.StringFieldRefInput<$PrismaModel>
startsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>
endsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>
not?: Prisma.NestedStringNullableWithAggregatesFilter<$PrismaModel> | string | null
_count?: Prisma.NestedIntNullableFilter<$PrismaModel>
_min?: Prisma.NestedStringNullableFilter<$PrismaModel>
_max?: Prisma.NestedStringNullableFilter<$PrismaModel>
}
export type NestedJsonNullableFilter<$PrismaModel = never> =
| Prisma.PatchUndefined<
Prisma.Either<Required<NestedJsonNullableFilterBase<$PrismaModel>>, Exclude<keyof Required<NestedJsonNullableFilterBase<$PrismaModel>>, 'path'>>,
Required<NestedJsonNullableFilterBase<$PrismaModel>>
>
| Prisma.OptionalFlat<Omit<Required<NestedJsonNullableFilterBase<$PrismaModel>>, 'path'>>
export type NestedJsonNullableFilterBase<$PrismaModel = never> = {
equals?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel> | Prisma.JsonNullValueFilter
path?: string[]
mode?: Prisma.QueryMode | Prisma.EnumQueryModeFieldRefInput<$PrismaModel>
string_contains?: string | Prisma.StringFieldRefInput<$PrismaModel>
string_starts_with?: string | Prisma.StringFieldRefInput<$PrismaModel>
string_ends_with?: string | Prisma.StringFieldRefInput<$PrismaModel>
array_starts_with?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel> | null
array_ends_with?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel> | null
array_contains?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel> | null
lt?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel>
lte?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel>
gt?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel>
gte?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel>
not?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel> | Prisma.JsonNullValueFilter
}
export type NestedIntNullableWithAggregatesFilter<$PrismaModel = never> = {
equals?: number | Prisma.IntFieldRefInput<$PrismaModel> | null
in?: number[] | Prisma.ListIntFieldRefInput<$PrismaModel> | null
notIn?: number[] | Prisma.ListIntFieldRefInput<$PrismaModel> | null
lt?: number | Prisma.IntFieldRefInput<$PrismaModel>
lte?: number | Prisma.IntFieldRefInput<$PrismaModel>
gt?: number | Prisma.IntFieldRefInput<$PrismaModel>
gte?: number | Prisma.IntFieldRefInput<$PrismaModel>
not?: Prisma.NestedIntNullableWithAggregatesFilter<$PrismaModel> | number | null
_count?: Prisma.NestedIntNullableFilter<$PrismaModel>
_avg?: Prisma.NestedFloatNullableFilter<$PrismaModel>
_sum?: Prisma.NestedIntNullableFilter<$PrismaModel>
_min?: Prisma.NestedIntNullableFilter<$PrismaModel>
_max?: Prisma.NestedIntNullableFilter<$PrismaModel>
}
export type NestedFloatNullableFilter<$PrismaModel = never> = {
equals?: number | Prisma.FloatFieldRefInput<$PrismaModel> | null
in?: number[] | Prisma.ListFloatFieldRefInput<$PrismaModel> | null
notIn?: number[] | Prisma.ListFloatFieldRefInput<$PrismaModel> | null
lt?: number | Prisma.FloatFieldRefInput<$PrismaModel>
lte?: number | Prisma.FloatFieldRefInput<$PrismaModel>
gt?: number | Prisma.FloatFieldRefInput<$PrismaModel>
gte?: number | Prisma.FloatFieldRefInput<$PrismaModel>
not?: Prisma.NestedFloatNullableFilter<$PrismaModel> | number | null
}

View File

@ -0,0 +1,15 @@
/* !!! This is code generated by Prisma. Do not edit directly. !!! */
/* eslint-disable */
// biome-ignore-all lint: generated file
// @ts-nocheck
/*
* This file exports all enum related types from the schema.
*
* 🟢 You can import this file directly.
*/
// This file is empty because there are no enums in the schema.
export {}

View File

@ -0,0 +1,210 @@
/* !!! This is code generated by Prisma. Do not edit directly. !!! */
/* eslint-disable */
// biome-ignore-all lint: generated file
// @ts-nocheck
/*
* WARNING: This is an internal file that is subject to change!
*
* 🛑 Under no circumstances should you import this file directly! 🛑
*
* Please import the `PrismaClient` class from the `client.ts` file instead.
*/
import * as runtime from "@prisma/client/runtime/client"
import type * as Prisma from "./prismaNamespace"
const config: runtime.GetPrismaClientConfig = {
"previewFeatures": [],
"clientVersion": "7.2.0",
"engineVersion": "0c8ef2ce45c83248ab3df073180d5eda9e8be7a3",
"activeProvider": "postgresql",
"inlineSchema": "generator client {\n provider = \"prisma-client\"\n output = \"../lib/prisma/client\"\n}\n\ndatasource db {\n provider = \"postgresql\"\n}\n\nmodel activity_logs {\n id Int @id @default(autoincrement())\n timestamp DateTime? @db.Timestamp(6)\n status String? @db.VarChar\n confidence String? @db.VarChar\n details Json?\n\n @@index([id], map: \"ix_activity_logs_id\")\n}\n\nmodel training_menus {\n id Int @id @default(autoincrement())\n name String? @db.VarChar\n exercises Json?\n created_at DateTime? @db.Timestamp(6)\n user_recaps user_recaps[]\n\n @@index([id], map: \"ix_training_menus_id\")\n @@index([name], map: \"ix_training_menus_name\")\n}\n\nmodel user_recaps {\n id Int @id @default(autoincrement())\n menu_id Int?\n summary Json?\n completed_at DateTime? @db.Timestamp(6)\n training_menus training_menus? @relation(fields: [menu_id], references: [id], onDelete: NoAction, onUpdate: NoAction)\n\n @@index([id], map: \"ix_user_recaps_id\")\n}\n",
"runtimeDataModel": {
"models": {},
"enums": {},
"types": {}
}
}
config.runtimeDataModel = JSON.parse("{\"models\":{\"activity_logs\":{\"fields\":[{\"name\":\"id\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"timestamp\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"status\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"confidence\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"details\",\"kind\":\"scalar\",\"type\":\"Json\"}],\"dbName\":null},\"training_menus\":{\"fields\":[{\"name\":\"id\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"name\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"exercises\",\"kind\":\"scalar\",\"type\":\"Json\"},{\"name\":\"created_at\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"user_recaps\",\"kind\":\"object\",\"type\":\"user_recaps\",\"relationName\":\"training_menusTouser_recaps\"}],\"dbName\":null},\"user_recaps\":{\"fields\":[{\"name\":\"id\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"menu_id\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"summary\",\"kind\":\"scalar\",\"type\":\"Json\"},{\"name\":\"completed_at\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"training_menus\",\"kind\":\"object\",\"type\":\"training_menus\",\"relationName\":\"training_menusTouser_recaps\"}],\"dbName\":null}},\"enums\":{},\"types\":{}}")
async function decodeBase64AsWasm(wasmBase64: string): Promise<WebAssembly.Module> {
const { Buffer } = await import('node:buffer')
const wasmArray = Buffer.from(wasmBase64, 'base64')
return new WebAssembly.Module(wasmArray)
}
config.compilerWasm = {
getRuntime: async () => await import("@prisma/client/runtime/query_compiler_bg.postgresql.mjs"),
getQueryCompilerWasmModule: async () => {
const { wasm } = await import("@prisma/client/runtime/query_compiler_bg.postgresql.wasm-base64.mjs")
return await decodeBase64AsWasm(wasm)
}
}
export type LogOptions<ClientOptions extends Prisma.PrismaClientOptions> =
'log' extends keyof ClientOptions ? ClientOptions['log'] extends Array<Prisma.LogLevel | Prisma.LogDefinition> ? Prisma.GetEvents<ClientOptions['log']> : never : never
export interface PrismaClientConstructor {
/**
* ## Prisma Client
*
* Type-safe database client for TypeScript
* @example
* ```
* const prisma = new PrismaClient()
* // Fetch zero or more Activity_logs
* const activity_logs = await prisma.activity_logs.findMany()
* ```
*
* Read more in our [docs](https://pris.ly/d/client).
*/
new <
Options extends Prisma.PrismaClientOptions = Prisma.PrismaClientOptions,
LogOpts extends LogOptions<Options> = LogOptions<Options>,
OmitOpts extends Prisma.PrismaClientOptions['omit'] = Options extends { omit: infer U } ? U : Prisma.PrismaClientOptions['omit'],
ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs
>(options: Prisma.Subset<Options, Prisma.PrismaClientOptions> ): PrismaClient<LogOpts, OmitOpts, ExtArgs>
}
/**
* ## Prisma Client
*
* Type-safe database client for TypeScript
* @example
* ```
* const prisma = new PrismaClient()
* // Fetch zero or more Activity_logs
* const activity_logs = await prisma.activity_logs.findMany()
* ```
*
* Read more in our [docs](https://pris.ly/d/client).
*/
export interface PrismaClient<
in LogOpts extends Prisma.LogLevel = never,
in out OmitOpts extends Prisma.PrismaClientOptions['omit'] = undefined,
in out ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs
> {
[K: symbol]: { types: Prisma.TypeMap<ExtArgs>['other'] }
$on<V extends LogOpts>(eventType: V, callback: (event: V extends 'query' ? Prisma.QueryEvent : Prisma.LogEvent) => void): PrismaClient;
/**
* Connect with the database
*/
$connect(): runtime.Types.Utils.JsPromise<void>;
/**
* Disconnect from the database
*/
$disconnect(): runtime.Types.Utils.JsPromise<void>;
/**
* Executes a prepared raw query and returns the number of affected rows.
* @example
* ```
* const result = await prisma.$executeRaw`UPDATE User SET cool = ${true} WHERE email = ${'user@email.com'};`
* ```
*
* Read more in our [docs](https://pris.ly/d/raw-queries).
*/
$executeRaw<T = unknown>(query: TemplateStringsArray | Prisma.Sql, ...values: any[]): Prisma.PrismaPromise<number>;
/**
* Executes a raw query and returns the number of affected rows.
* Susceptible to SQL injections, see documentation.
* @example
* ```
* const result = await prisma.$executeRawUnsafe('UPDATE User SET cool = $1 WHERE email = $2 ;', true, 'user@email.com')
* ```
*
* Read more in our [docs](https://pris.ly/d/raw-queries).
*/
$executeRawUnsafe<T = unknown>(query: string, ...values: any[]): Prisma.PrismaPromise<number>;
/**
* Performs a prepared raw query and returns the `SELECT` data.
* @example
* ```
* const result = await prisma.$queryRaw`SELECT * FROM User WHERE id = ${1} OR email = ${'user@email.com'};`
* ```
*
* Read more in our [docs](https://pris.ly/d/raw-queries).
*/
$queryRaw<T = unknown>(query: TemplateStringsArray | Prisma.Sql, ...values: any[]): Prisma.PrismaPromise<T>;
/**
* Performs a raw query and returns the `SELECT` data.
* Susceptible to SQL injections, see documentation.
* @example
* ```
* const result = await prisma.$queryRawUnsafe('SELECT * FROM User WHERE id = $1 OR email = $2;', 1, 'user@email.com')
* ```
*
* Read more in our [docs](https://pris.ly/d/raw-queries).
*/
$queryRawUnsafe<T = unknown>(query: string, ...values: any[]): Prisma.PrismaPromise<T>;
/**
* Allows the running of a sequence of read/write operations that are guaranteed to either succeed or fail as a whole.
* @example
* ```
* const [george, bob, alice] = await prisma.$transaction([
* prisma.user.create({ data: { name: 'George' } }),
* prisma.user.create({ data: { name: 'Bob' } }),
* prisma.user.create({ data: { name: 'Alice' } }),
* ])
* ```
*
* Read more in our [docs](https://www.prisma.io/docs/concepts/components/prisma-client/transactions).
*/
$transaction<P extends Prisma.PrismaPromise<any>[]>(arg: [...P], options?: { isolationLevel?: Prisma.TransactionIsolationLevel }): runtime.Types.Utils.JsPromise<runtime.Types.Utils.UnwrapTuple<P>>
$transaction<R>(fn: (prisma: Omit<PrismaClient, runtime.ITXClientDenyList>) => runtime.Types.Utils.JsPromise<R>, options?: { maxWait?: number, timeout?: number, isolationLevel?: Prisma.TransactionIsolationLevel }): runtime.Types.Utils.JsPromise<R>
$extends: runtime.Types.Extensions.ExtendsHook<"extends", Prisma.TypeMapCb<OmitOpts>, ExtArgs, runtime.Types.Utils.Call<Prisma.TypeMapCb<OmitOpts>, {
extArgs: ExtArgs
}>>
/**
* `prisma.activity_logs`: Exposes CRUD operations for the **activity_logs** model.
* Example usage:
* ```ts
* // Fetch zero or more Activity_logs
* const activity_logs = await prisma.activity_logs.findMany()
* ```
*/
get activity_logs(): Prisma.activity_logsDelegate<ExtArgs, { omit: OmitOpts }>;
/**
* `prisma.training_menus`: Exposes CRUD operations for the **training_menus** model.
* Example usage:
* ```ts
* // Fetch zero or more Training_menus
* const training_menus = await prisma.training_menus.findMany()
* ```
*/
get training_menus(): Prisma.training_menusDelegate<ExtArgs, { omit: OmitOpts }>;
/**
* `prisma.user_recaps`: Exposes CRUD operations for the **user_recaps** model.
* Example usage:
* ```ts
* // Fetch zero or more User_recaps
* const user_recaps = await prisma.user_recaps.findMany()
* ```
*/
get user_recaps(): Prisma.user_recapsDelegate<ExtArgs, { omit: OmitOpts }>;
}
export function getPrismaClientClass(): PrismaClientConstructor {
return runtime.getPrismaClient(config) as unknown as PrismaClientConstructor
}

View File

@ -0,0 +1,977 @@
/* !!! This is code generated by Prisma. Do not edit directly. !!! */
/* eslint-disable */
// biome-ignore-all lint: generated file
// @ts-nocheck
/*
* WARNING: This is an internal file that is subject to change!
*
* 🛑 Under no circumstances should you import this file directly! 🛑
*
* All exports from this file are wrapped under a `Prisma` namespace object in the client.ts file.
* While this enables partial backward compatibility, it is not part of the stable public API.
*
* If you are looking for your Models, Enums, and Input Types, please import them from the respective
* model files in the `model` directory!
*/
import * as runtime from "@prisma/client/runtime/client"
import type * as Prisma from "../models"
import { type PrismaClient } from "./class"
export type * from '../models'
export type DMMF = typeof runtime.DMMF
export type PrismaPromise<T> = runtime.Types.Public.PrismaPromise<T>
/**
* Prisma Errors
*/
export const PrismaClientKnownRequestError = runtime.PrismaClientKnownRequestError
export type PrismaClientKnownRequestError = runtime.PrismaClientKnownRequestError
export const PrismaClientUnknownRequestError = runtime.PrismaClientUnknownRequestError
export type PrismaClientUnknownRequestError = runtime.PrismaClientUnknownRequestError
export const PrismaClientRustPanicError = runtime.PrismaClientRustPanicError
export type PrismaClientRustPanicError = runtime.PrismaClientRustPanicError
export const PrismaClientInitializationError = runtime.PrismaClientInitializationError
export type PrismaClientInitializationError = runtime.PrismaClientInitializationError
export const PrismaClientValidationError = runtime.PrismaClientValidationError
export type PrismaClientValidationError = runtime.PrismaClientValidationError
/**
* Re-export of sql-template-tag
*/
export const sql = runtime.sqltag
export const empty = runtime.empty
export const join = runtime.join
export const raw = runtime.raw
export const Sql = runtime.Sql
export type Sql = runtime.Sql
/**
* Decimal.js
*/
export const Decimal = runtime.Decimal
export type Decimal = runtime.Decimal
export type DecimalJsLike = runtime.DecimalJsLike
/**
* Extensions
*/
export type Extension = runtime.Types.Extensions.UserArgs
export const getExtensionContext = runtime.Extensions.getExtensionContext
export type Args<T, F extends runtime.Operation> = runtime.Types.Public.Args<T, F>
export type Payload<T, F extends runtime.Operation = never> = runtime.Types.Public.Payload<T, F>
export type Result<T, A, F extends runtime.Operation> = runtime.Types.Public.Result<T, A, F>
export type Exact<A, W> = runtime.Types.Public.Exact<A, W>
export type PrismaVersion = {
client: string
engine: string
}
/**
* Prisma Client JS version: 7.2.0
* Query Engine version: 0c8ef2ce45c83248ab3df073180d5eda9e8be7a3
*/
export const prismaVersion: PrismaVersion = {
client: "7.2.0",
engine: "0c8ef2ce45c83248ab3df073180d5eda9e8be7a3"
}
/**
* Utility Types
*/
export type Bytes = runtime.Bytes
export type JsonObject = runtime.JsonObject
export type JsonArray = runtime.JsonArray
export type JsonValue = runtime.JsonValue
export type InputJsonObject = runtime.InputJsonObject
export type InputJsonArray = runtime.InputJsonArray
export type InputJsonValue = runtime.InputJsonValue
export const NullTypes = {
DbNull: runtime.NullTypes.DbNull as (new (secret: never) => typeof runtime.DbNull),
JsonNull: runtime.NullTypes.JsonNull as (new (secret: never) => typeof runtime.JsonNull),
AnyNull: runtime.NullTypes.AnyNull as (new (secret: never) => typeof runtime.AnyNull),
}
/**
* Helper for filtering JSON entries that have `null` on the database (empty on the db)
*
* @see https://www.prisma.io/docs/concepts/components/prisma-client/working-with-fields/working-with-json-fields#filtering-on-a-json-field
*/
export const DbNull = runtime.DbNull
/**
* Helper for filtering JSON entries that have JSON `null` values (not empty on the db)
*
* @see https://www.prisma.io/docs/concepts/components/prisma-client/working-with-fields/working-with-json-fields#filtering-on-a-json-field
*/
export const JsonNull = runtime.JsonNull
/**
* Helper for filtering JSON entries that are `Prisma.DbNull` or `Prisma.JsonNull`
*
* @see https://www.prisma.io/docs/concepts/components/prisma-client/working-with-fields/working-with-json-fields#filtering-on-a-json-field
*/
export const AnyNull = runtime.AnyNull
type SelectAndInclude = {
select: any
include: any
}
type SelectAndOmit = {
select: any
omit: any
}
/**
* From T, pick a set of properties whose keys are in the union K
*/
type Prisma__Pick<T, K extends keyof T> = {
[P in K]: T[P];
};
export type Enumerable<T> = T | Array<T>;
/**
* Subset
* @desc From `T` pick properties that exist in `U`. Simple version of Intersection
*/
export type Subset<T, U> = {
[key in keyof T]: key extends keyof U ? T[key] : never;
};
/**
* SelectSubset
* @desc From `T` pick properties that exist in `U`. Simple version of Intersection.
* Additionally, it validates, if both select and include are present. If the case, it errors.
*/
export type SelectSubset<T, U> = {
[key in keyof T]: key extends keyof U ? T[key] : never
} &
(T extends SelectAndInclude
? 'Please either choose `select` or `include`.'
: T extends SelectAndOmit
? 'Please either choose `select` or `omit`.'
: {})
/**
* Subset + Intersection
* @desc From `T` pick properties that exist in `U` and intersect `K`
*/
export type SubsetIntersection<T, U, K> = {
[key in keyof T]: key extends keyof U ? T[key] : never
} &
K
type Without<T, U> = { [P in Exclude<keyof T, keyof U>]?: never };
/**
* XOR is needed to have a real mutually exclusive union type
* https://stackoverflow.com/questions/42123407/does-typescript-support-mutually-exclusive-types
*/
export type XOR<T, U> =
T extends object ?
U extends object ?
(Without<T, U> & U) | (Without<U, T> & T)
: U : T
/**
* Is T a Record?
*/
type IsObject<T extends any> = T extends Array<any>
? False
: T extends Date
? False
: T extends Uint8Array
? False
: T extends BigInt
? False
: T extends object
? True
: False
/**
* If it's T[], return T
*/
export type UnEnumerate<T extends unknown> = T extends Array<infer U> ? U : T
/**
* From ts-toolbelt
*/
type __Either<O extends object, K extends Key> = Omit<O, K> &
{
// Merge all but K
[P in K]: Prisma__Pick<O, P & keyof O> // With K possibilities
}[K]
type EitherStrict<O extends object, K extends Key> = Strict<__Either<O, K>>
type EitherLoose<O extends object, K extends Key> = ComputeRaw<__Either<O, K>>
type _Either<
O extends object,
K extends Key,
strict extends Boolean
> = {
1: EitherStrict<O, K>
0: EitherLoose<O, K>
}[strict]
export type Either<
O extends object,
K extends Key,
strict extends Boolean = 1
> = O extends unknown ? _Either<O, K, strict> : never
export type Union = any
export type PatchUndefined<O extends object, O1 extends object> = {
[K in keyof O]: O[K] extends undefined ? At<O1, K> : O[K]
} & {}
/** Helper Types for "Merge" **/
export type IntersectOf<U extends Union> = (
U extends unknown ? (k: U) => void : never
) extends (k: infer I) => void
? I
: never
export type Overwrite<O extends object, O1 extends object> = {
[K in keyof O]: K extends keyof O1 ? O1[K] : O[K];
} & {};
type _Merge<U extends object> = IntersectOf<Overwrite<U, {
[K in keyof U]-?: At<U, K>;
}>>;
type Key = string | number | symbol;
type AtStrict<O extends object, K extends Key> = O[K & keyof O];
type AtLoose<O extends object, K extends Key> = O extends unknown ? AtStrict<O, K> : never;
export type At<O extends object, K extends Key, strict extends Boolean = 1> = {
1: AtStrict<O, K>;
0: AtLoose<O, K>;
}[strict];
export type ComputeRaw<A extends any> = A extends Function ? A : {
[K in keyof A]: A[K];
} & {};
export type OptionalFlat<O> = {
[K in keyof O]?: O[K];
} & {};
type _Record<K extends keyof any, T> = {
[P in K]: T;
};
// cause typescript not to expand types and preserve names
type NoExpand<T> = T extends unknown ? T : never;
// this type assumes the passed object is entirely optional
export type AtLeast<O extends object, K extends string> = NoExpand<
O extends unknown
? | (K extends keyof O ? { [P in K]: O[P] } & O : O)
| {[P in keyof O as P extends K ? P : never]-?: O[P]} & O
: never>;
type _Strict<U, _U = U> = U extends unknown ? U & OptionalFlat<_Record<Exclude<Keys<_U>, keyof U>, never>> : never;
export type Strict<U extends object> = ComputeRaw<_Strict<U>>;
/** End Helper Types for "Merge" **/
export type Merge<U extends object> = ComputeRaw<_Merge<Strict<U>>>;
export type Boolean = True | False
export type True = 1
export type False = 0
export type Not<B extends Boolean> = {
0: 1
1: 0
}[B]
export type Extends<A1 extends any, A2 extends any> = [A1] extends [never]
? 0 // anything `never` is false
: A1 extends A2
? 1
: 0
export type Has<U extends Union, U1 extends Union> = Not<
Extends<Exclude<U1, U>, U1>
>
export type Or<B1 extends Boolean, B2 extends Boolean> = {
0: {
0: 0
1: 1
}
1: {
0: 1
1: 1
}
}[B1][B2]
export type Keys<U extends Union> = U extends unknown ? keyof U : never
export type GetScalarType<T, O> = O extends object ? {
[P in keyof T]: P extends keyof O
? O[P]
: never
} : never
type FieldPaths<
T,
U = Omit<T, '_avg' | '_sum' | '_count' | '_min' | '_max'>
> = IsObject<T> extends True ? U : T
export type GetHavingFields<T> = {
[K in keyof T]: Or<
Or<Extends<'OR', K>, Extends<'AND', K>>,
Extends<'NOT', K>
> extends True
? // infer is only needed to not hit TS limit
// based on the brilliant idea of Pierre-Antoine Mills
// https://github.com/microsoft/TypeScript/issues/30188#issuecomment-478938437
T[K] extends infer TK
? GetHavingFields<UnEnumerate<TK> extends object ? Merge<UnEnumerate<TK>> : never>
: never
: {} extends FieldPaths<T[K]>
? never
: K
}[keyof T]
/**
* Convert tuple to union
*/
type _TupleToUnion<T> = T extends (infer E)[] ? E : never
type TupleToUnion<K extends readonly any[]> = _TupleToUnion<K>
export type MaybeTupleToUnion<T> = T extends any[] ? TupleToUnion<T> : T
/**
* Like `Pick`, but additionally can also accept an array of keys
*/
export type PickEnumerable<T, K extends Enumerable<keyof T> | keyof T> = Prisma__Pick<T, MaybeTupleToUnion<K>>
/**
* Exclude all keys with underscores
*/
export type ExcludeUnderscoreKeys<T extends string> = T extends `_${string}` ? never : T
export type FieldRef<Model, FieldType> = runtime.FieldRef<Model, FieldType>
type FieldRefInputType<Model, FieldType> = Model extends never ? never : FieldRef<Model, FieldType>
export const ModelName = {
activity_logs: 'activity_logs',
training_menus: 'training_menus',
user_recaps: 'user_recaps'
} as const
export type ModelName = (typeof ModelName)[keyof typeof ModelName]
export interface TypeMapCb<GlobalOmitOptions = {}> extends runtime.Types.Utils.Fn<{extArgs: runtime.Types.Extensions.InternalArgs }, runtime.Types.Utils.Record<string, any>> {
returns: TypeMap<this['params']['extArgs'], GlobalOmitOptions>
}
export type TypeMap<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs, GlobalOmitOptions = {}> = {
globalOmitOptions: {
omit: GlobalOmitOptions
}
meta: {
modelProps: "activity_logs" | "training_menus" | "user_recaps"
txIsolationLevel: TransactionIsolationLevel
}
model: {
activity_logs: {
payload: Prisma.$activity_logsPayload<ExtArgs>
fields: Prisma.activity_logsFieldRefs
operations: {
findUnique: {
args: Prisma.activity_logsFindUniqueArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$activity_logsPayload> | null
}
findUniqueOrThrow: {
args: Prisma.activity_logsFindUniqueOrThrowArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$activity_logsPayload>
}
findFirst: {
args: Prisma.activity_logsFindFirstArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$activity_logsPayload> | null
}
findFirstOrThrow: {
args: Prisma.activity_logsFindFirstOrThrowArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$activity_logsPayload>
}
findMany: {
args: Prisma.activity_logsFindManyArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$activity_logsPayload>[]
}
create: {
args: Prisma.activity_logsCreateArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$activity_logsPayload>
}
createMany: {
args: Prisma.activity_logsCreateManyArgs<ExtArgs>
result: BatchPayload
}
createManyAndReturn: {
args: Prisma.activity_logsCreateManyAndReturnArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$activity_logsPayload>[]
}
delete: {
args: Prisma.activity_logsDeleteArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$activity_logsPayload>
}
update: {
args: Prisma.activity_logsUpdateArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$activity_logsPayload>
}
deleteMany: {
args: Prisma.activity_logsDeleteManyArgs<ExtArgs>
result: BatchPayload
}
updateMany: {
args: Prisma.activity_logsUpdateManyArgs<ExtArgs>
result: BatchPayload
}
updateManyAndReturn: {
args: Prisma.activity_logsUpdateManyAndReturnArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$activity_logsPayload>[]
}
upsert: {
args: Prisma.activity_logsUpsertArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$activity_logsPayload>
}
aggregate: {
args: Prisma.Activity_logsAggregateArgs<ExtArgs>
result: runtime.Types.Utils.Optional<Prisma.AggregateActivity_logs>
}
groupBy: {
args: Prisma.activity_logsGroupByArgs<ExtArgs>
result: runtime.Types.Utils.Optional<Prisma.Activity_logsGroupByOutputType>[]
}
count: {
args: Prisma.activity_logsCountArgs<ExtArgs>
result: runtime.Types.Utils.Optional<Prisma.Activity_logsCountAggregateOutputType> | number
}
}
}
training_menus: {
payload: Prisma.$training_menusPayload<ExtArgs>
fields: Prisma.training_menusFieldRefs
operations: {
findUnique: {
args: Prisma.training_menusFindUniqueArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$training_menusPayload> | null
}
findUniqueOrThrow: {
args: Prisma.training_menusFindUniqueOrThrowArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$training_menusPayload>
}
findFirst: {
args: Prisma.training_menusFindFirstArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$training_menusPayload> | null
}
findFirstOrThrow: {
args: Prisma.training_menusFindFirstOrThrowArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$training_menusPayload>
}
findMany: {
args: Prisma.training_menusFindManyArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$training_menusPayload>[]
}
create: {
args: Prisma.training_menusCreateArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$training_menusPayload>
}
createMany: {
args: Prisma.training_menusCreateManyArgs<ExtArgs>
result: BatchPayload
}
createManyAndReturn: {
args: Prisma.training_menusCreateManyAndReturnArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$training_menusPayload>[]
}
delete: {
args: Prisma.training_menusDeleteArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$training_menusPayload>
}
update: {
args: Prisma.training_menusUpdateArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$training_menusPayload>
}
deleteMany: {
args: Prisma.training_menusDeleteManyArgs<ExtArgs>
result: BatchPayload
}
updateMany: {
args: Prisma.training_menusUpdateManyArgs<ExtArgs>
result: BatchPayload
}
updateManyAndReturn: {
args: Prisma.training_menusUpdateManyAndReturnArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$training_menusPayload>[]
}
upsert: {
args: Prisma.training_menusUpsertArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$training_menusPayload>
}
aggregate: {
args: Prisma.Training_menusAggregateArgs<ExtArgs>
result: runtime.Types.Utils.Optional<Prisma.AggregateTraining_menus>
}
groupBy: {
args: Prisma.training_menusGroupByArgs<ExtArgs>
result: runtime.Types.Utils.Optional<Prisma.Training_menusGroupByOutputType>[]
}
count: {
args: Prisma.training_menusCountArgs<ExtArgs>
result: runtime.Types.Utils.Optional<Prisma.Training_menusCountAggregateOutputType> | number
}
}
}
user_recaps: {
payload: Prisma.$user_recapsPayload<ExtArgs>
fields: Prisma.user_recapsFieldRefs
operations: {
findUnique: {
args: Prisma.user_recapsFindUniqueArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$user_recapsPayload> | null
}
findUniqueOrThrow: {
args: Prisma.user_recapsFindUniqueOrThrowArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$user_recapsPayload>
}
findFirst: {
args: Prisma.user_recapsFindFirstArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$user_recapsPayload> | null
}
findFirstOrThrow: {
args: Prisma.user_recapsFindFirstOrThrowArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$user_recapsPayload>
}
findMany: {
args: Prisma.user_recapsFindManyArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$user_recapsPayload>[]
}
create: {
args: Prisma.user_recapsCreateArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$user_recapsPayload>
}
createMany: {
args: Prisma.user_recapsCreateManyArgs<ExtArgs>
result: BatchPayload
}
createManyAndReturn: {
args: Prisma.user_recapsCreateManyAndReturnArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$user_recapsPayload>[]
}
delete: {
args: Prisma.user_recapsDeleteArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$user_recapsPayload>
}
update: {
args: Prisma.user_recapsUpdateArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$user_recapsPayload>
}
deleteMany: {
args: Prisma.user_recapsDeleteManyArgs<ExtArgs>
result: BatchPayload
}
updateMany: {
args: Prisma.user_recapsUpdateManyArgs<ExtArgs>
result: BatchPayload
}
updateManyAndReturn: {
args: Prisma.user_recapsUpdateManyAndReturnArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$user_recapsPayload>[]
}
upsert: {
args: Prisma.user_recapsUpsertArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$user_recapsPayload>
}
aggregate: {
args: Prisma.User_recapsAggregateArgs<ExtArgs>
result: runtime.Types.Utils.Optional<Prisma.AggregateUser_recaps>
}
groupBy: {
args: Prisma.user_recapsGroupByArgs<ExtArgs>
result: runtime.Types.Utils.Optional<Prisma.User_recapsGroupByOutputType>[]
}
count: {
args: Prisma.user_recapsCountArgs<ExtArgs>
result: runtime.Types.Utils.Optional<Prisma.User_recapsCountAggregateOutputType> | number
}
}
}
}
} & {
other: {
payload: any
operations: {
$executeRaw: {
args: [query: TemplateStringsArray | Sql, ...values: any[]],
result: any
}
$executeRawUnsafe: {
args: [query: string, ...values: any[]],
result: any
}
$queryRaw: {
args: [query: TemplateStringsArray | Sql, ...values: any[]],
result: any
}
$queryRawUnsafe: {
args: [query: string, ...values: any[]],
result: any
}
}
}
}
/**
* Enums
*/
export const TransactionIsolationLevel = runtime.makeStrictEnum({
ReadUncommitted: 'ReadUncommitted',
ReadCommitted: 'ReadCommitted',
RepeatableRead: 'RepeatableRead',
Serializable: 'Serializable'
} as const)
export type TransactionIsolationLevel = (typeof TransactionIsolationLevel)[keyof typeof TransactionIsolationLevel]
export const Activity_logsScalarFieldEnum = {
id: 'id',
timestamp: 'timestamp',
status: 'status',
confidence: 'confidence',
details: 'details'
} as const
export type Activity_logsScalarFieldEnum = (typeof Activity_logsScalarFieldEnum)[keyof typeof Activity_logsScalarFieldEnum]
export const Training_menusScalarFieldEnum = {
id: 'id',
name: 'name',
exercises: 'exercises',
created_at: 'created_at'
} as const
export type Training_menusScalarFieldEnum = (typeof Training_menusScalarFieldEnum)[keyof typeof Training_menusScalarFieldEnum]
export const User_recapsScalarFieldEnum = {
id: 'id',
menu_id: 'menu_id',
summary: 'summary',
completed_at: 'completed_at'
} as const
export type User_recapsScalarFieldEnum = (typeof User_recapsScalarFieldEnum)[keyof typeof User_recapsScalarFieldEnum]
export const SortOrder = {
asc: 'asc',
desc: 'desc'
} as const
export type SortOrder = (typeof SortOrder)[keyof typeof SortOrder]
export const NullableJsonNullValueInput = {
DbNull: DbNull,
JsonNull: JsonNull
} as const
export type NullableJsonNullValueInput = (typeof NullableJsonNullValueInput)[keyof typeof NullableJsonNullValueInput]
export const QueryMode = {
default: 'default',
insensitive: 'insensitive'
} as const
export type QueryMode = (typeof QueryMode)[keyof typeof QueryMode]
export const JsonNullValueFilter = {
DbNull: DbNull,
JsonNull: JsonNull,
AnyNull: AnyNull
} as const
export type JsonNullValueFilter = (typeof JsonNullValueFilter)[keyof typeof JsonNullValueFilter]
export const NullsOrder = {
first: 'first',
last: 'last'
} as const
export type NullsOrder = (typeof NullsOrder)[keyof typeof NullsOrder]
/**
* Field references
*/
/**
* Reference to a field of type 'Int'
*/
export type IntFieldRefInput<$PrismaModel> = FieldRefInputType<$PrismaModel, 'Int'>
/**
* Reference to a field of type 'Int[]'
*/
export type ListIntFieldRefInput<$PrismaModel> = FieldRefInputType<$PrismaModel, 'Int[]'>
/**
* Reference to a field of type 'DateTime'
*/
export type DateTimeFieldRefInput<$PrismaModel> = FieldRefInputType<$PrismaModel, 'DateTime'>
/**
* Reference to a field of type 'DateTime[]'
*/
export type ListDateTimeFieldRefInput<$PrismaModel> = FieldRefInputType<$PrismaModel, 'DateTime[]'>
/**
* Reference to a field of type 'String'
*/
export type StringFieldRefInput<$PrismaModel> = FieldRefInputType<$PrismaModel, 'String'>
/**
* Reference to a field of type 'String[]'
*/
export type ListStringFieldRefInput<$PrismaModel> = FieldRefInputType<$PrismaModel, 'String[]'>
/**
* Reference to a field of type 'Json'
*/
export type JsonFieldRefInput<$PrismaModel> = FieldRefInputType<$PrismaModel, 'Json'>
/**
* Reference to a field of type 'QueryMode'
*/
export type EnumQueryModeFieldRefInput<$PrismaModel> = FieldRefInputType<$PrismaModel, 'QueryMode'>
/**
* Reference to a field of type 'Float'
*/
export type FloatFieldRefInput<$PrismaModel> = FieldRefInputType<$PrismaModel, 'Float'>
/**
* Reference to a field of type 'Float[]'
*/
export type ListFloatFieldRefInput<$PrismaModel> = FieldRefInputType<$PrismaModel, 'Float[]'>
/**
* Batch Payload for updateMany & deleteMany & createMany
*/
export type BatchPayload = {
count: number
}
export const defineExtension = runtime.Extensions.defineExtension as unknown as runtime.Types.Extensions.ExtendsHook<"define", TypeMapCb, runtime.Types.Extensions.DefaultArgs>
export type DefaultPrismaClient = PrismaClient
export type ErrorFormat = 'pretty' | 'colorless' | 'minimal'
export type PrismaClientOptions = ({
/**
* Instance of a Driver Adapter, e.g., like one provided by `@prisma/adapter-pg`.
*/
adapter: runtime.SqlDriverAdapterFactory
accelerateUrl?: never
} | {
/**
* Prisma Accelerate URL allowing the client to connect through Accelerate instead of a direct database.
*/
accelerateUrl: string
adapter?: never
}) & {
/**
* @default "colorless"
*/
errorFormat?: ErrorFormat
/**
* @example
* ```
* // Shorthand for `emit: 'stdout'`
* log: ['query', 'info', 'warn', 'error']
*
* // Emit as events only
* log: [
* { emit: 'event', level: 'query' },
* { emit: 'event', level: 'info' },
* { emit: 'event', level: 'warn' }
* { emit: 'event', level: 'error' }
* ]
*
* / Emit as events and log to stdout
* og: [
* { emit: 'stdout', level: 'query' },
* { emit: 'stdout', level: 'info' },
* { emit: 'stdout', level: 'warn' }
* { emit: 'stdout', level: 'error' }
*
* ```
* Read more in our [docs](https://pris.ly/d/logging).
*/
log?: (LogLevel | LogDefinition)[]
/**
* The default values for transactionOptions
* maxWait ?= 2000
* timeout ?= 5000
*/
transactionOptions?: {
maxWait?: number
timeout?: number
isolationLevel?: TransactionIsolationLevel
}
/**
* Global configuration for omitting model fields by default.
*
* @example
* ```
* const prisma = new PrismaClient({
* omit: {
* user: {
* password: true
* }
* }
* })
* ```
*/
omit?: GlobalOmitConfig
/**
* SQL commenter plugins that add metadata to SQL queries as comments.
* Comments follow the sqlcommenter format: https://google.github.io/sqlcommenter/
*
* @example
* ```
* const prisma = new PrismaClient({
* adapter,
* comments: [
* traceContext(),
* queryInsights(),
* ],
* })
* ```
*/
comments?: runtime.SqlCommenterPlugin[]
}
export type GlobalOmitConfig = {
activity_logs?: Prisma.activity_logsOmit
training_menus?: Prisma.training_menusOmit
user_recaps?: Prisma.user_recapsOmit
}
/* Types for Logging */
export type LogLevel = 'info' | 'query' | 'warn' | 'error'
export type LogDefinition = {
level: LogLevel
emit: 'stdout' | 'event'
}
export type CheckIsLogLevel<T> = T extends LogLevel ? T : never;
export type GetLogType<T> = CheckIsLogLevel<
T extends LogDefinition ? T['level'] : T
>;
export type GetEvents<T extends any[]> = T extends Array<LogLevel | LogDefinition>
? GetLogType<T[number]>
: never;
export type QueryEvent = {
timestamp: Date
query: string
params: string
duration: number
target: string
}
export type LogEvent = {
timestamp: Date
message: string
target: string
}
/* End Types for Logging */
export type PrismaAction =
| 'findUnique'
| 'findUniqueOrThrow'
| 'findMany'
| 'findFirst'
| 'findFirstOrThrow'
| 'create'
| 'createMany'
| 'createManyAndReturn'
| 'update'
| 'updateMany'
| 'updateManyAndReturn'
| 'upsert'
| 'delete'
| 'deleteMany'
| 'executeRaw'
| 'queryRaw'
| 'aggregate'
| 'count'
| 'runCommandRaw'
| 'findRaw'
| 'groupBy'
/**
* `PrismaClient` proxy available in interactive transactions.
*/
export type TransactionClient = Omit<DefaultPrismaClient, runtime.ITXClientDenyList>

View File

@ -0,0 +1,145 @@
/* !!! This is code generated by Prisma. Do not edit directly. !!! */
/* eslint-disable */
// biome-ignore-all lint: generated file
// @ts-nocheck
/*
* WARNING: This is an internal file that is subject to change!
*
* 🛑 Under no circumstances should you import this file directly! 🛑
*
* All exports from this file are wrapped under a `Prisma` namespace object in the browser.ts file.
* While this enables partial backward compatibility, it is not part of the stable public API.
*
* If you are looking for your Models, Enums, and Input Types, please import them from the respective
* model files in the `model` directory!
*/
import * as runtime from "@prisma/client/runtime/index-browser"
export type * from '../models'
export type * from './prismaNamespace'
export const Decimal = runtime.Decimal
export const NullTypes = {
DbNull: runtime.NullTypes.DbNull as (new (secret: never) => typeof runtime.DbNull),
JsonNull: runtime.NullTypes.JsonNull as (new (secret: never) => typeof runtime.JsonNull),
AnyNull: runtime.NullTypes.AnyNull as (new (secret: never) => typeof runtime.AnyNull),
}
/**
* Helper for filtering JSON entries that have `null` on the database (empty on the db)
*
* @see https://www.prisma.io/docs/concepts/components/prisma-client/working-with-fields/working-with-json-fields#filtering-on-a-json-field
*/
export const DbNull = runtime.DbNull
/**
* Helper for filtering JSON entries that have JSON `null` values (not empty on the db)
*
* @see https://www.prisma.io/docs/concepts/components/prisma-client/working-with-fields/working-with-json-fields#filtering-on-a-json-field
*/
export const JsonNull = runtime.JsonNull
/**
* Helper for filtering JSON entries that are `Prisma.DbNull` or `Prisma.JsonNull`
*
* @see https://www.prisma.io/docs/concepts/components/prisma-client/working-with-fields/working-with-json-fields#filtering-on-a-json-field
*/
export const AnyNull = runtime.AnyNull
export const ModelName = {
activity_logs: 'activity_logs',
training_menus: 'training_menus',
user_recaps: 'user_recaps'
} as const
export type ModelName = (typeof ModelName)[keyof typeof ModelName]
/*
* Enums
*/
export const TransactionIsolationLevel = {
ReadUncommitted: 'ReadUncommitted',
ReadCommitted: 'ReadCommitted',
RepeatableRead: 'RepeatableRead',
Serializable: 'Serializable'
} as const
export type TransactionIsolationLevel = (typeof TransactionIsolationLevel)[keyof typeof TransactionIsolationLevel]
export const Activity_logsScalarFieldEnum = {
id: 'id',
timestamp: 'timestamp',
status: 'status',
confidence: 'confidence',
details: 'details'
} as const
export type Activity_logsScalarFieldEnum = (typeof Activity_logsScalarFieldEnum)[keyof typeof Activity_logsScalarFieldEnum]
export const Training_menusScalarFieldEnum = {
id: 'id',
name: 'name',
exercises: 'exercises',
created_at: 'created_at'
} as const
export type Training_menusScalarFieldEnum = (typeof Training_menusScalarFieldEnum)[keyof typeof Training_menusScalarFieldEnum]
export const User_recapsScalarFieldEnum = {
id: 'id',
menu_id: 'menu_id',
summary: 'summary',
completed_at: 'completed_at'
} as const
export type User_recapsScalarFieldEnum = (typeof User_recapsScalarFieldEnum)[keyof typeof User_recapsScalarFieldEnum]
export const SortOrder = {
asc: 'asc',
desc: 'desc'
} as const
export type SortOrder = (typeof SortOrder)[keyof typeof SortOrder]
export const NullableJsonNullValueInput = {
DbNull: 'DbNull',
JsonNull: 'JsonNull'
} as const
export type NullableJsonNullValueInput = (typeof NullableJsonNullValueInput)[keyof typeof NullableJsonNullValueInput]
export const QueryMode = {
default: 'default',
insensitive: 'insensitive'
} as const
export type QueryMode = (typeof QueryMode)[keyof typeof QueryMode]
export const JsonNullValueFilter = {
DbNull: 'DbNull',
JsonNull: 'JsonNull',
AnyNull: 'AnyNull'
} as const
export type JsonNullValueFilter = (typeof JsonNullValueFilter)[keyof typeof JsonNullValueFilter]
export const NullsOrder = {
first: 'first',
last: 'last'
} as const
export type NullsOrder = (typeof NullsOrder)[keyof typeof NullsOrder]

View File

@ -0,0 +1,14 @@
/* !!! This is code generated by Prisma. Do not edit directly. !!! */
/* eslint-disable */
// biome-ignore-all lint: generated file
// @ts-nocheck
/*
* This is a barrel export file for all models and their related types.
*
* 🟢 You can import this file directly.
*/
export type * from './models/activity_logs'
export type * from './models/training_menus'
export type * from './models/user_recaps'
export type * from './commonInputTypes'

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

32
next.config.ts Normal file
View File

@ -0,0 +1,32 @@
const nextConfig = {
// Transpile the detector wrapper (ESM) but let @mediapipe/pose be handled by alias or externals
transpilePackages: ['@tensorflow-models/pose-detection', '@/app/generated/client'],
experimental: {
esmExternals: "loose", // Allow mixing CJS/ESM
},
webpack: (config: any) => {
// Alias @mediapipe/pose to our shim which expects window.Pose
const path = require('path');
config.resolve.alias['@mediapipe/pose'] = path.resolve(__dirname, 'lib/mediapipe-shim.js');
config.resolve.extensionAlias = {
'.js': ['.ts', '.tsx', '.js', '.jsx'],
};
return config;
},
async headers() {
return [
{
source: "/api/:path*",
headers: [
{ key: "Access-Control-Allow-Origin", value: "*" },
{ key: "Access-Control-Allow-Methods", value: "GET, POST, PUT, DELETE, OPTIONS" },
{ key: "Access-Control-Allow-Headers", value: "Content-Type, Authorization" },
]
}
];
}
};
export default nextConfig;

8377
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

48
package.json Normal file
View File

@ -0,0 +1,48 @@
{
"name": "straps-dev",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "concurrently \"next dev --webpack\" \"prisma studio\"",
"build": "prisma generate && next build --webpack",
"start": "next start",
"lint": "eslint",
"db:studio": "prisma studio"
},
"dependencies": {
"@mediapipe/pose": "^0.5.1675469404",
"@mediapipe/tasks-vision": "^0.10.22-rc.20250304",
"@prisma/client": "^6.19.1",
"@tensorflow-models/pose-detection": "^2.1.3",
"@tensorflow/tfjs": "^4.22.0",
"@tensorflow/tfjs-backend-webgl": "^4.22.0",
"axios": "^1.13.2",
"bcryptjs": "^3.0.3",
"clsx": "^2.1.1",
"framer-motion": "^12.23.26",
"lucide-react": "^0.562.0",
"next": "^16.1.1",
"next-auth": "^5.0.0-beta.30",
"prisma": "^6.19.1",
"react": "^19.2.3",
"react-dom": "^19.2.3",
"tailwind-merge": "^3.4.0",
"zod": "^4.2.1"
},
"devDependencies": {
"@tailwindcss/postcss": "^4",
"@types/bcryptjs": "^2.4.6",
"@types/node": "^20",
"@types/react": "^19",
"@types/react-dom": "^19",
"concurrently": "^9.2.1",
"eslint": "^9",
"eslint-config-next": "16.1.0",
"tailwindcss": "^4",
"tsx": "^4.21.0",
"typescript": "^5"
},
"prisma": {
"seed": "npx tsx prisma/seed.ts"
}
}

7
postcss.config.mjs Normal file
View File

@ -0,0 +1,7 @@
const config = {
plugins: {
"@tailwindcss/postcss": {},
},
};
export default config;

18
prisma.config.ts Normal file
View File

@ -0,0 +1,18 @@
// This file was generated by Prisma, and assumes you have installed the following:
// npm install --save-dev prisma dotenv
import "dotenv/config";
import { defineConfig } from "prisma/config";
export default defineConfig({
schema: "prisma/schema.prisma",
migrations: {
path: "prisma/migrations",
},
datasource: {
url: process.env["DATABASE_URL"]!,
},
// @ts-expect-error - seed is valid at runtime but missing from type definition in this version
seed: {
command: "npx tsx prisma/seed.ts",
},
});

View File

@ -0,0 +1,87 @@
-- CreateTable
CREATE TABLE "users" (
"id" TEXT NOT NULL,
"name" VARCHAR NOT NULL,
"role" VARCHAR NOT NULL,
"coach_id" TEXT,
"created_at" TIMESTAMP(6) DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "users_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "activity_logs" (
"id" SERIAL NOT NULL,
"timestamp" TIMESTAMP(6),
"status" VARCHAR,
"confidence" VARCHAR,
"details" JSONB,
"user_id" TEXT,
CONSTRAINT "activity_logs_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "training_menus" (
"id" SERIAL NOT NULL,
"name" VARCHAR,
"exercises" JSONB,
"created_at" TIMESTAMP(6),
"author_id" TEXT,
"client_id" TEXT,
CONSTRAINT "training_menus_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "user_recaps" (
"id" SERIAL NOT NULL,
"menu_id" INTEGER,
"user_id" TEXT,
"summary" JSONB,
"completed_at" TIMESTAMP(6),
CONSTRAINT "user_recaps_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE INDEX "ix_users_id" ON "users"("id");
-- CreateIndex
CREATE INDEX "ix_users_coach_id" ON "users"("coach_id");
-- CreateIndex
CREATE INDEX "ix_activity_logs_id" ON "activity_logs"("id");
-- CreateIndex
CREATE INDEX "ix_training_menus_id" ON "training_menus"("id");
-- CreateIndex
CREATE INDEX "ix_training_menus_name" ON "training_menus"("name");
-- CreateIndex
CREATE INDEX "ix_training_menus_author_id" ON "training_menus"("author_id");
-- CreateIndex
CREATE INDEX "ix_user_recaps_id" ON "user_recaps"("id");
-- CreateIndex
CREATE INDEX "ix_user_recaps_user_id" ON "user_recaps"("user_id");
-- AddForeignKey
ALTER TABLE "users" ADD CONSTRAINT "users_coach_id_fkey" FOREIGN KEY ("coach_id") REFERENCES "users"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "activity_logs" ADD CONSTRAINT "activity_logs_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "users"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "training_menus" ADD CONSTRAINT "training_menus_author_id_fkey" FOREIGN KEY ("author_id") REFERENCES "users"("id") ON DELETE SET NULL ON UPDATE NO ACTION;
-- AddForeignKey
ALTER TABLE "training_menus" ADD CONSTRAINT "training_menus_client_id_fkey" FOREIGN KEY ("client_id") REFERENCES "users"("id") ON DELETE SET NULL ON UPDATE NO ACTION;
-- AddForeignKey
ALTER TABLE "user_recaps" ADD CONSTRAINT "user_recaps_menu_id_fkey" FOREIGN KEY ("menu_id") REFERENCES "training_menus"("id") ON DELETE NO ACTION ON UPDATE NO ACTION;
-- AddForeignKey
ALTER TABLE "user_recaps" ADD CONSTRAINT "user_recaps_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "users"("id") ON DELETE SET NULL ON UPDATE CASCADE;

View File

@ -0,0 +1,3 @@
-- This migration is skipped because its changes (adding client_id)
-- were manually merged into the previous migration to fix type mismatches (Integer vs String IDs).
-- This file is kept to preserve migration history order.

View File

@ -0,0 +1,14 @@
-- DropForeignKey
ALTER TABLE "training_menus" DROP CONSTRAINT "training_menus_author_id_fkey";
-- DropForeignKey
ALTER TABLE "training_menus" DROP CONSTRAINT "training_menus_client_id_fkey";
-- CreateIndex
CREATE INDEX "ix_activity_logs_user_id" ON "activity_logs"("user_id");
-- AddForeignKey
ALTER TABLE "training_menus" ADD CONSTRAINT "training_menus_author_id_fkey" FOREIGN KEY ("author_id") REFERENCES "users"("id") ON DELETE NO ACTION ON UPDATE NO ACTION;
-- AddForeignKey
ALTER TABLE "training_menus" ADD CONSTRAINT "training_menus_client_id_fkey" FOREIGN KEY ("client_id") REFERENCES "users"("id") ON DELETE NO ACTION ON UPDATE NO ACTION;

View File

@ -0,0 +1,3 @@
# Please do not edit this file manually
# It should be added in your version-control system (e.g., Git)
provider = "postgresql"

75
prisma/schema.prisma Normal file
View File

@ -0,0 +1,75 @@
generator client {
provider = "prisma-client"
output = "../app/generated/client"
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
model users {
id String @id @default(cuid()) // We will override this with our custom generator in logic, but cuid as fallback
name String @db.VarChar
role String @db.VarChar // COACH, CLIENT
coach_id String?
created_at DateTime? @default(now()) @db.Timestamp(6)
// Relations
coach users? @relation("CoachClients", fields: [coach_id], references: [id])
clients users[] @relation("CoachClients")
created_menus training_menus[] @relation("CreatedMenus")
assigned_menus training_menus[] @relation("AssignedMenus")
recaps user_recaps[]
activity_logs activity_logs[]
@@index([id], map: "ix_users_id")
@@index([coach_id], map: "ix_users_coach_id")
}
model activity_logs {
id Int @id @default(autoincrement())
timestamp DateTime? @db.Timestamp(6)
status String? @db.VarChar
confidence String? @db.VarChar
details Json?
user_id String?
user users? @relation(fields: [user_id], references: [id])
@@index([id], map: "ix_activity_logs_id")
@@index([user_id], map: "ix_activity_logs_user_id")
}
model training_menus {
id Int @id @default(autoincrement())
name String? @db.VarChar
exercises Json?
created_at DateTime? @db.Timestamp(6)
author_id String?
client_id String?
author users? @relation("CreatedMenus", fields: [author_id], references: [id], onDelete: NoAction, onUpdate: NoAction)
assigned_client users? @relation("AssignedMenus", fields: [client_id], references: [id], onDelete: NoAction, onUpdate: NoAction)
user_recaps user_recaps[]
@@index([id], map: "ix_training_menus_id")
@@index([name], map: "ix_training_menus_name")
@@index([author_id], map: "ix_training_menus_author_id")
}
model user_recaps {
id Int @id @default(autoincrement())
menu_id Int?
user_id String?
summary Json?
completed_at DateTime? @db.Timestamp(6)
training_menus training_menus? @relation(fields: [menu_id], references: [id], onDelete: NoAction, onUpdate: NoAction)
user users? @relation(fields: [user_id], references: [id])
@@index([id], map: "ix_user_recaps_id")
@@index([user_id], map: "ix_user_recaps_user_id")
}

79
prisma/seed.ts Normal file
View File

@ -0,0 +1,79 @@
import 'dotenv/config';
import { PrismaClient } from '../app/generated/client/client';
const prisma = new PrismaClient();
async function main() {
console.log('Start seeding ...');
// Create Coach 1 (Will link to Client 1 & 2)
const coach1 = await prisma.users.upsert({
where: { id: "C00001" },
update: {},
create: {
id: "C00001",
name: 'Coach One',
role: 'COACH',
},
});
// Create Coach 2 (Will link to Client 3)
const coach2 = await prisma.users.upsert({
where: { id: "C00002" },
update: {},
create: {
id: "C00002",
name: 'Coach Two',
role: 'COACH',
},
});
// Create Client 1 (Linked to Coach 1)
const client1 = await prisma.users.upsert({
where: { id: "U00001" },
update: {},
create: {
id: "U00001",
name: 'Client One',
role: 'CLIENT',
coach_id: coach1.id,
},
});
// Create Client 2 (Linked to Coach 1)
const client2 = await prisma.users.upsert({
where: { id: "U00002" },
update: {},
create: {
id: "U00002",
name: 'Client Two',
role: 'CLIENT',
coach_id: coach1.id,
},
});
// Create Client 3 (Linked to Coach 2)
const client3 = await prisma.users.upsert({
where: { id: "U00003" },
update: {},
create: {
id: "U00003",
name: 'Client Three',
role: 'CLIENT',
coach_id: coach2.id,
},
});
console.log({ coach1, coach2, client1, client2, client3 });
}
main()
.then(async () => {
await prisma.$disconnect();
})
.catch(async (e) => {
console.error(e);
await prisma.$disconnect();
process.exit(1);
});

8
public/favicon.svg Normal file
View File

@ -0,0 +1,8 @@
<svg viewBox="0 0 24 24" width="48" height="48" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M7.4 7H4.6C4.26863 7 4 7.26863 4 7.6V16.4C4 16.7314 4.26863 17 4.6 17H7.4C7.73137 17 8 16.7314 8 16.4V7.6C8 7.26863 7.73137 7 7.4 7Z" stroke="#1E40AF" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M19.4 7H16.6C16.2686 7 16 7.26863 16 7.6V16.4C16 16.7314 16.2686 17 16.6 17H19.4C19.7314 17 20 16.7314 20 16.4V7.6C20 7.26863 19.7314 7 19.4 7Z" stroke="#1E40AF" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M1 14.4V9.6C1 9.26863 1.26863 9 1.6 9H3.4C3.73137 9 4 9.26863 4 9.6V14.4C4 14.7314 3.73137 15 3.4 15H1.6C1.26863 15 1 14.7314 1 14.4Z" stroke="#1E40AF" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M23 14.4V9.6C23 9.26863 22.7314 9 22.4 9H20.6C20.2686 9 20 9.26863 20 9.6V14.4C20 14.7314 20.2686 15 20.6 15H22.4C22.7314 15 23 14.7314 23 14.4Z" stroke="#1E40AF" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M8 12H16" stroke="#1E40AF" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

1
public/file.svg Normal file
View File

@ -0,0 +1 @@
<svg fill="none" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg"><path d="M14.5 13.5V5.41a1 1 0 0 0-.3-.7L9.8.29A1 1 0 0 0 9.08 0H1.5v13.5A2.5 2.5 0 0 0 4 16h8a2.5 2.5 0 0 0 2.5-2.5m-1.5 0v-7H8v-5H3v12a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1M9.5 5V2.12L12.38 5zM5.13 5h-.62v1.25h2.12V5zm-.62 3h7.12v1.25H4.5zm.62 3h-.62v1.25h7.12V11z" clip-rule="evenodd" fill="#666" fill-rule="evenodd"/></svg>

After

Width:  |  Height:  |  Size: 391 B

1
public/globe.svg Normal file
View File

@ -0,0 +1 @@
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><g clip-path="url(#a)"><path fill-rule="evenodd" clip-rule="evenodd" d="M10.27 14.1a6.5 6.5 0 0 0 3.67-3.45q-1.24.21-2.7.34-.31 1.83-.97 3.1M8 16A8 8 0 1 0 8 0a8 8 0 0 0 0 16m.48-1.52a7 7 0 0 1-.96 0H7.5a4 4 0 0 1-.84-1.32q-.38-.89-.63-2.08a40 40 0 0 0 3.92 0q-.25 1.2-.63 2.08a4 4 0 0 1-.84 1.31zm2.94-4.76q1.66-.15 2.95-.43a7 7 0 0 0 0-2.58q-1.3-.27-2.95-.43a18 18 0 0 1 0 3.44m-1.27-3.54a17 17 0 0 1 0 3.64 39 39 0 0 1-4.3 0 17 17 0 0 1 0-3.64 39 39 0 0 1 4.3 0m1.1-1.17q1.45.13 2.69.34a6.5 6.5 0 0 0-3.67-3.44q.65 1.26.98 3.1M8.48 1.5l.01.02q.41.37.84 1.31.38.89.63 2.08a40 40 0 0 0-3.92 0q.25-1.2.63-2.08a4 4 0 0 1 .85-1.32 7 7 0 0 1 .96 0m-2.75.4a6.5 6.5 0 0 0-3.67 3.44 29 29 0 0 1 2.7-.34q.31-1.83.97-3.1M4.58 6.28q-1.66.16-2.95.43a7 7 0 0 0 0 2.58q1.3.27 2.95.43a18 18 0 0 1 0-3.44m.17 4.71q-1.45-.12-2.69-.34a6.5 6.5 0 0 0 3.67 3.44q-.65-1.27-.98-3.1" fill="#666"/></g><defs><clipPath id="a"><path fill="#fff" d="M0 0h16v16H0z"/></clipPath></defs></svg>

After

Width:  |  Height:  |  Size: 1.0 KiB

File diff suppressed because one or more lines are too long

1
public/next.svg Normal file
View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 394 80"><path fill="#000" d="M262 0h68.5v12.7h-27.2v66.6h-13.6V12.7H262V0ZM149 0v12.7H94v20.4h44.3v12.6H94v21h55v12.6H80.5V0h68.7zm34.3 0h-17.8l63.8 79.4h17.9l-32-39.7 32-39.6h-17.9l-23 28.6-23-28.6zm18.3 56.7-9-11-27.1 33.7h17.8l18.3-22.7z"/><path fill="#000" d="M81 79.3 17 0H0v79.3h13.6V17l50.2 62.3H81Zm252.6-.4c-1 0-1.8-.4-2.5-1s-1.1-1.6-1.1-2.6.3-1.8 1-2.5 1.6-1 2.6-1 1.8.3 2.5 1a3.4 3.4 0 0 1 .6 4.3 3.7 3.7 0 0 1-3 1.8zm23.2-33.5h6v23.3c0 2.1-.4 4-1.3 5.5a9.1 9.1 0 0 1-3.8 3.5c-1.6.8-3.5 1.3-5.7 1.3-2 0-3.7-.4-5.3-1s-2.8-1.8-3.7-3.2c-.9-1.3-1.4-3-1.4-5h6c.1.8.3 1.6.7 2.2s1 1.2 1.6 1.5c.7.4 1.5.5 2.4.5 1 0 1.8-.2 2.4-.6a4 4 0 0 0 1.6-1.8c.3-.8.5-1.8.5-3V45.5zm30.9 9.1a4.4 4.4 0 0 0-2-3.3 7.5 7.5 0 0 0-4.3-1.1c-1.3 0-2.4.2-3.3.5-.9.4-1.6 1-2 1.6a3.5 3.5 0 0 0-.3 4c.3.5.7.9 1.3 1.2l1.8 1 2 .5 3.2.8c1.3.3 2.5.7 3.7 1.2a13 13 0 0 1 3.2 1.8 8.1 8.1 0 0 1 3 6.5c0 2-.5 3.7-1.5 5.1a10 10 0 0 1-4.4 3.5c-1.8.8-4.1 1.2-6.8 1.2-2.6 0-4.9-.4-6.8-1.2-2-.8-3.4-2-4.5-3.5a10 10 0 0 1-1.7-5.6h6a5 5 0 0 0 3.5 4.6c1 .4 2.2.6 3.4.6 1.3 0 2.5-.2 3.5-.6 1-.4 1.8-1 2.4-1.7a4 4 0 0 0 .8-2.4c0-.9-.2-1.6-.7-2.2a11 11 0 0 0-2.1-1.4l-3.2-1-3.8-1c-2.8-.7-5-1.7-6.6-3.2a7.2 7.2 0 0 1-2.4-5.7 8 8 0 0 1 1.7-5 10 10 0 0 1 4.3-3.5c2-.8 4-1.2 6.4-1.2 2.3 0 4.4.4 6.2 1.2 1.8.8 3.2 2 4.3 3.4 1 1.4 1.5 3 1.5 5h-5.8z"/></svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

1
public/vercel.svg Normal file
View File

@ -0,0 +1 @@
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1155 1000"><path d="m577.3 0 577.4 1000H0z" fill="#fff"/></svg>

After

Width:  |  Height:  |  Size: 128 B

1
public/window.svg Normal file
View File

@ -0,0 +1 @@
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path fill-rule="evenodd" clip-rule="evenodd" d="M1.5 2.5h13v10a1 1 0 0 1-1 1h-11a1 1 0 0 1-1-1zM0 1h16v11.5a2.5 2.5 0 0 1-2.5 2.5h-11A2.5 2.5 0 0 1 0 12.5zm3.75 4.5a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5M7 4.75a.75.75 0 1 1-1.5 0 .75.75 0 0 1 1.5 0m1.75.75a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5" fill="#666"/></svg>

After

Width:  |  Height:  |  Size: 385 B

20
scripts/check-links.ts Normal file
View File

@ -0,0 +1,20 @@
import 'dotenv/config';
import { PrismaClient } from '../app/generated/client/client';
const prisma = new PrismaClient();
async function main() {
const users = await prisma.users.findMany();
console.log("--- All Users ---");
users.forEach(u => console.log(`${u.name} (${u.role}): ID=${u.id}, CoachID=${u.coach_id}`));
}
main()
.catch((e) => {
console.error(e);
process.exit(1);
})
.finally(async () => {
await prisma.$disconnect();
});

39
scripts/check_logs.ts Normal file
View File

@ -0,0 +1,39 @@
// @ts-nocheck
import { PrismaClient } from '../app/generated/client/client';
import * as dotenv from 'dotenv';
dotenv.config();
const prisma = new PrismaClient();
async function main() {
console.log("Checking Activity Logs...");
const logs = await prisma.activity_logs.findMany({
take: 10,
orderBy: {
timestamp: 'desc',
},
include: {
user: {
select: { name: true }
}
}
});
if (logs.length === 0) {
console.log("No logs found.");
} else {
console.log(`Found ${logs.length} logs:`);
logs.forEach(log => {
console.log(`[${log.timestamp?.toISOString()}] User: ${log.user?.name || log.user_id} | Status: ${log.status} | Details: ${JSON.stringify(log.details)}`);
});
}
}
main()
.catch((e) => {
console.error(e);
process.exit(1);
})
.finally(async () => {
await prisma.$disconnect();
});

33
scripts/seed_log.ts Normal file
View File

@ -0,0 +1,33 @@
// @ts-nocheck
import { PrismaClient } from '../app/generated/client/client';
import * as dotenv from 'dotenv';
dotenv.config();
const prisma = new PrismaClient();
async function main() {
console.log("Seeding Mock Log...");
// Get a user
const user = await prisma.users.findFirst({ where: { role: 'CLIENT' } });
if (!user) {
console.error("No client user found to attach log to.");
return;
}
const log = await prisma.activity_logs.create({
data: {
user_id: user.id,
timestamp: new Date(),
status: 'TEST_LOG',
confidence: '1.0',
details: { message: "Manual verification log" }
}
});
console.log("Created Log ID:", log.id);
}
main()
.catch((e) => { console.error(e); process.exit(1); })
.finally(async () => { await prisma.$disconnect(); });

Some files were not shown because too many files have changed in this diff Show More