Files
breakpilot-lehrer/admin-lehrer/app/(admin)/education/edu-search/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

320 lines
13 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
'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>
)
}