'use client' import React, { useState, useEffect, useCallback, useRef, useMemo } from 'react' import { RAG_PDF_MAPPING } from './rag-pdf-mapping' import { REGULATIONS_IN_RAG, REGULATION_INFO } from '../rag-constants' import { RegGroupKey } from './ChunkBrowserConstants' import { getStructuralInfo } from './ChunkBrowserHelpers' import { ChunkBrowserSidebar } from './ChunkBrowserSidebar' import { ChunkBrowserToolbar } from './ChunkBrowserToolbar' import { ChunkBrowserContent } from './ChunkBrowserContent' interface ChunkBrowserQAProps { apiProxy: string } export function ChunkBrowserQA({ apiProxy }: ChunkBrowserQAProps) { // Filter-Sidebar const [selectedRegulation, setSelectedRegulation] = useState(null) const [regulationCounts, setRegulationCounts] = useState>({}) const [filterSearch, setFilterSearch] = useState('') const [countsLoading, setCountsLoading] = useState(false) // Dokument-Chunks (sequenziell) const [docChunks, setDocChunks] = useState[]>([]) const [docChunkIndex, setDocChunkIndex] = useState(0) const [docTotalChunks, setDocTotalChunks] = useState(0) const [docLoading, setDocLoading] = useState(false) const docChunksRef = useRef(docChunks) docChunksRef.current = docChunks // Split-View const [splitViewActive, setSplitViewActive] = useState(true) const [chunksPerPage, setChunksPerPage] = useState(6) const [fullscreen, setFullscreen] = useState(false) // Collection const [collection, setCollection] = useState('bp_compliance_ce') // PDF existence check const [pdfExists, setPdfExists] = useState(null) // Sidebar collapsed groups const [collapsedGroups, setCollapsedGroups] = useState>(new Set()) // Build grouped regulations for sidebar const regulationsInCollection = Object.entries(REGULATIONS_IN_RAG) .filter(([, info]) => info.collection === collection) .map(([code]) => code) const groupedRegulations = useMemo(() => { const groups: Record = { eu_regulation: [], eu_directive: [], de_law: [], at_law: [], ch_law: [], national_law: [], bsi_standard: [], eu_guideline: [], international_standard: [], other: [], } for (const code of regulationsInCollection) { const reg = REGULATION_INFO.find(r => r.code === code) const type = (reg?.type || 'other') as RegGroupKey const groupKey = type in groups ? type : 'other' groups[groupKey].push({ code, name: reg?.name || code, type: reg?.type || 'unknown' }) } return groups }, [regulationsInCollection.join(',')]) // Load regulation counts for current collection const loadRegulationCounts = useCallback(async (col: string) => { const entries = Object.entries(REGULATIONS_IN_RAG) .filter(([, info]) => info.collection === col && info.qdrant_id) if (entries.length === 0) return const qdrantIdToCode: Record = {} for (const [code, info] of entries) { if (!qdrantIdToCode[info.qdrant_id]) qdrantIdToCode[info.qdrant_id] = [] qdrantIdToCode[info.qdrant_id].push(code) } const uniqueQdrantIds = Object.keys(qdrantIdToCode) setCountsLoading(true) try { const params = new URLSearchParams({ action: 'regulation-counts-batch', collection: col, qdrant_ids: uniqueQdrantIds.join(','), }) const res = await fetch(`${apiProxy}?${params}`) if (res.ok) { const data = await res.json() const mapped: Record = {} for (const [qid, count] of Object.entries(data.counts as Record)) { const codes = qdrantIdToCode[qid] || [] for (const code of codes) { mapped[code] = count } } setRegulationCounts(prev => ({ ...prev, ...mapped })) } } catch (error) { console.error('Failed to load regulation counts:', error) } finally { setCountsLoading(false) } }, [apiProxy]) // Load all chunks for a regulation (paginated scroll) const loadDocumentChunks = useCallback(async (regulationCode: string) => { const ragInfo = REGULATIONS_IN_RAG[regulationCode] if (!ragInfo || !ragInfo.qdrant_id) return setDocLoading(true) setDocChunks([]) setDocChunkIndex(0) setDocTotalChunks(0) const allChunks: Record[] = [] let offset: string | null = null try { let safety = 0 do { const params = new URLSearchParams({ action: 'scroll', collection: ragInfo.collection, limit: '100', filter_key: 'regulation_id', filter_value: ragInfo.qdrant_id, }) if (offset) params.append('offset', offset) const res = await fetch(`${apiProxy}?${params}`) if (!res.ok) break const data = await res.json() const chunks = data.chunks || [] allChunks.push(...chunks) offset = data.next_offset || null safety++ } while (offset && safety < 200) allChunks.sort((a, b) => { const ai = Number(a.chunk_index ?? a.chunk_id ?? 0) const bi = Number(b.chunk_index ?? b.chunk_id ?? 0) return ai - bi }) setDocChunks(allChunks) setDocTotalChunks(allChunks.length) setDocChunkIndex(0) } catch (error) { console.error('Failed to load document chunks:', error) } finally { setDocLoading(false) } }, [apiProxy]) // Initial load useEffect(() => { loadRegulationCounts(collection) }, [collection, loadRegulationCounts]) // Check PDF existence when regulation changes useEffect(() => { if (!selectedRegulation) { setPdfExists(null); return } const mapping = RAG_PDF_MAPPING[selectedRegulation] if (!mapping) { setPdfExists(false); return } const url = `/rag-originals/${mapping.filename}` fetch(url, { method: 'HEAD' }) .then(res => setPdfExists(res.ok)) .catch(() => setPdfExists(false)) }, [selectedRegulation]) // Keyboard navigation const handleKeyDown = useCallback((e: KeyboardEvent) => { if (e.key === 'Escape' && fullscreen) { e.preventDefault() setFullscreen(false) } else if (e.key === 'ArrowLeft' || e.key === 'ArrowUp') { e.preventDefault() setDocChunkIndex(i => Math.max(0, i - 1)) } else if (e.key === 'ArrowRight' || e.key === 'ArrowDown') { e.preventDefault() setDocChunkIndex(i => Math.min(docChunksRef.current.length - 1, i + 1)) } }, [fullscreen]) useEffect(() => { if (fullscreen || (selectedRegulation && docChunks.length > 0)) { window.addEventListener('keydown', handleKeyDown) return () => window.removeEventListener('keydown', handleKeyDown) } }, [selectedRegulation, docChunks.length, handleKeyDown, fullscreen]) // Handlers const handleSelectRegulation = (code: string) => { setSelectedRegulation(code) loadDocumentChunks(code) } const handleCollectionChange = (col: string) => { setCollection(col) setSelectedRegulation(null) setDocChunks([]) setDocChunkIndex(0) setDocTotalChunks(0) setRegulationCounts({}) } const toggleGroup = (group: string) => { setCollapsedGroups(prev => { const next = new Set(prev) if (next.has(group)) next.delete(group) else next.add(group) return next }) } // Filter sidebar items const filteredRegulations = useMemo(() => { if (!filterSearch.trim()) return groupedRegulations const term = filterSearch.toLowerCase() const filtered: typeof groupedRegulations = { eu_regulation: [], eu_directive: [], de_law: [], at_law: [], ch_law: [], national_law: [], bsi_standard: [], eu_guideline: [], international_standard: [], other: [], } for (const [group, items] of Object.entries(groupedRegulations)) { filtered[group as RegGroupKey] = items.filter( r => r.code.toLowerCase().includes(term) || r.name.toLowerCase().includes(term) ) } return filtered }, [groupedRegulations, filterSearch]) const currentChunk = docChunks[docChunkIndex] || null const structInfo = getStructuralInfo(currentChunk) return (
{ if (docChunkIndex > 0) setDocChunkIndex(i => i - 1) }} onNext={() => { if (docChunkIndex < docChunks.length - 1) setDocChunkIndex(i => i + 1) }} onJumpTo={setDocChunkIndex} />
) }