Files
breakpilot-lehrer/admin-lehrer/app/(admin)/ai/rag-pipeline/page.tsx
Benjamin Admin 9ba420fa91
Some checks failed
CI / go-lint (push) Has been skipped
CI / python-lint (push) Has been skipped
CI / nodejs-lint (push) Has been skipped
CI / test-go-school (push) Successful in 42s
CI / test-go-edu-search (push) Successful in 34s
CI / test-python-klausur (push) Failing after 2m51s
CI / test-python-agent-core (push) Successful in 21s
CI / test-nodejs-website (push) Successful in 29s
Fix: Remove broken getKlausurApiUrl and clean up empty lines
sed replacement left orphaned hostname references in story page
and empty lines in getApiBase functions.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-24 16:02:04 +02:00

278 lines
10 KiB
TypeScript

'use client'
/**
* RAG Pipeline Page
*
* Dokument-Indexierung fuer die semantische Suche.
* Teil der KI-Daten-Pipeline:
* OCR-Labeling -> RAG Pipeline -> Daten & RAG
*/
import { PagePurpose } from '@/components/common/PagePurpose'
import { AIModuleSidebarResponsive } from '@/components/ai/AIModuleSidebar'
import { useRagPipeline } from './useRagPipeline'
import { TabButton } from './_components/SharedWidgets'
import { TrainingJobCard } from './_components/TrainingJobCard'
import { DatasetOverview } from './_components/DatasetOverview'
import { ArchitectureTab } from './_components/ArchitectureTab'
import { DataSourcesTab } from './_components/DataSourcesTab'
import { NewTrainingModal } from './_components/NewTrainingModal'
export default function TrainingDashboardPage() {
const {
activeTab,
setActiveTab,
jobs,
stats,
dataSources,
showNewTrainingModal,
setShowNewTrainingModal,
setSelectedJob,
isLoading,
error,
setError,
handleStartTraining,
handlePauseJob,
handleResumeJob,
handleCancelJob,
} = useRagPipeline()
return (
<div className="min-h-screen bg-gradient-to-br from-gray-50 to-gray-100 dark:from-gray-900 dark:to-gray-800 p-8">
<div className="max-w-7xl mx-auto">
{/* Header */}
<div className="mb-8">
<div className="flex justify-between items-start mb-4">
<div>
<h1 className="text-3xl font-bold text-gray-900 dark:text-white">
RAG-Indexierung
</h1>
<p className="text-gray-500 dark:text-gray-400 mt-1">
Dokumente fuer die semantische Suche aufbereiten und indexieren
</p>
</div>
{activeTab === 'dashboard' && (
<button
onClick={() => setShowNewTrainingModal(true)}
className="px-6 py-3 bg-gradient-to-r from-blue-600 to-blue-700 text-white font-medium rounded-xl shadow-lg hover:shadow-xl transition-all hover:-translate-y-0.5"
>
<span className="flex items-center gap-2">
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 4v16m8-8H4" />
</svg>
Neue Indexierung
</span>
</button>
)}
</div>
<PagePurpose
title="RAG Pipeline"
purpose="Indexieren Sie Dokumente fuer die semantische Suche. Das System extrahiert Text aus PDFs (OCR), teilt ihn in Chunks auf, erstellt Vektor-Embeddings und speichert diese in Qdrant. Teil der KI-Daten-Pipeline: Empfaengt Ground Truth vom OCR-Labeling und liefert Embeddings an Daten & RAG."
audience={['Entwickler', 'Data Scientists', 'Bildungs-Admins']}
architecture={{
services: ['klausur-service (Python)', 'embedding-service (Python)'],
databases: ['Qdrant (Vektor)', 'PostgreSQL', 'MinIO'],
}}
relatedPages={[
{ name: 'OCR-Labeling', href: '/ai/ocr-labeling', description: 'Ground Truth erstellen' },
{ name: 'Daten & RAG', href: '/ai/rag', description: 'Indexierte Daten durchsuchen' },
{ name: 'Klausur-Korrektur', href: '/ai/klausur-korrektur', description: 'RAG-Suche nutzen' },
]}
collapsible={true}
defaultCollapsed={true}
/>
{/* Tabs */}
<div className="flex gap-2 bg-white dark:bg-gray-800 rounded-xl p-1 shadow-sm border border-gray-200 dark:border-gray-700 w-fit mt-4">
<TabButton active={activeTab === 'dashboard'} onClick={() => setActiveTab('dashboard')}>
Dashboard
</TabButton>
<TabButton active={activeTab === 'architecture'} onClick={() => setActiveTab('architecture')}>
Architektur
</TabButton>
<TabButton active={activeTab === 'sources'} onClick={() => setActiveTab('sources')}>
Datenquellen
</TabButton>
</div>
</div>
<AIModuleSidebarResponsive currentModule="rag-pipeline" />
{/* Error Banner */}
{error && (
<div className="mb-6 p-4 bg-amber-50 dark:bg-amber-900/30 border border-amber-200 dark:border-amber-700 rounded-xl">
<div className="flex items-center gap-3">
<span className="text-amber-500">&#9888;</span>
<span className="text-amber-800 dark:text-amber-200">{error}</span>
<button
onClick={() => setError(null)}
className="ml-auto text-amber-600 hover:text-amber-800"
>
&#10005;
</button>
</div>
</div>
)}
{/* Tab Content */}
{activeTab === 'architecture' ? (
<ArchitectureTab />
) : activeTab === 'sources' ? (
<DataSourcesTab sources={dataSources} />
) : isLoading ? (
<LoadingSpinner />
) : (
<DashboardContent
jobs={jobs}
stats={stats}
activeTab={activeTab}
setActiveTab={setActiveTab}
setShowNewTrainingModal={setShowNewTrainingModal}
setSelectedJob={setSelectedJob}
handlePauseJob={handlePauseJob}
handleResumeJob={handleResumeJob}
handleCancelJob={handleCancelJob}
/>
)}
</div>
<NewTrainingModal
isOpen={showNewTrainingModal}
onClose={() => setShowNewTrainingModal(false)}
onSubmit={handleStartTraining}
/>
</div>
)
}
// --- Internal layout components ---
function LoadingSpinner() {
return (
<div className="flex items-center justify-center py-20">
<div className="text-center">
<div className="w-12 h-12 border-4 border-blue-200 border-t-blue-600 rounded-full animate-spin mx-auto mb-4" />
<p className="text-gray-500 dark:text-gray-400">Lade Daten...</p>
</div>
</div>
)
}
function DashboardContent({ jobs, stats, setActiveTab, setShowNewTrainingModal, setSelectedJob, handlePauseJob, handleResumeJob, handleCancelJob }: {
jobs: import('./types').TrainingJob[]
stats: import('./types').DatasetStats
activeTab: string
setActiveTab: (tab: 'dashboard' | 'architecture' | 'sources') => void
setShowNewTrainingModal: (show: boolean) => void
setSelectedJob: (job: import('./types').TrainingJob | null) => void
handlePauseJob: (jobId: string) => Promise<void>
handleResumeJob: (jobId: string) => Promise<void>
handleCancelJob: (jobId: string) => Promise<void>
}) {
return (
<div className="grid grid-cols-3 gap-6">
{/* Training Jobs */}
<div className="col-span-2 space-y-6">
{jobs.length === 0 ? (
<EmptyJobsState onStart={() => setShowNewTrainingModal(true)} />
) : (
jobs.map(job => (
<TrainingJobCard
key={job.id}
job={job}
onPause={() => handlePauseJob(job.id)}
onResume={() => handleResumeJob(job.id)}
onStop={() => handleCancelJob(job.id)}
onViewDetails={() => setSelectedJob(job)}
/>
))
)}
</div>
{/* Sidebar */}
<div className="space-y-6">
<DatasetOverview stats={stats} />
<QuickActions setActiveTab={setActiveTab} />
</div>
</div>
)
}
function EmptyJobsState({ onStart }: { onStart: () => void }) {
return (
<div className="bg-white dark:bg-gray-800 rounded-2xl shadow-lg p-12 text-center">
<div className="w-16 h-16 mx-auto mb-4 bg-gray-100 dark:bg-gray-700 rounded-full flex items-center justify-center">
<svg className="w-8 h-8 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9.663 17h4.673M12 3v1m6.364 1.636l-.707.707M21 12h-1M4 12H3m3.343-5.657l-.707-.707m2.828 9.9a5 5 0 117.072 0l-.548.547A3.374 3.374 0 0014 18.469V19a2 2 0 11-4 0v-.531c0-.895-.356-1.754-.988-2.386l-.548-.547z" />
</svg>
</div>
<h3 className="text-lg font-medium text-gray-900 dark:text-white mb-2">
Keine aktive Indexierung
</h3>
<p className="text-gray-500 dark:text-gray-400 mb-6">
Starten Sie eine neue Indexierung, um Dokumente fuer die Suche aufzubereiten.
</p>
<button
onClick={onStart}
className="px-6 py-2 bg-blue-600 text-white font-medium rounded-lg hover:bg-blue-700"
>
Indexierung starten
</button>
</div>
)
}
function QuickActions({ setActiveTab }: {
setActiveTab: (tab: 'dashboard' | 'architecture' | 'sources') => void
}) {
return (
<div className="bg-white dark:bg-gray-800 rounded-2xl shadow-lg border border-gray-200 dark:border-gray-700 p-6">
<h3 className="text-lg font-semibold text-gray-900 dark:text-white mb-4">
Schnellaktionen
</h3>
<div className="space-y-3">
<QuickActionButton
icon="📐"
title="Architektur ansehen"
subtitle="Wie funktioniert das System?"
onClick={() => setActiveTab('architecture')}
/>
<QuickActionButton
icon="📚"
title="Datenquellen"
subtitle="Dokumente hinzufuegen"
onClick={() => setActiveTab('sources')}
/>
<QuickActionButton
icon="🔍"
title="Suche testen"
subtitle="RAG-Qualitaet pruefen"
/>
</div>
</div>
)
}
function QuickActionButton({ icon, title, subtitle, onClick }: {
icon: string
title: string
subtitle: string
onClick?: () => void
}) {
return (
<button
onClick={onClick}
className="w-full px-4 py-3 text-left bg-gray-50 dark:bg-gray-900 rounded-xl hover:bg-gray-100 dark:hover:bg-gray-700 transition"
>
<span className="flex items-center gap-3">
<span className="text-xl">{icon}</span>
<span>
<span className="block font-medium text-gray-900 dark:text-white">{title}</span>
<span className="text-sm text-gray-500">{subtitle}</span>
</span>
</span>
</button>
)
}