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))
}4. Achievement Gallery Component
// 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.