feat: add Chunk-Browser tab to RAG page

- New 'Chunk-Browser' tab for sequential chunk browsing
- Qdrant scroll API proxy (scroll + collection-count actions)
- Pagination with prev/next through all chunks in a collection
- Text search filter with highlighting
- Click to expand chunk and see all metadata
- 'In Chunks suchen' button now navigates to Chunk-Browser with correct collection

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Benjamin Admin
2026-02-28 09:35:52 +01:00
parent 954103cdf2
commit 491df4e1b0
2 changed files with 317 additions and 3 deletions

View File

@@ -66,6 +66,72 @@ export async function GET(request: NextRequest) {
url += `/traceability?chunk_id=${encodeURIComponent(chunkId || '')}&regulation=${encodeURIComponent(regulation || '')}`
break
}
case 'scroll': {
const collection = searchParams.get('collection') || 'bp_compliance_gesetze'
const limit = parseInt(searchParams.get('limit') || '20', 10)
const offsetParam = searchParams.get('offset')
const filterKey = searchParams.get('filter_key')
const filterValue = searchParams.get('filter_value')
const textSearch = searchParams.get('text_search')
const scrollBody: Record<string, unknown> = {
limit: Math.min(limit, 100),
with_payload: true,
with_vector: false,
}
if (offsetParam) {
scrollBody.offset = offsetParam
}
if (filterKey && filterValue) {
scrollBody.filter = {
must: [{ key: filterKey, match: { value: filterValue } }],
}
}
const scrollRes = await fetch(`${QDRANT_URL}/collections/${encodeURIComponent(collection)}/points/scroll`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(scrollBody),
cache: 'no-store',
})
if (!scrollRes.ok) {
return NextResponse.json({ error: 'Qdrant scroll failed' }, { status: scrollRes.status })
}
const scrollData = await scrollRes.json()
const points = (scrollData.result?.points || []).map((p: { id: string; payload?: Record<string, unknown> }) => ({
id: p.id,
...p.payload,
}))
// Client-side text search filter
let filtered = points
if (textSearch && textSearch.trim()) {
const term = textSearch.toLowerCase()
filtered = points.filter((p: Record<string, unknown>) => {
const text = String(p.text || p.content || p.chunk_text || '')
return text.toLowerCase().includes(term)
})
}
return NextResponse.json({
chunks: filtered,
next_offset: scrollData.result?.next_page_offset || null,
total_in_page: points.length,
})
}
case 'collection-count': {
const col = searchParams.get('collection') || 'bp_compliance_gesetze'
const countRes = await fetch(`${QDRANT_URL}/collections/${encodeURIComponent(col)}`, {
cache: 'no-store',
})
if (!countRes.ok) {
return NextResponse.json({ count: 0 })
}
const countData = await countRes.json()
return NextResponse.json({
count: countData.result?.points_count || 0,
})
}
default:
return NextResponse.json({ error: 'Unknown action' }, { status: 400 })
}