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>
This commit is contained in:
Benjamin Boenisch
2026-02-11 23:47:26 +01:00
commit 5a31f52310
1224 changed files with 425430 additions and 0 deletions

View File

@@ -0,0 +1,501 @@
'use client'
import { useState, useEffect, useCallback } from 'react'
import dynamic from 'next/dynamic'
import {
AOIResponse,
AOITheme,
AOIQuality,
Difficulty,
GeoJSONPolygon,
LearningNode,
GeoServiceHealth,
DemoTemplate,
} from './types'
// Dynamic imports for map components (no SSR)
const AOISelector = dynamic(
() => import('@/components/geo-lernwelt/AOISelector'),
{ ssr: false, loading: () => <MapLoadingPlaceholder /> }
)
const UnityViewer = dynamic(
() => import('@/components/geo-lernwelt/UnityViewer'),
{ ssr: false }
)
// API base URL
const GEO_SERVICE_URL = process.env.NEXT_PUBLIC_GEO_SERVICE_URL || 'http://localhost:8088'
// Loading placeholder for map
function MapLoadingPlaceholder() {
return (
<div className="w-full h-[400px] bg-slate-800 rounded-xl flex items-center justify-center">
<div className="text-white/60 flex flex-col items-center gap-2">
<div className="w-8 h-8 border-2 border-white/30 border-t-white rounded-full animate-spin" />
<span>Karte wird geladen...</span>
</div>
</div>
)
}
// Theme icons and colors
const THEME_CONFIG: Record<AOITheme, { icon: string; color: string; label: string }> = {
topographie: { icon: '🏔️', color: 'bg-amber-500', label: 'Topographie' },
landnutzung: { icon: '🏘️', color: 'bg-green-500', label: 'Landnutzung' },
orientierung: { icon: '🧭', color: 'bg-blue-500', label: 'Orientierung' },
geologie: { icon: '🪨', color: 'bg-stone-500', label: 'Geologie' },
hydrologie: { icon: '💧', color: 'bg-cyan-500', label: 'Hydrologie' },
vegetation: { icon: '🌲', color: 'bg-emerald-500', label: 'Vegetation' },
}
export default function GeoLernweltPage() {
// State
const [serviceHealth, setServiceHealth] = useState<GeoServiceHealth | null>(null)
const [currentAOI, setCurrentAOI] = useState<AOIResponse | null>(null)
const [drawnPolygon, setDrawnPolygon] = useState<GeoJSONPolygon | null>(null)
const [selectedTheme, setSelectedTheme] = useState<AOITheme>('topographie')
const [quality, setQuality] = useState<AOIQuality>('medium')
const [difficulty, setDifficulty] = useState<Difficulty>('mittel')
const [learningNodes, setLearningNodes] = useState<LearningNode[]>([])
const [isLoading, setIsLoading] = useState(false)
const [error, setError] = useState<string | null>(null)
const [activeTab, setActiveTab] = useState<'map' | 'unity'>('map')
const [demoTemplate, setDemoTemplate] = useState<DemoTemplate | null>(null)
// Check service health on mount
useEffect(() => {
checkServiceHealth()
loadMainauTemplate()
}, [])
const checkServiceHealth = async () => {
try {
const res = await fetch(`${GEO_SERVICE_URL}/health`)
if (res.ok) {
const health = await res.json()
setServiceHealth(health)
}
} catch (e) {
console.error('Service health check failed:', e)
setServiceHealth(null)
}
}
const loadMainauTemplate = async () => {
try {
const res = await fetch(`${GEO_SERVICE_URL}/api/v1/aoi/templates/mainau`)
if (res.ok) {
const template = await res.json()
setDemoTemplate(template)
}
} catch (e) {
console.error('Failed to load Mainau template:', e)
}
}
const handlePolygonDrawn = useCallback((polygon: GeoJSONPolygon) => {
setDrawnPolygon(polygon)
setError(null)
}, [])
const handleCreateAOI = async () => {
if (!drawnPolygon) {
setError('Bitte zeichne zuerst ein Gebiet auf der Karte.')
return
}
setIsLoading(true)
setError(null)
try {
const res = await fetch(`${GEO_SERVICE_URL}/api/v1/aoi`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
polygon: drawnPolygon,
theme: selectedTheme,
quality,
}),
})
if (!res.ok) {
const errorData = await res.json()
throw new Error(errorData.detail || 'Fehler beim Erstellen des Gebiets')
}
const aoi = await res.json()
setCurrentAOI(aoi)
// Poll for completion
pollAOIStatus(aoi.aoi_id)
} catch (e) {
setError(e instanceof Error ? e.message : 'Unbekannter Fehler')
} finally {
setIsLoading(false)
}
}
const pollAOIStatus = async (aoiId: string) => {
const poll = async () => {
try {
const res = await fetch(`${GEO_SERVICE_URL}/api/v1/aoi/${aoiId}`)
if (res.ok) {
const aoi = await res.json()
setCurrentAOI(aoi)
if (aoi.status === 'completed') {
// Load learning nodes
generateLearningNodes(aoiId)
} else if (aoi.status === 'failed') {
setError('Verarbeitung fehlgeschlagen')
} else {
// Continue polling
setTimeout(poll, 2000)
}
}
} catch (e) {
console.error('Polling error:', e)
}
}
poll()
}
const generateLearningNodes = async (aoiId: string) => {
try {
const res = await fetch(`${GEO_SERVICE_URL}/api/v1/learning/generate`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
aoi_id: aoiId,
theme: selectedTheme,
difficulty,
node_count: 5,
language: 'de',
}),
})
if (res.ok) {
const data = await res.json()
setLearningNodes(data.nodes)
}
} catch (e) {
console.error('Failed to generate learning nodes:', e)
}
}
const handleLoadDemo = () => {
if (demoTemplate) {
setDrawnPolygon(demoTemplate.polygon)
setSelectedTheme(demoTemplate.suggested_themes[0] || 'topographie')
}
}
return (
<div className="min-h-screen bg-gradient-to-br from-slate-900 via-indigo-950 to-slate-900">
{/* Header */}
<header className="border-b border-white/10 bg-black/20 backdrop-blur-lg">
<div className="max-w-7xl mx-auto px-4 py-4">
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
<span className="text-3xl">🌍</span>
<div>
<h1 className="text-xl font-semibold text-white">Geo-Lernwelt</h1>
<p className="text-sm text-white/60">Interaktive Erdkunde-Lernplattform</p>
</div>
</div>
{/* Service Status */}
<div className="flex items-center gap-2">
<div
className={`w-2 h-2 rounded-full ${
serviceHealth?.status === 'healthy'
? 'bg-green-500'
: 'bg-yellow-500 animate-pulse'
}`}
/>
<span className="text-sm text-white/60">
{serviceHealth?.status === 'healthy' ? 'Verbunden' : 'Verbinde...'}
</span>
</div>
</div>
</div>
</header>
<main className="max-w-7xl mx-auto px-4 py-6">
{/* Tab Navigation */}
<div className="flex gap-2 mb-6">
<button
onClick={() => setActiveTab('map')}
className={`px-4 py-2 rounded-lg font-medium transition-all ${
activeTab === 'map'
? 'bg-white/10 text-white'
: 'text-white/60 hover:text-white hover:bg-white/5'
}`}
>
🗺 Gebiet waehlen
</button>
<button
onClick={() => setActiveTab('unity')}
disabled={!currentAOI || currentAOI.status !== 'completed'}
className={`px-4 py-2 rounded-lg font-medium transition-all ${
activeTab === 'unity'
? 'bg-white/10 text-white'
: 'text-white/60 hover:text-white hover:bg-white/5'
} disabled:opacity-40 disabled:cursor-not-allowed`}
>
🎮 3D-Lernwelt
</button>
</div>
{/* Error Message */}
{error && (
<div className="mb-6 p-4 bg-red-500/20 border border-red-500/50 rounded-xl text-red-200">
{error}
</div>
)}
{activeTab === 'map' ? (
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
{/* Map Section (2/3) */}
<div className="lg:col-span-2 space-y-4">
{/* Map Card */}
<div className="bg-white/5 backdrop-blur-lg rounded-2xl border border-white/10 overflow-hidden">
<div className="p-4 border-b border-white/10">
<div className="flex items-center justify-between">
<h2 className="text-lg font-medium text-white">Gebiet auf der Karte waehlen</h2>
{demoTemplate && (
<button
onClick={handleLoadDemo}
className="px-3 py-1.5 text-sm bg-blue-500/20 hover:bg-blue-500/30 text-blue-300 rounded-lg transition-colors"
>
📍 Demo: Insel Mainau
</button>
)}
</div>
<p className="text-sm text-white/60 mt-1">
Zeichne ein Polygon (max. 4 km²) um das gewuenschte Lerngebiet
</p>
</div>
<div className="h-[500px]">
<AOISelector
onPolygonDrawn={handlePolygonDrawn}
initialPolygon={drawnPolygon}
maxAreaKm2={4}
geoServiceUrl={GEO_SERVICE_URL}
/>
</div>
</div>
{/* Attribution */}
<div className="text-xs text-white/40 text-center">
Kartendaten: © OpenStreetMap contributors (ODbL) | Hoehenmodell: © Copernicus DEM
</div>
</div>
{/* Settings Panel (1/3) */}
<div className="space-y-4">
{/* Theme Selection */}
<div className="bg-white/5 backdrop-blur-lg rounded-2xl border border-white/10 p-4">
<h3 className="text-white font-medium mb-3">Lernthema</h3>
<div className="grid grid-cols-2 gap-2">
{(Object.keys(THEME_CONFIG) as AOITheme[]).map((theme) => {
const config = THEME_CONFIG[theme]
return (
<button
key={theme}
onClick={() => setSelectedTheme(theme)}
className={`p-3 rounded-xl text-left transition-all ${
selectedTheme === theme
? 'bg-white/15 border border-white/30'
: 'bg-white/5 border border-transparent hover:bg-white/10'
}`}
>
<span className="text-2xl">{config.icon}</span>
<div className="text-sm text-white mt-1">{config.label}</div>
</button>
)
})}
</div>
</div>
{/* Quality Selection */}
<div className="bg-white/5 backdrop-blur-lg rounded-2xl border border-white/10 p-4">
<h3 className="text-white font-medium mb-3">Qualitaet</h3>
<div className="flex gap-2">
{(['low', 'medium', 'high'] as AOIQuality[]).map((q) => (
<button
key={q}
onClick={() => setQuality(q)}
className={`flex-1 py-2 rounded-lg text-sm transition-all ${
quality === q
? 'bg-white/15 text-white border border-white/30'
: 'bg-white/5 text-white/60 hover:bg-white/10'
}`}
>
{q === 'low' ? 'Schnell' : q === 'medium' ? 'Standard' : 'Hoch'}
</button>
))}
</div>
</div>
{/* Difficulty Selection */}
<div className="bg-white/5 backdrop-blur-lg rounded-2xl border border-white/10 p-4">
<h3 className="text-white font-medium mb-3">Schwierigkeitsgrad</h3>
<div className="flex gap-2">
{(['leicht', 'mittel', 'schwer'] as Difficulty[]).map((d) => (
<button
key={d}
onClick={() => setDifficulty(d)}
className={`flex-1 py-2 rounded-lg text-sm capitalize transition-all ${
difficulty === d
? 'bg-white/15 text-white border border-white/30'
: 'bg-white/5 text-white/60 hover:bg-white/10'
}`}
>
{d}
</button>
))}
</div>
</div>
{/* Area Info */}
{drawnPolygon && (
<div className="bg-white/5 backdrop-blur-lg rounded-2xl border border-white/10 p-4">
<h3 className="text-white font-medium mb-2">Ausgewaehltes Gebiet</h3>
<div className="text-sm text-white/60">
<p>Polygon gezeichnet </p>
<p className="text-white/40 text-xs mt-1">
Klicke &quot;Lernwelt erstellen&quot; um fortzufahren
</p>
</div>
</div>
)}
{/* Create Button */}
<button
onClick={handleCreateAOI}
disabled={!drawnPolygon || isLoading}
className={`w-full py-4 rounded-xl font-medium text-lg transition-all ${
drawnPolygon && !isLoading
? 'bg-gradient-to-r from-blue-500 to-purple-500 text-white hover:from-blue-600 hover:to-purple-600'
: 'bg-white/10 text-white/40 cursor-not-allowed'
}`}
>
{isLoading ? (
<span className="flex items-center justify-center gap-2">
<span className="w-5 h-5 border-2 border-white/30 border-t-white rounded-full animate-spin" />
Wird erstellt...
</span>
) : (
'🚀 Lernwelt erstellen'
)}
</button>
{/* AOI Status */}
{currentAOI && (
<div className="bg-white/5 backdrop-blur-lg rounded-2xl border border-white/10 p-4">
<h3 className="text-white font-medium mb-2">Status</h3>
<div className="space-y-2">
<div className="flex items-center gap-2">
<div
className={`w-2 h-2 rounded-full ${
currentAOI.status === 'completed'
? 'bg-green-500'
: currentAOI.status === 'failed'
? 'bg-red-500'
: 'bg-yellow-500 animate-pulse'
}`}
/>
<span className="text-sm text-white/80 capitalize">
{currentAOI.status === 'queued'
? 'In Warteschlange...'
: currentAOI.status === 'processing'
? 'Wird verarbeitet...'
: currentAOI.status === 'completed'
? 'Fertig!'
: 'Fehlgeschlagen'}
</span>
</div>
{currentAOI.area_km2 > 0 && (
<p className="text-xs text-white/50">
Flaeche: {currentAOI.area_km2.toFixed(2)} km²
</p>
)}
</div>
</div>
)}
</div>
</div>
) : (
/* Unity 3D Viewer Tab */
<div className="space-y-4">
<div className="bg-white/5 backdrop-blur-lg rounded-2xl border border-white/10 overflow-hidden">
<div className="p-4 border-b border-white/10 flex items-center justify-between">
<div>
<h2 className="text-lg font-medium text-white">3D-Lernwelt</h2>
<p className="text-sm text-white/60">
Erkunde das Gebiet und bearbeite die Lernstationen
</p>
</div>
<div className="flex items-center gap-2">
<span className="text-sm text-white/60">
{learningNodes.length} Lernstationen
</span>
</div>
</div>
<div className="h-[600px]">
{currentAOI && currentAOI.status === 'completed' ? (
<UnityViewer
aoiId={currentAOI.aoi_id}
manifestUrl={currentAOI.manifest_url}
learningNodes={learningNodes}
geoServiceUrl={GEO_SERVICE_URL}
/>
) : (
<div className="h-full flex items-center justify-center text-white/60">
Erstelle zuerst ein Lerngebiet im Tab &quot;Gebiet waehlen&quot;
</div>
)}
</div>
</div>
{/* Learning Nodes List */}
{learningNodes.length > 0 && (
<div className="bg-white/5 backdrop-blur-lg rounded-2xl border border-white/10 p-4">
<h3 className="text-white font-medium mb-3">Lernstationen</h3>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-3">
{learningNodes.map((node, idx) => (
<div
key={node.id}
className="p-3 bg-white/5 rounded-xl border border-white/10"
>
<div className="flex items-center gap-2 mb-2">
<span className="w-6 h-6 bg-blue-500/30 rounded-full flex items-center justify-center text-xs text-white">
{idx + 1}
</span>
<span className="text-white font-medium text-sm">{node.title}</span>
</div>
<p className="text-xs text-white/60 line-clamp-2">{node.question}</p>
<div className="mt-2 flex items-center gap-2">
<span className="text-xs bg-white/10 px-2 py-0.5 rounded text-white/60">
{node.points} Punkte
</span>
{node.approved && (
<span className="text-xs text-green-400"> Freigegeben</span>
)}
</div>
</div>
))}
</div>
</div>
)}
</div>
)}
</main>
</div>
)
}

View File

@@ -0,0 +1,282 @@
/**
* GeoEdu Service - TypeScript Types
* Types for the geography learning platform
*/
// Geographic types
export interface Position {
latitude: number
longitude: number
altitude?: number
}
export interface Bounds {
west: number
south: number
east: number
north: number
}
export interface GeoJSONPolygon {
type: 'Polygon'
coordinates: number[][][]
}
// AOI (Area of Interest) types
export type AOIStatus = 'queued' | 'processing' | 'completed' | 'failed'
export type AOIQuality = 'low' | 'medium' | 'high'
export type AOITheme =
| 'topographie'
| 'landnutzung'
| 'orientierung'
| 'geologie'
| 'hydrologie'
| 'vegetation'
export interface AOIRequest {
polygon: GeoJSONPolygon
theme: AOITheme
quality: AOIQuality
}
export interface AOIResponse {
aoi_id: string
status: AOIStatus
area_km2: number
estimated_size_mb: number
message?: string
download_url?: string
manifest_url?: string
created_at?: string
completed_at?: string
}
export interface AOIManifest {
version: string
aoi_id: string
created_at: string
bounds: Bounds
center: Position
area_km2: number
theme: AOITheme
quality: AOIQuality
assets: {
terrain: { file: string; config: string }
osm_features: { file: string }
learning_positions: { file: string }
attribution: { file: string }
}
unity: {
coordinate_system: string
scale: number
terrain_resolution: number
}
}
// Learning Node types
export type NodeType = 'question' | 'observation' | 'exploration'
export type Difficulty = 'leicht' | 'mittel' | 'schwer'
export interface LearningNode {
id: string
aoi_id: string
title: string
theme: AOITheme
position: Position
question: string
hints: string[]
answer: string
explanation: string
node_type: NodeType
points: number
approved: boolean
media?: {
type: 'image' | 'audio' | 'video'
url: string
}[]
tags?: string[]
difficulty?: Difficulty
grade_level?: string
}
export interface LearningNodeRequest {
aoi_id: string
theme: AOITheme
difficulty: Difficulty
node_count: number
grade_level?: string
language?: string
}
export interface LearningNodeResponse {
aoi_id: string
theme: string
nodes: LearningNode[]
total_count: number
generation_model: string
}
// Theme template types
export interface ThemeTemplate {
id: AOITheme
name: string
description: string
icon: string
grade_levels: string[]
example_questions: string[]
keywords: string[]
}
export interface LearningTemplates {
themes: ThemeTemplate[]
difficulties: {
id: Difficulty
name: string
description: string
}[]
supported_languages: string[]
}
// Attribution types
export interface AttributionSource {
name: string
license: string
url: string
attribution: string
required: boolean
logo_url?: string
}
export interface Attribution {
sources: AttributionSource[]
generated_at: string
notice: string
}
// Tile metadata types
export interface TileMetadata {
name: string
description: string
format: string
scheme: string
minzoom: number
maxzoom: number
bounds: [number, number, number, number]
center: [number, number, number]
attribution: string
data_available: boolean
last_updated?: string
}
export interface DEMMetadata {
name: string
description: string
resolution_m: number
coverage: string
bounds: [number, number, number, number]
vertical_datum: string
horizontal_datum: string
license: string
attribution: string
data_available: boolean
tiles_generated: number
}
// Service health status
export interface GeoServiceHealth {
status: 'healthy' | 'degraded' | 'unhealthy'
service: string
version: string
environment: string
data_status: {
pmtiles_available: boolean
dem_available: boolean
tile_cache_dir: boolean
bundle_dir: boolean
}
config: {
max_aoi_size_km2: number
supported_themes: AOITheme[]
}
}
// Map style types (for MapLibre)
export interface MapStyle {
version: number
name: string
metadata: {
description: string
attribution: string
}
sources: {
[key: string]: {
type: string
tiles?: string[]
url?: string
minzoom?: number
maxzoom?: number
attribution?: string
tileSize?: number
}
}
layers: MapLayer[]
terrain?: {
source: string
exaggeration: number
}
}
export interface MapLayer {
id: string
type: string
source?: string // Optional for background layers
'source-layer'?: string
minzoom?: number
maxzoom?: number
filter?: unknown[] // MapLibre filter expressions can have mixed types
layout?: Record<string, unknown>
paint?: Record<string, unknown>
}
// UI State types
export interface GeoLernweltState {
// Current AOI
currentAOI: AOIResponse | null
drawnPolygon: GeoJSONPolygon | null
// Selected theme and settings
selectedTheme: AOITheme
quality: AOIQuality
difficulty: Difficulty
// Learning nodes
learningNodes: LearningNode[]
selectedNode: LearningNode | null
// UI state
isDrawing: boolean
isLoading: boolean
error: string | null
// Unity viewer state
unityReady: boolean
unityProgress: number
}
// API response wrapper
export interface ApiResponse<T> {
data?: T
error?: string
message?: string
}
// Demo templates
export interface DemoTemplate {
name: string
description: string
polygon: GeoJSONPolygon
center: [number, number]
area_km2: number
suggested_themes: AOITheme[]
features: string[]
}