backend-lehrer (10 files): - game/database.py (785 → 5), correction_api.py (683 → 4) - classroom_engine/antizipation.py (676 → 5) - llm_gateway schools/edu_search already done in prior batch klausur-service (12 files): - orientation_crop_api.py (694 → 5), pdf_export.py (677 → 4) - zeugnis_crawler.py (676 → 5), grid_editor_api.py (671 → 5) - eh_templates.py (658 → 5), mail/api.py (651 → 5) - qdrant_service.py (638 → 5), training_api.py (625 → 4) website (6 pages): - middleware (696 → 8), mail (733 → 6), consent (628 → 8) - compliance/risks (622 → 5), export (502 → 5), brandbook (629 → 7) studio-v2 (3 components): - B2BMigrationWizard (848 → 3), CleanupPanel (765 → 2) - dashboard-experimental (739 → 2) admin-lehrer (4 files): - uebersetzungen (769 → 4), manager (670 → 2) - ChunkBrowserQA (675 → 6), dsfa/page (674 → 5) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
235 lines
9.7 KiB
TypeScript
235 lines
9.7 KiB
TypeScript
'use client'
|
|
|
|
import React from 'react'
|
|
import { STRUCTURAL_KEYS, HIDDEN_KEYS } from './ChunkBrowserConstants'
|
|
import { getChunkText, getStructuralInfo } from './ChunkBrowserHelpers'
|
|
import { RAG_PDF_MAPPING } from './rag-pdf-mapping'
|
|
import { REGULATIONS_IN_RAG } from '../rag-constants'
|
|
|
|
interface ChunkBrowserContentProps {
|
|
selectedRegulation: string | null
|
|
docLoading: boolean
|
|
docChunks: Record<string, unknown>[]
|
|
docChunkIndex: number
|
|
docTotalChunks: number
|
|
splitViewActive: boolean
|
|
chunksPerPage: number
|
|
pdfExists: boolean | null
|
|
}
|
|
|
|
export function ChunkBrowserContent({
|
|
selectedRegulation,
|
|
docLoading,
|
|
docChunks,
|
|
docChunkIndex,
|
|
docTotalChunks,
|
|
splitViewActive,
|
|
chunksPerPage,
|
|
pdfExists,
|
|
}: ChunkBrowserContentProps) {
|
|
const currentChunk = docChunks[docChunkIndex] || null
|
|
const prevChunk = docChunkIndex > 0 ? docChunks[docChunkIndex - 1] : null
|
|
const nextChunk = docChunkIndex < docChunks.length - 1 ? docChunks[docChunkIndex + 1] : null
|
|
|
|
const structInfo = getStructuralInfo(currentChunk)
|
|
|
|
// PDF page estimation
|
|
const estimatePdfPage = (chunk: Record<string, unknown> | null, chunkIdx: number): number => {
|
|
if (chunk) {
|
|
const pages = chunk.pages as number[] | undefined
|
|
if (Array.isArray(pages) && pages.length > 0) return pages[0]
|
|
const page = chunk.page as number | undefined
|
|
if (typeof page === 'number' && page > 0) return page
|
|
}
|
|
const mapping = selectedRegulation ? RAG_PDF_MAPPING[selectedRegulation] : null
|
|
const cpp = mapping?.chunksPerPage || chunksPerPage
|
|
return Math.floor(chunkIdx / cpp) + 1
|
|
}
|
|
|
|
const pdfPage = estimatePdfPage(currentChunk, docChunkIndex)
|
|
const pdfMapping = selectedRegulation ? RAG_PDF_MAPPING[selectedRegulation] : null
|
|
const pdfUrl = pdfMapping ? `/rag-originals/${pdfMapping.filename}#page=${pdfPage}` : null
|
|
|
|
// Overlap extraction
|
|
const getOverlapPrev = (): string => {
|
|
if (!prevChunk) return ''
|
|
const text = getChunkText(prevChunk)
|
|
return text.length > 150 ? '...' + text.slice(-150) : text
|
|
}
|
|
|
|
const getOverlapNext = (): string => {
|
|
if (!nextChunk) return ''
|
|
const text = getChunkText(nextChunk)
|
|
return text.length > 150 ? text.slice(0, 150) + '...' : text
|
|
}
|
|
|
|
if (!selectedRegulation) {
|
|
return (
|
|
<div className="flex-1 flex items-center justify-center bg-white rounded-xl border border-slate-200">
|
|
<div className="text-center text-slate-400 space-y-2">
|
|
<div className="text-4xl">🔍</div>
|
|
<p className="text-sm">Dokument in der Sidebar auswaehlen, um QA zu starten.</p>
|
|
<p className="text-xs text-slate-300">Pfeiltasten: Chunk vor/zurueck</p>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
if (docLoading) {
|
|
return (
|
|
<div className="flex-1 flex items-center justify-center bg-white rounded-xl border border-slate-200">
|
|
<div className="text-center text-slate-500 space-y-2">
|
|
<div className="animate-spin text-3xl">⚙</div>
|
|
<p className="text-sm">Chunks werden geladen...</p>
|
|
<p className="text-xs text-slate-400">
|
|
{selectedRegulation}: {REGULATIONS_IN_RAG[selectedRegulation]?.chunks.toLocaleString() || '?'} Chunks erwartet
|
|
</p>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
return (
|
|
<div className={`flex-1 grid gap-3 min-h-0 ${splitViewActive ? 'grid-cols-2' : 'grid-cols-1'}`}>
|
|
{/* Chunk-Text Panel */}
|
|
<div className="bg-white rounded-xl border border-slate-200 flex flex-col min-h-0 overflow-hidden">
|
|
<div className="flex-shrink-0 px-4 py-2 bg-slate-50 border-b border-slate-100 flex items-center justify-between">
|
|
<span className="text-sm font-medium text-slate-700">Chunk-Text</span>
|
|
<div className="flex items-center gap-2">
|
|
{structInfo.article && (
|
|
<span className="px-2 py-0.5 bg-blue-50 text-blue-700 text-xs font-medium rounded border border-blue-200">
|
|
{structInfo.article}
|
|
</span>
|
|
)}
|
|
{structInfo.section && (
|
|
<span className="px-2 py-0.5 bg-purple-50 text-purple-700 text-xs rounded border border-purple-200">
|
|
{structInfo.section}
|
|
</span>
|
|
)}
|
|
<span className="text-xs text-slate-400 tabular-nums">
|
|
#{docChunkIndex} / {docTotalChunks - 1}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="flex-1 overflow-y-auto min-h-0 p-4 space-y-3">
|
|
{/* Overlap from previous chunk */}
|
|
{prevChunk && (
|
|
<div className="text-xs text-slate-400 bg-amber-50 border-l-2 border-amber-300 px-3 py-2 rounded-r">
|
|
<div className="font-medium text-amber-600 mb-1">↑ Ende vorheriger Chunk #{docChunkIndex - 1}</div>
|
|
<p className="whitespace-pre-wrap break-words leading-relaxed">{getOverlapPrev()}</p>
|
|
</div>
|
|
)}
|
|
|
|
{/* Current chunk text */}
|
|
{currentChunk ? (
|
|
<div className="text-sm text-slate-800 whitespace-pre-wrap break-words leading-relaxed border-l-2 border-teal-400 pl-3">
|
|
{getChunkText(currentChunk)}
|
|
</div>
|
|
) : (
|
|
<div className="text-sm text-slate-400 italic">Kein Chunk-Text vorhanden.</div>
|
|
)}
|
|
|
|
{/* Overlap from next chunk */}
|
|
{nextChunk && (
|
|
<div className="text-xs text-slate-400 bg-amber-50 border-l-2 border-amber-300 px-3 py-2 rounded-r">
|
|
<div className="font-medium text-amber-600 mb-1">↓ Anfang naechster Chunk #{docChunkIndex + 1}</div>
|
|
<p className="whitespace-pre-wrap break-words leading-relaxed">{getOverlapNext()}</p>
|
|
</div>
|
|
)}
|
|
|
|
{/* Metadata */}
|
|
{currentChunk && (
|
|
<div className="mt-4 pt-3 border-t border-slate-100">
|
|
<div className="text-xs font-medium text-slate-500 mb-2">Metadaten</div>
|
|
<div className="grid grid-cols-2 gap-x-4 gap-y-1 text-xs">
|
|
{Object.entries(currentChunk)
|
|
.filter(([k]) => !HIDDEN_KEYS.has(k))
|
|
.sort(([a], [b]) => {
|
|
const aStruct = STRUCTURAL_KEYS.has(a) ? 0 : 1
|
|
const bStruct = STRUCTURAL_KEYS.has(b) ? 0 : 1
|
|
return aStruct - bStruct || a.localeCompare(b)
|
|
})
|
|
.map(([k, v]) => (
|
|
<div key={k} className={`flex gap-1 ${STRUCTURAL_KEYS.has(k) ? 'col-span-2 font-medium' : ''}`}>
|
|
<span className="font-medium text-slate-500 flex-shrink-0">{k}:</span>
|
|
<span className="text-slate-700 break-all">
|
|
{Array.isArray(v) ? v.join(', ') : String(v)}
|
|
</span>
|
|
</div>
|
|
))}
|
|
</div>
|
|
<div className="mt-3 pt-2 border-t border-slate-50">
|
|
<div className="text-xs text-slate-400">
|
|
Chunk-Laenge: {getChunkText(currentChunk).length} Zeichen
|
|
{getChunkText(currentChunk).length < 50 && (
|
|
<span className="ml-2 text-orange-500 font-medium">⚠ Sehr kurz</span>
|
|
)}
|
|
{getChunkText(currentChunk).length > 2000 && (
|
|
<span className="ml-2 text-orange-500 font-medium">⚠ Sehr lang</span>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
{/* PDF-Viewer Panel */}
|
|
{splitViewActive && (
|
|
<div className="bg-white rounded-xl border border-slate-200 flex flex-col min-h-0 overflow-hidden">
|
|
<div className="flex-shrink-0 px-4 py-2 bg-slate-50 border-b border-slate-100 flex items-center justify-between">
|
|
<span className="text-sm font-medium text-slate-700">Original-PDF</span>
|
|
<div className="flex items-center gap-2">
|
|
<span className="text-xs text-slate-400">
|
|
Seite ~{pdfPage}
|
|
{pdfMapping?.totalPages ? ` / ${pdfMapping.totalPages}` : ''}
|
|
</span>
|
|
{pdfUrl && (
|
|
<a
|
|
href={pdfUrl.split('#')[0]}
|
|
target="_blank"
|
|
rel="noopener noreferrer"
|
|
className="text-xs text-teal-600 hover:text-teal-800 underline"
|
|
>
|
|
Oeffnen ↗
|
|
</a>
|
|
)}
|
|
</div>
|
|
</div>
|
|
<div className="flex-1 min-h-0 relative">
|
|
{pdfUrl && pdfExists ? (
|
|
<iframe
|
|
key={`${selectedRegulation}-${pdfPage}`}
|
|
src={pdfUrl}
|
|
className="absolute inset-0 w-full h-full border-0"
|
|
title="Original PDF"
|
|
/>
|
|
) : (
|
|
<div className="flex items-center justify-center h-full text-slate-400 text-sm p-4">
|
|
<div className="text-center space-y-2">
|
|
<div className="text-3xl">📄</div>
|
|
{!pdfMapping ? (
|
|
<>
|
|
<p>Kein PDF-Mapping fuer {selectedRegulation}.</p>
|
|
<p className="text-xs">rag-pdf-mapping.ts ergaenzen.</p>
|
|
</>
|
|
) : pdfExists === false ? (
|
|
<>
|
|
<p className="font-medium text-orange-600">PDF nicht vorhanden</p>
|
|
<p className="text-xs">Datei <code className="bg-slate-100 px-1 rounded">{pdfMapping.filename}</code> fehlt in ~/rag-originals/</p>
|
|
<p className="text-xs mt-1">Bitte manuell herunterladen und dort ablegen.</p>
|
|
</>
|
|
) : (
|
|
<p>PDF wird geprueft...</p>
|
|
)}
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
)
|
|
}
|