PaperBotXiv / index.html
YashB1's picture
Update index.html
a0301eb verified
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Research Paper Chatbot</title>
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
<!-- βœ… Add Markdown parsing library -->
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
overflow-x: hidden;
}
.bg-animation {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: -1;
opacity: 0.3;
}
/* βœ… Login Modal Styles */
.login-modal {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.8);
display: flex;
justify-content: center;
align-items: center;
z-index: 1001;
}
.login-form {
background: white;
padding: 40px;
border-radius: 20px;
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
max-width: 500px;
width: 90%;
text-align: center;
}
.login-form h2 {
color: #333;
margin-bottom: 20px;
font-size: 1.8rem;
}
.login-form p {
color: #666;
margin-bottom: 30px;
line-height: 1.5;
}
.login-input-group {
position: relative;
margin-bottom: 20px;
}
.login-input {
width: 100%;
padding: 15px 50px 15px 15px;
border: 2px solid #e0e0e0;
border-radius: 10px;
font-size: 16px;
transition: border-color 0.3s ease;
}
.login-input:focus {
outline: none;
border-color: #667eea;
box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
}
.login-submit {
background: linear-gradient(135deg, #667eea, #764ba2);
color: white;
border: none;
padding: 15px 30px;
border-radius: 10px;
cursor: pointer;
font-weight: 600;
font-size: 16px;
transition: all 0.3s ease;
width: 100%;
margin-bottom: 15px;
}
.login-submit:hover {
transform: translateY(-2px);
box-shadow: 0 10px 20px rgba(102, 126, 234, 0.3);
}
.login-submit:disabled {
opacity: 0.6;
cursor: not-allowed;
transform: none;
}
.login-error {
background: rgba(220, 53, 69, 0.1);
color: #dc3545;
padding: 10px;
border-radius: 8px;
margin-bottom: 15px;
border: 1px solid rgba(220, 53, 69, 0.2);
display: none;
}
/* βœ… API Key Modal Styles */
.api-key-modal {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.8);
display: flex;
justify-content: center;
align-items: center;
z-index: 1000;
}
.api-key-form {
background: white;
padding: 40px;
border-radius: 20px;
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
max-width: 500px;
width: 90%;
text-align: center;
}
.api-key-form h2 {
color: #333;
margin-bottom: 20px;
font-size: 1.8rem;
}
.api-key-form p {
color: #666;
margin-bottom: 30px;
line-height: 1.5;
}
.api-key-input-group {
position: relative;
margin-bottom: 20px;
}
.api-key-input {
width: 100%;
padding: 15px 50px 15px 15px;
border: 2px solid #e0e0e0;
border-radius: 10px;
font-size: 16px;
transition: border-color 0.3s ease;
}
.api-key-input:focus {
outline: none;
border-color: #667eea;
box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
}
.toggle-visibility {
position: absolute;
right: 15px;
top: 50%;
transform: translateY(-50%);
background: none;
border: none;
font-size: 16px;
cursor: pointer;
color: #667eea;
transition: color 0.3s ease;
}
.toggle-visibility:hover {
color: #764ba2;
}
.api-key-submit {
background: linear-gradient(135deg, #667eea, #764ba2);
color: white;
border: none;
padding: 15px 30px;
border-radius: 10px;
cursor: pointer;
font-weight: 600;
font-size: 16px;
transition: all 0.3s ease;
}
.api-key-submit:hover {
transform: translateY(-2px);
box-shadow: 0 10px 20px rgba(102, 126, 234, 0.3);
}
.api-key-submit:disabled {
opacity: 0.6;
cursor: not-allowed;
transform: none;
}
.container {
max-width: 1400px;
margin: 0 auto;
padding: 20px;
position: relative;
z-index: 1;
}
.header {
text-align: center;
margin-bottom: 40px;
color: white;
}
.header h1 {
font-size: 3rem;
font-weight: 700;
margin-bottom: 10px;
text-shadow: 2px 2px 4px rgba(0,0,0,0.3);
background: linear-gradient(45deg, #fff, #e0e0e0);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
.header p {
font-size: 1.2rem;
opacity: 0.9;
}
/* βœ… User Status in header */
.user-status {
display: inline-flex;
align-items: center;
gap: 15px;
background: rgba(255, 255, 255, 0.2);
padding: 8px 15px;
border-radius: 20px;
margin-top: 10px;
font-size: 14px;
}
.user-info {
display: flex;
align-items: center;
gap: 8px;
}
.api-key-status {
display: flex;
align-items: center;
gap: 8px;
cursor: pointer;
transition: background 0.3s ease;
padding: 4px 8px;
border-radius: 15px;
}
.api-key-status:hover {
background: rgba(255, 255, 255, 0.1);
}
.logout-btn {
background: rgba(255, 255, 255, 0.2);
color: white;
border: none;
padding: 4px 12px;
border-radius: 15px;
cursor: pointer;
font-size: 12px;
transition: background 0.3s ease;
}
.logout-btn:hover {
background: rgba(255, 255, 255, 0.3);
}
.main-content {
display: grid;
grid-template-columns: 350px 1fr;
gap: 30px;
height: calc(100vh - 200px);
}
.sidebar {
background: rgba(255, 255, 255, 0.95);
backdrop-filter: blur(10px);
border-radius: 20px;
padding: 30px;
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.1);
overflow-y: auto;
}
.chat-container {
background: rgba(255, 255, 255, 0.95);
backdrop-filter: blur(10px);
border-radius: 20px;
display: flex;
flex-direction: column;
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.1);
}
.section-title {
font-size: 1.3rem;
font-weight: 600;
margin-bottom: 20px;
color: #333;
border-bottom: 2px solid #667eea;
padding-bottom: 10px;
}
.upload-area {
border: 2px dashed #667eea;
border-radius: 15px;
padding: 30px;
text-align: center;
margin-bottom: 20px;
transition: all 0.3s ease;
cursor: pointer;
}
.upload-area:hover {
border-color: #764ba2;
background: rgba(102, 126, 234, 0.05);
transform: translateY(-2px);
}
.upload-area.dragover {
border-color: #764ba2;
background: rgba(102, 126, 234, 0.1);
transform: scale(1.02);
}
.upload-icon {
font-size: 3rem;
color: #667eea;
margin-bottom: 15px;
}
.input-group {
margin-bottom: 20px;
}
.input-group label {
display: block;
margin-bottom: 8px;
font-weight: 600;
color: #333;
}
.input-group input {
width: 100%;
padding: 12px;
border: 2px solid #e0e0e0;
border-radius: 10px;
font-size: 14px;
transition: border-color 0.3s ease;
}
.input-group input:focus {
outline: none;
border-color: #667eea;
box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
}
.btn {
background: linear-gradient(135deg, #667eea, #764ba2);
color: white;
border: none;
padding: 12px 24px;
border-radius: 10px;
cursor: pointer;
font-weight: 600;
transition: all 0.3s ease;
width: 100%;
margin-bottom: 10px;
}
.btn:hover {
transform: translateY(-2px);
box-shadow: 0 10px 20px rgba(102, 126, 234, 0.3);
}
.btn:disabled {
opacity: 0.6;
cursor: not-allowed;
transform: none;
}
.papers-list {
margin-top: 30px;
}
.paper-item {
background: linear-gradient(135deg, #f8f9ff, #f0f2ff);
padding: 15px;
border-radius: 12px;
margin-bottom: 15px;
border-left: 4px solid #667eea;
transition: all 0.3s ease;
}
.paper-item:hover {
transform: translateX(5px);
box-shadow: 0 5px 15px rgba(102, 126, 234, 0.2);
}
.paper-title {
font-weight: 600;
color: #333;
margin-bottom: 8px;
font-size: 14px;
line-height: 1.4;
}
.paper-actions {
display: flex;
gap: 8px;
}
.btn-small {
background: linear-gradient(135deg, #667eea, #764ba2);
color: white;
border: none;
padding: 6px 12px;
border-radius: 6px;
cursor: pointer;
font-size: 12px;
font-weight: 500;
transition: all 0.3s ease;
}
.btn-small:hover {
transform: translateY(-1px);
box-shadow: 0 5px 10px rgba(102, 126, 234, 0.3);
}
.chat-header {
padding: 25px 30px;
border-bottom: 1px solid rgba(0, 0, 0, 0.1);
background: linear-gradient(135deg, #667eea, #764ba2);
color: white;
border-radius: 20px 20px 0 0;
}
.chat-header h2 {
font-size: 1.5rem;
font-weight: 600;
}
.chat-messages {
flex: 1;
padding: 20px;
overflow-y: auto;
max-height: calc(100vh - 400px);
}
.message {
margin-bottom: 20px;
animation: fadeInUp 0.5s ease;
}
@keyframes fadeInUp {
from {
opacity: 0;
transform: translateY(20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.message.user {
text-align: right;
}
.message.bot {
text-align: left;
}
.message-bubble {
display: inline-block;
max-width: 70%;
padding: 15px 20px;
border-radius: 20px;
font-size: 14px;
line-height: 1.5;
}
.message.user .message-bubble {
background: linear-gradient(135deg, #667eea, #764ba2);
color: white;
border-bottom-right-radius: 5px;
}
.message.bot .message-bubble {
background: #f8f9fa;
color: #333;
border-bottom-left-radius: 5px;
border: 1px solid #e9ecef;
}
/* Enhanced styles for Markdown content in bot messages */
.message.bot .message-bubble h1,
.message.bot .message-bubble h2,
.message.bot .message-bubble h3 {
color: #667eea;
margin: 15px 0 10px 0;
font-weight: 600;
}
.message.bot .message-bubble h1 { font-size: 18px; }
.message.bot .message-bubble h2 { font-size: 16px; }
.message.bot .message-bubble h3 { font-size: 14px; }
.message.bot .message-bubble strong {
color: #495057;
font-weight: 600;
}
.message.bot .message-bubble table {
width: 100%;
border-collapse: collapse;
margin: 15px 0;
font-size: 12px;
}
.message.bot .message-bubble table th,
.message.bot .message-bubble table td {
padding: 8px 12px;
text-align: left;
border: 1px solid #dee2e6;
}
.message.bot .message-bubble table th {
background: linear-gradient(135deg, #667eea, #764ba2);
color: white;
font-weight: 600;
}
.message.bot .message-bubble table tr:nth-child(even) {
background-color: rgba(102, 126, 234, 0.05);
}
.message.bot .message-bubble ul,
.message.bot .message-bubble ol {
margin: 10px 0;
padding-left: 20px;
}
.message.bot .message-bubble li {
margin-bottom: 5px;
}
.message.bot .message-bubble code {
background: rgba(102, 126, 234, 0.1);
padding: 2px 6px;
border-radius: 4px;
font-family: 'Courier New', monospace;
font-size: 12px;
}
.message.bot .message-bubble pre {
background: rgba(102, 126, 234, 0.1);
padding: 15px;
border-radius: 8px;
overflow-x: auto;
margin: 10px 0;
}
.message.bot .message-bubble pre code {
background: none;
padding: 0;
}
.message.bot .message-bubble blockquote {
border-left: 4px solid #667eea;
padding-left: 15px;
margin: 15px 0;
font-style: italic;
color: #6c757d;
}
.chat-input {
padding: 20px;
border-top: 1px solid rgba(0, 0, 0, 0.1);
background: rgba(248, 249, 250, 0.5);
border-radius: 0 0 20px 20px;
}
.input-container {
display: flex;
gap: 10px;
}
.chat-input input {
flex: 1;
padding: 15px 20px;
border: 2px solid #e0e0e0;
border-radius: 25px;
font-size: 14px;
transition: border-color 0.3s ease;
}
.chat-input input:focus {
outline: none;
border-color: #667eea;
box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
}
.send-btn {
background: linear-gradient(135deg, #667eea, #764ba2);
color: white;
border: none;
padding: 15px 25px;
border-radius: 25px;
cursor: pointer;
font-weight: 600;
transition: all 0.3s ease;
}
.send-btn:hover {
transform: scale(1.05);
box-shadow: 0 5px 15px rgba(102, 126, 234, 0.3);
}
.loading {
display: inline-block;
width: 20px;
height: 20px;
border: 3px solid #f3f3f3;
border-top: 3px solid #667eea;
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.status-indicator {
padding: 8px 12px;
border-radius: 20px;
font-size: 12px;
font-weight: 500;
margin-bottom: 15px;
text-align: center;
}
.status-ready {
background: rgba(40, 167, 69, 0.1);
color: #28a745;
border: 1px solid rgba(40, 167, 69, 0.2);
}
.status-processing {
background: rgba(255, 193, 7, 0.1);
color: #ffc107;
border: 1px solid rgba(255, 193, 7, 0.2);
}
.status-empty {
background: rgba(108, 117, 125, 0.1);
color: #6c757d;
border: 1px solid rgba(108, 117, 125, 0.2);
}
@media (max-width: 1024px) {
.main-content {
grid-template-columns: 1fr;
gap: 20px;
}
.sidebar {
order: 2;
}
.chat-container {
order: 1;
height: 60vh;
}
}
@media (max-width: 768px) {
.container {
padding: 10px;
}
.header h1 {
font-size: 2rem;
}
.main-content {
height: auto;
}
.message-bubble {
max-width: 85%;
}
.message.bot .message-bubble table {
font-size: 10px;
}
.message.bot .message-bubble table th,
.message.bot .message-bubble table td {
padding: 4px 6px;
}
}
</style>
</head>
<body>
<canvas class="bg-animation"></canvas>
<!-- βœ… Login Modal -->
<div id="loginModal" class="login-modal">
<div class="login-form">
<h2 id="authModalTitle">πŸ” Login</h2>
<p id="authModalDesc">Please enter your credentials to access the Research Paper Chatbot.</p>
<div class="login-error" id="loginError">Invalid username or password</div>
<div class="login-input-group">
<input type="text" id="usernameInput" class="login-input" placeholder="Username">
</div>
<div class="login-input-group">
<input type="password" id="passwordInput" class="login-input" placeholder="Password">
</div>
<button id="authSubmit" class="login-submit">Login</button>
<div style="text-align: center; margin-top: 15px;">
<span style="color: #666;">Don't have an account? </span>
<button type="button" id="toggleAuth" style="background: none; border: none; color: #667eea; cursor: pointer; text-decoration: underline; font-weight: 600;">Register here</button>
</div>
</div>
</div>
<!-- βœ… API Key Modal -->
<div id="apiKeyModal" class="api-key-modal" style="display: none;">
<div class="api-key-form">
<h2>πŸ”‘ Enter GROQ API Key</h2>
<p>Please enter your GROQ API key to use the chatbot. Your key will be stored securely in your session.</p>
<div class="api-key-input-group">
<input type="password" id="apiKeyInput" class="api-key-input" placeholder="Enter your GROQ API key...">
<button type="button" class="toggle-visibility" onclick="toggleApiKeyVisibility()">πŸ‘οΈ</button>
</div>
<button id="apiKeySubmit" class="api-key-submit">Connect</button>
</div>
</div>
<div class="container">
<div class="header">
<h1>🧠 PaperBotXiv: Research Paper Chatbot</h1>
<p>Upload papers, explore references, and chat with your research collection</p>
<!-- βœ… User Status -->
<div class="user-status">
<div class="user-info">
<span>πŸ‘€</span>
<span id="currentUser">Not logged in</span>
</div>
<div class="api-key-status" onclick="showApiKeyModal()">
<span>πŸ”‘</span>
<span id="apiKeyStatusText">API Key: Not connected</span>
</div>
<button class="logout-btn" onclick="logout()">Logout</button>
</div>
</div>
<div class="main-content">
<div class="sidebar">
<div class="section-title" style="text-align: center;">πŸ“„ Add Papers</div>
<div class="upload-area" id="uploadArea">
<div class="upload-icon">πŸ“Ž</div>
<p><strong>Drop PDF files here</strong></p>
<p>or click to browse</p>
<input type="file" id="fileInput" accept=".pdf" multiple style="display: none;">
</div>
<div class="input-group">
<label for="arxivUrl" style="display: block; text-align: center;">OR</label>
<input type="text" id="arxivUrl" placeholder="ArXiv URL/ID" style="text-align: center;">
</div>
<button class="btn" id="addArxivBtn">Add from ArXiv</button>
<div id="statusIndicator" class="status-indicator status-empty">
No papers loaded
</div>
<div class="papers-list">
<div class="section-title" style="text-align: center;">πŸ“š Your Papers</div>
<div id="papersList"></div>
</div>
</div>
<div class="chat-container">
<div class="chat-header"><h2>πŸ’¬ Chat with Papers</h2></div>
<div class="chat-messages" id="chatMessages">
<div class="message bot">
<div class="message-bubble">
Welcome! Upload some research papers to get started. I can help you understand the content, find connections between papers, and answer questions about your research collection. You can also ask me general questions anytime!
</div>
</div>
</div>
<div class="chat-input">
<div class="input-container">
<input type="text" id="questionInput" placeholder="Ask about your papers...">
<button class="send-btn" id="sendBtn">Send</button>
</div>
</div>
</div>
</div>
</div>
<script>
const API_BASE = "https://yashb1-rag-chatbot-backend.hf.space"; // FastAPI backend
// App state
let papers = [];
let isProcessing = false;
let isLoggedIn = false;
let authCredentials = null;
let isLoginMode = true; // true for login, false for register
// DOM elements
const loginModal = document.getElementById('loginModal');
const usernameInput = document.getElementById('usernameInput');
const passwordInput = document.getElementById('passwordInput');
const authSubmit = document.getElementById('authSubmit');
const loginError = document.getElementById('loginError');
const currentUser = document.getElementById('currentUser');
const authModalTitle = document.getElementById('authModalTitle');
const authModalDesc = document.getElementById('authModalDesc');
const toggleAuth = document.getElementById('toggleAuth');
const apiKeyModal = document.getElementById('apiKeyModal');
const apiKeyInput = document.getElementById('apiKeyInput');
const apiKeySubmit = document.getElementById('apiKeySubmit');
const apiKeyStatusText = document.getElementById('apiKeyStatusText');
const uploadArea = document.getElementById('uploadArea');
const fileInput = document.getElementById('fileInput');
const arxivUrl = document.getElementById('arxivUrl');
const addArxivBtn = document.getElementById('addArxivBtn');
const statusIndicator = document.getElementById('statusIndicator');
const papersList = document.getElementById('papersList');
const chatMessages = document.getElementById('chatMessages');
const questionInput = document.getElementById('questionInput');
const sendBtn = document.getElementById('sendBtn');
// Configure Markdown parser for better table rendering
marked.setOptions({
breaks: true,
gfm: true,
tables: true,
sanitize: false
});
// βœ… Authentication Functions
function showLoginModal() {
loginModal.style.display = 'flex';
usernameInput.focus();
showLoginError(''); // Clear any errors using the error function
updateAuthModal();
}
function hideLoginModal() {
loginModal.style.display = 'none';
}
function toggleAuthMode() {
isLoginMode = !isLoginMode;
updateAuthModal();
showLoginError(''); // Clear any errors using the error function
usernameInput.value = '';
passwordInput.value = '';
usernameInput.focus();
}
function updateAuthModal() {
if (isLoginMode) {
authModalTitle.textContent = 'πŸ” Login';
authModalDesc.textContent = 'Please enter your credentials to access the Research Paper Chatbot.';
authSubmit.textContent = 'Login';
toggleAuth.innerHTML = '<span style="color: #666;">Don\'t have an account? </span><span style="color: #667eea; text-decoration: underline;">Register here</span>';
} else {
authModalTitle.textContent = 'πŸ“ Register';
authModalDesc.textContent = 'Create a new account to access the Research Paper Chatbot.';
authSubmit.textContent = 'Register';
toggleAuth.innerHTML = '<span style="color: #666;">Already have an account? </span><span style="color: #667eea; text-decoration: underline;">Login here</span>';
}
}
async function login() {
const username = usernameInput.value.trim();
const password = passwordInput.value.trim();
if (!username || !password) {
showLoginError('Please enter both username and password');
return;
}
// Validation for registration
if (!isLoginMode) {
if (username.length < 3) {
showLoginError('Username must be at least 3 characters');
return;
}
if (password.length < 6) {
showLoginError('Password must be at least 6 characters');
return;
}
}
authSubmit.disabled = true;
authSubmit.textContent = isLoginMode ? 'Logging in...' : 'Creating account...';
try {
if (isLoginMode) {
// Login flow
const credentials = btoa(`${username}:${password}`);
// Test authentication by making a simple request
const response = await fetch(`${API_BASE}/ask/?q=test`, {
method: 'GET',
headers: {
'Authorization': `Basic ${credentials}`
}
});
if (response.ok) {
// Login successful
authCredentials = credentials;
isLoggedIn = true;
currentUser.textContent = username;
hideLoginModal();
// Check for existing user data and API key
await checkExistingUserData();
} else if (response.status === 401) {
showLoginError('Invalid username or password');
} else {
showLoginError(`Login failed: ${response.statusText}`);
}
} else {
// Registration flow
const response = await fetch(`${API_BASE}/register/`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
username: username,
password: password
})
});
if (response.ok) {
// Registration successful, switch to login mode
showLoginError(''); // Clear any errors
addBotMessage('βœ… Account created successfully! Please login with your new credentials.');
isLoginMode = true;
updateAuthModal();
usernameInput.value = username; // Keep username filled
passwordInput.value = '';
passwordInput.focus();
} else {
const errorData = await response.json();
showLoginError(errorData.detail || 'Registration failed');
}
}
} catch (error) {
console.error('Auth error:', error);
showLoginError('Failed to connect to server');
} finally {
authSubmit.disabled = false;
authSubmit.textContent = isLoginMode ? 'Login' : 'Register';
}
}
function showLoginError(message) {
if (message && message.trim()) {
loginError.textContent = message;
loginError.style.display = 'block';
} else {
loginError.textContent = '';
loginError.style.display = 'none';
}
}
async function checkExistingUserData() {
try {
// Get user's existing data
const userDataResponse = await makeAuthenticatedRequest(`${API_BASE}/user_data/`);
if (userDataResponse.ok) {
const userData = await userDataResponse.json();
// Restore papers if user has any
if (userData.detailed_papers && userData.detailed_papers.length > 0) {
papers = [];
let nextId = Date.now();
userData.detailed_papers.forEach((paperData, index) => {
// Add main paper
const paper = {
id: nextId++,
title: paperData.title,
type: 'restored',
hasReferences: paperData.has_references,
referencesLoaded: paperData.references_loaded
};
papers.push(paper);
// Add reference papers if they exist
if (paperData.references && paperData.references.length > 0) {
paperData.references.forEach(refTitle => {
papers.push({
id: nextId++,
title: refTitle,
type: 'reference',
hasReferences: false,
referencesLoaded: false,
isReference: true
});
});
}
});
updatePapersList();
updateStatus();
addBotMessage(`βœ… Welcome back! Restored all paper(s) from your previous session.`);
} else if (userData.papers && userData.papers.length > 0) {
// Fallback to old format for backward compatibility
papers = userData.papers.map((title, index) => ({
id: Date.now() + index,
title: title,
type: 'restored',
hasReferences: false,
referencesLoaded: false
}));
updatePapersList();
updateStatus();
addBotMessage(`βœ… Welcome back! Restored ${userData.papers.length} paper(s) from your previous session.`);
}
// Check API key status
if (userData.has_api_key) {
apiKeyStatusText.textContent = 'API Key: Connected';
showApiKeyModalWithOption();
} else {
// Clear API key input and status for new login session
apiKeyInput.value = '';
apiKeyStatusText.textContent = 'API Key: Not connected';
showApiKeyModal();
}
} else {
// Fallback to normal flow
apiKeyInput.value = '';
apiKeyStatusText.textContent = 'API Key: Not connected';
showApiKeyModal();
addBotMessage('βœ… Login successful! Please enter your GROQ API key to continue.');
}
} catch (error) {
console.error('Error checking user data:', error);
// Fallback to normal flow
apiKeyInput.value = '';
apiKeyStatusText.textContent = 'API Key: Not connected';
showApiKeyModal();
addBotMessage('βœ… Login successful! Please enter your GROQ API key to continue.');
}
}
function showApiKeyModalWithOption() {
if (!isLoggedIn) {
showLoginModal();
return;
}
// Update modal to show option for existing API key
const apiKeyForm = document.querySelector('.api-key-form');
apiKeyForm.innerHTML = `
<h2>πŸ”‘ API Key Options</h2>
<p>You have an existing GROQ API key. Choose an option:</p>
<button type="button" class="api-key-submit" onclick="useExistingApiKey()" style="margin-bottom: 15px; width: 100%;">
Use Existing API Key
</button>
<button type="button" class="api-key-submit" onclick="enterNewApiKey()" style="background: rgba(102, 126, 234, 0.1); color: #667eea; width: 100%;">
Enter New API Key
</button>
`;
apiKeyModal.style.display = 'flex';
}
async function useExistingApiKey() {
hideApiKeyModal();
addBotMessage('βœ… Using your existing API key. You can start uploading papers and asking questions!');
}
function enterNewApiKey() {
// Reset modal to normal API key entry
const apiKeyForm = document.querySelector('.api-key-form');
apiKeyForm.innerHTML = `
<h2>πŸ”‘ Enter GROQ API Key</h2>
<p>Please enter your GROQ API key to use the chatbot. Your key will be stored securely in your session.</p>
<div class="api-key-input-group">
<input type="password" id="apiKeyInput" class="api-key-input" placeholder="Enter your GROQ API key...">
<button type="button" class="toggle-visibility" onclick="toggleApiKeyVisibility()">πŸ‘οΈ</button>
</div>
<button id="apiKeySubmit" class="api-key-submit">Connect</button>
`;
// Re-initialize the input and submit button references
const newApiKeyInput = document.getElementById('apiKeyInput');
const newApiKeySubmit = document.getElementById('apiKeySubmit');
newApiKeyInput.value = '';
newApiKeyInput.focus();
// Re-add event listeners
newApiKeySubmit.addEventListener('click', setApiKey);
newApiKeyInput.addEventListener('keypress', (e) => {
if (e.key === 'Enter') setApiKey();
});
}
function logout() {
isLoggedIn = false;
authCredentials = null;
isLoginMode = true; // Reset to login mode
currentUser.textContent = 'Not logged in';
apiKeyStatusText.textContent = 'API Key: Not connected';
papers = [];
updatePapersList();
updateStatus();
// Clear input fields
usernameInput.value = '';
passwordInput.value = '';
// Clear API key input
apiKeyInput.value = '';
// Clear chat messages
chatMessages.innerHTML = `
<div class="message bot">
<div class="message-bubble">
Welcome! Upload some research papers to get started. I can help you understand the content, find connections between papers, and answer questions about your research collection. You can also ask me general questions anytime!
</div>
</div>
`;
showLoginModal();
}
// βœ… API Key Functions
function toggleApiKeyVisibility() {
// Always get the current API key input element dynamically
const currentApiKeyInput = document.getElementById('apiKeyInput');
const button = document.querySelector('.toggle-visibility');
if (!currentApiKeyInput) return;
if (currentApiKeyInput.type === 'password') {
currentApiKeyInput.type = 'text';
button.textContent = 'πŸ™ˆ';
} else {
currentApiKeyInput.type = 'password';
button.textContent = 'πŸ‘οΈ';
}
}
function showApiKeyModal() {
if (!isLoggedIn) {
showLoginModal();
return;
}
// Clear the API key input for new users
const currentApiKeyInput = document.getElementById('apiKeyInput');
if (currentApiKeyInput) {
currentApiKeyInput.value = '';
}
apiKeyModal.style.display = 'flex';
if (currentApiKeyInput) {
currentApiKeyInput.focus();
}
}
function hideApiKeyModal() {
apiKeyModal.style.display = 'none';
}
async function setApiKey() {
// Always get the current API key input element dynamically
const currentApiKeyInput = document.getElementById('apiKeyInput');
const currentApiKeySubmit = document.getElementById('apiKeySubmit');
if (!currentApiKeyInput) {
alert('API key input not found');
return;
}
const key = currentApiKeyInput.value.trim();
if (!key) {
alert('Please enter a valid API key');
return;
}
currentApiKeySubmit.disabled = true;
currentApiKeySubmit.textContent = 'Connecting...';
try {
// Send API key to backend for ephemeral storage
const response = await fetch(`${API_BASE}/set_api_key/`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Basic ${authCredentials}`
},
body: JSON.stringify({ api_key: key })
});
if (!response.ok) {
throw new Error(`Failed to set API key: ${response.statusText}`);
}
apiKeyStatusText.textContent = `API Key: Connected`;
hideApiKeyModal();
addBotMessage('βœ… GROQ API key connected successfully!');
} catch (error) {
console.error('API key setup error:', error);
addBotMessage(`❌ Failed to connect API key: ${error.message}`);
} finally {
currentApiKeySubmit.disabled = false;
currentApiKeySubmit.textContent = 'Connect';
}
}
// βœ… Initialize app - show login modal first
function initApp() {
showLoginModal();
}
// Background animation
function initBackgroundAnimation() {
const canvas = document.querySelector('.bg-animation');
const ctx = canvas.getContext('2d');
let animationId;
function resizeCanvas() {
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
}
resizeCanvas();
window.addEventListener('resize', resizeCanvas);
const particles = [];
for (let i = 0; i < 50; i++) {
particles.push({
x: Math.random() * canvas.width,
y: Math.random() * canvas.height,
vx: (Math.random() - 0.5) * 0.5,
vy: (Math.random() - 0.5) * 0.5,
size: Math.random() * 2 + 1
});
}
function animate() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.fillStyle = 'rgba(255, 255, 255, 0.1)';
particles.forEach(particle => {
particle.x += particle.vx;
particle.y += particle.vy;
if (particle.x < 0 || particle.x > canvas.width) particle.vx *= -1;
if (particle.y < 0 || particle.y > canvas.height) particle.vy *= -1;
ctx.beginPath();
ctx.arc(particle.x, particle.y, particle.size, 0, Math.PI * 2);
ctx.fill();
});
animationId = requestAnimationFrame(animate);
}
animate();
}
// Event Listeners
authSubmit.addEventListener('click', login);
toggleAuth.addEventListener('click', toggleAuthMode);
usernameInput.addEventListener('keypress', (e) => {
if (e.key === 'Enter') passwordInput.focus();
});
passwordInput.addEventListener('keypress', (e) => {
if (e.key === 'Enter') login();
});
apiKeySubmit.addEventListener('click', setApiKey);
apiKeyInput.addEventListener('keypress', (e) => {
if (e.key === 'Enter') setApiKey();
});
// File upload handlers
uploadArea.addEventListener('click', () => {
if (!isLoggedIn) {
addBotMessage('Please login first to upload papers');
return;
}
fileInput.click();
});
uploadArea.addEventListener('dragover', e => { e.preventDefault(); if (isLoggedIn) uploadArea.classList.add('dragover'); });
uploadArea.addEventListener('dragleave', () => uploadArea.classList.remove('dragover'));
uploadArea.addEventListener('drop', e => {
e.preventDefault();
uploadArea.classList.remove('dragover');
if (isLoggedIn) handleFiles(e.dataTransfer.files);
else addBotMessage('Please login first to upload papers');
});
fileInput.addEventListener('change', e => handleFiles(e.target.files));
// ArXiv handler
addArxivBtn.addEventListener('click', () => {
if (!isLoggedIn) {
addBotMessage('Please login first to add papers');
return;
}
const url = arxivUrl.value.trim();
if (url) { addPaperFromArxiv(url); arxivUrl.value = ''; }
});
// Chat handlers
questionInput.addEventListener('keypress', e => {
if (e.key === 'Enter') {
if (!isLoggedIn) {
addBotMessage('Please login first to ask questions');
return;
}
sendMessage();
}
});
sendBtn.addEventListener('click', () => {
if (!isLoggedIn) {
addBotMessage('Please login first to ask questions');
return;
}
sendMessage();
});
// Helper function to make authenticated requests
async function makeAuthenticatedRequest(url, options = {}) {
if (!authCredentials) {
throw new Error('Not authenticated');
}
const headers = {
'Authorization': `Basic ${authCredentials}`,
...options.headers
};
return fetch(url, {
...options,
headers
});
}
// Functions (updated to use authentication)
function handleFiles(files) {
Array.from(files).forEach(file => {
if (file.type === 'application/pdf') addPaperFromPDF(file);
});
}
async function addPaperFromPDF(file) {
setStatus('processing', 'Processing PDF...');
const formData = new FormData();
formData.append("file", file);
try {
const res = await makeAuthenticatedRequest(`${API_BASE}/upload_pdf/`, {
method: "POST",
body: formData
});
if (!res.ok) {
throw new Error(`HTTP ${res.status}: ${res.statusText}`);
}
const data = await res.json();
const paper = {
id: Date.now(),
title: data.context_papers.slice(-1)[0] || file.name,
type: 'pdf',
hasReferences: false,
referencesLoaded: false
};
papers.push(paper);
updatePapersList();
isProcessing = false;
updateStatus();
addBotMessage(`βœ… Added: "${paper.title}"`);
} catch (err) {
console.error('PDF upload error:', err);
addBotMessage(`❌ PDF upload failed: ${err.message}`);
isProcessing = false;
updateStatus();
}
}
async function addPaperFromArxiv(url) {
setStatus('processing', 'Fetching from ArXiv...');
const formData = new FormData();
formData.append("arxiv_id", url);
try {
const res = await makeAuthenticatedRequest(`${API_BASE}/add_arxiv/`, {
method: "POST",
body: formData
});
if (!res.ok) {
throw new Error(`HTTP ${res.status}: ${res.statusText}`);
}
const data = await res.json();
const paper = {
id: Date.now(),
title: data.context_papers.slice(-1)[0] || `ArXiv: ${url}`,
type: 'arxiv',
hasReferences: false,
referencesLoaded: false
};
papers.push(paper);
updatePapersList();
isProcessing = false;
updateStatus();
addBotMessage(`βœ… Added: "${paper.title}"`);
} catch (err) {
console.error('ArXiv fetch error:', err);
addBotMessage(`❌ ArXiv fetch failed: ${err.message}`);
isProcessing = false;
updateStatus();
}
}
async function addReferences(paperId) {
const paperIndex = papers.findIndex(p => p.id === paperId);
if (paperIndex === -1) {
addBotMessage("❌ Paper not found");
return;
}
if (papers[paperIndex].referencesLoaded) {
addBotMessage(`πŸ“š References for "${papers[paperIndex].title}" already loaded`);
return;
}
// Calculate the backend index by counting only main papers (not reference papers) before this one
let backendIndex = 0;
for (let i = 0; i < paperIndex; i++) {
if (!papers[i].isReference) {
backendIndex++;
}
}
console.log(`Frontend index: ${paperIndex}, Backend index: ${backendIndex}`);
console.log(`Paper: "${papers[paperIndex].title}", Type: ${papers[paperIndex].type}`);
setStatus('processing', 'Loading references...(might take longer)');
const formData = new FormData();
formData.append("index", backendIndex.toString());
try {
const res = await makeAuthenticatedRequest(`${API_BASE}/add_references/`, {
method: "POST",
body: formData
});
if (!res.ok) {
throw new Error(`HTTP ${res.status}: ${res.statusText}`);
}
const data = await res.json();
papers[paperIndex].hasReferences = true;
papers[paperIndex].referencesLoaded = true;
if (data.references && data.references.length > 0) {
data.references.forEach(refTitle => {
papers.push({
id: Date.now() + Math.random(),
title: refTitle,
type: 'reference',
hasReferences: false,
referencesLoaded: false,
isReference: true
});
});
}
updatePapersList();
isProcessing = false;
updateStatus();
if (data.references && data.references.length > 0) {
addBotMessage(`πŸ“š Added ${data.references.length} references for "${papers[paperIndex].title}": ${data.references.join(", ")}`);
} else {
addBotMessage(`πŸ“š No references found for "${papers[paperIndex].title}"`);
}
} catch (err) {
console.error('Reference fetch error:', err);
addBotMessage(`❌ Reference fetch failed for "${papers[paperIndex].title}": ${err.message}`);
isProcessing = false;
updateStatus();
}
}
async function sendMessage() {
const question = questionInput.value.trim();
if (!question) return;
if (papers.length === 0) {
addBotMessage("Please upload some papers first before asking questions!");
return;
}
addUserMessage(question);
questionInput.value = '';
setStatus('processing', 'Thinking...');
try {
const res = await makeAuthenticatedRequest(`${API_BASE}/ask/?q=${encodeURIComponent(question)}`);
if (!res.ok) {
throw new Error(`HTTP ${res.status}: ${res.statusText}`);
}
const data = await res.json();
addBotMessage(data.answer || "I couldn't find an answer to your question.");
isProcessing = false;
updateStatus();
} catch (err) {
console.error('QA error:', err);
addBotMessage(`❌ Failed to get answer: ${err.message}`);
isProcessing = false;
updateStatus();
}
}
function updatePapersList() {
papersList.innerHTML = '';
papers.forEach(paper => {
const div = document.createElement('div');
div.className = 'paper-item';
if (paper.isReference) {
div.style.borderLeft = '4px solid #28a745';
div.style.backgroundColor = 'rgba(40, 167, 69, 0.05)';
} else if (paper.type === 'restored') {
div.style.borderLeft = '4px solid #17a2b8';
div.style.backgroundColor = 'rgba(23, 162, 184, 0.05)';
}
div.innerHTML = `
<div class="paper-title">
${paper.isReference ? 'πŸ“Ž ' : ''}${paper.type === 'restored' ? 'πŸ”„ ' : ''}${paper.title}
${paper.isReference ? ' <small style="color: #6c757d;">(Reference)</small>' : ''}
${paper.type === 'restored' ? ' <small style="color: #17a2b8;">(Restored)</small>' : ''}
</div>
<div class="paper-actions">
${!paper.isReference ? `
<button class="btn-small" onclick="addReferences(${paper.id})" ${paper.referencesLoaded ? 'disabled' : ''}>
${paper.referencesLoaded ? 'βœ“ Refs Loaded' : '+ Add References'}
</button>
` : '<small style="color: #6c757d;">Reference paper</small>'}
</div>`;
papersList.appendChild(div);
});
}
function setStatus(type, message) {
statusIndicator.className = `status-indicator status-${type}`;
statusIndicator.textContent = message;
isProcessing = (type === 'processing');
if (type === 'processing') {
setTimeout(() => {
if (isProcessing) {
console.warn('Status stuck in processing, force clearing...');
updateStatus();
}
}, 30000);
}
}
function updateStatus() {
if (!isProcessing) {
if (papers.length === 0) {
setStatus('empty', 'No papers loaded');
} else {
setStatus('ready', `${papers.length} paper${papers.length > 1 ? 's' : ''} loaded`);
questionInput.disabled = false;
sendBtn.disabled = false;
}
}
}
function addUserMessage(msg) {
const div = document.createElement('div');
div.className = 'message user';
div.innerHTML = `<div class="message-bubble">${msg}</div>`;
chatMessages.appendChild(div);
chatMessages.scrollTop = chatMessages.scrollHeight;
}
function addBotMessage(msg) {
const div = document.createElement('div');
div.className = 'message bot';
const renderedMessage = marked.parse(msg);
div.innerHTML = `<div class="message-bubble">${renderedMessage}</div>`;
chatMessages.appendChild(div);
chatMessages.scrollTop = chatMessages.scrollHeight;
}
// Initialize app
initBackgroundAnimation();
initApp();
window.addReferences = addReferences;
window.logout = logout;
window.toggleAuthMode = toggleAuthMode;
window.useExistingApiKey = useExistingApiKey;
window.enterNewApiKey = enterNewApiKey;
</script>
</body>
</html>