Spaces:
Sleeping
Sleeping
Download templates/index.html from PrernaKu/GraphRagEx: direct link, hf CLI and curl.
- Browser
- Download file 21.1 kB
-
https://huggingface.co/spaces/PrernaKu/GraphRagEx/resolve/main/templates/index.html
- Command line
-
hf download hf://spaces/PrernaKu/GraphRagEx/templates/index.html
-
curl -L -o index.html https://huggingface.co/spaces/PrernaKu/GraphRagEx/resolve/main/templates/index.html
21.1 kB
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>Graph RAG Chatbot</title> | |
| <script src="https://cdn.jsdelivr.net/npm/chart.js@3.9.1/dist/chart.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; | |
| padding: 20px; | |
| } | |
| .container { | |
| max-width: 1400px; | |
| margin: 0 auto; | |
| display: grid; | |
| grid-template-columns: 1fr 1fr; | |
| gap: 20px; | |
| } | |
| @media (max-width: 1024px) { | |
| .container { | |
| grid-template-columns: 1fr; | |
| } | |
| } | |
| .card { | |
| background: white; | |
| border-radius: 12px; | |
| box-shadow: 0 10px 30px rgba(0, 0, 0, 0.2); | |
| padding: 24px; | |
| display: flex; | |
| flex-direction: column; | |
| } | |
| .card h2 { | |
| color: #667eea; | |
| margin-bottom: 16px; | |
| font-size: 20px; | |
| border-bottom: 2px solid #667eea; | |
| padding-bottom: 10px; | |
| } | |
| .upload-zone { | |
| border: 2px dashed #667eea; | |
| border-radius: 8px; | |
| padding: 40px 20px; | |
| text-align: center; | |
| cursor: pointer; | |
| transition: all 0.3s ease; | |
| background: #f8f9fa; | |
| } | |
| .upload-zone:hover { | |
| background: #e8eaf6; | |
| border-color: #764ba2; | |
| } | |
| .upload-zone.dragover { | |
| background: #e8eaf6; | |
| border-color: #764ba2; | |
| transform: scale(1.02); | |
| } | |
| .upload-zone input { | |
| display: none; | |
| } | |
| .upload-zone p { | |
| color: #666; | |
| margin: 10px 0; | |
| } | |
| .button { | |
| background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); | |
| color: white; | |
| border: none; | |
| padding: 10px 20px; | |
| border-radius: 6px; | |
| cursor: pointer; | |
| font-weight: 600; | |
| transition: transform 0.2s, box-shadow 0.2s; | |
| margin: 5px; | |
| } | |
| .button:hover:not(:disabled) { | |
| transform: translateY(-2px); | |
| box-shadow: 0 5px 15px rgba(102, 126, 234, 0.4); | |
| } | |
| .button:disabled { | |
| opacity: 0.5; | |
| cursor: not-allowed; | |
| } | |
| .button.secondary { | |
| background: #f0f0f0; | |
| color: #333; | |
| } | |
| .documents-list { | |
| display: flex; | |
| flex-direction: column; | |
| gap: 12px; | |
| max-height: 500px; | |
| overflow-y: auto; | |
| } | |
| .document-item { | |
| background: #f8f9fa; | |
| border-left: 4px solid #667eea; | |
| padding: 16px; | |
| border-radius: 6px; | |
| transition: all 0.3s ease; | |
| } | |
| .document-item:hover { | |
| transform: translateX(4px); | |
| box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); | |
| } | |
| .document-item.processing { | |
| border-left-color: #ff9800; | |
| } | |
| .document-item.ready { | |
| border-left-color: #4caf50; | |
| } | |
| .document-item.error { | |
| border-left-color: #f44336; | |
| } | |
| .doc-name { | |
| font-weight: 600; | |
| color: #333; | |
| margin-bottom: 8px; | |
| } | |
| .doc-status { | |
| display: flex; | |
| align-items: center; | |
| gap: 8px; | |
| margin-bottom: 8px; | |
| } | |
| .status-badge { | |
| display: inline-block; | |
| padding: 4px 12px; | |
| border-radius: 20px; | |
| font-size: 12px; | |
| font-weight: 600; | |
| } | |
| .status-badge.processing { | |
| background: #fff3cd; | |
| color: #856404; | |
| } | |
| .status-badge.ready { | |
| background: #d4edda; | |
| color: #155724; | |
| } | |
| .status-badge.error { | |
| background: #f8d7da; | |
| color: #721c24; | |
| } | |
| .progress-bar { | |
| width: 100%; | |
| height: 6px; | |
| background: #e0e0e0; | |
| border-radius: 3px; | |
| overflow: hidden; | |
| margin-bottom: 8px; | |
| } | |
| .progress-fill { | |
| height: 100%; | |
| background: linear-gradient(90deg, #667eea, #764ba2); | |
| width: 0%; | |
| transition: width 0.3s ease; | |
| } | |
| .doc-info { | |
| display: flex; | |
| gap: 20px; | |
| font-size: 13px; | |
| color: #666; | |
| margin-bottom: 10px; | |
| } | |
| .doc-info span { | |
| display: flex; | |
| align-items: center; | |
| gap: 4px; | |
| } | |
| .doc-actions { | |
| display: flex; | |
| gap: 8px; | |
| } | |
| .doc-actions button { | |
| flex: 1; | |
| padding: 6px 12px; | |
| font-size: 12px; | |
| } | |
| .chat-container { | |
| display: flex; | |
| flex-direction: column; | |
| height: 600px; | |
| } | |
| .chat-messages { | |
| flex: 1; | |
| overflow-y: auto; | |
| margin-bottom: 16px; | |
| padding: 12px; | |
| background: #f8f9fa; | |
| border-radius: 8px; | |
| display: flex; | |
| flex-direction: column; | |
| gap: 12px; | |
| } | |
| .message { | |
| padding: 12px 16px; | |
| border-radius: 8px; | |
| word-wrap: break-word; | |
| max-width: 100%; | |
| } | |
| .message.user { | |
| background: #667eea; | |
| color: white; | |
| align-self: flex-end; | |
| margin-left: 20%; | |
| } | |
| .message.bot { | |
| background: white; | |
| color: #333; | |
| align-self: flex-start; | |
| margin-right: 20%; | |
| border: 1px solid #ddd; | |
| } | |
| .chat-input-area { | |
| display: flex; | |
| gap: 8px; | |
| margin-top: auto; | |
| } | |
| .chat-input-area input { | |
| flex: 1; | |
| padding: 12px; | |
| border: 1px solid #ddd; | |
| border-radius: 6px; | |
| font-size: 14px; | |
| } | |
| .chat-input-area input:focus { | |
| outline: none; | |
| border-color: #667eea; | |
| box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1); | |
| } | |
| select { | |
| padding: 8px; | |
| border: 1px solid #ddd; | |
| border-radius: 6px; | |
| font-size: 14px; | |
| } | |
| select:focus { | |
| outline: none; | |
| border-color: #667eea; | |
| box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1); | |
| } | |
| .graph-preview { | |
| margin-top: 12px; | |
| text-align: center; | |
| } | |
| .graph-preview img { | |
| max-width: 100%; | |
| border-radius: 6px; | |
| border: 1px solid #ddd; | |
| margin-top: 8px; | |
| } | |
| .graph-preview a { | |
| color: #667eea; | |
| text-decoration: none; | |
| font-weight: 600; | |
| display: inline-block; | |
| margin-top: 8px; | |
| } | |
| .graph-preview a:hover { | |
| text-decoration: underline; | |
| } | |
| .spinner { | |
| display: inline-block; | |
| width: 12px; | |
| height: 12px; | |
| border: 2px solid #f3f3f3; | |
| border-top: 2px solid #667eea; | |
| border-radius: 50%; | |
| animation: spin 0.8s linear infinite; | |
| } | |
| @keyframes spin { | |
| 0% { transform: rotate(0deg); } | |
| 100% { transform: rotate(360deg); } | |
| } | |
| .stats { | |
| display: grid; | |
| grid-template-columns: repeat(2, 1fr); | |
| gap: 12px; | |
| margin-top: 12px; | |
| } | |
| .stat-item { | |
| background: #f8f9fa; | |
| padding: 12px; | |
| border-radius: 6px; | |
| text-align: center; | |
| } | |
| .stat-label { | |
| font-size: 12px; | |
| color: #666; | |
| margin-bottom: 4px; | |
| } | |
| .stat-value { | |
| font-size: 18px; | |
| font-weight: 700; | |
| color: #667eea; | |
| } | |
| .error-text { | |
| color: #f44336; | |
| font-size: 12px; | |
| margin-top: 8px; | |
| } | |
| .tabs { | |
| display: flex; | |
| gap: 8px; | |
| margin-bottom: 16px; | |
| } | |
| .tab-btn { | |
| padding: 8px 16px; | |
| border: none; | |
| background: #f0f0f0; | |
| color: #666; | |
| border-radius: 6px 6px 0 0; | |
| cursor: pointer; | |
| font-weight: 600; | |
| transition: all 0.3s ease; | |
| } | |
| .tab-btn.active { | |
| background: white; | |
| color: #667eea; | |
| border-bottom: 3px solid #667eea; | |
| } | |
| .tab-content { | |
| display: none; | |
| } | |
| .tab-content.active { | |
| display: block; | |
| } | |
| .empty-state { | |
| text-align: center; | |
| color: #999; | |
| padding: 20px; | |
| } | |
| .loading-skeleton { | |
| background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%); | |
| background-size: 200% 100%; | |
| animation: loading 1.5s infinite; | |
| height: 20px; | |
| border-radius: 4px; | |
| margin-bottom: 10px; | |
| } | |
| @keyframes loading { | |
| 0% { background-position: 200% 0; } | |
| 100% { background-position: -200% 0; } | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <div class="container"> | |
| <!-- Upload Section --> | |
| <div class="card"> | |
| <h2>π€ Upload Documents</h2> | |
| <div class="upload-zone" id="uploadZone"> | |
| <svg style="width: 48px; height: 48px; margin: 0 auto; color: #667eea;" fill="none" stroke="currentColor" viewBox="0 0 24 24"> | |
| <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M15 13l-3-3m0 0l-3 3m3-3v12"/> | |
| </svg> | |
| <p><strong>Drop files here or click to upload</strong></p> | |
| <p style="font-size: 12px;">Supported: PDF, CSV, TXT (max 50MB)</p> | |
| <input type="file" id="fileInput" multiple accept=".pdf,.csv,.txt"> | |
| </div> | |
| <div style="margin-top: 16px;"> | |
| <h3 style="margin-bottom: 12px;">π Uploaded Documents</h3> | |
| <div class="documents-list" id="documentsList"> | |
| <div class="empty-state">No documents uploaded yet</div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Chat & Graph Section --> | |
| <div class="card"> | |
| <h2>π¬ RAG Chatbot</h2> | |
| <div class="tabs"> | |
| <button class="tab-btn active" onclick="switchTab('chat')">Chat</button> | |
| <button class="tab-btn" onclick="switchTab('graph')">Knowledge Graph</button> | |
| </div> | |
| <!-- Chat Tab --> | |
| <div id="chat" class="tab-content active" style="display: flex; flex-direction: column; flex: 1;"> | |
| <select id="documentSelect" style="margin-bottom: 12px;"> | |
| <option value="">Select a document...</option> | |
| </select> | |
| <div class="chat-messages" id="chatMessages"></div> | |
| <div class="chat-input-area"> | |
| <input type="text" id="queryInput" placeholder="Ask a question..." /> | |
| <button class="button" id="sendBtn" onclick="sendQuery()" disabled>Send</button> | |
| </div> | |
| </div> | |
| <!-- Graph Tab --> | |
| <div id="graph" class="tab-content" style="flex: 1; overflow-y: auto;"> | |
| <select id="graphDocSelect" style="margin-bottom: 12px; width: 100%;"> | |
| <option value="">Select a document to view graph...</option> | |
| </select> | |
| <div id="graphContainer" class="empty-state"> | |
| No graph selected | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <script> | |
| const API_BASE = '/api'; | |
| let refreshInterval; | |
| let selectedDoc = ''; | |
| let selectedGraphDoc = ''; | |
| function switchTab(tab) { | |
| document.querySelectorAll('.tab-content').forEach(el => el.classList.remove('active')); | |
| document.querySelectorAll('.tab-btn').forEach(el => el.classList.remove('active')); | |
| document.getElementById(tab).classList.add('active'); | |
| event.target.classList.add('active'); | |
| if (tab === 'graph' && selectedGraphDoc) { | |
| loadGraphVisualization(selectedGraphDoc); | |
| } | |
| } | |
| function setupUpload() { | |
| const zone = document.getElementById('uploadZone'); | |
| const fileInput = document.getElementById('fileInput'); | |
| zone.addEventListener('click', () => fileInput.click()); | |
| ['dragenter', 'dragover', 'dragleave', 'drop'].forEach(evt => { | |
| zone.addEventListener(evt, preventDefaults, false); | |
| }); | |
| function preventDefaults(e) { | |
| e.preventDefault(); | |
| e.stopPropagation(); | |
| } | |
| ['dragenter', 'dragover'].forEach(evt => { | |
| zone.addEventListener(evt, () => zone.classList.add('dragover'), false); | |
| }); | |
| ['dragleave', 'drop'].forEach(evt => { | |
| zone.addEventListener(evt, () => zone.classList.remove('dragover'), false); | |
| }); | |
| zone.addEventListener('drop', handleDrop, false); | |
| fileInput.addEventListener('change', (e) => handleFiles(e.target.files)); | |
| } | |
| function handleDrop(e) { | |
| const files = e.dataTransfer.files; | |
| handleFiles(files); | |
| } | |
| function handleFiles(files) { | |
| const formData = new FormData(); | |
| for (let file of files) { | |
| formData.append('files', file); | |
| } | |
| fetch(`${API_BASE}/upload`, { | |
| method: 'POST', | |
| body: formData | |
| }) | |
| .then(r => r.json()) | |
| .then(data => { | |
| if (data.success) { | |
| document.getElementById('fileInput').value = ''; | |
| refreshDocuments(); | |
| } | |
| }) | |
| .catch(err => console.error('Upload error:', err)); | |
| } | |
| function refreshDocuments() { | |
| fetch(`${API_BASE}/documents`) | |
| .then(r => r.json()) | |
| .then(data => { | |
| renderDocuments(data.documents); | |
| updateSelects(data.documents); | |
| }) | |
| .catch(err => console.error('Fetch error:', err)); | |
| } | |
| function renderDocuments(docs) { | |
| const container = document.getElementById('documentsList'); | |
| if (Object.keys(docs).length === 0) { | |
| container.innerHTML = '<div class="empty-state">No documents uploaded yet</div>'; | |
| return; | |
| } | |
| container.innerHTML = Object.entries(docs).map(([name, doc]) => { | |
| const status = doc.status || 'queued'; | |
| const progress = doc.progress || 0; | |
| return ` | |
| <div class="document-item ${status}"> | |
| <div class="doc-name">${name}</div> | |
| <div class="doc-status"> | |
| <span class="status-badge ${status}"> | |
| ${status === 'processing' ? '<span class="spinner"></span>' : ''} | |
| ${status.toUpperCase()} | |
| </span> | |
| </div> | |
| <div class="progress-bar"> | |
| <div class="progress-fill" style="width: ${progress}%"></div> | |
| </div> | |
| ${status === 'ready' ? ` | |
| <div class="doc-info"> | |
| <span>π ${doc.chunks || 0} chunks</span> | |
| <span>π ${doc.entities || 0} entities</span> | |
| </div> | |
| <div class="graph-preview"> | |
| <a href="${doc.graph_image}" target="_blank">π View Full Graph</a> | |
| </div> | |
| ` : ''} | |
| ${doc.error ? `<div class="error-text">β ${doc.error}</div>` : ''} | |
| <div class="doc-actions"> | |
| <button class="button secondary" style="flex: 1;" onclick="deleteDocument('${name}')">Delete</button> | |
| </div> | |
| </div> | |
| `; | |
| }).join(''); | |
| } | |
| function updateSelects(docs) { | |
| const docSelect = document.getElementById('documentSelect'); | |
| const graphDocSelect = document.getElementById('graphDocSelect'); | |
| const readyDocs = Object.entries(docs) | |
| .filter(([_, doc]) => doc.status === 'ready') | |
| .map(([name, _]) => name); | |
| [docSelect, graphDocSelect].forEach(select => { | |
| const current = select.value; | |
| select.innerHTML = '<option value="">Select a document...</option>'; | |
| readyDocs.forEach(name => { | |
| select.innerHTML += `<option value="${name}">${name}</option>`; | |
| }); | |
| select.value = current; | |
| }); | |
| if (readyDocs.length > 0) { | |
| document.getElementById('sendBtn').disabled = false; | |
| } | |
| } | |
| function deleteDocument(name) { | |
| if (!confirm(`Delete "${name}"?`)) return; | |
| fetch(`${API_BASE}/delete/${name}`, { method: 'DELETE' }) | |
| .then(() => refreshDocuments()) | |
| .catch(err => console.error('Delete error:', err)); | |
| } | |
| function sendQuery() { | |
| const query = document.getElementById('queryInput').value.trim(); | |
| const doc = document.getElementById('documentSelect').value; | |
| if (!query || !doc) return; | |
| const messagesDiv = document.getElementById('chatMessages'); | |
| messagesDiv.innerHTML += `<div class="message user">${query}</div>`; | |
| document.getElementById('queryInput').value = ''; | |
| const loadingMsg = document.createElement('div'); | |
| loadingMsg.className = 'message bot'; | |
| loadingMsg.innerHTML = '<span class="spinner"></span> Thinking...'; | |
| messagesDiv.appendChild(loadingMsg); | |
| messagesDiv.scrollTop = messagesDiv.scrollHeight; | |
| fetch(`${API_BASE}/query`, { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify({ query, document: doc }) | |
| }) | |
| .then(r => r.json()) | |
| .then(data => { | |
| loadingMsg.remove(); | |
| const answer = data.answer || data.error || 'Error processing query'; | |
| messagesDiv.innerHTML += `<div class="message bot">${answer}</div>`; | |
| messagesDiv.scrollTop = messagesDiv.scrollHeight; | |
| }) | |
| .catch(err => { | |
| loadingMsg.remove(); | |
| messagesDiv.innerHTML += `<div class="message bot">Error: ${err.message}</div>`; | |
| }); | |
| } | |
| function loadGraphVisualization(docName) { | |
| const container = document.getElementById('graphContainer'); | |
| if (!docName) { | |
| container.innerHTML = '<div class="empty-state">No document selected</div>'; | |
| return; | |
| } | |
| container.innerHTML = ` | |
| <img src="/graph-image/${docName}" alt="Knowledge Graph" style="width: 100%; border-radius: 6px;"> | |
| `; | |
| } | |
| document.getElementById('documentSelect').addEventListener('change', (e) => { | |
| selectedDoc = e.target.value; | |
| }); | |
| document.getElementById('graphDocSelect').addEventListener('change', (e) => { | |
| selectedGraphDoc = e.target.value; | |
| loadGraphVisualization(selectedGraphDoc); | |
| }); | |
| document.getElementById('queryInput').addEventListener('keypress', (e) => { | |
| if (e.key === 'Enter' && !e.shiftKey) { | |
| e.preventDefault(); | |
| sendQuery(); | |
| } | |
| }); | |
| document.addEventListener('DOMContentLoaded', () => { | |
| setupUpload(); | |
| refreshDocuments(); | |
| refreshInterval = setInterval(refreshDocuments, 1500); | |
| }); | |
| </script> | |
| </body> | |
| </html> | |