diff --git a/CL_ImplementationGuide_1.md b/CL_ImplementationGuide_1.md new file mode 100644 index 0000000..1d93093 --- /dev/null +++ b/CL_ImplementationGuide_1.md @@ -0,0 +1,1867 @@ +# Fitness TUI Implementation Guide + +## Table of Contents +1. [Project Setup](#project-setup) +2. [Development Environment](#development-environment) +3. [Core Dependencies](#core-dependencies) +4. [Project Structure](#project-structure) +5. [Implementation Phases](#implementation-phases) +6. [Detailed Implementation Steps](#detailed-implementation-steps) +7. [Testing Strategy](#testing-strategy) +8. [Deployment](#deployment) + +## Project Setup + +### Initialize Go Module +```bash +mkdir fitness-tui +cd fitness-tui +go mod init github.com/yourusername/fitness-tui +``` + +### Create Directory Structure +```bash +mkdir -p {cmd,internal/{tui/{screens,components,models},garmin,analysis,storage,charts,config}} +mkdir -p {test,docs,examples} +touch cmd/main.go +touch internal/tui/app.go +``` + +### Git Setup +```bash +git init +echo "# Fitness TUI" > README.md +``` + +Create `.gitignore`: +```gitignore +# Binaries +*.exe +*.exe~ +*.dll +*.so +*.dylib +fitness-tui + +# Test binary +*.test + +# Output of the go coverage tool +*.out + +# Go workspace file +go.work + +# IDE +.vscode/ +.idea/ + +# Config files with credentials +config.yaml +*.env + +# Build artifacts +dist/ +build/ +``` + +## Development Environment + +### Required Go Version +- Go 1.21+ (for better error handling and performance) + +### Development Tools +```bash +# Install development tools +go install golang.org/x/tools/cmd/goimports@latest +go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest +go install github.com/air-verse/air@latest # Hot reload during development +``` + +### IDE Configuration +Add to `.vscode/settings.json`: +```json +{ + "go.useLanguageServer": true, + "go.formatTool": "goimports", + "go.lintTool": "golangci-lint", + "go.testFlags": ["-v"], + "go.testTimeout": "10s" +} +``` + +## Core Dependencies + +### Add Dependencies +```bash +# TUI Framework +go get github.com/charmbracelet/bubbletea@latest +go get github.com/charmbracelet/lipgloss@latest +go get github.com/charmbracelet/bubbles@latest + +# Garmin Integration (check latest version of go-garth) +go get github.com/sstent/go-garth@latest + +# HTTP Client for OpenRouter +go get github.com/go-resty/resty/v2@latest + +# Configuration +go get github.com/spf13/viper@latest +go get github.com/spf13/cobra@latest + +# GPX Processing +go get github.com/tkrajina/gpxgo/gpx@latest + +# File System Operations +go get github.com/fsnotify/fsnotify@latest + +# Testing +go get github.com/stretchr/testify@latest +``` + +### Verify Dependencies +```bash +go mod tidy +go mod verify +``` + +## Project Structure + +``` +fitness-tui/ +├── cmd/ +│ └── main.go # Application entry point +├── internal/ +│ ├── config/ +│ │ ├── config.go # Configuration management +│ │ └── defaults.go # Default configuration values +│ ├── tui/ +│ │ ├── app.go # Main TUI application +│ │ ├── screens/ +│ │ │ ├── activity_list.go +│ │ │ ├── activity_detail.go +│ │ │ ├── route_list.go +│ │ │ ├── plan_list.go +│ │ │ └── help.go +│ │ ├── components/ +│ │ │ ├── chart.go # ASCII chart components +│ │ │ ├── list.go # List components +│ │ │ └── modal.go # Modal dialogs +│ │ └── models/ +│ │ ├── activity.go +│ │ ├── route.go +│ │ ├── plan.go +│ │ └── analysis.go +│ ├── garmin/ +│ │ ├── client.go # Garmin API client wrapper +│ │ ├── sync.go # Activity synchronization +│ │ └── auth.go # Authentication handling +│ ├── analysis/ +│ │ ├── llm.go # OpenRouter LLM client +│ │ ├── cache.go # Analysis caching +│ │ └── prompts.go # LLM prompt templates +│ ├── storage/ +│ │ ├── files.go # File-based storage operations +│ │ ├── activities.go # Activity storage management +│ │ ├── routes.go # Route storage management +│ │ └── migrations.go # Data format migrations +│ └── charts/ +│ ├── ascii.go # ASCII chart generation +│ └── sparkline.go # Sparkline utilities +├── test/ +│ ├── fixtures/ # Test data files +│ └── integration/ # Integration tests +├── docs/ +├── examples/ +│ └── sample-config.yaml +└── README.md +``` + +## Implementation Phases + +### Phase 1: Core Foundation (Week 1-2) +- [ ] Project setup and configuration management +- [ ] Basic TUI structure with bubbletea +- [ ] File storage system +- [ ] Garmin client integration + +### Phase 2: Activity Management (Week 3-4) +- [ ] Activity data models +- [ ] Activity list screen +- [ ] Activity detail screen +- [ ] Basic ASCII charts + +### Phase 3: LLM Analysis (Week 5-6) +- [ ] OpenRouter integration +- [ ] Analysis caching system +- [ ] Analysis display in TUI + +### Phase 4: Routes & Polish (Week 7-8) +- [ ] GPX file handling +- [ ] Route management screens +- [ ] Error handling and edge cases +- [ ] Documentation and testing + +## Detailed Implementation Steps + +### Step 1: Configuration System + +#### `internal/config/config.go` +```go +package config + +import ( + "os" + "path/filepath" + + "github.com/spf13/viper" +) + +type Config struct { + Garmin struct { + Username string `mapstructure:"username"` + Password string `mapstructure:"password"` + } `mapstructure:"garmin"` + + OpenRouter struct { + APIKey string `mapstructure:"api_key"` + Model string `mapstructure:"model"` + BaseURL string `mapstructure:"base_url"` + } `mapstructure:"openrouter"` + + Storage struct { + DataDir string `mapstructure:"data_dir"` + } `mapstructure:"storage"` +} + +func Load() (*Config, error) { + homeDir, err := os.UserHomeDir() + if err != nil { + return nil, err + } + + configDir := filepath.Join(homeDir, ".fitness-tui") + if err := os.MkdirAll(configDir, 0755); err != nil { + return nil, err + } + + viper.SetConfigName("config") + viper.SetConfigType("yaml") + viper.AddConfigPath(configDir) + + // Set defaults + setDefaults() + + if err := viper.ReadInConfig(); err != nil { + if _, ok := err.(viper.ConfigFileNotFoundError); ok { + // Create default config file + if err := viper.SafeWriteConfig(); err != nil { + return nil, err + } + } else { + return nil, err + } + } + + var config Config + if err := viper.Unmarshal(&config); err != nil { + return nil, err + } + + return &config, nil +} + +func setDefaults() { + homeDir, _ := os.UserHomeDir() + viper.SetDefault("storage.data_dir", filepath.Join(homeDir, ".fitness-tui")) + viper.SetDefault("openrouter.model", "deepseek/deepseek-r1-05028:free") + viper.SetDefault("openrouter.base_url", "https://openrouter.ai/api/v1") +} +``` + +### Step 2: Data Models + +#### `internal/tui/models/activity.go` +```go +package models + +import ( + "time" + + "github.com/tkrajina/gpxgo/gpx" +) + +type Activity struct { + ID string `json:"id"` + Name string `json:"name"` + Type string `json:"type"` + Date time.Time `json:"date"` + Duration time.Duration `json:"duration"` + Distance float64 `json:"distance"` // meters + Metrics ActivityMetrics `json:"metrics"` + GPXData *gpx.GPX `json:"-"` + GPXPath string `json:"gpx_path,omitempty"` + Analysis *Analysis `json:"analysis,omitempty"` +} + +type ActivityMetrics struct { + AvgHeartRate int `json:"avg_heart_rate,omitempty"` + MaxHeartRate int `json:"max_heart_rate,omitempty"` + AvgPace float64 `json:"avg_pace,omitempty"` // seconds per km + AvgSpeed float64 `json:"avg_speed,omitempty"` // km/h + ElevationGain float64 `json:"elevation_gain,omitempty"` // meters + ElevationLoss float64 `json:"elevation_loss,omitempty"` // meters + Calories int `json:"calories,omitempty"` + AvgCadence int `json:"avg_cadence,omitempty"` + AvgPower int `json:"avg_power,omitempty"` + Temperature float64 `json:"temperature,omitempty"` // celsius +} + +type Analysis struct { + ActivityID string `json:"activity_id"` + WorkoutType string `json:"workout_type"` + GeneratedAt time.Time `json:"generated_at"` + Content string `json:"content"` + Insights []string `json:"insights"` + FilePath string `json:"file_path"` +} + +// Helper methods +func (a *Activity) FormattedDuration() string { + hours := int(a.Duration.Hours()) + minutes := int(a.Duration.Minutes()) % 60 + seconds := int(a.Duration.Seconds()) % 60 + + if hours > 0 { + return fmt.Sprintf("%d:%02d:%02d", hours, minutes, seconds) + } + return fmt.Sprintf("%d:%02d", minutes, seconds) +} + +func (a *Activity) FormattedDistance() string { + km := a.Distance / 1000 + if km >= 10 { + return fmt.Sprintf("%.1fkm", km) + } + return fmt.Sprintf("%.2fkm", km) +} + +func (a *Activity) FormattedPace() string { + if a.Metrics.AvgPace <= 0 { + return "--:--" + } + + minutes := int(a.Metrics.AvgPace / 60) + seconds := int(a.Metrics.AvgPace) % 60 + return fmt.Sprintf("%d:%02d/km", minutes, seconds) +} +``` + +### Step 3: Storage System + +#### `internal/storage/activities.go` +```go +package storage + +import ( + "encoding/json" + "fmt" + "io/ioutil" + "os" + "path/filepath" + "sort" + "time" + + "github.com/yourusername/fitness-tui/internal/tui/models" +) + +type ActivityStorage struct { + dataDir string +} + +func NewActivityStorage(dataDir string) *ActivityStorage { + activitiesDir := filepath.Join(dataDir, "activities") + os.MkdirAll(activitiesDir, 0755) + + return &ActivityStorage{ + dataDir: dataDir, + } +} + +func (s *ActivityStorage) Save(activity *models.Activity) error { + activitiesDir := filepath.Join(s.dataDir, "activities") + filename := fmt.Sprintf("%s-%s.json", + activity.Date.Format("2006-01-02"), + sanitizeFilename(activity.Name)) + + filepath := filepath.Join(activitiesDir, filename) + + data, err := json.MarshalIndent(activity, "", " ") + if err != nil { + return fmt.Errorf("failed to marshal activity: %w", err) + } + + if err := ioutil.WriteFile(filepath, data, 0644); err != nil { + return fmt.Errorf("failed to write activity file: %w", err) + } + + return nil +} + +func (s *ActivityStorage) LoadAll() ([]*models.Activity, error) { + activitiesDir := filepath.Join(s.dataDir, "activities") + + files, err := ioutil.ReadDir(activitiesDir) + if err != nil { + if os.IsNotExist(err) { + return []*models.Activity{}, nil + } + return nil, err + } + + var activities []*models.Activity + + for _, file := range files { + if filepath.Ext(file.Name()) != ".json" { + continue + } + + activity, err := s.loadActivity(filepath.Join(activitiesDir, file.Name())) + if err != nil { + // Log error but continue loading other activities + continue + } + + activities = append(activities, activity) + } + + // Sort by date (newest first) + sort.Slice(activities, func(i, j int) bool { + return activities[i].Date.After(activities[j].Date) + }) + + return activities, nil +} + +func (s *ActivityStorage) loadActivity(filePath string) (*models.Activity, error) { + data, err := ioutil.ReadFile(filePath) + if err != nil { + return nil, err + } + + var activity models.Activity + if err := json.Unmarshal(data, &activity); err != nil { + return nil, err + } + + return &activity, nil +} + +func sanitizeFilename(name string) string { + // Replace invalid filename characters + replacer := strings.NewReplacer( + "/", "-", + "\\", "-", + ":", "-", + "*", "-", + "?", "-", + "\"", "-", + "<", "-", + ">", "-", + "|", "-", + " ", "-", + ) + return replacer.Replace(name) +} +``` + +### Step 4: TUI Application Structure + +#### `internal/tui/app.go` +```go +package tui + +import ( + "fmt" + "log" + + tea "github.com/charmbracelet/bubbletea" + "github.com/charmbracelet/lipgloss" + + "github.com/yourusername/fitness-tui/internal/config" + "github.com/yourusername/fitness-tui/internal/tui/screens" + "github.com/yourusername/fitness-tui/internal/storage" +) + +type Screen int + +const ( + ScreenMain Screen = iota + ScreenActivityList + ScreenActivityDetail + ScreenRouteList + ScreenPlanList + ScreenHelp +) + +type App struct { + config *config.Config + activityStorage *storage.ActivityStorage + + currentScreen Screen + screens map[Screen]tea.Model + + width int + height int + + statusMessage string + errorMessage string +} + +func NewApp(cfg *config.Config) *App { + activityStorage := storage.NewActivityStorage(cfg.Storage.DataDir) + + app := &App{ + config: cfg, + activityStorage: activityStorage, + currentScreen: ScreenMain, + screens: make(map[Screen]tea.Model), + } + + // Initialize screens + app.screens[ScreenActivityList] = screens.NewActivityList(activityStorage) + app.screens[ScreenHelp] = screens.NewHelp() + + return app +} + +func (a *App) Init() tea.Cmd { + return tea.SetWindowTitle("Fitness TUI") +} + +func (a *App) Update(msg tea.Msg) (tea.Model, tea.Cmd) { + switch msg := msg.(type) { + case tea.WindowSizeMsg: + a.width = msg.Width + a.height = msg.Height + return a, nil + + case tea.KeyMsg: + switch msg.String() { + case "ctrl+c", "q": + if a.currentScreen == ScreenMain { + return a, tea.Quit + } + // Go back to main screen + a.currentScreen = ScreenMain + return a, nil + + case "h", "?": + a.currentScreen = ScreenHelp + return a, nil + + case "a": + if a.currentScreen == ScreenMain { + a.currentScreen = ScreenActivityList + return a, a.screens[ScreenActivityList].Init() + } + + case "r": + if a.currentScreen == ScreenMain { + a.currentScreen = ScreenRouteList + return a, nil + } + + case "p": + if a.currentScreen == ScreenMain { + a.currentScreen = ScreenPlanList + return a, nil + } + } + } + + // Delegate to current screen + if screen, exists := a.screens[a.currentScreen]; exists { + updatedScreen, cmd := screen.Update(msg) + a.screens[a.currentScreen] = updatedScreen + return a, cmd + } + + return a, nil +} + +func (a *App) View() string { + if a.currentScreen == ScreenMain { + return a.renderMainMenu() + } + + if screen, exists := a.screens[a.currentScreen]; exists { + return screen.View() + } + + return "Unknown screen" +} + +func (a *App) renderMainMenu() string { + style := lipgloss.NewStyle(). + Padding(1, 2). + Border(lipgloss.RoundedBorder()). + BorderForeground(lipgloss.Color("62")) + + menu := fmt.Sprintf(`Fitness TUI v1.0 + +Navigation: +[A] Activities - View and analyze your workouts +[R] Routes - Manage GPX routes and segments +[P] Plans - Training plans and workouts +[S] Sync - Sync with Garmin +[H] Help - Show help +[Q] Quit - Exit application + +Status: %s`, a.statusMessage) + + return style.Render(menu) +} + +func (a *App) Run() error { + p := tea.NewProgram(a, tea.WithAltScreen()) + _, err := p.Run() + return err +} +``` + +### Step 5: Activity List Screen + +#### `internal/tui/screens/activity_list.go` +```go +package screens + +import ( + "fmt" + "strings" + + "github.com/charmbracelet/bubbles/list" + tea "github.com/charmbracelet/bubbletea" + "github.com/charmbracelet/lipgloss" + + "github.com/yourusername/fitness-tui/internal/tui/models" + "github.com/yourusername/fitness-tui/internal/storage" +) + +type ActivityList struct { + list list.Model + storage *storage.ActivityStorage + width int + height int +} + +type activityItem struct { + activity *models.Activity +} + +func (i activityItem) FilterValue() string { return i.activity.Name } + +func (i activityItem) Title() string { + return fmt.Sprintf("%s - %s", + i.activity.Date.Format("2006-01-02"), + i.activity.Name) +} + +func (i activityItem) Description() string { + return fmt.Sprintf("%s %s %s", + i.activity.FormattedDistance(), + i.activity.FormattedDuration(), + i.activity.FormattedPace()) +} + +func NewActivityList(storage *storage.ActivityStorage) *ActivityList { + items := []list.Item{} + + delegate := list.NewDefaultDelegate() + delegate.Styles.SelectedTitle = delegate.Styles.SelectedTitle. + Foreground(lipgloss.Color("170")). + BorderLeftForeground(lipgloss.Color("170")) + delegate.Styles.SelectedDesc = delegate.Styles.SelectedDesc. + Foreground(lipgloss.Color("243")) + + l := list.New(items, delegate, 0, 0) + l.Title = "Activities" + l.SetShowStatusBar(false) + l.SetFilteringEnabled(false) + l.Styles.Title = lipgloss.NewStyle(). + MarginLeft(2). + Foreground(lipgloss.Color("62")). + Bold(true) + + return &ActivityList{ + list: l, + storage: storage, + } +} + +func (m *ActivityList) Init() tea.Cmd { + return m.loadActivities +} + +func (m *ActivityList) Update(msg tea.Msg) (tea.Model, tea.Cmd) { + switch msg := msg.(type) { + case tea.WindowSizeMsg: + m.width = msg.Width + m.height = msg.Height + m.list.SetWidth(msg.Width) + m.list.SetHeight(msg.Height - 4) // Reserve space for instructions + return m, nil + + case tea.KeyMsg: + switch msg.String() { + case "s": + return m, m.syncActivities + case "enter": + if selectedItem := m.list.SelectedItem(); selectedItem != nil { + // TODO: Navigate to activity detail + return m, nil + } + } + + case activitiesLoadedMsg: + items := make([]list.Item, len(msg.activities)) + for i, activity := range msg.activities { + items[i] = activityItem{activity: activity} + } + m.list.SetItems(items) + return m, nil + + case syncCompleteMsg: + return m, m.loadActivities + } + + var cmd tea.Cmd + m.list, cmd = m.list.Update(msg) + return m, cmd +} + +func (m *ActivityList) View() string { + instructions := lipgloss.NewStyle(). + Foreground(lipgloss.Color("241")). + MarginTop(1). + Render("↑↓ navigate • enter view • s sync • q back") + + return fmt.Sprintf("%s\n%s", m.list.View(), instructions) +} + +// Commands +type activitiesLoadedMsg struct { + activities []*models.Activity +} + +type syncCompleteMsg struct{} + +func (m *ActivityList) loadActivities() tea.Msg { + activities, err := m.storage.LoadAll() + if err != nil { + // TODO: Handle error properly + return activitiesLoadedMsg{activities: []*models.Activity{}} + } + return activitiesLoadedMsg{activities: activities} +} + +func (m *ActivityList) syncActivities() tea.Msg { + // TODO: Implement Garmin sync + return syncCompleteMsg{} +} +``` + +### Step 6: ASCII Charts + +#### `internal/charts/ascii.go` +```go +package charts + +import ( + "fmt" + "math" + "strings" +) + +const ( + // Unicode block characters for charts + BlockEmpty = " " + Block1 = "▁" + Block2 = "▂" + Block3 = "▃" + Block4 = "▄" + Block5 = "▅" + Block6 = "▆" + Block7 = "▇" + Block8 = "█" +) + +var blockChars = []string{BlockEmpty, Block1, Block2, Block3, Block4, Block5, Block6, Block7, Block8} + +// SparklineChart generates a simple sparkline from data points +func SparklineChart(data []float64, width int) string { + if len(data) == 0 { + return strings.Repeat(BlockEmpty, width) + } + + // Find min/max for normalization + min, max := data[0], data[0] + for _, v := range data { + if v < min { + min = v + } + if v > max { + max = v + } + } + + // Handle case where all values are the same + if min == max { + return strings.Repeat(Block4, width) + } + + // Downsample data to fit width + sampledData := sampleData(data, width) + + var result strings.Builder + for _, value := range sampledData { + // Normalize to 0-8 range (9 levels including empty) + normalized := (value - min) / (max - min) + level := int(normalized * 8) + if level > 8 { + level = 8 + } + result.WriteString(blockChars[level]) + } + + return result.String() +} + +// LineChart generates a multi-line ASCII chart +func LineChart(data []float64, width, height int, title string) string { + if len(data) == 0 { + return fmt.Sprintf("%s\n%s", title, strings.Repeat("-", width)) + } + + // Find min/max for scaling + min, max := data[0], data[0] + for _, v := range data { + if v < min { + min = v + } + if v > max { + max = v + } + } + + if min == max { + // All values are the same + line := strings.Repeat("-", width) + var chart strings.Builder + chart.WriteString(fmt.Sprintf("%s (%.2f)\n", title, min)) + for i := 0; i < height; i++ { + if i == height/2 { + chart.WriteString(line + "\n") + } else { + chart.WriteString(strings.Repeat(" ", width) + "\n") + } + } + return chart.String() + } + + // Sample data to fit width + sampledData := sampleData(data, width) + + // Create chart grid + grid := make([][]rune, height) + for i := range grid { + grid[i] = make([]rune, width) + for j := range grid[i] { + grid[i][j] = ' ' + } + } + + // Plot data points + for x, value := range sampledData { + // Scale to chart height (inverted because we draw top to bottom) + normalized := (value - min) / (max - min) + y := height - 1 - int(normalized*float64(height-1)) + if y < 0 { + y = 0 + } + if y >= height { + y = height - 1 + } + + if x < width { + grid[y][x] = '•' + } + } + + // Render chart + var chart strings.Builder + chart.WriteString(fmt.Sprintf("%s (%.2f - %.2f)\n", title, min, max)) + + for _, row := range grid { + chart.WriteString(string(row) + "\n") + } + + return chart.String() +} + +// sampleData downsamples data array to target length +func sampleData(data []float64, targetLength int) []float64 { + if len(data) <= targetLength { + return data + } + + sampled := make([]float64, targetLength) + ratio := float64(len(data)) / float64(targetLength) + + for i := 0; i < targetLength; i++ { + index := int(float64(i) * ratio) + if index >= len(data) { + index = len(data) - 1 + } + sampled[i] = data[index] + } + + return sampled +} + +// ChartData represents different types of chart data +type ChartData struct { + Label string + Values []float64 + Unit string +} + +// MultiChart renders multiple charts in a stacked view +func MultiChart(charts []ChartData, width int) string { + var result strings.Builder + + for i, chart := range charts { + if i > 0 { + result.WriteString("\n") + } + + title := chart.Label + if chart.Unit != "" { + title += fmt.Sprintf(" (%s)", chart.Unit) + } + + sparkline := SparklineChart(chart.Values, width) + result.WriteString(fmt.Sprintf("%-12s %s", title+":", sparkline)) + } + + return result.String() +} +``` + +### Step 7: Main Entry Point + +#### `cmd/main.go` +```go +package main + +import ( + "fmt" + "log" + "os" + + "github.com/yourusername/fitness-tui/internal/config" + "github.com/yourusername/fitness-tui/internal/tui" +) + +func main() { + // Load configuration + cfg, err := config.Load() + if err != nil { + log.Fatalf("Failed to load configuration: %v", err) + } + + // Validate required configuration + if err := validateConfig(cfg); err != nil { + fmt.Fprintf(os.Stderr, "Configuration error: %v\n", err) + fmt.Fprintf(os.Stderr, "Please edit ~/.fitness-tui/config.yaml and add your credentials\n") + os.Exit(1) + } + + // Create and run TUI application + app := tui.NewApp(cfg) + if err := app.Run(); err != nil { + log.Fatalf("Application error: %v", err) + } +} + +func validateConfig(cfg *config.Config) error { + if cfg.Garmin.Username == "" { + return fmt.Errorf("Garmin username is required") + } + + if cfg.Garmin.Password == "" { + return fmt.Errorf("Garmin password is required") + } + + if cfg.OpenRouter.APIKey == "" { + return fmt.Errorf("OpenRouter API key is required for analysis features") + } + + return nil +} +``` + +## Testing Strategy + +### Unit Tests Structure +```bash +# Create test files +touch internal/storage/activities_test.go +touch internal/charts/ascii_test.go +touch internal/tui/models/activity_test.go +``` + +#### Example Test File: `internal/charts/ascii_test.go` +```go +package charts + +import ( + "strings" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestSparklineChart(t *testing.T) { + tests := []struct { + name string + data []float64 + width int + expected string + }{ + { + name: "empty data", + data: []float64{}, + width: 10, + expected: " ", // 10 spaces + }, + { + name: "single value", + data: []float64{50}, + width: 5, + expected: "▄▄▄▄▄", + }, + { + name: "ascending values", + data: []float64{1, 2, 3, 4, 5}, + width: 5, + expected: "▁▂▄▆█", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := SparklineChart(tt.data, tt.width) + assert.Equal(t, tt.expected, result) + assert.Len(t, result, tt.width) + }) + } +} + +func TestLineChart(t *testing.T) { + data := []float64{1, 3, 2, 5, 4} + result := LineChart(data, 10, 5, "Test Chart") + + // Check that it returns a multi-line string + lines := strings.Split(result, "\n") + assert.True(t, len(lines) >= 5) + + // Check title is included + assert.Contains(t, lines[0], "Test Chart") +} +``` + +### Integration Tests + +#### `test/integration/storage_test.go` +```go +package integration + +import ( + "os" + "path/filepath" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/yourusername/fitness-tui/internal/storage" + "github.com/yourusername/fitness-tui/internal/tui/models" +) + +func TestActivityStorageIntegration(t *testing.T) { + // Create temporary directory + tmpDir, err := os.MkdirTemp("", "fitness-tui-test-*") + require.NoError(t, err) + defer os.RemoveAll(tmpDir) + + storage := storage.NewActivityStorage(tmpDir) + + // Create test activity + activity := &models.Activity{ + ID: "test-123", + Name: "Test Run", + Type: "running", + Date: time.Now(), + Duration: 30 * time.Minute, + Distance: 5000, // 5km + Metrics: models.ActivityMetrics{ + AvgHeartRate: 150, + AvgPace: 300, // 5:00/km + }, + } + + // Test save + err = storage.Save(activity) + require.NoError(t, err) + + // Test load + activities, err := storage.LoadAll() + require.NoError(t, err) + require.Len(t, activities, 1) + + loaded := activities[0] + assert.Equal(t, activity.ID, loaded.ID) + assert.Equal(t, activity.Name, loaded.Name) + assert.Equal(t, activity.Distance, loaded.Distance) +} +``` + +### Makefile for Development + +#### `Makefile` +```makefile +.PHONY: build test run clean install dev lint fmt + +# Go parameters +GOCMD=go +GOBUILD=$(GOCMD) build +GOCLEAN=$(GOCMD) clean +GOTEST=$(GOCMD) test +GOGET=$(GOCMD) get +GOMOD=$(GOCMD) mod +BINARY_NAME=fitness-tui + +# Build the application +build: + $(GOBUILD) -o $(BINARY_NAME) -v ./cmd + +# Run tests +test: + $(GOTEST) -v ./... + +# Run tests with coverage +test-coverage: + $(GOTEST) -v -coverprofile=coverage.out ./... + $(GOCMD) tool cover -html=coverage.out -o coverage.html + +# Run the application +run: + $(GOBUILD) -o $(BINARY_NAME) -v ./cmd + ./$(BINARY_NAME) + +# Development mode with hot reload +dev: + air + +# Clean build artifacts +clean: + $(GOCLEAN) + rm -f $(BINARY_NAME) + rm -f coverage.out coverage.html + +# Install dependencies +install: + $(GOMOD) tidy + $(GOMOD) download + +# Lint the code +lint: + golangci-lint run + +# Format the code +fmt: + goimports -w . + $(GOCMD) fmt ./... + +# Build for multiple platforms +build-all: + GOOS=linux GOARCH=amd64 $(GOBUILD) -o dist/$(BINARY_NAME)-linux-amd64 ./cmd + GOOS=windows GOARCH=amd64 $(GOBUILD) -o dist/$(BINARY_NAME)-windows-amd64.exe ./cmd + GOOS=darwin GOARCH=amd64 $(GOBUILD) -o dist/$(BINARY_NAME)-darwin-amd64 ./cmd + GOOS=darwin GOARCH=arm64 $(GOBUILD) -o dist/$(BINARY_NAME)-darwin-arm64 ./cmd + +# Install the binary +install-bin: build + cp $(BINARY_NAME) $(GOPATH)/bin/ +``` + +### Step 8: Garmin Integration + +#### `internal/garmin/client.go` +```go +package garmin + +import ( + "context" + "fmt" + "time" + + "github.com/sstent/go-garth" + + "github.com/yourusername/fitness-tui/internal/tui/models" +) + +type Client struct { + client *garth.Client + username string + password string +} + +func NewClient(username, password string) *Client { + return &Client{ + username: username, + password: password, + } +} + +func (c *Client) Connect() error { + client := garth.NewClient(garth.Credentials{ + Username: c.username, + Password: c.password, + }) + + if err := client.Login(); err != nil { + return fmt.Errorf("failed to login to Garmin: %w", err) + } + + c.client = client + return nil +} + +func (c *Client) GetActivities(ctx context.Context, limit int) ([]*models.Activity, error) { + if c.client == nil { + return nil, fmt.Errorf("client not connected") + } + + // Get activities from Garmin + gActivities, err := c.client.GetActivities(ctx, 0, limit) + if err != nil { + return nil, fmt.Errorf("failed to fetch activities: %w", err) + } + + var activities []*models.Activity + for _, gActivity := range gActivities { + activity := c.convertActivity(gActivity) + activities = append(activities, activity) + } + + return activities, nil +} + +func (c *Client) GetActivityDetails(ctx context.Context, activityID string) (*models.Activity, error) { + if c.client == nil { + return nil, fmt.Errorf("client not connected") + } + + details, err := c.client.GetActivityDetails(ctx, activityID) + if err != nil { + return nil, fmt.Errorf("failed to fetch activity details: %w", err) + } + + activity := c.convertActivityWithDetails(details) + return activity, nil +} + +func (c *Client) convertActivity(gActivity interface{}) *models.Activity { + // This function needs to be implemented based on the actual + // structure returned by go-garth library + // The exact implementation depends on the go-garth API + + // Placeholder implementation + return &models.Activity{ + ID: fmt.Sprintf("garmin-%v", gActivity), + Name: "Imported Activity", + Type: "running", + Date: time.Now(), + Duration: 30 * time.Minute, + Distance: 5000, + Metrics: models.ActivityMetrics{}, + } +} + +func (c *Client) convertActivityWithDetails(details interface{}) *models.Activity { + // Similar to convertActivity but with more detailed information + // Implementation depends on go-garth API structure + + activity := c.convertActivity(details) + // Add detailed metrics here + return activity +} +``` + +### Step 9: LLM Analysis Integration + +#### `internal/analysis/llm.go` +```go +package analysis + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "net/http" + "time" + + "github.com/yourusername/fitness-tui/internal/tui/models" +) + +type OpenRouterClient struct { + apiKey string + baseURL string + model string + client *http.Client +} + +type OpenRouterRequest struct { + Model string `json:"model"` + Messages []Message `json:"messages"` + MaxTokens int `json:"max_tokens,omitempty"` +} + +type Message struct { + Role string `json:"role"` + Content string `json:"content"` +} + +type OpenRouterResponse struct { + Choices []Choice `json:"choices"` + Error *APIError `json:"error,omitempty"` +} + +type Choice struct { + Message Message `json:"message"` +} + +type APIError struct { + Message string `json:"message"` + Type string `json:"type"` +} + +func NewOpenRouterClient(apiKey, baseURL, model string) *OpenRouterClient { + return &OpenRouterClient{ + apiKey: apiKey, + baseURL: baseURL, + model: model, + client: &http.Client{Timeout: 30 * time.Second}, + } +} + +func (c *OpenRouterClient) AnalyzeActivity(ctx context.Context, activity *models.Activity, workoutContext string) (*models.Analysis, error) { + prompt := c.buildAnalysisPrompt(activity, workoutContext) + + request := OpenRouterRequest{ + Model: c.model, + Messages: []Message{ + { + Role: "user", + Content: prompt, + }, + }, + MaxTokens: 1000, + } + + requestBody, err := json.Marshal(request) + if err != nil { + return nil, fmt.Errorf("failed to marshal request: %w", err) + } + + req, err := http.NewRequestWithContext(ctx, "POST", c.baseURL+"/chat/completions", bytes.NewBuffer(requestBody)) + if err != nil { + return nil, fmt.Errorf("failed to create request: %w", err) + } + + req.Header.Set("Content-Type", "application/json") + req.Header.Set("Authorization", "Bearer "+c.apiKey) + + resp, err := c.client.Do(req) + if err != nil { + return nil, fmt.Errorf("failed to make request: %w", err) + } + defer resp.Body.Close() + + var response OpenRouterResponse + if err := json.NewDecoder(resp.Body).Decode(&response); err != nil { + return nil, fmt.Errorf("failed to decode response: %w", err) + } + + if response.Error != nil { + return nil, fmt.Errorf("API error: %s", response.Error.Message) + } + + if len(response.Choices) == 0 { + return nil, fmt.Errorf("no response generated") + } + + content := response.Choices[0].Message.Content + insights := c.extractInsights(content) + + analysis := &models.Analysis{ + ActivityID: activity.ID, + WorkoutType: workoutContext, + GeneratedAt: time.Now(), + Content: content, + Insights: insights, + } + + return analysis, nil +} + +func (c *OpenRouterClient) buildAnalysisPrompt(activity *models.Activity, workoutContext string) string { + return fmt.Sprintf(`Analyze this fitness activity in the context of the intended workout: + +INTENDED WORKOUT: %s + +ACTIVITY DETAILS: +- Name: %s +- Type: %s +- Date: %s +- Duration: %s +- Distance: %s +- Average Heart Rate: %d bpm +- Average Pace: %s +- Elevation Gain: %.0fm + +Please provide: +1. How well did this activity match the intended workout? +2. Performance analysis (pacing, heart rate zones, effort level) +3. Areas for improvement +4. Recovery recommendations +5. Any red flags or injury risk factors + +Format your response in markdown with clear sections.`, + workoutContext, + activity.Name, + activity.Type, + activity.Date.Format("2006-01-02"), + activity.FormattedDuration(), + activity.FormattedDistance(), + activity.Metrics.AvgHeartRate, + activity.FormattedPace(), + activity.Metrics.ElevationGain, + ) +} + +func (c *OpenRouterClient) extractInsights(content string) []string { + // Simple insight extraction - look for bullet points or numbered lists + // This is a basic implementation - could be enhanced with NLP + + insights := []string{} + lines := strings.Split(content, "\n") + + for _, line := range lines { + line = strings.TrimSpace(line) + if strings.HasPrefix(line, "- ") || strings.HasPrefix(line, "* ") { + insight := strings.TrimPrefix(strings.TrimPrefix(line, "- "), "* ") + if len(insight) > 10 { // Filter out very short lines + insights = append(insights, insight) + } + } + } + + return insights +} +``` + +#### `internal/analysis/cache.go` +```go +package analysis + +import ( + "encoding/json" + "fmt" + "os" + "path/filepath" + "time" + + "github.com/yourusername/fitness-tui/internal/tui/models" +) + +type AnalysisCache struct { + dataDir string +} + +func NewAnalysisCache(dataDir string) *AnalysisCache { + analysisDir := filepath.Join(dataDir, "analysis") + os.MkdirAll(analysisDir, 0755) + + return &AnalysisCache{ + dataDir: dataDir, + } +} + +func (c *AnalysisCache) Save(analysis *models.Analysis) error { + // Save as markdown file for easy reading + mdPath := c.getMarkdownPath(analysis.ActivityID, analysis.WorkoutType) + if err := os.WriteFile(mdPath, []byte(analysis.Content), 0644); err != nil { + return fmt.Errorf("failed to save analysis markdown: %w", err) + } + + // Save metadata as JSON + metaPath := c.getMetadataPath(analysis.ActivityID, analysis.WorkoutType) + metadata := struct { + ActivityID string `json:"activity_id"` + WorkoutType string `json:"workout_type"` + GeneratedAt time.Time `json:"generated_at"` + Insights []string `json:"insights"` + MarkdownPath string `json:"markdown_path"` + }{ + ActivityID: analysis.ActivityID, + WorkoutType: analysis.WorkoutType, + GeneratedAt: analysis.GeneratedAt, + Insights: analysis.Insights, + MarkdownPath: mdPath, + } + + data, err := json.MarshalIndent(metadata, "", " ") + if err != nil { + return fmt.Errorf("failed to marshal analysis metadata: %w", err) + } + + if err := os.WriteFile(metaPath, data, 0644); err != nil { + return fmt.Errorf("failed to save analysis metadata: %w", err) + } + + analysis.FilePath = mdPath + return nil +} + +func (c *AnalysisCache) Load(activityID, workoutType string) (*models.Analysis, error) { + metaPath := c.getMetadataPath(activityID, workoutType) + + if _, err := os.Stat(metaPath); os.IsNotExist(err) { + return nil, nil // No cached analysis + } + + data, err := os.ReadFile(metaPath) + if err != nil { + return nil, fmt.Errorf("failed to read analysis metadata: %w", err) + } + + var metadata struct { + ActivityID string `json:"activity_id"` + WorkoutType string `json:"workout_type"` + GeneratedAt time.Time `json:"generated_at"` + Insights []string `json:"insights"` + MarkdownPath string `json:"markdown_path"` + } + + if err := json.Unmarshal(data, &metadata); err != nil { + return nil, fmt.Errorf("failed to unmarshal analysis metadata: %w", err) + } + + // Load markdown content + content, err := os.ReadFile(metadata.MarkdownPath) + if err != nil { + return nil, fmt.Errorf("failed to read analysis content: %w", err) + } + + analysis := &models.Analysis{ + ActivityID: metadata.ActivityID, + WorkoutType: metadata.WorkoutType, + GeneratedAt: metadata.GeneratedAt, + Content: string(content), + Insights: metadata.Insights, + FilePath: metadata.MarkdownPath, + } + + return analysis, nil +} + +func (c *AnalysisCache) getMarkdownPath(activityID, workoutType string) string { + filename := fmt.Sprintf("%s-%s-analysis.md", activityID, sanitizeFilename(workoutType)) + return filepath.Join(c.dataDir, "analysis", filename) +} + +func (c *AnalysisCache) getMetadataPath(activityID, workoutType string) string { + filename := fmt.Sprintf("%s-%s-meta.json", activityID, sanitizeFilename(workoutType)) + return filepath.Join(c.dataDir, "analysis", filename) +} + +func sanitizeFilename(name string) string { + // Replace invalid filename characters + replacer := strings.NewReplacer( + "/", "-", "\\", "-", ":", "-", "*", "-", + "?", "-", "\"", "-", "<", "-", ">", "-", + "|", "-", " ", "-", + ) + return replacer.Replace(name) +} +``` + +### Step 10: Development Configuration + +#### `.air.toml` (for hot reload during development) +```toml +root = "." +testdata_dir = "testdata" +tmp_dir = "tmp" + +[build] + args_bin = [] + bin = "./tmp/main" + cmd = "go build -o ./tmp/main ./cmd" + delay = 0 + exclude_dir = ["assets", "tmp", "vendor", "testdata", "dist"] + exclude_file = [] + exclude_regex = ["_test.go"] + exclude_unchanged = false + follow_symlink = false + full_bin = "" + include_dir = [] + include_ext = ["go", "tpl", "tmpl", "html"] + include_file = [] + kill_delay = "0s" + log = "build-errors.log" + poll = false + poll_interval = 0 + rerun = false + rerun_delay = 500 + send_interrupt = false + stop_on_root = false + +[color] + app = "" + build = "yellow" + main = "magenta" + runner = "green" + watcher = "cyan" + +[log] + main_only = false + time = false + +[misc] + clean_on_exit = false + +[screen] + clear_on_rebuild = false + keep_scroll = true +``` + +#### `examples/sample-config.yaml` +```yaml +garmin: + username: "your-garmin-username" + password: "your-garmin-password" + +openrouter: + api_key: "your-openrouter-api-key" + model: "deepseek/deepseek-r1-05028:free" + base_url: "https://openrouter.ai/api/v1" + +storage: + data_dir: "~/.fitness-tui" +``` + +## Deployment + +### Building for Release + +#### `.goreleaser.yaml` +```yaml +project_name: fitness-tui + +before: + hooks: + - go mod tidy + - go generate ./... + +builds: + - env: + - CGO_ENABLED=0 + main: ./cmd + goos: + - linux + - windows + - darwin + goarch: + - amd64 + - arm64 + ignore: + - goos: windows + goarch: arm64 + +archives: + - format: tar.gz + name_template: >- + {{ .ProjectName }}_ + {{- title .Os }}_ + {{- if eq .Arch "amd64" }}x86_64 + {{- else if eq .Arch "386" }}i386 + {{- else }}{{ .Arch }}{{ end }} + {{- if .Arm }}v{{ .Arm }}{{ end }} + format_overrides: + - goos: windows + format: zip + +checksum: + name_template: 'checksums.txt' + +changelog: + sort: asc + filters: + exclude: + - '^docs:' + - '^test:' + +release: + github: + owner: yourusername + name: fitness-tui +``` + +### Installation Script + +#### `install.sh` +```bash +#!/bin/bash +set -e + +# Fitness TUI Installation Script + +REPO="yourusername/fitness-tui" +INSTALL_DIR="/usr/local/bin" + +# Detect OS and architecture +OS=$(uname -s | tr '[:upper:]' '[:lower:]') +ARCH=$(uname -m) + +case $ARCH in + x86_64) ARCH="x86_64" ;; + arm64|aarch64) ARCH="arm64" ;; + *) echo "Unsupported architecture: $ARCH" && exit 1 ;; +esac + +echo "Installing Fitness TUI for $OS-$ARCH..." + +# Get latest release +LATEST=$(curl -s "https://api.github.com/repos/$REPO/releases/latest" | grep -o '"tag_name": "[^"]*' | cut -d'"' -f4) + +if [ -z "$LATEST" ]; then + echo "Failed to get latest release" + exit 1 +fi + +echo "Latest version: $LATEST" + +# Download URL +FILENAME="fitness-tui_${OS^}_${ARCH}.tar.gz" +if [ "$OS" = "windows" ]; then + FILENAME="fitness-tui_Windows_${ARCH}.zip" +fi + +URL="https://github.com/$REPO/releases/download/$LATEST/$FILENAME" + +echo "Downloading $URL..." +curl -L "$URL" -o "/tmp/$FILENAME" + +# Extract and install +cd /tmp +if [ "$OS" = "windows" ]; then + unzip -q "$FILENAME" +else + tar -xzf "$FILENAME" +fi + +# Move to install directory +sudo mv fitness-tui "$INSTALL_DIR/" +sudo chmod +x "$INSTALL_DIR/fitness-tui" + +echo "Fitness TUI installed successfully to $INSTALL_DIR/fitness-tui" +echo "Run 'fitness-tui' to get started!" + +# Cleanup +rm -f "/tmp/$FILENAME" +``` + +## Development Workflow + +### Daily Development Commands +```bash +# Start development mode +make dev + +# Run tests +make test + +# Run tests with coverage +make test-coverage + +# Format code +make fmt + +# Lint code +make lint + +# Build application +make build + +# Run the built application +./fitness-tui +``` + +### Git Workflow +```bash +# Feature development +git checkout -b feature/activity-analysis +# ... make changes ... +make fmt lint test +git add . +git commit -m "feat: add activity analysis with LLM integration" +git push origin feature/activity-analysis +# Create PR + +# Release preparation +git checkout main +git tag v1.0.0 +git push origin v1.0.0 +# GitHub Actions will build and release +``` + +## Next Steps + +1. **Phase 1 Implementation**: Start with the core foundation + - Set up project structure + - Implement configuration system + - Create basic TUI with bubbletea + - Add file storage for activities + +2. **Testing**: Write tests as you implement each component + - Unit tests for business logic + - Integration tests for storage + - TUI interaction tests + +3. **Documentation**: + - Add godoc comments to all public functions + - Create user documentation + - Add examples and troubleshooting guides + +4. **Performance**: + - Profile the application + - Optimize chart rendering + - Add caching where appropriate + +This implementation guide provides a complete roadmap for building the fitness TUI application. Each step includes detailed code examples and follows Go best practices for maintainable, testable code. \ No newline at end of file diff --git a/CL_LLMPromptforAnalysis.md b/CL_LLMPromptforAnalysis.md new file mode 100644 index 0000000..ea71726 --- /dev/null +++ b/CL_LLMPromptforAnalysis.md @@ -0,0 +1,66 @@ +# Fitness TUI LLM Analysis Rules + +## Core Instructions + +You are a specialized fitness analysis AI integrated into a terminal-based fitness application. Your role is to provide expert analysis of athletic activities by comparing them against intended workout plans and providing actionable insights. + +## Input Context + +You will receive: +- **Activity Data**: Metrics from completed workouts (duration, distance, pace, heart rate, elevation, etc.) +- **Intended Workout**: Natural language description of what the athlete planned to do +- **Historical Context**: Previous analyses may be referenced for trend analysis + +## Analysis Framework + +### 1. Workout Adherence Assessment +- Compare actual performance against intended workout goals +- Identify deviations from planned intensity, duration, or structure +- Rate adherence on a scale: Excellent (90-100%), Good (70-89%), Fair (50-69%), Poor (<50%) +- Explain specific areas where the workout matched or differed from intentions + +### 2. Performance Analysis +Evaluate these key areas: + +#### Pacing Strategy +- Analyze pace distribution throughout the activity +- Identify pacing errors (too fast start, fading finish, inconsistent splits) +- Compare actual pace to target zones if specified in workout plan +- Flag negative splits, positive splits, or erratic pacing patterns + +#### Heart Rate Analysis +- Assess time in different heart rate zones (if data available) +- Flag excessive time in high zones for easy workouts +- Flag insufficient intensity for hard workouts +- Identify heart rate drift patterns indicating fatigue or dehydration + +#### Effort-Duration Relationship +- Evaluate if effort level was appropriate for workout duration +- Assess sustainability of the chosen intensity +- Compare perceived effort indicators (pace, HR) to workout goals + +### 3. Training Load Assessment +- Evaluate workout difficulty relative to training phase +- Assess recovery needs based on intensity and duration +- Flag potentially excessive training stress +- Consider cumulative fatigue if historical data is available + +### 4. Technical Observations +Analyze technical aspects when data is available: +- Cadence patterns and efficiency +- Power output consistency (if available) +- Elevation gain/loss and pacing on hills +- Environmental factors (temperature, weather impact) + +## Output Format Requirements + +Structure your analysis using these sections: + +### Executive Summary +- One sentence overall assessment +- Adherence rating with brief justification +- Primary recommendation + +### Workout Adherence Analysis +- How well did the activity match the intended workout? +- Specific \ No newline at end of file diff --git a/CL_backendfixes.md b/CL_backendfixes.md deleted file mode 100644 index b098283..0000000 --- a/CL_backendfixes.md +++ /dev/null @@ -1,261 +0,0 @@ -# 🎯 **Backend Implementation TODO List** - -## **Priority 1: Core API Gaps (Essential)** - -### **1.1 Plan Generation Endpoint** -- [ ] **Add plan generation endpoint** in `app/routes/plan.py` - ```python - @router.post("/generate", response_model=PlanSchema) - async def generate_plan( - plan_request: PlanGenerationRequest, - db: AsyncSession = Depends(get_db) - ): - ``` -- [ ] **Create PlanGenerationRequest schema** in `app/schemas/plan.py` - ```python - class PlanGenerationRequest(BaseModel): - rule_ids: List[UUID] - goals: Dict[str, Any] - user_preferences: Optional[Dict[str, Any]] = None - duration_weeks: int = 12 - ``` -- [ ] **Update AIService.generate_plan()** to handle rule fetching from DB -- [ ] **Add validation** for rule compatibility and goal requirements -- [ ] **Add tests** for plan generation workflow - -### **1.2 Rule Parsing API** -- [ ] **Add natural language rule parsing endpoint** in `app/routes/rule.py` - ```python - @router.post("/parse-natural-language") - async def parse_natural_language_rules( - request: NaturalLanguageRuleRequest, - db: AsyncSession = Depends(get_db) - ): - ``` -- [ ] **Create request/response schemas** in `app/schemas/rule.py` - ```python - class NaturalLanguageRuleRequest(BaseModel): - natural_language_text: str - rule_name: str - - class ParsedRuleResponse(BaseModel): - parsed_rules: Dict[str, Any] - confidence_score: Optional[float] - suggestions: Optional[List[str]] - ``` -- [ ] **Enhance AIService.parse_rules_from_natural_language()** with better error handling -- [ ] **Add rule validation** after parsing -- [ ] **Add preview mode** before saving parsed rules - -### **1.3 Section Integration with GPX Parsing** -- [ ] **Update `app/services/gpx.py`** to create sections automatically - ```python - async def parse_gpx_with_sections(file_path: str, route_id: UUID, db: AsyncSession) -> dict: - # Parse GPX into segments - # Create Section records for each segment - # Return enhanced GPX data with section metadata - ``` -- [ ] **Modify `app/routes/gpx.py`** to create sections after route creation -- [ ] **Add section creation logic** in GPX upload workflow -- [ ] **Update Section model** to include more GPX-derived metadata -- [ ] **Add section querying endpoints** for route visualization - -## **Priority 2: Data Model Enhancements** - -### **2.1 Missing Schema Fields** -- [ ] **Add missing fields to User model** in `app/models/user.py` - ```python - class User(BaseModel): - name: Optional[str] - email: Optional[str] - fitness_level: Optional[str] - preferences: Optional[JSON] - ``` -- [ ] **Enhance Plan model** with additional metadata - ```python - class Plan(BaseModel): - user_id: Optional[UUID] = Column(ForeignKey("users.id")) - name: str - description: Optional[str] - start_date: Optional[Date] - end_date: Optional[Date] - goal_type: Optional[str] - active: Boolean = Column(default=True) - ``` -- [ ] **Add plan-rule relationship table** (already exists but ensure proper usage) -- [ ] **Update all schemas** to match enhanced models - -### **2.2 Database Relationships** -- [ ] **Fix User-Plan relationship** in models -- [ ] **Add cascade delete rules** where appropriate -- [ ] **Add database constraints** for data integrity -- [ ] **Create missing indexes** for performance - ```sql - CREATE INDEX idx_workouts_garmin_activity_id ON workouts(garmin_activity_id); - CREATE INDEX idx_plans_user_active ON plans(user_id, active); - CREATE INDEX idx_analyses_workout_approved ON analyses(workout_id, approved); - ``` - -## **Priority 3: API Completeness** - -### **3.1 Export/Import Functionality** -- [ ] **Create export service** `app/services/export_import.py` - ```python - class ExportImportService: - async def export_user_data(user_id: UUID) -> bytes: - async def export_routes() -> bytes: - async def import_user_data(data: bytes, user_id: UUID): - ``` -- [ ] **Add export endpoints** in new `app/routes/export.py` - ```python - @router.get("/export/routes") - @router.get("/export/plans/{plan_id}") - @router.get("/export/user-data") - @router.post("/import/routes") - @router.post("/import/plans") - ``` -- [ ] **Support multiple formats** (JSON, GPX, ZIP) -- [ ] **Add data validation** for imports -- [ ] **Handle version compatibility** for imports - -### **3.2 Enhanced Dashboard API** -- [ ] **Expand dashboard data** in `app/routes/dashboard.py` - ```python - @router.get("/metrics/weekly") - @router.get("/metrics/monthly") - @router.get("/progress/{plan_id}") - @router.get("/upcoming-workouts") - ``` -- [ ] **Add aggregation queries** for metrics -- [ ] **Cache dashboard data** for performance -- [ ] **Add real-time updates** capability - -### **3.3 Advanced Workout Features** -- [ ] **Add workout comparison endpoint** - ```python - @router.get("/workouts/{workout_id}/compare/{compare_workout_id}") - ``` -- [ ] **Add workout search/filtering** - ```python - @router.get("/workouts/search") - async def search_workouts( - activity_type: Optional[str] = None, - date_range: Optional[DateRange] = None, - power_range: Optional[PowerRange] = None - ): - ``` -- [ ] **Add bulk workout operations** -- [ ] **Add workout tagging system** - -## **Priority 4: Service Layer Improvements** - -### **4.1 AI Service Enhancements** -- [ ] **Add prompt caching** to reduce API calls -- [ ] **Implement prompt A/B testing** framework -- [ ] **Add AI response validation** and confidence scoring -- [ ] **Create AI service health checks** -- [ ] **Add fallback mechanisms** for AI failures -- [ ] **Implement rate limiting** for AI calls -- [ ] **Add cost tracking** for AI API usage - -### **4.2 Garmin Service Improvements** -- [ ] **Add incremental sync** instead of full sync -- [ ] **Implement activity deduplication** logic -- [ ] **Add webhook support** for real-time sync -- [ ] **Enhance error recovery** for failed syncs -- [ ] **Add activity type filtering** -- [ ] **Support multiple Garmin accounts** per user - -### **4.3 Plan Evolution Enhancements** -- [ ] **Add plan comparison** functionality -- [ ] **Implement plan rollback** mechanism -- [ ] **Add plan branching** for different scenarios -- [ ] **Create plan templates** system -- [ ] **Add automated plan adjustments** based on performance - -## **Priority 5: Validation & Error Handling** - -### **5.1 Input Validation** -- [ ] **Add comprehensive Pydantic validators** for all schemas -- [ ] **Validate GPX file integrity** before processing -- [ ] **Add business rule validation** (e.g., plan dates, workout conflicts) -- [ ] **Validate AI responses** before storing -- [ ] **Add file size/type restrictions** - -### **5.2 Error Handling** -- [ ] **Create custom exception hierarchy** - ```python - class CyclingCoachException(Exception): - class GarminSyncError(CyclingCoachException): - class AIServiceError(CyclingCoachException): - class PlanGenerationError(CyclingCoachException): - ``` -- [ ] **Add global exception handler** -- [ ] **Improve error messages** for user feedback -- [ ] **Add error recovery mechanisms** -- [ ] **Log errors with context** for debugging - -## **Priority 6: Performance & Monitoring** - -### **6.1 Performance Optimizations** -- [ ] **Add database query optimization** -- [ ] **Implement caching** for frequently accessed data -- [ ] **Add connection pooling** configuration -- [ ] **Optimize GPX file parsing** for large files -- [ ] **Add pagination** to list endpoints -- [ ] **Implement background job queue** for long-running tasks - -### **6.2 Enhanced Monitoring** -- [ ] **Add application metrics** (response times, error rates) -- [ ] **Create health check dependencies** -- [ ] **Add performance profiling** endpoints -- [ ] **Implement alerting** for critical errors -- [ ] **Add audit logging** for data changes - -## **Priority 7: Security & Configuration** - -### **7.1 Security Improvements** -- [ ] **Implement user authentication/authorization** -- [ ] **Add rate limiting** to prevent abuse -- [ ] **Validate file uploads** for security -- [ ] **Add CORS configuration** properly -- [ ] **Implement request/response logging** (without sensitive data) -- [ ] **Add API versioning** support - -### **7.2 Configuration Management** -- [ ] **Add environment-specific configs** -- [ ] **Validate configuration** on startup -- [ ] **Add feature flags** system -- [ ] **Implement secrets management** -- [ ] **Add configuration reload** without restart - -## **Priority 8: Testing & Documentation** - -### **8.1 Testing** -- [ ] **Create comprehensive test suite** - - Unit tests for services - - Integration tests for API endpoints - - Database migration tests - - AI service mock tests -- [ ] **Add test fixtures** for common data -- [ ] **Implement test database** setup/teardown -- [ ] **Add performance tests** for critical paths -- [ ] **Create end-to-end tests** for workflows - -### **8.2 Documentation** -- [ ] **Generate OpenAPI documentation** -- [ ] **Add endpoint documentation** with examples -- [ ] **Create service documentation** -- [ ] **Document deployment procedures** -- [ ] **Add troubleshooting guides** - ---- - -## **🎯 Recommended Implementation Order:** - -1. **Week 1:** Priority 1 (Core API gaps) - Essential for feature completeness -2. **Week 2:** Priority 2 (Data model) + Priority 5.1 (Validation) - Foundation improvements -3. **Week 3:** Priority 3.1 (Export/Import) + Priority 4.1 (AI improvements) - User-facing features -4. **Week 4:** Priority 6 (Performance) + Priority 8.1 (Testing) - Production readiness - -This todo list will bring your backend implementation to 100% design doc compliance and beyond, making it production-ready with enterprise-level features! 🚀 \ No newline at end of file diff --git a/CL_frontendfixes.md b/CL_frontendfixes.md deleted file mode 100644 index 84e7897..0000000 --- a/CL_frontendfixes.md +++ /dev/null @@ -1,255 +0,0 @@ -# Frontend Development TODO List - -## 🚨 Critical Missing Features (High Priority) - -### 1. Rules Management System -- [ ] **Create Rules page component** (`/src/pages/Rules.jsx`) - - [ ] Natural language textarea editor - - [ ] AI parsing button with loading state - - [ ] JSON preview pane with syntax highlighting - - [ ] Rule validation feedback - - [ ] Save/cancel actions -- [ ] **Create RuleEditor component** (`/src/components/rules/RuleEditor.jsx`) - - [ ] Rich text input with auto-resize - - [ ] Character count and validation - - [ ] Template suggestions dropdown -- [ ] **Create RulePreview component** (`/src/components/rules/RulePreview.jsx`) - - [ ] JSON syntax highlighting (use `react-json-view`) - - [ ] Editable JSON with validation - - [ ] Diff view for rule changes -- [ ] **Create RulesList component** (`/src/components/rules/RulesList.jsx`) - - [ ] Rule set selection dropdown - - [ ] Version history per rule set - - [ ] Delete/duplicate rule sets -- [ ] **API Integration** - - [ ] `POST /api/rules` - Create new rule set - - [ ] `PUT /api/rules/{id}` - Update rule set - - [ ] `GET /api/rules` - List all rule sets - - [ ] `POST /api/rules/{id}/parse` - AI parsing endpoint - -### 2. Plan Generation Workflow -- [ ] **Create PlanGeneration page** (`/src/pages/PlanGeneration.jsx`) - - [ ] Goal selection interface - - [ ] Rule set selection - - [ ] Plan parameters (duration, weekly hours) - - [ ] Progress tracking for AI generation -- [ ] **Create GoalSelector component** (`/src/components/plans/GoalSelector.jsx`) - - [ ] Predefined goal templates - - [ ] Custom goal input - - [ ] Goal validation -- [ ] **Create PlanParameters component** (`/src/components/plans/PlanParameters.jsx`) - - [ ] Duration slider (4-20 weeks) - - [ ] Weekly hours slider (5-15 hours) - - [ ] Difficulty level selection - - [ ] Available days checkboxes -- [ ] **Enhance PlanTimeline component** - - [ ] Week-by-week breakdown - - [ ] Workout details expandable cards - - [ ] Progress tracking indicators - - [ ] Edit individual workouts -- [ ] **API Integration** - - [ ] `POST /api/plans/generate` - Generate new plan - - [ ] `GET /api/plans/{id}/preview` - Preview before saving - - [ ] Plan generation status polling - -### 3. Route Management & Visualization -- [ ] **Enhance RoutesPage** (`/src/pages/RoutesPage.jsx`) - - [ ] Route list with metadata - - [ ] GPX file upload integration - - [ ] Route preview cards - - [ ] Search and filter functionality -- [ ] **Create RouteVisualization component** (`/src/components/routes/RouteVisualization.jsx`) - - [ ] Interactive map (use Leaflet.js) - - [ ] GPX track overlay - - [ ] Elevation profile chart - - [ ] Distance markers -- [ ] **Create RouteMetadata component** (`/src/components/routes/RouteMetadata.jsx`) - - [ ] Distance, elevation gain, grade analysis - - [ ] Estimated time calculations - - [ ] Difficulty rating - - [ ] Notes/description editing -- [ ] **Create SectionManager component** (`/src/components/routes/SectionManager.jsx`) - - [ ] Split routes into sections - - [ ] Section-specific metadata - - [ ] Gear recommendations per section -- [ ] **Dependencies to add** - - [ ] `npm install leaflet react-leaflet` - - [ ] GPX parsing library integration - -### 4. Export/Import System -- [ ] **Create ExportImport page** (`/src/pages/ExportImport.jsx`) - - [ ] Export options (JSON, ZIP) - - [ ] Import validation - - [ ] Bulk operations -- [ ] **Create DataExporter component** (`/src/components/export/DataExporter.jsx`) - - [ ] Selective export (routes, rules, plans) - - [ ] Format selection (JSON, GPX, ZIP) - - [ ] Export progress tracking -- [ ] **Create DataImporter component** (`/src/components/export/DataImporter.jsx`) - - [ ] File validation and preview - - [ ] Conflict resolution interface - - [ ] Import progress tracking -- [ ] **API Integration** - - [ ] `GET /api/export` - Generate export package - - [ ] `POST /api/import` - Import data package - - [ ] `POST /api/import/validate` - Validate before import - -## 🔧 Code Quality & Architecture Improvements - -### 5. Enhanced Error Handling -- [ ] **Create GlobalErrorHandler** (`/src/components/GlobalErrorHandler.jsx`) - - [ ] Centralized error logging - - [ ] User-friendly error messages - - [ ] Retry mechanisms -- [ ] **Improve API error handling** - - [ ] Consistent error response format - - [ ] Network error recovery - - [ ] Timeout handling -- [ ] **Add error boundaries** - - [ ] Page-level error boundaries - - [ ] Component-level error recovery - -### 6. State Management Improvements -- [ ] **Enhance AuthContext** - - [ ] Add user preferences - - [ ] API caching layer - - [ ] Offline capability detection -- [ ] **Create AppStateContext** (`/src/context/AppStateContext.jsx`) - - [ ] Global loading states - - [ ] Toast notifications - - [ ] Modal management -- [ ] **Add React Query** (Optional but recommended) - - [ ] `npm install @tanstack/react-query` - - [ ] API data caching - - [ ] Background refetching - - [ ] Optimistic updates - -### 7. UI/UX Enhancements -- [ ] **Improve responsive design** - - [ ] Better mobile navigation - - [ ] Touch-friendly interactions - - [ ] Responsive charts and maps -- [ ] **Add loading skeletons** - - [ ] Replace generic spinners - - [ ] Component-specific skeletons - - [ ] Progressive loading -- [ ] **Create ConfirmDialog component** (`/src/components/ui/ConfirmDialog.jsx`) - - [ ] Delete confirmations - - [ ] Destructive action warnings - - [ ] Custom confirmation messages -- [ ] **Add keyboard shortcuts** - - [ ] Navigation shortcuts - - [ ] Action shortcuts - - [ ] Help overlay - -## 🧪 Testing & Quality Assurance - -### 8. Testing Infrastructure -- [ ] **Expand component tests** - - [ ] Rules management tests - - [ ] Plan generation tests - - [ ] Route visualization tests -- [ ] **Add integration tests** - - [ ] API integration tests - - [ ] User workflow tests - - [ ] Error scenario tests -- [ ] **Performance testing** - - [ ] Large dataset handling - - [ ] Chart rendering performance - - [ ] Memory leak detection - -### 9. Development Experience -- [ ] **Add Storybook** (Optional) - - [ ] Component documentation - - [ ] Design system documentation - - [ ] Interactive component testing -- [ ] **Improve build process** - - [ ] Bundle size optimization - - [ ] Dead code elimination - - [ ] Tree shaking verification -- [ ] **Add development tools** - - [ ] React DevTools integration - - [ ] Performance monitoring - - [ ] Bundle analyzer - -## 📚 Documentation & Dependencies - -### 10. Missing Dependencies -```json -{ - "leaflet": "^1.9.4", - "react-leaflet": "^4.2.1", - "react-json-view": "^1.21.3", - "@tanstack/react-query": "^4.32.0", - "react-hook-form": "^7.45.0", - "react-select": "^5.7.4", - "file-saver": "^2.0.5" -} -``` - -### 11. Configuration Files -- [ ] **Create environment config** (`/src/config/index.js`) - - [ ] API endpoints configuration - - [ ] Feature flags - - [ ] Environment-specific settings -- [ ] **Add TypeScript support** (Optional) - - [ ] Convert critical components - - [ ] Add type definitions - - [ ] Improve IDE support - -## 🚀 Deployment & Performance - -### 12. Production Readiness -- [ ] **Optimize bundle size** - - [ ] Code splitting implementation - - [ ] Lazy loading for routes - - [ ] Image optimization -- [ ] **Add PWA features** (Optional) - - [ ] Service worker - - [ ] Offline functionality - - [ ] App manifest -- [ ] **Performance monitoring** - - [ ] Core Web Vitals tracking - - [ ] Error tracking integration - - [ ] User analytics - -## 📅 Implementation Priority - -### Phase 1 (Week 1-2): Core Missing Features -1. Rules Management System -2. Plan Generation Workflow -3. Enhanced Route Management - -### Phase 2 (Week 3): Data Management -1. Export/Import System -2. Enhanced Error Handling -3. State Management Improvements - -### Phase 3 (Week 4): Polish & Quality -1. UI/UX Enhancements -2. Testing Infrastructure -3. Performance Optimization - -### Phase 4 (Ongoing): Maintenance -1. Documentation -2. Monitoring -3. User Feedback Integration - ---- - -## 🎯 Success Criteria - -- [ ] All design document workflows implemented -- [ ] 90%+ component test coverage -- [ ] Mobile-responsive design -- [ ] Sub-3s initial page load -- [ ] Accessibility compliance (WCAG 2.1 AA) -- [ ] Cross-browser compatibility (Chrome, Firefox, Safari, Edge) - -## 📝 Notes - -- **Prioritize user-facing features** over internal architecture improvements -- **Test each feature** as you implement it -- **Consider Progressive Web App features** for offline functionality -- **Plan for internationalization** if expanding globally -- **Monitor bundle size** as you add dependencies \ No newline at end of file diff --git a/CL_implementation_guide.md b/CL_implementation_guide.md deleted file mode 100644 index 1ea9149..0000000 --- a/CL_implementation_guide.md +++ /dev/null @@ -1,1565 +0,0 @@ -# Junior Developer Implementation Guide -## AI Cycling Coach - Critical Features Implementation - -This guide provides step-by-step instructions to implement the missing core features identified in the codebase evaluation. - ---- - -## 🎯 **Implementation Phases Overview** - -| Phase | Focus | Duration | Difficulty | -|-------|-------|----------|------------| -| **Phase 1** | Backend Core APIs | 2-3 weeks | Medium | -| **Phase 2** | Frontend Core Features | 3-4 weeks | Medium | -| **Phase 3** | Integration & Testing | 1-2 weeks | Easy-Medium | -| **Phase 4** | Polish & Production | 1-2 weeks | Easy | - ---- - -# Phase 1: Backend Core APIs Implementation - -## Step 1.1: Plan Generation Endpoint - -### **File:** `backend/app/routes/plan.py` - -**Add this endpoint to the existing router:** - -```python -from app.schemas.plan import PlanGenerationRequest, PlanGenerationResponse -from app.services.ai_service import AIService, AIServiceError - -@router.post("/generate", response_model=PlanGenerationResponse) -async def generate_plan( - request: PlanGenerationRequest, - db: AsyncSession = Depends(get_db) -): - """Generate a new training plan using AI based on rules and goals.""" - try: - # Fetch rules from database - rules_query = select(Rule).where(Rule.id.in_(request.rule_ids)) - result = await db.execute(rules_query) - rules = result.scalars().all() - - if len(rules) != len(request.rule_ids): - raise HTTPException(status_code=404, detail="One or more rules not found") - - # Get plaintext rules - rule_texts = [rule.rule_text for rule in rules] - - # Initialize AI service - ai_service = AIService(db) - - # Generate plan - plan_data = await ai_service.generate_plan(rule_texts, request.goals.dict()) - - # Create plan record - db_plan = Plan( - jsonb_plan=plan_data, - version=1, - parent_plan_id=None - ) - db.add(db_plan) - await db.commit() - await db.refresh(db_plan) - - return PlanGenerationResponse( - plan=db_plan, - generation_metadata={ - "rules_used": len(rules), - "goals": request.goals.dict(), - "generated_at": datetime.utcnow().isoformat() - } - ) - - except AIServiceError as e: - raise HTTPException(status_code=503, detail=f"AI service error: {str(e)}") - except Exception as e: - raise HTTPException(status_code=500, detail=f"Plan generation failed: {str(e)}") -``` - -### **File:** `backend/app/schemas/plan.py` - -**Add these new schemas:** - -```python -from typing import Dict, List, Optional, Any -from pydantic import BaseModel, Field -from uuid import UUID - -class TrainingGoals(BaseModel): - """Training goals for plan generation.""" - primary_goal: str = Field(..., description="Primary training goal") - target_weekly_hours: int = Field(..., ge=3, le=20, description="Target hours per week") - fitness_level: str = Field(..., description="Current fitness level") - event_date: Optional[str] = Field(None, description="Target event date (YYYY-MM-DD)") - preferred_routes: List[int] = Field(default=[], description="Preferred route IDs") - avoid_days: List[str] = Field(default=[], description="Days to avoid training") - -class PlanGenerationRequest(BaseModel): - """Request schema for plan generation.""" - rule_ids: List[UUID] = Field(..., description="Rule set IDs to apply") - goals: TrainingGoals = Field(..., description="Training goals") - duration_weeks: int = Field(4, ge=1, le=20, description="Plan duration in weeks") - user_preferences: Optional[Dict[str, Any]] = Field(None, description="Additional preferences") - -class PlanGenerationResponse(BaseModel): - """Response schema for plan generation.""" - plan: Plan = Field(..., description="Generated training plan") - generation_metadata: Dict[str, Any] = Field(..., description="Generation metadata") - - class Config: - orm_mode = True -``` - ---- - - - - - -**Add these endpoints after the existing routes:** - -```python -from app.schemas.rule import NaturalLanguageRuleRequest, ParsedRuleResponse - -@router.post("/parse-natural-language", response_model=ParsedRuleResponse) -async def parse_natural_language_rules( - request: NaturalLanguageRuleRequest, - db: AsyncSession = Depends(get_db) -): - """Parse natural language text into structured training rules.""" - try: - # Initialize AI service - ai_service = AIService(db) - - # Parse rules using AI - parsed_data = await ai_service.parse_rules_from_natural_language( - request.natural_language_text - ) - - # Validate parsed rules - validation_result = _validate_parsed_rules(parsed_data) - - return ParsedRuleResponse( - parsed_rules=parsed_data, - confidence_score=parsed_data.get("confidence", 0.0), - suggestions=validation_result.get("suggestions", []), - validation_errors=validation_result.get("errors", []), - rule_name=request.rule_name - ) - - except AIServiceError as e: - raise HTTPException(status_code=503, detail=f"AI parsing failed: {str(e)}") - except Exception as e: - raise HTTPException(status_code=500, detail=f"Rule parsing failed: {str(e)}") - -@router.post("/validate-rules") -async def validate_rule_consistency( - rules_data: Dict[str, Any], - db: AsyncSession = Depends(get_db) -): - """Validate rule consistency and detect conflicts.""" - validation_result = _validate_parsed_rules(rules_data) - return { - "is_valid": len(validation_result.get("errors", [])) == 0, - "errors": validation_result.get("errors", []), - "warnings": validation_result.get("warnings", []), - "suggestions": validation_result.get("suggestions", []) - } - -def _validate_parsed_rules(parsed_rules: Dict[str, Any]) -> Dict[str, List[str]]: - """Validate parsed rules for consistency and completeness.""" - errors = [] - warnings = [] - suggestions = [] - - # Check for required fields - required_fields = ["max_rides_per_week", "min_rest_between_hard"] - for field in required_fields: - if field not in parsed_rules: - errors.append(f"Missing required field: {field}") - - # Validate numeric ranges - max_rides = parsed_rules.get("max_rides_per_week", 0) - if max_rides > 7: - errors.append("Maximum rides per week cannot exceed 7") - elif max_rides < 1: - errors.append("Must have at least 1 ride per week") - - # Check for conflicts - max_hours = parsed_rules.get("max_duration_hours", 0) - if max_rides and max_hours: - avg_duration = max_hours / max_rides - if avg_duration > 5: - warnings.append("Very long average ride duration detected") - elif avg_duration < 0.5: - warnings.append("Very short average ride duration detected") - - # Provide suggestions - if "weather_constraints" not in parsed_rules: - suggestions.append("Consider adding weather constraints for outdoor rides") - - return { - "errors": errors, - "warnings": warnings, - "suggestions": suggestions - } -``` - -### **File:** `backend/app/schemas/rule.py` - -**Replace the existing content with:** - -```python -from pydantic import BaseModel, Field, validator -from typing import Optional, Dict, Any, List - -class NaturalLanguageRuleRequest(BaseModel): - """Request schema for natural language rule parsing.""" - natural_language_text: str = Field( - ..., - min_length=10, - max_length=5000, - description="Natural language rule description" - ) - rule_name: str = Field(..., min_length=1, max_length=100, description="Rule set name") - - @validator('natural_language_text') - def validate_text_content(cls, v): - # Check for required keywords - required_keywords = ['ride', 'week', 'hour', 'day', 'rest', 'training'] - if not any(keyword in v.lower() for keyword in required_keywords): - raise ValueError("Text must contain training-related keywords") - return v - -class ParsedRuleResponse(BaseModel): - """Response schema for parsed rules.""" - parsed_rules: Dict[str, Any] = Field(..., description="Structured rule data") - confidence_score: Optional[float] = Field(None, ge=0.0, le=1.0, description="Parsing confidence") - suggestions: List[str] = Field(default=[], description="Improvement suggestions") - validation_errors: List[str] = Field(default=[], description="Validation errors") - rule_name: str = Field(..., description="Rule set name") - -class RuleBase(BaseModel): - """Base rule schema.""" - name: str = Field(..., min_length=1, max_length=100) - description: Optional[str] = Field(None, max_length=500) - user_defined: bool = Field(True, description="Whether rule is user-defined") - jsonb_rules: Dict[str, Any] = Field(..., description="Structured rule data") - version: int = Field(1, ge=1, description="Rule version") - parent_rule_id: Optional[int] = Field(None, description="Parent rule for versioning") - -class RuleCreate(RuleBase): - """Schema for creating new rules.""" - pass - -class Rule(RuleBase): - """Complete rule schema with database fields.""" - id: int - created_at: datetime - updated_at: datetime - - class Config: - orm_mode = True -``` - ---- - - - - - -**Add these enhanced methods:** - -```python -async def parse_rules_from_natural_language(self, natural_language: str) -> Dict[str, Any]: - """Enhanced natural language rule parsing with better prompts.""" - prompt_template = await self.prompt_manager.get_active_prompt("rule_parsing") - - if not prompt_template: - # Fallback prompt if none exists in database - prompt_template = """ - Parse the following natural language training rules into structured JSON format. - - Input: "{user_rules}" - - Required output format: - {{ - "max_rides_per_week": , - "min_rest_between_hard": , - "max_duration_hours": , - "intensity_limits": {{ - "max_zone_5_minutes_per_week": , - "max_consecutive_hard_days": - }}, - "weather_constraints": {{ - "min_temperature": , - "max_wind_speed": , - "no_rain": - }}, - "schedule_constraints": {{ - "preferred_days": [], - "avoid_days": [] - }}, - "confidence": <0.0-1.0> - }} - - Extract specific numbers and constraints. If information is missing, omit the field. - """ - - prompt = prompt_template.format(user_rules=natural_language) - response = await self._make_ai_request(prompt) - parsed_data = self._parse_rules_response(response) - - # Add confidence scoring - if "confidence" not in parsed_data: - parsed_data["confidence"] = self._calculate_parsing_confidence( - natural_language, parsed_data - ) - - return parsed_data - -def _calculate_parsing_confidence(self, input_text: str, parsed_data: Dict) -> float: - """Calculate confidence score for rule parsing.""" - confidence = 0.5 # Base confidence - - # Increase confidence for explicit numbers - import re - numbers = re.findall(r'\d+', input_text) - if len(numbers) >= 2: - confidence += 0.2 - - # Increase confidence for key training terms - training_terms = ['rides', 'hours', 'week', 'rest', 'recovery', 'training'] - found_terms = sum(1 for term in training_terms if term in input_text.lower()) - confidence += min(0.3, found_terms * 0.05) - - # Decrease confidence if parsed data is sparse - if len(parsed_data) < 3: - confidence -= 0.2 - - return max(0.0, min(1.0, confidence)) -``` - ---- - -# Phase 2: Frontend Core Features Implementation - -## Step 2.1: Simplified Rules Management - -### **File:** `frontend/src/pages/Rules.jsx` - -**Replace with simplified plaintext rules interface:** - -```jsx -import React, { useState, useEffect } from 'react'; -import { toast } from 'react-toastify'; -import RuleEditor from '../components/rules/RuleEditor'; -import RulePreview from '../components/rules/RulePreview'; -import RulesList from '../components/rules/RulesList'; -import { useAuth } from '../context/AuthContext'; -import * as ruleService from '../services/ruleService'; - -const Rules = () => { - const { apiKey } = useAuth(); - const [activeTab, setActiveTab] = useState('list'); - const [rules, setRules] = useState([]); - const [selectedRule, setSelectedRule] = useState(null); - const [naturalLanguageText, setNaturalLanguageText] = useState(''); - const [parsedRules, setParsedRules] = useState(null); - const [isLoading, setIsLoading] = useState(false); - - useEffect(() => { - loadRules(); - }, []); - - const loadRules = async () => { - try { - const response = await ruleService.getRules(); - setRules(response.data); - } catch (error) { - console.error('Failed to load rules:', error); - toast.error('Failed to load rules'); - } - }; - - const handleParseRules = async (parsedData) => { - setParsedRules(parsedData); - setActiveTab('preview'); - }; - - const handleSaveRules = async (ruleName, finalRules) => { - setIsLoading(true); - try { - const ruleData = { - name: ruleName, - jsonb_rules: finalRules, - user_defined: true, - version: 1 - }; - - await ruleService.createRule(ruleData); - toast.success('Rules saved successfully!'); - - // Reset form and reload rules - setNaturalLanguageText(''); - setParsedRules(null); - setActiveTab('list'); - await loadRules(); - } catch (error) { - console.error('Failed to save rules:', error); - toast.error('Failed to save rules'); - } finally { - setIsLoading(false); - } - }; - - const handleEditRule = (rule) => { - setSelectedRule(rule); - setNaturalLanguageText(rule.description || ''); - setParsedRules(rule.jsonb_rules); - setActiveTab('edit'); - }; - - return ( -
-
-
-

Training Rules

-

- Define your training constraints and preferences using natural language -

-
- - -
- - {/* Tab Navigation */} -
- -
- - {/* Tab Content */} - {activeTab === 'list' && ( - { - try { - await ruleService.deleteRule(ruleId); - toast.success('Rule deleted'); - await loadRules(); - } catch (error) { - toast.error('Failed to delete rule'); - } - }} - /> - )} - - {isEditing ? ( -
-
- - setRuleName(e.target.value)} - className="w-full p-3 border border-gray-300 rounded-lg" - /> -
- -
- -