Spaces:
Running
Running
| <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> |