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>
252 lines
9.9 KiB
TypeScript
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>
|
|
)
|
|
}
|