Files
breakpilot-lehrer/studio-v2/app/page.tsx
Benjamin Admin b6983ab1dc [split-required] Split 500-1000 LOC files across all services
backend-lehrer (5 files):
- alerts_agent/db/repository.py (992 → 5), abitur_docs_api.py (956 → 3)
- teacher_dashboard_api.py (951 → 3), services/pdf_service.py (916 → 3)
- mail/mail_db.py (987 → 6)

klausur-service (5 files):
- legal_templates_ingestion.py (942 → 3), ocr_pipeline_postprocess.py (929 → 4)
- ocr_pipeline_words.py (876 → 3), ocr_pipeline_ocr_merge.py (616 → 2)
- KorrekturPage.tsx (956 → 6)

website (5 pages):
- mail (985 → 9), edu-search (958 → 8), mac-mini (950 → 7)
- ocr-labeling (946 → 7), audit-workspace (871 → 4)

studio-v2 (5 files + 1 deleted):
- page.tsx (946 → 5), MessagesContext.tsx (925 → 4)
- korrektur (914 → 6), worksheet-cleanup (899 → 6)
- useVocabWorksheet.ts (888 → 3)
- Deleted dead page-original.tsx (934 LOC)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-24 23:35:37 +02:00

252 lines
9.9 KiB
TypeScript

'use client'
import { useState, useEffect, useCallback } from 'react'
import { BPIcon } from '@/components/Logo'
import { useTheme } from '@/lib/ThemeContext'
import { Sidebar } from '@/components/Sidebar'
import { OnboardingWizard, OnboardingData } from '@/components/OnboardingWizard'
import { ChatOverlay } from '@/components/ChatOverlay'
import { AiPrompt } from '@/components/AiPrompt'
import { Footer } from '@/components/Footer'
import { BackgroundBlobs } from './_components/BackgroundBlobs'
import { HeaderBar } from './_components/HeaderBar'
import { DashboardContent } from './_components/DashboardContent'
import { DocumentsTab } from './_components/DocumentsTab'
import { UploadModal, QRModal } from './_components/UploadModals'
// LocalStorage Keys
const ONBOARDING_KEY = 'bp_onboarding_complete'
const USER_DATA_KEY = 'bp_user_data'
const DOCUMENTS_KEY = 'bp_documents'
const FIRST_VISIT_KEY = 'bp_first_dashboard_visit'
const SESSION_ID_KEY = 'bp_session_id'
interface StoredDocument {
id: string
name: string
type: string
size: number
uploadedAt: Date
url?: string
}
export default function HomePage() {
const [selectedTab, setSelectedTab] = useState('dashboard')
const [showOnboarding, setShowOnboarding] = useState<boolean | null>(null)
const [userData, setUserData] = useState<OnboardingData | null>(null)
const [documents, setDocuments] = useState<StoredDocument[]>([])
const [showUploadModal, setShowUploadModal] = useState(false)
const [showQRModal, setShowQRModal] = useState(false)
const [isFirstVisit, setIsFirstVisit] = useState(false)
const [sessionId, setSessionId] = useState<string>('')
const [showAlertsDropdown, setShowAlertsDropdown] = useState(false)
const { isDark } = useTheme()
// Funktion zum Laden von Uploads aus der API
const fetchUploadsFromAPI = useCallback(async (sid: string) => {
if (!sid) return
try {
const response = await fetch(`/api/uploads?sessionId=${encodeURIComponent(sid)}`)
if (response.ok) {
const data = await response.json()
if (data.uploads && data.uploads.length > 0) {
const apiDocs: StoredDocument[] = data.uploads.map((u: any) => ({
id: u.id, name: u.name, type: u.type, size: u.size,
uploadedAt: new Date(u.uploadedAt), url: u.dataUrl
}))
setDocuments(prev => {
const existingIds = new Set(prev.map(d => d.id))
const newDocs = apiDocs.filter(d => !existingIds.has(d.id))
return newDocs.length > 0 ? [...prev, ...newDocs] : prev
})
}
}
} catch (error) {
console.error('Error fetching uploads:', error)
}
}, [])
// Prüfe beim Laden, ob Onboarding abgeschlossen ist
useEffect(() => {
const onboardingComplete = localStorage.getItem(ONBOARDING_KEY)
const storedUserData = localStorage.getItem(USER_DATA_KEY)
const storedDocs = localStorage.getItem(DOCUMENTS_KEY)
const firstVisit = localStorage.getItem(FIRST_VISIT_KEY)
let storedSessionId = localStorage.getItem(SESSION_ID_KEY)
if (!storedSessionId) {
storedSessionId = `session-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`
localStorage.setItem(SESSION_ID_KEY, storedSessionId)
}
setSessionId(storedSessionId)
if (onboardingComplete === 'true' && storedUserData) {
setUserData(JSON.parse(storedUserData))
setShowOnboarding(false)
if (storedDocs) setDocuments(JSON.parse(storedDocs))
if (!firstVisit) {
setIsFirstVisit(true)
localStorage.setItem(FIRST_VISIT_KEY, 'true')
}
fetchUploadsFromAPI(storedSessionId)
} else {
setShowOnboarding(true)
}
}, [fetchUploadsFromAPI])
// Polling fuer neue Uploads
useEffect(() => {
if (!sessionId || showOnboarding) return
const interval = setInterval(() => fetchUploadsFromAPI(sessionId), 3000)
return () => clearInterval(interval)
}, [sessionId, showOnboarding, fetchUploadsFromAPI])
// Dokumente in localStorage speichern
useEffect(() => {
if (documents.length > 0) {
localStorage.setItem(DOCUMENTS_KEY, JSON.stringify(documents))
}
}, [documents])
const handleUploadComplete = (uploadedDocs: any[]) => {
const newDocs: StoredDocument[] = uploadedDocs.map(d => ({
id: d.id, name: d.name, type: d.type, size: d.size,
uploadedAt: d.uploadedAt, url: d.url
}))
setDocuments(prev => [...prev, ...newDocs])
setIsFirstVisit(false)
}
const handleDeleteDocument = async (id: string) => {
setDocuments(prev => prev.filter(d => d.id !== id))
try {
await fetch(`/api/uploads?id=${encodeURIComponent(id)}`, { method: 'DELETE' })
} catch (error) {
console.error('Error deleting from API:', error)
}
}
const handleRenameDocument = (id: string, newName: string) => {
setDocuments(prev => prev.map(d => d.id === id ? { ...d, name: newName } : d))
}
const handleOnboardingComplete = (data: OnboardingData) => {
localStorage.setItem(ONBOARDING_KEY, 'true')
localStorage.setItem(USER_DATA_KEY, JSON.stringify(data))
setUserData(data)
setShowOnboarding(false)
}
// Loading screen
if (showOnboarding === null) {
return (
<div className={`min-h-screen flex items-center justify-center ${
isDark
? 'bg-gradient-to-br from-indigo-900 via-purple-900 to-pink-800'
: 'bg-gradient-to-br from-slate-100 via-blue-50 to-indigo-100'
}`}>
<div className="flex items-center gap-4">
<BPIcon variant="cupertino" size={48} className="animate-pulse" />
<span className={`text-xl font-medium ${isDark ? 'text-white' : 'text-slate-900'}`}>Laden...</span>
</div>
</div>
)
}
if (showOnboarding) {
return <OnboardingWizard onComplete={handleOnboardingComplete} />
}
return (
<div className={`min-h-screen relative overflow-hidden flex flex-col ${
isDark
? 'bg-gradient-to-br from-indigo-900 via-purple-900 to-pink-800'
: 'bg-gradient-to-br from-slate-100 via-blue-50 to-indigo-100'
}`}>
<BackgroundBlobs isDark={isDark} />
<div className="relative z-10 flex min-h-screen gap-6 p-4">
<Sidebar selectedTab={selectedTab} onTabChange={setSelectedTab} />
<main className="flex-1">
<HeaderBar showAlertsDropdown={showAlertsDropdown} setShowAlertsDropdown={setShowAlertsDropdown} />
{/* Welcome message for first visit */}
{isFirstVisit && documents.length === 0 && (
<div className={`mb-8 p-6 rounded-3xl border backdrop-blur-xl ${
isDark
? 'bg-gradient-to-r from-purple-500/20 to-pink-500/20 border-purple-500/30'
: 'bg-gradient-to-r from-purple-50 to-pink-50 border-purple-200'
}`}>
<div className="flex items-start gap-6">
<div className={`w-16 h-16 rounded-2xl flex items-center justify-center text-3xl ${
isDark ? 'bg-white/20' : 'bg-white shadow-lg'
}`}>🎉</div>
<div className="flex-1">
<h2 className={`text-xl font-bold mb-2 ${isDark ? 'text-white' : 'text-slate-900'}`}>
Willkommen bei BreakPilot Studio!
</h2>
<p className={`mb-4 ${isDark ? 'text-white/70' : 'text-slate-600'}`}>
Grossartig, dass Sie hier sind! Laden Sie jetzt Ihr erstes Dokument hoch,
um die KI-gestuetzte Korrektur zu erleben. Sie koennen Dateien von Ihrem
Computer oder Mobiltelefon hochladen.
</p>
<div className="flex gap-3">
<button onClick={() => setShowUploadModal(true)}
className="px-6 py-3 bg-gradient-to-r from-purple-500 to-pink-500 text-white rounded-xl font-medium hover:shadow-lg hover:shadow-purple-500/30 transition-all hover:scale-105">
Dokument hochladen
</button>
<button onClick={() => setShowQRModal(true)}
className={`px-6 py-3 rounded-xl font-medium transition-all ${
isDark ? 'bg-white/20 text-white hover:bg-white/30' : 'bg-white text-slate-700 hover:bg-slate-50 shadow'
}`}>
Mit Mobiltelefon hochladen
</button>
</div>
</div>
<button onClick={() => setIsFirstVisit(false)}
className={`p-2 rounded-lg ${isDark ? 'hover:bg-white/10' : 'hover:bg-white'}`}>
<svg className={`w-5 h-5 ${isDark ? 'text-white/60' : 'text-slate-400'}`} fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
</div>
)}
<AiPrompt />
{selectedTab === 'dokumente' ? (
<DocumentsTab
documents={documents}
onDelete={handleDeleteDocument}
onRename={handleRenameDocument}
setShowUploadModal={setShowUploadModal}
setShowQRModal={setShowQRModal}
/>
) : (
<DashboardContent
documents={documents}
setShowUploadModal={setShowUploadModal}
setSelectedTab={setSelectedTab}
/>
)}
</main>
</div>
{showUploadModal && (
<UploadModal
documents={documents}
onUploadComplete={handleUploadComplete}
onClose={() => setShowUploadModal(false)}
onGoToDocuments={() => { setShowUploadModal(false); setSelectedTab('dokumente') }}
/>
)}
{showQRModal && <QRModal sessionId={sessionId} onClose={() => setShowQRModal(false)} />}
<ChatOverlay typewriterEnabled={true} typewriterSpeed={25} autoDismissMs={0} maxQueue={5} />
<Footer />
</div>
)
}