feat(companion): Migrate Companion from admin-v2 to studio-v2 as pure lesson tool

Remove Companion module entirely from admin-v2. Rebuild in studio-v2 as a
focused lesson timer (no dashboard mode). Direct flow: start → active → ended.
Fix timer bug where lastTickRef reset prevented countdown. Add companion link
to Sidebar and i18n translations.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
BreakPilot Dev
2026-02-09 23:50:20 +01:00
parent 2f8ffb7352
commit 0f7be76e41
35 changed files with 327 additions and 1435 deletions

View File

@@ -1,364 +0,0 @@
/**
* Constants for Companion Module
* Phase colors, defaults, and configuration
*/
import { PhaseId, PhaseDurations, Phase, TeacherSettings } from './types'
// ============================================================================
// Phase Colors (Didactic Color Psychology)
// ============================================================================
export const PHASE_COLORS: Record<PhaseId, { hex: string; tailwind: string; gradient: string }> = {
einstieg: {
hex: '#4A90E2',
tailwind: 'bg-blue-500',
gradient: 'from-blue-500 to-blue-600',
},
erarbeitung: {
hex: '#F5A623',
tailwind: 'bg-orange-500',
gradient: 'from-orange-500 to-orange-600',
},
sicherung: {
hex: '#7ED321',
tailwind: 'bg-green-500',
gradient: 'from-green-500 to-green-600',
},
transfer: {
hex: '#9013FE',
tailwind: 'bg-purple-600',
gradient: 'from-purple-600 to-purple-700',
},
reflexion: {
hex: '#6B7280',
tailwind: 'bg-gray-500',
gradient: 'from-gray-500 to-gray-600',
},
}
// ============================================================================
// Phase Definitions
// ============================================================================
export const PHASE_SHORT_NAMES: Record<PhaseId, string> = {
einstieg: 'E',
erarbeitung: 'A',
sicherung: 'S',
transfer: 'T',
reflexion: 'R',
}
export const PHASE_DISPLAY_NAMES: Record<PhaseId, string> = {
einstieg: 'Einstieg',
erarbeitung: 'Erarbeitung',
sicherung: 'Sicherung',
transfer: 'Transfer',
reflexion: 'Reflexion',
}
export const PHASE_DESCRIPTIONS: Record<PhaseId, string> = {
einstieg: 'Motivation, Kontext setzen, Vorwissen aktivieren',
erarbeitung: 'Hauptinhalt, aktives Lernen, neue Konzepte',
sicherung: 'Konsolidierung, Zusammenfassung, Uebungen',
transfer: 'Anwendung, neue Kontexte, kreative Aufgaben',
reflexion: 'Rueckblick, Selbsteinschaetzung, Ausblick',
}
export const PHASE_ORDER: PhaseId[] = [
'einstieg',
'erarbeitung',
'sicherung',
'transfer',
'reflexion',
]
// ============================================================================
// Default Durations (in minutes)
// ============================================================================
export const DEFAULT_PHASE_DURATIONS: PhaseDurations = {
einstieg: 8,
erarbeitung: 20,
sicherung: 10,
transfer: 7,
reflexion: 5,
}
export const DEFAULT_LESSON_LENGTH = 45 // minutes (German standard)
export const EXTENDED_LESSON_LENGTH = 50 // minutes (with buffer)
// ============================================================================
// Timer Thresholds (in seconds)
// ============================================================================
export const TIMER_WARNING_THRESHOLD = 5 * 60 // 5 minutes = warning (yellow)
export const TIMER_CRITICAL_THRESHOLD = 2 * 60 // 2 minutes = critical (red)
// ============================================================================
// SVG Pie Timer Constants
// ============================================================================
export const PIE_TIMER_RADIUS = 42
export const PIE_TIMER_CIRCUMFERENCE = 2 * Math.PI * PIE_TIMER_RADIUS // ~263.89
export const PIE_TIMER_STROKE_WIDTH = 8
export const PIE_TIMER_SIZE = 120 // viewBox size
// ============================================================================
// Timer Color Classes
// ============================================================================
export const TIMER_COLOR_CLASSES = {
plenty: 'text-green-500 stroke-green-500',
warning: 'text-amber-500 stroke-amber-500',
critical: 'text-red-500 stroke-red-500',
overtime: 'text-red-600 stroke-red-600 animate-pulse',
}
export const TIMER_BG_COLORS = {
plenty: 'bg-green-500/10',
warning: 'bg-amber-500/10',
critical: 'bg-red-500/10',
overtime: 'bg-red-600/20',
}
// ============================================================================
// Keyboard Shortcuts
// ============================================================================
export const KEYBOARD_SHORTCUTS = {
PAUSE_RESUME: ' ', // Spacebar
EXTEND_5MIN: 'e',
NEXT_PHASE: 'n',
CLOSE_MODAL: 'Escape',
SHOW_HELP: '?',
} as const
export const KEYBOARD_SHORTCUT_DESCRIPTIONS: Record<string, string> = {
' ': 'Pause/Fortsetzen',
'e': '+5 Minuten',
'n': 'Naechste Phase',
'Escape': 'Modal schliessen',
'?': 'Hilfe anzeigen',
}
// ============================================================================
// Default Settings
// ============================================================================
export const DEFAULT_TEACHER_SETTINGS: TeacherSettings = {
defaultPhaseDurations: DEFAULT_PHASE_DURATIONS,
preferredLessonLength: DEFAULT_LESSON_LENGTH,
autoAdvancePhases: true,
soundNotifications: true,
showKeyboardShortcuts: true,
highContrastMode: false,
onboardingCompleted: false,
}
// ============================================================================
// System Templates
// ============================================================================
export const SYSTEM_TEMPLATES = [
{
templateId: 'standard-45',
name: 'Standard (45 Min)',
description: 'Klassische Unterrichtsstunde',
durations: DEFAULT_PHASE_DURATIONS,
isSystemTemplate: true,
},
{
templateId: 'double-90',
name: 'Doppelstunde (90 Min)',
description: 'Fuer laengere Arbeitsphasen',
durations: {
einstieg: 10,
erarbeitung: 45,
sicherung: 15,
transfer: 12,
reflexion: 8,
},
isSystemTemplate: true,
},
{
templateId: 'math-focused',
name: 'Mathematik-fokussiert',
description: 'Lange Erarbeitung und Sicherung',
durations: {
einstieg: 5,
erarbeitung: 25,
sicherung: 10,
transfer: 5,
reflexion: 5,
},
isSystemTemplate: true,
},
{
templateId: 'language-practice',
name: 'Sprachpraxis',
description: 'Betont kommunikative Phasen',
durations: {
einstieg: 10,
erarbeitung: 15,
sicherung: 8,
transfer: 10,
reflexion: 7,
},
isSystemTemplate: true,
},
]
// ============================================================================
// Suggestion Icons (Lucide icon names)
// ============================================================================
export const SUGGESTION_ICONS = {
grading: 'ClipboardCheck',
homework: 'BookOpen',
planning: 'Calendar',
meeting: 'Users',
deadline: 'Clock',
material: 'FileText',
communication: 'MessageSquare',
default: 'Lightbulb',
}
// ============================================================================
// Priority Colors
// ============================================================================
export const PRIORITY_COLORS = {
urgent: {
bg: 'bg-red-100',
text: 'text-red-700',
border: 'border-red-200',
dot: 'bg-red-500',
},
high: {
bg: 'bg-orange-100',
text: 'text-orange-700',
border: 'border-orange-200',
dot: 'bg-orange-500',
},
medium: {
bg: 'bg-yellow-100',
text: 'text-yellow-700',
border: 'border-yellow-200',
dot: 'bg-yellow-500',
},
low: {
bg: 'bg-slate-100',
text: 'text-slate-700',
border: 'border-slate-200',
dot: 'bg-slate-400',
},
}
// ============================================================================
// Event Type Icons & Colors
// ============================================================================
export const EVENT_TYPE_CONFIG = {
exam: {
icon: 'FileQuestion',
color: 'text-red-600',
bg: 'bg-red-50',
},
parent_meeting: {
icon: 'Users',
color: 'text-blue-600',
bg: 'bg-blue-50',
},
deadline: {
icon: 'Clock',
color: 'text-amber-600',
bg: 'bg-amber-50',
},
other: {
icon: 'Calendar',
color: 'text-slate-600',
bg: 'bg-slate-50',
},
}
// ============================================================================
// Storage Keys
// ============================================================================
export const STORAGE_KEYS = {
SETTINGS: 'companion_settings',
CURRENT_SESSION: 'companion_current_session',
ONBOARDING_STATE: 'companion_onboarding',
CUSTOM_TEMPLATES: 'companion_custom_templates',
LAST_MODE: 'companion_last_mode',
}
// ============================================================================
// API Endpoints (relative to backend)
// ============================================================================
export const API_ENDPOINTS = {
DASHBOARD: '/api/state/dashboard',
LESSON_START: '/api/classroom/sessions',
LESSON_UPDATE: '/api/classroom/sessions', // + /{id}
TEMPLATES: '/api/classroom/templates',
SETTINGS: '/api/teacher/settings',
FEEDBACK: '/api/feedback',
}
// ============================================================================
// Helper Functions
// ============================================================================
/**
* Create default phases array from durations
*/
export function createDefaultPhases(durations: PhaseDurations = DEFAULT_PHASE_DURATIONS): Phase[] {
return PHASE_ORDER.map((phaseId, index) => ({
id: phaseId,
shortName: PHASE_SHORT_NAMES[phaseId],
displayName: PHASE_DISPLAY_NAMES[phaseId],
duration: durations[phaseId],
status: index === 0 ? 'active' : 'planned',
color: PHASE_COLORS[phaseId].hex,
}))
}
/**
* Calculate total duration from phase durations
*/
export function calculateTotalDuration(durations: PhaseDurations): number {
return Object.values(durations).reduce((sum, d) => sum + d, 0)
}
/**
* Get timer color status based on remaining time
*/
export function getTimerColorStatus(
remainingSeconds: number,
isOvertime: boolean
): 'plenty' | 'warning' | 'critical' | 'overtime' {
if (isOvertime) return 'overtime'
if (remainingSeconds <= TIMER_CRITICAL_THRESHOLD) return 'critical'
if (remainingSeconds <= TIMER_WARNING_THRESHOLD) return 'warning'
return 'plenty'
}
/**
* Format seconds as MM:SS
*/
export function formatTime(seconds: number): string {
const absSeconds = Math.abs(seconds)
const mins = Math.floor(absSeconds / 60)
const secs = absSeconds % 60
const sign = seconds < 0 ? '-' : ''
return `${sign}${mins.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`
}
/**
* Format minutes as "X Min"
*/
export function formatMinutes(minutes: number): string {
return `${minutes} Min`
}

View File

@@ -1,2 +0,0 @@
export * from './types'
export * from './constants'

View File

@@ -1,329 +0,0 @@
/**
* TypeScript Types for Companion Module
* Migration from Flask companion.py/companion_js.py
*/
// ============================================================================
// Phase System
// ============================================================================
export type PhaseId = 'einstieg' | 'erarbeitung' | 'sicherung' | 'transfer' | 'reflexion'
export interface Phase {
id: PhaseId
shortName: string // E, A, S, T, R
displayName: string
duration: number // minutes
status: 'planned' | 'active' | 'completed'
actualTime?: number // seconds (actual time spent)
color: string // hex color
}
export interface PhaseContext {
currentPhase: PhaseId
phaseDisplayName: string
}
// ============================================================================
// Dashboard / Companion Mode
// ============================================================================
export interface CompanionStats {
classesCount: number
studentsCount: number
learningUnitsCreated: number
gradesEntered: number
}
export interface Progress {
percentage: number
completed: number
total: number
}
export type SuggestionPriority = 'urgent' | 'high' | 'medium' | 'low'
export interface Suggestion {
id: string
title: string
description: string
priority: SuggestionPriority
icon: string // lucide icon name
actionTarget: string // navigation path
estimatedTime: number // minutes
}
export type EventType = 'exam' | 'parent_meeting' | 'deadline' | 'other'
export interface UpcomingEvent {
id: string
title: string
date: string // ISO date string
type: EventType
inDays: number
}
export interface CompanionData {
context: PhaseContext
stats: CompanionStats
phases: Phase[]
progress: Progress
suggestions: Suggestion[]
upcomingEvents: UpcomingEvent[]
}
// ============================================================================
// Lesson Mode
// ============================================================================
export type LessonStatus =
| 'not_started'
| 'in_progress'
| 'paused'
| 'completed'
| 'overtime'
export interface LessonPhase {
phase: PhaseId
duration: number // planned duration in minutes
status: 'planned' | 'active' | 'completed' | 'skipped'
actualTime: number // actual time spent in seconds
startedAt?: string // ISO timestamp
completedAt?: string // ISO timestamp
}
export interface Homework {
id: string
title: string
description?: string
dueDate: string // ISO date
attachments?: string[]
completed?: boolean
}
export interface Material {
id: string
title: string
type: 'document' | 'video' | 'presentation' | 'link' | 'other'
url?: string
fileName?: string
}
export interface LessonReflection {
rating: number // 1-5 stars
notes: string
nextSteps: string
savedAt?: string
}
export interface LessonSession {
sessionId: string
classId: string
className: string
subject: string
topic?: string
startTime: string // ISO timestamp
endTime?: string // ISO timestamp
phases: LessonPhase[]
totalPlannedDuration: number // minutes
currentPhaseIndex: number
elapsedTime: number // seconds
isPaused: boolean
pausedAt?: string
pauseDuration: number // total pause time in seconds
overtimeMinutes: number
status: LessonStatus
homeworkList: Homework[]
materials: Material[]
reflection?: LessonReflection
}
// ============================================================================
// Lesson Templates
// ============================================================================
export interface PhaseDurations {
einstieg: number
erarbeitung: number
sicherung: number
transfer: number
reflexion: number
}
export interface LessonTemplate {
templateId: string
name: string
description?: string
subject?: string
durations: PhaseDurations
isSystemTemplate: boolean
createdBy?: string
createdAt?: string
}
// ============================================================================
// Settings
// ============================================================================
export interface TeacherSettings {
defaultPhaseDurations: PhaseDurations
preferredLessonLength: number // minutes (default 45)
autoAdvancePhases: boolean
soundNotifications: boolean
showKeyboardShortcuts: boolean
highContrastMode: boolean
onboardingCompleted: boolean
selectedTemplateId?: string
}
// ============================================================================
// Timer State
// ============================================================================
export type TimerColorStatus = 'plenty' | 'warning' | 'critical' | 'overtime'
export interface TimerState {
isRunning: boolean
isPaused: boolean
elapsedSeconds: number
remainingSeconds: number
totalSeconds: number
progress: number // 0-1
colorStatus: TimerColorStatus
currentPhase: LessonPhase | null
}
// ============================================================================
// Forms
// ============================================================================
export interface LessonStartFormData {
classId: string
subject: string
topic?: string
templateId?: string
customDurations?: PhaseDurations
}
export interface Class {
id: string
name: string
grade: string
studentCount: number
}
// ============================================================================
// Feedback
// ============================================================================
export type FeedbackType = 'bug' | 'feature' | 'feedback'
export interface FeedbackSubmission {
type: FeedbackType
title: string
description: string
screenshot?: string // base64
sessionId?: string
metadata?: Record<string, unknown>
}
// ============================================================================
// Onboarding
// ============================================================================
export interface OnboardingStep {
step: number
title: string
description: string
completed: boolean
}
export interface OnboardingState {
currentStep: number
totalSteps: number
steps: OnboardingStep[]
selectedState?: string // Bundesland
selectedSchoolType?: string
completed: boolean
}
// ============================================================================
// WebSocket Messages
// ============================================================================
export type WSMessageType =
| 'phase_update'
| 'timer_tick'
| 'overtime_warning'
| 'pause_toggle'
| 'session_end'
| 'sync_request'
export interface WSMessage {
type: WSMessageType
payload: {
sessionId: string
phase?: number
elapsed?: number
isPaused?: boolean
overtimeMinutes?: number
[key: string]: unknown
}
timestamp: string
}
// ============================================================================
// API Responses
// ============================================================================
export interface APIResponse<T> {
success: boolean
data?: T
error?: string
message?: string
}
export interface DashboardResponse extends APIResponse<CompanionData> {}
export interface LessonResponse extends APIResponse<LessonSession> {}
export interface TemplatesResponse extends APIResponse<{ templates: LessonTemplate[] }> {}
export interface SettingsResponse extends APIResponse<TeacherSettings> {}
// ============================================================================
// Component Props
// ============================================================================
export type CompanionMode = 'companion' | 'lesson' | 'classic'
export interface ModeToggleProps {
currentMode: CompanionMode
onModeChange: (mode: CompanionMode) => void
}
export interface PhaseTimelineProps {
phases: Phase[]
currentPhaseIndex: number
onPhaseClick?: (index: number) => void
}
export interface VisualPieTimerProps {
progress: number // 0-1
remainingSeconds: number
totalSeconds: number
colorStatus: TimerColorStatus
isPaused: boolean
currentPhaseName: string
phaseColor: string
}
export interface QuickActionsBarProps {
onExtend: (minutes: number) => void
onPause: () => void
onResume: () => void
onSkip: () => void
isPaused: boolean
isLastPhase: boolean
disabled?: boolean
}