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>
320 lines
13 KiB
TypeScript
320 lines
13 KiB
TypeScript
'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>
|
||
)
|
||
}
|