Skip to Content
📚 MyStoryFlow Docs — Your guide to preserving family stories

F019: Progress Tracking & Analytics (MyStoryFlow Integration)

Overview

Create a comprehensive progress tracking system within MyStoryFlow to monitor author improvement, celebrate milestones, and provide motivation through beautifully designed literary-themed visualizations.

Dependencies

  • F000: MyStoryFlow Monorepo Structure
  • F000B: Shared Packages (@mystoryflow/*)
  • F017: Author Dashboard
  • F018: Analysis Display

Quick Implementation

Using MyStoryFlow Components

  • LineChart with vintage paper background
  • Progress bars with amber/gold gradients
  • Achievement cards with ornate borders
  • Badge components with literary icons
  • Timeline view for history tracking

New Requirements

  • Progress calculation algorithms
  • Milestone detection with celebrations
  • Achievement system with rare books theme
  • Trend analysis with storytelling insights

MVP Implementation

1. Database Schema

-- Progress tracking (integrated with MyStoryFlow) CREATE TABLE author_progress ( id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), user_id UUID REFERENCES auth.users(id), manuscript_id UUID REFERENCES analyzer.manuscripts(id), metric_type VARCHAR(50) NOT NULL, -- 'overall', 'structure', 'character', etc. current_value DECIMAL(5,2) NOT NULL, previous_value DECIMAL(5,2), target_value DECIMAL(5,2), recorded_at TIMESTAMP DEFAULT NOW() ); -- Writing goals CREATE TABLE writing_goals ( id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), user_id UUID REFERENCES auth.users(id), goal_type VARCHAR(50) NOT NULL, -- 'score', 'word_count', 'analyses_per_month' target_value DECIMAL(10,2) NOT NULL, current_value DECIMAL(10,2) DEFAULT 0, deadline DATE, status VARCHAR(20) DEFAULT 'active', -- 'active', 'completed', 'expired' created_at TIMESTAMP DEFAULT NOW() ); -- Achievements/Milestones CREATE TABLE author_achievements ( id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), user_id UUID REFERENCES auth.users(id), achievement_code VARCHAR(50) NOT NULL, achievement_name VARCHAR(100) NOT NULL, achievement_description TEXT, earned_at TIMESTAMP DEFAULT NOW(), metadata JSONB DEFAULT '{}' ); -- Progress snapshots CREATE TABLE progress_snapshots ( id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), user_id UUID REFERENCES auth.users(id), snapshot_date DATE NOT NULL, total_manuscripts INTEGER, average_score DECIMAL(5,2), best_score DECIMAL(5,2), total_words_analyzed INTEGER, achievements_earned INTEGER, created_at TIMESTAMP DEFAULT NOW() ); -- Indexes CREATE INDEX idx_author_progress_user_id ON author_progress(user_id); CREATE INDEX idx_writing_goals_user_id ON writing_goals(user_id); CREATE INDEX idx_author_achievements_user_id ON author_achievements(user_id); CREATE UNIQUE INDEX idx_progress_snapshots_user_date ON progress_snapshots(user_id, snapshot_date);

2. Progress Tracking Service

// apps/analyzer/src/lib/progress/progress-tracker.ts import { getSupabaseBrowserClient } from '@mystoryflow/supabase' import { cache } from '@mystoryflow/cache' interface ProgressMetric { type: string current: number previous?: number change?: number trend: 'improving' | 'declining' | 'stable' } interface Achievement { code: string name: string description: string icon: string rarity: 'common' | 'rare' | 'epic' | 'legendary' } export class ProgressTracker { private supabase = getSupabaseBrowserClient() // Achievement definitions private achievements: Record<string, Achievement> = { first_analysis: { code: 'first_analysis', name: 'First Steps', description: 'Complete your first manuscript analysis', icon: '🎯', rarity: 'common' }, score_80_plus: { code: 'score_80_plus', name: 'Excellence Achieved', description: 'Score 80% or higher on any manuscript', icon: '⭐', rarity: 'rare' }, score_90_plus: { code: 'score_90_plus', name: 'Master Wordsmith', description: 'Score 90% or higher on any manuscript', icon: '🏆', rarity: 'epic' }, improvement_20: { code: 'improvement_20', name: 'Major Improvement', description: 'Improve a manuscript score by 20% or more', icon: '📈', rarity: 'rare' }, streak_7_days: { code: 'streak_7_days', name: 'Dedicated Writer', description: 'Analyze manuscripts 7 days in a row', icon: '🔥', rarity: 'rare' }, manuscripts_10: { code: 'manuscripts_10', name: 'Prolific Author', description: 'Analyze 10 different manuscripts', icon: '📚', rarity: 'epic' }, perfect_category: { code: 'perfect_category', name: 'Category Master', description: 'Score 95% or higher in any category', icon: '💯', rarity: 'legendary' } } async recordProgress( userId: string, manuscriptId: string, analysisData: any ): Promise<void> { // Record overall progress await this.recordMetric( userId, manuscriptId, 'overall', analysisData.overall_score ) // Record category progress for (const [category, score] of Object.entries(analysisData.category_scores)) { await this.recordMetric( userId, manuscriptId, category, score as number ) } // Check for achievements await this.checkAchievements(userId, analysisData) // Update daily snapshot await this.updateDailySnapshot(userId) } async recordMetric( userId: string, manuscriptId: string, metricType: string, value: number ): Promise<void> { // Get previous value const { data: previous } = await this.supabase .from('analyzer.author_progress') .select('current_value') .eq('user_id', userId) .eq('manuscript_id', manuscriptId) .eq('metric_type', metricType) .order('recorded_at', { ascending: false }) .limit(1) .single() // Insert new progress record await this.supabase .from('analyzer.author_progress') .insert({ user_id: userId, manuscript_id: manuscriptId, metric_type: metricType, current_value: value, previous_value: previous?.current_value }) } async checkAchievements(userId: string, analysisData: any): Promise<void> { const earnedAchievements: string[] = [] // Check first analysis const { count: analysisCount } = await this.supabase .from('analyzer.manuscript_analyses') .select('*', { count: 'exact' }) .eq('user_id', userId) if (analysisCount === 1) { earnedAchievements.push('first_analysis') } // Check score achievements if (analysisData.overall_score >= 80) { earnedAchievements.push('score_80_plus') } if (analysisData.overall_score >= 90) { earnedAchievements.push('score_90_plus') } // Check category perfection for (const score of Object.values(analysisData.category_scores)) { if (score >= 95) { earnedAchievements.push('perfect_category') break } } // Check improvement achievement const previousScore = await this.getPreviousScore( userId, analysisData.manuscript_id ) if (previousScore && analysisData.overall_score - previousScore >= 20) { earnedAchievements.push('improvement_20') } // Award new achievements for (const achievementCode of earnedAchievements) { await this.awardAchievement(userId, achievementCode) } } async awardAchievement(userId: string, achievementCode: string): Promise<void> { // Check if already earned const { data: existing } = await this.supabase .from('analyzer.author_achievements') .select('id') .eq('user_id', userId) .eq('achievement_code', achievementCode) .single() if (!existing) { const achievement = this.achievements[achievementCode] await this.supabase .from('analyzer.author_achievements') .insert({ user_id: userId, achievement_code: achievementCode, achievement_name: achievement.name, achievement_description: achievement.description, metadata: { icon: achievement.icon, rarity: achievement.rarity } }) } } async getProgressTrends( userId: string, days: number = 30 ): Promise<ProgressTrend[]> { const startDate = new Date() startDate.setDate(startDate.getDate() - days) const { data: snapshots } = await this.supabase .from('analyzer.progress_snapshots') .select('*') .eq('user_id', userId) .gte('snapshot_date', startDate.toISOString()) .order('snapshot_date') return this.calculateTrends(snapshots || []) } private calculateTrends(snapshots: any[]): ProgressTrend[] { if (snapshots.length < 2) return [] return snapshots.map((snapshot, index) => { if (index === 0) { return { date: snapshot.snapshot_date, averageScore: snapshot.average_score, change: 0, trend: 'stable' as const } } const previous = snapshots[index - 1] const change = snapshot.average_score - previous.average_score return { date: snapshot.snapshot_date, averageScore: snapshot.average_score, change, trend: change > 1 ? 'improving' : change < -1 ? 'declining' : 'stable' } }) } async updateDailySnapshot(userId: string): Promise<void> { const today = new Date().toISOString().split('T')[0] // Calculate snapshot data const { data: stats } = await this.supabase.rpc('get_user_progress_stats', { p_user_id: userId }) if (stats) { await this.supabase .from('analyzer.progress_snapshots') .upsert({ user_id: userId, snapshot_date: today, total_manuscripts: stats.total_manuscripts, average_score: stats.average_score, best_score: stats.best_score, total_words_analyzed: stats.total_words_analyzed, achievements_earned: stats.achievements_earned }) } } }

3. Progress Dashboard Component

// apps/analyzer-app/src/components/progress/ProgressDashboard.tsx 'use client' import { Card, Tabs, TabsContent, TabsList, TabsTrigger } from '@mystoryflow/ui' import { LineChart, BarChart } from '@mystoryflow/ui/charts' import { TrendingUp, Trophy, Target, Calendar } from 'lucide-react' import { ProgressOverview } from './ProgressOverview' import { AchievementGallery } from './AchievementGallery' import { GoalTracker } from './GoalTracker' import { ImprovementTimeline } from './ImprovementTimeline' interface ProgressDashboardProps { userId: string trends: any[] achievements: any[] goals: any[] recentProgress: any[] } export function ProgressDashboard({ userId, trends, achievements, goals, recentProgress }: ProgressDashboardProps) { return ( <div className="space-y-6"> {/* Overview Cards */} <ProgressOverview trends={trends} totalManuscripts={recentProgress.length} averageImprovement={calculateAverageImprovement(recentProgress)} /> {/* Main Content Tabs */} <Tabs defaultValue="trends" className="space-y-4"> <TabsList className="grid grid-cols-4 w-full"> <TabsTrigger value="trends" className="flex items-center gap-2"> <TrendingUp className="h-4 w-4" /> <span className="hidden sm:inline">Trends</span> </TabsTrigger> <TabsTrigger value="achievements" className="flex items-center gap-2"> <Trophy className="h-4 w-4" /> <span className="hidden sm:inline">Achievements</span> </TabsTrigger> <TabsTrigger value="goals" className="flex items-center gap-2"> <Target className="h-4 w-4" /> <span className="hidden sm:inline">Goals</span> </TabsTrigger> <TabsTrigger value="timeline" className="flex items-center gap-2"> <Calendar className="h-4 w-4" /> <span className="hidden sm:inline">Timeline</span> </TabsTrigger> </TabsList> <TabsContent value="trends"> <TrendAnalysis trends={trends} /> </TabsContent> <TabsContent value="achievements"> <AchievementGallery achievements={achievements} /> </TabsContent> <TabsContent value="goals"> <GoalTracker goals={goals} userId={userId} /> </TabsContent> <TabsContent value="timeline"> <ImprovementTimeline progress={recentProgress} /> </TabsContent> </Tabs> </div> ) } // Trend Analysis Component function TrendAnalysis({ trends }: { trends: any[] }) { const chartData = trends.map(trend => ({ date: new Date(trend.date).toLocaleDateString('en-US', { month: 'short', day: 'numeric' }), score: trend.averageScore, manuscripts: trend.totalManuscripts })) return ( <Card className="p-6"> <h3 className="text-lg font-semibold mb-4">Score Trends</h3> <div className="h-80"> <LineChart data={chartData} lines={[ { key: 'score', color: '#10b981', name: 'Average Score' } ]} xKey="date" yDomain={[0, 100]} /> </div> <div className="mt-6 grid grid-cols-3 gap-4 text-center"> <div> <p className="text-2xl font-bold text-green-600"> {trends[trends.length - 1]?.averageScore || 0}% </p> <p className="text-sm text-muted-foreground">Current Average</p> </div> <div> <p className="text-2xl font-bold"> {calculateTrend(trends)}% </p> <p className="text-sm text-muted-foreground">30-Day Change</p> </div> <div> <p className="text-2xl font-bold text-blue-600"> {findBestScore(trends)}% </p> <p className="text-sm text-muted-foreground">Best Score</p> </div> </div> </Card> ) } function calculateAverageImprovement(progress: any[]): number { if (progress.length === 0) return 0 const improvements = progress .filter(p => p.previous_value) .map(p => p.current_value - p.previous_value) if (improvements.length === 0) return 0 return Math.round( improvements.reduce((a, b) => a + b, 0) / improvements.length ) } function calculateTrend(trends: any[]): number { if (trends.length < 2) return 0 const first = trends[0].averageScore const last = trends[trends.length - 1].averageScore return Math.round(((last - first) / first) * 100) } function findBestScore(trends: any[]): number { return Math.max(...trends.map(t => t.averageScore || 0)) }
// apps/analyzer-app/src/components/progress/AchievementGallery.tsx 'use client' import { Card, Badge } from '@mystoryflow/ui' import { motion } from 'framer-motion' import { Lock } from 'lucide-react' interface Achievement { code: string name: string description: string earned_at?: string metadata: { icon: string rarity: string } } export function AchievementGallery({ achievements }: { achievements: Achievement[] }) { const allAchievements = getAchievementDefinitions() const earnedCodes = new Set(achievements.map(a => a.code)) const rarityColors = { common: 'bg-gray-200 dark:bg-gray-700', rare: 'bg-blue-200 dark:bg-blue-700', epic: 'bg-purple-200 dark:bg-purple-700', legendary: 'bg-amber-200 dark:bg-amber-700' } return ( <Card className="p-6"> <div className="flex items-center justify-between mb-6"> <h3 className="text-lg font-semibold">Achievements</h3> <Badge variant="secondary"> {achievements.length} / {allAchievements.length} Unlocked </Badge> </div> <div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4"> {allAchievements.map((achievement, index) => { const isEarned = earnedCodes.has(achievement.code) const earned = achievements.find(a => a.code === achievement.code) return ( <motion.div key={achievement.code} initial={{ opacity: 0, scale: 0.8 }} animate={{ opacity: 1, scale: 1 }} transition={{ delay: index * 0.05 }} className={` relative p-4 rounded-lg border-2 text-center ${isEarned ? 'border-primary bg-primary/5' : 'border-gray-200 dark:border-gray-700 opacity-50' } `} > {!isEarned && ( <div className="absolute inset-0 flex items-center justify-center"> <Lock className="h-8 w-8 text-gray-400" /> </div> )} <div className={`text-4xl mb-2 ${!isEarned && 'filter grayscale'}`}> {achievement.metadata.icon} </div> <h4 className="font-medium text-sm mb-1"> {achievement.name} </h4> <p className="text-xs text-muted-foreground mb-2"> {achievement.description} </p> <Badge variant="outline" className={`text-xs ${ isEarned ? rarityColors[achievement.metadata.rarity] : '' }`} > {achievement.metadata.rarity} </Badge> {earned && ( <p className="text-xs text-muted-foreground mt-2"> Earned {new Date(earned.earned_at).toLocaleDateString()} </p> )} </motion.div> ) })} </div> </Card> ) } function getAchievementDefinitions(): Achievement[] { return [ { code: 'first_analysis', name: 'First Steps', description: 'Complete your first analysis', metadata: { icon: '🎯', rarity: 'common' } }, { code: 'score_80_plus', name: 'Excellence', description: 'Score 80% or higher', metadata: { icon: '⭐', rarity: 'rare' } }, { code: 'score_90_plus', name: 'Master', description: 'Score 90% or higher', metadata: { icon: '🏆', rarity: 'epic' } }, { code: 'improvement_20', name: 'Major Progress', description: 'Improve by 20%+', metadata: { icon: '📈', rarity: 'rare' } }, { code: 'streak_7_days', name: 'Dedicated', description: '7-day streak', metadata: { icon: '🔥', rarity: 'rare' } }, { code: 'manuscripts_10', name: 'Prolific', description: '10 manuscripts', metadata: { icon: '📚', rarity: 'epic' } }, { code: 'perfect_category', name: 'Perfection', description: '95%+ in category', metadata: { icon: '💯', rarity: 'legendary' } }, { code: 'all_categories_70', name: 'Well Rounded', description: '70%+ all categories', metadata: { icon: '🎨', rarity: 'epic' } } ] }

5. Goal Tracking Component

// apps/analyzer-app/src/components/progress/GoalTracker.tsx 'use client' import { useState } from 'react' import { Card, Button, Progress, Badge, Input, Select } from '@mystoryflow/ui' import { Plus, Target, Calendar, TrendingUp } from 'lucide-react' import { createGoal, updateGoal } from '@/lib/progress' export function GoalTracker({ goals, userId }: { goals: any[], userId: string }) { const [showNewGoal, setShowNewGoal] = useState(false) const activeGoals = goals.filter(g => g.status === 'active') const completedGoals = goals.filter(g => g.status === 'completed') return ( <div className="space-y-6"> {/* Active Goals */} <Card className="p-6 bg-gradient-to-br from-amber-50 to-white border-amber-200"> <div className="flex items-center justify-between mb-4"> <h3 className="text-lg font-semibold">Active Goals</h3> <Button size="sm" onClick={() => setShowNewGoal(true)} > <Plus className="h-4 w-4 mr-2" /> New Goal </Button> </div> {activeGoals.length === 0 ? ( <p className="text-center text-muted-foreground py-8"> No active goals. Create one to track your progress! </p> ) : ( <div className="space-y-4"> {activeGoals.map(goal => ( <GoalCard key={goal.id} goal={goal} onUpdate={updateGoal} /> ))} </div> )} </Card> {/* Completed Goals */} {completedGoals.length > 0 && ( <Card className="p-6"> <h3 className="text-lg font-semibold mb-4">Completed Goals</h3> <div className="space-y-2"> {completedGoals.map(goal => ( <div key={goal.id} className="flex items-center justify-between p-3 bg-green-50 dark:bg-green-950/20 rounded-lg"> <div> <p className="font-medium">{getGoalLabel(goal)}</p> <p className="text-sm text-muted-foreground"> Completed {new Date(goal.completed_at).toLocaleDateString()} </p> </div> <Badge variant="success">✓ Complete</Badge> </div> ))} </div> </Card> )} {/* New Goal Form */} {showNewGoal && ( <NewGoalForm userId={userId} onClose={() => setShowNewGoal(false)} /> )} </div> ) } function GoalCard({ goal, onUpdate }: { goal: any, onUpdate: any }) { const progress = (goal.current_value / goal.target_value) * 100 const daysLeft = goal.deadline ? Math.ceil((new Date(goal.deadline) - new Date()) / (1000 * 60 * 60 * 24)) : null return ( <div className="space-y-3 p-4 border rounded-lg"> <div className="flex items-start justify-between"> <div> <p className="font-medium">{getGoalLabel(goal)}</p> {goal.deadline && ( <p className="text-sm text-muted-foreground flex items-center gap-1 mt-1"> <Calendar className="h-3 w-3" /> {daysLeft > 0 ? `${daysLeft} days left` : 'Overdue'} </p> )} </div> <Badge variant={progress >= 100 ? 'success' : 'secondary'}> {Math.round(progress)}% </Badge> </div> <Progress value={progress} className="h-2" /> <div className="flex justify-between text-sm text-muted-foreground"> <span>Current: {goal.current_value}</span> <span>Target: {goal.target_value}</span> </div> </div> ) } function getGoalLabel(goal: any): string { switch (goal.goal_type) { case 'score': return `Achieve ${goal.target_value}% average score` case 'word_count': return `Analyze ${goal.target_value.toLocaleString()} words` case 'analyses_per_month': return `Complete ${goal.target_value} analyses this month` case 'improvement': return `Improve by ${goal.target_value}%` default: return goal.goal_type } }

MVP Acceptance Criteria

  • Track progress across multiple analyses
  • Visual trend charts for improvement
  • Achievement system with unlockables
  • Goal setting and tracking
  • Progress snapshots over time
  • Milestone celebrations
  • Mobile-responsive progress views
  • Real-time progress updates

Post-MVP Enhancements

  • Writing streak tracking
  • Social sharing of achievements
  • Leaderboards (opt-in)
  • Progress predictions
  • Custom goal types
  • Progress reports via email
  • Gamification elements
  • Progress API for integrations

Implementation Time

  • Development: 2 days
  • Testing: 0.5 days
  • Total: 2.5 days

Dependencies

  • F016-REVISION-TRACKING (version history)
  • F017-AUTHOR-DASHBOARD (dashboard integration)
  • Progress calculation algorithms

Next Feature

After completion, proceed to F020-NOTIFICATION-SYSTEM for alerts.