Spaces:
Running
Running
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>Hybrid Multimodal Document Engine</title> | |
| <style> | |
| :root { --bg-main: #f8fafc; --panel-bg: #ffffff; --border-color: #e2e8f0; --text-dark: #1e293b; --accent-blue: #2563eb; } | |
| body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; background-color: var(--bg-main); color: var(--text-dark); margin: 0; padding: 20px; } | |
| .container { max-width: 1200px; margin: 0 auto; } | |
| .grid { display: grid; grid-template-columns: 1fr; gap: 20px; margin-top: 20px; } | |
| .panel { background: var(--panel-bg); border: 1px solid var(--border-color); border-radius: 8px; padding: 20px; box-shadow: 0 1px 3px rgba(0,0,0,0.05); } | |
| h2 { margin-top: 0; font-size: 1.25rem; } | |
| .form-group { margin-bottom: 15px; } | |
| label { display: block; margin-bottom: 5px; font-weight: 600; } | |
| input[type="file"], select, input[type="text"], input[type="password"] { width: 100%; padding: 10px; border: 1px solid var(--border-color); border-radius: 4px; box-sizing: border-box; } | |
| button { background-color: var(--accent-blue); color: white; border: none; padding: 10px 20px; border-radius: 4px; cursor: pointer; font-weight: 600; } | |
| button:hover { opacity: 0.9; } | |
| button:disabled { background-color: #94a3b8; cursor: not-allowed; } | |
| .btn-secondary { background-color: #64748b; padding: 8px 15px; font-size: 0.85rem; margin-top: 5px; } | |
| .btn-warning { background-color: #f59e0b; color: white; } | |
| table { width: 100%; border-collapse: collapse; margin-top: 15px; font-size: 0.95rem; } | |
| th, td { text-align: left; padding: 10px; border-bottom: 1px solid var(--border-color); } | |
| th { background-color: #f1f5f9; } | |
| .progress-container { margin-top: 20px; padding: 15px; border: 1px solid #cbd5e1; border-radius: 6px; background-color: #f8fafc; display: none; } | |
| .progress-bar-bg { width: 100%; height: 12px; background-color: #e2e8f0; border-radius: 6px; overflow: hidden; margin: 10px 0; } | |
| .progress-bar-fill { height: 100%; background-color: #22c55e; width: 0%; transition: width 0.5s linear; } | |
| .status-text { font-weight: bold; color: #334155; } | |
| .time-remaining { font-size: 0.9rem; color: #64748b; font-style: italic; } | |
| .extracted-value-highlight { color: #16a34a; font-weight: 700; } | |
| .error-highlight { color: #ef4444; font-weight: 700; } | |
| .chat-box { height: 300px; overflow-y: auto; border: 1px solid var(--border-color); padding: 10px; border-radius: 4px; margin-bottom: 10px; background: #fafafa; font-size: 0.95rem; line-height: 1.5; } | |
| .chat-input-area { display: flex; gap: 10px; } | |
| </style> | |
| </head> | |
| <body> | |
| <div class="container"> | |
| <h1>Multilingual Document Translator & Data Extractor</h1> | |
| <div class="form-group"> | |
| <label> (Powered by Gemini for cloud-based AI processing. Document data is securely transmitted for analysis.)<span style="font-weight:normal; color:#64748b;"></span></label> | |
| </div> | |
| <div class="grid"> | |
| <div class="panel"> | |
| <h2>Upload Your Documents</h2> | |
| <form id="uploadForm"> | |
| <div class="form-group"> | |
| <label>Document to Process (PDF)</label> | |
| <input type="file" id="docFile" accept=".pdf" required> | |
| </div> | |
| <div class="form-group"> | |
| <label>Excel Formatting Sheet (.xlsx)</label> | |
| <input type="file" id="excelFile" accept=".xlsx" required> | |
| </div> | |
| <div class="form-group"> | |
| <label>Google Gemini API Key <span style="font-weight:normal; color:#64748b;"></span></label> | |
| <input type="password" id="apiKey" placeholder="AIzaSy..." autocomplete="off"> | |
| <button type="button" id="syncModelsBtn" class="btn-secondary">🔄 Sync Available Models</button> | |
| </div> | |
| <div class="form-group"> | |
| <label>Processing Mode</label> | |
| <select id="modelSelection"> | |
| <!-- <option value="gemini-1.5-flash">Cloud: Gemini 1.5 Flash (Uses Vision RAG)</option> | |
| <option value="qwen2.5:14b">Local: Qwen 2.5 14B (Strict Text Privacy)</option> --> | |
| </select> | |
| </div> | |
| <button type="submit" id="submitBtn">Start Processing</button> | |
| <button type="button" id="retryBtn" class="btn-warning" style="display:none; margin-top:10px;">⚠️ Rate Limit / Errors Detected. Switch Models and Retry Failed Fields</button> | |
| </form> | |
| <div class="progress-container" id="progressArea"> | |
| <div class="status-text" id="statusText">Initializing System...</div> | |
| <div class="progress-bar-bg"> | |
| <div class="progress-bar-fill" id="progressBar"></div> | |
| </div> | |
| <div class="time-remaining" id="timeRemaining">Calculating estimated completion time...</div> | |
| </div> | |
| <div id="downloadArea" style="margin-top: 15px; display: none;"> | |
| <button id="downloadBtn" style="background-color: #16a34a;">Download Clean Formatted Excel Sheet</button> | |
| </div> | |
| </div> | |
| <div class="panel"> | |
| <h2>Processing Progress</h2> | |
| <div style="overflow-x: auto;"> | |
| <table id="logsTable"> | |
| <thead> | |
| <tr> | |
| <th>Spreadsheet</th> | |
| <th>Field</th> | |
| <th>Extracted Information (English)</th> | |
| <th>Found on Page(s)</th> | |
| <th>Processing Time</th> | |
| </tr> | |
| </thead> | |
| <tbody> | |
| <tr><td colspan="5" style="text-align: center; color: #64748b;">Upload your files and click "Start Processing".</td></tr> | |
| </tbody> | |
| </table> | |
| </div> | |
| </div> | |
| <div class="panel"> | |
| <h2>Ask About the Document</h2> | |
| <div class="chat-box" id="chatBox"> | |
| <p style="color: #64748b; font-style: italic;">Ask questions about the document or extracted information...</p> | |
| </div> | |
| <div class="chat-input-area"> | |
| <input type="text" id="chatInput" placeholder="Example: Summarize the key information in this document."> | |
| <button id="sendChatBtn">Ask AI</button> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <script> | |
| let generatedFilePath = ""; | |
| let currentVisualProgress = 0; | |
| let targetVisualProgress = 0; | |
| let progressInterval = null; | |
| function startContinuousProgress() { | |
| if(progressInterval) clearInterval(progressInterval); | |
| progressInterval = setInterval(() => { | |
| if(currentVisualProgress < targetVisualProgress - 0.5) { | |
| currentVisualProgress += 0.16; | |
| document.getElementById('progressBar').style.width = currentVisualProgress + '%'; | |
| } | |
| }, 1000); | |
| } | |
| document.getElementById('syncModelsBtn').addEventListener('click', async () => { | |
| const apiKey = document.getElementById('apiKey').value.trim(); | |
| const selectEl = document.getElementById('modelSelection'); | |
| const btn = document.getElementById('syncModelsBtn'); | |
| btn.innerText = "⏳ Syncing..."; btn.disabled = true; | |
| selectEl.innerHTML = '<option>Fetching available models...</option>'; | |
| try { | |
| const response = await fetch(`/api/models?api_key=${encodeURIComponent(apiKey)}`); | |
| const data = await response.json(); | |
| selectEl.innerHTML = ''; | |
| data.models.forEach(m => { | |
| const opt = document.createElement('option'); | |
| opt.value = m.value; opt.textContent = m.label; | |
| selectEl.appendChild(opt); | |
| }); | |
| const flashOption = Array.from(selectEl.options).find(opt => opt.value === 'gemini-1.5-flash'); | |
| if (flashOption) flashOption.selected = true; | |
| } catch (err) { | |
| selectEl.innerHTML = '<option value="qwen2.5:14b">Local: Qwen 2.5 14B</option>'; | |
| } finally { | |
| btn.innerText = "🔄 Sync Available Models"; btn.disabled = false; | |
| } | |
| }); | |
| async function executePipeline(retryFailed = false) { | |
| const submitBtn = document.getElementById('submitBtn'); | |
| const retryBtn = document.getElementById('retryBtn'); | |
| const statusText = document.getElementById('statusText'); | |
| const timeRemainingEl = document.getElementById('timeRemaining'); | |
| submitBtn.disabled = true; | |
| retryBtn.style.display = 'none'; | |
| document.getElementById('progressArea').style.display = 'block'; | |
| document.getElementById('downloadArea').style.display = 'none'; | |
| if(!retryFailed) { | |
| currentVisualProgress = 0; targetVisualProgress = 0; | |
| document.getElementById('logsTable').querySelector('tbody').innerHTML = ""; | |
| } | |
| startContinuousProgress(); | |
| const formData = new FormData(); | |
| formData.append('doc_file', document.getElementById('docFile').files[0]); | |
| formData.append('excel_file', document.getElementById('excelFile').files[0]); | |
| formData.append('model_selection', document.getElementById('modelSelection').value); | |
| formData.append('api_key', document.getElementById('apiKey').value); | |
| formData.append('retry_failed_only', retryFailed ? "true" : "false"); | |
| let latencyHistory = []; | |
| try { | |
| const response = await fetch('/api/process', { method: 'POST', body: formData }); | |
| const reader = response.body.getReader(); | |
| const decoder = new TextDecoder("utf-8"); | |
| let buffer = ""; | |
| while (true) { | |
| const { value, done } = await reader.read(); | |
| if (done) break; | |
| buffer += decoder.decode(value, { stream: true }); | |
| const lines = buffer.split('\n'); | |
| buffer = lines.pop(); | |
| for (let line of lines) { | |
| if (!line.trim()) continue; | |
| try { | |
| const data = JSON.parse(line); | |
| if (data.status === "phase") { | |
| statusText.innerText = data.message; | |
| if(data.phase === 1) targetVisualProgress = 25; | |
| else if(data.phase === 2) targetVisualProgress = 50; | |
| else if(data.phase === 3) targetVisualProgress = 75; | |
| else if(data.phase === 4) targetVisualProgress = 80; | |
| currentVisualProgress = targetVisualProgress; | |
| document.getElementById('progressBar').style.width = currentVisualProgress + '%'; | |
| } | |
| else if (data.status === "extracted") { | |
| statusText.innerText = `Extracting ${data.current} of ${data.total} fields...`; | |
| targetVisualProgress = 80 + ((data.current / data.total) * 20); | |
| currentVisualProgress = targetVisualProgress; | |
| document.getElementById('progressBar').style.width = currentVisualProgress + '%'; | |
| const isError = data.value.includes("Error") || data.value.includes("Not Found"); | |
| const valClass = isError ? "error-highlight" : "extracted-value-highlight"; | |
| const tr = document.createElement('tr'); | |
| tr.innerHTML = `<td>${data.sheet}</td><td>${data.column}</td><td class="${valClass}">${data.value}</td><td>Pg: ${data.pages.join(', ')}</td><td>${data.latency}s</td>`; | |
| document.getElementById('logsTable').querySelector('tbody').prepend(tr); | |
| latencyHistory.push(data.latency); | |
| const avgTime = latencyHistory.reduce((a, b) => a + b, 0) / latencyHistory.length; | |
| const itemsLeft = data.total - data.current; | |
| const isCloud = document.getElementById('modelSelection').value.startsWith("gemini"); | |
| const divisor = isCloud ? 8 : 1; | |
| const secLeft = Math.round(itemsLeft * avgTime / divisor); | |
| if (itemsLeft > 0) { | |
| timeRemainingEl.innerText = `Estimated Time Remaining: ~${secLeft} seconds`; | |
| } | |
| } | |
| else if (data.status === "rate_limit" || data.status === "done") { | |
| clearInterval(progressInterval); | |
| currentVisualProgress = 100; | |
| document.getElementById('progressBar').style.width = '100%'; | |
| generatedFilePath = data.file_path; | |
| document.getElementById('downloadArea').style.display = 'block'; | |
| if(data.status === "rate_limit") { | |
| statusText.innerText = "⚠️ Pipeline paused due to API Limits or Failures."; | |
| retryBtn.style.display = 'block'; | |
| } else { | |
| statusText.innerText = "✅ Extraction Successfully Completed!"; | |
| timeRemainingEl.innerText = "All parallel agents resolved."; | |
| } | |
| } | |
| } catch (e) { console.error("Parse err", line); } | |
| } | |
| } | |
| } catch (err) { | |
| alert("Network communication failure: " + err.message); | |
| clearInterval(progressInterval); | |
| } finally { | |
| submitBtn.disabled = false; | |
| } | |
| } | |
| document.getElementById('uploadForm').addEventListener('submit', (e) => { e.preventDefault(); executePipeline(false); }); | |
| document.getElementById('retryBtn').addEventListener('click', () => { executePipeline(true); }); | |
| document.getElementById('downloadBtn').addEventListener('click', () => { | |
| if(generatedFilePath) window.location.href = `/api/download?path=${encodeURIComponent(generatedFilePath)}`; | |
| }); | |
| document.getElementById('sendChatBtn').addEventListener('click', async () => { | |
| const input = document.getElementById('chatInput'); | |
| const chatBox = document.getElementById('chatBox'); | |
| const modelSelection = document.getElementById('modelSelection').value; | |
| const apiKey = document.getElementById('apiKey').value; | |
| const text = input.value.trim(); | |
| if(!text) return; | |
| chatBox.innerHTML += `<p><strong>User:</strong> ${text}</p>`; | |
| input.value = ""; | |
| chatBox.scrollTop = chatBox.scrollHeight; | |
| try { | |
| const response = await fetch('/api/chat', { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify({ query: text, model_selection: modelSelection, api_key: apiKey }) | |
| }); | |
| const data = await response.json(); | |
| if (!response.ok) { | |
| chatBox.innerHTML += ` | |
| <div style="background-color: #fef2f2; border-left: 4px solid #ef4444; padding: 10px; margin: 10px 0; border-radius: 4px;"> | |
| <p style="color: #b91c1c; margin: 0;"><strong>⚠️ System Alert:</strong> ${data.detail}</p> | |
| </div>`; | |
| } else { | |
| let sourceInfo = data.citations_pages.length > 0 ? `<br><small style="color: #2563eb; font-size: 0.85rem;">[Sources Traced on Pages: ${data.citations_pages.join(', ')}]</small>` : ''; | |
| chatBox.innerHTML += `<p><strong>Agent (${modelSelection}):</strong> ${data.response} ${sourceInfo}</p>`; | |
| } | |
| chatBox.scrollTop = chatBox.scrollHeight; | |
| } catch (err) { | |
| chatBox.innerHTML += `<p style="color: red;">Failed to communicate with API.</p>`; | |
| } | |
| }); | |
| </script> | |
| </body> | |
| </html> |