A previous `git pull --rebase origin main` dropped 177 local commits,
losing 3400+ files across admin-v2, backend, studio-v2, website,
klausur-service, and many other services. The partial restore attempt
(660295e2) only recovered some files.
This commit restores all missing files from pre-rebase ref 98933f5e
while preserving post-rebase additions (night-scheduler, night-mode UI,
NightModeWidget dashboard integration).
Restored features include:
- AI Module Sidebar (FAB), OCR Labeling, OCR Compare
- GPU Dashboard, RAG Pipeline, Magic Help
- Klausur-Korrektur (8 files), Abitur-Archiv (5+ files)
- Companion, Zeugnisse-Crawler, Screen Flow
- Full backend, studio-v2, website, klausur-service
- All compliance SDKs, agent-core, voice-service
- CI/CD configs, documentation, scripts
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
528 lines
19 KiB
TypeScript
528 lines
19 KiB
TypeScript
'use client'
|
|
|
|
/**
|
|
* Communication Tool - Lehrer-Eltern-Kommunikation
|
|
*
|
|
* KI-gestuetzte Unterstuetzung fuer professionelle, rechtlich fundierte
|
|
* und empathische Elternkommunikation basierend auf den Prinzipien
|
|
* der gewaltfreien Kommunikation (GFK).
|
|
*/
|
|
|
|
import { useState, useEffect } from 'react'
|
|
|
|
const API_BASE = process.env.NEXT_PUBLIC_BACKEND_URL || 'http://localhost:8000'
|
|
|
|
interface Option {
|
|
value: string
|
|
label: string
|
|
}
|
|
|
|
interface LegalReference {
|
|
law: string
|
|
paragraph: string
|
|
title: string
|
|
summary: string
|
|
relevance: string
|
|
}
|
|
|
|
interface GFKPrinciple {
|
|
principle: string
|
|
description: string
|
|
example: string
|
|
}
|
|
|
|
interface ValidationResult {
|
|
is_valid: boolean
|
|
issues: string[]
|
|
suggestions: string[]
|
|
positive_elements: string[]
|
|
gfk_score: number
|
|
}
|
|
|
|
export default function CommunicationToolPage() {
|
|
// Form state
|
|
const [communicationType, setCommunicationType] = useState('behavior')
|
|
const [tone, setTone] = useState('professional')
|
|
const [state, setState] = useState('NRW')
|
|
const [studentName, setStudentName] = useState('')
|
|
const [parentName, setParentName] = useState('')
|
|
const [situation, setSituation] = useState('')
|
|
const [additionalInfo, setAdditionalInfo] = useState('')
|
|
|
|
// Options
|
|
const [types, setTypes] = useState<Option[]>([])
|
|
const [tones, setTones] = useState<Option[]>([])
|
|
const [states, setStates] = useState<Option[]>([])
|
|
|
|
// Result state
|
|
const [generatedMessage, setGeneratedMessage] = useState('')
|
|
const [subject, setSubject] = useState('')
|
|
const [validation, setValidation] = useState<ValidationResult | null>(null)
|
|
const [legalRefs, setLegalRefs] = useState<LegalReference[]>([])
|
|
const [gfkPrinciples, setGfkPrinciples] = useState<GFKPrinciple[]>([])
|
|
|
|
// UI state
|
|
const [loading, setLoading] = useState(false)
|
|
const [error, setError] = useState<string | null>(null)
|
|
const [showGFKInfo, setShowGFKInfo] = useState(false)
|
|
const [showLegalInfo, setShowLegalInfo] = useState(false)
|
|
|
|
// Fetch options on mount
|
|
useEffect(() => {
|
|
const fetchOptions = async () => {
|
|
try {
|
|
const [typesRes, tonesRes, statesRes, gfkRes] = await Promise.all([
|
|
fetch(`${API_BASE}/v1/communication/types`),
|
|
fetch(`${API_BASE}/v1/communication/tones`),
|
|
fetch(`${API_BASE}/v1/communication/states`),
|
|
fetch(`${API_BASE}/v1/communication/gfk-principles`),
|
|
])
|
|
|
|
if (typesRes.ok) setTypes(await typesRes.json())
|
|
if (tonesRes.ok) setTones(await tonesRes.json())
|
|
if (statesRes.ok) setStates(await statesRes.json())
|
|
if (gfkRes.ok) setGfkPrinciples(await gfkRes.json())
|
|
} catch (e) {
|
|
console.error('Fehler beim Laden der Optionen:', e)
|
|
// Fallback-Werte
|
|
setTypes([
|
|
{ value: 'behavior', label: 'Verhalten/Disziplin' },
|
|
{ value: 'academic', label: 'Schulleistungen' },
|
|
{ value: 'attendance', label: 'Fehlzeiten' },
|
|
{ value: 'meeting_invite', label: 'Einladung zum Gespraech' },
|
|
{ value: 'positive_feedback', label: 'Positives Feedback' },
|
|
{ value: 'concern', label: 'Bedenken aeussern' },
|
|
{ value: 'conflict', label: 'Konfliktloesung' },
|
|
])
|
|
setTones([
|
|
{ value: 'professional', label: 'Professionell-freundlich' },
|
|
{ value: 'formal', label: 'Sehr foermlich' },
|
|
{ value: 'warm', label: 'Warmherzig' },
|
|
{ value: 'concerned', label: 'Besorgt' },
|
|
])
|
|
setStates([
|
|
{ value: 'NRW', label: 'Nordrhein-Westfalen' },
|
|
{ value: 'BY', label: 'Bayern' },
|
|
{ value: 'BW', label: 'Baden-Wuerttemberg' },
|
|
])
|
|
}
|
|
}
|
|
fetchOptions()
|
|
}, [])
|
|
|
|
// Generate message
|
|
const handleGenerate = async () => {
|
|
if (!studentName || !parentName || !situation) {
|
|
setError('Bitte fuellen Sie alle Pflichtfelder aus.')
|
|
return
|
|
}
|
|
|
|
setLoading(true)
|
|
setError(null)
|
|
|
|
try {
|
|
const res = await fetch(`${API_BASE}/v1/communication/generate`, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({
|
|
communication_type: communicationType,
|
|
tone,
|
|
state,
|
|
student_name: studentName,
|
|
parent_name: parentName,
|
|
situation,
|
|
additional_info: additionalInfo || undefined,
|
|
}),
|
|
})
|
|
|
|
if (!res.ok) {
|
|
throw new Error(`Server-Fehler: ${res.status}`)
|
|
}
|
|
|
|
const data = await res.json()
|
|
setGeneratedMessage(data.message)
|
|
setSubject(data.subject)
|
|
setValidation(data.validation)
|
|
setLegalRefs(data.legal_references || [])
|
|
} catch (e) {
|
|
setError(`Fehler bei der Generierung: ${e instanceof Error ? e.message : 'Unbekannter Fehler'}`)
|
|
} finally {
|
|
setLoading(false)
|
|
}
|
|
}
|
|
|
|
// Improve existing text
|
|
const handleImprove = async () => {
|
|
if (!generatedMessage) return
|
|
|
|
setLoading(true)
|
|
setError(null)
|
|
|
|
try {
|
|
const res = await fetch(`${API_BASE}/v1/communication/improve`, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ text: generatedMessage }),
|
|
})
|
|
|
|
if (!res.ok) {
|
|
throw new Error(`Server-Fehler: ${res.status}`)
|
|
}
|
|
|
|
const data = await res.json()
|
|
if (data.was_improved) {
|
|
setGeneratedMessage(data.improved_text)
|
|
// Re-validate
|
|
const valRes = await fetch(`${API_BASE}/v1/communication/validate`, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ text: data.improved_text }),
|
|
})
|
|
if (valRes.ok) {
|
|
setValidation(await valRes.json())
|
|
}
|
|
}
|
|
} catch (e) {
|
|
setError(`Fehler bei der Verbesserung: ${e instanceof Error ? e.message : 'Unbekannter Fehler'}`)
|
|
} finally {
|
|
setLoading(false)
|
|
}
|
|
}
|
|
|
|
// Copy to clipboard
|
|
const handleCopy = async () => {
|
|
try {
|
|
await navigator.clipboard.writeText(generatedMessage)
|
|
alert('In die Zwischenablage kopiert!')
|
|
} catch (e) {
|
|
console.error('Kopieren fehlgeschlagen:', e)
|
|
}
|
|
}
|
|
|
|
return (
|
|
<div className="min-h-screen bg-gray-50 py-8">
|
|
<div className="max-w-6xl mx-auto px-4">
|
|
{/* Header */}
|
|
<div className="mb-8">
|
|
<h1 className="text-3xl font-bold text-gray-900 mb-2">
|
|
Kommunikationsassistent
|
|
</h1>
|
|
<p className="text-gray-600">
|
|
KI-gestuetzte Unterstuetzung fuer professionelle Elternkommunikation
|
|
nach den Prinzipien der gewaltfreien Kommunikation (GFK)
|
|
</p>
|
|
</div>
|
|
|
|
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
|
{/* Input Form */}
|
|
<div className="lg:col-span-1 space-y-6">
|
|
<div className="bg-white rounded-lg shadow p-6">
|
|
<h2 className="text-lg font-semibold mb-4">Nachricht erstellen</h2>
|
|
|
|
{/* Communication Type */}
|
|
<div className="mb-4">
|
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
|
Art der Kommunikation *
|
|
</label>
|
|
<select
|
|
value={communicationType}
|
|
onChange={(e) => setCommunicationType(e.target.value)}
|
|
className="w-full border rounded-md p-2"
|
|
>
|
|
{types.map((t) => (
|
|
<option key={t.value} value={t.value}>
|
|
{t.label}
|
|
</option>
|
|
))}
|
|
</select>
|
|
</div>
|
|
|
|
{/* Tone */}
|
|
<div className="mb-4">
|
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
|
Tonalitaet
|
|
</label>
|
|
<select
|
|
value={tone}
|
|
onChange={(e) => setTone(e.target.value)}
|
|
className="w-full border rounded-md p-2"
|
|
>
|
|
{tones.map((t) => (
|
|
<option key={t.value} value={t.value}>
|
|
{t.label}
|
|
</option>
|
|
))}
|
|
</select>
|
|
</div>
|
|
|
|
{/* State */}
|
|
<div className="mb-4">
|
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
|
Bundesland (fuer rechtliche Referenzen)
|
|
</label>
|
|
<select
|
|
value={state}
|
|
onChange={(e) => setState(e.target.value)}
|
|
className="w-full border rounded-md p-2"
|
|
>
|
|
{states.map((s) => (
|
|
<option key={s.value} value={s.value}>
|
|
{s.label}
|
|
</option>
|
|
))}
|
|
</select>
|
|
</div>
|
|
|
|
{/* Student Name */}
|
|
<div className="mb-4">
|
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
|
Name des Schuelers/der Schuelerin *
|
|
</label>
|
|
<input
|
|
type="text"
|
|
value={studentName}
|
|
onChange={(e) => setStudentName(e.target.value)}
|
|
placeholder="z.B. Max"
|
|
className="w-full border rounded-md p-2"
|
|
/>
|
|
</div>
|
|
|
|
{/* Parent Name */}
|
|
<div className="mb-4">
|
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
|
Anrede der Eltern *
|
|
</label>
|
|
<input
|
|
type="text"
|
|
value={parentName}
|
|
onChange={(e) => setParentName(e.target.value)}
|
|
placeholder="z.B. Frau Mueller"
|
|
className="w-full border rounded-md p-2"
|
|
/>
|
|
</div>
|
|
|
|
{/* Situation */}
|
|
<div className="mb-4">
|
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
|
Beschreibung der Situation *
|
|
</label>
|
|
<textarea
|
|
value={situation}
|
|
onChange={(e) => setSituation(e.target.value)}
|
|
placeholder="Beschreiben Sie die Situation sachlich und konkret..."
|
|
rows={4}
|
|
className="w-full border rounded-md p-2"
|
|
/>
|
|
</div>
|
|
|
|
{/* Additional Info */}
|
|
<div className="mb-4">
|
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
|
Zusaetzliche Informationen (optional)
|
|
</label>
|
|
<textarea
|
|
value={additionalInfo}
|
|
onChange={(e) => setAdditionalInfo(e.target.value)}
|
|
placeholder="Besondere Umstaende, gewuenschte Termine, etc."
|
|
rows={2}
|
|
className="w-full border rounded-md p-2"
|
|
/>
|
|
</div>
|
|
|
|
{/* Error */}
|
|
{error && (
|
|
<div className="mb-4 p-3 bg-red-50 text-red-700 rounded-md text-sm">
|
|
{error}
|
|
</div>
|
|
)}
|
|
|
|
{/* Generate Button */}
|
|
<button
|
|
onClick={handleGenerate}
|
|
disabled={loading}
|
|
className="w-full bg-blue-600 text-white py-2 px-4 rounded-md hover:bg-blue-700 disabled:opacity-50"
|
|
>
|
|
{loading ? 'Wird generiert...' : 'Nachricht generieren'}
|
|
</button>
|
|
</div>
|
|
|
|
{/* GFK Info Toggle */}
|
|
<div className="bg-white rounded-lg shadow p-6">
|
|
<button
|
|
onClick={() => setShowGFKInfo(!showGFKInfo)}
|
|
className="flex items-center justify-between w-full text-left"
|
|
>
|
|
<span className="font-semibold">Was ist GFK?</span>
|
|
<span>{showGFKInfo ? '-' : '+'}</span>
|
|
</button>
|
|
{showGFKInfo && (
|
|
<div className="mt-4 space-y-4">
|
|
<p className="text-sm text-gray-600">
|
|
Die Gewaltfreie Kommunikation (GFK) nach Marshall Rosenberg
|
|
ist ein Kommunikationsmodell, das auf vier Schritten basiert:
|
|
</p>
|
|
{gfkPrinciples.map((p, i) => (
|
|
<div key={i} className="border-l-4 border-blue-500 pl-3">
|
|
<h4 className="font-medium">{i + 1}. {p.principle}</h4>
|
|
<p className="text-sm text-gray-600">{p.description}</p>
|
|
<p className="text-xs text-gray-500 italic mt-1">
|
|
Beispiel: {p.example}
|
|
</p>
|
|
</div>
|
|
))}
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Output Area */}
|
|
<div className="lg:col-span-2 space-y-6">
|
|
{/* Generated Message */}
|
|
<div className="bg-white rounded-lg shadow p-6">
|
|
<div className="flex items-center justify-between mb-4">
|
|
<h2 className="text-lg font-semibold">Generierte Nachricht</h2>
|
|
{generatedMessage && (
|
|
<div className="flex gap-2">
|
|
<button
|
|
onClick={handleImprove}
|
|
disabled={loading}
|
|
className="text-sm bg-green-100 text-green-700 px-3 py-1 rounded hover:bg-green-200"
|
|
>
|
|
Verbessern
|
|
</button>
|
|
<button
|
|
onClick={handleCopy}
|
|
className="text-sm bg-gray-100 text-gray-700 px-3 py-1 rounded hover:bg-gray-200"
|
|
>
|
|
Kopieren
|
|
</button>
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{subject && (
|
|
<div className="mb-3 p-2 bg-gray-50 rounded">
|
|
<span className="text-sm font-medium text-gray-500">Betreff: </span>
|
|
<span className="text-sm">{subject}</span>
|
|
</div>
|
|
)}
|
|
|
|
{generatedMessage ? (
|
|
<textarea
|
|
value={generatedMessage}
|
|
onChange={(e) => setGeneratedMessage(e.target.value)}
|
|
className="w-full border rounded-md p-4 min-h-[400px] font-mono text-sm"
|
|
/>
|
|
) : (
|
|
<div className="text-gray-400 text-center py-20 border-2 border-dashed rounded-md">
|
|
Hier erscheint die generierte Nachricht
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{/* Validation */}
|
|
{validation && (
|
|
<div className="bg-white rounded-lg shadow p-6">
|
|
<h3 className="text-lg font-semibold mb-4">GFK-Analyse</h3>
|
|
|
|
{/* Score */}
|
|
<div className="mb-4">
|
|
<div className="flex items-center justify-between mb-1">
|
|
<span className="text-sm font-medium">GFK-Score</span>
|
|
<span className={`text-sm font-bold ${
|
|
validation.gfk_score >= 0.8 ? 'text-green-600' :
|
|
validation.gfk_score >= 0.6 ? 'text-yellow-600' : 'text-red-600'
|
|
}`}>
|
|
{Math.round(validation.gfk_score * 100)}%
|
|
</span>
|
|
</div>
|
|
<div className="w-full bg-gray-200 rounded-full h-2">
|
|
<div
|
|
className={`h-2 rounded-full ${
|
|
validation.gfk_score >= 0.8 ? 'bg-green-500' :
|
|
validation.gfk_score >= 0.6 ? 'bg-yellow-500' : 'bg-red-500'
|
|
}`}
|
|
style={{ width: `${validation.gfk_score * 100}%` }}
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Issues */}
|
|
{validation.issues.length > 0 && (
|
|
<div className="mb-4">
|
|
<h4 className="text-sm font-medium text-red-600 mb-2">
|
|
Verbesserungsvorschlaege:
|
|
</h4>
|
|
<ul className="text-sm space-y-1">
|
|
{validation.issues.map((issue, i) => (
|
|
<li key={i} className="flex items-start">
|
|
<span className="text-red-500 mr-2">!</span>
|
|
<span>{issue}</span>
|
|
</li>
|
|
))}
|
|
</ul>
|
|
</div>
|
|
)}
|
|
|
|
{/* Positive Elements */}
|
|
{validation.positive_elements.length > 0 && (
|
|
<div>
|
|
<h4 className="text-sm font-medium text-green-600 mb-2">
|
|
Positive Elemente:
|
|
</h4>
|
|
<ul className="text-sm space-y-1">
|
|
{validation.positive_elements.map((elem, i) => (
|
|
<li key={i} className="flex items-start">
|
|
<span className="text-green-500 mr-2">+</span>
|
|
<span>{elem}</span>
|
|
</li>
|
|
))}
|
|
</ul>
|
|
</div>
|
|
)}
|
|
</div>
|
|
)}
|
|
|
|
{/* Legal References */}
|
|
{legalRefs.length > 0 && (
|
|
<div className="bg-white rounded-lg shadow p-6">
|
|
<button
|
|
onClick={() => setShowLegalInfo(!showLegalInfo)}
|
|
className="flex items-center justify-between w-full text-left"
|
|
>
|
|
<h3 className="text-lg font-semibold">Rechtliche Grundlagen</h3>
|
|
<span>{showLegalInfo ? '-' : '+'}</span>
|
|
</button>
|
|
{showLegalInfo && (
|
|
<div className="mt-4 space-y-4">
|
|
{legalRefs.map((ref, i) => (
|
|
<div key={i} className="border rounded-md p-3 bg-gray-50">
|
|
<div className="font-medium">
|
|
{ref.law} {ref.paragraph}
|
|
</div>
|
|
<div className="text-sm text-gray-600">{ref.title}</div>
|
|
<div className="text-sm mt-1">{ref.summary}</div>
|
|
<div className="text-xs text-blue-600 mt-1">
|
|
Relevanz: {ref.relevance}
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
)}
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Footer */}
|
|
<div className="mt-8 text-center text-sm text-gray-500">
|
|
<p>
|
|
Hinweis: Die generierten Texte sind Vorschlaege und sollten vor dem
|
|
Versand ueberprueft und ggf. angepasst werden.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|