Files
breakpilot-lehrer/studio-v2/lib/ActivityContext.tsx
Benjamin Boenisch 5a31f52310 Initial commit: breakpilot-lehrer - Lehrer KI Platform
Services: Admin-Lehrer, Backend-Lehrer, Studio v2, Website,
Klausur-Service, School-Service, Voice-Service, Geo-Service,
BreakPilot Drive, Agent-Core

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-11 23:47:26 +01:00

362 lines
10 KiB
TypeScript

'use client'
import React, { createContext, useContext, useState, useCallback, useEffect, useRef } from 'react'
/**
* Activity Tracking System
*
* Tracks user activities and calculates time savings compared to manual work.
* Used for the "Zeitersparnis" dashboard tile and weekly reports.
*/
// =============================================================================
// TYPES
// =============================================================================
export type ActivityType =
| 'vocab_extraction'
| 'klausur_correction'
| 'worksheet_creation'
| 'fairness_check'
| 'gutachten_generation'
export interface ActivityMetadata {
vocabCount?: number
studentCount?: number
criteriaCount?: number
elementCount?: number
pageCount?: number
description?: string
}
export interface ActivityLog {
id: string
type: ActivityType
startedAt: string // ISO string for serialization
completedAt: string
actualSeconds: number
estimatedManualSeconds: number
savedSeconds: number
metadata: ActivityMetadata
}
export interface ActivityStats {
todaySavedSeconds: number
weekSavedSeconds: number
monthSavedSeconds: number
totalSavedSeconds: number
activityCount: number
lastActivity: ActivityLog | null
}
interface ActiveActivity {
type: ActivityType
startedAt: Date
metadata: ActivityMetadata
}
interface ActivityContextType {
// Start tracking an activity
startActivity: (type: ActivityType, metadata?: ActivityMetadata) => void
// Complete the current activity
completeActivity: (additionalMetadata?: ActivityMetadata) => ActivityLog | null
// Cancel without saving
cancelActivity: () => void
// Check if currently tracking
isTracking: boolean
currentActivity: ActiveActivity | null
// Get statistics
stats: ActivityStats
// Get recent activities
recentActivities: ActivityLog[]
// Refresh stats from storage
refreshStats: () => void
}
// =============================================================================
// ESTIMATION FORMULAS
// =============================================================================
/**
* Calculate estimated manual time based on activity type and metadata
*/
function calculateManualEstimate(type: ActivityType, metadata: ActivityMetadata): number {
switch (type) {
case 'vocab_extraction': {
// Typing: ~25 seconds per vocab (word + translation + example)
// Layout: 15 minutes base
const vocabCount = metadata.vocabCount || 20
return (vocabCount * 25) + (15 * 60)
}
case 'klausur_correction': {
// Reading + grading + gutachten: ~45 minutes per student
const studentCount = metadata.studentCount || 1
return studentCount * 45 * 60
}
case 'worksheet_creation': {
// Design + formatting: ~30 seconds per element + 20 min base
const elementCount = metadata.elementCount || 10
return (elementCount * 30) + (20 * 60)
}
case 'fairness_check': {
// Manual comparison: ~5 minutes per student
const studentCount = metadata.studentCount || 10
return studentCount * 5 * 60
}
case 'gutachten_generation': {
// Writing gutachten manually: ~15 minutes per student
const studentCount = metadata.studentCount || 1
return studentCount * 15 * 60
}
default:
return 10 * 60 // 10 minutes default
}
}
// =============================================================================
// STORAGE
// =============================================================================
const STORAGE_KEY = 'breakpilot_activity_logs'
function loadActivities(): ActivityLog[] {
if (typeof window === 'undefined') return []
try {
const stored = localStorage.getItem(STORAGE_KEY)
return stored ? JSON.parse(stored) : []
} catch {
return []
}
}
function saveActivities(activities: ActivityLog[]): void {
if (typeof window === 'undefined') return
try {
// Keep only last 1000 activities to prevent storage overflow
const trimmed = activities.slice(-1000)
localStorage.setItem(STORAGE_KEY, JSON.stringify(trimmed))
} catch (e) {
console.error('Failed to save activities:', e)
}
}
function addActivity(activity: ActivityLog): void {
const activities = loadActivities()
activities.push(activity)
saveActivities(activities)
}
// =============================================================================
// STATISTICS CALCULATION
// =============================================================================
function calculateStats(activities: ActivityLog[]): ActivityStats {
const now = new Date()
const todayStart = new Date(now.getFullYear(), now.getMonth(), now.getDate())
const weekStart = new Date(todayStart)
weekStart.setDate(weekStart.getDate() - weekStart.getDay() + 1) // Monday
const monthStart = new Date(now.getFullYear(), now.getMonth(), 1)
let todaySavedSeconds = 0
let weekSavedSeconds = 0
let monthSavedSeconds = 0
let totalSavedSeconds = 0
for (const activity of activities) {
const activityDate = new Date(activity.completedAt)
totalSavedSeconds += activity.savedSeconds
if (activityDate >= monthStart) {
monthSavedSeconds += activity.savedSeconds
}
if (activityDate >= weekStart) {
weekSavedSeconds += activity.savedSeconds
}
if (activityDate >= todayStart) {
todaySavedSeconds += activity.savedSeconds
}
}
const lastActivity = activities.length > 0 ? activities[activities.length - 1] : null
return {
todaySavedSeconds,
weekSavedSeconds,
monthSavedSeconds,
totalSavedSeconds,
activityCount: activities.length,
lastActivity,
}
}
// =============================================================================
// CONTEXT
// =============================================================================
const ActivityContext = createContext<ActivityContextType | null>(null)
export function ActivityProvider({ children }: { children: React.ReactNode }) {
const [currentActivity, setCurrentActivity] = useState<ActiveActivity | null>(null)
const [stats, setStats] = useState<ActivityStats>({
todaySavedSeconds: 0,
weekSavedSeconds: 0,
monthSavedSeconds: 0,
totalSavedSeconds: 0,
activityCount: 0,
lastActivity: null,
})
const [recentActivities, setRecentActivities] = useState<ActivityLog[]>([])
// Load initial stats
useEffect(() => {
refreshStats()
}, [])
const refreshStats = useCallback(() => {
const activities = loadActivities()
setStats(calculateStats(activities))
setRecentActivities(activities.slice(-10).reverse())
}, [])
const startActivity = useCallback((type: ActivityType, metadata: ActivityMetadata = {}) => {
setCurrentActivity({
type,
startedAt: new Date(),
metadata,
})
}, [])
const completeActivity = useCallback((additionalMetadata: ActivityMetadata = {}): ActivityLog | null => {
if (!currentActivity) return null
const completedAt = new Date()
const actualSeconds = Math.round((completedAt.getTime() - currentActivity.startedAt.getTime()) / 1000)
const mergedMetadata = { ...currentActivity.metadata, ...additionalMetadata }
const estimatedManualSeconds = calculateManualEstimate(currentActivity.type, mergedMetadata)
const savedSeconds = Math.max(0, estimatedManualSeconds - actualSeconds)
const log: ActivityLog = {
id: `${Date.now()}-${Math.random().toString(36).slice(2, 9)}`,
type: currentActivity.type,
startedAt: currentActivity.startedAt.toISOString(),
completedAt: completedAt.toISOString(),
actualSeconds,
estimatedManualSeconds,
savedSeconds,
metadata: mergedMetadata,
}
addActivity(log)
setCurrentActivity(null)
refreshStats()
return log
}, [currentActivity, refreshStats])
const cancelActivity = useCallback(() => {
setCurrentActivity(null)
}, [])
return (
<ActivityContext.Provider
value={{
startActivity,
completeActivity,
cancelActivity,
isTracking: currentActivity !== null,
currentActivity,
stats,
recentActivities,
refreshStats,
}}
>
{children}
</ActivityContext.Provider>
)
}
export function useActivity() {
const context = useContext(ActivityContext)
if (!context) {
// Return a no-op version if not wrapped in provider
return {
startActivity: () => {},
completeActivity: () => null,
cancelActivity: () => {},
isTracking: false,
currentActivity: null,
stats: {
todaySavedSeconds: 0,
weekSavedSeconds: 0,
monthSavedSeconds: 0,
totalSavedSeconds: 0,
activityCount: 0,
lastActivity: null,
},
recentActivities: [],
refreshStats: () => {},
}
}
return context
}
// =============================================================================
// UTILITY FUNCTIONS
// =============================================================================
/**
* Format seconds as human-readable duration
*/
export function formatDuration(seconds: number): string {
if (seconds < 60) {
return `${seconds}s`
}
const hours = Math.floor(seconds / 3600)
const minutes = Math.floor((seconds % 3600) / 60)
if (hours > 0) {
return minutes > 0 ? `${hours}h ${minutes}min` : `${hours}h`
}
return `${minutes}min`
}
/**
* Format seconds as compact duration (for tiles)
*/
export function formatDurationCompact(seconds: number): { value: string; unit: string } {
if (seconds < 60) {
return { value: String(seconds), unit: 's' }
}
const hours = Math.floor(seconds / 3600)
const minutes = Math.floor((seconds % 3600) / 60)
if (hours > 0) {
const decimal = Math.round((minutes / 60) * 10) / 10
return { value: String(hours + (decimal > 0 ? decimal : 0)), unit: 'h' }
}
return { value: String(minutes), unit: 'min' }
}
/**
* Get activity type display name
*/
export function getActivityTypeName(type: ActivityType): string {
const names: Record<ActivityType, string> = {
vocab_extraction: 'Vokabelextraktion',
klausur_correction: 'Klausurkorrektur',
worksheet_creation: 'Arbeitsblatt erstellt',
fairness_check: 'Fairness-Check',
gutachten_generation: 'Gutachten generiert',
}
return names[type] || type
}