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
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>
278 lines
10 KiB
TypeScript
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">⚠</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"
|
|
>
|
|
✕
|
|
</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>
|
|
)
|
|
}
|