berohan commited on
Commit
e413948
·
verified ·
1 Parent(s): a7ac7bf

Upload 19 files

Browse files
.env.example ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ GROQ_API_KEY=your_groq_api_key_here
2
+ PORT=7860
3
+ HOST=0.0.0.0
Dockerfile ADDED
@@ -0,0 +1,29 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM python:3.11-slim
2
+
3
+ # Set up a new user named "user" with user ID 1000
4
+ RUN useradd -m -u 1000 user
5
+
6
+ WORKDIR /app
7
+
8
+ RUN apt-get update && apt-get install -y \
9
+ gcc \
10
+ g++ \
11
+ && rm -rf /var/lib/apt/lists/*
12
+
13
+ COPY requirements.txt .
14
+ RUN pip install --no-cache-dir -r requirements.txt
15
+
16
+ COPY . .
17
+
18
+ RUN mkdir -p uploads static/css static/js && chown -R user:user /app
19
+
20
+ # Switch to the "user" user
21
+ USER user
22
+
23
+ # Set environment variables
24
+ ENV HOME=/home/user \
25
+ PATH=/home/user/.local/bin:$PATH
26
+
27
+ EXPOSE 7860
28
+
29
+ CMD ["python", "-m", "uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "7860"]
README.md ADDED
@@ -0,0 +1,234 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Studyson - RAG Document QA & Summarization API
2
+
3
+ A full-stack Retrieval-Augmented Generation (RAG) system for intelligent document question-answering and summarization. Built with FastAPI, LlamaIndex, and Groq AI.
4
+
5
+ ## Features
6
+
7
+ - **📄 PDF Document Processing**: Upload and index PDF documents with intelligent text extraction
8
+ - **🌐 Web Content Scraping**: Scrape and index content from URLs
9
+ - **💬 Interactive Q&A Chat**: Ask questions about your documents with streaming responses
10
+ - **📝 Smart Summarization**: Generate concise summaries of indexed documents
11
+ - **🔍 Source Citations**: Get verifiable citations with exact source snippets
12
+ - **⚡ Real-time Streaming**: Token-by-token streaming for responsive user experience
13
+ - **🎨 Modern UI**: Clean, responsive web interface with tabbed navigation
14
+ - **🐳 Docker Support**: Easy deployment with Docker and Docker Compose
15
+
16
+ ## Tech Stack
17
+
18
+ ### Backend
19
+ - **FastAPI**: Modern Python web framework
20
+ - **LlamaIndex**: RAG orchestration and document indexing
21
+ - **Groq**: Lightning-fast LLM inference (Llama 3.1)
22
+ - **FastEmbed**: Lightweight embeddings (BGE-small)
23
+ - **PyMuPDF**: Advanced PDF text extraction
24
+ - **BeautifulSoup**: HTML parsing and web scraping
25
+ - **Pydantic**: Data validation and settings management
26
+
27
+ ### Frontend
28
+ - **HTML5/CSS3/JavaScript**: Vanilla web technologies
29
+ - **Server-Sent Events (SSE)**: Real-time streaming responses
30
+
31
+ ## Architecture
32
+
33
+ ### Ingestion Pipeline
34
+ 1. User uploads PDF or provides URL
35
+ 2. Content extraction (PyMuPDF for PDFs, BeautifulSoup for web)
36
+ 3. Text chunking and embedding via LlamaIndex + FastEmbed
37
+ 4. In-memory vector index creation
38
+
39
+ ### Query Pipeline
40
+ 1. Question embedding generation
41
+ 2. Semantic similarity search for relevant chunks
42
+ 3. Context + question sent to Groq LLM
43
+ 4. Streaming response with source citations
44
+
45
+ ## Installation
46
+
47
+ ### Prerequisites
48
+ - Python 3.10 or higher
49
+ - Groq API key ([Get it free here](https://console.groq.com))
50
+
51
+ ### Local Setup
52
+
53
+ 1. **Clone the repository**
54
+ ```bash
55
+ git clone <repository-url>
56
+ cd studyrag
57
+ ```
58
+
59
+ 2. **Create virtual environment**
60
+ ```bash
61
+ python -m venv venv
62
+ source venv/bin/activate # On Windows: venv\Scripts\activate
63
+ ```
64
+
65
+ 3. **Install dependencies**
66
+ ```bash
67
+ pip install -r requirements.txt
68
+ ```
69
+
70
+ 4. **Set up environment variables**
71
+ ```bash
72
+ cp .env.example .env
73
+ ```
74
+
75
+ Edit `.env` and add your Groq API key:
76
+ ```
77
+ GROQ_API_KEY=your_groq_api_key_here
78
+ PORT=7860
79
+ HOST=0.0.0.0
80
+ ```
81
+
82
+ 5. **Run the application**
83
+ ```bash
84
+ uvicorn app.main:app --reload --port 7860
85
+ ```
86
+
87
+ 6. **Access the application**
88
+
89
+ Open your browser and navigate to: `http://localhost:7860`
90
+
91
+ ### Docker Setup
92
+
93
+ 1. **Set environment variables**
94
+ ```bash
95
+ cp .env.example .env
96
+ # Edit .env with your Groq API key
97
+ ```
98
+
99
+ 2. **Build and run with Docker Compose**
100
+ ```bash
101
+ docker-compose up --build
102
+ ```
103
+
104
+ ## API Endpoints
105
+
106
+ | Method | Endpoint | Description |
107
+ |--------|----------|-------------|
108
+ | GET | `/` | Serves the web UI |
109
+ | POST | `/upload` | Upload PDF document |
110
+ | POST | `/scrape` | Scrape URL content |
111
+ | POST | `/stream_query` | Stream Q&A response |
112
+ | POST | `/query` | Get Q&A response |
113
+ | POST | `/summarize` | Generate summary |
114
+ | POST | `/reset` | Clear all documents |
115
+ | GET | `/status` | Get system status |
116
+
117
+ ## Project Structure
118
+
119
+ ```
120
+ studyrag/
121
+ ├── app/
122
+ │ ├── __init__.py
123
+ │ ├── main.py # FastAPI application
124
+ │ ├── config.py # Configuration settings
125
+ │ ├── models/
126
+ │ │ └── schemas.py # Pydantic models
127
+ │ ├── services/
128
+ │ │ └── rag_service.py # RAG logic
129
+ │ └── utils/
130
+ │ └── document_processor.py
131
+ ├── static/
132
+ │ ├── css/style.css
133
+ │ ├── js/app.js
134
+ │ └── index.html
135
+ ├── .env.example
136
+ ├── .gitignore
137
+ ├── Dockerfile
138
+ ├── docker-compose.yml
139
+ ├── Procfile
140
+ ├── requirements.txt
141
+ └── README.md
142
+ ```
143
+
144
+ ## Configuration
145
+
146
+ ### Environment Variables
147
+
148
+ - `GROQ_API_KEY`: Your Groq API key (required, free tier available)
149
+ - `HOST`: Server host (default: 0.0.0.0)
150
+ - `PORT`: Server port (default: 7860)
151
+
152
+ ### Application Settings
153
+
154
+ Edit `app/config.py` to modify:
155
+ - `upload_dir`: Upload directory path
156
+ - `max_file_size`: Maximum file size (default: 10MB)
157
+
158
+ ## Deployment
159
+
160
+ ### Deploy to Hugging Face Spaces (Recommended - Free)
161
+
162
+ 1. Push code to GitHub
163
+ 2. Go to [huggingface.co](https://huggingface.co) and create an account
164
+ 3. Click your profile → **New Space**
165
+ 4. Configure:
166
+ - **Space name**: `studyson`
167
+ - **SDK**: Select **Docker**
168
+ - **Hardware**: CPU basic (free)
169
+ 5. Under **Files** → Link to GitHub repo (or upload files)
170
+ 6. Add secret: `GROQ_API_KEY` in Space Settings → Variables
171
+ 7. The Space will auto-build and deploy!
172
+
173
+ **Your app will be live at:** `https://huggingface.co/spaces/YOUR_USERNAME/studyson`
174
+
175
+ ## Features in Detail
176
+
177
+ ### RAG Pipeline
178
+ - **Chunking**: Intelligent text splitting for optimal context windows
179
+ - **Embeddings**: FastEmbed BGE-small for semantic understanding (lightweight)
180
+ - **Retrieval**: Top-k similarity search with configurable parameters
181
+ - **Generation**: Groq Llama 3.1 for fast, accurate responses
182
+
183
+ ### Streaming
184
+ - Server-Sent Events (SSE) for real-time token delivery
185
+ - Progressive rendering in the UI
186
+ - Graceful error handling
187
+
188
+ ### Source Attribution
189
+ - Exact text snippets from source documents
190
+ - Similarity scores for transparency
191
+ - Multiple source support per answer
192
+
193
+ ## Limitations
194
+
195
+ - In-memory vector storage (resets on restart)
196
+ - PDF-only document support (extensible to other formats)
197
+ - Single-user session management
198
+ - No authentication/authorization
199
+
200
+ ## Troubleshooting
201
+
202
+ ### Common Issues
203
+
204
+ **Import errors:**
205
+ ```bash
206
+ pip install --upgrade -r requirements.txt
207
+ ```
208
+
209
+ **API key errors:**
210
+ - Verify your `.env` file has the correct `GROQ_API_KEY`
211
+ - Check API key validity at [console.groq.com](https://console.groq.com)
212
+
213
+ **Port already in use:**
214
+ ```bash
215
+ uvicorn app.main:app --port 8000
216
+ ```
217
+
218
+ **File upload fails:**
219
+ - Check file size is under 10MB
220
+
221
+ ## License
222
+
223
+ MIT License - feel free to use this project for learning and development.
224
+
225
+ ## Acknowledgments
226
+
227
+ - [LlamaIndex](https://www.llamaindex.ai/) for RAG orchestration
228
+ - [Groq](https://groq.com/) for lightning-fast LLM inference
229
+ - [FastEmbed](https://github.com/qdrant/fastembed) for lightweight embeddings
230
+ - [FastAPI](https://fastapi.tiangolo.com/) for the web framework
231
+
232
+ ---
233
+
234
+ Built with ❤️ using RAG technology
__init__.cpython-313.pyc ADDED
Binary file (170 Bytes). View file
 
__init__.py ADDED
File without changes
app.js ADDED
@@ -0,0 +1,390 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ const API_BASE = '';
2
+
3
+ // Navigation
4
+ const navItems = document.querySelectorAll('.nav-item');
5
+ const viewContainers = document.querySelectorAll('.view-container');
6
+ const currentViewName = document.getElementById('current-view-name');
7
+
8
+ const viewNames = {
9
+ 'upload': 'Upload Files',
10
+ 'web': 'Web Import',
11
+ 'chat': 'Q&A Chat',
12
+ 'summary': 'Summarize'
13
+ };
14
+
15
+ navItems.forEach(item => {
16
+ item.addEventListener('click', () => {
17
+ const viewId = item.dataset.view;
18
+ switchView(viewId);
19
+ });
20
+ });
21
+
22
+ function switchView(viewId) {
23
+ navItems.forEach(n => n.classList.remove('active'));
24
+ viewContainers.forEach(v => v.classList.remove('active'));
25
+
26
+ const activeNav = document.querySelector(`[data-view="${viewId}"]`);
27
+ const activeView = document.getElementById(`view-${viewId}`);
28
+
29
+ if (activeNav) activeNav.classList.add('active');
30
+ if (activeView) activeView.classList.add('active');
31
+ if (currentViewName) currentViewName.textContent = viewNames[viewId] || viewId;
32
+ }
33
+
34
+ // File Upload
35
+ const fileInput = document.getElementById('file-input');
36
+ const dropZone = document.querySelector('.drop-zone');
37
+ const dropZoneTitle = document.getElementById('drop-zone-title');
38
+ const fileInfo = document.getElementById('file-info');
39
+ const fileNameDisplay = document.getElementById('file-name-display');
40
+ const fileSizeDisplay = document.getElementById('file-size-display');
41
+ const uploadForm = document.getElementById('upload-form');
42
+ const uploadResult = document.getElementById('upload-result');
43
+
44
+ fileInput.addEventListener('change', handleFileSelect);
45
+
46
+ function handleFileSelect(e) {
47
+ const file = e.target.files[0];
48
+ if (file) {
49
+ fileNameDisplay.textContent = file.name;
50
+ fileSizeDisplay.textContent = formatFileSize(file.size);
51
+ dropZoneTitle.textContent = 'File selected';
52
+ fileInfo.style.display = 'flex';
53
+ }
54
+ }
55
+
56
+ function formatFileSize(bytes) {
57
+ if (bytes === 0) return '0 Bytes';
58
+ const k = 1024;
59
+ const sizes = ['Bytes', 'KB', 'MB', 'GB'];
60
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
61
+ return Math.round((bytes / Math.pow(k, i)) * 100) / 100 + ' ' + sizes[i];
62
+ }
63
+
64
+ uploadForm.addEventListener('submit', async (e) => {
65
+ e.preventDefault();
66
+ const file = fileInput.files[0];
67
+
68
+ if (!file) {
69
+ showResult(uploadResult, 'Please select a file', 'error');
70
+ return;
71
+ }
72
+
73
+ const formData = new FormData();
74
+ formData.append('file', file);
75
+
76
+ const submitBtn = uploadForm.querySelector('button[type="submit"]');
77
+ const originalText = submitBtn.innerHTML;
78
+ submitBtn.disabled = true;
79
+ submitBtn.innerHTML = '<span class="loading"></span><span>Uploading...</span>';
80
+
81
+ try {
82
+ const response = await fetch(`${API_BASE}/upload`, {
83
+ method: 'POST',
84
+ body: formData
85
+ });
86
+
87
+ const data = await response.json();
88
+
89
+ if (response.ok) {
90
+ showResult(uploadResult, data.message, 'success');
91
+ fileInput.value = '';
92
+ fileInfo.style.display = 'none';
93
+ dropZoneTitle.textContent = 'Drop your PDF here';
94
+ await updateStatus();
95
+ } else {
96
+ showResult(uploadResult, data.detail || 'Upload failed', 'error');
97
+ }
98
+ } catch (error) {
99
+ showResult(uploadResult, `Error: ${error.message}`, 'error');
100
+ } finally {
101
+ submitBtn.disabled = false;
102
+ submitBtn.innerHTML = originalText;
103
+ }
104
+ });
105
+
106
+ // Web Scrape
107
+ const scrapeForm = document.getElementById('scrape-form');
108
+ const scrapeResult = document.getElementById('scrape-result');
109
+
110
+ scrapeForm.addEventListener('submit', async (e) => {
111
+ e.preventDefault();
112
+ const url = document.getElementById('url-input').value;
113
+
114
+ const submitBtn = scrapeForm.querySelector('button[type="submit"]');
115
+ const originalText = submitBtn.innerHTML;
116
+ submitBtn.disabled = true;
117
+ submitBtn.innerHTML = '<span class="loading"></span><span>Fetching...</span>';
118
+
119
+ try {
120
+ const response = await fetch(`${API_BASE}/scrape_and_index`, {
121
+ method: 'POST',
122
+ headers: { 'Content-Type': 'application/json' },
123
+ body: JSON.stringify({ url })
124
+ });
125
+
126
+ const data = await response.json();
127
+
128
+ if (response.ok) {
129
+ showResult(scrapeResult, data.message, 'success');
130
+ document.getElementById('url-input').value = '';
131
+ await updateStatus();
132
+ } else {
133
+ showResult(scrapeResult, data.detail || 'Scraping failed', 'error');
134
+ }
135
+ } catch (error) {
136
+ showResult(scrapeResult, `Error: ${error.message}`, 'error');
137
+ } finally {
138
+ submitBtn.disabled = false;
139
+ submitBtn.innerHTML = originalText;
140
+ }
141
+ });
142
+
143
+ // Chat
144
+ const chatForm = document.getElementById('chat-form');
145
+ const chatMessages = document.getElementById('chat-messages');
146
+ const questionInput = document.getElementById('question-input');
147
+
148
+ chatForm.addEventListener('submit', async (e) => {
149
+ e.preventDefault();
150
+ const question = questionInput.value.trim();
151
+
152
+ if (!question) return;
153
+
154
+ const emptyState = chatMessages.querySelector('.empty-state');
155
+ if (emptyState) emptyState.remove();
156
+
157
+ addMessage(question, 'user');
158
+ questionInput.value = '';
159
+
160
+ const assistantMessage = addMessage('', 'assistant');
161
+ const messageContent = assistantMessage.querySelector('.message-content');
162
+
163
+ const submitBtn = chatForm.querySelector('button[type="submit"]');
164
+ submitBtn.disabled = true;
165
+
166
+ try {
167
+ const response = await fetch(`${API_BASE}/stream_query`, {
168
+ method: 'POST',
169
+ headers: { 'Content-Type': 'application/json' },
170
+ body: JSON.stringify({ question })
171
+ });
172
+
173
+ if (!response.ok) {
174
+ const error = await response.json();
175
+ messageContent.textContent = `Error: ${error.detail}`;
176
+ return;
177
+ }
178
+
179
+ const reader = response.body.getReader();
180
+ const decoder = new TextDecoder();
181
+ let buffer = '';
182
+ let fullAnswer = '';
183
+
184
+ while (true) {
185
+ const { done, value } = await reader.read();
186
+ if (done) break;
187
+
188
+ buffer += decoder.decode(value, { stream: true });
189
+ const lines = buffer.split('\n');
190
+ buffer = lines.pop();
191
+
192
+ for (const line of lines) {
193
+ if (line.startsWith('data: ')) {
194
+ const data = line.slice(6);
195
+
196
+ if (data === '[DONE]') continue;
197
+
198
+ try {
199
+ const parsed = JSON.parse(data);
200
+
201
+ if (parsed.token) {
202
+ fullAnswer += parsed.token;
203
+ messageContent.textContent = fullAnswer;
204
+ chatMessages.scrollTop = chatMessages.scrollHeight;
205
+ } else if (parsed.final_answer) {
206
+ messageContent.textContent = parsed.final_answer;
207
+
208
+ if (parsed.sources && parsed.sources.length > 0) {
209
+ const sourcesDiv = document.createElement('div');
210
+ sourcesDiv.className = 'message-sources';
211
+ sourcesDiv.innerHTML = '<strong>📚 Sources:</strong>';
212
+
213
+ parsed.sources.forEach((source, idx) => {
214
+ const sourceItem = document.createElement('div');
215
+ sourceItem.className = 'source-item';
216
+ sourceItem.innerHTML = `
217
+ <strong>${idx + 1}. ${source.file_name}</strong>
218
+ <p>${source.text}...</p>
219
+ `;
220
+ sourcesDiv.appendChild(sourceItem);
221
+ });
222
+
223
+ assistantMessage.querySelector('.message-bubble').appendChild(sourcesDiv);
224
+ }
225
+ } else if (parsed.error) {
226
+ messageContent.textContent = `Error: ${parsed.error}`;
227
+ }
228
+ } catch (e) {
229
+ console.error('Parse error:', e);
230
+ }
231
+ }
232
+ }
233
+ }
234
+ } catch (error) {
235
+ messageContent.textContent = `Error: ${error.message}`;
236
+ } finally {
237
+ submitBtn.disabled = false;
238
+ }
239
+ });
240
+
241
+ function addMessage(text, sender) {
242
+ const messageDiv = document.createElement('div');
243
+ messageDiv.className = `message ${sender}`;
244
+
245
+ const bubbleDiv = document.createElement('div');
246
+ bubbleDiv.className = 'message-bubble';
247
+
248
+ const contentDiv = document.createElement('div');
249
+ contentDiv.className = 'message-content';
250
+ contentDiv.textContent = text;
251
+
252
+ bubbleDiv.appendChild(contentDiv);
253
+ messageDiv.appendChild(bubbleDiv);
254
+ chatMessages.appendChild(messageDiv);
255
+ chatMessages.scrollTop = chatMessages.scrollHeight;
256
+
257
+ return messageDiv;
258
+ }
259
+
260
+ // Summary
261
+ const summarizeForm = document.getElementById('summarize-form');
262
+ const summaryResult = document.getElementById('summary-result');
263
+ const lengthSlider = document.getElementById('max-length');
264
+ const lengthDisplay = document.getElementById('length-display');
265
+
266
+ lengthSlider.addEventListener('input', (e) => {
267
+ lengthDisplay.textContent = e.target.value;
268
+ });
269
+
270
+ summarizeForm.addEventListener('submit', async (e) => {
271
+ e.preventDefault();
272
+ const maxLength = parseInt(lengthSlider.value);
273
+
274
+ const submitBtn = summarizeForm.querySelector('button[type="submit"]');
275
+ const originalText = submitBtn.innerHTML;
276
+ submitBtn.disabled = true;
277
+ submitBtn.innerHTML = '<span class="loading"></span><span>Generating...</span>';
278
+
279
+ showResult(summaryResult, 'Generating summary...', 'info');
280
+
281
+ try {
282
+ const response = await fetch(`${API_BASE}/summarize`, {
283
+ method: 'POST',
284
+ headers: { 'Content-Type': 'application/json' },
285
+ body: JSON.stringify({ max_length: maxLength })
286
+ });
287
+
288
+ const data = await response.json();
289
+
290
+ if (response.ok) {
291
+ const result = `
292
+ <h3>📄 Summary (${data.word_count} words)</h3>
293
+ <p>${data.summary}</p>
294
+ <p style="margin-top: 1rem; padding-top: 1rem; border-top: 1px solid var(--border); font-size: 0.9rem; color: var(--text-secondary);">
295
+ <strong>Sources:</strong> ${data.source_documents.join(', ')}
296
+ </p>
297
+ `;
298
+ summaryResult.innerHTML = result;
299
+ summaryResult.classList.add('show', 'success');
300
+ } else {
301
+ showResult(summaryResult, data.detail || 'Summarization failed', 'error');
302
+ }
303
+ } catch (error) {
304
+ showResult(summaryResult, `Error: ${error.message}`, 'error');
305
+ } finally {
306
+ submitBtn.disabled = false;
307
+ submitBtn.innerHTML = originalText;
308
+ }
309
+ });
310
+
311
+ // Reset
312
+ const resetBtn = document.getElementById('reset-btn-sidebar');
313
+
314
+ resetBtn.addEventListener('click', async () => {
315
+ if (!confirm('Are you sure you want to reset all documents? This action cannot be undone.')) {
316
+ return;
317
+ }
318
+
319
+ try {
320
+ const response = await fetch(`${API_BASE}/reset`, {
321
+ method: 'POST'
322
+ });
323
+
324
+ const data = await response.json();
325
+
326
+ if (response.ok) {
327
+ alert(data.message);
328
+ // Force page reload to clear all state
329
+ window.location.reload();
330
+ } else {
331
+ alert(data.detail || 'Reset failed');
332
+ }
333
+ } catch (error) {
334
+ alert(`Error: ${error.message}`);
335
+ }
336
+ });
337
+
338
+ // Helper Functions
339
+ function showResult(element, message, type) {
340
+ element.innerHTML = message;
341
+ element.className = `result-message show ${type}`;
342
+ setTimeout(() => {
343
+ element.classList.remove('show');
344
+ }, 5000);
345
+ }
346
+
347
+ function clearAllResults() {
348
+ uploadResult.classList.remove('show');
349
+ scrapeResult.classList.remove('show');
350
+ summaryResult.classList.remove('show');
351
+ }
352
+
353
+ async function updateStatus() {
354
+ try {
355
+ const response = await fetch(`${API_BASE}/status`);
356
+ const data = await response.json();
357
+
358
+ if (data.details) {
359
+ const count = data.details.document_count || 0;
360
+ const docCountSidebar = document.getElementById('doc-count-sidebar');
361
+ if (docCountSidebar) {
362
+ docCountSidebar.textContent = count;
363
+ }
364
+
365
+ const statusPulse = document.getElementById('status-pulse');
366
+ const statusTextSidebar = document.getElementById('status-text-sidebar');
367
+ const statusTextTop = document.getElementById('status-text-top');
368
+
369
+ if (data.details.has_documents) {
370
+ if (statusPulse) statusPulse.style.background = 'var(--success)';
371
+ if (statusTextSidebar) statusTextSidebar.textContent = 'Ready';
372
+ if (statusTextTop) statusTextTop.textContent = 'System Ready';
373
+ } else {
374
+ if (statusPulse) statusPulse.style.background = 'var(--text-light)';
375
+ if (statusTextSidebar) statusTextSidebar.textContent = 'No Docs';
376
+ if (statusTextTop) statusTextTop.textContent = 'No Documents';
377
+ }
378
+ }
379
+ } catch (error) {
380
+ console.error('Status update failed:', error);
381
+ const statusPulse = document.getElementById('status-pulse');
382
+ const statusTextTop = document.getElementById('status-text-top');
383
+ if (statusPulse) statusPulse.style.background = 'var(--danger)';
384
+ if (statusTextTop) statusTextTop.textContent = 'Connection Error';
385
+ }
386
+ }
387
+
388
+ // Initial status update
389
+ updateStatus();
390
+ setInterval(updateStatus, 30000);
config.cpython-313.pyc ADDED
Binary file (925 Bytes). View file
 
config.py ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from pydantic_settings import BaseSettings, SettingsConfigDict
2
+ from pathlib import Path
3
+
4
+
5
+ class Settings(BaseSettings):
6
+ groq_api_key: str
7
+ host: str = "0.0.0.0"
8
+ port: int = 7860 # HuggingFace Spaces default port
9
+ upload_dir: Path = Path("uploads")
10
+ max_file_size: int = 10 * 1024 * 1024
11
+
12
+ model_config = SettingsConfigDict(
13
+ env_file=".env",
14
+ env_file_encoding="utf-8",
15
+ case_sensitive=False
16
+ )
17
+
18
+
19
+ settings = Settings()
docker-compose.yml ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ version: '3.8'
2
+
3
+ services:
4
+ studyson:
5
+ build: .
6
+ ports:
7
+ - "7860:7860"
8
+ environment:
9
+ - GROQ_API_KEY=${GROQ_API_KEY}
10
+ - HOST=0.0.0.0
11
+ - PORT=7860
12
+ volumes:
13
+ - ./uploads:/app/uploads
14
+ restart: unless-stopped
document_processor.py ADDED
@@ -0,0 +1,56 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import fitz
2
+ from bs4 import BeautifulSoup
3
+ import aiohttp
4
+ from pathlib import Path
5
+ from typing import Optional
6
+
7
+
8
+ class DocumentProcessor:
9
+
10
+ @staticmethod
11
+ async def extract_pdf_text(file_path: Path) -> str:
12
+ doc = fitz.open(file_path)
13
+ text_parts = []
14
+
15
+ for page in doc:
16
+ text = page.get_text()
17
+ text_parts.append(text)
18
+
19
+ doc.close()
20
+ return "\n\n".join(text_parts)
21
+
22
+ @staticmethod
23
+ async def scrape_url(url: str) -> tuple[str, str]:
24
+ async with aiohttp.ClientSession() as session:
25
+ async with session.get(str(url)) as response:
26
+ html = await response.text()
27
+ soup = BeautifulSoup(html, 'html.parser')
28
+
29
+ for script in soup(["script", "style", "nav", "footer", "header"]):
30
+ script.decompose()
31
+
32
+ title = soup.find('title')
33
+ title_text = title.get_text().strip() if title else "Web Document"
34
+
35
+ text = soup.get_text(separator='\n', strip=True)
36
+
37
+ lines = [line.strip() for line in text.splitlines() if line.strip()]
38
+ cleaned_text = '\n'.join(lines)
39
+
40
+ return title_text, cleaned_text
41
+
42
+ @staticmethod
43
+ def validate_file_type(filename: str, allowed_extensions: set = {'.pdf'}) -> bool:
44
+ return Path(filename).suffix.lower() in allowed_extensions
45
+
46
+ @staticmethod
47
+ def clean_text(text: str) -> str:
48
+ lines = text.split('\n')
49
+ cleaned_lines = []
50
+
51
+ for line in lines:
52
+ line = line.strip()
53
+ if len(line) > 0:
54
+ cleaned_lines.append(line)
55
+
56
+ return '\n'.join(cleaned_lines)
index.html ADDED
@@ -0,0 +1,191 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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>Studyson - Smart Document Assistant</title>
7
+ <link rel="stylesheet" href="/static/css/style.css">
8
+ <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
9
+ </head>
10
+ <body>
11
+ <div class="app-layout">
12
+ <!-- Sidebar Navigation -->
13
+ <aside class="sidebar">
14
+ <div class="logo-section">
15
+ <div class="logo-icon">
16
+ <svg width="40" height="40" viewBox="0 0 40 40" fill="none">
17
+ <rect width="40" height="40" rx="12" fill="url(#grad1)"/>
18
+ <path d="M12 14h16M12 20h16M12 26h10" stroke="white" stroke-width="2.5" stroke-linecap="round"/>
19
+ <defs>
20
+ <linearGradient id="grad1" x1="0" y1="0" x2="40" y2="40">
21
+ <stop offset="0%" stop-color="#667eea"/>
22
+ <stop offset="100%" stop-color="#764ba2"/>
23
+ </linearGradient>
24
+ </defs>
25
+ </svg>
26
+ </div>
27
+ <h1 class="logo-text">Studyson</h1>
28
+ </div>
29
+
30
+ <nav class="nav-menu">
31
+ <button class="nav-item active" data-view="upload">
32
+ <span class="nav-icon">📁</span>
33
+ <span class="nav-label">Upload Files</span>
34
+ </button>
35
+ <button class="nav-item" data-view="web">
36
+ <span class="nav-icon">🌍</span>
37
+ <span class="nav-label">Web Import</span>
38
+ </button>
39
+ <button class="nav-item" data-view="chat">
40
+ <span class="nav-icon">💭</span>
41
+ <span class="nav-label">Q&A Chat</span>
42
+ </button>
43
+ <button class="nav-item" data-view="summary">
44
+ <span class="nav-icon">📊</span>
45
+ <span class="nav-label">Summarize</span>
46
+ </button>
47
+ </nav>
48
+
49
+ <div class="sidebar-footer">
50
+ <div class="stats-card">
51
+ <div class="stat-item">
52
+ <span class="stat-label">Documents</span>
53
+ <span class="stat-value" id="doc-count-sidebar">0</span>
54
+ </div>
55
+ <div class="stat-indicator">
56
+ <div class="pulse-dot" id="status-pulse"></div>
57
+ <span id="status-text-sidebar">Ready</span>
58
+ </div>
59
+ </div>
60
+ <button id="reset-btn-sidebar" class="btn-reset">
61
+ <span>🗑️</span>
62
+ <span>Clear All</span>
63
+ </button>
64
+ </div>
65
+ </aside>
66
+
67
+ <!-- Main Content Area -->
68
+ <main class="main-content">
69
+ <div class="topbar">
70
+ <div class="breadcrumb">
71
+ <span class="breadcrumb-item" id="current-view-name">Upload Files</span>
72
+ </div>
73
+ <div class="topbar-actions">
74
+ <div class="status-badge" id="status-badge">
75
+ <span class="status-dot"></span>
76
+ <span id="status-text-top">System Ready</span>
77
+ </div>
78
+ </div>
79
+ </div>
80
+
81
+ <!-- Upload View -->
82
+ <div class="view-container active" id="view-upload">
83
+ <div class="view-header">
84
+ <h2 class="view-title">Upload PDF Documents</h2>
85
+ <p class="view-description">Import your PDF files to create a searchable knowledge base</p>
86
+ </div>
87
+
88
+ <div class="content-card">
89
+ <form id="upload-form" class="upload-area">
90
+ <input type="file" id="file-input" accept=".pdf" hidden>
91
+ <label for="file-input" class="drop-zone">
92
+ <div class="drop-zone-icon">📄</div>
93
+ <h3 class="drop-zone-title" id="drop-zone-title">Drop your PDF here</h3>
94
+ <p class="drop-zone-desc">or click to browse files</p>
95
+ <div class="file-info" id="file-info" style="display: none;">
96
+ <span class="file-icon">📎</span>
97
+ <span class="file-name-display" id="file-name-display"></span>
98
+ <span class="file-size" id="file-size-display"></span>
99
+ </div>
100
+ </label>
101
+ <button type="submit" class="btn-primary btn-large">
102
+ <span>Upload & Index Document</span>
103
+ <span class="btn-arrow">→</span>
104
+ </button>
105
+ </form>
106
+ <div id="upload-result" class="result-message"></div>
107
+ </div>
108
+ </div>
109
+
110
+ <!-- Web Scrape View -->
111
+ <div class="view-container" id="view-web">
112
+ <div class="view-header">
113
+ <h2 class="view-title">Import from Web</h2>
114
+ <p class="view-description">Extract and index content from any webpage</p>
115
+ </div>
116
+
117
+ <div class="content-card">
118
+ <form id="scrape-form" class="web-form">
119
+ <div class="input-group">
120
+ <span class="input-icon">🔗</span>
121
+ <input type="url" id="url-input" class="input-field" placeholder="https://example.com/article" required>
122
+ </div>
123
+ <button type="submit" class="btn-primary btn-large">
124
+ <span>Fetch & Index Content</span>
125
+ <span class="btn-arrow">→</span>
126
+ </button>
127
+ </form>
128
+ <div id="scrape-result" class="result-message"></div>
129
+ </div>
130
+ </div>
131
+
132
+ <!-- Chat View -->
133
+ <div class="view-container" id="view-chat">
134
+ <div class="view-header">
135
+ <h2 class="view-title">Interactive Q&A</h2>
136
+ <p class="view-description">Ask questions and get intelligent answers from your documents</p>
137
+ </div>
138
+
139
+ <div class="chat-container">
140
+ <div class="messages-area" id="chat-messages">
141
+ <div class="empty-state">
142
+ <div class="empty-icon">💬</div>
143
+ <h3>Start a Conversation</h3>
144
+ <p>Ask me anything about your indexed documents</p>
145
+ </div>
146
+ </div>
147
+ <form id="chat-form" class="chat-input-area">
148
+ <div class="chat-input-wrapper">
149
+ <input type="text" id="question-input" class="chat-input" placeholder="Type your question here..." required>
150
+ <button type="submit" class="btn-send">
151
+ <svg width="24" height="24" viewBox="0 0 24 24" fill="none">
152
+ <path d="M22 2L11 13M22 2l-7 20-4-9-9-4 20-7z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
153
+ </svg>
154
+ </button>
155
+ </div>
156
+ </form>
157
+ </div>
158
+ </div>
159
+
160
+ <!-- Summary View -->
161
+ <div class="view-container" id="view-summary">
162
+ <div class="view-header">
163
+ <h2 class="view-title">Document Summarization</h2>
164
+ <p class="view-description">Generate comprehensive summaries of your indexed content</p>
165
+ </div>
166
+
167
+ <div class="content-card">
168
+ <form id="summarize-form" class="summary-form">
169
+ <div class="form-group">
170
+ <label class="form-label">Summary Length</label>
171
+ <div class="slider-container">
172
+ <input type="range" id="max-length" class="range-slider" min="100" max="2000" step="100" value="500">
173
+ <div class="slider-value">
174
+ <span id="length-display">500</span> words
175
+ </div>
176
+ </div>
177
+ </div>
178
+ <button type="submit" class="btn-primary btn-large">
179
+ <span>Generate Summary</span>
180
+ <span class="btn-arrow">✨</span>
181
+ </button>
182
+ </form>
183
+ <div id="summary-result" class="result-message summary-output"></div>
184
+ </div>
185
+ </div>
186
+ </main>
187
+ </div>
188
+
189
+ <script src="/static/js/app.js"></script>
190
+ </body>
191
+ </html>
main.cpython-313.pyc ADDED
Binary file (10.3 kB). View file
 
main.py ADDED
@@ -0,0 +1,212 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import FastAPI, UploadFile, File, HTTPException
2
+ from fastapi.responses import StreamingResponse, FileResponse
3
+ from fastapi.staticfiles import StaticFiles
4
+ from fastapi.middleware.cors import CORSMiddleware
5
+ from pathlib import Path
6
+ import json
7
+ from typing import AsyncGenerator
8
+
9
+ from app.config import settings
10
+ from app.models.schemas import (
11
+ ScrapeRequest, QueryRequest, FinalResponse,
12
+ StatusResponse, SummarizeRequest, SummaryResponse
13
+ )
14
+ from app.services.rag_service import RAGService
15
+ from app.utils.document_processor import DocumentProcessor
16
+
17
+ app = FastAPI(
18
+ title="Studyson RAG API",
19
+ description="Document QA and Summarization using RAG",
20
+ version="1.0.0"
21
+ )
22
+
23
+ app.add_middleware(
24
+ CORSMiddleware,
25
+ allow_origins=["*"],
26
+ allow_credentials=True,
27
+ allow_methods=["*"],
28
+ allow_headers=["*"],
29
+ )
30
+
31
+ rag_service = RAGService()
32
+ doc_processor = DocumentProcessor()
33
+
34
+ app.mount("/static", StaticFiles(directory="static"), name="static")
35
+
36
+
37
+ @app.get("/")
38
+ async def read_root():
39
+ return FileResponse("static/index.html")
40
+
41
+
42
+ @app.post("/upload", response_model=StatusResponse)
43
+ async def upload_document(file: UploadFile = File(...)):
44
+ if not doc_processor.validate_file_type(file.filename):
45
+ raise HTTPException(status_code=400, detail="Only PDF files are supported")
46
+
47
+ if file.size and file.size > settings.max_file_size:
48
+ raise HTTPException(status_code=400, detail="File size exceeds maximum limit")
49
+
50
+ settings.upload_dir.mkdir(exist_ok=True)
51
+ file_path = settings.upload_dir / file.filename
52
+
53
+ try:
54
+ with open(file_path, "wb") as buffer:
55
+ content = await file.read()
56
+ buffer.write(content)
57
+
58
+ text = await doc_processor.extract_pdf_text(file_path)
59
+ cleaned_text = doc_processor.clean_text(text)
60
+
61
+ rag_service.create_index_from_text(cleaned_text, file.filename)
62
+
63
+ return StatusResponse(
64
+ status="success",
65
+ message=f"Document '{file.filename}' uploaded and indexed successfully",
66
+ details={
67
+ "filename": file.filename,
68
+ "text_length": len(cleaned_text),
69
+ "indexed_documents": rag_service.get_indexed_documents()
70
+ }
71
+ )
72
+
73
+ except Exception as e:
74
+ if file_path.exists():
75
+ file_path.unlink()
76
+ raise HTTPException(status_code=500, detail=f"Error processing document: {str(e)}")
77
+
78
+
79
+ @app.post("/scrape_and_index", response_model=StatusResponse)
80
+ async def scrape_and_index(request: ScrapeRequest):
81
+ try:
82
+ title, text = await doc_processor.scrape_url(str(request.url))
83
+ cleaned_text = doc_processor.clean_text(text)
84
+
85
+ rag_service.create_index_from_text(cleaned_text, title)
86
+
87
+ return StatusResponse(
88
+ status="success",
89
+ message=f"URL content indexed successfully",
90
+ details={
91
+ "url": str(request.url),
92
+ "title": title,
93
+ "text_length": len(cleaned_text),
94
+ "indexed_documents": rag_service.get_indexed_documents()
95
+ }
96
+ )
97
+
98
+ except Exception as e:
99
+ raise HTTPException(status_code=500, detail=f"Error scraping URL: {str(e)}")
100
+
101
+
102
+ @app.post("/stream_query")
103
+ async def stream_query(request: QueryRequest):
104
+ if not rag_service.has_documents():
105
+ raise HTTPException(status_code=400, detail="No documents indexed. Please upload a document first.")
106
+
107
+ async def event_generator() -> AsyncGenerator[str, None]:
108
+ try:
109
+ answer_parts = []
110
+
111
+ async for token in rag_service.stream_query(request.question):
112
+ answer_parts.append(token)
113
+ yield f"data: {json.dumps({'token': token})}\n\n"
114
+
115
+ full_answer = "".join(answer_parts)
116
+
117
+ _, sources = await rag_service.query(request.question)
118
+
119
+ final_response = FinalResponse(
120
+ final_answer=full_answer,
121
+ sources=[source.model_dump() for source in sources]
122
+ )
123
+
124
+ yield f"data: [DONE]\n\n"
125
+ yield f"data: {json.dumps(final_response.model_dump())}\n\n"
126
+
127
+ except Exception as e:
128
+ yield f"data: {json.dumps({'error': str(e)})}\n\n"
129
+
130
+ return StreamingResponse(
131
+ event_generator(),
132
+ media_type="text/event-stream",
133
+ headers={
134
+ "Cache-Control": "no-cache",
135
+ "Connection": "keep-alive",
136
+ }
137
+ )
138
+
139
+
140
+ @app.post("/query", response_model=FinalResponse)
141
+ async def query(request: QueryRequest):
142
+ if not rag_service.has_documents():
143
+ raise HTTPException(status_code=400, detail="No documents indexed. Please upload a document first.")
144
+
145
+ try:
146
+ answer, sources = await rag_service.query(request.question)
147
+
148
+ return FinalResponse(
149
+ final_answer=answer,
150
+ sources=sources
151
+ )
152
+
153
+ except Exception as e:
154
+ raise HTTPException(status_code=500, detail=f"Error processing query: {str(e)}")
155
+
156
+
157
+ @app.post("/summarize", response_model=SummaryResponse)
158
+ async def summarize(request: SummarizeRequest):
159
+ if not rag_service.has_documents():
160
+ raise HTTPException(status_code=400, detail="No documents indexed. Please upload a document first.")
161
+
162
+ try:
163
+ summary = await rag_service.summarize(max_length=request.max_length)
164
+
165
+ word_count = len(summary.split())
166
+
167
+ return SummaryResponse(
168
+ summary=summary,
169
+ word_count=word_count,
170
+ source_documents=rag_service.get_indexed_documents()
171
+ )
172
+
173
+ except Exception as e:
174
+ raise HTTPException(status_code=500, detail=f"Error generating summary: {str(e)}")
175
+
176
+
177
+ @app.post("/reset", response_model=StatusResponse)
178
+ async def reset_index():
179
+ try:
180
+ rag_service.reset_index()
181
+
182
+ # Only try to delete files if uploads directory exists
183
+ if settings.upload_dir.exists():
184
+ for file_path in settings.upload_dir.glob("*"):
185
+ if file_path.is_file():
186
+ file_path.unlink()
187
+
188
+ return StatusResponse(
189
+ status="success",
190
+ message="Index reset successfully. All documents removed."
191
+ )
192
+
193
+ except Exception as e:
194
+ raise HTTPException(status_code=500, detail=f"Error resetting index: {str(e)}")
195
+
196
+
197
+ @app.get("/status", response_model=StatusResponse)
198
+ async def get_status():
199
+ return StatusResponse(
200
+ status="online",
201
+ message="Studyson RAG API is running",
202
+ details={
203
+ "has_documents": rag_service.has_documents(),
204
+ "indexed_documents": rag_service.get_indexed_documents(),
205
+ "document_count": len(rag_service.get_indexed_documents())
206
+ }
207
+ )
208
+
209
+
210
+ if __name__ == "__main__":
211
+ import uvicorn
212
+ uvicorn.run(app, host=settings.host, port=settings.port)
rag_service.cpython-313.pyc ADDED
Binary file (6.49 kB). View file
 
rag_service.py ADDED
@@ -0,0 +1,107 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from llama_index.core import VectorStoreIndex, Document, Settings
2
+ from llama_index.llms.groq import Groq
3
+ from llama_index.embeddings.fastembed import FastEmbedEmbedding
4
+ from llama_index.core.chat_engine import CondensePlusContextChatEngine
5
+ from typing import Optional, AsyncGenerator, List
6
+ from app.config import settings
7
+ from app.models.schemas import SourceInfo
8
+ import os
9
+
10
+
11
+ class RAGService:
12
+
13
+ def __init__(self):
14
+ os.environ["GROQ_API_KEY"] = settings.groq_api_key
15
+ self._llm_initialized = False
16
+ self.index: Optional[VectorStoreIndex] = None
17
+ self.chat_engine = None
18
+ self.indexed_documents = []
19
+
20
+ def _initialize_llm(self):
21
+ if not self._llm_initialized:
22
+ Settings.llm = Groq(model="llama-3.1-8b-instant", api_key=settings.groq_api_key)
23
+ # Use FastEmbed - lightweight embeddings optimized for low memory
24
+ Settings.embed_model = FastEmbedEmbedding(model_name="BAAI/bge-small-en-v1.5")
25
+ self._llm_initialized = True
26
+
27
+ def create_index_from_text(self, text: str, source_name: str) -> None:
28
+ self._initialize_llm()
29
+ document = Document(text=text, metadata={"source": source_name})
30
+ self.indexed_documents.append(source_name)
31
+
32
+ if self.index is None:
33
+ self.index = VectorStoreIndex.from_documents([document])
34
+ else:
35
+ self.index.insert(document)
36
+
37
+ self.chat_engine = self.index.as_chat_engine(
38
+ chat_mode="condense_plus_context",
39
+ verbose=True
40
+ )
41
+
42
+ def create_index_from_documents(self, documents: List[Document]) -> None:
43
+ self._initialize_llm()
44
+ for doc in documents:
45
+ if "source" in doc.metadata:
46
+ self.indexed_documents.append(doc.metadata["source"])
47
+
48
+ if self.index is None:
49
+ self.index = VectorStoreIndex.from_documents(documents)
50
+ else:
51
+ for doc in documents:
52
+ self.index.insert(doc)
53
+
54
+ self.chat_engine = self.index.as_chat_engine(
55
+ chat_mode="condense_plus_context",
56
+ verbose=True
57
+ )
58
+
59
+ async def stream_query(self, question: str) -> AsyncGenerator[str, None]:
60
+ if self.chat_engine is None:
61
+ raise ValueError("No documents indexed. Please upload a document first.")
62
+
63
+ response = await self.chat_engine.astream_chat(question)
64
+
65
+ async for token in response.async_response_gen():
66
+ yield token
67
+
68
+ async def query(self, question: str) -> tuple[str, List[SourceInfo]]:
69
+ if self.index is None:
70
+ raise ValueError("No documents indexed. Please upload a document first.")
71
+
72
+ query_engine = self.index.as_query_engine(similarity_top_k=3)
73
+ response = await query_engine.aquery(question)
74
+
75
+ sources = []
76
+ if hasattr(response, 'source_nodes'):
77
+ for node in response.source_nodes:
78
+ source_info = SourceInfo(
79
+ file_name=node.metadata.get("source", "Unknown"),
80
+ text=node.text[:300],
81
+ score=node.score if hasattr(node, 'score') else None
82
+ )
83
+ sources.append(source_info)
84
+
85
+ return str(response), sources
86
+
87
+ async def summarize(self, max_length: int = 500) -> str:
88
+ if self.index is None:
89
+ raise ValueError("No documents indexed. Please upload a document first.")
90
+
91
+ query_engine = self.index.as_query_engine()
92
+
93
+ summary_prompt = f"Provide a comprehensive summary of all the documents in approximately {max_length} words. Focus on the main ideas, key points, and important details."
94
+
95
+ response = await query_engine.aquery(summary_prompt)
96
+ return str(response)
97
+
98
+ def reset_index(self) -> None:
99
+ self.index = None
100
+ self.chat_engine = None
101
+ self.indexed_documents = []
102
+
103
+ def get_indexed_documents(self) -> List[str]:
104
+ return self.indexed_documents
105
+
106
+ def has_documents(self) -> bool:
107
+ return self.index is not None
requirements.txt ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ fastapi>=0.115.0
2
+ uvicorn[standard]>=0.32.0
3
+ python-multipart>=0.0.12
4
+ llama-index>=0.12.0
5
+ llama-index-llms-groq>=0.3.0
6
+ llama-index-embeddings-fastembed>=0.3.0
7
+ pypdf>=5.1.0
8
+ pymupdf>=1.24.0
9
+ python-dotenv>=1.0.0
10
+ beautifulsoup4>=4.12.0
11
+ aiohttp>=3.11.0
12
+ pydantic>=2.9.0
13
+ pydantic-settings>=2.6.0
14
+ fastembed>=0.4.0
schemas.cpython-313.pyc ADDED
Binary file (2.29 kB). View file
 
schemas.py ADDED
@@ -0,0 +1,38 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from pydantic import BaseModel, HttpUrl
2
+ from typing import List, Optional
3
+
4
+
5
+ class ScrapeRequest(BaseModel):
6
+ url: HttpUrl
7
+
8
+
9
+ class QueryRequest(BaseModel):
10
+ question: str
11
+
12
+
13
+ class SourceInfo(BaseModel):
14
+ file_name: str
15
+ text: str
16
+ score: Optional[float] = None
17
+
18
+
19
+ class FinalResponse(BaseModel):
20
+ final_answer: str
21
+ sources: List[SourceInfo]
22
+
23
+
24
+ class SummarizeRequest(BaseModel):
25
+ max_length: Optional[int] = 500
26
+ style: Optional[str] = "concise"
27
+
28
+
29
+ class SummaryResponse(BaseModel):
30
+ summary: str
31
+ word_count: int
32
+ source_documents: List[str]
33
+
34
+
35
+ class StatusResponse(BaseModel):
36
+ status: str
37
+ message: str
38
+ details: Optional[dict] = None
style.css ADDED
@@ -0,0 +1,730 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ * {
2
+ margin: 0;
3
+ padding: 0;
4
+ box-sizing: border-box;
5
+ }
6
+
7
+ :root {
8
+ --primary: #667eea;
9
+ --primary-dark: #5568d3;
10
+ --secondary: #764ba2;
11
+ --accent: #f093fb;
12
+ --bg-main: #f7f9fc;
13
+ --bg-card: #ffffff;
14
+ --bg-sidebar: #1a1f36;
15
+ --text-primary: #2d3748;
16
+ --text-secondary: #718096;
17
+ --text-light: #a0aec0;
18
+ --border: #e2e8f0;
19
+ --success: #48bb78;
20
+ --danger: #f56565;
21
+ --warning: #ed8936;
22
+ --shadow-sm: 0 1px 3px rgba(0, 0, 0, 0.08);
23
+ --shadow-md: 0 4px 12px rgba(0, 0, 0, 0.1);
24
+ --shadow-lg: 0 10px 30px rgba(0, 0, 0, 0.12);
25
+ }
26
+
27
+ body {
28
+ font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
29
+ background: var(--bg-main);
30
+ color: var(--text-primary);
31
+ line-height: 1.6;
32
+ overflow-x: hidden;
33
+ }
34
+
35
+ .app-layout {
36
+ display: flex;
37
+ min-height: 100vh;
38
+ }
39
+
40
+ /* ===== SIDEBAR ===== */
41
+ .sidebar {
42
+ width: 280px;
43
+ background: var(--bg-sidebar);
44
+ color: white;
45
+ display: flex;
46
+ flex-direction: column;
47
+ position: fixed;
48
+ height: 100vh;
49
+ left: 0;
50
+ top: 0;
51
+ z-index: 100;
52
+ box-shadow: var(--shadow-lg);
53
+ }
54
+
55
+ .logo-section {
56
+ padding: 2rem 1.5rem;
57
+ display: flex;
58
+ align-items: center;
59
+ gap: 1rem;
60
+ border-bottom: 1px solid rgba(255, 255, 255, 0.1);
61
+ }
62
+
63
+ .logo-icon {
64
+ flex-shrink: 0;
65
+ }
66
+
67
+ .logo-text {
68
+ font-size: 1.5rem;
69
+ font-weight: 700;
70
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
71
+ -webkit-background-clip: text;
72
+ -webkit-text-fill-color: transparent;
73
+ background-clip: text;
74
+ }
75
+
76
+ .nav-menu {
77
+ flex: 1;
78
+ padding: 1.5rem 1rem;
79
+ display: flex;
80
+ flex-direction: column;
81
+ gap: 0.5rem;
82
+ }
83
+
84
+ .nav-item {
85
+ display: flex;
86
+ align-items: center;
87
+ gap: 1rem;
88
+ padding: 1rem 1.25rem;
89
+ background: transparent;
90
+ border: none;
91
+ border-radius: 12px;
92
+ color: rgba(255, 255, 255, 0.7);
93
+ cursor: pointer;
94
+ transition: all 0.3s ease;
95
+ font-size: 0.95rem;
96
+ font-weight: 500;
97
+ width: 100%;
98
+ text-align: left;
99
+ }
100
+
101
+ .nav-item:hover {
102
+ background: rgba(255, 255, 255, 0.08);
103
+ color: white;
104
+ transform: translateX(4px);
105
+ }
106
+
107
+ .nav-item.active {
108
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
109
+ color: white;
110
+ box-shadow: 0 4px 15px rgba(102, 126, 234, 0.4);
111
+ }
112
+
113
+ .nav-icon {
114
+ font-size: 1.5rem;
115
+ }
116
+
117
+ .sidebar-footer {
118
+ padding: 1.5rem 1rem;
119
+ border-top: 1px solid rgba(255, 255, 255, 0.1);
120
+ }
121
+
122
+ .stats-card {
123
+ background: rgba(255, 255, 255, 0.05);
124
+ padding: 1rem;
125
+ border-radius: 12px;
126
+ margin-bottom: 1rem;
127
+ }
128
+
129
+ .stat-item {
130
+ display: flex;
131
+ justify-content: space-between;
132
+ align-items: center;
133
+ margin-bottom: 0.75rem;
134
+ }
135
+
136
+ .stat-label {
137
+ font-size: 0.85rem;
138
+ color: rgba(255, 255, 255, 0.6);
139
+ }
140
+
141
+ .stat-value {
142
+ font-size: 1.5rem;
143
+ font-weight: 700;
144
+ color: var(--accent);
145
+ }
146
+
147
+ .stat-indicator {
148
+ display: flex;
149
+ align-items: center;
150
+ gap: 0.5rem;
151
+ font-size: 0.85rem;
152
+ color: rgba(255, 255, 255, 0.7);
153
+ }
154
+
155
+ .pulse-dot {
156
+ width: 8px;
157
+ height: 8px;
158
+ background: var(--success);
159
+ border-radius: 50%;
160
+ animation: pulse 2s infinite;
161
+ }
162
+
163
+ @keyframes pulse {
164
+ 0%, 100% { opacity: 1; transform: scale(1); }
165
+ 50% { opacity: 0.5; transform: scale(1.1); }
166
+ }
167
+
168
+ .btn-reset {
169
+ width: 100%;
170
+ padding: 0.875rem 1rem;
171
+ background: rgba(245, 101, 101, 0.1);
172
+ border: 1px solid rgba(245, 101, 101, 0.3);
173
+ border-radius: 10px;
174
+ color: #fc8181;
175
+ font-weight: 500;
176
+ cursor: pointer;
177
+ transition: all 0.3s ease;
178
+ display: flex;
179
+ align-items: center;
180
+ justify-content: center;
181
+ gap: 0.5rem;
182
+ }
183
+
184
+ .btn-reset:hover {
185
+ background: rgba(245, 101, 101, 0.2);
186
+ border-color: rgba(245, 101, 101, 0.5);
187
+ }
188
+
189
+ /* ===== MAIN CONTENT ===== */
190
+ .main-content {
191
+ margin-left: 280px;
192
+ flex: 1;
193
+ min-height: 100vh;
194
+ display: flex;
195
+ flex-direction: column;
196
+ }
197
+
198
+ .topbar {
199
+ background: white;
200
+ padding: 1.5rem 2.5rem;
201
+ display: flex;
202
+ justify-content: space-between;
203
+ align-items: center;
204
+ box-shadow: var(--shadow-sm);
205
+ position: sticky;
206
+ top: 0;
207
+ z-index: 50;
208
+ }
209
+
210
+ .breadcrumb-item {
211
+ font-size: 1.25rem;
212
+ font-weight: 600;
213
+ color: var(--text-primary);
214
+ }
215
+
216
+ .status-badge {
217
+ display: flex;
218
+ align-items: center;
219
+ gap: 0.5rem;
220
+ padding: 0.5rem 1rem;
221
+ background: var(--bg-main);
222
+ border-radius: 20px;
223
+ font-size: 0.875rem;
224
+ font-weight: 500;
225
+ }
226
+
227
+ .status-dot {
228
+ width: 8px;
229
+ height: 8px;
230
+ background: var(--success);
231
+ border-radius: 50%;
232
+ animation: pulse 2s infinite;
233
+ }
234
+
235
+ /* ===== VIEW CONTAINERS ===== */
236
+ .view-container {
237
+ display: none;
238
+ padding: 2.5rem;
239
+ flex: 1;
240
+ }
241
+
242
+ .view-container.active {
243
+ display: block;
244
+ }
245
+
246
+ .view-header {
247
+ margin-bottom: 2rem;
248
+ }
249
+
250
+ .view-title {
251
+ font-size: 2rem;
252
+ font-weight: 700;
253
+ color: var(--text-primary);
254
+ margin-bottom: 0.5rem;
255
+ }
256
+
257
+ .view-description {
258
+ font-size: 1.05rem;
259
+ color: var(--text-secondary);
260
+ }
261
+
262
+ .content-card {
263
+ background: var(--bg-card);
264
+ border-radius: 16px;
265
+ padding: 2.5rem;
266
+ box-shadow: var(--shadow-md);
267
+ }
268
+
269
+ /* ===== UPLOAD AREA ===== */
270
+ .upload-area {
271
+ display: flex;
272
+ flex-direction: column;
273
+ gap: 1.5rem;
274
+ }
275
+
276
+ .drop-zone {
277
+ border: 3px dashed var(--border);
278
+ border-radius: 16px;
279
+ padding: 3rem 2rem;
280
+ text-align: center;
281
+ cursor: pointer;
282
+ transition: all 0.3s ease;
283
+ background: var(--bg-main);
284
+ }
285
+
286
+ .drop-zone:hover {
287
+ border-color: var(--primary);
288
+ background: #f7faff;
289
+ }
290
+
291
+ .drop-zone-icon {
292
+ font-size: 4rem;
293
+ margin-bottom: 1rem;
294
+ }
295
+
296
+ .drop-zone-title {
297
+ font-size: 1.35rem;
298
+ font-weight: 600;
299
+ color: var(--text-primary);
300
+ margin-bottom: 0.5rem;
301
+ }
302
+
303
+ .drop-zone-desc {
304
+ color: var(--text-secondary);
305
+ font-size: 1rem;
306
+ }
307
+
308
+ .file-info {
309
+ margin-top: 1.5rem;
310
+ padding: 1.25rem;
311
+ background: white;
312
+ border-radius: 12px;
313
+ display: flex;
314
+ align-items: center;
315
+ gap: 1rem;
316
+ box-shadow: var(--shadow-sm);
317
+ }
318
+
319
+ .file-icon {
320
+ font-size: 2rem;
321
+ }
322
+
323
+ .file-name-display {
324
+ flex: 1;
325
+ font-weight: 600;
326
+ color: var(--text-primary);
327
+ }
328
+
329
+ .file-size {
330
+ color: var(--text-secondary);
331
+ font-size: 0.875rem;
332
+ }
333
+
334
+ /* ===== BUTTONS ===== */
335
+ .btn-primary {
336
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
337
+ color: white;
338
+ border: none;
339
+ border-radius: 12px;
340
+ font-weight: 600;
341
+ cursor: pointer;
342
+ transition: all 0.3s ease;
343
+ box-shadow: 0 4px 15px rgba(102, 126, 234, 0.3);
344
+ }
345
+
346
+ .btn-primary:hover {
347
+ transform: translateY(-2px);
348
+ box-shadow: 0 6px 20px rgba(102, 126, 234, 0.4);
349
+ }
350
+
351
+ .btn-primary:active {
352
+ transform: translateY(0);
353
+ }
354
+
355
+ .btn-large {
356
+ padding: 1.25rem 2rem;
357
+ font-size: 1.05rem;
358
+ display: flex;
359
+ align-items: center;
360
+ justify-content: center;
361
+ gap: 0.75rem;
362
+ }
363
+
364
+ .btn-arrow {
365
+ font-size: 1.5rem;
366
+ transition: transform 0.3s ease;
367
+ }
368
+
369
+ .btn-primary:hover .btn-arrow {
370
+ transform: translateX(4px);
371
+ }
372
+
373
+ /* ===== WEB FORM ===== */
374
+ .web-form {
375
+ display: flex;
376
+ flex-direction: column;
377
+ gap: 1.5rem;
378
+ }
379
+
380
+ .input-group {
381
+ position: relative;
382
+ }
383
+
384
+ .input-icon {
385
+ position: absolute;
386
+ left: 1.25rem;
387
+ top: 50%;
388
+ transform: translateY(-50%);
389
+ font-size: 1.5rem;
390
+ }
391
+
392
+ .input-field {
393
+ width: 100%;
394
+ padding: 1.25rem 1.25rem 1.25rem 3.5rem;
395
+ border: 2px solid var(--border);
396
+ border-radius: 12px;
397
+ font-size: 1rem;
398
+ transition: all 0.3s ease;
399
+ font-family: inherit;
400
+ }
401
+
402
+ .input-field:focus {
403
+ outline: none;
404
+ border-color: var(--primary);
405
+ box-shadow: 0 0 0 4px rgba(102, 126, 234, 0.1);
406
+ }
407
+
408
+ /* ===== CHAT CONTAINER ===== */
409
+ .chat-container {
410
+ background: var(--bg-card);
411
+ border-radius: 16px;
412
+ box-shadow: var(--shadow-md);
413
+ display: flex;
414
+ flex-direction: column;
415
+ height: 600px;
416
+ }
417
+
418
+ .messages-area {
419
+ flex: 1;
420
+ overflow-y: auto;
421
+ padding: 2rem;
422
+ }
423
+
424
+ .empty-state {
425
+ height: 100%;
426
+ display: flex;
427
+ flex-direction: column;
428
+ align-items: center;
429
+ justify-content: center;
430
+ color: var(--text-secondary);
431
+ text-align: center;
432
+ }
433
+
434
+ .empty-icon {
435
+ font-size: 4rem;
436
+ margin-bottom: 1rem;
437
+ opacity: 0.5;
438
+ }
439
+
440
+ .empty-state h3 {
441
+ font-size: 1.5rem;
442
+ margin-bottom: 0.5rem;
443
+ color: var(--text-primary);
444
+ }
445
+
446
+ .message {
447
+ margin-bottom: 1.5rem;
448
+ display: flex;
449
+ gap: 1rem;
450
+ animation: slideIn 0.3s ease;
451
+ }
452
+
453
+ @keyframes slideIn {
454
+ from {
455
+ opacity: 0;
456
+ transform: translateY(10px);
457
+ }
458
+ to {
459
+ opacity: 1;
460
+ transform: translateY(0);
461
+ }
462
+ }
463
+
464
+ .message-bubble {
465
+ max-width: 70%;
466
+ padding: 1.25rem 1.5rem;
467
+ border-radius: 16px;
468
+ line-height: 1.6;
469
+ }
470
+
471
+ .message.user .message-bubble {
472
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
473
+ color: white;
474
+ margin-left: auto;
475
+ border-bottom-right-radius: 4px;
476
+ }
477
+
478
+ .message.assistant .message-bubble {
479
+ background: var(--bg-main);
480
+ color: var(--text-primary);
481
+ border-bottom-left-radius: 4px;
482
+ }
483
+
484
+ .message-sources {
485
+ margin-top: 1rem;
486
+ padding-top: 1rem;
487
+ border-top: 1px solid rgba(255, 255, 255, 0.2);
488
+ font-size: 0.875rem;
489
+ }
490
+
491
+ .source-item {
492
+ margin-top: 0.75rem;
493
+ padding: 0.75rem;
494
+ background: rgba(255, 255, 255, 0.1);
495
+ border-radius: 8px;
496
+ font-size: 0.85rem;
497
+ }
498
+
499
+ .source-item strong {
500
+ display: block;
501
+ margin-bottom: 0.25rem;
502
+ }
503
+
504
+ .chat-input-area {
505
+ padding: 1.5rem;
506
+ border-top: 1px solid var(--border);
507
+ }
508
+
509
+ .chat-input-wrapper {
510
+ display: flex;
511
+ gap: 1rem;
512
+ align-items: center;
513
+ }
514
+
515
+ .chat-input {
516
+ flex: 1;
517
+ padding: 1rem 1.5rem;
518
+ border: 2px solid var(--border);
519
+ border-radius: 12px;
520
+ font-size: 1rem;
521
+ font-family: inherit;
522
+ transition: all 0.3s ease;
523
+ }
524
+
525
+ .chat-input:focus {
526
+ outline: none;
527
+ border-color: var(--primary);
528
+ box-shadow: 0 0 0 4px rgba(102, 126, 234, 0.1);
529
+ }
530
+
531
+ .btn-send {
532
+ width: 50px;
533
+ height: 50px;
534
+ border-radius: 12px;
535
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
536
+ border: none;
537
+ color: white;
538
+ cursor: pointer;
539
+ display: flex;
540
+ align-items: center;
541
+ justify-content: center;
542
+ transition: all 0.3s ease;
543
+ flex-shrink: 0;
544
+ }
545
+
546
+ .btn-send:hover {
547
+ transform: scale(1.05);
548
+ box-shadow: 0 4px 15px rgba(102, 126, 234, 0.4);
549
+ }
550
+
551
+ /* ===== SUMMARY FORM ===== */
552
+ .summary-form {
553
+ display: flex;
554
+ flex-direction: column;
555
+ gap: 2rem;
556
+ }
557
+
558
+ .form-group {
559
+ display: flex;
560
+ flex-direction: column;
561
+ gap: 1rem;
562
+ }
563
+
564
+ .form-label {
565
+ font-size: 1.05rem;
566
+ font-weight: 600;
567
+ color: var(--text-primary);
568
+ }
569
+
570
+ .slider-container {
571
+ display: flex;
572
+ align-items: center;
573
+ gap: 1.5rem;
574
+ }
575
+
576
+ .range-slider {
577
+ flex: 1;
578
+ height: 8px;
579
+ border-radius: 4px;
580
+ background: linear-gradient(90deg, #667eea 0%, #764ba2 100%);
581
+ outline: none;
582
+ -webkit-appearance: none;
583
+ }
584
+
585
+ .range-slider::-webkit-slider-thumb {
586
+ -webkit-appearance: none;
587
+ width: 24px;
588
+ height: 24px;
589
+ border-radius: 50%;
590
+ background: white;
591
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
592
+ cursor: pointer;
593
+ }
594
+
595
+ .range-slider::-moz-range-thumb {
596
+ width: 24px;
597
+ height: 24px;
598
+ border-radius: 50%;
599
+ background: white;
600
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
601
+ cursor: pointer;
602
+ border: none;
603
+ }
604
+
605
+ .slider-value {
606
+ min-width: 100px;
607
+ padding: 0.75rem 1.25rem;
608
+ background: var(--bg-main);
609
+ border-radius: 8px;
610
+ font-weight: 600;
611
+ text-align: center;
612
+ color: var(--primary);
613
+ }
614
+
615
+ /* ===== RESULT MESSAGES ===== */
616
+ .result-message {
617
+ margin-top: 1.5rem;
618
+ padding: 1.25rem 1.5rem;
619
+ border-radius: 12px;
620
+ display: none;
621
+ animation: slideIn 0.3s ease;
622
+ }
623
+
624
+ .result-message.show {
625
+ display: block;
626
+ }
627
+
628
+ .result-message.success {
629
+ background: #f0fff4;
630
+ border: 1px solid #9ae6b4;
631
+ color: #22543d;
632
+ }
633
+
634
+ .result-message.error {
635
+ background: #fff5f5;
636
+ border: 1px solid #feb2b2;
637
+ color: #742a2a;
638
+ }
639
+
640
+ .result-message.info {
641
+ background: #ebf8ff;
642
+ border: 1px solid #90cdf4;
643
+ color: #2c5282;
644
+ }
645
+
646
+ .summary-output h3 {
647
+ font-size: 1.25rem;
648
+ margin-bottom: 1rem;
649
+ color: var(--text-primary);
650
+ }
651
+
652
+ .summary-output p {
653
+ line-height: 1.8;
654
+ margin-bottom: 1rem;
655
+ }
656
+
657
+ /* ===== RESPONSIVE ===== */
658
+ @media (max-width: 1024px) {
659
+ .sidebar {
660
+ width: 240px;
661
+ }
662
+
663
+ .main-content {
664
+ margin-left: 240px;
665
+ }
666
+ }
667
+
668
+ @media (max-width: 768px) {
669
+ .sidebar {
670
+ transform: translateX(-100%);
671
+ transition: transform 0.3s ease;
672
+ }
673
+
674
+ .sidebar.mobile-open {
675
+ transform: translateX(0);
676
+ }
677
+
678
+ .main-content {
679
+ margin-left: 0;
680
+ }
681
+
682
+ .topbar {
683
+ padding: 1rem 1.5rem;
684
+ }
685
+
686
+ .view-container {
687
+ padding: 1.5rem;
688
+ }
689
+
690
+ .content-card {
691
+ padding: 1.5rem;
692
+ }
693
+
694
+ .message-bubble {
695
+ max-width: 85%;
696
+ }
697
+ }
698
+
699
+ /* ===== LOADING STATE ===== */
700
+ .loading {
701
+ display: inline-block;
702
+ width: 18px;
703
+ height: 18px;
704
+ border: 3px solid rgba(255, 255, 255, 0.3);
705
+ border-top-color: white;
706
+ border-radius: 50%;
707
+ animation: spin 0.8s linear infinite;
708
+ }
709
+
710
+ @keyframes spin {
711
+ to { transform: rotate(360deg); }
712
+ }
713
+
714
+ /* ===== SCROLLBAR ===== */
715
+ ::-webkit-scrollbar {
716
+ width: 10px;
717
+ }
718
+
719
+ ::-webkit-scrollbar-track {
720
+ background: var(--bg-main);
721
+ }
722
+
723
+ ::-webkit-scrollbar-thumb {
724
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
725
+ border-radius: 5px;
726
+ }
727
+
728
+ ::-webkit-scrollbar-thumb:hover {
729
+ background: linear-gradient(135deg, #5568d3 0%, #6a3f8f 100%);
730
+ }