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,319 @@
'use client'
/**
* Education Search Page
* Bildungsquellen und Crawler-Verwaltung
*/
import { useState, useCallback } from 'react'
import { PagePurpose } from '@/components/common/PagePurpose'
import { Search, Database, RefreshCw, ExternalLink, FileText, BookOpen, FolderOpen } from 'lucide-react'
import { DokumenteTab } from '@/components/education/DokumenteTab'
interface DataSource {
id: string
name: string
type: 'api' | 'crawler' | 'manual'
status: 'active' | 'inactive' | 'error'
lastUpdate?: string
documentCount: number
url?: string
}
const DATA_SOURCES: DataSource[] = [
{
id: 'nibis',
name: 'NiBiS (Niedersachsen)',
type: 'crawler',
status: 'active',
lastUpdate: '2026-01-20',
documentCount: 1250,
url: 'https://nibis.de',
},
{
id: 'kmk',
name: 'KMK Beschluesse',
type: 'crawler',
status: 'active',
lastUpdate: '2026-01-10',
documentCount: 450,
url: 'https://kmk.org',
},
]
export default function EduSearchPage() {
const [searchQuery, setSearchQuery] = useState('')
const [activeTab, setActiveTab] = useState<'search' | 'documents' | 'sources' | 'crawler'>('search')
const [documentCount, setDocumentCount] = useState<number>(0)
const handleDocumentCountChange = useCallback((count: number) => {
setDocumentCount(count)
}, [])
return (
<div className="space-y-6">
<PagePurpose
title="Education Search"
purpose="Durchsuchen Sie Bildungsquellen und verwalten Sie Crawler fuer Lehrplaene, Erlasse und Schulinformationen. Zentraler Zugang zu bildungsrelevanten Dokumenten."
audience={['Content Manager', 'Entwickler', 'Bildungs-Admins']}
architecture={{
services: ['edu-search-service (Go)', 'OpenSearch'],
databases: ['OpenSearch (bp_documents_v1)', 'PostgreSQL'],
}}
collapsible={true}
defaultCollapsed={true}
/>
{/* Stats */}
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
<div className="bg-white rounded-xl border border-slate-200 p-4">
<div className="text-3xl font-bold text-blue-600">
{DATA_SOURCES.reduce((sum, s) => sum + s.documentCount, 0).toLocaleString()}
</div>
<div className="text-sm text-slate-500">Dokumente gesamt</div>
</div>
<div className="bg-white rounded-xl border border-slate-200 p-4">
<div className="text-3xl font-bold text-green-600">{DATA_SOURCES.length}</div>
<div className="text-sm text-slate-500">Datenquellen</div>
</div>
<div className="bg-white rounded-xl border border-slate-200 p-4">
<div className="text-3xl font-bold text-purple-600">
{DATA_SOURCES.filter(s => s.type === 'crawler').length}
</div>
<div className="text-sm text-slate-500">Aktive Crawler</div>
</div>
<div className="bg-white rounded-xl border border-slate-200 p-4">
<div className="text-3xl font-bold text-orange-600">16</div>
<div className="text-sm text-slate-500">Bundeslaender</div>
</div>
</div>
{/* Tabs */}
<div className="flex gap-2">
<button
onClick={() => setActiveTab('search')}
className={`px-4 py-2 rounded-lg font-medium transition-colors ${
activeTab === 'search'
? 'bg-blue-600 text-white'
: 'bg-slate-100 text-slate-600 hover:bg-slate-200'
}`}
>
<Search className="w-4 h-4 inline mr-2" />
Suche
</button>
<button
onClick={() => setActiveTab('documents')}
className={`px-4 py-2 rounded-lg font-medium transition-colors ${
activeTab === 'documents'
? 'bg-blue-600 text-white'
: 'bg-slate-100 text-slate-600 hover:bg-slate-200'
}`}
>
<FolderOpen className="w-4 h-4 inline mr-2" />
Dokumente
{documentCount > 0 && (
<span className="ml-2 px-1.5 py-0.5 bg-white/20 rounded text-xs">
{documentCount}
</span>
)}
</button>
<button
onClick={() => setActiveTab('sources')}
className={`px-4 py-2 rounded-lg font-medium transition-colors ${
activeTab === 'sources'
? 'bg-blue-600 text-white'
: 'bg-slate-100 text-slate-600 hover:bg-slate-200'
}`}
>
<Database className="w-4 h-4 inline mr-2" />
Datenquellen
</button>
<button
onClick={() => setActiveTab('crawler')}
className={`px-4 py-2 rounded-lg font-medium transition-colors ${
activeTab === 'crawler'
? 'bg-blue-600 text-white'
: 'bg-slate-100 text-slate-600 hover:bg-slate-200'
}`}
>
<RefreshCw className="w-4 h-4 inline mr-2" />
Crawler
</button>
</div>
{/* Search Tab */}
{activeTab === 'search' && (
<div className="bg-white rounded-xl border border-slate-200 p-6">
<div className="flex gap-4 mb-6">
<input
type="text"
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
placeholder="Suche nach Lehrplaenen, Erlassen, Curricula..."
className="flex-1 px-4 py-3 border border-slate-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 text-lg"
/>
<button className="px-8 py-3 bg-blue-600 text-white rounded-lg hover:bg-blue-700 font-medium">
Suchen
</button>
</div>
<div className="flex flex-wrap gap-2 mb-6">
<span className="text-sm text-slate-500 mr-2">Schnellfilter:</span>
<button className="px-3 py-1 bg-slate-100 text-slate-700 rounded-full text-sm hover:bg-slate-200">
Lehrplaene
</button>
<button className="px-3 py-1 bg-slate-100 text-slate-700 rounded-full text-sm hover:bg-slate-200">
Erlasse
</button>
<button className="px-3 py-1 bg-slate-100 text-slate-700 rounded-full text-sm hover:bg-slate-200">
Kerncurricula
</button>
<button className="px-3 py-1 bg-slate-100 text-slate-700 rounded-full text-sm hover:bg-slate-200">
Abitur
</button>
<button className="px-3 py-1 bg-slate-100 text-slate-700 rounded-full text-sm hover:bg-slate-200">
Niedersachsen
</button>
</div>
<div className="text-center py-12 text-slate-500">
<BookOpen className="w-16 h-16 mx-auto mb-4 opacity-50" />
<p>Geben Sie einen Suchbegriff ein, um Bildungsdokumente zu durchsuchen</p>
<p className="text-sm mt-2">Die Suche durchsucht alle angebundenen Datenquellen</p>
</div>
</div>
)}
{/* Documents Tab */}
{activeTab === 'documents' && (
<DokumenteTab onDocumentCountChange={handleDocumentCountChange} />
)}
{/* Sources Tab */}
{activeTab === 'sources' && (
<div className="bg-white rounded-xl border border-slate-200 overflow-hidden">
<table className="w-full text-sm">
<thead className="bg-slate-50 border-b border-slate-200">
<tr>
<th className="text-left px-4 py-3 font-medium text-slate-600">Datenquelle</th>
<th className="text-left px-4 py-3 font-medium text-slate-600">Typ</th>
<th className="text-center px-4 py-3 font-medium text-slate-600">Status</th>
<th className="text-right px-4 py-3 font-medium text-slate-600">Dokumente</th>
<th className="text-left px-4 py-3 font-medium text-slate-600">Letztes Update</th>
<th className="text-center px-4 py-3 font-medium text-slate-600">Aktion</th>
</tr>
</thead>
<tbody>
{DATA_SOURCES.map((source) => (
<tr key={source.id} className="border-b border-slate-100 hover:bg-slate-50">
<td className="px-4 py-3">
<div className="flex items-center gap-2">
<Database className="w-4 h-4 text-slate-400" />
<div className="font-medium text-slate-900">{source.name}</div>
</div>
</td>
<td className="px-4 py-3">
<span className={`px-2 py-0.5 rounded-full text-xs ${
source.type === 'api' ? 'bg-blue-100 text-blue-700' :
source.type === 'crawler' ? 'bg-purple-100 text-purple-700' :
'bg-slate-100 text-slate-700'
}`}>
{source.type.toUpperCase()}
</span>
</td>
<td className="px-4 py-3 text-center">
<span className={`px-2 py-0.5 rounded-full text-xs ${
source.status === 'active' ? 'bg-green-100 text-green-700' :
source.status === 'error' ? 'bg-red-100 text-red-700' :
'bg-slate-100 text-slate-700'
}`}>
{source.status === 'active' ? 'Aktiv' : source.status === 'error' ? 'Fehler' : 'Inaktiv'}
</span>
</td>
<td className="px-4 py-3 text-right font-medium">
{source.documentCount.toLocaleString()}
</td>
<td className="px-4 py-3 text-slate-500">
{source.lastUpdate || '-'}
</td>
<td className="px-4 py-3 text-center">
{source.url && (
<a
href={source.url}
target="_blank"
rel="noopener noreferrer"
className="p-1 text-blue-600 hover:bg-blue-50 rounded inline-block"
title="Quelle oeffnen"
>
<ExternalLink className="w-4 h-4" />
</a>
)}
</td>
</tr>
))}
</tbody>
</table>
</div>
)}
{/* Crawler Tab */}
{activeTab === 'crawler' && (
<div className="space-y-6">
<div className="bg-white rounded-xl border border-slate-200 p-6">
<h3 className="text-lg font-semibold text-slate-900 mb-4">Crawler-Verwaltung</h3>
<p className="text-sm text-slate-600 mb-4">
Hier koennen Sie die Crawler fuer verschiedene Bildungsquellen steuern.
Das System crawlt ausschliesslich oeffentliche Bildungsdokumente (Lehrplaene, Erlasse, Curricula). Keine Personendaten.
</p>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="border border-slate-200 rounded-lg p-4">
<div className="flex items-center gap-2 mb-2">
<FileText className="w-5 h-5 text-purple-600" />
<span className="font-medium">NiBiS Crawler</span>
</div>
<p className="text-sm text-slate-600 mb-3">
Crawlt Lehrplaene und Erlasse aus Niedersachsen
</p>
<button className="px-4 py-2 bg-purple-600 text-white rounded-lg hover:bg-purple-700 text-sm">
Crawl starten
</button>
</div>
<div className="border border-slate-200 rounded-lg p-4">
<div className="flex items-center gap-2 mb-2">
<FileText className="w-5 h-5 text-blue-600" />
<span className="font-medium">KMK Crawler</span>
</div>
<p className="text-sm text-slate-600 mb-3">
Crawlt Beschluesse der Kultusministerkonferenz
</p>
<button className="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 text-sm">
Crawl starten
</button>
</div>
</div>
</div>
</div>
)}
{/* Info Box */}
<div className="bg-blue-50 border border-blue-200 rounded-xl p-6">
<h3 className="font-semibold text-blue-800 flex items-center gap-2">
<span></span>
Verwandte Module
</h3>
<div className="mt-3 grid grid-cols-1 md:grid-cols-2 gap-4">
<a href="/education/zeugnisse-crawler" className="block p-3 bg-white rounded-lg hover:shadow-md transition-shadow">
<div className="font-medium text-slate-900">Zeugnisse-Crawler</div>
<div className="text-sm text-slate-500">Zeugnis-Strukturen verwalten</div>
</a>
<a href="/ai/rag-pipeline" className="block p-3 bg-white rounded-lg hover:shadow-md transition-shadow">
<div className="font-medium text-slate-900">RAG Pipeline</div>
<div className="text-sm text-slate-500">Bildungsdokumente indexieren</div>
</a>
</div>
</div>
</div>
)
}