diff --git a/.plans/phase1_enhancement_plan.md b/.plans/phase1_enhancement_plan.md index 562a3fa..4d83e06 100644 --- a/.plans/phase1_enhancement_plan.md +++ b/.plans/phase1_enhancement_plan.md @@ -2,7 +2,7 @@ **Source**: User request (2026-04-23) **Scope**: Frontend navigation + RAG Database management page + page-aware chunking with chunk PDFs -**Status**: 🔄 In Progress — Feature 1 ✅ Complete, Feature 2 backend ✅ Complete, Feature 2 frontend & Feature 3 pending +**Status**: 🔄 In Progress — Features 1-2 ✅ Complete, Feature 3 pending --- @@ -205,12 +205,12 @@ class DeleteResponse(BaseModel): - [x] `GET /api/v1/documents` returns all documents with chunk counts - [x] `DELETE /api/v1/documents/{document_id}` removes all chunks from ChromaDB + associated chunk PDFs - [x] `DELETE /api/v1/chunks/{chunk_id}` removes a single chunk -- [ ] RAG Database page shows all documents with chunk counts -- [ ] User can expand a document to see its chunks -- [ ] User can delete a document (with confirmation) -- [ ] User can delete individual chunks (with confirmation) -- [ ] User can upload documents from this page -- [ ] Stats displayed: total documents, total chunks +- [x] RAG Database page shows all documents with chunk counts +- [x] User can expand a document to see its chunks +- [x] User can delete a document (with confirmation) +- [x] User can delete individual chunks (with confirmation) +- [x] User can upload documents from this page +- [x] Stats displayed: total documents, total chunks - [ ] Uploading a file with existing filename triggers automatic replacement (old data deleted first) --- @@ -531,7 +531,7 @@ Feature 3 (Page-Aware Chunking) ← Modifies ingestion pipeline |-----------|---------|-------|---------|----------|--------| | 1.5.1 | 1 | Nav bar + routing + page scaffold | None | NavBar, LTTPage, RAGDatabasePage, App.tsx refactor | ✅ Complete | | 1.5.2 | 2 | Backend CRUD for documents/chunks | documents router, RAGService methods, schemas | None | ✅ Complete | -| 1.5.3 | 2 | Frontend RAG Database page | None | RAGDatabasePage, DocumentList, ChunkList, DocumentUpload, API hooks | 📋 Pending | +| 1.5.3 | 2 | Frontend RAG Database page | None | RAGDatabasePage, DocumentList, ChunkList, DocumentUpload, API hooks | ✅ Complete | | 1.5.4 | 3 | Page-aware parsing & chunking | pdf_parser, chunking, metadata enhancements | None | 📋 Pending | | 1.5.5 | 3 | Chunk PDF generation & storage | pdf_extractor, config, ingest pipeline refactor | None | 📋 Pending | | 1.5.6 | 3 | Chunk file serving + frontend links | documents router endpoint | ResponsePanel clickable links, ChunkList updates | 📋 Pending | diff --git a/frontend/src/components/ChunkList.tsx b/frontend/src/components/ChunkList.tsx new file mode 100644 index 0000000..2951b1f --- /dev/null +++ b/frontend/src/components/ChunkList.tsx @@ -0,0 +1,80 @@ +import React from 'react' +import { Trash2 } from 'lucide-react' +import type { ChunkInfo } from '../types' + +interface ChunkListProps { + chunks: ChunkInfo[] + isLoading: boolean + onDeleteChunk: (chunkId: string) => void + isDeleting: boolean +} + +export const ChunkList: React.FC = ({ + chunks, + isLoading, + onDeleteChunk, + isDeleting, +}) => { + if (isLoading) { + return ( +
+
+
+
+
+ ) + } + + if (chunks.length === 0) { + return ( +
+ No chunks found +
+ ) + } + + return ( +
+ {chunks.map((chunk) => ( +
+
+
+ + Chunk {chunk.chunk_index} + + + Page: {chunk.page_number !== null ? chunk.page_number : 'N/A'} + +
+
+ {chunk.content_summary.length > 100 + ? `${chunk.content_summary.slice(0, 100)}...` + : chunk.content_summary} +
+
+ +
+ ))} +
+ ) +} diff --git a/frontend/src/components/DocumentList.tsx b/frontend/src/components/DocumentList.tsx new file mode 100644 index 0000000..f257c86 --- /dev/null +++ b/frontend/src/components/DocumentList.tsx @@ -0,0 +1,70 @@ +import React from 'react' +import { FileText, ChevronDown, ChevronRight, Trash2 } from 'lucide-react' +import type { DocumentInfo } from '../types' + +interface DocumentListProps { + documents: DocumentInfo[] + expandedId: string | null + onToggleExpand: (documentId: string) => void + onDelete: (documentId: string, filename: string) => void + isDeleting: boolean +} + +export const DocumentList: React.FC = ({ + documents, + expandedId, + onToggleExpand, + onDelete, + isDeleting, +}) => { + return ( +
+ {documents.map((doc) => ( +
+
+
+ +
+
{doc.filename}
+
+ {doc.chunk_count} chunks • Uploaded {doc.upload_date} +
+
+
+
+ + +
+
+
+ ))} +
+ ) +} diff --git a/frontend/src/components/DocumentUpload.tsx b/frontend/src/components/DocumentUpload.tsx new file mode 100644 index 0000000..686c782 --- /dev/null +++ b/frontend/src/components/DocumentUpload.tsx @@ -0,0 +1,71 @@ +import React from 'react' +import { Upload, Loader2, CheckCircle, AlertCircle } from 'lucide-react' + +interface DocumentUploadProps { + onUpload: (file: File) => void + isLoading: boolean + success: string | null + error: string | null +} + +export const DocumentUpload: React.FC = ({ + onUpload, + isLoading, + success, + error, +}) => { + const handleFileChange = (event: React.ChangeEvent) => { + const file = event.target.files?.[0] + if (file) { + onUpload(file) + } + } + + return ( +
+ + + + {success && ( +
+ + {success} +
+ )} + + {error && ( +
+ + {error} +
+ )} +
+ ) +} diff --git a/frontend/src/lib/api.ts b/frontend/src/lib/api.ts index 289fdc6..eefd00a 100644 --- a/frontend/src/lib/api.ts +++ b/frontend/src/lib/api.ts @@ -1,5 +1,5 @@ import axios from 'axios' -import type { QueryRequest, QueryResponse, IngestResponse } from '../types' +import type { QueryRequest, QueryResponse, IngestResponse, DocumentListResponse, ChunkInfo, DeleteResponse } from '../types' const BASE_URL: string = import.meta.env.VITE_API_BASE_URL ?? 'http://localhost:8000/api/v1' @@ -18,3 +18,23 @@ export const ingestDocument = async (file: File): Promise => { }) return resp.data } + +export const listDocuments = async (): Promise => { + const resp = await apiClient.get('/documents') + return resp.data +} + +export const listChunks = async (documentId: string): Promise => { + const resp = await apiClient.get(`/documents/${documentId}/chunks`) + return resp.data +} + +export const deleteDocument = async (documentId: string): Promise => { + const resp = await apiClient.delete(`/documents/${documentId}`) + return resp.data +} + +export const deleteChunk = async (chunkId: string): Promise => { + const resp = await apiClient.delete(`/chunks/${chunkId}`) + return resp.data +} diff --git a/frontend/src/lib/queries.tsx b/frontend/src/lib/queries.tsx index 780b649..6741d7e 100644 --- a/frontend/src/lib/queries.tsx +++ b/frontend/src/lib/queries.tsx @@ -1,7 +1,7 @@ import React from 'react' -import { QueryClient, QueryClientProvider, useMutation } from '@tanstack/react-query' -import { queryDocument, ingestDocument } from './api' -import type { QueryRequest, QueryResponse, IngestResponse } from '../types' +import { QueryClient, QueryClientProvider, useMutation, useQuery, useQueryClient } from '@tanstack/react-query' +import { queryDocument, ingestDocument, listDocuments, listChunks, deleteDocument, deleteChunk } from './api' +import type { QueryRequest, QueryResponse, IngestResponse, DocumentListResponse, ChunkInfo, DeleteResponse } from '../types' export const queryClient = new QueryClient() @@ -17,6 +17,41 @@ export const useIngestDocument = () => { }) } +export const useDocuments = () => { + return useQuery({ + queryKey: ['documents'], + queryFn: listDocuments, + }) +} + +export const useDocumentChunks = (documentId: string | null) => { + return useQuery({ + queryKey: ['documents', documentId, 'chunks'], + queryFn: () => listChunks(documentId!), + enabled: documentId !== null, + }) +} + +export const useDeleteDocument = () => { + const queryClient = useQueryClient() + return useMutation({ + mutationFn: deleteDocument, + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ['documents'] }) + }, + }) +} + +export const useDeleteChunk = () => { + const queryClient = useQueryClient() + return useMutation({ + mutationFn: deleteChunk, + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ['documents'] }) + }, + }) +} + export const AppQueryProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => { return {children} } diff --git a/frontend/src/pages/RAGDatabasePage.tsx b/frontend/src/pages/RAGDatabasePage.tsx index 2ab48d8..3bf6ee5 100644 --- a/frontend/src/pages/RAGDatabasePage.tsx +++ b/frontend/src/pages/RAGDatabasePage.tsx @@ -1,12 +1,144 @@ -import React from 'react' -import { Database } from 'lucide-react' +import React, { useState } from 'react' +import { Database, AlertCircle } from 'lucide-react' +import { useQueryClient } from '@tanstack/react-query' +import { useDocuments, useDocumentChunks, useDeleteDocument, useDeleteChunk, useIngestDocument } from '../lib/queries' +import { DocumentList } from '../components/DocumentList' +import { ChunkList } from '../components/ChunkList' +import { DocumentUpload } from '../components/DocumentUpload' export const RAGDatabasePage: React.FC = () => { + const [expandedId, setExpandedId] = useState(null) + const [uploadSuccess, setUploadSuccess] = useState(null) + const [uploadError, setUploadError] = useState(null) + + const { data: documentsData, isLoading: isLoadingDocuments, error: documentsError } = useDocuments() + const { data: chunks, isLoading: isLoadingChunks } = useDocumentChunks(expandedId) + const deleteDocumentMutation = useDeleteDocument() + const deleteChunkMutation = useDeleteChunk() + const ingestDocumentMutation = useIngestDocument() + const queryClient = useQueryClient() + + const handleToggleExpand = (documentId: string) => { + setExpandedId(expandedId === documentId ? null : documentId) + } + + const handleDeleteDocument = (documentId: string, filename: string) => { + const doc = documentsData?.documents.find((d) => d.document_id === documentId) + const chunkCount = doc?.chunk_count ?? 0 + if (window.confirm(`Delete "${filename}" and all its ${chunkCount} chunks?`)) { + deleteDocumentMutation.mutate(documentId) + } + } + + const handleDeleteChunk = (chunkId: string) => { + const chunk = chunks?.find((c) => c.chunk_id === chunkId) + const chunkIndex = chunk?.chunk_index ?? 0 + if (window.confirm(`Delete chunk ${chunkIndex}?`)) { + deleteChunkMutation.mutate(chunkId) + } + } + + const handleUpload = (file: File) => { + setUploadSuccess(null) + setUploadError(null) + ingestDocumentMutation.mutate(file, { + onSuccess: (data) => { + setUploadSuccess(`Uploaded ${data.filename}`) + queryClient.invalidateQueries({ queryKey: ['documents'] }) + setTimeout(() => setUploadSuccess(null), 3000) + }, + onError: (error) => { + setUploadError(error.message) + }, + }) + } + + if (isLoadingDocuments) { + return ( +
+
+
+
+
+
+
+
+ ) + } + + if (documentsError) { + return ( +
+
+
+ + Failed to load documents: {documentsError.message} +
+
+
+ ) + } + + const documents = documentsData?.documents ?? [] + const hasDocuments = documents.length > 0 + return ( -
-
- -
RAG Database Management — Coming Soon
+
+
+
+

RAG Database

+ +
+
+ + {hasDocuments && ( +
+
+ Total: {documentsData?.total_documents ?? 0} documents,{' '} + {documentsData?.total_chunks ?? 0} chunks +
+
+ )} + +
+ {!hasDocuments ? ( +
+
+ +
No documents uploaded yet
+
Upload a document to get started
+
+
+ ) : ( +
+ {documents.map((doc) => ( +
+ + {expandedId === doc.document_id && ( +
+ +
+ )} +
+ ))} +
+ )}
) diff --git a/frontend/src/types/index.ts b/frontend/src/types/index.ts index 162bf25..907a861 100644 --- a/frontend/src/types/index.ts +++ b/frontend/src/types/index.ts @@ -20,3 +20,29 @@ export interface IngestResponse { chunk_count: number filename: string } + +export interface DocumentInfo { + document_id: string + filename: string + chunk_count: number + upload_date: string +} + +export interface ChunkInfo { + chunk_id: string + chunk_index: number + content_summary: string + page_number: number | null + chunk_file_path: string | null +} + +export interface DocumentListResponse { + documents: DocumentInfo[] + total_documents: number + total_chunks: number +} + +export interface DeleteResponse { + deleted: boolean + message: string +}