Skip to content

Commit 4a22ec9

Browse files
committed
feat: AI-Powered PDF Chatbot
1 parent 1f18acb commit 4a22ec9

8 files changed

Lines changed: 570 additions & 1 deletion

File tree

app.py

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
import subprocess
1010
import threading
1111
import atexit
12+
from utils.chatbot import PDFChatbot
1213

1314
app = Flask(__name__, static_folder='assets', template_folder='pages')
1415
CORS(app)
@@ -169,6 +170,47 @@ def download():
169170
def serve_pages(filename):
170171
return send_from_directory('pages', filename)
171172

173+
@app.route('/api/chat/init', methods=['POST'])
174+
def chat_init():
175+
data = request.get_json()
176+
pdf_path_req = data.get('path')
177+
178+
if not pdf_path_req:
179+
return jsonify({'success': False, 'message': 'PDF path is required.'}), 400
180+
181+
# Ensure it's a relative path from app root
182+
pdf_path = pdf_path_req.lstrip('/')
183+
184+
if not os.path.exists(pdf_path):
185+
return jsonify({'success': False, 'message': f'PDF file not found: {pdf_path}'}), 404
186+
187+
try:
188+
chatbot = PDFChatbot.get_instance(pdf_path)
189+
chatbot.initialize()
190+
return jsonify({'success': True, 'message': 'Chatbot initialized successfully.'})
191+
except Exception as e:
192+
logger.error(f"Chatbot init error: {str(e)}")
193+
return jsonify({'success': False, 'message': str(e)}), 500
194+
195+
@app.route('/api/chat/ask', methods=['POST'])
196+
def chat_ask():
197+
data = request.get_json()
198+
pdf_path_req = data.get('path')
199+
question = data.get('question')
200+
201+
if not pdf_path_req or not question:
202+
return jsonify({'success': False, 'message': 'Path and question are required.'}), 400
203+
204+
pdf_path = pdf_path_req.lstrip('/')
205+
206+
try:
207+
chatbot = PDFChatbot.get_instance(pdf_path)
208+
answer = chatbot.ask(question)
209+
return jsonify({'success': True, 'answer': answer})
210+
except Exception as e:
211+
logger.error(f"Chatbot ask error: {str(e)}")
212+
return jsonify({'success': False, 'message': str(e)}), 500
213+
172214
@app.route('/api/admin/delete-material', methods=['POST'])
173215
def delete_material():
174216
"""

assets/css/chat.css

Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
/* Chat Page Specific Styles */
2+
.chat-container {
3+
max-width: 900px;
4+
margin: 80px auto 20px;
5+
padding: 20px;
6+
display: flex;
7+
flex-direction: column;
8+
height: calc(100vh - 120px);
9+
background: var(--bg-color);
10+
border-radius: 12px;
11+
box-shadow: 0 4px 6px var(--shadow-color);
12+
}
13+
14+
.chat-header {
15+
display: flex;
16+
align-items: center;
17+
gap: 15px;
18+
padding-bottom: 15px;
19+
border-bottom: 1px solid var(--border-color);
20+
margin-bottom: 15px;
21+
}
22+
23+
.back-btn {
24+
background: var(--primary-color);
25+
color: white;
26+
border: none;
27+
padding: 8px 15px;
28+
border-radius: 6px;
29+
cursor: pointer;
30+
font-size: 14px;
31+
transition: background 0.3s;
32+
}
33+
34+
.back-btn:hover {
35+
background: var(--primary-hover);
36+
}
37+
38+
.chat-header h2 {
39+
font-size: 1.2rem;
40+
color: var(--text-color);
41+
margin: 0;
42+
white-space: nowrap;
43+
overflow: hidden;
44+
text-overflow: ellipsis;
45+
}
46+
47+
.chat-box {
48+
flex-grow: 1;
49+
overflow-y: auto;
50+
padding: 10px;
51+
display: flex;
52+
flex-direction: column;
53+
gap: 15px;
54+
}
55+
56+
.message {
57+
max-width: 80%;
58+
padding: 12px 16px;
59+
border-radius: 8px;
60+
line-height: 1.5;
61+
font-size: 0.95rem;
62+
}
63+
64+
.user-message {
65+
align-self: flex-end;
66+
background: var(--primary-color);
67+
color: white;
68+
border-bottom-right-radius: 0;
69+
}
70+
71+
.ai-message {
72+
align-self: flex-start;
73+
background: var(--card-bg);
74+
color: var(--text-color);
75+
border: 1px solid var(--border-color);
76+
border-bottom-left-radius: 0;
77+
}
78+
79+
.system-message {
80+
align-self: center;
81+
background: transparent;
82+
color: var(--text-muted);
83+
font-size: 0.85rem;
84+
font-style: italic;
85+
border: none;
86+
}
87+
88+
.chat-input-container {
89+
padding-top: 15px;
90+
border-top: 1px solid var(--border-color);
91+
}
92+
93+
#chat-form {
94+
display: flex;
95+
gap: 10px;
96+
}
97+
98+
#chat-input {
99+
flex-grow: 1;
100+
padding: 12px 15px;
101+
border: 1px solid var(--border-color);
102+
border-radius: 8px;
103+
background: var(--input-bg);
104+
color: var(--text-color);
105+
font-size: 1rem;
106+
transition: border-color 0.3s;
107+
}
108+
109+
#chat-input:focus {
110+
outline: none;
111+
border-color: var(--primary-color);
112+
}
113+
114+
#chat-input:disabled {
115+
opacity: 0.6;
116+
cursor: not-allowed;
117+
}
118+
119+
#send-btn {
120+
background: var(--primary-color);
121+
color: white;
122+
border: none;
123+
padding: 0 20px;
124+
border-radius: 8px;
125+
cursor: pointer;
126+
font-size: 1rem;
127+
transition: background 0.3s;
128+
}
129+
130+
#send-btn:hover:not(:disabled) {
131+
background: var(--primary-hover);
132+
}
133+
134+
#send-btn:disabled {
135+
opacity: 0.6;
136+
cursor: not-allowed;
137+
}
138+
139+
/* markdown styles within ai messages */
140+
.ai-message p { margin-bottom: 10px; }
141+
.ai-message p:last-child { margin-bottom: 0; }
142+
.ai-message code {
143+
background: rgba(0,0,0,0.1);
144+
padding: 2px 4px;
145+
border-radius: 4px;
146+
font-family: monospace;
147+
}
148+
[data-theme="dark"] .ai-message code {
149+
background: rgba(255,255,255,0.1);
150+
}

assets/css/notes.css

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -270,6 +270,44 @@ body > nav h1 {
270270
display: inline-block !important;
271271
}
272272

273+
/* Chat Button - gradient style for AI feature */
274+
.chat-btn {
275+
display: inline-flex;
276+
align-items: center;
277+
justify-content: center;
278+
background: linear-gradient(135deg, #6366f1, #a855f7);
279+
color: white;
280+
padding: 0.75rem 1.25rem;
281+
border-radius: 8px;
282+
text-decoration: none;
283+
font-weight: 500;
284+
transition: all var(--transition-speed) ease;
285+
border: none;
286+
cursor: pointer;
287+
font-size: 1rem;
288+
min-width: 120px;
289+
text-align: center;
290+
align-self: flex-start;
291+
pointer-events: auto;
292+
z-index: 2;
293+
position: relative;
294+
}
295+
296+
.chat-btn:hover {
297+
transform: translateY(-2px);
298+
box-shadow: 0 4px 12px rgba(168, 85, 247, 0.4);
299+
}
300+
301+
.chat-btn:active {
302+
transform: translateY(0);
303+
}
304+
305+
.chat-btn i {
306+
font-size: 1rem;
307+
margin-right: 8px !important;
308+
display: inline-block !important;
309+
}
310+
273311
/* Update Download Button */
274312
.download-btn {
275313
display: inline-flex;

assets/js/pages/chat.js

Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
document.addEventListener('DOMContentLoaded', () => {
2+
const urlParams = new URLSearchParams(window.location.search);
3+
const pdfPath = urlParams.get('path');
4+
const pdfTitle = urlParams.get('title') || 'PDF Document';
5+
6+
if (!pdfPath) {
7+
showSystemMessage('Error: No PDF document specified.', true);
8+
return;
9+
}
10+
11+
document.getElementById('pdf-title').textContent = `Chat: ${pdfTitle}`;
12+
13+
// Initialize chatbot
14+
initChatbot(pdfPath);
15+
16+
const chatForm = document.getElementById('chat-form');
17+
chatForm.addEventListener('submit', async (e) => {
18+
e.preventDefault();
19+
const input = document.getElementById('chat-input');
20+
const question = input.value.trim();
21+
if (!question) return;
22+
23+
// Add user message to UI
24+
addMessage(question, 'user');
25+
input.value = '';
26+
27+
// Disable input while waiting
28+
setFormState(false);
29+
30+
// Show typing indicator
31+
const typingId = addTypingIndicator();
32+
33+
try {
34+
const response = await fetch('/api/chat/ask', {
35+
method: 'POST',
36+
headers: { 'Content-Type': 'application/json' },
37+
body: JSON.stringify({ path: pdfPath, question: question })
38+
});
39+
40+
const data = await response.json();
41+
42+
// Remove typing indicator
43+
removeElement(typingId);
44+
45+
if (data.success) {
46+
addMessage(data.answer, 'ai');
47+
} else {
48+
addMessage(`Error: ${data.message}`, 'ai');
49+
}
50+
} catch (error) {
51+
removeElement(typingId);
52+
addMessage('Failed to connect to the server. Please try again.', 'ai');
53+
}
54+
55+
// Re-enable input
56+
setFormState(true);
57+
});
58+
});
59+
60+
async function initChatbot(path) {
61+
try {
62+
const response = await fetch('/api/chat/init', {
63+
method: 'POST',
64+
headers: { 'Content-Type': 'application/json' },
65+
body: JSON.stringify({ path: path })
66+
});
67+
const data = await response.json();
68+
69+
const initMessageElem = document.getElementById('init-message');
70+
if (data.success) {
71+
initMessageElem.innerHTML = `<i class="fas fa-check-circle" style="color: #4CAF50;"></i> Document analyzed successfully! You can now ask questions.`;
72+
setFormState(true);
73+
} else {
74+
initMessageElem.innerHTML = `<i class="fas fa-exclamation-circle" style="color: #f44336;"></i> Initialization failed: ${data.message}`;
75+
}
76+
} catch (error) {
77+
document.getElementById('init-message').innerHTML = `<i class="fas fa-exclamation-circle" style="color: #f44336;"></i> Server connection failed.`;
78+
}
79+
}
80+
81+
function setFormState(enabled) {
82+
document.getElementById('chat-input').disabled = !enabled;
83+
document.getElementById('send-btn').disabled = !enabled;
84+
if (enabled) {
85+
document.getElementById('chat-input').focus();
86+
}
87+
}
88+
89+
function addMessage(text, sender) {
90+
const box = document.getElementById('chat-box');
91+
const msgDiv = document.createElement('div');
92+
msgDiv.className = `message ${sender}-message`;
93+
94+
// Simple markdown to HTML for AI messages (bold, newlines)
95+
let formattedText = text;
96+
if (sender === 'ai') {
97+
formattedText = formattedText
98+
.replace(/\\*\\*(.*?)\\*\\*/g, '<strong>$1</strong>')
99+
.replace(/\\n/g, '<br>');
100+
}
101+
102+
msgDiv.innerHTML = formattedText;
103+
box.appendChild(msgDiv);
104+
box.scrollTop = box.scrollHeight;
105+
}
106+
107+
function addTypingIndicator() {
108+
const box = document.getElementById('chat-box');
109+
const msgDiv = document.createElement('div');
110+
const id = 'typing-' + Date.now();
111+
msgDiv.id = id;
112+
msgDiv.className = `message ai-message`;
113+
msgDiv.innerHTML = '<i class="fas fa-circle-notch fa-spin"></i> Generating answer...';
114+
box.appendChild(msgDiv);
115+
box.scrollTop = box.scrollHeight;
116+
return id;
117+
}
118+
119+
function removeElement(id) {
120+
const el = document.getElementById(id);
121+
if (el) el.remove();
122+
}

assets/js/pages/notes/notes-main.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,9 @@ function displayMaterials(semesterId, branchId, subjectId) {
128128
target="_blank" class="view-btn">
129129
View PDF
130130
</a>
131+
<a href="chat.html?path=${encodeURIComponent(filePath)}&title=${encodeURIComponent(material.title)}" class="chat-btn">
132+
<i class="fas fa-robot"></i> Chat
133+
</a>
131134
<a href="${absoluteFilePath}" download="${safeFileName}" class="download-btn">
132135
Download
133136
</a>

0 commit comments

Comments
 (0)