Spaces:
Running
Running
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>RAG Nexus | Intelligent Document Analysis</title> | |
| <!-- Importing Phosphor Icons for modern UI --> | |
| <script src="https://unpkg.com/@phosphor-icons/web"></script> | |
| <style> | |
| :root { | |
| /* Theme Colors - Gradio Soft Theme inspired */ | |
| --primary: #4f46e5; | |
| --primary-hover: #4338ca; | |
| --secondary: #8b5cf6; | |
| --background: #f3f4f6; | |
| --surface: #ffffff; | |
| --text-main: #1f2937; | |
| --text-muted: #6b7280; | |
| --border: #e5e7eb; | |
| --danger: #ef4444; | |
| --success: #10b981; | |
| --warning: #f59e0b; | |
| /* Spacing & Radius */ | |
| --radius-md: 8px; | |
| --radius-lg: 12px; | |
| --shadow-sm: 0 1px 2px 0 rgb(0 0 0 / 0.05); | |
| --shadow-md: 0 4px 6px -1px rgb(0 0 0 / 0.1); | |
| /* Fonts */ | |
| --font-family: 'Inter', system-ui, -apple-system, sans-serif; | |
| } | |
| * { | |
| box-sizing: border-box; | |
| margin: 0; | |
| padding: 0; | |
| } | |
| body { | |
| font-family: var(--font-family); | |
| background-color: var(--background); | |
| color: var(--text-main); | |
| line-height: 1.5; | |
| min-height: 100vh; | |
| display: flex; | |
| flex-direction: column; | |
| } | |
| /* --- Header --- */ | |
| header { | |
| background: var(--surface); | |
| border-bottom: 1px solid var(--border); | |
| padding: 1rem 2rem; | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: center; | |
| box-shadow: var(--shadow-sm); | |
| } | |
| .brand { | |
| display: flex; | |
| align-items: center; | |
| gap: 0.75rem; | |
| } | |
| .brand h1 { | |
| font-size: 1.25rem; | |
| font-weight: 700; | |
| background: linear-gradient(to right, var(--primary), var(--secondary)); | |
| -webkit-background-clip: text; | |
| -webkit-text-fill-color: transparent; | |
| } | |
| .brand i { | |
| font-size: 1.5rem; | |
| color: var(--primary); | |
| } | |
| .anycoder-link { | |
| font-size: 0.875rem; | |
| color: var(--text-muted); | |
| text-decoration: none; | |
| transition: color 0.2s; | |
| display: flex; | |
| align-items: center; | |
| gap: 0.5rem; | |
| } | |
| .anycoder-link:hover { | |
| color: var(--primary); | |
| } | |
| /* --- Main Layout --- */ | |
| main { | |
| flex: 1; | |
| padding: 2rem; | |
| max-width: 1200px; | |
| margin: 0 auto; | |
| width: 100%; | |
| } | |
| /* --- Tabs --- */ | |
| .tabs { | |
| display: flex; | |
| gap: 0.5rem; | |
| margin-bottom: 1.5rem; | |
| overflow-x: auto; | |
| padding-bottom: 0.5rem; | |
| border-bottom: 1px solid var(--border); | |
| } | |
| .tab-btn { | |
| background: none; | |
| border: none; | |
| padding: 0.75rem 1.25rem; | |
| font-size: 0.95rem; | |
| font-weight: 500; | |
| color: var(--text-muted); | |
| cursor: pointer; | |
| border-radius: var(--radius-md); | |
| transition: all 0.2s; | |
| display: flex; | |
| align-items: center; | |
| gap: 0.5rem; | |
| white-space: nowrap; | |
| } | |
| .tab-btn:hover { | |
| background-color: rgba(79, 70, 229, 0.05); | |
| color: var(--primary); | |
| } | |
| .tab-btn.active { | |
| background-color: var(--primary); | |
| color: white; | |
| } | |
| /* --- Content Sections --- */ | |
| .tab-content { | |
| display: none; | |
| animation: fadeIn 0.3s ease-out; | |
| } | |
| .tab-content.active { | |
| display: block; | |
| } | |
| @keyframes fadeIn { | |
| from { opacity: 0; transform: translateY(5px); } | |
| to { opacity: 1; transform: translateY(0); } | |
| } | |
| .card { | |
| background: var(--surface); | |
| border-radius: var(--radius-lg); | |
| border: 1px solid var(--border); | |
| padding: 1.5rem; | |
| box-shadow: var(--shadow-sm); | |
| margin-bottom: 1.5rem; | |
| } | |
| .card-header { | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: center; | |
| margin-bottom: 1.5rem; | |
| } | |
| .card-title { | |
| font-size: 1.1rem; | |
| font-weight: 600; | |
| color: var(--text-main); | |
| } | |
| /* --- Forms & Inputs --- */ | |
| .form-group { | |
| margin-bottom: 1rem; | |
| } | |
| label { | |
| display: block; | |
| margin-bottom: 0.5rem; | |
| font-weight: 500; | |
| font-size: 0.9rem; | |
| color: var(--text-main); | |
| } | |
| input[type="text"], | |
| textarea, | |
| select { | |
| width: 100%; | |
| padding: 0.75rem; | |
| border: 1px solid var(--border); | |
| border-radius: var(--radius-md); | |
| font-family: inherit; | |
| font-size: 0.95rem; | |
| transition: border-color 0.2s; | |
| } | |
| input[type="text"]:focus, | |
| textarea:focus, | |
| select:focus { | |
| outline: none; | |
| border-color: var(--primary); | |
| box-shadow: 0 0 0 3px rgba(79, 70, 229, 0.1); | |
| } | |
| /* --- Buttons --- */ | |
| .btn { | |
| display: inline-flex; | |
| align-items: center; | |
| justify-content: center; | |
| gap: 0.5rem; | |
| padding: 0.75rem 1.5rem; | |
| border-radius: var(--radius-md); | |
| font-weight: 500; | |
| cursor: pointer; | |
| transition: all 0.2s; | |
| border: none; | |
| font-size: 0.95rem; | |
| } | |
| .btn-primary { | |
| background-color: var(--primary); | |
| color: white; | |
| } | |
| .btn-primary:hover { | |
| background-color: var(--primary-hover); | |
| } | |
| .btn-secondary { | |
| background-color: white; | |
| border: 1px solid var(--border); | |
| color: var(--text-main); | |
| } | |
| .btn-secondary:hover { | |
| border-color: var(--text-muted); | |
| } | |
| .btn-danger { | |
| background-color: #fee2e2; | |
| color: var(--danger); | |
| } | |
| .btn-danger:hover { | |
| background-color: #fecaca; | |
| } | |
| /* --- Tables --- */ | |
| .table-container { | |
| overflow-x: auto; | |
| } | |
| table { | |
| width: 100%; | |
| border-collapse: collapse; | |
| font-size: 0.9rem; | |
| } | |
| th, td { | |
| text-align: left; | |
| padding: 0.75rem 1rem; | |
| border-bottom: 1px solid var(--border); | |
| } | |
| th { | |
| background-color: #f9fafb; | |
| font-weight: 600; | |
| color: var(--text-muted); | |
| } | |
| tr:last-child td { | |
| border-bottom: none; | |
| } | |
| /* --- Upload Area --- */ | |
| .upload-area { | |
| border: 2px dashed var(--border); | |
| border-radius: var(--radius-lg); | |
| padding: 3rem; | |
| text-align: center; | |
| cursor: pointer; | |
| transition: all 0.2s; | |
| background-color: #f9fafb; | |
| } | |
| .upload-area:hover, .upload-area.dragover { | |
| border-color: var(--primary); | |
| background-color: rgba(79, 70, 229, 0.02); | |
| } | |
| .upload-icon { | |
| font-size: 3rem; | |
| color: var(--text-muted); | |
| margin-bottom: 1rem; | |
| } | |
| /* --- Analytics Grid --- */ | |
| .stats-grid { | |
| display: grid; | |
| grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); | |
| gap: 1.5rem; | |
| margin-bottom: 2rem; | |
| } | |
| .stat-card { | |
| background: white; | |
| padding: 1.5rem; | |
| border-radius: var(--radius-lg); | |
| border: 1px solid var(--border); | |
| display: flex; | |
| flex-direction: column; | |
| align-items: center; | |
| text-align: center; | |
| } | |
| .stat-value { | |
| font-size: 2.5rem; | |
| font-weight: 700; | |
| color: var(--primary); | |
| margin: 0.5rem 0; | |
| } | |
| .stat-label { | |
| color: var(--text-muted); | |
| font-weight: 500; | |
| } | |
| .stat-icon { | |
| font-size: 1.5rem; | |
| color: var(--secondary); | |
| background: #f3f4f6; | |
| padding: 0.5rem; | |
| border-radius: 50%; | |
| } | |
| /* --- Toast Notification --- */ | |
| #toast-container { | |
| position: fixed; | |
| bottom: 2rem; | |
| right: 2rem; | |
| z-index: 1000; | |
| display: flex; | |
| flex-direction: column; | |
| gap: 0.75rem; | |
| } | |
| .toast { | |
| background: white; | |
| padding: 1rem 1.5rem; | |
| border-radius: var(--radius-md); | |
| box-shadow: var(--shadow-md); | |
| border-left: 4px solid var(--primary); | |
| display: flex; | |
| align-items: center; | |
| gap: 0.75rem; | |
| animation: slideIn 0.3s ease-out; | |
| max-width: 400px; | |
| } | |
| .toast.success { border-left-color: var(--success); } | |
| .toast.error { border-left-color: var(--danger); } | |
| .toast.warning { border-left-color: var(--warning); } | |
| @keyframes slideIn { | |
| from { transform: translateX(100%); opacity: 0; } | |
| to { transform: translateX(0); opacity: 1; } | |
| } | |
| /* --- Markdown Output --- */ | |
| .markdown-body { | |
| line-height: 1.7; | |
| color: var(--text-main); | |
| } | |
| .markdown-body h3 { margin-top: 1.5rem; margin-bottom: 0.75rem; } | |
| .markdown-body ul { padding-left: 1.5rem; margin-bottom: 1rem; } | |
| .markdown-body strong { color: var(--primary); } | |
| /* Responsive */ | |
| @media (max-width: 768px) { | |
| header { padding: 1rem; flex-direction: column; gap: 1rem; align-items: flex-start; } | |
| .tabs { padding-bottom: 1rem; } | |
| main { padding: 1rem; } | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <header> | |
| <div class="brand"> | |
| <i class="ph ph-magic-wand"></i> | |
| <h1>RAG Nexus</h1> | |
| </div> | |
| <a href="https://huggingface.co/spaces/akhaliq/anycoder" target="_blank" class="anycoder-link"> | |
| Built with anycoder <i class="ph ph-arrow-square-out"></i> | |
| </a> | |
| </header> | |
| <main> | |
| <!-- Navigation Tabs --> | |
| <nav class="tabs"> | |
| <button class="tab-btn active" onclick="switchTab('upload')"> | |
| <i class="ph ph-upload-simple"></i> Upload | |
| </button> | |
| <button class="tab-btn" onclick="switchTab('documents')"> | |
| <i class="ph ph-files"></i> Documents | |
| </button> | |
| <button class="tab-btn" onclick="switchTab('axioms')"> | |
| <i class="ph ph-lightning"></i> Axioms | |
| </button> | |
| <button class="tab-btn" onclick="switchTab('generate')"> | |
| <i class="ph ph-robot"></i> Generate | |
| </button> | |
| <button class="tab-btn" onclick="switchTab('analytics')"> | |
| <i class="ph ph-chart-bar"></i> Analytics | |
| </button> | |
| </nav> | |
| <!-- Upload Tab --> | |
| <section id="tab-upload" class="tab-content active"> | |
| <div class="card"> | |
| <div class="card-header"> | |
| <h2 class="card-title">Upload Documents</h2> | |
| <div id="upload-status"></div> | |
| </div> | |
| <div class="upload-area" id="drop-zone"> | |
| <i class="ph ph-file-arrow-up upload-icon"></i> | |
| <h3>Drag & Drop files here</h3> | |
| <p class="text-muted">or click to browse (.txt, .md, .json, .csv)</p> | |
| <input type="file" id="file-input" multiple accept=".txt,.md,.json,.csv" style="display: none;"> | |
| </div> | |
| <div style="margin-top: 1.5rem; text-align: right;"> | |
| <button class="btn btn-primary" onclick="processFiles()"> | |
| <i class="ph ph-cpu"></i> Process Files | |
| </button> | |
| </div> | |
| <div id="upload-queue" style="margin-top: 1.5rem; display: none;"> | |
| <h4>Upload Queue</h4> | |
| <div class="table-container"> | |
| <table id="queue-table"> | |
| <thead> | |
| <tr> | |
| <th>File</th> | |
| <th>Status</th> | |
| <th>Size</th> | |
| </tr> | |
| </thead> | |
| <tbody><!-- JS populates this --></tbody> | |
| </table> | |
| </div> | |
| </div> | |
| </div> | |
| </section> | |
| <!-- Documents Tab --> | |
| <section id="tab-documents" class="tab-content"> | |
| <div class="card"> | |
| <div class="card-header"> | |
| <h2 class="card-title">Indexed Documents</h2> | |
| <button class="btn btn-danger" onclick="clearAllData()"> | |
| <i class="ph ph-trash"></i> Clear All | |
| </button> | |
| </div> | |
| <div class="form-group"> | |
| <input type="text" id="doc-search" placeholder="Search documents..." onkeyup="renderDocuments()"> | |
| </div> | |
| <div class="table-container"> | |
| <table id="documents-table"> | |
| <thead> | |
| <tr> | |
| <th>Name</th> | |
| <th>Size</th> | |
| <th>Uploaded</th> | |
| <th>ID</th> | |
| </tr> | |
| </thead> | |
| <tbody></tbody> | |
| </table> | |
| </div> | |
| </div> | |
| </section> | |
| <!-- Axioms Tab --> | |
| <section id="tab-axioms" class="tab-content"> | |
| <div class="card"> | |
| <div class="card-header"> | |
| <h2 class="card-title">Extracted Axioms</h2> | |
| <button class="btn btn-secondary" onclick="exportAxioms()"> | |
| <i class="ph ph-download-simple"></i> Export JSON | |
| </button> | |
| </div> | |
| <div style="display: flex; gap: 1rem; margin-bottom: 1rem;"> | |
| <div style="flex: 2;"> | |
| <input type="text" id="axiom-search" placeholder="Search axioms..." onkeyup="renderAxioms()"> | |
| </div> | |
| <div style="flex: 1;"> | |
| <select id="axiom-filter" onchange="renderAxioms()"> | |
| <option value="">All Documents</option> | |
| </select> | |
| </div> | |
| </div> | |
| <div class="table-container"> | |
| <table id="axioms-table"> | |
| <thead> | |
| <tr> | |
| <th>Document</th> | |
| <th>Source</th> | |
| <th>Axiom</th> | |
| <th>Confidence</th> | |
| </tr> | |
| </thead> | |
| <tbody></tbody> | |
| </table> | |
| </div> | |
| </div> | |
| </section> | |
| <!-- Generate Tab --> | |
| <section id="tab-generate" class="tab-content"> | |
| <div class="card"> | |
| <div class="card-header"> | |
| <h2 class="card-title">Intelligent Response Generation</h2> | |
| </div> | |
| <div class="form-group"> | |
| <label>Enter your query</label> | |
| <textarea id="query-input" rows="4" placeholder="Ask anything about your documents... (e.g., 'What are the fundamental principles based on the uploaded documents?')"></textarea> | |
| </div> | |
| <div style="display: flex; gap: 2rem; margin-bottom: 1.5rem;"> | |
| <label style="display: flex; align-items: center; gap: 0.5rem; cursor: pointer;"> | |
| <input type="checkbox" id="use-axioms" checked> Use Axioms | |
| </label> | |
| <label style="display: flex; align-items: center; gap: 0.5rem; cursor: pointer;"> | |
| <input type="checkbox" id="use-context" checked> Use Context (RAG) | |
| </label> | |
| </div> | |
| <button class="btn btn-primary" onclick="generateResponse()" style="width: 100%; margin-bottom: 2rem;"> | |
| <i class="ph ph-sparkle"></i> Generate Response | |
| </button> | |
| <div id="response-area" style="display: none;"> | |
| <div class="card" style="background: #f9fafb; border: none;"> | |
| <label>Generated Response</label> | |
| <div id="markdown-output" class="markdown-body"></div> | |
| </div> | |
| <details style="margin-top: 1rem;"> | |
| <summary style="cursor: pointer; padding: 0.5rem; font-weight: 600;">📚 Retrieved Context & Axioms</summary> | |
| <div class="card" style="margin-top: 1rem;"> | |
| <textarea id="context-output" rows="5" readonly style="background: white; font-family: monospace; font-size: 0.85rem;"></textarea> | |
| </div> | |
| </details> | |
| </div> | |
| </div> | |
| </section> | |
| <!-- Analytics Tab --> | |
| <section id="tab-analytics" class="tab-content"> | |
| <div class="stats-grid"> | |
| <div class="stat-card"> | |
| <div class="stat-icon"><i class="ph ph-file-text"></i></div> | |
| <div class="stat-value" id="stat-doc-count">0</div> | |
| <div class="stat-label">Documents</div> | |
| </div> | |
| <div class="stat-card"> | |
| <div class="stat-icon"><i class="ph ph-lightning"></i></div> | |
| <div class="stat-value" id="stat-axiom-count">0</div> | |
| <div class="stat-label">Axioms</div> | |
| </div> | |
| <div class="stat-card"> | |
| <div class="stat-icon"><i class="ph ph-hard-drives"></i></div> | |
| <div class="stat-value" id="stat-storage">0MB</div> | |
| <div class="stat-label">Storage Used</div> | |
| </div> | |
| </div> | |
| <div class="card"> | |
| <div class="card-header"> | |
| <h2 class="card-title">Recent Activity</h2> | |
| </div> | |
| <div class="table-container"> | |
| <table id="activity-table"> | |
| <thead> | |
| <tr> | |
| <th>Action</th> | |
| <th>Details</th> | |
| <th>Timestamp</th> | |
| </tr> | |
| </thead> | |
| <tbody></tbody> | |
| </table> | |
| </div> | |
| </div> | |
| </section> | |
| </main> | |
| <div id="toast-container"></div> | |
| <script> | |
| /** | |
| * RAG Nexus - Frontend & Simulated Backend | |
| * Handles document processing, storage, and retrieval using LocalStorage and JS logic. | |
| */ | |
| // --- State Management --- | |
| const DB_KEY = 'rag_nexus_db'; | |
| let state = { | |
| documents: [], // { id, name, content, size, uploaded_at, chunk_count } | |
| axioms: [], // { id, doc_id, source, axiom, confidence } | |
| activity: [] // { action, details, timestamp } | |
| }; | |
| // --- Initialization --- | |
| function init() { | |
| loadState(); | |
| updateStats(); | |
| renderDocuments(); | |
| renderAxioms(); | |
| renderActivity(); | |
| updateAxiomFilter(); | |
| logActivity('system', 'Application initialized'); | |
| } | |
| function loadState() { | |
| const saved = localStorage.getItem(DB_KEY); | |
| if (saved) { | |
| try { | |
| state = JSON.parse(saved); | |
| } catch (e) { | |
| console.error("Failed to load state", e); | |
| } | |
| } | |
| } | |
| function saveState() { | |
| try { | |
| localStorage.setItem(DB_KEY, JSON.stringify(state)); | |
| updateStats(); | |
| } catch (e) { | |
| showToast('Storage quota exceeded! Please clear data.', 'error'); | |
| } | |
| } | |
| function logActivity(action, details) { | |
| const entry = { | |
| action, | |
| details: typeof details === 'object' ? JSON.stringify(details) : details, | |
| timestamp: new Date().toISOString() | |
| }; | |
| state.activity.unshift(entry); | |
| if (state.activity.length > 50) state.activity.pop(); // Keep last 50 | |
| saveState(); | |
| renderActivity(); | |
| } | |
| // --- UI Helpers --- | |
| function switchTab(tabId) { | |
| document.querySelectorAll('.tab-btn').forEach(btn => btn.classList.remove('active')); | |
| document.querySelectorAll('.tab-content').forEach(content => content.classList.remove('active')); | |
| // Find specific button based on onclick attribute matching is tricky, simpler to use index or query | |
| // Just iterating manually for simplicity | |
| const buttons = document.querySelectorAll('.tab-btn'); | |
| if(tabId === 'upload') buttons[0].classList.add('active'); | |
| if(tabId === 'documents') buttons[1].classList.add('active'); | |
| if(tabId === 'axioms') buttons[2].classList.add('active'); | |
| if(tabId === 'generate') buttons[3].classList.add('active'); | |
| if(tabId === 'analytics') buttons[4].classList.add('active'); | |
| document.getElementById(`tab-${tabId}`).classList.add('active'); | |
| } | |
| function showToast(message, type = 'info') { | |
| const container = document.getElementById('toast-container'); | |
| const toast = document.createElement('div'); | |
| toast.className = `toast ${type}`; | |
| let icon = 'info'; | |
| if(type === 'success') icon = 'check-circle'; | |
| if(type === 'error') icon = 'warning-circle'; | |
| if(type === 'warning') icon = 'warning'; | |
| toast.innerHTML = `<i class="ph ph-${icon}" style="font-size: 1.25rem;"></i> <span>${message}</span>`; | |
| container.appendChild(toast); | |
| setTimeout(() => { | |
| toast.style.opacity = '0'; | |
| toast.style.transform = 'translateX(100%)'; | |
| setTimeout(() => toast.remove(), 300); | |
| }, 3000); | |
| } | |
| // --- Upload & Processing --- | |
| const dropZone = document.getElementById('drop-zone'); | |
| const fileInput = document.getElementById('file-input'); | |
| let queuedFiles = []; | |
| dropZone.addEventListener('click', () => fileInput.click()); | |
| dropZone.addEventListener('dragover', (e) => { | |
| e.preventDefault(); | |
| dropZone.classList.add('dragover'); | |
| }); | |
| dropZone.addEventListener('dragleave', () => dropZone.classList.remove('dragover')); | |
| dropZone.addEventListener('drop', (e) => { | |
| e.preventDefault(); | |
| dropZone.classList.remove('dragover'); | |
| handleFiles(e.dataTransfer.files); | |
| }); | |
| fileInput.addEventListener('change', (e) => handleFiles(e.target.files)); | |
| function handleFiles(files) { | |
| if (!files.length) return; | |
| const queueDiv = document.getElementById('upload-queue'); | |
| const tbody = document.querySelector('#queue-table tbody'); | |
| tbody.innerHTML = ''; | |
| queueDiv.style.display = 'block'; | |
| queuedFiles = Array.from(files); | |
| queuedFiles.forEach(file => { | |
| const tr = document.createElement('tr'); | |
| tr.innerHTML = ` | |
| <td>${file.name}</td> | |
| <td><span style="color: var(--warning)">Pending</span></td> | |
| <td>${formatBytes(file.size)}</td> | |
| `; | |
| tbody.appendChild(tr); | |
| }); | |
| } | |
| async function processFiles() { | |
| if (queuedFiles.length === 0) { | |
| showToast("No files to process", "warning"); | |
| return; | |
| } | |
| let processedCount = 0; | |
| const tbody = document.querySelector('#queue-table tbody'); | |
| const rows = tbody.querySelectorAll('tr'); | |
| for (let i = 0; i < queuedFiles.length; i++) { | |
| const file = queuedFiles[i]; | |
| const statusCell = rows[i].cells[1]; | |
| try { | |
| // Read file content | |
| const text = await readFileAsText(file); | |
| // Create Document Object | |
| const doc = { | |
| id: generateHash(file.name + file.size + Date.now()), | |
| name: file.name, | |
| content: text, | |
| size: file.size, | |
| uploaded_at: new Date().toISOString(), | |
| chunk_count: Math.ceil(text.length / 500) | |
| }; | |
| // Extract Axioms | |
| const newAxioms = extractAxioms(text, doc.id); | |
| // Update State | |
| state.documents.push(doc); | |
| state.axioms.push(...newAxioms); | |
| statusCell.innerHTML = '<span style="color: var(--success)">Processed</span>'; | |
| processedCount++; | |
| logActivity('document_uploaded', { name: file.name, size: file.size }); | |
| } catch (err) { | |
| console.error(err); | |
| statusCell.innerHTML = '<span style="color: var(--danger)">Failed</span>'; | |
| logActivity('upload_failed', { name: file.name, error: err.message }); | |
| } | |
| } | |
| saveState(); | |
| showToast(`Processed ${processedCount}/${queuedFiles.length} files`, processedCount === queuedFiles.length ? 'success' : 'warning'); | |
| queuedFiles = []; // Clear queue | |
| // Refresh other tabs | |
| renderDocuments(); | |
| renderAxioms(); | |
| updateAxiomFilter(); | |
| } | |
| function readFileAsText(file) { | |
| return new Promise((resolve, reject) => { | |
| const reader = new FileReader(); | |
| reader.onload = (e) => resolve(e.target.result); | |
| reader.onerror = (e) => reject(e); | |
| reader.readAsText(file); | |
| }); | |
| } | |
| function generateHash(str) { | |
| let hash = 0; | |
| for (let i = 0; i < str.length; i++) { | |
| const char = str.charCodeAt(i); | |
| hash = ((hash << 5) - hash) + char; | |
| hash = hash & hash; | |
| } | |
| return Math.abs(hash).toString(16); | |
| } | |
| function formatBytes(bytes, decimals = 2) { | |
| if (!+bytes) return '0 Bytes'; | |
| const k = 1024; | |
| const dm = decimals < 0 ? 0 : decimals; | |
| const sizes = ['Bytes', 'KB', 'MB', 'GB']; | |
| const i = Math.floor(Math.log(bytes) / Math.log(k)); | |
| return `${parseFloat((bytes / Math.pow(k, i)).toFixed(dm))} ${sizes[i]}`; | |
| } | |
| // --- NLP / Backend Simulation Logic --- | |
| function extractAxioms(text, docId) { | |
| const axioms = []; | |
| // Split into sentences | |
| const sentences = text.match(/[^\.!\?]+[\.!\?]+/g) || [text]; | |
| const keyPhrases = [ | |
| "principle", "law", "theorem", "axiom", "fundamental", | |
| "always", "never", "must", "should", "rule", "definition", | |
| "is defined as", "refers to", "means that", "implies", "important" | |
| ]; | |
| sentences.forEach((sentence, idx) => { | |
| sentence = sentence.trim(); | |
| if (sentence.length < 20 || sentence.length > 200) return; | |
| let confidence = 0.0; | |
| const lower = sentence.toLowerCase(); | |
| keyPhrases.forEach(phrase => { | |
| if (lower.includes(phrase)) confidence += 0.2; | |
| }); | |
| // Structural checks | |
| if (sentence[0] === sentence[0].toUpperCase() && sentence.includes(":")) confidence += 0.1; | |
| // Normalize | |
| confidence = Math.min(1.0, confidence); | |
| if (confidence > 0.3) { | |
| axioms.push({ | |
| id: Date.now() + Math.random(), | |
| doc_id: docId, | |
| source: `Section ${idx + 1}`, | |
| axiom: sentence, | |
| confidence: parseFloat(confidence.toFixed(2)) | |
| }); | |
| } | |
| }); | |
| return axioms; | |
| } | |
| function generateResponse() { | |
| const query = document.getElementById('query-input').value.trim(); | |
| const useAxioms = document.getElementById('use-axioms').checked; | |
| const useContext = document.getElementById('use-context').checked; | |
| if (!query) { | |
| showToast("Please enter a query", "warning"); | |
| return; | |
| } | |
| // UI Feedback | |
| const btn = document.querySelector('#tab-generate .btn-primary'); | |
| const originalText = btn.innerHTML; | |
| btn.innerHTML = '<i class="ph ph-spinner ph-spin"></i> Analyzing...'; | |
| btn.disabled = true; | |
| setTimeout(() => { | |
| // 1. Retrieve Context (Simple TF-IDF / Keyword matching simulation) | |
| let contextText = ""; | |
| let retrievedDocs = []; | |
| if (useContext && state.documents.length > 0) { | |
| const queryTokens = tokenize(query); | |
| const scoredDocs = state.documents.map(doc => { | |
| const contentTokens = tokenize(doc.content); | |
| let score = 0; | |
| queryTokens.forEach(t => { | |
| if (contentTokens.includes(t)) score++; | |
| }); | |
| return { doc, score }; | |
| }); | |
| scoredDocs.sort((a, b) => b.score - a.score); | |
| const topDocs = scoredDocs.slice(0, 3).filter(d => d.score > 0); | |
| if (topDocs.length > 0) { | |
| topDocs.forEach(d => { | |
| contextText += `\n\n--- From ${d.doc.name} ---\n${d.doc.content.substring(0, 300)}...`; | |
| retrievedDocs.push(d.doc.name); | |
| }); | |
| } else { | |
| contextText = "No specific relevant context found in documents."; | |
| } | |
| } | |
| // 2. Retrieve Axioms | |
| let usedAxioms = []; | |
| if (useAxioms) { | |
| // Randomly pick some axioms or filter by query keywords | |
| const filtered = state.axioms.filter(a => { | |
| const tokens = tokenize(a.axiom); | |
| return tokenize(query).some(q => tokens.includes(q)); | |
| }); | |
| usedAxioms = filtered.length > 0 ? filtered.slice(0, 3) : state.axioms.slice(0, 3); | |
| } | |
| // 3. Construct Response (Template-based) | |
| const responseMarkdown = buildResponseTemplate(query, contextText, usedAxioms); | |
| // 4. Update UI | |
| const responseArea = document.getElementById('response-area'); | |
| const markdownOutput = document.getElementById('markdown-output'); | |
| const contextOutput = document.getElementById('context-output'); | |
| responseArea.style.display = 'block'; | |
| markdownOutput.innerHTML = parseMarkdown(responseMarkdown); | |
| contextOutput.value = `Retrieved Documents:\n${retrievedDocs.join('\n') || 'None'}\n\nUsed Axioms:\n${usedAxioms.map(a => a.axiom).join('\n') || 'None'}`; | |
| logActivity('response_generated', { query: query.substring(0, 50) }); | |
| btn.innerHTML = originalText; | |
| btn.disabled = false; | |
| showToast("Response generated successfully", "success"); | |
| }, 1500); // Fake delay for "processing" | |
| } | |
| function tokenize(text) { | |
| return text.toLowerCase() | |
| .replace(/[^\w\s]/g, '') | |
| .split(/\s+/) | |
| .filter(w => w.length > 2); | |
| } | |
| function buildResponseTemplate(query, context, axioms) { | |
| let html = `<h3>Response to: "${query}"</h3>`; | |
| if (axioms.length > 0) { | |
| html += `<p><strong>Key Principles:</strong></p><ul>`; | |
| axioms.forEach(a => { | |
| html += `<li>${a.axiom}</li>`; | |
| }); | |
| html += `</ul>`; | |
| } | |
| html += `<p><strong>Analysis:</strong><br>`; | |
| if (context && context !== "No specific relevant context found in documents.") { | |
| html += `Based on the uploaded documents, I found relevant sections that address your query. The context suggests that the subject matter is related to the core principles extracted above. <br><br>`; | |
| html += `<strong>Synthesized Answer:</strong><br>`; | |
| html += `According to the extracted principles${axioms.length > 0 ? `, such as "${axioms[0].axiom.substring(0, 50)}..."` : ''}, the documents support the conclusion that the query involves specific rules or definitions. The contextual fragments provide evidence that these rules are applied consistently within the provided text.`; | |
| } else { | |
| html += `I could not find specific context in the documents to fully answer this query. However, based on the general axioms extracted: <br>`; | |
| if(axioms.length > 0) html += `"${axioms[0].axiom}"`; | |
| else html += "No axioms available."; | |
| } | |
| html += `</p>`; | |
| return html; | |
| } | |
| function parseMarkdown(md) { | |
| // Very simple markdown parser | |
| return md | |
| .replace(/^### (.*$)/gim, '<h3>$1</h3>') | |
| .replace(/^### (.*$)/gim, '<h3>$1</h3>') | |
| .replace(/\*\*(.*)\*\*/gim, '<strong>$1</strong>') | |
| .replace(/\*(.*)\*/gim, '<em>$1</em>') | |
| .replace(/\n/gim, '<br>'); | |
| } | |
| // --- Rendering Functions --- | |
| function renderDocuments() { | |
| const tbody = document.querySelector('#documents-table tbody'); | |
| const search = document.getElementById('doc-search').value.toLowerCase(); | |
| tbody.innerHTML = ''; | |
| const filtered = state.documents.filter(d => d.name.toLowerCase().includes(search)); | |
| if (filtered.length === 0) { | |
| tbody.innerHTML = '<tr><td colspan="4" style="text-align:center; color: var(--text-muted)">No documents found</td></tr>'; | |
| return; | |
| } | |
| filtered.forEach(doc => { | |
| const tr = document.createElement('tr'); | |
| tr.innerHTML = ` | |
| <td style="font-weight: 500">${doc.name}</td> | |
| <td>${formatBytes(doc.size)}</td> | |
| <td>${new Date(doc.uploaded_at).toLocaleDateString()}</td> | |
| <td style="font-family: monospace; color: var(--text-muted)">${doc.id.substring(0, 8)}...</td> | |
| `; | |
| tbody.appendChild(tr); | |
| }); | |
| } | |
| function renderAxioms() { | |
| const tbody = document.querySelector('#axioms-table tbody'); | |
| const search = document.getElementById('axiom-search').value.toLowerCase(); | |
| const filter = document.getElementById('axiom-filter').value; | |
| tbody.innerHTML = ''; | |
| let filtered = state.axioms.filter(a => a.axiom.toLowerCase().includes(search)); | |
| if (filter) { | |
| const doc = state.documents.find(d => d.id === filter); | |
| const docName = doc ? doc.name : ""; | |
| filtered = filtered.filter(a => { | |
| const d = state.documents.find(doc => doc.id === a.doc_id); | |
| return d && d.name === docName; | |
| }); | |
| } | |
| // Sort by confidence | |
| filtered.sort((a, b) => b.confidence - a.confidence); | |
| if (filtered.length === 0) { | |
| tbody.innerHTML = '<tr><td colspan="4" style="text-align:center; color: var(--text-muted)">No axioms found</td></tr>'; | |
| return; | |
| } | |
| filtered.forEach(ax => { | |
| const doc = state.documents.find(d => d.id === ax.doc_id); | |
| const tr = document.createElement('tr'); | |
| tr.innerHTML = |