GraphRagEx / templates /index.html
PrernaKu's picture
Upload 30 files
d095c73 verified
Raw History Blame Contribute Delete
21.1 kB
<!DOCTYPE html>
<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>