basudevm23's picture
Update index.html
bd9bd3e verified
Raw
History Blame Contribute Delete
26.7 kB
<!DOCTYPE html>
<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>Document Language (for OCR Scanning)</label>
<select id="ocrLang">
<option value="eng+spa">English & Spanish (Default)</option>
<option value="eng">English Only (Fastest)</option>
<option value="eng+fra">English & French</option>
<option value="eng+deu">English & German</option>
<option value="eng+ita">English & Italian</option>
<option value="eng+por">English & Portuguese</option>
<option value="eng+ara">English & Arabic</option>
<option value="eng+chi_sim">English & Simplified Chinese</option>
<option value="eng+msa">English & Malay</option>
<option value="eng+hin">English & Hindi</option>
<option value="eng+rus">English & Russian</option>
<option value="eng+jpn">English & Japanese</option>
</select>
</div>
<div class="form-group">
<label>Processing Mode</label>
<select id="modelSelection">
<!-- <option value="gemini-2.5-flash">Cloud: Gemini 2.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 extractTextLocally(file, progressCallback) {
const arrayBuffer = await file.arrayBuffer();
const pdf = await pdfjsLib.getDocument(arrayBuffer).promise;
const numPages = pdf.numPages;
let extractedChunks = [];
// 1. Quick check on Page 1 to see if this is a scanned image document
const firstPage = await pdf.getPage(1);
const firstContent = await firstPage.getTextContent();
const isScanned = firstContent.items.map(item => item.str).join(' ').trim().length < 50;
let scheduler = null;
if (isScanned) {
// Spin up to 4 parallel AI threads based on the user's processor cores!
const selectedLang = document.getElementById('ocrLang')?.value || 'eng+spa';
const numWorkers = Math.min(4, (navigator.hardwareConcurrency || 4));
progressCallback(`Phase 1 (Local CPU): Booting ${numWorkers}-Core Parallel AI Pool...`, 2);
scheduler = Tesseract.createScheduler();
for (let w = 0; w < numWorkers; w++) {
const worker = await Tesseract.createWorker(selectedLang);
scheduler.addWorker(worker);
}
}
let completedPages = 0;
const pageTasks = [];
// 2. Queue up all pages to run across the parallel worker pool
for (let i = 1; i <= numPages; i++) {
pageTasks.push((async () => {
const page = await pdf.getPage(i);
const textContent = await page.getTextContent();
let pageText = textContent.items.map(item => item.str).join(' ');
// If text is missing (< 50 chars) and scheduler is running, feed image to multi-core pool
if (pageText.trim().length < 50 && scheduler) {
const viewport = page.getViewport({ scale: 1.2 }); // Optimized for speed & accuracy
const canvas = document.createElement('canvas');
const context = canvas.getContext('2d');
canvas.height = viewport.height;
canvas.width = viewport.width;
await page.render({
canvasContext: context,
viewport: viewport
}).promise;
// Send canvas to whomever is free in the worker pool!
const {
data: { text }
} = await scheduler.addJob('recognize', canvas);
pageText = text;
// Immediately free browser memory!
canvas.width = 0;
canvas.height = 0;
}
completedPages++;
const prog = Math.round(2 + ((completedPages / numPages) * 18));
progressCallback(
`Phase 1 (Local CPU): Scanned Page ${completedPages} of ${numPages}...`,
prog
);
// Layout-aware chunking matching your exact existing formatting
let cleanText = pageText.replace(/\n{3,}/g, '\n\n');
let blocks = cleanText.split('\n\n');
let currentChunk = "";
let pageChunks = [];
for (let block of blocks) {
if (currentChunk.length + block.length < 800) {
currentChunk += block + "\n\n";
} else {
if (currentChunk.trim()) {
pageChunks.push({
page: i,
text: currentChunk.trim()
});
}
currentChunk = currentChunk.slice(-150) + block + "\n\n";
}
}
if (currentChunk.trim()) {
pageChunks.push({
page: i,
text: currentChunk.trim()
});
}
return pageChunks;
})());
}
// 3. Run all threads simultaneously and wait for completion
const results = await Promise.all(pageTasks);
results.forEach(chunks => extractedChunks.push(...chunks));
if (scheduler) {
await scheduler.terminate();
}
return JSON.stringify(extractedChunks);
}
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 = "";
}
let localExtractedJson = null;
if (!retryFailed) {
try {
const docFile = document.getElementById('docFile').files[0];
localExtractedJson = await extractTextLocally(docFile, (msg, prog) => {
statusText.innerText = msg;
currentVisualProgress = prog;
document.getElementById('progressBar').style.width = prog + '%';
});
} catch (err) {
console.warn("Browser OCR skipped or failed, server will handle it:", err);
}
}
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");
// Send the locally extracted text to the backend!
if (localExtractedJson) {
formData.append('local_extracted_text', localExtractedJson);
}
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 === "error") {
clearInterval(progressInterval);
alert("🚨 Backend Error: " + data.message);
statusText.innerText = "❌ Error: " + data.message;
document.getElementById('progressBar').style.backgroundColor = "#ef4444";
break;
}
else 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>
<!-- Browser-Side PDF and OCR Engines -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/tesseract.js@5/dist/tesseract.min.js"></script>
<script>
pdfjsLib.GlobalWorkerOptions.workerSrc = 'https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.worker.min.js';
</script>
</body>
</html>