sbv Claude commited on
Commit
185e35f
Β·
0 Parent(s):

Initial commit: IntelliDoc AI - RAG document analysis platform

Browse files

Built a complete RAG webapp with professional UI and enterprise features:

Frontend:
- Modern landing page with IntelliDoc AI branding
- Professional marketing sections (features, how it works)
- Clean upload interface with file validation
- Modal chat overlay for Q&A
- Responsive design with smooth animations
- Vanilla HTML/CSS/JS (no framework dependencies)

Backend:
- FastAPI server with CORS support
- Docling integration for multi-format document processing (PDF, DOCX, PPTX, XLSX, HTML)
- ChromaDB vector store with sentence-transformers embeddings (local, no API costs)
- Google Gemini 2.0 Flash for answer generation
- Session-based architecture with 20-page document limit
- Simple RAG pipeline: chunk β†’ embed β†’ search β†’ generate

Deployment:
- Docker containerization with docker-compose
- Deployment configs for Render, Railway, Fly.io
- Automated run scripts for Mac/Linux/Windows
- Port 8001 (avoiding conflicts with existing containers)

Key Features:
- Privacy-focused (no data storage, session-based)
- Fast processing (<30s for most documents)
- Source citation in answers
- Free embeddings (sentence-transformers)
- Pay-per-question with Gemini API

πŸ€– Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

Files changed (13) hide show
  1. .gitignore +78 -0
  2. Dockerfile +28 -0
  3. QUICKSTART.md +103 -0
  4. README.md +315 -0
  5. backend/main.py +321 -0
  6. docker-compose.yml +13 -0
  7. frontend/index.html +335 -0
  8. frontend/script.js +319 -0
  9. frontend/style.css +898 -0
  10. render.yaml +9 -0
  11. requirements.txt +10 -0
  12. run.bat +32 -0
  13. run.sh +31 -0
.gitignore ADDED
@@ -0,0 +1,78 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Python
2
+ __pycache__/
3
+ *.py[cod]
4
+ *$py.class
5
+ *.so
6
+ .Python
7
+ build/
8
+ develop-eggs/
9
+ dist/
10
+ downloads/
11
+ eggs/
12
+ .eggs/
13
+ lib/
14
+ lib64/
15
+ parts/
16
+ sdist/
17
+ var/
18
+ wheels/
19
+ pip-wheel-metadata/
20
+ share/python-wheels/
21
+ *.egg-info/
22
+ .installed.cfg
23
+ *.egg
24
+ MANIFEST
25
+
26
+ # Virtual Environment
27
+ venv/
28
+ env/
29
+ ENV/
30
+ .venv/
31
+
32
+ # IDE
33
+ .vscode/
34
+ .idea/
35
+ *.swp
36
+ *.swo
37
+ *.swn
38
+ .DS_Store
39
+
40
+ # Logs
41
+ *.log
42
+ logs/
43
+
44
+ # Data files
45
+ *.pdf
46
+ *.docx
47
+ *.pptx
48
+ *.xlsx
49
+ uploaded_files/
50
+ chroma_db/
51
+ .chroma/
52
+
53
+ # Environment variables
54
+ .env
55
+ .env.local
56
+ .env.*.local
57
+
58
+ # Testing
59
+ .pytest_cache/
60
+ .coverage
61
+ htmlcov/
62
+
63
+ # OS
64
+ .DS_Store
65
+ .DS_Store?
66
+ ._*
67
+ .Spotlight-V100
68
+ .Trashes
69
+ ehthumbs.db
70
+ Thumbs.db
71
+
72
+ # Docker
73
+ .dockerignore
74
+
75
+ # Temporary files
76
+ tmp/
77
+ temp/
78
+ *.tmp
Dockerfile ADDED
@@ -0,0 +1,28 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM python:3.11-slim
2
+
3
+ # Set working directory
4
+ WORKDIR /app
5
+
6
+ # Install system dependencies
7
+ RUN apt-get update && apt-get install -y \
8
+ gcc \
9
+ g++ \
10
+ && rm -rf /var/lib/apt/lists/*
11
+
12
+ # Copy requirements and install Python dependencies
13
+ COPY requirements.txt .
14
+ RUN pip install --no-cache-dir -r requirements.txt
15
+
16
+ # Pre-download embedding model
17
+ RUN python -c "from sentence_transformers import SentenceTransformer; SentenceTransformer('sentence-transformers/all-MiniLM-L6-v2')"
18
+
19
+ # Copy application code
20
+ COPY backend/ backend/
21
+ COPY frontend/ frontend/
22
+
23
+ # Expose port
24
+ EXPOSE 8001
25
+
26
+ # Run the application
27
+ WORKDIR /app/backend
28
+ CMD ["sh", "-c", "uvicorn main:app --host 0.0.0.0 --port ${PORT:-8001}"]
QUICKSTART.md ADDED
@@ -0,0 +1,103 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # πŸš€ Quick Start Guide
2
+
3
+ Get up and running in 2 minutes!
4
+
5
+ ## Prerequisites
6
+
7
+ 1. **Python 3.9+** installed
8
+ 2. **Google Gemini API Key** - Get one free at [Google AI Studio](https://makersuite.google.com/app/apikey)
9
+
10
+ ## Option 1: Easy Start (Recommended)
11
+
12
+ ### Mac/Linux
13
+ ```bash
14
+ ./run.sh
15
+ ```
16
+
17
+ ### Windows
18
+ ```bash
19
+ run.bat
20
+ ```
21
+
22
+ That's it! Open http://localhost:8000 in your browser.
23
+
24
+ ## Option 2: Manual Start
25
+
26
+ ```bash
27
+ # Create virtual environment
28
+ python3 -m venv venv
29
+
30
+ # Activate it
31
+ source venv/bin/activate # Mac/Linux
32
+ # OR
33
+ venv\Scripts\activate # Windows
34
+
35
+ # Install dependencies
36
+ pip install -r requirements.txt
37
+
38
+ # Run server
39
+ cd backend
40
+ python main.py
41
+ ```
42
+
43
+ Open http://localhost:8000
44
+
45
+ ## Option 3: Docker
46
+
47
+ ```bash
48
+ # Build and run
49
+ docker-compose up --build
50
+
51
+ # Or just:
52
+ docker build -t docling-rag .
53
+ docker run -p 8000:8000 docling-rag
54
+ ```
55
+
56
+ Open http://localhost:8000
57
+
58
+ ## Using the App
59
+
60
+ 1. **Enter API Key**: Paste your Google Gemini API key
61
+ 2. **Upload Document**: Select a PDF, DOCX, PPTX, XLSX, or HTML file (max 20 pages)
62
+ 3. **Click "Upload & Process"**: Wait for processing to complete
63
+ 4. **Ask Questions**: Type your question and click "Send"
64
+ 5. **View Answers**: Get AI-generated answers with source citations
65
+
66
+ ## First Time Setup Notes
67
+
68
+ - **First run** will download the embedding model (~90MB) - this is normal
69
+ - Subsequent runs will be much faster
70
+ - The model is cached locally at `~/.cache/torch/sentence_transformers/`
71
+
72
+ ## Troubleshooting
73
+
74
+ ### Port already in use
75
+ ```bash
76
+ # Kill process on port 8000
77
+ lsof -ti:8000 | xargs kill -9 # Mac/Linux
78
+ ```
79
+
80
+ ### API key not working
81
+ - Make sure you're using a **Google Gemini API key**, not OpenAI
82
+ - Check it's enabled at [Google AI Studio](https://makersuite.google.com/app/apikey)
83
+
84
+ ### Document processing fails
85
+ - Make sure document is under 20 pages
86
+ - Try a different file format
87
+ - Check if file is corrupted
88
+
89
+ ## What's Next?
90
+
91
+ Check out [README.md](README.md) for:
92
+ - Full documentation
93
+ - Deployment instructions
94
+ - Configuration options
95
+ - API documentation
96
+
97
+ ## Need Help?
98
+
99
+ Open an issue on GitHub or check the logs in your terminal for error messages.
100
+
101
+ ---
102
+
103
+ Happy RAG-ing! πŸŽ‰
README.md ADDED
@@ -0,0 +1,315 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # πŸ“š Docling RAG Demo Webapp
2
+
3
+ A simple, clean RAG (Retrieval-Augmented Generation) demo application built with vanilla HTML/CSS/JS frontend and Python FastAPI backend. Uses [Docling](https://github.com/docling-project/docling) for document processing and Google Gemini for AI responses.
4
+
5
+ ## Features
6
+
7
+ - πŸ“„ **Multi-format support**: PDF, DOCX, PPTX, XLSX, HTML
8
+ - πŸ” **Smart document parsing** with Docling
9
+ - πŸ’¬ **Clean chat interface** with vanilla JavaScript
10
+ - 🎯 **Source citations** showing where answers come from
11
+ - ⚑ **Simple architecture** - No heavy frameworks
12
+ - πŸ”’ **Privacy-focused**: API keys only used in your session
13
+ - πŸ“ **Page limit**: Max 20 pages for fast processing
14
+ - πŸš€ **Easy deployment** to Render, Railway, or any platform
15
+
16
+ ## Architecture
17
+
18
+ **Simple & Scalable Stack:**
19
+ ```
20
+ Frontend (HTML/CSS/JS)
21
+ ↓
22
+ FastAPI Backend
23
+ β”œβ”€β”€ Docling (document processing)
24
+ β”œβ”€β”€ ChromaDB (vector store, in-memory)
25
+ β”œβ”€β”€ sentence-transformers (embeddings, local)
26
+ └── Google Gemini API (LLM)
27
+ ```
28
+
29
+ **No LangChain, No Streamlit** - Just the essentials!
30
+
31
+ ## Project Structure
32
+
33
+ ```
34
+ docling-rag-webapp/
35
+ β”œβ”€β”€ backend/
36
+ β”‚ └── main.py # FastAPI server
37
+ β”œβ”€β”€ frontend/
38
+ β”‚ β”œβ”€β”€ index.html # UI
39
+ β”‚ β”œβ”€β”€ style.css # Styling
40
+ β”‚ └── script.js # Logic
41
+ β”œβ”€β”€ requirements.txt # Python dependencies
42
+ └── README.md
43
+ ```
44
+
45
+ ## Local Setup
46
+
47
+ ### Prerequisites
48
+
49
+ - Python 3.9+
50
+ - Google Gemini API key ([Get one free](https://makersuite.google.com/app/apikey))
51
+
52
+ ### Installation
53
+
54
+ 1. **Clone the repository**
55
+ ```bash
56
+ git clone <your-repo-url>
57
+ cd docling-rag-webapp
58
+ ```
59
+
60
+ 2. **Create virtual environment**
61
+ ```bash
62
+ python -m venv venv
63
+ source venv/bin/activate # Windows: venv\Scripts\activate
64
+ ```
65
+
66
+ 3. **Install dependencies**
67
+ ```bash
68
+ pip install -r requirements.txt
69
+ ```
70
+
71
+ 4. **Run the server**
72
+ ```bash
73
+ cd backend
74
+ python main.py
75
+ ```
76
+
77
+ 5. **Open browser**
78
+ ```
79
+ http://localhost:8000
80
+ ```
81
+
82
+ ## Usage
83
+
84
+ 1. Enter your **Google Gemini API key**
85
+ 2. **Upload a document** (max 20 pages)
86
+ 3. Click **"Upload & Process"**
87
+ 4. **Ask questions** in the chat!
88
+
89
+ ### Example Questions
90
+
91
+ - "What is this document about?"
92
+ - "Summarize the main points"
93
+ - "What are the key findings?"
94
+ - "Explain [concept] from the document"
95
+
96
+ ## Deploy to Render (FREE)
97
+
98
+ ### Step 1: Prepare for Deployment
99
+
100
+ 1. **Create `render.yaml`** in the root directory:
101
+ ```yaml
102
+ services:
103
+ - type: web
104
+ name: docling-rag-app
105
+ runtime: python
106
+ buildCommand: pip install -r requirements.txt
107
+ startCommand: cd backend && uvicorn main:app --host 0.0.0.0 --port $PORT
108
+ ```
109
+
110
+ 2. **Push to GitHub**
111
+ ```bash
112
+ git init
113
+ git add .
114
+ git commit -m "Initial commit"
115
+ git branch -M main
116
+ git remote add origin <your-github-repo-url>
117
+ git push -u origin main
118
+ ```
119
+
120
+ ### Step 2: Deploy on Render
121
+
122
+ 1. Go to [render.com](https://render.com)
123
+ 2. Sign in with GitHub
124
+ 3. Click **"New +"** β†’ **"Web Service"**
125
+ 4. Connect your repository
126
+ 5. Render will detect `render.yaml` automatically
127
+ 6. Click **"Create Web Service"**
128
+ 7. Wait 3-5 minutes for deployment
129
+ 8. Your app is live! πŸŽ‰
130
+
131
+ **Free tier includes:**
132
+ - 750 hours/month
133
+ - Auto-sleep after 15 min inactivity
134
+ - Perfect for demos!
135
+
136
+ ## Alternative Deployment Options
137
+
138
+ ### Railway
139
+
140
+ ```bash
141
+ # Install Railway CLI
142
+ npm i -g @railway/cli
143
+
144
+ # Login and deploy
145
+ railway login
146
+ railway init
147
+ railway up
148
+ ```
149
+
150
+ ### Fly.io
151
+
152
+ ```bash
153
+ # Install flyctl
154
+ curl -L https://fly.io/install.sh | sh
155
+
156
+ # Deploy
157
+ fly launch
158
+ fly deploy
159
+ ```
160
+
161
+ ### Docker
162
+
163
+ ```dockerfile
164
+ # Dockerfile
165
+ FROM python:3.11-slim
166
+
167
+ WORKDIR /app
168
+ COPY requirements.txt .
169
+ RUN pip install -r requirements.txt
170
+
171
+ COPY backend/ backend/
172
+ COPY frontend/ frontend/
173
+
174
+ WORKDIR /app/backend
175
+ CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]
176
+ ```
177
+
178
+ ```bash
179
+ docker build -t docling-rag .
180
+ docker run -p 8000:8000 docling-rag
181
+ ```
182
+
183
+ ## Configuration
184
+
185
+ ### Adjust Page Limit
186
+
187
+ Edit `backend/main.py`:
188
+ ```python
189
+ MAX_PAGES = 20 # Change this
190
+ ```
191
+
192
+ ### Change Embedding Model
193
+
194
+ Edit `backend/main.py`:
195
+ ```python
196
+ embedding_model = SentenceTransformer('sentence-transformers/all-MiniLM-L6-v2')
197
+ # Try: 'all-mpnet-base-v2' for better quality
198
+ ```
199
+
200
+ ### Change Gemini Model
201
+
202
+ Edit `backend/main.py` in `generate_answer()`:
203
+ ```python
204
+ model = genai.GenerativeModel('gemini-2.0-flash-exp')
205
+ # Options: 'gemini-1.5-pro', 'gemini-1.5-flash'
206
+ ```
207
+
208
+ ### Chunk Size
209
+
210
+ Edit `backend/main.py`:
211
+ ```python
212
+ CHUNK_SIZE = 1000 # Characters per chunk
213
+ CHUNK_OVERLAP = 200 # Overlap between chunks
214
+ ```
215
+
216
+ ## API Endpoints
217
+
218
+ ### POST /upload
219
+ Upload and process a document
220
+ - **Body**: `multipart/form-data`
221
+ - `file`: Document file
222
+ - `api_key`: Google Gemini API key
223
+ - **Returns**: Session ID and processing stats
224
+
225
+ ### POST /ask
226
+ Ask a question about the document
227
+ - **Body**: `multipart/form-data`
228
+ - `session_id`: Session ID from upload
229
+ - `question`: User question
230
+ - **Returns**: Answer and source chunks
231
+
232
+ ### POST /clear
233
+ Clear a session
234
+ - **Body**: `multipart/form-data`
235
+ - `session_id`: Session ID to clear
236
+
237
+ ## How It Works
238
+
239
+ 1. **Upload**: Document is processed by Docling β†’ converts to markdown
240
+ 2. **Chunking**: Text is split into ~1000 character chunks with 200 char overlap
241
+ 3. **Embedding**: sentence-transformers creates vector embeddings (runs locally)
242
+ 4. **Storage**: Vectors stored in ChromaDB (in-memory for demo)
243
+ 5. **Query**: User question is embedded and similar chunks are retrieved
244
+ 6. **Generate**: Top 3 chunks + question sent to Gemini β†’ answer returned
245
+
246
+ ## Limitations
247
+
248
+ - **Page limit**: 20 pages (configurable)
249
+ - **Session storage**: In-memory (resets on server restart)
250
+ - **Single document**: One document per session
251
+ - **No persistence**: Documents not saved between sessions
252
+
253
+ ## Troubleshooting
254
+
255
+ ### "Error processing document"
256
+ - Document may be too large (>20 pages)
257
+ - Try a different file format
258
+ - Check if file is corrupted
259
+
260
+ ### "Error generating answer"
261
+ - Verify API key is correct
262
+ - Check Gemini API quota
263
+ - Ensure stable internet connection
264
+
265
+ ### "Session not found"
266
+ - Document processing may have failed
267
+ - Try uploading again
268
+ - Check server logs
269
+
270
+ ### Slow initial load
271
+ - First run downloads embedding model (~90MB)
272
+ - Subsequent runs are much faster
273
+ - Model is cached locally
274
+
275
+ ## Future Enhancements
276
+
277
+ Want to improve this? Ideas:
278
+
279
+ - [ ] Multiple document support
280
+ - [ ] Persistent vector store (PostgreSQL + pgvector)
281
+ - [ ] User authentication
282
+ - [ ] Document management dashboard
283
+ - [ ] Export chat history
284
+ - [ ] Support local LLMs (Ollama)
285
+ - [ ] File type validation
286
+ - [ ] Progress bar for processing
287
+ - [ ] Multilingual support
288
+
289
+ ## Why This Stack?
290
+
291
+ **Docling**: Best-in-class document parsing, handles complex PDFs
292
+ **ChromaDB**: Simple vector store, no setup needed
293
+ **sentence-transformers**: Free embeddings, runs locally
294
+ **Gemini**: Fast, generous free tier, great quality
295
+ **FastAPI**: Modern, fast, auto-docs
296
+ **Vanilla JS**: No build step, easy to understand
297
+
298
+ ## License
299
+
300
+ MIT License - use freely!
301
+
302
+ ## Acknowledgments
303
+
304
+ - [Docling](https://github.com/docling-project/docling) - Amazing document processing
305
+ - [ChromaDB](https://www.trychroma.com/) - Simple vector database
306
+ - [sentence-transformers](https://www.sbert.net/) - Excellent embedding models
307
+ - [Google Gemini](https://ai.google.dev/) - Powerful AI model
308
+
309
+ ## Support
310
+
311
+ Issues? Questions? [Open an issue](https://github.com/yourusername/docling-rag-webapp/issues)
312
+
313
+ ---
314
+
315
+ Built with ❀️ using simple, modern web technologies
backend/main.py ADDED
@@ -0,0 +1,321 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import FastAPI, UploadFile, File, Form, HTTPException
2
+ from fastapi.middleware.cors import CORSMiddleware
3
+ from fastapi.staticfiles import StaticFiles
4
+ from fastapi.responses import JSONResponse
5
+ import tempfile
6
+ import os
7
+ from typing import Optional
8
+ import uuid
9
+
10
+ from docling.document_converter import DocumentConverter
11
+ from pypdf import PdfReader
12
+ import chromadb
13
+ from chromadb.config import Settings
14
+ from sentence_transformers import SentenceTransformer
15
+ import google.generativeai as genai
16
+
17
+ # Constants
18
+ MAX_PAGES = 20
19
+ CHUNK_SIZE = 1000
20
+ CHUNK_OVERLAP = 200
21
+
22
+ # Initialize FastAPI
23
+ app = FastAPI(title="Docling RAG API")
24
+
25
+ # Enable CORS
26
+ app.add_middleware(
27
+ CORSMiddleware,
28
+ allow_origins=["*"],
29
+ allow_credentials=True,
30
+ allow_methods=["*"],
31
+ allow_headers=["*"],
32
+ )
33
+
34
+ # In-memory storage for sessions
35
+ sessions = {}
36
+
37
+ # Initialize embedding model (loads once at startup)
38
+ print("Loading embedding model...")
39
+ embedding_model = SentenceTransformer('sentence-transformers/all-MiniLM-L6-v2')
40
+ print("Embedding model loaded!")
41
+
42
+
43
+ class DocumentProcessor:
44
+ """Handles document processing and RAG functionality"""
45
+
46
+ def __init__(self, session_id: str):
47
+ self.session_id = session_id
48
+ self.chroma_client = chromadb.Client(Settings(
49
+ anonymized_telemetry=False,
50
+ allow_reset=True
51
+ ))
52
+ self.collection = None
53
+ self.document_content = ""
54
+
55
+ def check_pdf_pages(self, file_path: str) -> tuple[bool, int]:
56
+ """Check if PDF is within page limit"""
57
+ try:
58
+ reader = PdfReader(file_path)
59
+ num_pages = len(reader.pages)
60
+ return num_pages <= MAX_PAGES, num_pages
61
+ except Exception as e:
62
+ raise HTTPException(status_code=400, detail=f"Error reading PDF: {str(e)}")
63
+
64
+ def process_document(self, file_path: str, file_extension: str) -> dict:
65
+ """Process document with Docling"""
66
+ try:
67
+ # Check PDF page limit
68
+ if file_extension == "pdf":
69
+ within_limit, num_pages = self.check_pdf_pages(file_path)
70
+ if not within_limit:
71
+ raise HTTPException(
72
+ status_code=400,
73
+ detail=f"Document has {num_pages} pages. Maximum allowed: {MAX_PAGES} pages."
74
+ )
75
+
76
+ # Convert document with Docling
77
+ converter = DocumentConverter()
78
+ result = converter.convert(file_path)
79
+ markdown_content = result.document.export_to_markdown()
80
+
81
+ # Estimate pages for non-PDF formats
82
+ if file_extension != "pdf":
83
+ estimated_pages = len(markdown_content) // 3000
84
+ if estimated_pages > MAX_PAGES:
85
+ raise HTTPException(
86
+ status_code=400,
87
+ detail=f"Document too large! Estimated {estimated_pages} pages. Maximum: {MAX_PAGES}."
88
+ )
89
+
90
+ self.document_content = markdown_content
91
+
92
+ # Chunk the text
93
+ chunks = self._chunk_text(markdown_content)
94
+
95
+ # Create embeddings and store in ChromaDB
96
+ self._create_vector_store(chunks)
97
+
98
+ return {
99
+ "status": "success",
100
+ "message": "Document processed successfully",
101
+ "num_chunks": len(chunks),
102
+ "content_length": len(markdown_content)
103
+ }
104
+
105
+ except HTTPException:
106
+ raise
107
+ except Exception as e:
108
+ raise HTTPException(status_code=500, detail=f"Error processing document: {str(e)}")
109
+
110
+ def _chunk_text(self, text: str) -> list[str]:
111
+ """Simple text chunking with overlap"""
112
+ chunks = []
113
+ start = 0
114
+ text_length = len(text)
115
+
116
+ while start < text_length:
117
+ end = start + CHUNK_SIZE
118
+ chunk = text[start:end]
119
+
120
+ # Try to break at sentence boundary
121
+ if end < text_length:
122
+ last_period = chunk.rfind('.')
123
+ last_newline = chunk.rfind('\n')
124
+ break_point = max(last_period, last_newline)
125
+
126
+ if break_point > CHUNK_SIZE // 2:
127
+ chunk = chunk[:break_point + 1]
128
+ end = start + break_point + 1
129
+
130
+ chunks.append(chunk.strip())
131
+ start = end - CHUNK_OVERLAP
132
+
133
+ return [c for c in chunks if c] # Remove empty chunks
134
+
135
+ def _create_vector_store(self, chunks: list[str]):
136
+ """Create ChromaDB collection with embeddings"""
137
+ try:
138
+ # Create collection
139
+ collection_name = f"docs_{self.session_id}"
140
+ self.collection = self.chroma_client.create_collection(
141
+ name=collection_name,
142
+ metadata={"hnsw:space": "cosine"}
143
+ )
144
+
145
+ # Generate embeddings
146
+ embeddings = embedding_model.encode(chunks).tolist()
147
+
148
+ # Add to ChromaDB
149
+ ids = [f"chunk_{i}" for i in range(len(chunks))]
150
+ self.collection.add(
151
+ embeddings=embeddings,
152
+ documents=chunks,
153
+ ids=ids
154
+ )
155
+
156
+ except Exception as e:
157
+ raise HTTPException(status_code=500, detail=f"Error creating vector store: {str(e)}")
158
+
159
+ def search_similar(self, query: str, top_k: int = 3) -> list[str]:
160
+ """Search for similar chunks"""
161
+ if not self.collection:
162
+ raise HTTPException(status_code=400, detail="No document loaded")
163
+
164
+ try:
165
+ # Embed query
166
+ query_embedding = embedding_model.encode([query])[0].tolist()
167
+
168
+ # Search
169
+ results = self.collection.query(
170
+ query_embeddings=[query_embedding],
171
+ n_results=top_k
172
+ )
173
+
174
+ return results['documents'][0] if results['documents'] else []
175
+
176
+ except Exception as e:
177
+ raise HTTPException(status_code=500, detail=f"Error searching: {str(e)}")
178
+
179
+
180
+ def generate_answer(query: str, context_chunks: list[str], api_key: str) -> str:
181
+ """Generate answer using Google Gemini"""
182
+ try:
183
+ # Configure Gemini
184
+ genai.configure(api_key=api_key)
185
+ model = genai.GenerativeModel('gemini-2.0-flash-exp')
186
+
187
+ # Build context
188
+ context = "\n\n".join([f"Context {i+1}:\n{chunk}" for i, chunk in enumerate(context_chunks)])
189
+
190
+ # Build prompt
191
+ prompt = f"""You are a helpful assistant answering questions about a document.
192
+ Use the following context to answer the question. If you cannot answer based on the context, say so.
193
+
194
+ {context}
195
+
196
+ Question: {query}
197
+
198
+ Answer:"""
199
+
200
+ # Generate response
201
+ response = model.generate_content(prompt)
202
+
203
+ return response.text
204
+
205
+ except Exception as e:
206
+ raise HTTPException(status_code=500, detail=f"Error generating answer: {str(e)}")
207
+
208
+
209
+ # API Endpoints
210
+
211
+ @app.get("/api/health")
212
+ async def health():
213
+ return {"message": "Docling RAG API is running!", "status": "healthy"}
214
+
215
+
216
+ @app.post("/upload")
217
+ async def upload_document(
218
+ file: UploadFile = File(...),
219
+ api_key: str = Form(...)
220
+ ):
221
+ """Upload and process a document"""
222
+
223
+ # Validate file type
224
+ file_extension = file.filename.split('.')[-1].lower()
225
+ allowed_extensions = ['pdf', 'docx', 'pptx', 'xlsx', 'html']
226
+
227
+ if file_extension not in allowed_extensions:
228
+ raise HTTPException(
229
+ status_code=400,
230
+ detail=f"Unsupported file type. Allowed: {', '.join(allowed_extensions)}"
231
+ )
232
+
233
+ # Create session
234
+ session_id = str(uuid.uuid4())
235
+
236
+ # Save uploaded file temporarily
237
+ with tempfile.NamedTemporaryFile(delete=False, suffix=f".{file_extension}") as tmp_file:
238
+ content = await file.read()
239
+ tmp_file.write(content)
240
+ tmp_file_path = tmp_file.name
241
+
242
+ try:
243
+ # Process document
244
+ processor = DocumentProcessor(session_id)
245
+ result = processor.process_document(tmp_file_path, file_extension)
246
+
247
+ # Store session
248
+ sessions[session_id] = {
249
+ "processor": processor,
250
+ "api_key": api_key,
251
+ "filename": file.filename
252
+ }
253
+
254
+ return {
255
+ **result,
256
+ "session_id": session_id,
257
+ "filename": file.filename
258
+ }
259
+
260
+ finally:
261
+ # Clean up temp file
262
+ if os.path.exists(tmp_file_path):
263
+ os.unlink(tmp_file_path)
264
+
265
+
266
+ @app.post("/ask")
267
+ async def ask_question(
268
+ session_id: str = Form(...),
269
+ question: str = Form(...)
270
+ ):
271
+ """Ask a question about the uploaded document"""
272
+
273
+ # Get session
274
+ if session_id not in sessions:
275
+ raise HTTPException(status_code=404, detail="Session not found. Please upload a document first.")
276
+
277
+ session = sessions[session_id]
278
+ processor = session["processor"]
279
+ api_key = session["api_key"]
280
+
281
+ try:
282
+ # Search for relevant chunks
283
+ context_chunks = processor.search_similar(question, top_k=3)
284
+
285
+ if not context_chunks:
286
+ return {
287
+ "answer": "No relevant information found in the document.",
288
+ "sources": []
289
+ }
290
+
291
+ # Generate answer with Gemini
292
+ answer = generate_answer(question, context_chunks, api_key)
293
+
294
+ return {
295
+ "answer": answer,
296
+ "sources": context_chunks
297
+ }
298
+
299
+ except Exception as e:
300
+ raise HTTPException(status_code=500, detail=str(e))
301
+
302
+
303
+ @app.post("/clear")
304
+ async def clear_session(session_id: str = Form(...)):
305
+ """Clear a session"""
306
+ if session_id in sessions:
307
+ del sessions[session_id]
308
+ return {"status": "success", "message": "Session cleared"}
309
+ else:
310
+ raise HTTPException(status_code=404, detail="Session not found")
311
+
312
+
313
+ # Mount static files (frontend)
314
+ app.mount("/", StaticFiles(directory="../frontend", html=True), name="static")
315
+
316
+
317
+ if __name__ == "__main__":
318
+ import uvicorn
319
+ port = int(os.getenv("PORT", 8001))
320
+ print(f"πŸš€ Starting server on http://0.0.0.0:{port}")
321
+ uvicorn.run(app, host="0.0.0.0", port=port)
docker-compose.yml ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ services:
2
+ docling-rag:
3
+ container_name: docling-webapp
4
+ build: .
5
+ ports:
6
+ - "8001:8001"
7
+ volumes:
8
+ - ./backend:/app/backend
9
+ - ./frontend:/app/frontend
10
+ environment:
11
+ - PYTHONUNBUFFERED=1
12
+ - PORT=8001
13
+ restart: unless-stopped
frontend/index.html ADDED
@@ -0,0 +1,335 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>IntelliDoc AI - Transform Documents into Conversations</title>
7
+ <link rel="stylesheet" href="style.css">
8
+ <link rel="preconnect" href="https://fonts.googleapis.com">
9
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
10
+ <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
11
+ </head>
12
+ <body>
13
+ <!-- Navigation -->
14
+ <nav class="navbar">
15
+ <div class="nav-container">
16
+ <div class="logo">
17
+ <svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
18
+ <rect width="32" height="32" rx="8" fill="url(#gradient)"/>
19
+ <path d="M12 10h8v2h-8v-2zm0 4h8v2h-8v-2zm0 4h5v2h-5v-2z" fill="white"/>
20
+ <defs>
21
+ <linearGradient id="gradient" x1="0" y1="0" x2="32" y2="32">
22
+ <stop offset="0%" style="stop-color:#4F46E5"/>
23
+ <stop offset="100%" style="stop-color:#7C3AED"/>
24
+ </linearGradient>
25
+ </defs>
26
+ </svg>
27
+ <span class="logo-text">IntelliDoc<span class="ai-badge">AI</span></span>
28
+ </div>
29
+ <div class="nav-links">
30
+ <a href="#features">Features</a>
31
+ <a href="#how-it-works">How It Works</a>
32
+ <a href="#get-started" class="nav-cta">Get Started</a>
33
+ </div>
34
+ </div>
35
+ </nav>
36
+
37
+ <!-- Hero Section -->
38
+ <section class="hero" id="get-started">
39
+ <div class="hero-container">
40
+ <div class="hero-content">
41
+ <div class="badge">Powered by Advanced AI</div>
42
+ <h1 class="hero-title">Transform Your Documents into<br><span class="gradient-text">Intelligent Conversations</span></h1>
43
+ <p class="hero-description">Upload any document and ask questions in natural language. Get instant, accurate answers powered by cutting-edge AI technology.</p>
44
+
45
+ <div class="stats">
46
+ <div class="stat-item">
47
+ <div class="stat-number">5+</div>
48
+ <div class="stat-label">File Formats</div>
49
+ </div>
50
+ <div class="stat-item">
51
+ <div class="stat-number">20</div>
52
+ <div class="stat-label">Max Pages</div>
53
+ </div>
54
+ <div class="stat-item">
55
+ <div class="stat-number">&lt;30s</div>
56
+ <div class="stat-label">Processing Time</div>
57
+ </div>
58
+ </div>
59
+ </div>
60
+
61
+ <!-- Upload Card -->
62
+ <div class="upload-card" id="uploadCard">
63
+ <div class="card-header">
64
+ <h3>Start Analyzing</h3>
65
+ <p>Upload your document to begin</p>
66
+ </div>
67
+
68
+ <div class="form-group">
69
+ <label class="form-label">
70
+ <svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
71
+ <path d="M8 0a3 3 0 013 3v1h1a2 2 0 012 2v8a2 2 0 01-2 2H4a2 2 0 01-2-2V6a2 2 0 012-2h1V3a3 3 0 013-3zm0 2a1 1 0 00-1 1v1h2V3a1 1 0 00-1-1z"/>
72
+ </svg>
73
+ Google Gemini API Key
74
+ </label>
75
+ <input
76
+ type="password"
77
+ id="apiKey"
78
+ placeholder="Enter your API key"
79
+ class="input-field"
80
+ >
81
+ <small class="help-text">Get your free API key at <a href="https://makersuite.google.com/app/apikey" target="_blank" rel="noopener">Google AI Studio</a></small>
82
+ </div>
83
+
84
+ <div class="form-group">
85
+ <label class="form-label">
86
+ <svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
87
+ <path d="M4 0h8a2 2 0 012 2v12a2 2 0 01-2 2H4a2 2 0 01-2-2V2a2 2 0 012-2zm1 4v1h6V4H5zm0 3v1h6V7H5zm0 3v1h4v-1H5z"/>
88
+ </svg>
89
+ Select Document
90
+ </label>
91
+ <div class="file-upload-wrapper">
92
+ <input
93
+ type="file"
94
+ id="fileInput"
95
+ accept=".pdf,.docx,.pptx,.xlsx,.html"
96
+ class="file-input"
97
+ >
98
+ <label for="fileInput" class="file-upload-label">
99
+ <svg width="20" height="20" viewBox="0 0 20 20" fill="currentColor">
100
+ <path d="M16.88 9.1A4 4 0 0 1 16 17H5a5 5 0 0 1-1-9.9V7a3 3 0 0 1 4.52-2.59A4.98 4.98 0 0 1 17 8c0 .38-.04.74-.12 1.1zM11 11h3l-4-4-4 4h3v3h2v-3z"/>
101
+ </svg>
102
+ <span id="fileName">Click to browse or drag & drop</span>
103
+ </label>
104
+ </div>
105
+ <small class="help-text">Supported: PDF, DOCX, PPTX, XLSX, HTML (Max 20 pages)</small>
106
+ </div>
107
+
108
+ <button id="uploadBtn" class="btn-primary">
109
+ <svg width="20" height="20" viewBox="0 0 20 20" fill="currentColor">
110
+ <path d="M13 8V2H7v6H2l8 8 8-8h-5zM0 18h20v2H0v-2z"/>
111
+ </svg>
112
+ Process Document
113
+ </button>
114
+
115
+ <div id="uploadStatus" class="status-message"></div>
116
+ </div>
117
+ </div>
118
+ </section>
119
+
120
+ <!-- Features Section -->
121
+ <section class="features" id="features">
122
+ <div class="section-container">
123
+ <div class="section-header">
124
+ <h2>Why Choose IntelliDoc AI?</h2>
125
+ <p>Enterprise-grade document intelligence at your fingertips</p>
126
+ </div>
127
+
128
+ <div class="features-grid">
129
+ <div class="feature-card">
130
+ <div class="feature-icon">
131
+ <svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
132
+ <path d="M13 2L3 14h9l-1 8 10-12h-9l1-8z"/>
133
+ </svg>
134
+ </div>
135
+ <h3>Lightning Fast</h3>
136
+ <p>Process documents in seconds with our optimized AI pipeline</p>
137
+ </div>
138
+
139
+ <div class="feature-card">
140
+ <div class="feature-icon">
141
+ <svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
142
+ <path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/>
143
+ </svg>
144
+ </div>
145
+ <h3>Secure & Private</h3>
146
+ <p>Your data stays private. No storage, no tracking, complete confidentiality</p>
147
+ </div>
148
+
149
+ <div class="feature-card">
150
+ <div class="feature-icon">
151
+ <svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
152
+ <circle cx="12" cy="12" r="10"/>
153
+ <path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3"/>
154
+ <circle cx="12" cy="17" r="1"/>
155
+ </svg>
156
+ </div>
157
+ <h3>Smart Answers</h3>
158
+ <p>Get accurate, contextual answers with source citations</p>
159
+ </div>
160
+
161
+ <div class="feature-card">
162
+ <div class="feature-icon">
163
+ <svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
164
+ <path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/>
165
+ <path d="M14 2v6h6"/>
166
+ <path d="M16 13H8"/>
167
+ <path d="M16 17H8"/>
168
+ <path d="M10 9H8"/>
169
+ </svg>
170
+ </div>
171
+ <h3>Multi-Format</h3>
172
+ <p>Support for PDF, Word, PowerPoint, Excel, and HTML documents</p>
173
+ </div>
174
+
175
+ <div class="feature-card">
176
+ <div class="feature-icon">
177
+ <svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
178
+ <path d="M21 11.5a8.38 8.38 0 0 1-.9 3.8 8.5 8.5 0 0 1-7.6 4.7 8.38 8.38 0 0 1-3.8-.9L3 21l1.9-5.7a8.38 8.38 0 0 1-.9-3.8 8.5 8.5 0 0 1 4.7-7.6 8.38 8.38 0 0 1 3.8-.9h.5a8.48 8.48 0 0 1 8 8v.5z"/>
179
+ </svg>
180
+ </div>
181
+ <h3>Natural Language</h3>
182
+ <p>Ask questions in plain English, no technical skills required</p>
183
+ </div>
184
+
185
+ <div class="feature-card">
186
+ <div class="feature-icon">
187
+ <svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
188
+ <path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"/>
189
+ <polyline points="22 4 12 14.01 9 11.01"/>
190
+ </svg>
191
+ </div>
192
+ <h3>Source Verification</h3>
193
+ <p>Every answer includes references to the original document</p>
194
+ </div>
195
+ </div>
196
+ </div>
197
+ </section>
198
+
199
+ <!-- How It Works -->
200
+ <section class="how-it-works" id="how-it-works">
201
+ <div class="section-container">
202
+ <div class="section-header">
203
+ <h2>How It Works</h2>
204
+ <p>Three simple steps to intelligent document analysis</p>
205
+ </div>
206
+
207
+ <div class="steps">
208
+ <div class="step">
209
+ <div class="step-number">1</div>
210
+ <div class="step-content">
211
+ <h3>Upload Document</h3>
212
+ <p>Simply drag and drop or select your document. We support all major formats.</p>
213
+ </div>
214
+ </div>
215
+
216
+ <div class="step">
217
+ <div class="step-number">2</div>
218
+ <div class="step-content">
219
+ <h3>AI Processing</h3>
220
+ <p>Our advanced AI analyzes your document, understanding context and structure.</p>
221
+ </div>
222
+ </div>
223
+
224
+ <div class="step">
225
+ <div class="step-number">3</div>
226
+ <div class="step-content">
227
+ <h3>Ask & Get Answers</h3>
228
+ <p>Start conversing with your document. Get instant, accurate insights.</p>
229
+ </div>
230
+ </div>
231
+ </div>
232
+ </div>
233
+ </section>
234
+
235
+ <!-- Chat Interface -->
236
+ <div class="chat-overlay" id="chatOverlay">
237
+ <div class="chat-container">
238
+ <div class="chat-header">
239
+ <div class="chat-header-left">
240
+ <div class="status-indicator"></div>
241
+ <div>
242
+ <h3 id="documentName">Document Analysis</h3>
243
+ <p class="chat-subtitle">Ask anything about your document</p>
244
+ </div>
245
+ </div>
246
+ <div class="chat-header-actions">
247
+ <button id="clearBtn" class="btn-ghost">
248
+ <svg width="20" height="20" viewBox="0 0 20 20" fill="currentColor">
249
+ <path d="M6 2l2-2h4l2 2h4v2H2V2h4zM3 6h14l-1 14H4L3 6zm5 2v10h1V8H8zm3 0v10h1V8h-1z"/>
250
+ </svg>
251
+ New Document
252
+ </button>
253
+ </div>
254
+ </div>
255
+
256
+ <div class="messages-container" id="messagesContainer">
257
+ <div class="welcome-message">
258
+ <div class="welcome-icon">
259
+ <svg width="48" height="48" viewBox="0 0 48 48" fill="none">
260
+ <rect width="48" height="48" rx="12" fill="url(#gradient2)"/>
261
+ <path d="M18 15h12v3h-12v-3zm0 6h12v3h-12v-3zm0 6h8v3h-8v-3z" fill="white"/>
262
+ <defs>
263
+ <linearGradient id="gradient2" x1="0" y1="0" x2="48" y2="48">
264
+ <stop offset="0%" style="stop-color:#4F46E5"/>
265
+ <stop offset="100%" style="stop-color:#7C3AED"/>
266
+ </linearGradient>
267
+ </defs>
268
+ </svg>
269
+ </div>
270
+ <h3>Document Ready!</h3>
271
+ <p>Your document has been processed. Start asking questions below.</p>
272
+ <div class="example-questions">
273
+ <p class="example-label">Try asking:</p>
274
+ <div class="example-chips">
275
+ <span class="chip">"What is this document about?"</span>
276
+ <span class="chip">"Summarize the main points"</span>
277
+ <span class="chip">"What are the key findings?"</span>
278
+ </div>
279
+ </div>
280
+ </div>
281
+ </div>
282
+
283
+ <div class="input-container">
284
+ <div class="input-wrapper">
285
+ <input
286
+ type="text"
287
+ id="questionInput"
288
+ placeholder="Ask a question about your document..."
289
+ class="chat-input"
290
+ >
291
+ <button id="sendBtn" class="btn-send">
292
+ <svg width="20" height="20" viewBox="0 0 20 20" fill="currentColor">
293
+ <path d="M0 0l20 10L0 20V12l14-2-14-2z"/>
294
+ </svg>
295
+ </button>
296
+ </div>
297
+ <p class="input-hint">Press Enter to send β€’ AI-powered by Google Gemini</p>
298
+ </div>
299
+ </div>
300
+ </div>
301
+
302
+ <!-- Loading Overlay -->
303
+ <div class="loading-overlay" id="loadingOverlay">
304
+ <div class="loading-content">
305
+ <div class="spinner"></div>
306
+ <p id="loadingText">Processing...</p>
307
+ </div>
308
+ </div>
309
+
310
+ <!-- Footer -->
311
+ <footer class="footer">
312
+ <div class="footer-container">
313
+ <div class="footer-content">
314
+ <div class="footer-logo">
315
+ <svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
316
+ <rect width="32" height="32" rx="8" fill="url(#gradient3)"/>
317
+ <path d="M12 10h8v2h-8v-2zm0 4h8v2h-8v-2zm0 4h5v2h-5v-2z" fill="white"/>
318
+ <defs>
319
+ <linearGradient id="gradient3" x1="0" y1="0" x2="32" y2="32">
320
+ <stop offset="0%" style="stop-color:#4F46E5"/>
321
+ <stop offset="100%" style="stop-color:#7C3AED"/>
322
+ </linearGradient>
323
+ </defs>
324
+ </svg>
325
+ <span>IntelliDoc AI</span>
326
+ </div>
327
+ <p class="footer-text">Transform your documents into intelligent conversations</p>
328
+ <p class="footer-copyright">&copy; 2025 IntelliDoc AI. Powered by Docling & Google Gemini.</p>
329
+ </div>
330
+ </div>
331
+ </footer>
332
+
333
+ <script src="script.js"></script>
334
+ </body>
335
+ </html>
frontend/script.js ADDED
@@ -0,0 +1,319 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // State
2
+ let sessionId = null;
3
+ let apiKey = null;
4
+
5
+ // DOM Elements
6
+ const uploadCard = document.getElementById('uploadCard');
7
+ const chatOverlay = document.getElementById('chatOverlay');
8
+ const uploadBtn = document.getElementById('uploadBtn');
9
+ const sendBtn = document.getElementById('sendBtn');
10
+ const clearBtn = document.getElementById('clearBtn');
11
+ const fileInput = document.getElementById('fileInput');
12
+ const apiKeyInput = document.getElementById('apiKey');
13
+ const questionInput = document.getElementById('questionInput');
14
+ const messagesContainer = document.getElementById('messagesContainer');
15
+ const documentNameEl = document.getElementById('documentName');
16
+ const uploadStatus = document.getElementById('uploadStatus');
17
+ const loadingOverlay = document.getElementById('loadingOverlay');
18
+ const loadingText = document.getElementById('loadingText');
19
+ const fileNameSpan = document.getElementById('fileName');
20
+
21
+ // API Base URL
22
+ const API_BASE = window.location.origin;
23
+
24
+ // Event Listeners
25
+ uploadBtn.addEventListener('click', handleUpload);
26
+ sendBtn.addEventListener('click', handleSendQuestion);
27
+ clearBtn.addEventListener('click', handleClear);
28
+
29
+ questionInput.addEventListener('keypress', (e) => {
30
+ if (e.key === 'Enter' && !e.shiftKey) {
31
+ e.preventDefault();
32
+ handleSendQuestion();
33
+ }
34
+ });
35
+
36
+ // File input change handler
37
+ fileInput.addEventListener('change', (e) => {
38
+ const file = e.target.files[0];
39
+ if (file) {
40
+ fileNameSpan.textContent = file.name;
41
+ } else {
42
+ fileNameSpan.textContent = 'Click to browse or drag & drop';
43
+ }
44
+ });
45
+
46
+ // Show loading overlay
47
+ function showLoading(message = 'Processing...') {
48
+ loadingText.textContent = message;
49
+ loadingOverlay.classList.add('active');
50
+ }
51
+
52
+ // Hide loading overlay
53
+ function hideLoading() {
54
+ loadingOverlay.classList.remove('active');
55
+ }
56
+
57
+ // Show status message
58
+ function showStatus(message, type = 'info') {
59
+ uploadStatus.textContent = message;
60
+ uploadStatus.className = `status-message ${type}`;
61
+ }
62
+
63
+ // Hide status message
64
+ function hideStatus() {
65
+ uploadStatus.className = 'status-message';
66
+ }
67
+
68
+ // Handle document upload
69
+ async function handleUpload() {
70
+ const file = fileInput.files[0];
71
+ apiKey = apiKeyInput.value.trim();
72
+
73
+ // Validation
74
+ if (!apiKey) {
75
+ showStatus('Please enter your Google Gemini API key', 'error');
76
+ return;
77
+ }
78
+
79
+ if (!file) {
80
+ showStatus('Please select a file', 'error');
81
+ return;
82
+ }
83
+
84
+ // Check file size (50MB limit)
85
+ if (file.size > 50 * 1024 * 1024) {
86
+ showStatus('File too large. Maximum size: 50MB', 'error');
87
+ return;
88
+ }
89
+
90
+ try {
91
+ showLoading('Processing your document...');
92
+ hideStatus();
93
+
94
+ // Create FormData
95
+ const formData = new FormData();
96
+ formData.append('file', file);
97
+ formData.append('api_key', apiKey);
98
+
99
+ // Upload
100
+ const response = await fetch(`${API_BASE}/upload`, {
101
+ method: 'POST',
102
+ body: formData
103
+ });
104
+
105
+ const data = await response.json();
106
+
107
+ if (!response.ok) {
108
+ throw new Error(data.detail || 'Upload failed');
109
+ }
110
+
111
+ // Success
112
+ sessionId = data.session_id;
113
+ documentNameEl.textContent = data.filename;
114
+
115
+ // Show chat interface
116
+ chatOverlay.classList.add('active');
117
+
118
+ } catch (error) {
119
+ console.error('Upload error:', error);
120
+ showStatus(error.message, 'error');
121
+ } finally {
122
+ hideLoading();
123
+ }
124
+ }
125
+
126
+ // Handle sending question
127
+ async function handleSendQuestion() {
128
+ const question = questionInput.value.trim();
129
+
130
+ if (!question) {
131
+ return;
132
+ }
133
+
134
+ if (!sessionId) {
135
+ return;
136
+ }
137
+
138
+ // Disable input while processing
139
+ questionInput.disabled = true;
140
+ sendBtn.disabled = true;
141
+
142
+ // Add user message to chat
143
+ addMessage('user', question);
144
+
145
+ // Clear input
146
+ questionInput.value = '';
147
+
148
+ try {
149
+ // Create FormData
150
+ const formData = new FormData();
151
+ formData.append('session_id', sessionId);
152
+ formData.append('question', question);
153
+
154
+ // Ask question
155
+ const response = await fetch(`${API_BASE}/ask`, {
156
+ method: 'POST',
157
+ body: formData
158
+ });
159
+
160
+ const data = await response.json();
161
+
162
+ if (!response.ok) {
163
+ throw new Error(data.detail || 'Failed to get answer');
164
+ }
165
+
166
+ // Add assistant message to chat
167
+ addMessage('assistant', data.answer, data.sources);
168
+
169
+ } catch (error) {
170
+ console.error('Question error:', error);
171
+ addMessage('assistant', `Error: ${error.message}`, []);
172
+ } finally {
173
+ // Re-enable input
174
+ questionInput.disabled = false;
175
+ sendBtn.disabled = false;
176
+ questionInput.focus();
177
+ }
178
+ }
179
+
180
+ // Add message to chat
181
+ function addMessage(role, content, sources = null) {
182
+ // Remove welcome message if it exists
183
+ const welcomeMsg = messagesContainer.querySelector('.welcome-message');
184
+ if (welcomeMsg) {
185
+ welcomeMsg.remove();
186
+ }
187
+
188
+ const messageDiv = document.createElement('div');
189
+ messageDiv.className = `message ${role}`;
190
+
191
+ const header = document.createElement('div');
192
+ header.className = 'message-header';
193
+
194
+ if (role === 'user') {
195
+ header.innerHTML = `
196
+ <svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
197
+ <path d="M8 8a3 3 0 100-6 3 3 0 000 6zm0 2c-4 0-6 2-6 4v2h12v-2c0-2-2-4-6-4z"/>
198
+ </svg>
199
+ You
200
+ `;
201
+ } else {
202
+ header.innerHTML = `
203
+ <svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
204
+ <path d="M8 0a8 8 0 108 8 8 8 0 00-8-8zm0 14a6 6 0 116-6 6 6 0 01-6 6z"/>
205
+ <path d="M6.5 7a.5.5 0 01.5.5v3a.5.5 0 01-1 0v-3a.5.5 0 01.5-.5zm3 0a.5.5 0 01.5.5v3a.5.5 0 01-1 0v-3a.5.5 0 01.5-.5z"/>
206
+ </svg>
207
+ IntelliDoc AI
208
+ `;
209
+ }
210
+
211
+ const messageContent = document.createElement('div');
212
+ messageContent.className = 'message-content';
213
+ messageContent.textContent = content;
214
+
215
+ messageDiv.appendChild(header);
216
+ messageDiv.appendChild(messageContent);
217
+
218
+ // Add sources if available
219
+ if (sources && sources.length > 0) {
220
+ const sourcesDiv = document.createElement('div');
221
+ sourcesDiv.className = 'sources';
222
+
223
+ const sourcesHeader = document.createElement('div');
224
+ sourcesHeader.className = 'sources-header';
225
+ sourcesHeader.innerHTML = `
226
+ <svg width="14" height="14" viewBox="0 0 14 14" fill="currentColor" style="display: inline; margin-right: 4px;">
227
+ <path d="M9 4H5a1 1 0 000 2h4a1 1 0 000-2zM9 7H5a1 1 0 000 2h4a1 1 0 000-2z"/>
228
+ <path d="M11 0H3a2 2 0 00-2 2v10a2 2 0 002 2h8a2 2 0 002-2V2a2 2 0 00-2-2zm0 12H3V2h8v10z"/>
229
+ </svg>
230
+ Source References
231
+ `;
232
+ sourcesDiv.appendChild(sourcesHeader);
233
+
234
+ sources.forEach((source, index) => {
235
+ const sourceItem = document.createElement('div');
236
+ sourceItem.className = 'source-item';
237
+ sourceItem.textContent = source.substring(0, 200) + (source.length > 200 ? '...' : '');
238
+ sourcesDiv.appendChild(sourceItem);
239
+ });
240
+
241
+ messageDiv.appendChild(sourcesDiv);
242
+ }
243
+
244
+ messagesContainer.appendChild(messageDiv);
245
+
246
+ // Scroll to bottom
247
+ messagesContainer.scrollTop = messagesContainer.scrollHeight;
248
+ }
249
+
250
+ // Handle clear
251
+ async function handleClear() {
252
+ if (!confirm('Start a new conversation? This will clear the current document.')) {
253
+ return;
254
+ }
255
+
256
+ try {
257
+ if (sessionId) {
258
+ const formData = new FormData();
259
+ formData.append('session_id', sessionId);
260
+
261
+ await fetch(`${API_BASE}/clear`, {
262
+ method: 'POST',
263
+ body: formData
264
+ });
265
+ }
266
+ } catch (error) {
267
+ console.error('Clear error:', error);
268
+ } finally {
269
+ // Reset UI
270
+ sessionId = null;
271
+ apiKey = null;
272
+ messagesContainer.innerHTML = `
273
+ <div class="welcome-message">
274
+ <div class="welcome-icon">
275
+ <svg width="48" height="48" viewBox="0 0 48 48" fill="none">
276
+ <rect width="48" height="48" rx="12" fill="url(#gradient2)"/>
277
+ <path d="M18 15h12v3h-12v-3zm0 6h12v3h-12v-3zm0 6h8v3h-8v-3z" fill="white"/>
278
+ <defs>
279
+ <linearGradient id="gradient2" x1="0" y1="0" x2="48" y2="48">
280
+ <stop offset="0%" style="stop-color:#4F46E5"/>
281
+ <stop offset="100%" style="stop-color:#7C3AED"/>
282
+ </linearGradient>
283
+ </defs>
284
+ </svg>
285
+ </div>
286
+ <h3>Document Ready!</h3>
287
+ <p>Your document has been processed. Start asking questions below.</p>
288
+ <div class="example-questions">
289
+ <p class="example-label">Try asking:</p>
290
+ <div class="example-chips">
291
+ <span class="chip">"What is this document about?"</span>
292
+ <span class="chip">"Summarize the main points"</span>
293
+ <span class="chip">"What are the key findings?"</span>
294
+ </div>
295
+ </div>
296
+ </div>
297
+ `;
298
+ questionInput.value = '';
299
+ fileInput.value = '';
300
+ fileNameSpan.textContent = 'Click to browse or drag & drop';
301
+ apiKeyInput.value = '';
302
+ chatOverlay.classList.remove('active');
303
+ hideStatus();
304
+ }
305
+ }
306
+
307
+ // Smooth scroll for navigation
308
+ document.querySelectorAll('a[href^="#"]').forEach(anchor => {
309
+ anchor.addEventListener('click', function (e) {
310
+ e.preventDefault();
311
+ const target = document.querySelector(this.getAttribute('href'));
312
+ if (target) {
313
+ target.scrollIntoView({
314
+ behavior: 'smooth',
315
+ block: 'start'
316
+ });
317
+ }
318
+ });
319
+ });
frontend/style.css ADDED
@@ -0,0 +1,898 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ * {
2
+ margin: 0;
3
+ padding: 0;
4
+ box-sizing: border-box;
5
+ }
6
+
7
+ :root {
8
+ --primary: #4F46E5;
9
+ --primary-dark: #4338CA;
10
+ --secondary: #7C3AED;
11
+ --success: #10B981;
12
+ --error: #EF4444;
13
+ --warning: #F59E0B;
14
+ --gray-50: #F9FAFB;
15
+ --gray-100: #F3F4F6;
16
+ --gray-200: #E5E7EB;
17
+ --gray-300: #D1D5DB;
18
+ --gray-400: #9CA3AF;
19
+ --gray-500: #6B7280;
20
+ --gray-600: #4B5563;
21
+ --gray-700: #374151;
22
+ --gray-800: #1F2937;
23
+ --gray-900: #111827;
24
+ }
25
+
26
+ body {
27
+ font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
28
+ background: #FFFFFF;
29
+ color: var(--gray-900);
30
+ line-height: 1.6;
31
+ overflow-x: hidden;
32
+ }
33
+
34
+ /* Navbar */
35
+ .navbar {
36
+ position: fixed;
37
+ top: 0;
38
+ left: 0;
39
+ right: 0;
40
+ background: rgba(255, 255, 255, 0.95);
41
+ backdrop-filter: blur(10px);
42
+ border-bottom: 1px solid var(--gray-200);
43
+ z-index: 100;
44
+ }
45
+
46
+ .nav-container {
47
+ max-width: 1280px;
48
+ margin: 0 auto;
49
+ padding: 1rem 2rem;
50
+ display: flex;
51
+ justify-content: space-between;
52
+ align-items: center;
53
+ }
54
+
55
+ .logo {
56
+ display: flex;
57
+ align-items: center;
58
+ gap: 0.75rem;
59
+ font-weight: 700;
60
+ font-size: 1.25rem;
61
+ color: var(--gray-900);
62
+ }
63
+
64
+ .logo-text {
65
+ display: flex;
66
+ align-items: center;
67
+ gap: 0.25rem;
68
+ }
69
+
70
+ .ai-badge {
71
+ background: linear-gradient(135deg, var(--primary), var(--secondary));
72
+ -webkit-background-clip: text;
73
+ -webkit-text-fill-color: transparent;
74
+ background-clip: text;
75
+ font-size: 0.875rem;
76
+ font-weight: 800;
77
+ }
78
+
79
+ .nav-links {
80
+ display: flex;
81
+ gap: 2rem;
82
+ align-items: center;
83
+ }
84
+
85
+ .nav-links a {
86
+ color: var(--gray-600);
87
+ text-decoration: none;
88
+ font-weight: 500;
89
+ font-size: 0.9375rem;
90
+ transition: color 0.2s;
91
+ }
92
+
93
+ .nav-links a:hover {
94
+ color: var(--primary);
95
+ }
96
+
97
+ .nav-cta {
98
+ background: linear-gradient(135deg, var(--primary), var(--secondary));
99
+ color: white !important;
100
+ padding: 0.5rem 1.25rem;
101
+ border-radius: 0.5rem;
102
+ font-weight: 600;
103
+ }
104
+
105
+ .nav-cta:hover {
106
+ transform: translateY(-1px);
107
+ box-shadow: 0 4px 12px rgba(79, 70, 229, 0.4);
108
+ }
109
+
110
+ /* Hero Section */
111
+ .hero {
112
+ margin-top: 80px;
113
+ padding: 4rem 2rem;
114
+ background: linear-gradient(180deg, var(--gray-50) 0%, #FFFFFF 100%);
115
+ }
116
+
117
+ .hero-container {
118
+ max-width: 1280px;
119
+ margin: 0 auto;
120
+ display: grid;
121
+ grid-template-columns: 1fr 1fr;
122
+ gap: 4rem;
123
+ align-items: center;
124
+ }
125
+
126
+ .hero-content {
127
+ display: flex;
128
+ flex-direction: column;
129
+ gap: 1.5rem;
130
+ }
131
+
132
+ .badge {
133
+ display: inline-flex;
134
+ align-items: center;
135
+ width: fit-content;
136
+ padding: 0.5rem 1rem;
137
+ background: linear-gradient(135deg, rgba(79, 70, 229, 0.1), rgba(124, 58, 237, 0.1));
138
+ border: 1px solid rgba(79, 70, 229, 0.2);
139
+ border-radius: 2rem;
140
+ color: var(--primary);
141
+ font-size: 0.875rem;
142
+ font-weight: 600;
143
+ }
144
+
145
+ .hero-title {
146
+ font-size: 3.5rem;
147
+ font-weight: 800;
148
+ line-height: 1.1;
149
+ color: var(--gray-900);
150
+ letter-spacing: -0.02em;
151
+ }
152
+
153
+ .gradient-text {
154
+ background: linear-gradient(135deg, var(--primary), var(--secondary));
155
+ -webkit-background-clip: text;
156
+ -webkit-text-fill-color: transparent;
157
+ background-clip: text;
158
+ }
159
+
160
+ .hero-description {
161
+ font-size: 1.25rem;
162
+ color: var(--gray-600);
163
+ line-height: 1.8;
164
+ }
165
+
166
+ .stats {
167
+ display: flex;
168
+ gap: 3rem;
169
+ margin-top: 1rem;
170
+ }
171
+
172
+ .stat-item {
173
+ display: flex;
174
+ flex-direction: column;
175
+ }
176
+
177
+ .stat-number {
178
+ font-size: 2rem;
179
+ font-weight: 700;
180
+ color: var(--primary);
181
+ }
182
+
183
+ .stat-label {
184
+ font-size: 0.875rem;
185
+ color: var(--gray-600);
186
+ font-weight: 500;
187
+ }
188
+
189
+ /* Upload Card */
190
+ .upload-card {
191
+ background: white;
192
+ border-radius: 1.5rem;
193
+ padding: 2rem;
194
+ box-shadow: 0 20px 60px rgba(0, 0, 0, 0.08), 0 0 0 1px rgba(0, 0, 0, 0.05);
195
+ }
196
+
197
+ .card-header {
198
+ margin-bottom: 1.5rem;
199
+ }
200
+
201
+ .card-header h3 {
202
+ font-size: 1.5rem;
203
+ font-weight: 700;
204
+ color: var(--gray-900);
205
+ margin-bottom: 0.25rem;
206
+ }
207
+
208
+ .card-header p {
209
+ color: var(--gray-600);
210
+ font-size: 0.9375rem;
211
+ }
212
+
213
+ /* Form Elements */
214
+ .form-group {
215
+ margin-bottom: 1.5rem;
216
+ }
217
+
218
+ .form-label {
219
+ display: flex;
220
+ align-items: center;
221
+ gap: 0.5rem;
222
+ font-weight: 600;
223
+ color: var(--gray-700);
224
+ margin-bottom: 0.5rem;
225
+ font-size: 0.9375rem;
226
+ }
227
+
228
+ .form-label svg {
229
+ color: var(--gray-400);
230
+ }
231
+
232
+ .input-field {
233
+ width: 100%;
234
+ padding: 0.75rem 1rem;
235
+ border: 2px solid var(--gray-200);
236
+ border-radius: 0.75rem;
237
+ font-size: 0.9375rem;
238
+ font-family: inherit;
239
+ transition: all 0.2s;
240
+ background: white;
241
+ }
242
+
243
+ .input-field:focus {
244
+ outline: none;
245
+ border-color: var(--primary);
246
+ box-shadow: 0 0 0 3px rgba(79, 70, 229, 0.1);
247
+ }
248
+
249
+ .help-text {
250
+ display: block;
251
+ margin-top: 0.5rem;
252
+ color: var(--gray-500);
253
+ font-size: 0.8125rem;
254
+ }
255
+
256
+ .help-text a {
257
+ color: var(--primary);
258
+ text-decoration: none;
259
+ font-weight: 500;
260
+ }
261
+
262
+ .help-text a:hover {
263
+ text-decoration: underline;
264
+ }
265
+
266
+ /* File Upload */
267
+ .file-upload-wrapper {
268
+ position: relative;
269
+ }
270
+
271
+ .file-input {
272
+ position: absolute;
273
+ opacity: 0;
274
+ pointer-events: none;
275
+ }
276
+
277
+ .file-upload-label {
278
+ display: flex;
279
+ align-items: center;
280
+ justify-content: center;
281
+ gap: 0.75rem;
282
+ padding: 2rem;
283
+ border: 2px dashed var(--gray-300);
284
+ border-radius: 0.75rem;
285
+ cursor: pointer;
286
+ transition: all 0.2s;
287
+ background: var(--gray-50);
288
+ color: var(--gray-600);
289
+ font-weight: 500;
290
+ }
291
+
292
+ .file-upload-label:hover {
293
+ border-color: var(--primary);
294
+ background: rgba(79, 70, 229, 0.05);
295
+ color: var(--primary);
296
+ }
297
+
298
+ /* Buttons */
299
+ .btn-primary {
300
+ width: 100%;
301
+ display: flex;
302
+ align-items: center;
303
+ justify-content: center;
304
+ gap: 0.5rem;
305
+ padding: 0.875rem 1.5rem;
306
+ background: linear-gradient(135deg, var(--primary), var(--secondary));
307
+ color: white;
308
+ border: none;
309
+ border-radius: 0.75rem;
310
+ font-weight: 600;
311
+ font-size: 1rem;
312
+ cursor: pointer;
313
+ transition: all 0.2s;
314
+ font-family: inherit;
315
+ }
316
+
317
+ .btn-primary:hover:not(:disabled) {
318
+ transform: translateY(-2px);
319
+ box-shadow: 0 8px 24px rgba(79, 70, 229, 0.4);
320
+ }
321
+
322
+ .btn-primary:disabled {
323
+ opacity: 0.5;
324
+ cursor: not-allowed;
325
+ }
326
+
327
+ .btn-ghost {
328
+ display: flex;
329
+ align-items: center;
330
+ gap: 0.5rem;
331
+ padding: 0.5rem 1rem;
332
+ background: transparent;
333
+ color: var(--gray-600);
334
+ border: 1px solid var(--gray-300);
335
+ border-radius: 0.5rem;
336
+ font-weight: 500;
337
+ font-size: 0.9375rem;
338
+ cursor: pointer;
339
+ transition: all 0.2s;
340
+ font-family: inherit;
341
+ }
342
+
343
+ .btn-ghost:hover {
344
+ background: var(--gray-100);
345
+ color: var(--gray-900);
346
+ }
347
+
348
+ /* Status Messages */
349
+ .status-message {
350
+ margin-top: 1rem;
351
+ padding: 1rem;
352
+ border-radius: 0.75rem;
353
+ font-size: 0.9375rem;
354
+ display: none;
355
+ }
356
+
357
+ .status-message.success {
358
+ display: block;
359
+ background: rgba(16, 185, 129, 0.1);
360
+ color: var(--success);
361
+ border: 1px solid rgba(16, 185, 129, 0.2);
362
+ }
363
+
364
+ .status-message.error {
365
+ display: block;
366
+ background: rgba(239, 68, 68, 0.1);
367
+ color: var(--error);
368
+ border: 1px solid rgba(239, 68, 68, 0.2);
369
+ }
370
+
371
+ .status-message.info {
372
+ display: block;
373
+ background: rgba(79, 70, 229, 0.1);
374
+ color: var(--primary);
375
+ border: 1px solid rgba(79, 70, 229, 0.2);
376
+ }
377
+
378
+ /* Features Section */
379
+ .features {
380
+ padding: 6rem 2rem;
381
+ background: white;
382
+ }
383
+
384
+ .section-container {
385
+ max-width: 1280px;
386
+ margin: 0 auto;
387
+ }
388
+
389
+ .section-header {
390
+ text-align: center;
391
+ margin-bottom: 4rem;
392
+ }
393
+
394
+ .section-header h2 {
395
+ font-size: 2.5rem;
396
+ font-weight: 800;
397
+ color: var(--gray-900);
398
+ margin-bottom: 1rem;
399
+ }
400
+
401
+ .section-header p {
402
+ font-size: 1.125rem;
403
+ color: var(--gray-600);
404
+ }
405
+
406
+ .features-grid {
407
+ display: grid;
408
+ grid-template-columns: repeat(3, 1fr);
409
+ gap: 2rem;
410
+ }
411
+
412
+ .feature-card {
413
+ padding: 2rem;
414
+ background: var(--gray-50);
415
+ border-radius: 1rem;
416
+ transition: all 0.3s;
417
+ }
418
+
419
+ .feature-card:hover {
420
+ transform: translateY(-4px);
421
+ box-shadow: 0 12px 32px rgba(0, 0, 0, 0.08);
422
+ }
423
+
424
+ .feature-icon {
425
+ width: 48px;
426
+ height: 48px;
427
+ display: flex;
428
+ align-items: center;
429
+ justify-content: center;
430
+ background: linear-gradient(135deg, var(--primary), var(--secondary));
431
+ color: white;
432
+ border-radius: 0.75rem;
433
+ margin-bottom: 1.5rem;
434
+ }
435
+
436
+ .feature-card h3 {
437
+ font-size: 1.25rem;
438
+ font-weight: 700;
439
+ color: var(--gray-900);
440
+ margin-bottom: 0.5rem;
441
+ }
442
+
443
+ .feature-card p {
444
+ color: var(--gray-600);
445
+ line-height: 1.6;
446
+ }
447
+
448
+ /* How It Works */
449
+ .how-it-works {
450
+ padding: 6rem 2rem;
451
+ background: linear-gradient(180deg, white 0%, var(--gray-50) 100%);
452
+ }
453
+
454
+ .steps {
455
+ display: grid;
456
+ grid-template-columns: repeat(3, 1fr);
457
+ gap: 3rem;
458
+ }
459
+
460
+ .step {
461
+ display: flex;
462
+ flex-direction: column;
463
+ align-items: center;
464
+ text-align: center;
465
+ }
466
+
467
+ .step-number {
468
+ width: 64px;
469
+ height: 64px;
470
+ display: flex;
471
+ align-items: center;
472
+ justify-content: center;
473
+ background: linear-gradient(135deg, var(--primary), var(--secondary));
474
+ color: white;
475
+ font-size: 1.75rem;
476
+ font-weight: 700;
477
+ border-radius: 50%;
478
+ margin-bottom: 1.5rem;
479
+ }
480
+
481
+ .step-content h3 {
482
+ font-size: 1.5rem;
483
+ font-weight: 700;
484
+ color: var(--gray-900);
485
+ margin-bottom: 0.75rem;
486
+ }
487
+
488
+ .step-content p {
489
+ color: var(--gray-600);
490
+ line-height: 1.6;
491
+ }
492
+
493
+ /* Chat Overlay */
494
+ .chat-overlay {
495
+ position: fixed;
496
+ top: 0;
497
+ left: 0;
498
+ right: 0;
499
+ bottom: 0;
500
+ background: rgba(0, 0, 0, 0.5);
501
+ backdrop-filter: blur(4px);
502
+ z-index: 200;
503
+ display: none;
504
+ align-items: center;
505
+ justify-content: center;
506
+ padding: 2rem;
507
+ }
508
+
509
+ .chat-overlay.active {
510
+ display: flex;
511
+ }
512
+
513
+ .chat-container {
514
+ width: 100%;
515
+ max-width: 900px;
516
+ height: 90vh;
517
+ background: white;
518
+ border-radius: 1.5rem;
519
+ box-shadow: 0 24px 64px rgba(0, 0, 0, 0.2);
520
+ display: flex;
521
+ flex-direction: column;
522
+ overflow: hidden;
523
+ }
524
+
525
+ .chat-header {
526
+ display: flex;
527
+ justify-content: space-between;
528
+ align-items: center;
529
+ padding: 1.5rem 2rem;
530
+ border-bottom: 1px solid var(--gray-200);
531
+ background: var(--gray-50);
532
+ }
533
+
534
+ .chat-header-left {
535
+ display: flex;
536
+ align-items: center;
537
+ gap: 1rem;
538
+ }
539
+
540
+ .status-indicator {
541
+ width: 12px;
542
+ height: 12px;
543
+ background: var(--success);
544
+ border-radius: 50%;
545
+ animation: pulse 2s infinite;
546
+ }
547
+
548
+ @keyframes pulse {
549
+ 0%, 100% { opacity: 1; }
550
+ 50% { opacity: 0.5; }
551
+ }
552
+
553
+ .chat-header h3 {
554
+ font-size: 1.125rem;
555
+ font-weight: 700;
556
+ color: var(--gray-900);
557
+ margin-bottom: 0.125rem;
558
+ }
559
+
560
+ .chat-subtitle {
561
+ font-size: 0.875rem;
562
+ color: var(--gray-500);
563
+ }
564
+
565
+ /* Messages */
566
+ .messages-container {
567
+ flex: 1;
568
+ overflow-y: auto;
569
+ padding: 2rem;
570
+ background: white;
571
+ }
572
+
573
+ .welcome-message {
574
+ text-align: center;
575
+ padding: 3rem 2rem;
576
+ }
577
+
578
+ .welcome-icon {
579
+ display: inline-block;
580
+ margin-bottom: 1.5rem;
581
+ }
582
+
583
+ .welcome-message h3 {
584
+ font-size: 1.5rem;
585
+ font-weight: 700;
586
+ color: var(--gray-900);
587
+ margin-bottom: 0.5rem;
588
+ }
589
+
590
+ .welcome-message p {
591
+ color: var(--gray-600);
592
+ margin-bottom: 2rem;
593
+ }
594
+
595
+ .example-questions {
596
+ max-width: 600px;
597
+ margin: 0 auto;
598
+ }
599
+
600
+ .example-label {
601
+ font-weight: 600;
602
+ color: var(--gray-700);
603
+ margin-bottom: 1rem;
604
+ }
605
+
606
+ .example-chips {
607
+ display: flex;
608
+ flex-wrap: wrap;
609
+ gap: 0.75rem;
610
+ justify-content: center;
611
+ }
612
+
613
+ .chip {
614
+ padding: 0.5rem 1rem;
615
+ background: var(--gray-100);
616
+ border-radius: 2rem;
617
+ font-size: 0.875rem;
618
+ color: var(--gray-700);
619
+ }
620
+
621
+ .message {
622
+ margin-bottom: 1.5rem;
623
+ animation: fadeIn 0.3s;
624
+ }
625
+
626
+ @keyframes fadeIn {
627
+ from {
628
+ opacity: 0;
629
+ transform: translateY(10px);
630
+ }
631
+ to {
632
+ opacity: 1;
633
+ transform: translateY(0);
634
+ }
635
+ }
636
+
637
+ .message-header {
638
+ font-weight: 600;
639
+ margin-bottom: 0.5rem;
640
+ display: flex;
641
+ align-items: center;
642
+ gap: 0.5rem;
643
+ font-size: 0.875rem;
644
+ }
645
+
646
+ .message-content {
647
+ padding: 1rem 1.25rem;
648
+ border-radius: 1rem;
649
+ line-height: 1.6;
650
+ }
651
+
652
+ .message.user .message-header {
653
+ color: var(--primary);
654
+ }
655
+
656
+ .message.user .message-content {
657
+ background: linear-gradient(135deg, var(--primary), var(--secondary));
658
+ color: white;
659
+ margin-left: auto;
660
+ max-width: 70%;
661
+ border-bottom-right-radius: 0.25rem;
662
+ }
663
+
664
+ .message.assistant .message-header {
665
+ color: var(--gray-700);
666
+ }
667
+
668
+ .message.assistant .message-content {
669
+ background: var(--gray-100);
670
+ color: var(--gray-900);
671
+ max-width: 85%;
672
+ border-bottom-left-radius: 0.25rem;
673
+ }
674
+
675
+ .sources {
676
+ margin-top: 1rem;
677
+ padding: 1rem;
678
+ background: white;
679
+ border: 1px solid var(--gray-200);
680
+ border-radius: 0.75rem;
681
+ font-size: 0.875rem;
682
+ }
683
+
684
+ .sources-header {
685
+ font-weight: 600;
686
+ margin-bottom: 0.75rem;
687
+ color: var(--gray-700);
688
+ }
689
+
690
+ .source-item {
691
+ margin: 0.5rem 0;
692
+ padding: 0.75rem;
693
+ background: var(--gray-50);
694
+ border-radius: 0.5rem;
695
+ font-size: 0.8125rem;
696
+ color: var(--gray-600);
697
+ border-left: 3px solid var(--primary);
698
+ line-height: 1.5;
699
+ }
700
+
701
+ /* Input Container */
702
+ .input-container {
703
+ padding: 1.5rem 2rem;
704
+ border-top: 1px solid var(--gray-200);
705
+ background: white;
706
+ }
707
+
708
+ .input-wrapper {
709
+ display: flex;
710
+ gap: 0.75rem;
711
+ align-items: center;
712
+ }
713
+
714
+ .chat-input {
715
+ flex: 1;
716
+ padding: 0.875rem 1.25rem;
717
+ border: 2px solid var(--gray-200);
718
+ border-radius: 2rem;
719
+ font-size: 0.9375rem;
720
+ font-family: inherit;
721
+ transition: all 0.2s;
722
+ }
723
+
724
+ .chat-input:focus {
725
+ outline: none;
726
+ border-color: var(--primary);
727
+ box-shadow: 0 0 0 3px rgba(79, 70, 229, 0.1);
728
+ }
729
+
730
+ .btn-send {
731
+ width: 48px;
732
+ height: 48px;
733
+ display: flex;
734
+ align-items: center;
735
+ justify-content: center;
736
+ background: linear-gradient(135deg, var(--primary), var(--secondary));
737
+ color: white;
738
+ border: none;
739
+ border-radius: 50%;
740
+ cursor: pointer;
741
+ transition: all 0.2s;
742
+ }
743
+
744
+ .btn-send:hover:not(:disabled) {
745
+ transform: scale(1.05);
746
+ box-shadow: 0 4px 12px rgba(79, 70, 229, 0.4);
747
+ }
748
+
749
+ .btn-send:disabled {
750
+ opacity: 0.5;
751
+ cursor: not-allowed;
752
+ }
753
+
754
+ .input-hint {
755
+ margin-top: 0.75rem;
756
+ font-size: 0.8125rem;
757
+ color: var(--gray-500);
758
+ text-align: center;
759
+ }
760
+
761
+ /* Loading Overlay */
762
+ .loading-overlay {
763
+ position: fixed;
764
+ top: 0;
765
+ left: 0;
766
+ right: 0;
767
+ bottom: 0;
768
+ background: rgba(0, 0, 0, 0.75);
769
+ backdrop-filter: blur(4px);
770
+ display: none;
771
+ align-items: center;
772
+ justify-content: center;
773
+ z-index: 300;
774
+ }
775
+
776
+ .loading-overlay.active {
777
+ display: flex;
778
+ }
779
+
780
+ .loading-content {
781
+ text-align: center;
782
+ }
783
+
784
+ .spinner {
785
+ width: 64px;
786
+ height: 64px;
787
+ border: 4px solid rgba(255, 255, 255, 0.2);
788
+ border-top: 4px solid white;
789
+ border-radius: 50%;
790
+ animation: spin 1s linear infinite;
791
+ margin: 0 auto 1.5rem;
792
+ }
793
+
794
+ @keyframes spin {
795
+ 0% { transform: rotate(0deg); }
796
+ 100% { transform: rotate(360deg); }
797
+ }
798
+
799
+ .loading-overlay p {
800
+ color: white;
801
+ font-size: 1.125rem;
802
+ font-weight: 500;
803
+ }
804
+
805
+ /* Footer */
806
+ .footer {
807
+ padding: 3rem 2rem;
808
+ background: var(--gray-900);
809
+ color: white;
810
+ }
811
+
812
+ .footer-container {
813
+ max-width: 1280px;
814
+ margin: 0 auto;
815
+ }
816
+
817
+ .footer-content {
818
+ text-align: center;
819
+ }
820
+
821
+ .footer-logo {
822
+ display: inline-flex;
823
+ align-items: center;
824
+ gap: 0.75rem;
825
+ font-weight: 700;
826
+ font-size: 1.25rem;
827
+ margin-bottom: 1rem;
828
+ }
829
+
830
+ .footer-text {
831
+ color: var(--gray-400);
832
+ margin-bottom: 0.5rem;
833
+ }
834
+
835
+ .footer-copyright {
836
+ color: var(--gray-500);
837
+ font-size: 0.875rem;
838
+ }
839
+
840
+ /* Responsive */
841
+ @media (max-width: 1024px) {
842
+ .hero-container {
843
+ grid-template-columns: 1fr;
844
+ gap: 3rem;
845
+ }
846
+
847
+ .hero-title {
848
+ font-size: 2.5rem;
849
+ }
850
+
851
+ .features-grid {
852
+ grid-template-columns: repeat(2, 1fr);
853
+ }
854
+
855
+ .steps {
856
+ grid-template-columns: 1fr;
857
+ }
858
+ }
859
+
860
+ @media (max-width: 768px) {
861
+ .nav-links {
862
+ display: none;
863
+ }
864
+
865
+ .hero {
866
+ padding: 2rem 1rem;
867
+ }
868
+
869
+ .hero-title {
870
+ font-size: 2rem;
871
+ }
872
+
873
+ .hero-description {
874
+ font-size: 1rem;
875
+ }
876
+
877
+ .stats {
878
+ gap: 1.5rem;
879
+ }
880
+
881
+ .features-grid {
882
+ grid-template-columns: 1fr;
883
+ }
884
+
885
+ .section-header h2 {
886
+ font-size: 1.875rem;
887
+ }
888
+
889
+ .chat-container {
890
+ height: 100vh;
891
+ border-radius: 0;
892
+ }
893
+
894
+ .message.user .message-content,
895
+ .message.assistant .message-content {
896
+ max-width: 90%;
897
+ }
898
+ }
render.yaml ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ services:
2
+ - type: web
3
+ name: docling-rag-app
4
+ runtime: python
5
+ buildCommand: pip install -r requirements.txt
6
+ startCommand: cd backend && uvicorn main:app --host 0.0.0.0 --port $PORT
7
+ envVars:
8
+ - key: PYTHON_VERSION
9
+ value: 3.11.0
requirements.txt ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ fastapi==0.109.0
2
+ uvicorn[standard]==0.27.0
3
+ python-multipart==0.0.9
4
+ docling==2.15.0
5
+ chromadb==0.4.22
6
+ sentence-transformers==2.3.1
7
+ google-generativeai==0.3.2
8
+ pypdf==4.0.1
9
+ python-dotenv==1.0.1
10
+ numpy<2.0.0
run.bat ADDED
@@ -0,0 +1,32 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ @echo off
2
+
3
+ REM Docling RAG Webapp - Run Script (Windows)
4
+
5
+ echo πŸš€ Starting Docling RAG Webapp...
6
+ echo.
7
+
8
+ REM Check if venv exists
9
+ if not exist "venv" (
10
+ echo πŸ“¦ Creating virtual environment...
11
+ python -m venv venv
12
+ )
13
+
14
+ REM Activate venv
15
+ echo πŸ”„ Activating virtual environment...
16
+ call venv\Scripts\activate.bat
17
+
18
+ REM Install dependencies
19
+ echo πŸ“₯ Installing dependencies...
20
+ pip install -q -r requirements.txt
21
+
22
+ REM Check if embedding model needs to be downloaded
23
+ echo πŸ€– Checking embedding model...
24
+
25
+ REM Run the server
26
+ echo.
27
+ echo βœ… Starting server...
28
+ echo 🌐 Open http://localhost:8001 in your browser
29
+ echo.
30
+ cd backend
31
+ set PORT=8001
32
+ python main.py
run.sh ADDED
@@ -0,0 +1,31 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/bin/bash
2
+
3
+ # Docling RAG Webapp - Run Script
4
+
5
+ echo "πŸš€ Starting Docling RAG Webapp..."
6
+ echo ""
7
+
8
+ # Check if venv exists
9
+ if [ ! -d "venv" ]; then
10
+ echo "πŸ“¦ Creating virtual environment..."
11
+ python3 -m venv venv
12
+ fi
13
+
14
+ # Activate venv
15
+ echo "πŸ”„ Activating virtual environment..."
16
+ source venv/bin/activate
17
+
18
+ # Install dependencies
19
+ echo "πŸ“₯ Installing dependencies..."
20
+ pip install -q -r requirements.txt
21
+
22
+ # Check if embedding model needs to be downloaded
23
+ echo "πŸ€– Checking embedding model..."
24
+ python -c "from sentence_transformers import SentenceTransformer; SentenceTransformer('sentence-transformers/all-MiniLM-L6-v2')" 2>/dev/null || echo "Downloading embedding model (first time only)..."
25
+
26
+ # Run the server
27
+ echo ""
28
+ echo "βœ… Starting server..."
29
+ echo "🌐 Open http://localhost:8001 in your browser"
30
+ echo ""
31
+ cd backend && PORT=8001 python main.py