Fix ChunkBrowserQA layout: proper height constraints, remove bottom nav duplication
- Root container uses calc(100vh - 220px) for fixed viewport height - All flex children use min-h-0 to enable proper overflow scrolling - Removed duplicate bottom nav buttons (Zurueck/Weiter) that appeared in the middle of the chunk text — navigation is only in the header now - Chunk text panel scrolls internally with fixed header - Added prominent article/section badges in header and panel header - Added chunk length quality indicator (warns on very short/long chunks) - Structural metadata keys (article, section, pages) sorted first - Sidebar shows regulation name instead of code for better readability - PDF viewer uses pages metadata from payload when available Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -137,7 +137,6 @@ export function ChunkBrowserQA({ apiProxy }: ChunkBrowserQAProps) {
|
|||||||
let offset: string | null = null
|
let offset: string | null = null
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Paginated scroll, 100 at a time
|
|
||||||
let safety = 0
|
let safety = 0
|
||||||
do {
|
do {
|
||||||
const params = new URLSearchParams({
|
const params = new URLSearchParams({
|
||||||
@@ -157,7 +156,7 @@ export function ChunkBrowserQA({ apiProxy }: ChunkBrowserQAProps) {
|
|||||||
allChunks.push(...chunks)
|
allChunks.push(...chunks)
|
||||||
offset = data.next_offset || null
|
offset = data.next_offset || null
|
||||||
safety++
|
safety++
|
||||||
} while (offset && safety < 200) // safety limit ~20k chunks
|
} while (offset && safety < 200)
|
||||||
|
|
||||||
// Sort by chunk_index
|
// Sort by chunk_index
|
||||||
allChunks.sort((a, b) => {
|
allChunks.sort((a, b) => {
|
||||||
@@ -186,14 +185,22 @@ export function ChunkBrowserQA({ apiProxy }: ChunkBrowserQAProps) {
|
|||||||
const prevChunk = docChunkIndex > 0 ? docChunks[docChunkIndex - 1] : null
|
const prevChunk = docChunkIndex > 0 ? docChunks[docChunkIndex - 1] : null
|
||||||
const nextChunk = docChunkIndex < docChunks.length - 1 ? docChunks[docChunkIndex + 1] : null
|
const nextChunk = docChunkIndex < docChunks.length - 1 ? docChunks[docChunkIndex + 1] : null
|
||||||
|
|
||||||
// PDF page estimation
|
// PDF page estimation — use pages metadata if available
|
||||||
const estimatePdfPage = (chunkIndex: number): number => {
|
const estimatePdfPage = (chunk: Record<string, unknown> | null, chunkIdx: number): number => {
|
||||||
|
if (chunk) {
|
||||||
|
// Try pages array from payload (e.g. [7] or [7,8])
|
||||||
|
const pages = chunk.pages as number[] | undefined
|
||||||
|
if (Array.isArray(pages) && pages.length > 0) return pages[0]
|
||||||
|
// Try page field
|
||||||
|
const page = chunk.page as number | undefined
|
||||||
|
if (typeof page === 'number' && page > 0) return page
|
||||||
|
}
|
||||||
const mapping = selectedRegulation ? RAG_PDF_MAPPING[selectedRegulation] : null
|
const mapping = selectedRegulation ? RAG_PDF_MAPPING[selectedRegulation] : null
|
||||||
const cpp = mapping?.chunksPerPage || chunksPerPage
|
const cpp = mapping?.chunksPerPage || chunksPerPage
|
||||||
return Math.floor(chunkIndex / cpp) + 1
|
return Math.floor(chunkIdx / cpp) + 1
|
||||||
}
|
}
|
||||||
|
|
||||||
const pdfPage = currentChunk ? estimatePdfPage(docChunkIndex) : 1
|
const pdfPage = estimatePdfPage(currentChunk, docChunkIndex)
|
||||||
const pdfMapping = selectedRegulation ? RAG_PDF_MAPPING[selectedRegulation] : null
|
const pdfMapping = selectedRegulation ? RAG_PDF_MAPPING[selectedRegulation] : null
|
||||||
const pdfUrl = pdfMapping ? `/rag-originals/${pdfMapping.filename}#page=${pdfPage}` : null
|
const pdfUrl = pdfMapping ? `/rag-originals/${pdfMapping.filename}#page=${pdfPage}` : null
|
||||||
|
|
||||||
@@ -249,7 +256,27 @@ export function ChunkBrowserQA({ apiProxy }: ChunkBrowserQAProps) {
|
|||||||
// Get text content from a chunk
|
// Get text content from a chunk
|
||||||
const getChunkText = (chunk: Record<string, unknown> | null): string => {
|
const getChunkText = (chunk: Record<string, unknown> | null): string => {
|
||||||
if (!chunk) return ''
|
if (!chunk) return ''
|
||||||
return String(chunk.text || chunk.content || chunk.chunk_text || '')
|
return String(chunk.chunk_text || chunk.text || chunk.content || '')
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract structural metadata for prominent display
|
||||||
|
const getStructuralInfo = (chunk: Record<string, unknown> | null): { article?: string; section?: string; pages?: string } => {
|
||||||
|
if (!chunk) return {}
|
||||||
|
const result: { article?: string; section?: string; pages?: string } = {}
|
||||||
|
// Article / paragraph
|
||||||
|
const article = chunk.article || chunk.artikel || chunk.paragraph || chunk.section_title
|
||||||
|
if (article) result.article = String(article)
|
||||||
|
// Section
|
||||||
|
const section = chunk.section || chunk.chapter || chunk.abschnitt || chunk.kapitel
|
||||||
|
if (section) result.section = String(section)
|
||||||
|
// Pages
|
||||||
|
const pages = chunk.pages as number[] | undefined
|
||||||
|
if (Array.isArray(pages) && pages.length > 0) {
|
||||||
|
result.pages = pages.length === 1 ? `S. ${pages[0]}` : `S. ${pages[0]}-${pages[pages.length - 1]}`
|
||||||
|
} else if (chunk.page) {
|
||||||
|
result.pages = `S. ${chunk.page}`
|
||||||
|
}
|
||||||
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
// Overlap extraction
|
// Overlap extraction
|
||||||
@@ -287,10 +314,21 @@ export function ChunkBrowserQA({ apiProxy }: ChunkBrowserQAProps) {
|
|||||||
return reg?.name || code
|
return reg?.name || code
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Important metadata keys to show prominently
|
||||||
|
const STRUCTURAL_KEYS = new Set([
|
||||||
|
'article', 'artikel', 'paragraph', 'section_title', 'section', 'chapter',
|
||||||
|
'abschnitt', 'kapitel', 'pages', 'page',
|
||||||
|
])
|
||||||
|
const HIDDEN_KEYS = new Set([
|
||||||
|
'text', 'content', 'chunk_text', 'id', 'embedding',
|
||||||
|
])
|
||||||
|
|
||||||
|
const structInfo = getStructuralInfo(currentChunk)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-4">
|
<div className="flex flex-col" style={{ height: 'calc(100vh - 220px)' }}>
|
||||||
{/* Header bar */}
|
{/* Header bar — fixed height */}
|
||||||
<div className="bg-white rounded-xl border border-slate-200 p-4">
|
<div className="flex-shrink-0 bg-white rounded-xl border border-slate-200 p-3 mb-3">
|
||||||
<div className="flex flex-wrap items-center gap-4">
|
<div className="flex flex-wrap items-center gap-4">
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-xs font-medium text-slate-500 mb-1">Collection</label>
|
<label className="block text-xs font-medium text-slate-500 mb-1">Collection</label>
|
||||||
@@ -309,27 +347,49 @@ export function ChunkBrowserQA({ apiProxy }: ChunkBrowserQAProps) {
|
|||||||
<>
|
<>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<span className="text-sm font-semibold text-slate-900">
|
<span className="text-sm font-semibold text-slate-900">
|
||||||
QA-Modus: {selectedRegulation} — {getRegName(selectedRegulation)}
|
{selectedRegulation} — {getRegName(selectedRegulation)}
|
||||||
</span>
|
</span>
|
||||||
|
{structInfo.article && (
|
||||||
|
<span className="px-2 py-0.5 bg-blue-100 text-blue-800 text-xs font-medium rounded">
|
||||||
|
{structInfo.article}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
{structInfo.pages && (
|
||||||
|
<span className="px-2 py-0.5 bg-slate-100 text-slate-600 text-xs rounded">
|
||||||
|
{structInfo.pages}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2 ml-auto">
|
<div className="flex items-center gap-2 ml-auto">
|
||||||
<span className="text-sm text-slate-600">
|
|
||||||
Chunk {docChunkIndex + 1} / {docTotalChunks}
|
|
||||||
</span>
|
|
||||||
<button
|
<button
|
||||||
onClick={handlePrev}
|
onClick={handlePrev}
|
||||||
disabled={docChunkIndex === 0}
|
disabled={docChunkIndex === 0}
|
||||||
className="px-3 py-1 text-sm border rounded-lg bg-white hover:bg-slate-50 disabled:opacity-30"
|
className="px-3 py-1.5 text-sm font-medium border rounded-lg bg-white hover:bg-slate-50 disabled:opacity-30 disabled:cursor-not-allowed"
|
||||||
>
|
>
|
||||||
◀ Prev
|
◀ Zurueck
|
||||||
</button>
|
</button>
|
||||||
|
<span className="text-sm font-mono text-slate-600 min-w-[80px] text-center">
|
||||||
|
{docChunkIndex + 1} / {docTotalChunks}
|
||||||
|
</span>
|
||||||
<button
|
<button
|
||||||
onClick={handleNext}
|
onClick={handleNext}
|
||||||
disabled={docChunkIndex >= docChunks.length - 1}
|
disabled={docChunkIndex >= docChunks.length - 1}
|
||||||
className="px-3 py-1 text-sm border rounded-lg bg-white hover:bg-slate-50 disabled:opacity-30"
|
className="px-3 py-1.5 text-sm font-medium border rounded-lg bg-white hover:bg-slate-50 disabled:opacity-30 disabled:cursor-not-allowed"
|
||||||
>
|
>
|
||||||
Next ▶
|
Weiter ▶
|
||||||
</button>
|
</button>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
min={1}
|
||||||
|
max={docTotalChunks}
|
||||||
|
value={docChunkIndex + 1}
|
||||||
|
onChange={(e) => {
|
||||||
|
const v = parseInt(e.target.value, 10)
|
||||||
|
if (!isNaN(v) && v >= 1 && v <= docTotalChunks) setDocChunkIndex(v - 1)
|
||||||
|
}}
|
||||||
|
className="w-16 px-2 py-1 border rounded text-xs text-center"
|
||||||
|
title="Springe zu Chunk Nr."
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<label className="text-xs text-slate-500">Chunks/Seite:</label>
|
<label className="text-xs text-slate-500">Chunks/Seite:</label>
|
||||||
@@ -356,11 +416,11 @@ export function ChunkBrowserQA({ apiProxy }: ChunkBrowserQAProps) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Main content: Sidebar + Content */}
|
{/* Main content: Sidebar + Content — fills remaining height */}
|
||||||
<div className="flex gap-4" style={{ minHeight: '70vh' }}>
|
<div className="flex gap-3 flex-1 min-h-0">
|
||||||
{/* Sidebar */}
|
{/* Sidebar — scrollable */}
|
||||||
<div className="w-64 flex-shrink-0 bg-white rounded-xl border border-slate-200 overflow-hidden flex flex-col">
|
<div className="w-56 flex-shrink-0 bg-white rounded-xl border border-slate-200 flex flex-col min-h-0">
|
||||||
<div className="p-3 border-b border-slate-100">
|
<div className="flex-shrink-0 p-3 border-b border-slate-100">
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
value={filterSearch}
|
value={filterSearch}
|
||||||
@@ -369,10 +429,10 @@ export function ChunkBrowserQA({ apiProxy }: ChunkBrowserQAProps) {
|
|||||||
className="w-full px-2 py-1.5 border rounded-lg text-sm focus:ring-2 focus:ring-teal-500"
|
className="w-full px-2 py-1.5 border rounded-lg text-sm focus:ring-2 focus:ring-teal-500"
|
||||||
/>
|
/>
|
||||||
{countsLoading && (
|
{countsLoading && (
|
||||||
<div className="text-xs text-slate-400 mt-1">Counts werden geladen...</div>
|
<div className="text-xs text-slate-400 mt-1 animate-pulse">Counts laden...</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex-1 overflow-y-auto">
|
<div className="flex-1 overflow-y-auto min-h-0">
|
||||||
{GROUP_ORDER.map(group => {
|
{GROUP_ORDER.map(group => {
|
||||||
const items = filteredRegulations[group]
|
const items = filteredRegulations[group]
|
||||||
if (items.length === 0) return null
|
if (items.length === 0) return null
|
||||||
@@ -381,13 +441,13 @@ export function ChunkBrowserQA({ apiProxy }: ChunkBrowserQAProps) {
|
|||||||
<div key={group}>
|
<div key={group}>
|
||||||
<button
|
<button
|
||||||
onClick={() => toggleGroup(group)}
|
onClick={() => toggleGroup(group)}
|
||||||
className="w-full px-3 py-1.5 text-left text-xs font-semibold text-slate-500 bg-slate-50 hover:bg-slate-100 flex items-center justify-between"
|
className="w-full px-3 py-1.5 text-left text-xs font-semibold text-slate-500 bg-slate-50 hover:bg-slate-100 flex items-center justify-between sticky top-0 z-10"
|
||||||
>
|
>
|
||||||
<span>{GROUP_LABELS[group]}</span>
|
<span>{GROUP_LABELS[group]}</span>
|
||||||
<span className="text-slate-400">{isCollapsed ? '+' : '-'}</span>
|
<span className="text-slate-400">{isCollapsed ? '+' : '-'}</span>
|
||||||
</button>
|
</button>
|
||||||
{!isCollapsed && items.map(reg => {
|
{!isCollapsed && items.map(reg => {
|
||||||
const count = regulationCounts[reg.code] ?? REGULATIONS_IN_RAG[reg.code]?.chunks ?? 0
|
const count = regulationCounts[reg.code] ?? 0
|
||||||
const isSelected = selectedRegulation === reg.code
|
const isSelected = selectedRegulation === reg.code
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
@@ -397,8 +457,8 @@ export function ChunkBrowserQA({ apiProxy }: ChunkBrowserQAProps) {
|
|||||||
isSelected ? 'bg-teal-100 text-teal-900 font-medium' : 'text-slate-700'
|
isSelected ? 'bg-teal-100 text-teal-900 font-medium' : 'text-slate-700'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<span className="truncate">{reg.code}</span>
|
<span className="truncate text-xs">{reg.name || reg.code}</span>
|
||||||
<span className={`text-xs tabular-nums ${count > 0 ? 'text-slate-500' : 'text-slate-300'}`}>
|
<span className={`text-xs tabular-nums flex-shrink-0 ml-1 ${count > 0 ? 'text-slate-500' : 'text-slate-300'}`}>
|
||||||
{count > 0 ? count.toLocaleString() : '—'}
|
{count > 0 ? count.toLocaleString() : '—'}
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
@@ -410,13 +470,13 @@ export function ChunkBrowserQA({ apiProxy }: ChunkBrowserQAProps) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Content area */}
|
{/* Content area — fills remaining width and height */}
|
||||||
{!selectedRegulation ? (
|
{!selectedRegulation ? (
|
||||||
<div className="flex-1 flex items-center justify-center bg-white rounded-xl border border-slate-200">
|
<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-center text-slate-400 space-y-2">
|
||||||
<div className="text-4xl">🔍</div>
|
<div className="text-4xl">🔍</div>
|
||||||
<p className="text-sm">Waehle ein Dokument in der Sidebar, um die QA-Ansicht zu starten.</p>
|
<p className="text-sm">Dokument in der Sidebar auswaehlen, um QA zu starten.</p>
|
||||||
<p className="text-xs text-slate-300">Pfeiltasten navigieren zwischen Chunks.</p>
|
<p className="text-xs text-slate-300">Pfeiltasten: Chunk vor/zurueck</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : docLoading ? (
|
) : docLoading ? (
|
||||||
@@ -425,40 +485,57 @@ export function ChunkBrowserQA({ apiProxy }: ChunkBrowserQAProps) {
|
|||||||
<div className="animate-spin text-3xl">⚙</div>
|
<div className="animate-spin text-3xl">⚙</div>
|
||||||
<p className="text-sm">Chunks werden geladen...</p>
|
<p className="text-sm">Chunks werden geladen...</p>
|
||||||
<p className="text-xs text-slate-400">
|
<p className="text-xs text-slate-400">
|
||||||
{selectedRegulation}: {REGULATIONS_IN_RAG[selectedRegulation]?.chunks || '?'} Chunks erwartet
|
{selectedRegulation}: {REGULATIONS_IN_RAG[selectedRegulation]?.chunks.toLocaleString() || '?'} Chunks erwartet
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className={`flex-1 grid gap-4 ${splitViewActive ? 'grid-cols-2' : 'grid-cols-1'}`}>
|
<div className={`flex-1 grid gap-3 min-h-0 ${splitViewActive ? 'grid-cols-2' : 'grid-cols-1'}`}>
|
||||||
{/* Chunk-Text Panel */}
|
{/* Chunk-Text Panel — fixed height, internal scroll */}
|
||||||
<div className="bg-white rounded-xl border border-slate-200 overflow-hidden flex flex-col">
|
<div className="bg-white rounded-xl border border-slate-200 flex flex-col min-h-0 overflow-hidden">
|
||||||
<div className="px-4 py-2 bg-slate-50 border-b border-slate-100 flex items-center justify-between">
|
{/* Panel header */}
|
||||||
|
<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>
|
<span className="text-sm font-medium text-slate-700">Chunk-Text</span>
|
||||||
<span className="text-xs text-slate-400">
|
<div className="flex items-center gap-2">
|
||||||
Index: {docChunkIndex} / {docTotalChunks - 1}
|
{structInfo.article && (
|
||||||
</span>
|
<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>
|
||||||
<div className="flex-1 overflow-y-auto p-4 space-y-3">
|
|
||||||
|
{/* Scrollable content */}
|
||||||
|
<div className="flex-1 overflow-y-auto min-h-0 p-4 space-y-3">
|
||||||
{/* Overlap from previous chunk */}
|
{/* Overlap from previous chunk */}
|
||||||
{prevChunk && (
|
{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="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">↑ Overlap (vorheriger Chunk #{docChunkIndex - 1})</div>
|
<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>
|
<p className="whitespace-pre-wrap break-words leading-relaxed">{getOverlapPrev()}</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Current chunk text */}
|
{/* Current chunk text */}
|
||||||
{currentChunk && (
|
{currentChunk ? (
|
||||||
<div className="text-sm text-slate-800 whitespace-pre-wrap break-words leading-relaxed border-l-2 border-teal-400 pl-3">
|
<div className="text-sm text-slate-800 whitespace-pre-wrap break-words leading-relaxed border-l-2 border-teal-400 pl-3">
|
||||||
{getChunkText(currentChunk)}
|
{getChunkText(currentChunk)}
|
||||||
</div>
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="text-sm text-slate-400 italic">Kein Chunk-Text vorhanden.</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Overlap from next chunk */}
|
{/* Overlap from next chunk */}
|
||||||
{nextChunk && (
|
{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="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">↓ Overlap (naechster Chunk #{docChunkIndex + 1})</div>
|
<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>
|
<p className="whitespace-pre-wrap break-words leading-relaxed">{getOverlapNext()}</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -469,62 +546,62 @@ export function ChunkBrowserQA({ apiProxy }: ChunkBrowserQAProps) {
|
|||||||
<div className="text-xs font-medium text-slate-500 mb-2">Metadaten</div>
|
<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">
|
<div className="grid grid-cols-2 gap-x-4 gap-y-1 text-xs">
|
||||||
{Object.entries(currentChunk)
|
{Object.entries(currentChunk)
|
||||||
.filter(([k]) => !['text', 'content', 'chunk_text', 'id'].includes(k))
|
.filter(([k]) => !HIDDEN_KEYS.has(k))
|
||||||
|
.sort(([a], [b]) => {
|
||||||
|
// Structural keys first
|
||||||
|
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]) => (
|
.map(([k, v]) => (
|
||||||
<div key={k} className="flex gap-1">
|
<div key={k} className={`flex gap-1 ${STRUCTURAL_KEYS.has(k) ? 'col-span-2 font-medium' : ''}`}>
|
||||||
<span className="font-medium text-slate-500">{k}:</span>
|
<span className="font-medium text-slate-500 flex-shrink-0">{k}:</span>
|
||||||
<span className="text-slate-700 truncate">{String(v)}</span>
|
<span className="text-slate-700 break-all">
|
||||||
|
{Array.isArray(v) ? v.join(', ') : String(v)}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
{/* Chunk quality indicator */}
|
||||||
|
<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>
|
</div>
|
||||||
|
|
||||||
{/* Bottom nav */}
|
|
||||||
<div className="px-4 py-2 border-t border-slate-100 bg-slate-50 flex items-center justify-between">
|
|
||||||
<button
|
|
||||||
onClick={handlePrev}
|
|
||||||
disabled={docChunkIndex === 0}
|
|
||||||
className="px-3 py-1 text-xs border rounded bg-white hover:bg-slate-50 disabled:opacity-30"
|
|
||||||
>
|
|
||||||
◀ Zurueck
|
|
||||||
</button>
|
|
||||||
<div className="flex items-center gap-3">
|
|
||||||
<input
|
|
||||||
type="number"
|
|
||||||
min={0}
|
|
||||||
max={docTotalChunks - 1}
|
|
||||||
value={docChunkIndex}
|
|
||||||
onChange={(e) => {
|
|
||||||
const v = parseInt(e.target.value, 10)
|
|
||||||
if (!isNaN(v) && v >= 0 && v < docTotalChunks) setDocChunkIndex(v)
|
|
||||||
}}
|
|
||||||
className="w-20 px-2 py-1 border rounded text-xs text-center"
|
|
||||||
/>
|
|
||||||
<span className="text-xs text-slate-400">/ {docTotalChunks - 1}</span>
|
|
||||||
</div>
|
|
||||||
<button
|
|
||||||
onClick={handleNext}
|
|
||||||
disabled={docChunkIndex >= docChunks.length - 1}
|
|
||||||
className="px-3 py-1 text-xs border rounded bg-white hover:bg-slate-50 disabled:opacity-30"
|
|
||||||
>
|
|
||||||
Weiter ▶
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* PDF-Viewer Panel */}
|
{/* PDF-Viewer Panel */}
|
||||||
{splitViewActive && (
|
{splitViewActive && (
|
||||||
<div className="bg-white rounded-xl border border-slate-200 overflow-hidden flex flex-col">
|
<div className="bg-white rounded-xl border border-slate-200 flex flex-col min-h-0 overflow-hidden">
|
||||||
<div className="px-4 py-2 bg-slate-50 border-b border-slate-100 flex items-center justify-between">
|
<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>
|
<span className="text-sm font-medium text-slate-700">Original-PDF</span>
|
||||||
<span className="text-xs text-slate-400">
|
<div className="flex items-center gap-2">
|
||||||
Seite ~{pdfPage}
|
<span className="text-xs text-slate-400">
|
||||||
{pdfMapping?.totalPages ? ` / ${pdfMapping.totalPages}` : ''}
|
Seite ~{pdfPage}
|
||||||
</span>
|
{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>
|
||||||
<div className="flex-1 relative">
|
<div className="flex-1 min-h-0 relative">
|
||||||
{pdfUrl ? (
|
{pdfUrl ? (
|
||||||
<iframe
|
<iframe
|
||||||
key={`${selectedRegulation}-${pdfPage}`}
|
key={`${selectedRegulation}-${pdfPage}`}
|
||||||
@@ -533,30 +610,15 @@ export function ChunkBrowserQA({ apiProxy }: ChunkBrowserQAProps) {
|
|||||||
title="Original PDF"
|
title="Original PDF"
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<div className="flex items-center justify-center h-full text-slate-400 text-sm">
|
<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-center space-y-2">
|
||||||
<div className="text-3xl">📄</div>
|
<div className="text-3xl">📄</div>
|
||||||
<p>Kein PDF-Mapping fuer {selectedRegulation}.</p>
|
<p>Kein PDF-Mapping fuer {selectedRegulation}.</p>
|
||||||
<p className="text-xs">Bitte rag-pdf-mapping.ts ergaenzen und PDF in ~/rag-originals/ ablegen.</p>
|
<p className="text-xs">rag-pdf-mapping.ts ergaenzen und PDF in ~/rag-originals/ ablegen.</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
{pdfUrl && (
|
|
||||||
<div className="px-4 py-2 border-t border-slate-100 bg-slate-50 flex items-center justify-between">
|
|
||||||
<span className="text-xs text-slate-500">
|
|
||||||
{pdfMapping?.filename}
|
|
||||||
</span>
|
|
||||||
<a
|
|
||||||
href={pdfUrl.split('#')[0]}
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
className="text-xs text-teal-600 hover:text-teal-800"
|
|
||||||
>
|
|
||||||
PDF oeffnen ↗
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user