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>
411 lines
18 KiB
TypeScript
411 lines
18 KiB
TypeScript
'use client'
|
|
|
|
import React from 'react'
|
|
import type { PipelineCheckpoint } from '../types'
|
|
import type { UseRAGPageReturn } from '../_hooks/useRAGPage'
|
|
|
|
interface PipelineTabProps {
|
|
hook: UseRAGPageReturn
|
|
}
|
|
|
|
export function PipelineTab({ hook }: PipelineTabProps) {
|
|
const {
|
|
pipelineState,
|
|
pipelineLoading,
|
|
pipelineStarting,
|
|
autoRefresh,
|
|
setAutoRefresh,
|
|
elapsedTime,
|
|
fetchPipeline,
|
|
handleStartPipeline,
|
|
collectionStatus,
|
|
} = hook
|
|
|
|
return (
|
|
<div className="space-y-6">
|
|
{/* Pipeline Header */}
|
|
<div className="flex items-center justify-between flex-wrap gap-4">
|
|
<div className="flex items-center gap-4">
|
|
<h3 className="text-lg font-semibold text-slate-900">Compliance Pipeline Status</h3>
|
|
{pipelineState?.status === 'running' && elapsedTime && (
|
|
<div className="flex items-center gap-2 px-3 py-1.5 bg-blue-50 border border-blue-200 rounded-full">
|
|
<div className="w-2 h-2 bg-blue-500 rounded-full animate-pulse" />
|
|
<span className="text-sm font-medium text-blue-700">Laufzeit: {elapsedTime}</span>
|
|
</div>
|
|
)}
|
|
</div>
|
|
<div className="flex items-center gap-3">
|
|
<label className="flex items-center gap-2 text-sm text-slate-600 cursor-pointer">
|
|
<input
|
|
type="checkbox"
|
|
checked={autoRefresh}
|
|
onChange={(e) => setAutoRefresh(e.target.checked)}
|
|
className="w-4 h-4 text-teal-600 rounded border-slate-300 focus:ring-teal-500"
|
|
/>
|
|
Auto-Refresh
|
|
</label>
|
|
{(!pipelineState || pipelineState.status !== 'running') && (
|
|
<button
|
|
onClick={() => handleStartPipeline(false)}
|
|
disabled={pipelineStarting}
|
|
className="flex items-center gap-2 px-4 py-2 text-sm bg-green-600 text-white rounded-lg hover:bg-green-700 disabled:opacity-50"
|
|
>
|
|
{pipelineStarting ? (
|
|
<SpinnerIcon />
|
|
) : (
|
|
<PlayIcon />
|
|
)}
|
|
Pipeline starten
|
|
</button>
|
|
)}
|
|
<button
|
|
onClick={fetchPipeline}
|
|
disabled={pipelineLoading}
|
|
className="flex items-center gap-2 px-4 py-2 text-sm bg-teal-600 text-white rounded-lg hover:bg-teal-700 disabled:opacity-50"
|
|
>
|
|
{pipelineLoading ? <SpinnerIcon /> : <RefreshIcon />}
|
|
Aktualisieren
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
{/* No Data */}
|
|
{(!pipelineState || pipelineState.status === 'no_data') && !pipelineLoading && (
|
|
<NoDataCard pipelineStarting={pipelineStarting} handleStartPipeline={handleStartPipeline} />
|
|
)}
|
|
|
|
{/* Pipeline Status */}
|
|
{pipelineState && pipelineState.status !== 'no_data' && (
|
|
<>
|
|
{/* Status Card */}
|
|
<PipelineStatusCard pipelineState={pipelineState} />
|
|
|
|
{/* Current Progress */}
|
|
{pipelineState.status === 'running' && pipelineState.current_phase && (
|
|
<CurrentProgressCard pipelineState={pipelineState} collectionStatus={collectionStatus} />
|
|
)}
|
|
|
|
{/* Validation Summary */}
|
|
{pipelineState.validation_summary && (
|
|
<ValidationSummary summary={pipelineState.validation_summary} />
|
|
)}
|
|
|
|
{/* Checkpoints */}
|
|
<CheckpointsList checkpoints={pipelineState.checkpoints} />
|
|
|
|
{/* Summary */}
|
|
{Object.keys(pipelineState.summary || {}).length > 0 && (
|
|
<PipelineSummary summary={pipelineState.summary} />
|
|
)}
|
|
</>
|
|
)}
|
|
</div>
|
|
)
|
|
}
|
|
|
|
// --- Icons ---
|
|
|
|
function SpinnerIcon() {
|
|
return (
|
|
<svg className="animate-spin h-4 w-4" fill="none" viewBox="0 0 24 24">
|
|
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" />
|
|
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.7.689 3 7.938l3-2.647z" />
|
|
</svg>
|
|
)
|
|
}
|
|
|
|
function PlayIcon() {
|
|
return (
|
|
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M14.752 11.168l-3.197-2.132A1 1 0 0010 9.87v4.263a1 1 0 001.555.832l3.197-2.132a1 1 0 000-1.664z" />
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
|
</svg>
|
|
)
|
|
}
|
|
|
|
function RefreshIcon() {
|
|
return (
|
|
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
|
|
</svg>
|
|
)
|
|
}
|
|
|
|
// --- Sub-components ---
|
|
|
|
function NoDataCard({
|
|
pipelineStarting,
|
|
handleStartPipeline,
|
|
}: {
|
|
pipelineStarting: boolean
|
|
handleStartPipeline: (skip: boolean) => void
|
|
}) {
|
|
return (
|
|
<div className="bg-white rounded-xl border border-slate-200 p-8 text-center">
|
|
<div className="w-16 h-16 mx-auto mb-4 rounded-full bg-slate-100 flex items-center justify-center">
|
|
<svg className="w-8 h-8 text-slate-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
|
|
</svg>
|
|
</div>
|
|
<h4 className="text-lg font-semibold text-slate-900 mb-2">Keine Pipeline-Daten</h4>
|
|
<p className="text-slate-600 mb-4">
|
|
Es wurde noch keine Pipeline ausgefuehrt. Starten Sie die Compliance-Pipeline um Checkpoint-Daten zu sehen.
|
|
</p>
|
|
<button
|
|
onClick={() => handleStartPipeline(false)}
|
|
disabled={pipelineStarting}
|
|
className="inline-flex items-center gap-2 px-6 py-3 bg-green-600 text-white rounded-lg hover:bg-green-700 disabled:opacity-50"
|
|
>
|
|
{pipelineStarting ? (
|
|
<>
|
|
<svg className="animate-spin h-5 w-5" fill="none" viewBox="0 0 24 24">
|
|
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" />
|
|
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.7.689 3 7.938l3-2.647z" />
|
|
</svg>
|
|
Startet...
|
|
</>
|
|
) : (
|
|
<>
|
|
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M14.752 11.168l-3.197-2.132A1 1 0 0010 9.87v4.263a1 1 0 001.555.832l3.197-2.132a1 1 0 000-1.664z" />
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
|
</svg>
|
|
Pipeline jetzt starten
|
|
</>
|
|
)}
|
|
</button>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
function PipelineStatusCard({ pipelineState }: { pipelineState: any }) {
|
|
return (
|
|
<div className="bg-white rounded-xl border border-slate-200 p-6">
|
|
<div className="flex items-center justify-between">
|
|
<div className="flex items-center gap-4">
|
|
<div className={`w-12 h-12 rounded-xl flex items-center justify-center ${
|
|
pipelineState.status === 'completed' ? 'bg-green-100' :
|
|
pipelineState.status === 'running' ? 'bg-blue-100' :
|
|
pipelineState.status === 'failed' ? 'bg-red-100' : 'bg-slate-100'
|
|
}`}>
|
|
{pipelineState.status === 'completed' && (
|
|
<svg className="w-6 h-6 text-green-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
|
|
</svg>
|
|
)}
|
|
{pipelineState.status === 'running' && (
|
|
<svg className="w-6 h-6 text-blue-600 animate-spin" fill="none" viewBox="0 0 24 24">
|
|
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" />
|
|
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.7.689 3 7.938l3-2.647z" />
|
|
</svg>
|
|
)}
|
|
{pipelineState.status === 'failed' && (
|
|
<svg className="w-6 h-6 text-red-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
|
|
</svg>
|
|
)}
|
|
</div>
|
|
<div>
|
|
<h4 className="font-semibold text-slate-900">Pipeline {pipelineState.pipeline_id}</h4>
|
|
<p className="text-sm text-slate-500">
|
|
Gestartet: {pipelineState.started_at ? new Date(pipelineState.started_at).toLocaleString('de-DE') : '-'}
|
|
{pipelineState.completed_at && ` | Beendet: ${new Date(pipelineState.completed_at).toLocaleString('de-DE')}`}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
<span className={`px-3 py-1 rounded-full text-sm font-medium ${
|
|
pipelineState.status === 'completed' ? 'bg-green-100 text-green-700' :
|
|
pipelineState.status === 'running' ? 'bg-blue-100 text-blue-700' :
|
|
pipelineState.status === 'failed' ? 'bg-red-100 text-red-700' : 'bg-slate-100 text-slate-700'
|
|
}`}>
|
|
{pipelineState.status === 'completed' ? 'Abgeschlossen' :
|
|
pipelineState.status === 'running' ? 'Laeuft' :
|
|
pipelineState.status === 'failed' ? 'Fehlgeschlagen' : pipelineState.status}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
function CurrentProgressCard({ pipelineState, collectionStatus }: { pipelineState: any; collectionStatus: any }) {
|
|
return (
|
|
<div className="bg-gradient-to-r from-blue-50 to-indigo-50 rounded-xl border border-blue-200 p-6">
|
|
<div className="flex items-center justify-between mb-4">
|
|
<h4 className="font-semibold text-blue-900 flex items-center gap-2">
|
|
<svg className="w-5 h-5 animate-pulse" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 10V3L4 14h7v7l9-11h-7z" />
|
|
</svg>
|
|
Aktuelle Verarbeitung
|
|
</h4>
|
|
<span className="text-sm text-blue-600">Phase: {pipelineState.current_phase}</span>
|
|
</div>
|
|
|
|
{/* Phase Progress Indicator */}
|
|
<div className="flex items-center gap-2 mb-4">
|
|
{['ingestion', 'extraction', 'controls', 'measures'].map((phase, idx) => (
|
|
<div key={phase} className="flex-1 flex items-center">
|
|
<div className={`flex-1 h-2 rounded-full ${
|
|
pipelineState.current_phase === phase ? 'bg-blue-500 animate-pulse' :
|
|
pipelineState.checkpoints?.some((c: PipelineCheckpoint) => c.phase === phase && c.status === 'completed') ? 'bg-green-500' :
|
|
'bg-slate-200'
|
|
}`} />
|
|
{idx < 3 && <div className="w-2" />}
|
|
</div>
|
|
))}
|
|
</div>
|
|
<div className="flex justify-between text-xs text-slate-500 mb-4">
|
|
<span>Ingestion</span>
|
|
<span>Extraktion</span>
|
|
<span>Controls</span>
|
|
<span>Massnahmen</span>
|
|
</div>
|
|
|
|
{/* Current checkpoint details */}
|
|
{pipelineState.checkpoints?.filter((c: PipelineCheckpoint) => c.status === 'running').map((checkpoint: PipelineCheckpoint, idx: number) => (
|
|
<div key={idx} className="bg-white/60 rounded-lg p-4 mt-2">
|
|
<div className="flex items-center justify-between">
|
|
<div className="flex items-center gap-3">
|
|
<div className="w-3 h-3 bg-blue-500 rounded-full animate-pulse" />
|
|
<span className="font-medium text-slate-900">{checkpoint.name}</span>
|
|
</div>
|
|
{checkpoint.metrics && Object.keys(checkpoint.metrics).length > 0 && (
|
|
<div className="flex gap-2">
|
|
{Object.entries(checkpoint.metrics).slice(0, 3).map(([key, value]) => (
|
|
<span key={key} className="px-2 py-1 bg-blue-100 text-blue-700 rounded text-xs">
|
|
{key.replace(/_/g, ' ')}: {typeof value === 'number' ? value.toLocaleString() : String(value)}
|
|
</span>
|
|
))}
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
))}
|
|
|
|
{/* Live chunk count */}
|
|
<div className="mt-4 flex items-center justify-between text-sm">
|
|
<span className="text-slate-600">Chunks in Qdrant:</span>
|
|
<span className="font-bold text-blue-700">{collectionStatus?.totalPoints?.toLocaleString() || '-'}</span>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
function ValidationSummary({ summary }: { summary: { passed: number; warning: number; failed: number; total: number } }) {
|
|
return (
|
|
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
|
|
<div className="bg-white rounded-xl border border-green-200 p-4">
|
|
<p className="text-sm text-slate-500">Bestanden</p>
|
|
<p className="text-2xl font-bold text-green-600">{summary.passed}</p>
|
|
</div>
|
|
<div className="bg-white rounded-xl border border-yellow-200 p-4">
|
|
<p className="text-sm text-slate-500">Warnungen</p>
|
|
<p className="text-2xl font-bold text-yellow-600">{summary.warning}</p>
|
|
</div>
|
|
<div className="bg-white rounded-xl border border-red-200 p-4">
|
|
<p className="text-sm text-slate-500">Fehlgeschlagen</p>
|
|
<p className="text-2xl font-bold text-red-600">{summary.failed}</p>
|
|
</div>
|
|
<div className="bg-white rounded-xl border border-slate-200 p-4">
|
|
<p className="text-sm text-slate-500">Gesamt</p>
|
|
<p className="text-2xl font-bold text-slate-700">{summary.total}</p>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
function CheckpointsList({ checkpoints }: { checkpoints?: PipelineCheckpoint[] }) {
|
|
return (
|
|
<div className="bg-white rounded-xl border border-slate-200 overflow-hidden">
|
|
<div className="px-4 py-3 border-b bg-slate-50">
|
|
<h3 className="font-semibold text-slate-900">Checkpoints ({checkpoints?.length || 0})</h3>
|
|
</div>
|
|
<div className="divide-y">
|
|
{checkpoints?.map((checkpoint, idx) => (
|
|
<div key={idx} className="p-4">
|
|
<div className="flex items-center justify-between mb-2">
|
|
<div className="flex items-center gap-3">
|
|
<span className={`w-3 h-3 rounded-full ${
|
|
checkpoint.phase === 'ingestion' ? 'bg-blue-500' :
|
|
checkpoint.phase === 'extraction' ? 'bg-purple-500' :
|
|
checkpoint.phase === 'controls' ? 'bg-green-500' : 'bg-orange-500'
|
|
}`} />
|
|
<span className="font-medium text-slate-900">{checkpoint.name}</span>
|
|
<span className="text-sm text-slate-500">
|
|
({checkpoint.phase}) |
|
|
{checkpoint.duration_seconds ? ` ${checkpoint.duration_seconds.toFixed(1)}s` : ' -'}
|
|
</span>
|
|
</div>
|
|
<span className={`px-2 py-0.5 rounded text-xs font-medium ${
|
|
checkpoint.status === 'completed' ? 'bg-green-100 text-green-700' :
|
|
checkpoint.status === 'running' ? 'bg-blue-100 text-blue-700' :
|
|
checkpoint.status === 'failed' ? 'bg-red-100 text-red-700' : 'bg-slate-100 text-slate-700'
|
|
}`}>
|
|
{checkpoint.status}
|
|
</span>
|
|
</div>
|
|
|
|
{/* Metrics */}
|
|
{Object.keys(checkpoint.metrics || {}).length > 0 && (
|
|
<div className="flex flex-wrap gap-2 mt-2">
|
|
{Object.entries(checkpoint.metrics).map(([key, value]) => (
|
|
<span key={key} className="px-2 py-1 bg-slate-100 rounded text-xs text-slate-600">
|
|
{key.replace(/_/g, ' ')}: <strong>{typeof value === 'number' ? value.toLocaleString() : String(value)}</strong>
|
|
</span>
|
|
))}
|
|
</div>
|
|
)}
|
|
|
|
{/* Validations */}
|
|
{checkpoint.validations?.length > 0 && (
|
|
<div className="mt-3 space-y-1">
|
|
{checkpoint.validations.map((v, vIdx) => (
|
|
<div key={vIdx} className="flex items-center gap-2 text-sm">
|
|
<span className={`w-4 h-4 flex items-center justify-center ${
|
|
v.status === 'passed' ? 'text-green-500' :
|
|
v.status === 'warning' ? 'text-yellow-500' : 'text-red-500'
|
|
}`}>
|
|
{v.status === 'passed' ? '✓' : v.status === 'warning' ? '⚠' : '✗'}
|
|
</span>
|
|
<span className="text-slate-700">{v.name}:</span>
|
|
<span className="text-slate-500">{v.message}</span>
|
|
</div>
|
|
))}
|
|
</div>
|
|
)}
|
|
|
|
{/* Error */}
|
|
{checkpoint.error && (
|
|
<div className="mt-2 p-2 bg-red-50 border border-red-200 rounded text-sm text-red-700">
|
|
{checkpoint.error}
|
|
</div>
|
|
)}
|
|
</div>
|
|
))}
|
|
{(!checkpoints || checkpoints.length === 0) && (
|
|
<div className="p-4 text-center text-slate-500">
|
|
Noch keine Checkpoints vorhanden.
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
function PipelineSummary({ summary }: { summary: Record<string, any> }) {
|
|
return (
|
|
<div className="bg-white rounded-xl border border-slate-200 p-4">
|
|
<h4 className="font-semibold text-slate-900 mb-3">Zusammenfassung</h4>
|
|
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
|
|
{Object.entries(summary).map(([key, value]) => (
|
|
<div key={key}>
|
|
<p className="text-sm text-slate-500">{key.replace(/_/g, ' ')}</p>
|
|
<p className="font-bold text-slate-900">
|
|
{typeof value === 'number' ? value.toLocaleString() : String(value)}
|
|
</p>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|