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>
97 lines
5.2 KiB
TypeScript
97 lines
5.2 KiB
TypeScript
'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>
|
|
)
|
|
}
|