Upload PDFs, TXT, or Markdown files and chat with them using retrieval-augmented generation. Every AI answer includes verified source citations pointing back to specific document chunks — not just a chatbot wrapper.
┌─────────────┐ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ Upload │───►│ Chunk │───►│ BM25 │───►│ LLM │───►│ Cited │
│ PDF/TXT/MD │ │ 800 char │ │ Retrieval │ │ Generation │ │ Response │
│ │ │ w/ overlap │ │ top-K = 6 │ │ z-ai-sdk │ │ [Source N] │
└─────────────┘ └──────────────┘ └──────────────┘ └──────────────┘ └──────────────┘
- Upload & Extract — Drag-and-drop files. PDFs are parsed with
pdf-parse; TXT/MD read as UTF-8. - Chunk — Text is split into ~800-character chunks at sentence boundaries with 200-character overlap for context continuity.
- Retrieve (BM25) — When you ask a question, a BM25 engine scores all chunks against the query (TF-IDF with k1=1.5, b=0.75) and returns the top 6 most relevant.
- Generate — Retrieved chunks are injected into a structured prompt with citation instructions. The LLM generates an answer with inline
[Source N]references. - Cite — The response includes an expandable source panel showing each cited chunk's content, filename, and chunk number.
The LLM SDK available in this environment provides chat completions but not an embeddings endpoint. Rather than introducing an external vector database dependency, BM25 was implemented from scratch — it's the same algorithm powering Elasticsearch and Lucide, and it demonstrates deep understanding of information retrieval fundamentals.
| Feature | Details |
|---|---|
| Document Upload | Drag-and-drop or click; supports PDF, TXT, MD up to 20 MB |
| Smart Chunking | Sentence-boundary-aware; 800-char chunks with 200-char overlap |
| BM25 Retrieval | Industry-standard TF-IDF scoring (k1=1.5, b=0.75, top-K=6) |
| LLM Generation | Multi-turn conversation history via z-ai-web-dev-sdk |
| Source Citations | Inline [Source N] in answers + expandable panel with chunk content |
| Conversation Management | Create, switch between, and delete conversations; auto-titling |
| Document Management | Upload, list, delete; live stats (document count, chunk count) |
| Responsive Design | Mobile-first (375 px) through desktop (1920 px); collapsible sidebar |
| Markdown Rendering | AI responses rendered with react-markdown for rich formatting |
| Layer | Technology |
|---|---|
| Framework | Next.js 16 (App Router, Turbopack) |
| Language | TypeScript 5 |
| Styling | Tailwind CSS 4 + shadcn/ui (New York) |
| Database | SQLite via Prisma ORM |
| State | Zustand (client) |
| Retrieval | Custom BM25 engine (src/lib/bm25.ts) |
| Chunking | Custom sentence-boundary chunker (src/lib/chunker.ts) |
| PDF Parsing | pdf-parse |
| LLM | z-ai-web-dev-sdk (chat completions) |
| Markdown | react-markdown |
| Icons | Lucide React |
src/
├── app/
│ ├── layout.tsx # Root layout
│ ├── page.tsx # Main page (sidebar + chat)
│ └── api/
│ ├── chat/route.ts # RAG chat endpoint
│ ├── documents/
│ │ ├── upload/route.ts # File upload + text extraction + chunking
│ │ ├── list/route.ts # List all documents
│ │ └── delete/route.ts # Delete a document
│ └── conversations/
│ ├── create/route.ts # Create new conversation
│ ├── list/route.ts # List all conversations
│ ├── delete/route.ts # Delete a conversation
│ └── messages/route.ts # Get/set messages + linked documents
├── components/
│ ├── chat/
│ │ ├── document-upload.tsx # Drag-and-drop upload with file list
│ │ ├── conversation-list.tsx # Conversation sidebar panel
│ │ ├── chat-message.tsx # Message bubble with citations
│ │ └── empty-state.tsx # Contextual empty states
│ └── ui/ # shadcn/ui components
├── lib/
│ ├── bm25.ts # BM25 retrieval engine
│ ├── chunker.ts # Sentence-boundary text chunker
│ ├── db.ts # Prisma client singleton
│ └── utils.ts # Tailwind merge utility
└── store/
└── use-app-store.ts # Zustand global state
Document DocumentChunk Conversation
┌──────────┐ ┌──────────────┐ ┌──────────────┐
│ id │───┐ │ id │ │ id │
│ filename │ └──►│ documentId │ │ title │
│ fileType │ │ chunkIndex │ │ createdAt │
│ fileSize │ │ content │ │ updatedAt │
│ chunkCount│ │ startChar │ └──────┬───────┘
│ status │ │ endChar │ │
└──────────┘ └──────────────┘ │
│ ▲ │
└────────────────────┤ ┌────────┴────────┐
│ │ Conversation │
│ │ Document │
│ ├─────────────────┤
└──────────│ documentId │
│ conversationId │
└─────────────────┘
│
┌────────┴────────┐
│ Message │
├─────────────────┤
│ role (user/ai) │
│ content │
│ sources (JSON) │
│ conversationId │
└─────────────────┘
- Node.js 18+ or Bun
- A valid
z-ai-web-dev-sdkconfiguration
# Install dependencies
bun install
# Set up the database
bun run db:push
# Start the development server
bun run devThe app runs on http://localhost:3000.
| Command | Description |
|---|---|
bun run dev |
Start dev server with Turbopack |
bun run lint |
Run ESLint |
bun run db:push |
Push Prisma schema to SQLite |
bun run db:generate |
Regenerate Prisma Client |
| Method | Endpoint | Description |
|---|---|---|
POST |
/api/documents/upload |
Upload and process a document (multipart form) |
GET |
/api/documents/list |
List all documents |
POST |
/api/documents/delete |
Delete a document by ID |
POST |
/api/conversations/create |
Create a new conversation (link documents) |
GET |
/api/conversations/list |
List all conversations with document info |
POST |
/api/conversations/delete |
Delete a conversation by ID |
GET |
/api/conversations/messages?conversationId= |
Get messages for a conversation |
POST |
/api/conversations/messages |
Update linked documents for a conversation |
POST |
/api/chat |
Send a message (RAG: retrieve → generate → cite) |
- Upload documents — Drag PDF, TXT, or MD files onto the upload area in the sidebar.
- Start a chat — Click "New Chat" to create a conversation linked to all uploaded documents.
- Ask questions — Type natural language questions. The system retrieves relevant chunks via BM25, sends them to the LLM, and returns a cited answer.
- Inspect sources — Click "Source Citations" on any assistant message to see the exact chunks that informed the answer.
- BM25 over vector search — No external vector DB needed; BM25 is the gold standard for keyword-based retrieval and works well for structured documents.
- Sentence-boundary chunking — Chunks split at natural sentence endings (not mid-sentence) for better retrieval accuracy.
- Overlapping chunks — 200-character overlap ensures context isn't lost at chunk boundaries.
- SQLite + Prisma — Zero-config database; perfect for single-instance deployments and demos.
- Source citations as first-class citizens — Every answer shows where it came from, making the system auditable and trustworthy.
This project is for educational and portfolio demonstration purposes.
Have an idea for the next AI-powered project? Want to collaborate on something ambitious, or just want to bounce around concepts like this? I'm always open to connecting with builders, thinkers, and anyone who gets excited about turning ideas into working software.
Reach out — let's make something great together.