Files
breakpilot-lehrer/website/app/zeugnisse/page.tsx
Benjamin Admin 34da9f4cda [split-required] Split 700-870 LOC files across all services
backend-lehrer (11 files):
- llm_gateway/routes/schools.py (867 → 5), recording_api.py (848 → 6)
- messenger_api.py (840 → 5), print_generator.py (824 → 5)
- unit_analytics_api.py (751 → 5), classroom/routes/context.py (726 → 4)
- llm_gateway/routes/edu_search_seeds.py (710 → 4)

klausur-service (12 files):
- ocr_labeling_api.py (845 → 4), metrics_db.py (833 → 4)
- legal_corpus_api.py (790 → 4), page_crop.py (758 → 3)
- mail/ai_service.py (747 → 4), github_crawler.py (767 → 3)
- trocr_service.py (730 → 4), full_compliance_pipeline.py (723 → 4)
- dsfa_rag_api.py (715 → 4), ocr_pipeline_auto.py (705 → 4)

website (6 pages):
- audit-checklist (867 → 8), content (806 → 6)
- screen-flow (790 → 4), scraper (789 → 5)
- zeugnisse (776 → 5), modules (745 → 4)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-25 08:01:18 +02:00

125 lines
9.9 KiB
TypeScript

'use client'
import { useState, useEffect } from 'react'
import { UserPreferences, SearchResult, Message, BUNDESLAENDER, SCHULFORMEN, COMMON_QUESTIONS } from './_components/types'
import OnboardingWizard from './_components/OnboardingWizard'
import ChatInterface from './_components/ChatInterface'
import SearchResults from './_components/SearchResults'
export default function ZeugnissePage() {
const [preferences, setPreferences] = useState<UserPreferences>({
bundesland: null, schulform: null, hasSeenWizard: false, favorites: [], recentSearches: [],
})
const [showWizard, setShowWizard] = useState(true)
const [activeTab, setActiveTab] = useState<'chat' | 'search' | 'documents'>('chat')
const [searchQuery, setSearchQuery] = useState('')
const [searchResults, setSearchResults] = useState<SearchResult[]>([])
const [messages, setMessages] = useState<Message[]>([])
const [isLoading, setIsLoading] = useState(false)
useEffect(() => {
const saved = localStorage.getItem('zeugnis-preferences')
if (saved) { const parsed = JSON.parse(saved); setPreferences(parsed); setShowWizard(!parsed.hasSeenWizard) }
}, [])
const savePreferences = (newPrefs: Partial<UserPreferences>) => {
const updated = { ...preferences, ...newPrefs }
setPreferences(updated)
localStorage.setItem('zeugnis-preferences', JSON.stringify(updated))
}
const handleWizardComplete = (prefs: Partial<UserPreferences>) => {
savePreferences(prefs); setShowWizard(false)
}
const handleSendMessage = async (content: string) => {
const userMessage: Message = { id: Date.now().toString(), role: 'user', content, timestamp: new Date() }
setMessages(prev => [...prev, userMessage])
setIsLoading(true)
setTimeout(() => {
const assistantMessage: Message = {
id: (Date.now() + 1).toString(), role: 'assistant',
content: `Basierend auf der Zeugnisverordnung ${preferences.bundesland ? `fuer ${BUNDESLAENDER.find(b => b.code === preferences.bundesland)?.name}` : ''} kann ich Ihnen folgende Auskunft geben:\n\n${content.includes('Bemerkung') ? 'Bemerkungen zum Arbeits- und Sozialverhalten sollten wertschaetzend formuliert werden und konkrete Beobachtungen enthalten. Sie muessen den Vorgaben der jeweiligen Schulordnung entsprechen.' : content.includes('Noten') ? 'Im Zeugnis werden die Noten gemaess der geltenden Notenverordnung eingetragen. Die Notenskala reicht von 1 (sehr gut) bis 6 (ungenuegend).' : 'Ich habe in den Verordnungen relevante Informationen zu Ihrer Frage gefunden. Bitte beachten Sie die verlinkten Quellen fuer weitere Details.'}`,
sources: [{ id: '1', title: 'Zeugnisverordnung', bundesland: preferences.bundesland || 'ni', bundesland_name: BUNDESLAENDER.find(b => b.code === (preferences.bundesland || 'ni'))?.name || 'Niedersachsen', doc_type: 'Verordnung', snippet: 'Relevanter Auszug aus der Verordnung...', relevance_score: 0.92, url: '#', last_updated: '2024-01-15' }],
timestamp: new Date(),
}
setMessages(prev => [...prev, assistantMessage])
setIsLoading(false)
}, 1500)
}
const handleSearch = (query: string) => {
setSearchQuery(query)
if (!query.trim()) { setSearchResults([]); return }
setSearchResults([
{ id: '1', title: 'Zeugnisverordnung - Bemerkungen', bundesland: preferences.bundesland || 'ni', bundesland_name: BUNDESLAENDER.find(b => b.code === (preferences.bundesland || 'ni'))?.name || 'Niedersachsen', doc_type: 'Verordnung', snippet: 'Die Bemerkungen im Zeugnis sollen das Arbeits- und Sozialverhalten beschreiben...', relevance_score: 0.95, url: '#', last_updated: '2024-01-15' },
{ id: '2', title: 'Ergaenzende Bestimmungen fuer Zeugnisse', bundesland: preferences.bundesland || 'ni', bundesland_name: BUNDESLAENDER.find(b => b.code === (preferences.bundesland || 'ni'))?.name || 'Niedersachsen', doc_type: 'Erlass', snippet: 'Fuer die Erstellung von Zeugnissen gelten folgende ergaenzende Bestimmungen...', relevance_score: 0.87, url: '#', last_updated: '2024-02-20' },
])
savePreferences({ recentSearches: [query, ...preferences.recentSearches.filter(s => s !== query).slice(0, 9)] })
}
if (showWizard) return <OnboardingWizard onComplete={handleWizardComplete} />
return (
<div className="min-h-screen bg-gradient-to-br from-gray-50 to-gray-100 dark:from-gray-900 dark:to-gray-800">
<header className="bg-white dark:bg-gray-800 shadow-sm border-b border-gray-200 dark:border-gray-700">
<div className="max-w-7xl mx-auto px-6 py-4">
<div className="flex items-center justify-between">
<div className="flex items-center gap-4">
<div className="w-10 h-10 bg-gradient-to-br from-blue-500 to-purple-600 rounded-xl flex items-center justify-center"><span className="text-xl">📋</span></div>
<div>
<h1 className="text-xl font-bold text-gray-900 dark:text-white">Zeugnis-Assistent</h1>
<p className="text-sm text-gray-500 dark:text-gray-400">
{BUNDESLAENDER.find(b => b.code === preferences.bundesland)?.name || 'Alle Bundeslaender'}
{preferences.schulform && ` - ${SCHULFORMEN.find(s => s.id === preferences.schulform)?.name}`}
</p>
</div>
</div>
<button onClick={() => setShowWizard(true)} className="p-2 text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200 hover:bg-gray-100 dark:hover:bg-gray-700 rounded-lg transition" title="Einstellungen">
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z" /><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" /></svg>
</button>
</div>
<div className="flex gap-1 mt-4 -mb-px">
{([{ id: 'chat' as const, label: 'Assistent', icon: '💬' }, { id: 'search' as const, label: 'Suche', icon: '🔍' }, { id: 'documents' as const, label: 'Dokumente', icon: '📄' }]).map((tab) => (
<button key={tab.id} onClick={() => setActiveTab(tab.id)} className={`px-6 py-3 text-sm font-medium rounded-t-lg transition ${activeTab === tab.id ? 'bg-gray-100 dark:bg-gray-700 text-blue-600 dark:text-blue-400 border-b-2 border-blue-600' : 'text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white'}`}>
<span className="mr-2">{tab.icon}</span>{tab.label}
</button>
))}
</div>
</div>
</header>
<main className="max-w-7xl mx-auto p-6">
<div className="bg-white dark:bg-gray-800 rounded-2xl shadow-lg border border-gray-200 dark:border-gray-700 overflow-hidden min-h-[600px]">
{activeTab === 'chat' && <ChatInterface messages={messages} onSendMessage={handleSendMessage} isLoading={isLoading} bundesland={preferences.bundesland} />}
{activeTab === 'search' && (
<div className="p-6">
<div className="relative mb-6">
<input type="text" value={searchQuery} onChange={(e) => handleSearch(e.target.value)} placeholder="Suchen Sie in den Verordnungen..." className="w-full px-12 py-4 bg-gray-100 dark:bg-gray-900 border-0 rounded-xl text-lg focus:ring-2 focus:ring-blue-500" />
<svg className="absolute left-4 top-1/2 -translate-y-1/2 w-5 h-5 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" /></svg>
</div>
{!searchQuery && preferences.recentSearches.length > 0 && (
<div className="mb-6"><h3 className="text-sm font-medium text-gray-500 dark:text-gray-400 mb-3">Letzte Suchen</h3><div className="flex flex-wrap gap-2">{preferences.recentSearches.map((search, i) => (<button key={i} onClick={() => handleSearch(search)} className="px-3 py-1.5 text-sm bg-gray-100 dark:bg-gray-700 text-gray-700 dark:text-gray-300 rounded-full hover:bg-gray-200 dark:hover:bg-gray-600 transition">{search}</button>))}</div></div>
)}
{!searchQuery && (
<div className="mb-6"><h3 className="text-sm font-medium text-gray-500 dark:text-gray-400 mb-3">Haeufige Fragen</h3><div className="grid grid-cols-2 gap-3">{COMMON_QUESTIONS.map((q, i) => (<button key={i} onClick={() => handleSearch(q)} className="p-4 text-left bg-gray-50 dark:bg-gray-900 rounded-xl hover:bg-gray-100 dark:hover:bg-gray-800 transition"><p className="text-sm text-gray-700 dark:text-gray-300">{q}</p></button>))}</div></div>
)}
{searchQuery && <SearchResults results={searchResults} onSelect={(result) => { handleSendMessage(`Erklaere mir: ${result.title}`); setActiveTab('chat') }} />}
</div>
)}
{activeTab === 'documents' && (
<div className="p-6">
<div className="text-center py-12">
<span className="text-6xl">📚</span>
<h3 className="mt-4 text-xl font-semibold text-gray-900 dark:text-white">Dokumente-Browser</h3>
<p className="mt-2 text-gray-500 dark:text-gray-400 max-w-md mx-auto">Durchsuchen Sie alle verfuegbaren Verordnungen und Handreichungen fuer Ihr Bundesland.</p>
<button className="mt-6 px-6 py-3 bg-blue-600 text-white font-medium rounded-xl hover:bg-blue-700 transition">Dokumente durchsuchen</button>
</div>
</div>
)}
</div>
</main>
</div>
)
}