From 0f7be76e413dc321c569e451c7b5b5f23c6d6d06 Mon Sep 17 00:00:00 2001 From: BreakPilot Dev Date: Mon, 9 Feb 2026 23:50:20 +0100 Subject: [PATCH] feat(companion): Migrate Companion from admin-v2 to studio-v2 as pure lesson tool MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- .../(admin)/development/companion/page.tsx | 39 --- .../app/(admin)/education/companion/page.tsx | 76 ----- admin-v2/app/api/admin/companion/route.ts | 102 ------ .../companion/CompanionDashboard.tsx | 312 ------------------ admin-v2/components/companion/ModeToggle.tsx | 61 ---- .../companion/companion-mode/EventsCard.tsx | 173 ---------- .../companion/companion-mode/StatsGrid.tsx | 114 ------- .../companion-mode/SuggestionList.tsx | 170 ---------- admin-v2/components/companion/index.ts | 24 -- .../companion/lesson-mode/LessonContainer.tsx | 80 ----- admin-v2/hooks/companion/useCompanionData.ts | 156 --------- .../app/api}/companion/feedback/route.ts | 4 +- .../app/api}/companion/lesson/route.ts | 8 +- .../app/api}/companion/settings/route.ts | 6 +- studio-v2/app/companion/page.tsx | 51 +++ studio-v2/components/Sidebar.tsx | 7 + .../companion/CompanionDashboard.tsx | 222 +++++++++++++ .../companion/lesson-mode/HomeworkSection.tsx | 0 .../lesson-mode/LessonActiveView.tsx | 2 +- .../companion/lesson-mode/LessonEndedView.tsx | 0 .../companion/lesson-mode/LessonStartForm.tsx | 0 .../companion/lesson-mode}/PhaseTimeline.tsx | 113 ++----- .../companion/lesson-mode/QuickActionsBar.tsx | 0 .../lesson-mode/ReflectionSection.tsx | 0 .../companion/lesson-mode/VisualPieTimer.tsx | 0 .../companion/modals/FeedbackModal.tsx | 0 .../companion/modals/OnboardingModal.tsx | 0 .../companion/modals/SettingsModal.tsx | 0 .../hooks/companion/index.ts | 1 - .../hooks/companion/useKeyboardShortcuts.ts | 0 .../hooks/companion/useLessonSession.ts | 27 +- .../lib/companion/constants.ts | 0 .../lib/companion/index.ts | 0 .../lib/companion/types.ts | 0 studio-v2/lib/i18n.ts | 14 +- 35 files changed, 327 insertions(+), 1435 deletions(-) delete mode 100644 admin-v2/app/(admin)/development/companion/page.tsx delete mode 100644 admin-v2/app/(admin)/education/companion/page.tsx delete mode 100644 admin-v2/app/api/admin/companion/route.ts delete mode 100644 admin-v2/components/companion/CompanionDashboard.tsx delete mode 100644 admin-v2/components/companion/ModeToggle.tsx delete mode 100644 admin-v2/components/companion/companion-mode/EventsCard.tsx delete mode 100644 admin-v2/components/companion/companion-mode/StatsGrid.tsx delete mode 100644 admin-v2/components/companion/companion-mode/SuggestionList.tsx delete mode 100644 admin-v2/components/companion/index.ts delete mode 100644 admin-v2/components/companion/lesson-mode/LessonContainer.tsx delete mode 100644 admin-v2/hooks/companion/useCompanionData.ts rename {admin-v2/app/api/admin => studio-v2/app/api}/companion/feedback/route.ts (97%) rename {admin-v2/app/api/admin => studio-v2/app/api}/companion/lesson/route.ts (97%) rename {admin-v2/app/api/admin => studio-v2/app/api}/companion/settings/route.ts (96%) create mode 100644 studio-v2/app/companion/page.tsx create mode 100644 studio-v2/components/companion/CompanionDashboard.tsx rename {admin-v2 => studio-v2}/components/companion/lesson-mode/HomeworkSection.tsx (100%) rename {admin-v2 => studio-v2}/components/companion/lesson-mode/LessonActiveView.tsx (98%) rename {admin-v2 => studio-v2}/components/companion/lesson-mode/LessonEndedView.tsx (100%) rename {admin-v2 => studio-v2}/components/companion/lesson-mode/LessonStartForm.tsx (100%) rename {admin-v2/components/companion/companion-mode => studio-v2/components/companion/lesson-mode}/PhaseTimeline.tsx (51%) rename {admin-v2 => studio-v2}/components/companion/lesson-mode/QuickActionsBar.tsx (100%) rename {admin-v2 => studio-v2}/components/companion/lesson-mode/ReflectionSection.tsx (100%) rename {admin-v2 => studio-v2}/components/companion/lesson-mode/VisualPieTimer.tsx (100%) rename {admin-v2 => studio-v2}/components/companion/modals/FeedbackModal.tsx (100%) rename {admin-v2 => studio-v2}/components/companion/modals/OnboardingModal.tsx (100%) rename {admin-v2 => studio-v2}/components/companion/modals/SettingsModal.tsx (100%) rename {admin-v2 => studio-v2}/hooks/companion/index.ts (72%) rename {admin-v2 => studio-v2}/hooks/companion/useKeyboardShortcuts.ts (100%) rename {admin-v2 => studio-v2}/hooks/companion/useLessonSession.ts (95%) rename {admin-v2 => studio-v2}/lib/companion/constants.ts (100%) rename {admin-v2 => studio-v2}/lib/companion/index.ts (100%) rename {admin-v2 => studio-v2}/lib/companion/types.ts (100%) diff --git a/admin-v2/app/(admin)/development/companion/page.tsx b/admin-v2/app/(admin)/development/companion/page.tsx deleted file mode 100644 index 8499d6a..0000000 --- a/admin-v2/app/(admin)/development/companion/page.tsx +++ /dev/null @@ -1,39 +0,0 @@ -'use client' - -import { PagePurpose } from '@/components/common/PagePurpose' -import { getModuleByHref } from '@/lib/navigation' -import { GraduationCap, Construction } from 'lucide-react' - -export default function CompanionPage() { - const moduleInfo = getModuleByHref('/development/companion') - - return ( -
- {moduleInfo && ( - - )} - -
-
-
- -
-
-

Companion Dev

-

- Lesson-Modus Entwicklung fuer strukturiertes Lernen. -

-
- - In Entwicklung -
-
-
- ) -} diff --git a/admin-v2/app/(admin)/education/companion/page.tsx b/admin-v2/app/(admin)/education/companion/page.tsx deleted file mode 100644 index 448da1a..0000000 --- a/admin-v2/app/(admin)/education/companion/page.tsx +++ /dev/null @@ -1,76 +0,0 @@ -'use client' - -import { Suspense } from 'react' -import { PagePurpose } from '@/components/common/PagePurpose' -import { getModuleByHref } from '@/lib/navigation' -import { CompanionDashboard } from '@/components/companion/CompanionDashboard' -import { GraduationCap } from 'lucide-react' - -function LoadingFallback() { - return ( -
- {/* Header Skeleton */} -
-
-
- {[1, 2, 3, 4].map((i) => ( -
- ))} -
-
- - {/* Phase Timeline Skeleton */} -
-
-
- {[1, 2, 3, 4, 5].map((i) => ( -
-
- {i < 5 &&
} -
- ))} -
-
- - {/* Stats Skeleton */} -
- {[1, 2, 3, 4].map((i) => ( -
-
-
-
- ))} -
- - {/* Content Skeleton */} -
-
-
-
-
- ) -} - -export default function CompanionPage() { - const moduleInfo = getModuleByHref('/education/companion') - - return ( -
- {/* Page Purpose Header */} - {moduleInfo && ( - - )} - - {/* Main Companion Dashboard */} - }> - - -
- ) -} diff --git a/admin-v2/app/api/admin/companion/route.ts b/admin-v2/app/api/admin/companion/route.ts deleted file mode 100644 index b33c6f1..0000000 --- a/admin-v2/app/api/admin/companion/route.ts +++ /dev/null @@ -1,102 +0,0 @@ -import { NextResponse } from 'next/server' - -/** - * GET /api/admin/companion - * Proxy to backend /api/state/dashboard for companion dashboard data - */ -export async function GET() { - try { - const backendUrl = process.env.BACKEND_URL || 'http://localhost:8000' - - // TODO: Replace with actual backend call when endpoint is available - // const response = await fetch(`${backendUrl}/api/state/dashboard`, { - // method: 'GET', - // headers: { - // 'Content-Type': 'application/json', - // }, - // }) - // - // if (!response.ok) { - // throw new Error(`Backend responded with ${response.status}`) - // } - // - // const data = await response.json() - // return NextResponse.json(data) - - // Mock response for development - const mockData = { - success: true, - data: { - context: { - currentPhase: 'erarbeitung', - phaseDisplayName: 'Erarbeitung', - }, - stats: { - classesCount: 4, - studentsCount: 96, - learningUnitsCreated: 23, - gradesEntered: 156, - }, - phases: [ - { id: 'einstieg', shortName: 'E', displayName: 'Einstieg', duration: 8, status: 'completed', color: '#4A90E2' }, - { id: 'erarbeitung', shortName: 'A', displayName: 'Erarbeitung', duration: 20, status: 'active', color: '#F5A623' }, - { id: 'sicherung', shortName: 'S', displayName: 'Sicherung', duration: 10, status: 'planned', color: '#7ED321' }, - { id: 'transfer', shortName: 'T', displayName: 'Transfer', duration: 7, status: 'planned', color: '#9013FE' }, - { id: 'reflexion', shortName: 'R', displayName: 'Reflexion', duration: 5, status: 'planned', color: '#6B7280' }, - ], - progress: { - percentage: 65, - completed: 13, - total: 20, - }, - suggestions: [ - { - id: '1', - title: 'Klausuren korrigieren', - description: 'Deutsch LK - 12 unkorrigierte Arbeiten warten', - priority: 'urgent', - icon: 'ClipboardCheck', - actionTarget: '/ai/klausur-korrektur', - estimatedTime: 120, - }, - { - id: '2', - title: 'Elternsprechtag vorbereiten', - description: 'Notenuebersicht fuer 8b erstellen', - priority: 'high', - icon: 'Users', - actionTarget: '/education/grades', - estimatedTime: 30, - }, - ], - upcomingEvents: [ - { - id: 'e1', - title: 'Mathe-Test 9b', - date: new Date(Date.now() + 2 * 24 * 60 * 60 * 1000).toISOString(), - type: 'exam', - inDays: 2, - }, - { - id: 'e2', - title: 'Elternsprechtag', - date: new Date(Date.now() + 5 * 24 * 60 * 60 * 1000).toISOString(), - type: 'parent_meeting', - inDays: 5, - }, - ], - }, - } - - return NextResponse.json(mockData) - } catch (error) { - console.error('Companion dashboard error:', error) - return NextResponse.json( - { - success: false, - error: error instanceof Error ? error.message : 'Unknown error', - }, - { status: 500 } - ) - } -} diff --git a/admin-v2/components/companion/CompanionDashboard.tsx b/admin-v2/components/companion/CompanionDashboard.tsx deleted file mode 100644 index 5e8cb00..0000000 --- a/admin-v2/components/companion/CompanionDashboard.tsx +++ /dev/null @@ -1,312 +0,0 @@ -'use client' - -import { useState, useEffect, useCallback } from 'react' -import { Settings, MessageSquare, HelpCircle, RefreshCw } from 'lucide-react' -import { CompanionMode, TeacherSettings, FeedbackType } from '@/lib/companion/types' -import { DEFAULT_TEACHER_SETTINGS, STORAGE_KEYS } from '@/lib/companion/constants' - -// Components -import { ModeToggle } from './ModeToggle' -import { PhaseTimeline } from './companion-mode/PhaseTimeline' -import { StatsGrid } from './companion-mode/StatsGrid' -import { SuggestionList } from './companion-mode/SuggestionList' -import { EventsCard } from './companion-mode/EventsCard' -import { LessonContainer } from './lesson-mode/LessonContainer' -import { SettingsModal } from './modals/SettingsModal' -import { FeedbackModal } from './modals/FeedbackModal' -import { OnboardingModal } from './modals/OnboardingModal' - -// Hooks -import { useCompanionData } from '@/hooks/companion/useCompanionData' -import { useLessonSession } from '@/hooks/companion/useLessonSession' -import { useKeyboardShortcuts } from '@/hooks/companion/useKeyboardShortcuts' - -export function CompanionDashboard() { - // Mode state - const [mode, setMode] = useState('companion') - - // Modal states - const [showSettings, setShowSettings] = useState(false) - const [showFeedback, setShowFeedback] = useState(false) - const [showOnboarding, setShowOnboarding] = useState(false) - - // Settings - const [settings, setSettings] = useState(DEFAULT_TEACHER_SETTINGS) - - // Load settings from localStorage - useEffect(() => { - const stored = localStorage.getItem(STORAGE_KEYS.SETTINGS) - if (stored) { - try { - const parsed = JSON.parse(stored) - setSettings({ ...DEFAULT_TEACHER_SETTINGS, ...parsed }) - } catch { - // Invalid stored settings - } - } - - // Check if onboarding needed - const onboardingStored = localStorage.getItem(STORAGE_KEYS.ONBOARDING_STATE) - if (!onboardingStored) { - setShowOnboarding(true) - } - - // Restore last mode - const lastMode = localStorage.getItem(STORAGE_KEYS.LAST_MODE) as CompanionMode - if (lastMode && ['companion', 'lesson', 'classic'].includes(lastMode)) { - setMode(lastMode) - } - }, []) - - // Save mode to localStorage - useEffect(() => { - localStorage.setItem(STORAGE_KEYS.LAST_MODE, mode) - }, [mode]) - - // Companion data hook - const { data: companionData, loading: companionLoading, refresh } = useCompanionData() - - // Lesson session hook - const { - session, - startLesson, - endLesson, - pauseLesson, - resumeLesson, - extendTime, - skipPhase, - saveReflection, - addHomework, - removeHomework, - isPaused, - } = useLessonSession({ - onOvertimeStart: () => { - // Play sound if enabled - if (settings.soundNotifications) { - // TODO: Play notification sound - } - }, - }) - - // Handle pause/resume toggle - const handlePauseToggle = useCallback(() => { - if (isPaused) { - resumeLesson() - } else { - pauseLesson() - } - }, [isPaused, pauseLesson, resumeLesson]) - - // Keyboard shortcuts - useKeyboardShortcuts({ - onPauseResume: mode === 'lesson' && session ? handlePauseToggle : undefined, - onExtend: mode === 'lesson' && session && !isPaused ? () => extendTime(5) : undefined, - onNextPhase: mode === 'lesson' && session && !isPaused ? skipPhase : undefined, - onCloseModal: () => { - setShowSettings(false) - setShowFeedback(false) - setShowOnboarding(false) - }, - enabled: settings.showKeyboardShortcuts, - }) - - // Handle settings save - const handleSaveSettings = (newSettings: TeacherSettings) => { - setSettings(newSettings) - localStorage.setItem(STORAGE_KEYS.SETTINGS, JSON.stringify(newSettings)) - } - - // Handle feedback submit - const handleFeedbackSubmit = async (type: FeedbackType, title: string, description: string) => { - const response = await fetch('/api/admin/companion/feedback', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ - type, - title, - description, - sessionId: session?.sessionId, - }), - }) - - if (!response.ok) { - throw new Error('Failed to submit feedback') - } - } - - // Handle onboarding complete - const handleOnboardingComplete = (data: { state?: string; schoolType?: string }) => { - localStorage.setItem(STORAGE_KEYS.ONBOARDING_STATE, JSON.stringify({ - ...data, - completed: true, - completedAt: new Date().toISOString(), - })) - setShowOnboarding(false) - setSettings({ ...settings, onboardingCompleted: true }) - } - - // Handle lesson start - const handleStartLesson = (data: { classId: string; subject: string; topic?: string; templateId?: string }) => { - startLesson(data) - setMode('lesson') - } - - return ( -
- {/* Header */} -
- - -
- {/* Refresh Button */} - {mode === 'companion' && ( - - )} - - {/* Feedback Button */} - - - {/* Settings Button */} - - - {/* Help Button */} - -
-
- - {/* Main Content */} - {mode === 'companion' && ( -
- {/* Phase Timeline */} -
-

Aktuelle Phase

- {companionData ? ( - p.status === 'active')} - /> - ) : ( -
- )} -
- - {/* Stats */} - - - {/* Two Column Layout */} -
- {/* Suggestions */} - { - // Navigate to action target - window.location.href = suggestion.actionTarget - }} - /> - - {/* Events */} - -
- - {/* Quick Start Lesson Button */} -
-
-
-

Bereit fuer die naechste Stunde?

-

Starten Sie den Lesson-Modus fuer strukturierten Unterricht.

-
- -
-
-
- )} - - {mode === 'lesson' && ( - - )} - - {mode === 'classic' && ( -
-

Classic Mode

-

- Die klassische Ansicht ohne Timer und Phasenstruktur. -

-

- Dieser Modus ist fuer flexible Unterrichtsgestaltung gedacht. -

-
- )} - - {/* Modals */} - setShowSettings(false)} - settings={settings} - onSave={handleSaveSettings} - /> - - setShowFeedback(false)} - onSubmit={handleFeedbackSubmit} - /> - - setShowOnboarding(false)} - onComplete={handleOnboardingComplete} - /> -
- ) -} diff --git a/admin-v2/components/companion/ModeToggle.tsx b/admin-v2/components/companion/ModeToggle.tsx deleted file mode 100644 index 9cb6a60..0000000 --- a/admin-v2/components/companion/ModeToggle.tsx +++ /dev/null @@ -1,61 +0,0 @@ -'use client' - -import { GraduationCap, Timer, Layout } from 'lucide-react' -import { CompanionMode } from '@/lib/companion/types' - -interface ModeToggleProps { - currentMode: CompanionMode - onModeChange: (mode: CompanionMode) => void - disabled?: boolean -} - -const modes: { id: CompanionMode; label: string; icon: React.ReactNode; description: string }[] = [ - { - id: 'companion', - label: 'Companion', - icon: , - description: 'Dashboard mit Vorschlaegen', - }, - { - id: 'lesson', - label: 'Lesson', - icon: , - description: 'Timer und Phasen', - }, - { - id: 'classic', - label: 'Classic', - icon: , - description: 'Klassische Ansicht', - }, -] - -export function ModeToggle({ currentMode, onModeChange, disabled }: ModeToggleProps) { - return ( -
- {modes.map((mode) => { - const isActive = currentMode === mode.id - return ( - - ) - })} -
- ) -} diff --git a/admin-v2/components/companion/companion-mode/EventsCard.tsx b/admin-v2/components/companion/companion-mode/EventsCard.tsx deleted file mode 100644 index e39021e..0000000 --- a/admin-v2/components/companion/companion-mode/EventsCard.tsx +++ /dev/null @@ -1,173 +0,0 @@ -'use client' - -import { Calendar, FileQuestion, Users, Clock, ChevronRight } from 'lucide-react' -import { UpcomingEvent, EventType } from '@/lib/companion/types' -import { EVENT_TYPE_CONFIG } from '@/lib/companion/constants' - -interface EventsCardProps { - events: UpcomingEvent[] - onEventClick?: (event: UpcomingEvent) => void - loading?: boolean - maxItems?: number -} - -const iconMap: Record> = { - FileQuestion, - Users, - Clock, - Calendar, -} - -function getEventIcon(type: EventType) { - const config = EVENT_TYPE_CONFIG[type] - const Icon = iconMap[config.icon] || Calendar - return { Icon, ...config } -} - -function formatEventDate(dateStr: string, inDays: number): string { - if (inDays === 0) return 'Heute' - if (inDays === 1) return 'Morgen' - if (inDays < 7) return `In ${inDays} Tagen` - - const date = new Date(dateStr) - return date.toLocaleDateString('de-DE', { - weekday: 'short', - day: 'numeric', - month: 'short', - }) -} - -interface EventItemProps { - event: UpcomingEvent - onClick?: () => void -} - -function EventItem({ event, onClick }: EventItemProps) { - const { Icon, color, bg } = getEventIcon(event.type) - const isUrgent = event.inDays <= 2 - - return ( - - ) -} - -export function EventsCard({ - events, - onEventClick, - loading, - maxItems = 5, -}: EventsCardProps) { - const displayEvents = events.slice(0, maxItems) - - if (loading) { - return ( -
-
- -

Termine

-
-
- {[1, 2, 3].map((i) => ( -
- ))} -
-
- ) - } - - if (events.length === 0) { - return ( -
-
- -

Termine

-
-
- -

Keine anstehenden Termine

-
-
- ) - } - - return ( -
-
-
- -

Termine

-
- - {events.length} Termin{events.length !== 1 ? 'e' : ''} - -
- -
- {displayEvents.map((event) => ( - onEventClick?.(event)} - /> - ))} -
- - {events.length > maxItems && ( - - )} -
- ) -} - -/** - * Compact inline version for header/toolbar - */ -export function EventsInline({ events }: { events: UpcomingEvent[] }) { - const nextEvent = events[0] - - if (!nextEvent) { - return ( -
- - Keine Termine -
- ) - } - - const { Icon, color } = getEventIcon(nextEvent.type) - const isUrgent = nextEvent.inDays <= 2 - - return ( -
- - {nextEvent.title} - - - - {formatEventDate(nextEvent.date, nextEvent.inDays)} - -
- ) -} diff --git a/admin-v2/components/companion/companion-mode/StatsGrid.tsx b/admin-v2/components/companion/companion-mode/StatsGrid.tsx deleted file mode 100644 index dc12b3c..0000000 --- a/admin-v2/components/companion/companion-mode/StatsGrid.tsx +++ /dev/null @@ -1,114 +0,0 @@ -'use client' - -import { Users, GraduationCap, BookOpen, FileCheck } from 'lucide-react' -import { CompanionStats } from '@/lib/companion/types' - -interface StatsGridProps { - stats: CompanionStats - loading?: boolean -} - -interface StatCardProps { - label: string - value: number - icon: React.ReactNode - color: string - loading?: boolean -} - -function StatCard({ label, value, icon, color, loading }: StatCardProps) { - return ( -
-
-
-

{label}

- {loading ? ( -
- ) : ( -

{value}

- )} -
-
- {icon} -
-
-
- ) -} - -export function StatsGrid({ stats, loading }: StatsGridProps) { - const statCards = [ - { - label: 'Klassen', - value: stats.classesCount, - icon: , - color: 'bg-blue-100', - }, - { - label: 'Schueler', - value: stats.studentsCount, - icon: , - color: 'bg-green-100', - }, - { - label: 'Lerneinheiten', - value: stats.learningUnitsCreated, - icon: , - color: 'bg-purple-100', - }, - { - label: 'Noten', - value: stats.gradesEntered, - icon: , - color: 'bg-amber-100', - }, - ] - - return ( -
- {statCards.map((card) => ( - - ))} -
- ) -} - -/** - * Compact version of StatsGrid for sidebar or smaller spaces - */ -export function StatsGridCompact({ stats, loading }: StatsGridProps) { - const items = [ - { label: 'Klassen', value: stats.classesCount, icon: }, - { label: 'Schueler', value: stats.studentsCount, icon: }, - { label: 'Einheiten', value: stats.learningUnitsCreated, icon: }, - { label: 'Noten', value: stats.gradesEntered, icon: }, - ] - - return ( -
-

Statistiken

-
- {items.map((item) => ( -
-
- {item.icon} - {item.label} -
- {loading ? ( -
- ) : ( - {item.value} - )} -
- ))} -
-
- ) -} diff --git a/admin-v2/components/companion/companion-mode/SuggestionList.tsx b/admin-v2/components/companion/companion-mode/SuggestionList.tsx deleted file mode 100644 index d657a84..0000000 --- a/admin-v2/components/companion/companion-mode/SuggestionList.tsx +++ /dev/null @@ -1,170 +0,0 @@ -'use client' - -import { ChevronRight, Clock, Lightbulb, ClipboardCheck, BookOpen, Calendar, Users, MessageSquare, FileText } from 'lucide-react' -import { Suggestion, SuggestionPriority } from '@/lib/companion/types' -import { PRIORITY_COLORS } from '@/lib/companion/constants' - -interface SuggestionListProps { - suggestions: Suggestion[] - onSuggestionClick?: (suggestion: Suggestion) => void - loading?: boolean - maxItems?: number -} - -const iconMap: Record> = { - ClipboardCheck, - BookOpen, - Calendar, - Users, - Clock, - MessageSquare, - FileText, - Lightbulb, -} - -function getIcon(iconName: string) { - const Icon = iconMap[iconName] || Lightbulb - return Icon -} - -interface SuggestionCardProps { - suggestion: Suggestion - onClick?: () => void -} - -function SuggestionCard({ suggestion, onClick }: SuggestionCardProps) { - const priorityStyles = PRIORITY_COLORS[suggestion.priority] - const Icon = getIcon(suggestion.icon) - - return ( - - ) -} - -export function SuggestionList({ - suggestions, - onSuggestionClick, - loading, - maxItems = 5, -}: SuggestionListProps) { - // Sort by priority: urgent > high > medium > low - const priorityOrder: Record = { - urgent: 0, - high: 1, - medium: 2, - low: 3, - } - - const sortedSuggestions = [...suggestions] - .sort((a, b) => priorityOrder[a.priority] - priorityOrder[b.priority]) - .slice(0, maxItems) - - if (loading) { - return ( -
-
- -

Vorschlaege

-
-
- {[1, 2, 3].map((i) => ( -
- ))} -
-
- ) - } - - if (suggestions.length === 0) { - return ( -
-
- -

Vorschlaege

-
-
-
- -
-

Alles erledigt!

-

Keine offenen Aufgaben

-
-
- ) - } - - return ( -
-
-
- -

Vorschlaege

-
- - {suggestions.length} Aufgabe{suggestions.length !== 1 ? 'n' : ''} - -
- -
- {sortedSuggestions.map((suggestion) => ( - onSuggestionClick?.(suggestion)} - /> - ))} -
- - {suggestions.length > maxItems && ( - - )} -
- ) -} diff --git a/admin-v2/components/companion/index.ts b/admin-v2/components/companion/index.ts deleted file mode 100644 index 02f0a44..0000000 --- a/admin-v2/components/companion/index.ts +++ /dev/null @@ -1,24 +0,0 @@ -// Main components -export { CompanionDashboard } from './CompanionDashboard' -export { ModeToggle } from './ModeToggle' - -// Companion Mode components -export { PhaseTimeline, PhaseTimelineDetailed } from './companion-mode/PhaseTimeline' -export { StatsGrid, StatsGridCompact } from './companion-mode/StatsGrid' -export { SuggestionList } from './companion-mode/SuggestionList' -export { EventsCard, EventsInline } from './companion-mode/EventsCard' - -// Lesson Mode components -export { LessonContainer } from './lesson-mode/LessonContainer' -export { LessonStartForm } from './lesson-mode/LessonStartForm' -export { LessonActiveView } from './lesson-mode/LessonActiveView' -export { LessonEndedView } from './lesson-mode/LessonEndedView' -export { VisualPieTimer, CompactTimer } from './lesson-mode/VisualPieTimer' -export { QuickActionsBar, QuickActionsCompact } from './lesson-mode/QuickActionsBar' -export { HomeworkSection } from './lesson-mode/HomeworkSection' -export { ReflectionSection } from './lesson-mode/ReflectionSection' - -// Modals -export { SettingsModal } from './modals/SettingsModal' -export { FeedbackModal } from './modals/FeedbackModal' -export { OnboardingModal } from './modals/OnboardingModal' diff --git a/admin-v2/components/companion/lesson-mode/LessonContainer.tsx b/admin-v2/components/companion/lesson-mode/LessonContainer.tsx deleted file mode 100644 index 201bbf6..0000000 --- a/admin-v2/components/companion/lesson-mode/LessonContainer.tsx +++ /dev/null @@ -1,80 +0,0 @@ -'use client' - -import { LessonSession, LessonStatus } from '@/lib/companion/types' -import { LessonStartForm } from './LessonStartForm' -import { LessonActiveView } from './LessonActiveView' -import { LessonEndedView } from './LessonEndedView' - -interface LessonContainerProps { - session: LessonSession | null - onStartLesson: (data: { classId: string; subject: string; topic?: string; templateId?: string }) => void - onEndLesson: () => void - onPauseToggle: () => void - onExtendTime: (minutes: number) => void - onSkipPhase: () => void - onSaveReflection: (rating: number, notes: string, nextSteps: string) => void - onAddHomework: (title: string, dueDate: string) => void - onRemoveHomework: (id: string) => void - loading?: boolean -} - -export function LessonContainer({ - session, - onStartLesson, - onEndLesson, - onPauseToggle, - onExtendTime, - onSkipPhase, - onSaveReflection, - onAddHomework, - onRemoveHomework, - loading, -}: LessonContainerProps) { - // Determine which view to show based on session state - const getView = (): 'start' | 'active' | 'ended' => { - if (!session) return 'start' - - const status = session.status - if (status === 'completed') return 'ended' - if (status === 'not_started') return 'start' - - return 'active' - } - - const view = getView() - - if (view === 'start') { - return ( - - ) - } - - if (view === 'ended' && session) { - return ( - onEndLesson()} // This will clear the session and show start form - /> - ) - } - - if (session) { - return ( - - ) - } - - return null -} diff --git a/admin-v2/hooks/companion/useCompanionData.ts b/admin-v2/hooks/companion/useCompanionData.ts deleted file mode 100644 index 129f119..0000000 --- a/admin-v2/hooks/companion/useCompanionData.ts +++ /dev/null @@ -1,156 +0,0 @@ -'use client' - -import { useState, useEffect, useCallback } from 'react' -import { CompanionData } from '@/lib/companion/types' -import { createDefaultPhases } from '@/lib/companion/constants' - -interface UseCompanionDataOptions { - pollingInterval?: number // ms, default 30000 - autoRefresh?: boolean -} - -interface UseCompanionDataReturn { - data: CompanionData | null - loading: boolean - error: string | null - refresh: () => Promise - lastUpdated: Date | null -} - -// Mock data for development - will be replaced with actual API calls -function getMockData(): CompanionData { - return { - context: { - currentPhase: 'erarbeitung', - phaseDisplayName: 'Erarbeitung', - }, - stats: { - classesCount: 4, - studentsCount: 96, - learningUnitsCreated: 23, - gradesEntered: 156, - }, - phases: createDefaultPhases(), - progress: { - percentage: 65, - completed: 13, - total: 20, - }, - suggestions: [ - { - id: '1', - title: 'Klausuren korrigieren', - description: 'Deutsch LK - 12 unkorrigierte Arbeiten warten', - priority: 'urgent', - icon: 'ClipboardCheck', - actionTarget: '/ai/klausur-korrektur', - estimatedTime: 120, - }, - { - id: '2', - title: 'Elternsprechtag vorbereiten', - description: 'Notenuebersicht fuer 8b erstellen', - priority: 'high', - icon: 'Users', - actionTarget: '/education/grades', - estimatedTime: 30, - }, - { - id: '3', - title: 'Material hochladen', - description: 'Arbeitsblatt fuer naechste Woche bereitstellen', - priority: 'medium', - icon: 'FileText', - actionTarget: '/development/content', - estimatedTime: 15, - }, - { - id: '4', - title: 'Lernstandserhebung planen', - description: 'Mathe 7a - Naechster Test in 2 Wochen', - priority: 'low', - icon: 'Calendar', - actionTarget: '/education/planning', - estimatedTime: 45, - }, - ], - upcomingEvents: [ - { - id: 'e1', - title: 'Mathe-Test 9b', - date: new Date(Date.now() + 2 * 24 * 60 * 60 * 1000).toISOString(), - type: 'exam', - inDays: 2, - }, - { - id: 'e2', - title: 'Elternsprechtag', - date: new Date(Date.now() + 5 * 24 * 60 * 60 * 1000).toISOString(), - type: 'parent_meeting', - inDays: 5, - }, - { - id: 'e3', - title: 'Notenschluss Q1', - date: new Date(Date.now() + 14 * 24 * 60 * 60 * 1000).toISOString(), - type: 'deadline', - inDays: 14, - }, - ], - } -} - -export function useCompanionData(options: UseCompanionDataOptions = {}): UseCompanionDataReturn { - const { pollingInterval = 30000, autoRefresh = true } = options - - const [data, setData] = useState(null) - const [loading, setLoading] = useState(true) - const [error, setError] = useState(null) - const [lastUpdated, setLastUpdated] = useState(null) - - const fetchData = useCallback(async () => { - try { - // TODO: Replace with actual API call - // const response = await fetch('/api/admin/companion') - // if (!response.ok) throw new Error('Failed to fetch companion data') - // const result = await response.json() - // setData(result.data) - - // For now, use mock data with a small delay to simulate network - await new Promise((resolve) => setTimeout(resolve, 300)) - setData(getMockData()) - setLastUpdated(new Date()) - setError(null) - } catch (err) { - setError(err instanceof Error ? err.message : 'Unknown error') - } finally { - setLoading(false) - } - }, []) - - const refresh = useCallback(async () => { - setLoading(true) - await fetchData() - }, [fetchData]) - - // Initial fetch - useEffect(() => { - fetchData() - }, [fetchData]) - - // Polling - useEffect(() => { - if (!autoRefresh || pollingInterval <= 0) return - - const interval = setInterval(fetchData, pollingInterval) - return () => clearInterval(interval) - }, [autoRefresh, pollingInterval, fetchData]) - - return { - data, - loading, - error, - refresh, - lastUpdated, - } -} diff --git a/admin-v2/app/api/admin/companion/feedback/route.ts b/studio-v2/app/api/companion/feedback/route.ts similarity index 97% rename from admin-v2/app/api/admin/companion/feedback/route.ts rename to studio-v2/app/api/companion/feedback/route.ts index 64faa36..e1dd7b7 100644 --- a/admin-v2/app/api/admin/companion/feedback/route.ts +++ b/studio-v2/app/api/companion/feedback/route.ts @@ -1,7 +1,7 @@ import { NextRequest, NextResponse } from 'next/server' /** - * POST /api/admin/companion/feedback + * POST /api/companion/feedback * Submit feedback (bug report, feature request, general feedback) * Proxy to backend /api/feedback */ @@ -95,7 +95,7 @@ export async function POST(request: NextRequest) { } /** - * GET /api/admin/companion/feedback + * GET /api/companion/feedback * Get feedback history (admin only) */ export async function GET(request: NextRequest) { diff --git a/admin-v2/app/api/admin/companion/lesson/route.ts b/studio-v2/app/api/companion/lesson/route.ts similarity index 97% rename from admin-v2/app/api/admin/companion/lesson/route.ts rename to studio-v2/app/api/companion/lesson/route.ts index 4124bcd..7f9a397 100644 --- a/admin-v2/app/api/admin/companion/lesson/route.ts +++ b/studio-v2/app/api/companion/lesson/route.ts @@ -1,7 +1,7 @@ import { NextRequest, NextResponse } from 'next/server' /** - * POST /api/admin/companion/lesson + * POST /api/companion/lesson * Start a new lesson session * Proxy to backend /api/classroom/sessions */ @@ -71,7 +71,7 @@ export async function POST(request: NextRequest) { } /** - * GET /api/admin/companion/lesson + * GET /api/companion/lesson * Get current lesson session or list of recent sessions */ export async function GET(request: NextRequest) { @@ -116,7 +116,7 @@ export async function GET(request: NextRequest) { } /** - * PATCH /api/admin/companion/lesson + * PATCH /api/companion/lesson * Update lesson session (timer state, phase changes, etc.) */ export async function PATCH(request: NextRequest) { @@ -160,7 +160,7 @@ export async function PATCH(request: NextRequest) { } /** - * DELETE /api/admin/companion/lesson + * DELETE /api/companion/lesson * End/delete a lesson session */ export async function DELETE(request: NextRequest) { diff --git a/admin-v2/app/api/admin/companion/settings/route.ts b/studio-v2/app/api/companion/settings/route.ts similarity index 96% rename from admin-v2/app/api/admin/companion/settings/route.ts rename to studio-v2/app/api/companion/settings/route.ts index ad9683c..5133b9c 100644 --- a/admin-v2/app/api/admin/companion/settings/route.ts +++ b/studio-v2/app/api/companion/settings/route.ts @@ -17,7 +17,7 @@ const DEFAULT_SETTINGS = { } /** - * GET /api/admin/companion/settings + * GET /api/companion/settings * Get teacher settings * Proxy to backend /api/teacher/settings */ @@ -58,7 +58,7 @@ export async function GET() { } /** - * PUT /api/admin/companion/settings + * PUT /api/companion/settings * Update teacher settings */ export async function PUT(request: NextRequest) { @@ -110,7 +110,7 @@ export async function PUT(request: NextRequest) { } /** - * PATCH /api/admin/companion/settings + * PATCH /api/companion/settings * Partially update teacher settings */ export async function PATCH(request: NextRequest) { diff --git a/studio-v2/app/companion/page.tsx b/studio-v2/app/companion/page.tsx new file mode 100644 index 0000000..8b4509a --- /dev/null +++ b/studio-v2/app/companion/page.tsx @@ -0,0 +1,51 @@ +'use client' + +import { useTheme } from '@/lib/ThemeContext' +import { Sidebar } from '@/components/Sidebar' +import { CompanionDashboard } from '@/components/companion/CompanionDashboard' +import { ThemeToggle } from '@/components/ThemeToggle' +import { LanguageDropdown } from '@/components/LanguageDropdown' + +export default function CompanionPage() { + const { isDark } = useTheme() + + return ( +
+ {/* Animated Background Blobs */} +
+
+ + {/* Sidebar */} +
+ +
+ + {/* Main Content */} +
+ {/* Header */} +
+

+ Companion +

+
+ + +
+
+ + {/* Companion Dashboard */} +
+ +
+
+
+ ) +} diff --git a/studio-v2/components/Sidebar.tsx b/studio-v2/components/Sidebar.tsx index 7234f52..2929044 100644 --- a/studio-v2/components/Sidebar.tsx +++ b/studio-v2/components/Sidebar.tsx @@ -81,6 +81,12 @@ export function Sidebar({ selectedTab = 'dashboard', onTabChange }: SidebarProps )}, + { id: 'companion', labelKey: 'nav_companion', href: '/companion', icon: ( + + + + + )}, { id: 'meet', labelKey: 'nav_meet', href: '/meet', icon: ( @@ -111,6 +117,7 @@ export function Sidebar({ selectedTab = 'dashboard', onTabChange }: SidebarProps // Determine active item based on pathname or selectedTab const getActiveItem = () => { + if (pathname === '/companion') return 'companion' if (pathname === '/meet') return 'meet' if (pathname === '/vocab-worksheet') return 'vokabeln' if (pathname === '/worksheet-editor') return 'worksheet-editor' diff --git a/studio-v2/components/companion/CompanionDashboard.tsx b/studio-v2/components/companion/CompanionDashboard.tsx new file mode 100644 index 0000000..e382c32 --- /dev/null +++ b/studio-v2/components/companion/CompanionDashboard.tsx @@ -0,0 +1,222 @@ +'use client' + +import { useState, useEffect, useCallback } from 'react' +import { Settings, MessageSquare, HelpCircle, Timer } from 'lucide-react' +import { TeacherSettings, FeedbackType } from '@/lib/companion/types' +import { DEFAULT_TEACHER_SETTINGS, STORAGE_KEYS } from '@/lib/companion/constants' + +// Components +import { LessonStartForm } from './lesson-mode/LessonStartForm' +import { LessonActiveView } from './lesson-mode/LessonActiveView' +import { LessonEndedView } from './lesson-mode/LessonEndedView' +import { SettingsModal } from './modals/SettingsModal' +import { FeedbackModal } from './modals/FeedbackModal' +import { OnboardingModal } from './modals/OnboardingModal' + +// Hooks +import { useLessonSession } from '@/hooks/companion/useLessonSession' +import { useKeyboardShortcuts } from '@/hooks/companion/useKeyboardShortcuts' + +export function CompanionDashboard() { + // Modal states + const [showSettings, setShowSettings] = useState(false) + const [showFeedback, setShowFeedback] = useState(false) + const [showOnboarding, setShowOnboarding] = useState(false) + + // Settings + const [settings, setSettings] = useState(DEFAULT_TEACHER_SETTINGS) + + // Load settings from localStorage + useEffect(() => { + const stored = localStorage.getItem(STORAGE_KEYS.SETTINGS) + if (stored) { + try { + const parsed = JSON.parse(stored) + setSettings({ ...DEFAULT_TEACHER_SETTINGS, ...parsed }) + } catch { + // Invalid stored settings + } + } + + // Check if onboarding needed + const onboardingStored = localStorage.getItem(STORAGE_KEYS.ONBOARDING_STATE) + if (!onboardingStored) { + setShowOnboarding(true) + } + }, []) + + // Lesson session hook + const { + session, + startLesson, + endLesson, + clearSession, + pauseLesson, + resumeLesson, + extendTime, + skipPhase, + saveReflection, + addHomework, + removeHomework, + isPaused, + } = useLessonSession({ + onOvertimeStart: () => { + if (settings.soundNotifications) { + // TODO: Play notification sound + } + }, + }) + + // Handle pause/resume toggle + const handlePauseToggle = useCallback(() => { + if (isPaused) { + resumeLesson() + } else { + pauseLesson() + } + }, [isPaused, pauseLesson, resumeLesson]) + + // Keyboard shortcuts + useKeyboardShortcuts({ + onPauseResume: session ? handlePauseToggle : undefined, + onExtend: session && !isPaused ? () => extendTime(5) : undefined, + onNextPhase: session && !isPaused ? skipPhase : undefined, + onCloseModal: () => { + setShowSettings(false) + setShowFeedback(false) + setShowOnboarding(false) + }, + enabled: settings.showKeyboardShortcuts, + }) + + // Handle settings save + const handleSaveSettings = (newSettings: TeacherSettings) => { + setSettings(newSettings) + localStorage.setItem(STORAGE_KEYS.SETTINGS, JSON.stringify(newSettings)) + } + + // Handle feedback submit + const handleFeedbackSubmit = async (type: FeedbackType, title: string, description: string) => { + const response = await fetch('/api/companion/feedback', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + type, + title, + description, + sessionId: session?.sessionId, + }), + }) + + if (!response.ok) { + throw new Error('Failed to submit feedback') + } + } + + // Handle onboarding complete + const handleOnboardingComplete = (data: { state?: string; schoolType?: string }) => { + localStorage.setItem(STORAGE_KEYS.ONBOARDING_STATE, JSON.stringify({ + ...data, + completed: true, + completedAt: new Date().toISOString(), + })) + setShowOnboarding(false) + setSettings({ ...settings, onboardingCompleted: true }) + } + + // Determine current view based on session status + const renderContent = () => { + if (!session) { + return + } + + if (session.status === 'completed') { + return ( + + ) + } + + // in_progress or paused + return ( + + ) + } + + return ( +
+ {/* Header */} +
+
+
+ +
+

Unterrichtsstunde

+
+ +
+ {/* Feedback Button */} + + + {/* Settings Button */} + + + {/* Help Button */} + +
+
+ + {/* Main Content */} + {renderContent()} + + {/* Modals */} + setShowSettings(false)} + settings={settings} + onSave={handleSaveSettings} + /> + + setShowFeedback(false)} + onSubmit={handleFeedbackSubmit} + /> + + setShowOnboarding(false)} + onComplete={handleOnboardingComplete} + /> +
+ ) +} diff --git a/admin-v2/components/companion/lesson-mode/HomeworkSection.tsx b/studio-v2/components/companion/lesson-mode/HomeworkSection.tsx similarity index 100% rename from admin-v2/components/companion/lesson-mode/HomeworkSection.tsx rename to studio-v2/components/companion/lesson-mode/HomeworkSection.tsx diff --git a/admin-v2/components/companion/lesson-mode/LessonActiveView.tsx b/studio-v2/components/companion/lesson-mode/LessonActiveView.tsx similarity index 98% rename from admin-v2/components/companion/lesson-mode/LessonActiveView.tsx rename to studio-v2/components/companion/lesson-mode/LessonActiveView.tsx index 88e9fe0..6671bba 100644 --- a/admin-v2/components/companion/lesson-mode/LessonActiveView.tsx +++ b/studio-v2/components/companion/lesson-mode/LessonActiveView.tsx @@ -4,7 +4,7 @@ import { BookOpen, Clock, Users } from 'lucide-react' import { LessonSession } from '@/lib/companion/types' import { VisualPieTimer } from './VisualPieTimer' import { QuickActionsBar } from './QuickActionsBar' -import { PhaseTimelineDetailed } from '../companion-mode/PhaseTimeline' +import { PhaseTimelineDetailed } from './PhaseTimeline' import { PHASE_COLORS, PHASE_DISPLAY_NAMES, diff --git a/admin-v2/components/companion/lesson-mode/LessonEndedView.tsx b/studio-v2/components/companion/lesson-mode/LessonEndedView.tsx similarity index 100% rename from admin-v2/components/companion/lesson-mode/LessonEndedView.tsx rename to studio-v2/components/companion/lesson-mode/LessonEndedView.tsx diff --git a/admin-v2/components/companion/lesson-mode/LessonStartForm.tsx b/studio-v2/components/companion/lesson-mode/LessonStartForm.tsx similarity index 100% rename from admin-v2/components/companion/lesson-mode/LessonStartForm.tsx rename to studio-v2/components/companion/lesson-mode/LessonStartForm.tsx diff --git a/admin-v2/components/companion/companion-mode/PhaseTimeline.tsx b/studio-v2/components/companion/lesson-mode/PhaseTimeline.tsx similarity index 51% rename from admin-v2/components/companion/companion-mode/PhaseTimeline.tsx rename to studio-v2/components/companion/lesson-mode/PhaseTimeline.tsx index faadf12..7a4256d 100644 --- a/admin-v2/components/companion/companion-mode/PhaseTimeline.tsx +++ b/studio-v2/components/companion/lesson-mode/PhaseTimeline.tsx @@ -1,103 +1,29 @@ 'use client' import { Check } from 'lucide-react' -import { Phase } from '@/lib/companion/types' -import { PHASE_COLORS, formatMinutes } from '@/lib/companion/constants' +import { formatMinutes } from '@/lib/companion/constants' -interface PhaseTimelineProps { - phases: Phase[] +interface PhaseTimelinePhase { + id: string + shortName: string + displayName: string + duration: number + status: string + actualTime?: number + color: string +} + +interface PhaseTimelineDetailedProps { + phases: PhaseTimelinePhase[] currentPhaseIndex: number onPhaseClick?: (index: number) => void - compact?: boolean } -export function PhaseTimeline({ - phases, - currentPhaseIndex, - onPhaseClick, - compact = false, -}: PhaseTimelineProps) { - return ( -
- {phases.map((phase, index) => { - const isActive = index === currentPhaseIndex - const isCompleted = phase.status === 'completed' - const isPast = index < currentPhaseIndex - const colors = PHASE_COLORS[phase.id] - - return ( -
- {/* Phase Dot/Circle */} - - - {/* Connector Line */} - {index < phases.length - 1 && ( -
- )} -
- ) - })} -
- ) -} - -/** - * Detailed Phase Timeline with labels and durations - */ export function PhaseTimelineDetailed({ phases, currentPhaseIndex, onPhaseClick, -}: PhaseTimelineProps) { +}: PhaseTimelineDetailedProps) { return (

Unterrichtsphasen

@@ -107,7 +33,6 @@ export function PhaseTimelineDetailed({ const isActive = index === currentPhaseIndex const isCompleted = phase.status === 'completed' const isPast = index < currentPhaseIndex - const colors = PHASE_COLORS[phase.id] return (
@@ -118,7 +43,7 @@ export function PhaseTimelineDetailed({ className="flex-1 h-1" style={{ background: isPast || isCompleted - ? PHASE_COLORS[phases[index - 1].id].hex + ? phases[index - 1].color : '#e2e8f0', }} /> @@ -138,9 +63,9 @@ export function PhaseTimelineDetailed({ ${isActive ? 'ring-4 ring-offset-2 shadow-lg' : ''} `} style={{ - backgroundColor: isActive || isCompleted || isPast ? colors.hex : '#e2e8f0', + backgroundColor: isActive || isCompleted || isPast ? phase.color : '#e2e8f0', color: isActive || isCompleted || isPast ? 'white' : '#64748b', - '--tw-ring-color': isActive ? `${colors.hex}40` : undefined, + '--tw-ring-color': isActive ? `${phase.color}40` : undefined, } as React.CSSProperties} > {isCompleted ? ( @@ -152,7 +77,7 @@ export function PhaseTimelineDetailed({ {isActive && ( )} @@ -161,7 +86,7 @@ export function PhaseTimelineDetailed({
)} diff --git a/admin-v2/components/companion/lesson-mode/QuickActionsBar.tsx b/studio-v2/components/companion/lesson-mode/QuickActionsBar.tsx similarity index 100% rename from admin-v2/components/companion/lesson-mode/QuickActionsBar.tsx rename to studio-v2/components/companion/lesson-mode/QuickActionsBar.tsx diff --git a/admin-v2/components/companion/lesson-mode/ReflectionSection.tsx b/studio-v2/components/companion/lesson-mode/ReflectionSection.tsx similarity index 100% rename from admin-v2/components/companion/lesson-mode/ReflectionSection.tsx rename to studio-v2/components/companion/lesson-mode/ReflectionSection.tsx diff --git a/admin-v2/components/companion/lesson-mode/VisualPieTimer.tsx b/studio-v2/components/companion/lesson-mode/VisualPieTimer.tsx similarity index 100% rename from admin-v2/components/companion/lesson-mode/VisualPieTimer.tsx rename to studio-v2/components/companion/lesson-mode/VisualPieTimer.tsx diff --git a/admin-v2/components/companion/modals/FeedbackModal.tsx b/studio-v2/components/companion/modals/FeedbackModal.tsx similarity index 100% rename from admin-v2/components/companion/modals/FeedbackModal.tsx rename to studio-v2/components/companion/modals/FeedbackModal.tsx diff --git a/admin-v2/components/companion/modals/OnboardingModal.tsx b/studio-v2/components/companion/modals/OnboardingModal.tsx similarity index 100% rename from admin-v2/components/companion/modals/OnboardingModal.tsx rename to studio-v2/components/companion/modals/OnboardingModal.tsx diff --git a/admin-v2/components/companion/modals/SettingsModal.tsx b/studio-v2/components/companion/modals/SettingsModal.tsx similarity index 100% rename from admin-v2/components/companion/modals/SettingsModal.tsx rename to studio-v2/components/companion/modals/SettingsModal.tsx diff --git a/admin-v2/hooks/companion/index.ts b/studio-v2/hooks/companion/index.ts similarity index 72% rename from admin-v2/hooks/companion/index.ts rename to studio-v2/hooks/companion/index.ts index 85edd4c..8b85950 100644 --- a/admin-v2/hooks/companion/index.ts +++ b/studio-v2/hooks/companion/index.ts @@ -1,3 +1,2 @@ -export { useCompanionData } from './useCompanionData' export { useLessonSession } from './useLessonSession' export { useKeyboardShortcuts, useKeyboardShortcutHints } from './useKeyboardShortcuts' diff --git a/admin-v2/hooks/companion/useKeyboardShortcuts.ts b/studio-v2/hooks/companion/useKeyboardShortcuts.ts similarity index 100% rename from admin-v2/hooks/companion/useKeyboardShortcuts.ts rename to studio-v2/hooks/companion/useKeyboardShortcuts.ts diff --git a/admin-v2/hooks/companion/useLessonSession.ts b/studio-v2/hooks/companion/useLessonSession.ts similarity index 95% rename from admin-v2/hooks/companion/useLessonSession.ts rename to studio-v2/hooks/companion/useLessonSession.ts index a0529cb..dff3aea 100644 --- a/admin-v2/hooks/companion/useLessonSession.ts +++ b/studio-v2/hooks/companion/useLessonSession.ts @@ -29,6 +29,7 @@ interface UseLessonSessionReturn { templateId?: string }) => void endLesson: () => void + clearSession: () => void pauseLesson: () => void resumeLesson: () => void extendTime: (minutes: number) => void @@ -96,10 +97,11 @@ export function useLessonSession( const now = Date.now() const delta = Math.floor((now - lastTickRef.current) / 1000) - lastTickRef.current = now if (delta <= 0) return + lastTickRef.current = now + setSession((prev) => { if (!prev) return null @@ -138,7 +140,7 @@ export function useLessonSession( useEffect(() => { if (session?.status === 'in_progress' && !session.isPaused) { lastTickRef.current = Date.now() - timerRef.current = setInterval(tick, 100) // Update every 100ms for smooth animation + timerRef.current = setInterval(tick, 100) } else { if (timerRef.current) { clearInterval(timerRef.current) @@ -175,12 +177,10 @@ export function useLessonSession( if (stored) { try { const parsed = JSON.parse(stored) as LessonSession - // Only restore if session is not completed and not too old (< 24h) const sessionTime = new Date(parsed.startTime).getTime() const isRecent = Date.now() - sessionTime < 24 * 60 * 60 * 1000 if (parsed.status !== 'completed' && isRecent) { - // Pause the restored session setSession({ ...parsed, isPaused: true }) } } catch { @@ -197,7 +197,6 @@ export function useLessonSession( topic?: string templateId?: string }) => { - // Find template durations let durations = DEFAULT_PHASE_DURATIONS if (data.templateId) { const template = SYSTEM_TEMPLATES.find((t) => t.templateId === data.templateId) @@ -253,6 +252,11 @@ export function useLessonSession( onLessonComplete?.(completedSession) }, [session, onLessonComplete]) + const clearSession = useCallback(() => { + setSession(null) + localStorage.removeItem(STORAGE_KEYS.CURRENT_SESSION) + }, []) + const pauseLesson = useCallback(() => { if (!session || session.isPaused) return @@ -302,7 +306,6 @@ export function useLessonSession( currentPhase.duration += minutes - // Reset overtime trigger if we've added time if (hasTriggeredOvertimeRef.current) { const phaseDurationSeconds = currentPhase.duration * 60 if (currentPhase.actualTime < phaseDurationSeconds) { @@ -325,7 +328,6 @@ export function useLessonSession( const nextPhaseIndex = session.currentPhaseIndex + 1 - // Check if this was the last phase if (nextPhaseIndex >= session.phases.length) { endLesson() return @@ -336,14 +338,12 @@ export function useLessonSession( const updatedPhases = [...prev.phases] - // Complete current phase updatedPhases[prev.currentPhaseIndex] = { ...updatedPhases[prev.currentPhaseIndex], status: 'completed', completedAt: new Date().toISOString(), } - // Start next phase updatedPhases[nextPhaseIndex] = { ...updatedPhases[nextPhaseIndex], status: 'active', @@ -422,17 +422,12 @@ export function useLessonSession( [session] ) - // Clear session (for starting new) - const clearSession = useCallback(() => { - setSession(null) - localStorage.removeItem(STORAGE_KEYS.CURRENT_SESSION) - }, []) - return { session, timerState, startLesson, - endLesson: session?.status === 'completed' ? clearSession : endLesson, + endLesson, + clearSession, pauseLesson, resumeLesson, extendTime, diff --git a/admin-v2/lib/companion/constants.ts b/studio-v2/lib/companion/constants.ts similarity index 100% rename from admin-v2/lib/companion/constants.ts rename to studio-v2/lib/companion/constants.ts diff --git a/admin-v2/lib/companion/index.ts b/studio-v2/lib/companion/index.ts similarity index 100% rename from admin-v2/lib/companion/index.ts rename to studio-v2/lib/companion/index.ts diff --git a/admin-v2/lib/companion/types.ts b/studio-v2/lib/companion/types.ts similarity index 100% rename from admin-v2/lib/companion/types.ts rename to studio-v2/lib/companion/types.ts diff --git a/studio-v2/lib/i18n.ts b/studio-v2/lib/i18n.ts index 3cb73aa..2a2428a 100644 --- a/studio-v2/lib/i18n.ts +++ b/studio-v2/lib/i18n.ts @@ -37,7 +37,7 @@ export const translations: Record> = { nav_worksheet_editor: 'Arbeitsblätter', nav_worksheet_cleanup: 'Bereinigung', nav_korrektur: 'Korrekturen', - nav_compliance_pipeline: 'Compliance Pipeline', + nav_companion: 'Companion', nav_compliance_pipeline: 'Compliance Pipeline', nav_meet: 'Meet', nav_alerts: 'Alerts', nav_alerts_b2b: 'B2B Alerts', @@ -98,7 +98,7 @@ export const translations: Record> = { nav_worksheet_editor: 'Worksheets', nav_worksheet_cleanup: 'Cleanup', nav_korrektur: 'Corrections', - nav_compliance_pipeline: 'Compliance Pipeline', + nav_companion: 'Companion', nav_compliance_pipeline: 'Compliance Pipeline', nav_meet: 'Meet', nav_alerts: 'Alerts', nav_alerts_b2b: 'B2B Alerts', @@ -145,7 +145,7 @@ export const translations: Record> = { nav_worksheet_editor: 'Çalışma Sayfaları', nav_worksheet_cleanup: 'Temizleme', nav_korrektur: 'Düzeltmeler', - nav_compliance_pipeline: 'Uyum Boru Hattı', + nav_companion: 'Companion', nav_compliance_pipeline: 'Uyum Boru Hattı', nav_meet: 'Meet', nav_alerts: 'Uyarılar', nav_alerts_b2b: 'B2B Uyarılar', @@ -192,7 +192,7 @@ export const translations: Record> = { nav_worksheet_editor: 'أوراق العمل', nav_worksheet_cleanup: 'تنظيف', nav_korrektur: 'التصحيحات', - nav_compliance_pipeline: 'خط أنابيب الامتثال', + nav_companion: 'المرافق', nav_compliance_pipeline: 'خط أنابيب الامتثال', nav_meet: 'اجتماع', nav_alerts: 'تنبيهات', nav_alerts_b2b: 'تنبيهات الشركات', @@ -239,7 +239,7 @@ export const translations: Record> = { nav_worksheet_editor: 'Рабочие листы', nav_worksheet_cleanup: 'Очистка', nav_korrektur: 'Проверки', - nav_compliance_pipeline: 'Пайплайн соответствия', + nav_companion: 'Компаньон', nav_compliance_pipeline: 'Пайплайн соответствия', nav_meet: 'Встреча', nav_alerts: 'Оповещения', nav_alerts_b2b: 'B2B оповещения', @@ -286,7 +286,7 @@ export const translations: Record> = { nav_worksheet_editor: 'Робочі аркуші', nav_worksheet_cleanup: 'Очищення', nav_korrektur: 'Перевірки', - nav_compliance_pipeline: 'Пайплайн відповідності', + nav_companion: 'Компаньйон', nav_compliance_pipeline: 'Пайплайн відповідності', nav_meet: 'Зустріч', nav_alerts: 'Сповіщення', nav_alerts_b2b: 'B2B сповіщення', @@ -333,7 +333,7 @@ export const translations: Record> = { nav_worksheet_editor: 'Arkusze robocze', nav_worksheet_cleanup: 'Czyszczenie', nav_korrektur: 'Korekty', - nav_compliance_pipeline: 'Pipeline zgodności', + nav_companion: 'Companion', nav_compliance_pipeline: 'Pipeline zgodności', nav_meet: 'Spotkanie', nav_alerts: 'Powiadomienia', nav_alerts_b2b: 'Powiadomienia B2B',