'use client' import { useState, useEffect, useCallback } from 'react' import type { Collection, IngestionStatus, LiveProgress, IndexedStats, PendingFile, PendingFilesData, IngestionHistoryEntry } from '../types' const API_BASE = process.env.NEXT_PUBLIC_KLAUSUR_SERVICE_URL || 'http://localhost:8086' interface IngestionTabProps { status: IngestionStatus | null onRefresh: () => void } function IngestionTab({ status, onRefresh }: IngestionTabProps) { const [starting, setStarting] = useState(false) const [liveProgress, setLiveProgress] = useState(null) const [indexedStats, setIndexedStats] = useState(null) const [pendingFiles, setPendingFiles] = useState(null) const [ingestionHistory, setIngestionHistory] = useState([]) const [showHistory, setShowHistory] = useState(false) const [showPending, setShowPending] = useState(false) // Fetch indexed stats, pending files, and history on mount useEffect(() => { const fetchStats = async () => { try { const res = await fetch(`${API_BASE}/api/v1/admin/nibis/stats`) if (res.ok) { const data = await res.json() setIndexedStats(data) } } catch (err) { console.error('Failed to fetch stats:', err) } } const fetchPending = async () => { try { const res = await fetch(`${API_BASE}/api/v1/admin/rag/files/pending`) if (res.ok) { const data = await res.json() setPendingFiles(data) } } catch (err) { console.error('Failed to fetch pending files:', err) } } const fetchHistory = async () => { try { const res = await fetch(`${API_BASE}/api/v1/admin/rag/ingestion/history?limit=20`) if (res.ok) { const data = await res.json() setIngestionHistory(data.history || []) } } catch (err) { console.error('Failed to fetch history:', err) } } fetchStats() fetchPending() fetchHistory() }, []) // Poll for live progress when running useEffect(() => { let interval: NodeJS.Timeout | null = null const fetchProgress = async () => { try { const res = await fetch(`${API_BASE}/api/v1/admin/nibis/progress`) if (res.ok) { const data = await res.json() setLiveProgress(data) // Refresh stats when complete if (!data.running && data.phase === 'complete') { onRefresh() // Also refresh indexed stats const statsRes = await fetch(`${API_BASE}/api/v1/admin/nibis/stats`) if (statsRes.ok) { setIndexedStats(await statsRes.json()) } } } } catch (err) { console.error('Failed to fetch progress:', err) } } // Start polling immediately and every 1.5 seconds fetchProgress() interval = setInterval(fetchProgress, 1500) return () => { if (interval) clearInterval(interval) } }, [onRefresh]) const startIngestion = async () => { setStarting(true) try { await fetch(`${API_BASE}/api/v1/admin/nibis/ingest`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ ewh_only: true, incremental: true }), }) onRefresh() } catch (err) { console.error('Failed to start ingestion:', err) } finally { setStarting(false) } } const phaseLabels: Record = { idle: 'Bereit', extracting: 'Entpacke ZIP-Dateien...', discovering: 'Suche Dokumente...', indexing: 'Indexiere Dokumente...', complete: 'Abgeschlossen', } return (

Ingestion Status

Übersicht über laufende und vergangene Indexierungsvorgänge

{/* Live Progress (when running) */} {liveProgress?.running && (
{liveProgress.phase ? (phaseLabels[liveProgress.phase] || liveProgress.phase) : 'Läuft...'} {(liveProgress.percent ?? 0).toFixed(1)}%
{/* Progress Bar */}
{/* Current File */} {liveProgress.current_filename && (

[{liveProgress.current_doc}/{liveProgress.total_docs}]{' '} {liveProgress.current_filename}

)} {/* Live Stats */}

Indexiert

{liveProgress.documents_indexed}

Übersprungen

{liveProgress.documents_skipped}

Chunks

{liveProgress.chunks_created}

Fehler

0 ? 'text-red-600' : 'text-primary-900'}`}> {liveProgress.errors_count ?? 0}

)} {/* Indexed Data Overview - Always visible */} {indexedStats?.indexed && (

Indexierte Daten (Gesamt)

Chunks gesamt

{(indexedStats.total_chunks ?? 0).toLocaleString()}

Jahre

{indexedStats.years?.length ?? 0}

{(indexedStats.years?.length ?? 0) > 0 ? `${Math.min(...indexedStats.years!)} - ${Math.max(...indexedStats.years!)}` : '-' }

Fächer

{indexedStats.subjects?.length || 0}

Status

Bereit

{/* Years breakdown */} {indexedStats.years && indexedStats.years.length > 0 && (
{indexedStats.years.sort((a, b) => a - b).map((year) => ( {year} ))}
)}
)} {/* Last Run Status */} {!liveProgress?.running && (

Letzter Indexierungslauf

{liveProgress?.phase === 'complete' ? 'Abgeschlossen' : 'Bereit'}
{status && (

Zeitpunkt

{status.lastRun ? new Date(status.lastRun).toLocaleString('de-DE') : 'Noch nie' }

Neu indexiert

{status.documentsIndexed ?? '-'}

Neue Chunks

{status.chunksCreated ?? '-'}

Fehler

0 ? 'text-red-600' : 'text-slate-900'}`}> {status.errors.length}

)} {/* Show skipped info from live progress */} {liveProgress && (liveProgress.documents_skipped ?? 0) > 0 && (

{liveProgress.documents_skipped} Dokumente übersprungen (bereits indexiert)

)} {status?.errors && status.errors.length > 0 && (

Fehler

    {status.errors.slice(0, 5).map((error, i) => (
  • {error}
  • ))} {status.errors.length > 5 && (
  • ... und {status.errors.length - 5} weitere
  • )}
)}
)} {/* Pending Files Section */} {pendingFiles && (pendingFiles.pending_count ?? 0) > 0 && (

{pendingFiles.pending_count} Dateien warten auf Indexierung

{pendingFiles.indexed_count} von {pendingFiles.total_files} Dateien sind indexiert

{/* Pending by year summary */} {pendingFiles.by_year && Object.keys(pendingFiles.by_year).length > 0 && (
{Object.entries(pendingFiles.by_year) .sort(([a], [b]) => Number(b) - Number(a)) .map(([year, count]) => ( {year}: {count} Dateien ))}
)} {/* Pending files list */} {showPending && pendingFiles.pending_files && (
{pendingFiles.pending_files.map((file) => ( ))}
Dateiname Jahr Fach Niveau
{file.filename} {file.year} {file.subject} {file.niveau}
{(pendingFiles.pending_count ?? 0) > 100 && (

Zeige 100 von {pendingFiles.pending_count} Dateien

)}
)}
)} {/* Ingestion History Section */}

Indexierungs-Historie

{ingestionHistory.length === 0 ? (

Noch keine Indexierungsläufe durchgeführt.

) : (
{(showHistory ? ingestionHistory : ingestionHistory.slice(0, 3)).map((entry) => (
{entry.status === 'success' ? ( ) : ( )} {new Date(entry.started_at || entry.startedAt).toLocaleString('de-DE')}
{entry.collection}
{entry.stats && (
Gefunden:{' '} {entry.stats.documents_found}
Indexiert:{' '} {entry.stats.documents_indexed}
Übersprungen:{' '} {entry.stats.documents_skipped}
Chunks:{' '} {entry.stats.chunks_created}
)} {entry.filters && (
{entry.filters.year && ( Jahr: {entry.filters.year} )} {entry.filters.subject && ( Fach: {entry.filters.subject} )} {entry.filters.incremental && ( Inkrementell )}
)} {entry.error && (

{entry.error}

)}
))}
)}
) } // ============================================================================ // Documents Tab // ============================================================================ export { IngestionTab }