[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>
This commit is contained in:
96
website/app/zeugnisse/_components/ChatInterface.tsx
Normal file
96
website/app/zeugnisse/_components/ChatInterface.tsx
Normal file
@@ -0,0 +1,96 @@
|
||||
'use client'
|
||||
|
||||
import { useState, useEffect, useRef } from 'react'
|
||||
import { Message, BUNDESLAENDER, COMMON_QUESTIONS } from './types'
|
||||
|
||||
export default function ChatInterface({ messages, onSendMessage, isLoading, bundesland }: {
|
||||
messages: Message[]
|
||||
onSendMessage: (message: string) => void
|
||||
isLoading: boolean
|
||||
bundesland: string | null
|
||||
}) {
|
||||
const [input, setInput] = useState('')
|
||||
const messagesEndRef = useRef<HTMLDivElement>(null)
|
||||
const inputRef = useRef<HTMLTextAreaElement>(null)
|
||||
|
||||
useEffect(() => {
|
||||
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' })
|
||||
}, [messages])
|
||||
|
||||
const handleSubmit = (e: React.FormEvent) => {
|
||||
e.preventDefault()
|
||||
if (input.trim() && !isLoading) {
|
||||
onSendMessage(input.trim())
|
||||
setInput('')
|
||||
}
|
||||
}
|
||||
|
||||
const handleKeyDown = (e: React.KeyboardEvent) => {
|
||||
if (e.key === 'Enter' && !e.shiftKey) {
|
||||
e.preventDefault()
|
||||
handleSubmit(e)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-col h-full">
|
||||
<div className="flex-1 overflow-y-auto p-6 space-y-6">
|
||||
{messages.length === 0 ? (
|
||||
<div className="text-center py-12">
|
||||
<div className="w-20 h-20 mx-auto mb-6 bg-gradient-to-br from-blue-100 to-purple-100 dark:from-blue-900/30 dark:to-purple-900/30 rounded-full flex items-center justify-center"><span className="text-4xl">💬</span></div>
|
||||
<h3 className="text-xl font-semibold text-gray-900 dark:text-white mb-3">Stellen Sie eine Frage</h3>
|
||||
<p className="text-gray-500 dark:text-gray-400 mb-6 max-w-md mx-auto">
|
||||
Der Zeugnis-Assistent beantwortet Ihre Fragen basierend auf den offiziellen Verordnungen {bundesland ? `fuer ${BUNDESLAENDER.find(b => b.code === bundesland)?.name}` : ''}.
|
||||
</p>
|
||||
<div className="flex flex-wrap justify-center gap-2">
|
||||
{COMMON_QUESTIONS.slice(0, 3).map((q, i) => (
|
||||
<button key={i} onClick={() => onSendMessage(q)} className="px-4 py-2 text-sm bg-gray-100 dark:bg-gray-800 text-gray-700 dark:text-gray-300 rounded-full hover:bg-gray-200 dark:hover:bg-gray-700 transition">{q}</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
messages.map((message) => (
|
||||
<div key={message.id} className={`flex ${message.role === 'user' ? 'justify-end' : 'justify-start'}`}>
|
||||
<div className={`max-w-[80%] rounded-2xl p-4 ${message.role === 'user' ? 'bg-blue-600 text-white' : 'bg-white dark:bg-gray-800 shadow-lg border border-gray-200 dark:border-gray-700'}`}>
|
||||
<p className={`${message.role === 'user' ? 'text-white' : 'text-gray-900 dark:text-white'}`}>{message.content}</p>
|
||||
{message.sources && message.sources.length > 0 && (
|
||||
<div className="mt-4 pt-4 border-t border-gray-200 dark:border-gray-700">
|
||||
<p className="text-xs font-medium text-gray-500 dark:text-gray-400 mb-2">Quellen:</p>
|
||||
<div className="space-y-2">
|
||||
{message.sources.map((source) => (
|
||||
<a key={source.id} href={source.url} target="_blank" rel="noopener noreferrer" className="block p-2 bg-gray-50 dark:bg-gray-900 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-800 transition">
|
||||
<p className="text-sm font-medium text-blue-600 dark:text-blue-400">{source.title}</p>
|
||||
<p className="text-xs text-gray-500">{source.bundesland_name} - {source.doc_type}</p>
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
{isLoading && (
|
||||
<div className="flex justify-start">
|
||||
<div className="bg-white dark:bg-gray-800 rounded-2xl p-4 shadow-lg border border-gray-200 dark:border-gray-700">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-2 h-2 bg-blue-500 rounded-full animate-bounce" />
|
||||
<div className="w-2 h-2 bg-blue-500 rounded-full animate-bounce" style={{ animationDelay: '0.1s' }} />
|
||||
<div className="w-2 h-2 bg-blue-500 rounded-full animate-bounce" style={{ animationDelay: '0.2s' }} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div ref={messagesEndRef} />
|
||||
</div>
|
||||
<div className="p-4 border-t border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800">
|
||||
<form onSubmit={handleSubmit} className="flex gap-3">
|
||||
<textarea ref={inputRef} value={input} onChange={(e) => setInput(e.target.value)} onKeyDown={handleKeyDown} placeholder="Stellen Sie Ihre Frage..." rows={1} className="flex-1 px-4 py-3 bg-gray-100 dark:bg-gray-900 border-0 rounded-xl resize-none focus:ring-2 focus:ring-blue-500 text-gray-900 dark:text-white placeholder-gray-500" />
|
||||
<button type="submit" disabled={!input.trim() || isLoading} className="px-6 py-3 bg-blue-600 text-white rounded-xl hover:bg-blue-700 disabled:opacity-50 disabled:cursor-not-allowed transition">
|
||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 19l9 2-9-18-9 18 9-2zm0 0v-8" /></svg>
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
87
website/app/zeugnisse/_components/OnboardingWizard.tsx
Normal file
87
website/app/zeugnisse/_components/OnboardingWizard.tsx
Normal file
@@ -0,0 +1,87 @@
|
||||
'use client'
|
||||
|
||||
import { useState } from 'react'
|
||||
import { UserPreferences, BUNDESLAENDER, SCHULFORMEN } from './types'
|
||||
|
||||
export default function OnboardingWizard({ onComplete }: { onComplete: (prefs: Partial<UserPreferences>) => void }) {
|
||||
const [step, setStep] = useState(0)
|
||||
const [bundesland, setBundesland] = useState<string | null>(null)
|
||||
const [schulform, setSchulform] = useState<string | null>(null)
|
||||
|
||||
const steps = [
|
||||
{ title: 'Willkommen beim Zeugnis-Assistenten', subtitle: 'Ihr intelligenter Helfer fuer alle Fragen rund um Zeugnisse' },
|
||||
{ title: 'In welchem Bundesland unterrichten Sie?', subtitle: 'Wir zeigen Ihnen die passenden Verordnungen' },
|
||||
{ title: 'An welcher Schulform?', subtitle: 'So koennen wir die Informationen noch besser anpassen' },
|
||||
{ title: 'Alles eingerichtet!', subtitle: 'Sie koennen jetzt loslegen' },
|
||||
]
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center bg-gradient-to-br from-blue-600 to-purple-700">
|
||||
<div className="bg-white dark:bg-gray-800 rounded-3xl shadow-2xl w-full max-w-2xl overflow-hidden">
|
||||
<div className="h-2 bg-gray-100 dark:bg-gray-700">
|
||||
<div className="h-full bg-gradient-to-r from-blue-500 to-purple-500 transition-all duration-500" style={{ width: `${((step + 1) / steps.length) * 100}%` }} />
|
||||
</div>
|
||||
<div className="p-8">
|
||||
{step === 0 && (
|
||||
<div className="text-center py-8">
|
||||
<div className="w-24 h-24 mx-auto mb-6 bg-gradient-to-br from-blue-500 to-purple-600 rounded-full flex items-center justify-center"><span className="text-5xl">📋</span></div>
|
||||
<h2 className="text-3xl font-bold text-gray-900 dark:text-white mb-3">{steps[0].title}</h2>
|
||||
<p className="text-lg text-gray-600 dark:text-gray-400 mb-8">{steps[0].subtitle}</p>
|
||||
<div className="grid grid-cols-3 gap-4 text-center mb-8">
|
||||
<div className="p-4"><span className="text-3xl">🔍</span><p className="mt-2 text-sm text-gray-600 dark:text-gray-400">Schnelle Suche in Verordnungen</p></div>
|
||||
<div className="p-4"><span className="text-3xl">💬</span><p className="mt-2 text-sm text-gray-600 dark:text-gray-400">KI-gestuetzte Antworten</p></div>
|
||||
<div className="p-4"><span className="text-3xl">📚</span><p className="mt-2 text-sm text-gray-600 dark:text-gray-400">Alle 16 Bundeslaender</p></div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{step === 1 && (
|
||||
<div>
|
||||
<h2 className="text-2xl font-bold text-gray-900 dark:text-white mb-2 text-center">{steps[1].title}</h2>
|
||||
<p className="text-gray-600 dark:text-gray-400 mb-6 text-center">{steps[1].subtitle}</p>
|
||||
<div className="grid grid-cols-4 gap-3 max-h-80 overflow-y-auto">
|
||||
{BUNDESLAENDER.map((bl) => (
|
||||
<button key={bl.code} onClick={() => setBundesland(bl.code)} className={`p-4 rounded-xl border-2 transition-all text-center hover:scale-105 ${bundesland === bl.code ? 'border-blue-500 bg-blue-50 dark:bg-blue-900/30 shadow-lg' : 'border-gray-200 dark:border-gray-700 hover:border-blue-300'}`}>
|
||||
<span className="text-2xl">{bl.emoji}</span><p className="mt-2 text-sm font-medium text-gray-900 dark:text-white">{bl.name}</p>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{step === 2 && (
|
||||
<div>
|
||||
<h2 className="text-2xl font-bold text-gray-900 dark:text-white mb-2 text-center">{steps[2].title}</h2>
|
||||
<p className="text-gray-600 dark:text-gray-400 mb-6 text-center">{steps[2].subtitle}</p>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
{SCHULFORMEN.map((sf) => (
|
||||
<button key={sf.id} onClick={() => setSchulform(sf.id)} className={`p-6 rounded-xl border-2 transition-all hover:scale-105 ${schulform === sf.id ? 'border-blue-500 bg-blue-50 dark:bg-blue-900/30 shadow-lg' : 'border-gray-200 dark:border-gray-700 hover:border-blue-300'}`}>
|
||||
<span className="text-3xl">{sf.icon}</span><p className="mt-3 text-lg font-medium text-gray-900 dark:text-white">{sf.name}</p>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{step === 3 && (
|
||||
<div className="text-center py-8">
|
||||
<div className="w-24 h-24 mx-auto mb-6 bg-gradient-to-br from-green-400 to-emerald-600 rounded-full flex items-center justify-center"><span className="text-5xl">✓</span></div>
|
||||
<h2 className="text-3xl font-bold text-gray-900 dark:text-white mb-3">{steps[3].title}</h2>
|
||||
<p className="text-lg text-gray-600 dark:text-gray-400 mb-6">{steps[3].subtitle}</p>
|
||||
<div className="bg-gray-50 dark:bg-gray-900 rounded-xl p-6 text-left max-w-md mx-auto">
|
||||
<p className="text-sm text-gray-600 dark:text-gray-400 mb-3">Ihre Einstellungen:</p>
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center gap-3"><span className="text-xl">{BUNDESLAENDER.find(b => b.code === bundesland)?.emoji}</span><span className="font-medium text-gray-900 dark:text-white">{BUNDESLAENDER.find(b => b.code === bundesland)?.name}</span></div>
|
||||
<div className="flex items-center gap-3"><span className="text-xl">{SCHULFORMEN.find(s => s.id === schulform)?.icon}</span><span className="font-medium text-gray-900 dark:text-white">{SCHULFORMEN.find(s => s.id === schulform)?.name}</span></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="px-8 py-6 bg-gray-50 dark:bg-gray-900 flex justify-between">
|
||||
<button onClick={() => step > 0 && setStep(step - 1)} className={`px-6 py-2 text-gray-600 dark:text-gray-400 ${step === 0 ? 'invisible' : ''}`}>Zurueck</button>
|
||||
<button onClick={() => { if (step < 3) { setStep(step + 1) } else { onComplete({ bundesland, schulform, hasSeenWizard: true }) } }} disabled={step === 1 && !bundesland || step === 2 && !schulform} className="px-8 py-3 bg-gradient-to-r from-blue-600 to-purple-600 text-white font-medium rounded-xl shadow-lg hover:shadow-xl transition-all disabled:opacity-50 disabled:cursor-not-allowed">
|
||||
{step === 3 ? 'Loslegen' : 'Weiter'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
48
website/app/zeugnisse/_components/SearchResults.tsx
Normal file
48
website/app/zeugnisse/_components/SearchResults.tsx
Normal file
@@ -0,0 +1,48 @@
|
||||
'use client'
|
||||
|
||||
import { SearchResult } from './types'
|
||||
|
||||
export default function SearchResults({ results, onSelect }: {
|
||||
results: SearchResult[]
|
||||
onSelect: (result: SearchResult) => void
|
||||
}) {
|
||||
if (results.length === 0) {
|
||||
return (
|
||||
<div className="text-center py-12">
|
||||
<span className="text-4xl">🔍</span>
|
||||
<p className="mt-4 text-gray-500 dark:text-gray-400">Keine Ergebnisse gefunden</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
{results.map((result) => (
|
||||
<button
|
||||
key={result.id}
|
||||
onClick={() => onSelect(result)}
|
||||
className="w-full text-left p-4 bg-white dark:bg-gray-800 rounded-xl shadow-sm border border-gray-200 dark:border-gray-700 hover:shadow-md hover:border-blue-300 transition"
|
||||
>
|
||||
<div className="flex items-start justify-between">
|
||||
<div className="flex-1">
|
||||
<h4 className="font-medium text-gray-900 dark:text-white mb-1">{result.title}</h4>
|
||||
<p className="text-sm text-gray-500 dark:text-gray-400 mb-2">{result.bundesland_name} - {result.doc_type}</p>
|
||||
<p className="text-sm text-gray-600 dark:text-gray-300 line-clamp-2">{result.snippet}</p>
|
||||
</div>
|
||||
<div className="ml-4 flex items-center gap-2">
|
||||
<span className={`px-2 py-1 text-xs rounded-full ${
|
||||
result.relevance_score > 0.8
|
||||
? 'bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200'
|
||||
: result.relevance_score > 0.5
|
||||
? 'bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-200'
|
||||
: 'bg-gray-100 text-gray-800 dark:bg-gray-700 dark:text-gray-200'
|
||||
}`}>
|
||||
{Math.round(result.relevance_score * 100)}%
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
65
website/app/zeugnisse/_components/types.ts
Normal file
65
website/app/zeugnisse/_components/types.ts
Normal file
@@ -0,0 +1,65 @@
|
||||
export interface SearchResult {
|
||||
id: string
|
||||
title: string
|
||||
bundesland: string
|
||||
bundesland_name: string
|
||||
doc_type: string
|
||||
snippet: string
|
||||
relevance_score: number
|
||||
url: string
|
||||
last_updated: string
|
||||
}
|
||||
|
||||
export interface Message {
|
||||
id: string
|
||||
role: 'user' | 'assistant' | 'system'
|
||||
content: string
|
||||
sources?: SearchResult[]
|
||||
timestamp: Date
|
||||
}
|
||||
|
||||
export interface UserPreferences {
|
||||
bundesland: string | null
|
||||
schulform: string | null
|
||||
hasSeenWizard: boolean
|
||||
favorites: string[]
|
||||
recentSearches: string[]
|
||||
}
|
||||
|
||||
export const BUNDESLAENDER = [
|
||||
{ code: 'bw', name: 'Baden-Wuerttemberg', emoji: '🏰' },
|
||||
{ code: 'by', name: 'Bayern', emoji: '🦁' },
|
||||
{ code: 'be', name: 'Berlin', emoji: '🐻' },
|
||||
{ code: 'bb', name: 'Brandenburg', emoji: '🦅' },
|
||||
{ code: 'hb', name: 'Bremen', emoji: '🔑' },
|
||||
{ code: 'hh', name: 'Hamburg', emoji: '⚓' },
|
||||
{ code: 'he', name: 'Hessen', emoji: '🦁' },
|
||||
{ code: 'mv', name: 'Mecklenburg-Vorpommern', emoji: '🐂' },
|
||||
{ code: 'ni', name: 'Niedersachsen', emoji: '🐴' },
|
||||
{ code: 'nw', name: 'Nordrhein-Westfalen', emoji: '🏛️' },
|
||||
{ code: 'rp', name: 'Rheinland-Pfalz', emoji: '🍇' },
|
||||
{ code: 'sl', name: 'Saarland', emoji: '⚒️' },
|
||||
{ code: 'sn', name: 'Sachsen', emoji: '⚔️' },
|
||||
{ code: 'st', name: 'Sachsen-Anhalt', emoji: '🏰' },
|
||||
{ code: 'sh', name: 'Schleswig-Holstein', emoji: '🌊' },
|
||||
{ code: 'th', name: 'Thueringen', emoji: '🌲' },
|
||||
]
|
||||
|
||||
export const SCHULFORMEN = [
|
||||
{ id: 'grundschule', name: 'Grundschule', icon: '🎒' },
|
||||
{ id: 'hauptschule', name: 'Hauptschule', icon: '📚' },
|
||||
{ id: 'realschule', name: 'Realschule', icon: '📖' },
|
||||
{ id: 'gymnasium', name: 'Gymnasium', icon: '🎓' },
|
||||
{ id: 'gesamtschule', name: 'Gesamtschule', icon: '🏫' },
|
||||
{ id: 'foerderschule', name: 'Foerderschule', icon: '💚' },
|
||||
{ id: 'berufsschule', name: 'Berufsschule', icon: '🔧' },
|
||||
]
|
||||
|
||||
export const COMMON_QUESTIONS = [
|
||||
'Wie formuliere ich eine Bemerkung zur Arbeits- und Sozialverhalten?',
|
||||
'Welche Noten duerfen im Zeugnis stehen?',
|
||||
'Wann sind Zeugniskonferenzen durchzufuehren?',
|
||||
'Wie gehe ich mit Fehlzeiten um?',
|
||||
'Welche Unterschriften sind erforderlich?',
|
||||
'Wie werden Versetzungsentscheidungen dokumentiert?',
|
||||
]
|
||||
Reference in New Issue
Block a user