Files
breakpilot-lehrer/website/app/tools/communication/page.tsx
Benjamin Boenisch 5a31f52310 Initial commit: breakpilot-lehrer - Lehrer KI Platform
Services: Admin-Lehrer, Backend-Lehrer, Studio v2, Website,
Klausur-Service, School-Service, Voice-Service, Geo-Service,
BreakPilot Drive, Agent-Core

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-11 23:47:26 +01:00

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>
)
}