Skip to content

Commit b8bfa82

Browse files
Dev NirwalDev Nirwal
authored andcommitted
Feature: add admin login, toggle-based API key setup, and support for OpenAI
1 parent d3273ab commit b8bfa82

File tree

6 files changed

+380
-141
lines changed

6 files changed

+380
-141
lines changed

chat/templates/chat/index.html

Lines changed: 141 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
<head>
44
<meta charset="UTF-8">
55
<meta name="viewport" content="width=device-width, initial-scale=1.0">
6-
<title>PDF Chat with Gemini</title>
6+
<title>PDF Chat with Gemini & OpenAI</title>
77
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
88
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
99
<style>
@@ -151,6 +151,30 @@
151151
color: #667eea;
152152
font-weight: 600;
153153
}
154+
155+
.api-toggle {
156+
display: flex;
157+
background: rgba(255,255,255,0.1);
158+
border-radius: 10px;
159+
padding: 4px;
160+
margin-bottom: 15px;
161+
}
162+
.api-toggle-btn {
163+
flex: 1;
164+
border: none;
165+
background: transparent;
166+
color: white;
167+
padding: 6px;
168+
border-radius: 8px;
169+
font-size: 0.8rem;
170+
font-weight: 600;
171+
transition: all 0.2s;
172+
}
173+
.api-toggle-btn.active {
174+
background: white;
175+
color: #764ba2;
176+
box-shadow: 0 2px 5px rgba(0,0,0,0.1);
177+
}
154178
</style>
155179
</head>
156180
<body>
@@ -160,11 +184,52 @@
160184
<div class="col-md-3 col-lg-3 sidebar d-flex flex-column">
161185
<div class="mb-4 text-center">
162186
<h3 class="mt-2"><i class="fas fa-file-pdf me-2"></i>DocuChat AI</h3>
163-
<p class="small text-light opacity-75">Powered by Gemini</p>
187+
{% if is_admin %}
188+
<div class="d-flex align-items-center justify-content-center">
189+
<span class="badge bg-warning text-dark"><i class="fas fa-user-shield me-1"></i>Admin Mode</span>
190+
<a href="{% url 'logout' %}" class="text-light small ms-2"><i class="fas fa-sign-out-alt"></i></a>
191+
</div>
192+
{% else %}
193+
<a href="{% url 'login' %}" class="text-light small text-decoration-none opacity-75">Admin Login</a>
194+
{% endif %}
164195
</div>
165196

197+
{% if not is_admin %}
198+
<!-- API Key Section for Users -->
199+
<div class="api-section bg-white bg-opacity-10 p-3 rounded-3 mb-4">
200+
<h5 class="mb-3 small text-uppercase ls-1"><i class="fas fa-key me-2"></i>Provider Setup</h5>
201+
202+
<div class="api-toggle">
203+
<button onclick="toggleApi('gemini')" id="toggle-gemini" class="api-toggle-btn active">Gemini</button>
204+
<button onclick="toggleApi('openai')" id="toggle-openai" class="api-toggle-btn">OpenAI</button>
205+
</div>
206+
207+
<div id="api-input-container">
208+
<input type="password" id="api-key-input" class="form-control form-control-sm mb-2" placeholder="Enter Gemini Key">
209+
<button onclick="submitKey()" id="check-key-btn" class="btn btn-primary btn-sm w-100 py-2" style="font-size: 0.75rem;">
210+
<i class="fas fa-plug me-1"></i> Connect API
211+
</button>
212+
</div>
213+
214+
<div id="active-keys" class="mt-3">
215+
{% if has_gemini_key %}
216+
<div class="d-flex justify-content-between align-items-center mb-1">
217+
<span class="small"><i class="fas fa-check-circle text-success me-1"></i> Gemini Active</span>
218+
<button onclick="clearKeys()" class="btn btn-link text-light p-0 small" style="font-size: 0.65rem; text-decoration:none;">Clear</button>
219+
</div>
220+
{% endif %}
221+
{% if has_openai_key %}
222+
<div class="d-flex justify-content-between align-items-center">
223+
<span class="small"><i class="fas fa-check-circle text-success me-1"></i> OpenAI Active</span>
224+
<button onclick="clearKeys()" class="btn btn-link text-light p-0 small" style="font-size: 0.65rem; text-decoration:none;">Clear</button>
225+
</div>
226+
{% endif %}
227+
</div>
228+
</div>
229+
{% endif %}
230+
166231
<div class="upload-section bg-white bg-opacity-10 p-3 rounded-3 mb-4">
167-
<h5 class="mb-3"><i class="fas fa-cloud-upload-alt me-2"></i>Upload PDF</h5>
232+
<h5 class="mb-3 small text-uppercase ls-1"><i class="fas fa-cloud-upload-alt me-2"></i>Upload PDF</h5>
168233
<form action="{% url 'upload_pdf' %}" method="post" enctype="multipart/form-data">
169234
{% csrf_token %}
170235
<div class="mb-2">
@@ -173,11 +238,16 @@ <h5 class="mb-3"><i class="fas fa-cloud-upload-alt me-2"></i>Upload PDF</h5>
173238
<div class="mb-3">
174239
<input type="file" name="file" class="form-control form-control-sm" required id="id_file" accept=".pdf">
175240
</div>
176-
<button type="submit" class="btn btn-primary w-100 btn-sm"><i class="fas fa-cogs me-1"></i> Process Document</button>
241+
<button type="submit" class="btn btn-primary w-100 btn-sm" id="upload-btn" {% if not is_admin and not has_gemini_key and not has_openai_key %}disabled{% endif %}>
242+
<i class="fas fa-cogs me-1"></i> Process Document
243+
</button>
244+
{% if not is_admin and not has_gemini_key and not has_openai_key %}
245+
<p class="text-center small text-warning mt-2 mb-0" style="font-size: 0.7rem;">Connect an API to unlock</p>
246+
{% endif %}
177247
</form>
178248
</div>
179249

180-
<h5 class="mb-3 mt-2"><i class="fas fa-book me-2"></i>My Library</h5>
250+
<h5 class="mb-3 mt-2 small text-uppercase ls-1"><i class="fas fa-book me-2"></i>My Library</h5>
181251
<div id="pdf-list" class="flex-grow-1 overflow-auto pe-2">
182252
{% for doc in documents %}
183253
<div class="pdf-item {% if doc.status == 'FAILED' %}border-danger{% endif %}" data-id="{{ doc.id }}" data-status="{{ doc.status }}">
@@ -210,10 +280,10 @@ <h5 class="mb-3 mt-2"><i class="fas fa-book me-2"></i>My Library</h5>
210280
<div class="chat-header d-flex justify-content-between align-items-center">
211281
<h4 class="m-0 text-secondary" id="current-doc-title">Select a document to begin</h4>
212282
<div class="d-flex align-items-center">
213-
<label for="model-selector" class="me-2 mb-0 small text-muted">Model:</label>
214-
<select id="model-selector" class="form-select form-select-sm shadow-sm">
283+
<label for="model-selector" class="me-2 mb-0 small text-muted">Active Model:</label>
284+
<select id="model-selector" class="form-select form-select-sm shadow-sm" style="min-width: 200px;">
215285
{% for model in available_models %}
216-
<option value="{{ model.name }}" {% if model.name == "models/gemini-1.5-flash" %}selected{% endif %}>
286+
<option value="{{ model.name }}" {% if model.name == "models/gemini-1.5-flash" or model.name == "gpt-4o" %}selected{% endif %}>
217287
{{ model.display_name }}
218288
</option>
219289
{% empty %}
@@ -245,6 +315,49 @@ <h5>Welcome to DocuChat!</h5>
245315

246316
<script>
247317
let selectedDocId = null;
318+
let activeApiType = 'gemini';
319+
320+
function toggleApi(type) {
321+
activeApiType = type;
322+
document.getElementById('toggle-gemini').classList.toggle('active', type === 'gemini');
323+
document.getElementById('toggle-openai').classList.toggle('active', type === 'openai');
324+
document.getElementById('api-key-input').placeholder = `Enter ${type === 'gemini' ? 'Gemini' : 'OpenAI'} Key`;
325+
document.getElementById('api-key-input').value = '';
326+
}
327+
328+
async function submitKey() {
329+
const input = document.getElementById('api-key-input');
330+
const key = input.value.trim();
331+
if (!key) return;
332+
333+
const btn = document.getElementById('check-key-btn');
334+
const originalText = btn.innerHTML;
335+
btn.innerHTML = '<i class="fas fa-spinner fa-spin me-1"></i> Connecting...';
336+
btn.disabled = true;
337+
338+
const formData = new FormData();
339+
formData.append('key_type', activeApiType);
340+
formData.append('key', key);
341+
342+
try {
343+
const response = await fetch("{% url 'validate_api_key' %}", {
344+
method: 'POST',
345+
headers: { 'X-CSRFToken': '{{ csrf_token }}' },
346+
body: formData
347+
});
348+
const data = await response.json();
349+
if (data.success) {
350+
location.reload();
351+
} else {
352+
alert('Invalid Key: ' + data.error);
353+
}
354+
} catch (e) {
355+
alert('Connection error');
356+
} finally {
357+
btn.innerHTML = originalText;
358+
btn.disabled = false;
359+
}
360+
}
248361

249362
document.querySelectorAll('.pdf-item').forEach(item => {
250363
item.addEventListener('click', function() {
@@ -282,16 +395,12 @@ <h5>Welcome to DocuChat!</h5>
282395
if (!query || !selectedDocId) return;
283396

284397
const chatMessages = document.getElementById('chat-messages');
285-
286-
// Add user message
287398
const userMsgDiv = document.createElement('div');
288399
userMsgDiv.className = 'message user-message shadow-sm';
289400
userMsgDiv.innerText = query;
290401
chatMessages.appendChild(userMsgDiv);
291-
292402
input.value = '';
293403

294-
// Add loading message
295404
const loadingMsgDiv = document.createElement('div');
296405
loadingMsgDiv.className = 'message bot-message loading shadow-sm';
297406
loadingMsgDiv.innerHTML = '<i class="fas fa-robot me-2 text-primary opacity-50"></i>Analyzing document';
@@ -325,10 +434,9 @@ <h5>Welcome to DocuChat!</h5>
325434
chatMessages.removeChild(loadingMsgDiv);
326435
const botMsgDiv = document.createElement('div');
327436
botMsgDiv.className = 'message bot-message border-danger text-danger shadow-sm';
328-
botMsgDiv.innerHTML = `<i class="fas fa-exclamation-triangle me-2"></i>Connection error. Please try again.`;
437+
botMsgDiv.innerHTML = `<i class="fas fa-exclamation-triangle me-2"></i>Connection error.`;
329438
chatMessages.appendChild(botMsgDiv);
330439
}
331-
332440
chatMessages.scrollTop = chatMessages.scrollHeight;
333441
}
334442

@@ -338,54 +446,37 @@ <h5>Welcome to DocuChat!</h5>
338446
});
339447

340448
async function deleteDocument(event, docId) {
341-
event.stopPropagation(); // Prevent selecting the document when clicking delete
342-
343-
if (!confirm('Are you sure you want to delete this document and its chat history? This action cannot be undone.')) {
344-
return;
345-
}
449+
event.stopPropagation();
450+
if (!confirm('Are you sure?')) return;
346451

347452
const item = document.querySelector(`.pdf-item[data-id="${docId}"]`);
348-
const originalOpacity = item.style.opacity;
349453
item.style.opacity = '0.5';
350-
item.style.pointerEvents = 'none';
351454

352455
try {
353456
const response = await fetch(`/delete/${docId}/`, {
354457
method: 'POST',
355-
headers: {
356-
'X-CSRFToken': '{{ csrf_token }}'
357-
}
458+
headers: { 'X-CSRFToken': '{{ csrf_token }}' }
358459
});
359-
360460
const data = await response.json();
361-
362461
if (data.success) {
363-
item.style.transform = 'translateX(-20px)';
364-
item.style.opacity = '0';
365-
setTimeout(() => {
366-
item.remove();
367-
if (selectedDocId == docId) {
368-
selectedDocId = null;
369-
document.getElementById('query-input').disabled = true;
370-
document.getElementById('send-btn').disabled = true;
371-
document.getElementById('current-doc-title').innerText = 'Select a document to begin';
372-
document.getElementById('chat-messages').innerHTML = `
373-
<div class="text-center mt-5 text-muted">
374-
<i class="fas fa-comments fa-4x mb-3 opacity-25"></i>
375-
<h5>Welcome to DocuChat!</h5>
376-
<p>Select a processed document from the sidebar to ask questions about its content.</p>
377-
</div>`;
378-
}
379-
}, 300);
380-
} else {
381-
alert('Error: ' + (data.error || 'Failed to delete document'));
382-
item.style.opacity = originalOpacity;
383-
item.style.pointerEvents = 'auto';
462+
item.remove();
463+
if (selectedDocId == docId) location.reload();
384464
}
385465
} catch (error) {
386-
alert('Connection error. Failed to delete document.');
387-
item.style.opacity = originalOpacity;
388-
item.style.pointerEvents = 'auto';
466+
alert('Error deleting');
467+
item.style.opacity = '1';
468+
}
469+
}
470+
471+
async function clearKeys() {
472+
try {
473+
await fetch("{% url 'clear_keys' %}", {
474+
method: 'POST',
475+
headers: { 'X-CSRFToken': '{{ csrf_token }}' }
476+
});
477+
location.reload();
478+
} catch (e) {
479+
alert('Error clearing keys');
389480
}
390481
}
391482
</script>

chat/templates/chat/login.html

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
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>Admin Login - DocuChat AI</title>
7+
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
8+
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
9+
<style>
10+
body {
11+
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
12+
height: 100vh;
13+
display: flex;
14+
align-items: center;
15+
justify-content: center;
16+
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
17+
}
18+
.login-card {
19+
background: white;
20+
padding: 40px;
21+
border-radius: 20px;
22+
box-shadow: 0 10px 25px rgba(0,0,0,0.2);
23+
width: 100%;
24+
max-width: 400px;
25+
}
26+
.login-card h2 {
27+
color: #764ba2;
28+
font-weight: 700;
29+
margin-bottom: 30px;
30+
text-align: center;
31+
}
32+
.btn-primary {
33+
background-color: #667eea;
34+
border: none;
35+
padding: 12px;
36+
font-weight: 600;
37+
border-radius: 10px;
38+
width: 100%;
39+
transition: all 0.3s;
40+
}
41+
.btn-primary:hover {
42+
background-color: #764ba2;
43+
transform: translateY(-2px);
44+
}
45+
.form-control {
46+
padding: 12px;
47+
border-radius: 10px;
48+
margin-bottom: 20px;
49+
}
50+
.back-link {
51+
display: block;
52+
text-align: center;
53+
margin-top: 20px;
54+
color: #667eea;
55+
text-decoration: none;
56+
}
57+
</style>
58+
</head>
59+
<body>
60+
<div class="login-card">
61+
<h2><i class="fas fa-user-shield me-2"></i>Admin Login</h2>
62+
{% if error %}
63+
<div class="alert alert-danger">{{ error }}</div>
64+
{% endif %}
65+
<form method="post">
66+
{% csrf_token %}
67+
<div class="mb-3">
68+
<label class="form-label">Username</label>
69+
<input type="text" name="username" class="form-control" placeholder="Enter username" required>
70+
</div>
71+
<div class="mb-3">
72+
<label class="form-label">Password</label>
73+
<input type="password" name="password" class="form-control" placeholder="Enter password" required>
74+
</div>
75+
<button type="submit" class="btn btn-primary">Login</button>
76+
</form>
77+
<a href="{% url 'index' %}" class="back-link"><i class="fas fa-arrow-left me-1"></i> Back to App</a>
78+
</div>
79+
</body>
80+
</html>

chat/urls.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,4 +6,8 @@
66
path('upload/', views.upload_pdf, name='upload_pdf'),
77
path('ask/', views.ask_question, name='ask_question'),
88
path('delete/<int:doc_id>/', views.delete_document, name='delete_document'),
9+
path('login/', views.login_view, name='login'),
10+
path('logout/', views.logout_view, name='logout'),
11+
path('validate-key/', views.validate_api_key, name='validate_api_key'),
12+
path('clear-keys/', views.clear_keys, name='clear_keys'),
913
]

0 commit comments

Comments
 (0)