| <!DOCTYPE html> |
| <html lang="en"> |
| <head> |
| <meta charset="UTF-8"> |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> |
| <title>DocuSniff Pro - Smart Document Data Extraction</title> |
| <link rel="icon" type="image/x-icon" href="/static/favicon.ico"> |
| <script src="https://cdn.tailwindcss.com"></script> |
| <script src="https://unpkg.com/feather-icons"></script> |
| <script src="https://cdn.jsdelivr.net/npm/feather-icons/dist/feather.min.js"></script> |
| <script> |
| tailwind.config = { |
| theme: { |
| extend: { |
| colors: { |
| primary: '#6366f1', |
| secondary: '#8b5cf6', |
| } |
| } |
| } |
| } |
| </script> |
| <style> |
| .dropzone { |
| border: 2px dashed #c7d2fe; |
| transition: all 0.3s ease; |
| } |
| .dropzone.active { |
| border-color: #6366f1; |
| background-color: #eef2ff; |
| } |
| #previewContainer { |
| transition: all 0.3s ease; |
| } |
| .entity-card { |
| transition: all 0.2s ease; |
| } |
| .entity-card:hover { |
| transform: translateY(-2px); |
| box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05); |
| } |
| </style> |
| </head> |
| <body class="bg-gray-50 min-h-screen"> |
| <div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-12"> |
| |
| <header class="text-center mb-12"> |
| <h1 class="text-4xl font-bold text-gray-900 mb-2">DocuSniff Pro</h1> |
| <p class="text-lg text-gray-600">Upload documents and extract valuable business & personal information instantly</p> |
| <div class="flex justify-center mt-4"> |
| <span class="inline-flex items-center px-3 py-1 rounded-full bg-primary-100 text-primary-800 text-sm font-medium"> |
| <i data-feather="zap" class="w-4 h-4 mr-1"></i> Smart Data Extraction |
| </span> |
| </div> |
| </header> |
|
|
| |
| <div class="grid grid-cols-1 lg:grid-cols-3 gap-8"> |
| |
| <div class="lg:col-span-1 space-y-6"> |
| |
| <div class="bg-white rounded-xl shadow-md p-6"> |
| <h2 class="text-xl font-semibold text-gray-800 mb-4 flex items-center"> |
| <i data-feather="upload" class="w-5 h-5 mr-2 text-primary-500"></i> Upload Documents |
| </h2> |
| <div id="dropzone" class="dropzone rounded-lg p-8 text-center cursor-pointer"> |
| <i data-feather="file-plus" class="w-12 h-12 mx-auto text-gray-400 mb-3"></i> |
| <p class="text-gray-600 mb-2">Drag & drop files here</p> |
| <p class="text-sm text-gray-500 mb-4">or</p> |
| <input type="file" id="fileInput" class="hidden" accept=".pdf,.msg,.txt,.xlsx,.xls,.docx,.doc" multiple> |
| <button id="selectFilesBtn" class="bg-primary-500 hover:bg-primary-600 text-white px-4 py-2 rounded-md transition duration-200"> |
| Select Files |
| </button> |
| <p class="text-xs text-gray-500 mt-3">Supports: PDF, MSG, TXT, Excel, Word</p> |
| </div> |
| <div id="fileList" class="mt-4 space-y-2 hidden"> |
| <h3 class="text-sm font-medium text-gray-700">Selected Files:</h3> |
| <div id="selectedFiles" class="space-y-1"></div> |
| </div> |
| </div> |
|
|
| |
| <div class="bg-white rounded-xl shadow-md p-6"> |
| <h2 class="text-xl font-semibold text-gray-800 mb-4 flex items-center"> |
| <i data-feather="settings" class="w-5 h-5 mr-2 text-primary-500"></i> Extraction Options |
| </h2> |
| |
| |
| <div class="mb-6"> |
| <h3 class="text-sm font-medium text-gray-700 mb-2">Extract Entities:</h3> |
| <div class="grid grid-cols-2 gap-2"> |
| <label class="flex items-center space-x-2"> |
| <input type="checkbox" class="rounded text-primary-500" checked> |
| <span class="text-gray-800 text-sm">People</span> |
| </label> |
| <label class="flex items-center space-x-2"> |
| <input type="checkbox" class="rounded text-primary-500" checked> |
| <span class="text-gray-800 text-sm">Companies</span> |
| </label> |
| <label class="flex items-center space-x-2"> |
| <input type="checkbox" class="rounded text-primary-500" checked> |
| <span class="text-gray-800 text-sm">Emails</span> |
| </label> |
| <label class="flex items-center space-x-2"> |
| <input type="checkbox" class="rounded text-primary-500" checked> |
| <span class="text-gray-800 text-sm">Phone Numbers</span> |
| </label> |
| <label class="flex items-center space-x-2"> |
| <input type="checkbox" class="rounded text-primary-500"> |
| <span class="text-gray-800 text-sm">Addresses</span> |
| </label> |
| <label class="flex items-center space-x-2"> |
| <input type="checkbox" class="rounded text-primary-500"> |
| <span class="text-gray-800 text-sm">Dates</span> |
| </label> |
| <label class="flex items-center space-x-2"> |
| <input type="checkbox" class="rounded text-primary-500"> |
| <span class="text-gray-800 text-sm">Job Titles</span> |
| </label> |
| <label class="flex items-center space-x-2"> |
| <input type="checkbox" class="rounded text-primary-500"> |
| <span class="text-gray-800 text-sm">Financial Data</span> |
| </label> |
| </div> |
| </div> |
| |
| |
| <div> |
| <h3 class="text-sm font-medium text-gray-700 mb-2">Custom Keywords:</h3> |
| <div class="flex"> |
| <input type="text" id="customKeyword" class="flex-1 rounded-l-md border-gray-300 shadow-sm focus:border-primary-500 focus:ring-primary-500" placeholder="Add keywords to extract"> |
| <button id="addKeywordBtn" class="bg-primary-500 hover:bg-primary-600 text-white px-4 py-2 rounded-r-md transition duration-200"> |
| Add |
| </button> |
| </div> |
| <div id="keywordsList" class="flex flex-wrap gap-2 mt-2"> |
| |
| </div> |
| </div> |
| </div> |
|
|
| |
| <div class="space-y-3"> |
| <button id="extractBtn" class="w-full bg-primary-500 hover:bg-primary-600 text-white font-medium py-3 px-4 rounded-md transition duration-200 flex items-center justify-center disabled:opacity-50" disabled> |
| <i data-feather="search" class="w-5 h-5 mr-2"></i> Extract Information |
| </button> |
| <button id="clearBtn" class="w-full bg-gray-200 hover:bg-gray-300 text-gray-800 font-medium py-3 px-4 rounded-md transition duration-200 flex items-center justify-center"> |
| <i data-feather="trash-2" class="w-5 h-5 mr-2"></i> Clear All |
| </button> |
| </div> |
| </div> |
|
|
| |
| <div class="lg:col-span-2"> |
| <div class="bg-white rounded-xl shadow-md p-6"> |
| <h2 class="text-xl font-semibold text-gray-800 mb-6 flex items-center"> |
| <i data-feather="file-text" class="w-5 h-5 mr-2 text-primary-500"></i> Extracted Information |
| </h2> |
|
|
| <div id="emptyState" class="text-center py-12"> |
| <i data-feather="file-text" class="w-12 h-12 mx-auto text-gray-300 mb-4"></i> |
| <h3 class="text-lg font-medium text-gray-900 mb-1">No data extracted yet</h3> |
| <p class="text-gray-500">Upload documents and click "Extract Information" to get started</p> |
| </div> |
|
|
| <div id="resultsContainer" class="hidden"> |
| |
| <div id="previewContainer" class="mb-8"> |
| <div class="flex items-center justify-between mb-4"> |
| <h3 class="text-lg font-medium text-gray-800">Document Preview</h3> |
| <div class="flex space-x-2"> |
| <button id="prevDocBtn" class="text-gray-500 hover:text-primary-500 disabled:text-gray-300" disabled> |
| <i data-feather="chevron-left" class="w-5 h-5"></i> |
| </button> |
| <span id="docCounter" class="text-sm text-gray-500">1/1</span> |
| <button id="nextDocBtn" class="text-gray-500 hover:text-primary-500 disabled:text-gray-300" disabled> |
| <i data-feather="chevron-right" class="w-5 h-5"></i> |
| </button> |
| </div> |
| </div> |
| <div id="documentPreview" class="border rounded-lg p-4 h-64 overflow-y-auto bg-gray-50"> |
| |
| </div> |
| </div> |
|
|
| |
| <div> |
| <div class="flex items-center justify-between mb-4"> |
| <h3 class="text-lg font-medium text-gray-800">Extracted Entities</h3> |
| <div class="flex items-center space-x-2"> |
| <button id="exportBtn" class="text-sm bg-secondary-500 hover:bg-secondary-600 text-white px-3 py-1 rounded-md transition duration-200 flex items-center"> |
| <i data-feather="download" class="w-4 h-4 mr-1"></i> Export |
| </button> |
| </div> |
| </div> |
|
|
| |
| <div class="space-y-6"> |
| |
| <div id="peopleSection" class="hidden"> |
| <div class="flex items-center mb-3"> |
| <i data-feather="users" class="w-5 h-5 text-primary-500 mr-2"></i> |
| <h4 class="font-medium text-gray-800">People</h4> |
| <span id="peopleCount" class="ml-2 text-xs bg-primary-100 text-primary-800 px-2 py-1 rounded-full"></span> |
| </div> |
| <div id="peopleList" class="grid grid-cols-1 md:grid-cols-2 gap-3"> |
| |
| </div> |
| </div> |
|
|
| |
| <div id="companiesSection" class="hidden"> |
| <div class="flex items-center mb-3"> |
| <i data-feather="briefcase" class="w-5 h-5 text-primary-500 mr-2"></i> |
| <h4 class="font-medium text-gray-800">Companies</h4> |
| <span id="companiesCount" class="ml-2 text-xs bg-primary-100 text-primary-800 px-2 py-1 rounded-full"></span> |
| </div> |
| <div id="companiesList" class="grid grid-cols-1 md:grid-cols-2 gap-3"> |
| |
| </div> |
| </div> |
|
|
| |
| <div id="contactSection" class="hidden"> |
| <div class="flex items-center mb-3"> |
| <i data-feather="mail" class="w-5 h-5 text-primary-500 mr-2"></i> |
| <h4 class="font-medium text-gray-800">Contact Information</h4> |
| <span id="contactCount" class="ml-2 text-xs bg-primary-100 text-primary-800 px-2 py-1 rounded-full"></span> |
| </div> |
| <div id="contactList" class="grid grid-cols-1 md:grid-cols-2 gap-3"> |
| |
| </div> |
| </div> |
|
|
| |
| <div id="keywordsSection" class="hidden"> |
| <div class="flex items-center mb-3"> |
| <i data-feather="tag" class="w-5 h-5 text-primary-500 mr-2"></i> |
| <h4 class="font-medium text-gray-800">Custom Keywords</h4> |
| <span id="keywordsCount" class="ml-2 text-xs bg-primary-100 text-primary-800 px-2 py-1 rounded-full"></span> |
| </div> |
| <div id="keywordsResults" class="grid grid-cols-1 md:grid-cols-2 gap-3"> |
| |
| </div> |
| </div> |
| </div> |
| </div> |
| </div> |
| </div> |
| </div> |
| </div> |
| </div> |
|
|
| <script> |
| document.addEventListener('DOMContentLoaded', function() { |
| feather.replace(); |
| |
| |
| const dropzone = document.getElementById('dropzone'); |
| const fileInput = document.getElementById('fileInput'); |
| const selectFilesBtn = document.getElementById('selectFilesBtn'); |
| const fileList = document.getElementById('fileList'); |
| const selectedFiles = document.getElementById('selectedFiles'); |
| const extractBtn = document.getElementById('extractBtn'); |
| const clearBtn = document.getElementById('clearBtn'); |
| const addKeywordBtn = document.getElementById('addKeywordBtn'); |
| const customKeyword = document.getElementById('customKeyword'); |
| const keywordsList = document.getElementById('keywordsList'); |
| const emptyState = document.getElementById('emptyState'); |
| const resultsContainer = document.getElementById('resultsContainer'); |
| const documentPreview = document.getElementById('documentPreview'); |
| const prevDocBtn = document.getElementById('prevDocBtn'); |
| const nextDocBtn = document.getElementById('nextDocBtn'); |
| const docCounter = document.getElementById('docCounter'); |
| const exportBtn = document.getElementById('exportBtn'); |
| |
| |
| let files = []; |
| let currentDocIndex = 0; |
| let extractionResults = []; |
| let customKeywords = []; |
| |
| |
| selectFilesBtn.addEventListener('click', () => fileInput.click()); |
| |
| fileInput.addEventListener('change', handleFileSelect); |
| |
| dropzone.addEventListener('dragover', (e) => { |
| e.preventDefault(); |
| dropzone.classList.add('active'); |
| }); |
| |
| dropzone.addEventListener('dragleave', () => { |
| dropzone.classList.remove('active'); |
| }); |
| |
| dropzone.addEventListener('drop', (e) => { |
| e.preventDefault(); |
| dropzone.classList.remove('active'); |
| fileInput.files = e.dataTransfer.files; |
| handleFileSelect({ target: fileInput }); |
| }); |
| |
| extractBtn.addEventListener('click', extractInformation); |
| |
| clearBtn.addEventListener('click', clearAll); |
| |
| addKeywordBtn.addEventListener('click', addCustomKeyword); |
| |
| customKeyword.addEventListener('keypress', (e) => { |
| if (e.key === 'Enter') addCustomKeyword(); |
| }); |
| |
| prevDocBtn.addEventListener('click', () => navigateDocuments(-1)); |
| nextDocBtn.addEventListener('click', () => navigateDocuments(1)); |
| exportBtn.addEventListener('click', exportResults); |
| |
| |
| function handleFileSelect(e) { |
| files = Array.from(e.target.files); |
| updateFileList(); |
| extractBtn.disabled = files.length === 0; |
| } |
| |
| function updateFileList() { |
| selectedFiles.innerHTML = ''; |
| |
| if (files.length === 0) { |
| fileList.classList.add('hidden'); |
| return; |
| } |
| |
| fileList.classList.remove('hidden'); |
| |
| files.forEach((file, index) => { |
| const fileEl = document.createElement('div'); |
| fileEl.className = 'flex items-center justify-between p-2 bg-gray-50 rounded-lg'; |
| |
| const fileInfo = document.createElement('div'); |
| fileInfo.className = 'flex items-center'; |
| |
| const icon = document.createElement('i'); |
| icon.setAttribute('data-feather', getFileIcon(file.name)); |
| icon.className = 'w-4 h-4 mr-2 text-gray-500'; |
| |
| const name = document.createElement('span'); |
| name.className = 'text-sm text-gray-700 truncate max-w-xs'; |
| name.textContent = file.name; |
| |
| const removeBtn = document.createElement('button'); |
| removeBtn.className = 'text-gray-400 hover:text-red-500'; |
| removeBtn.innerHTML = '<i data-feather="x" class="w-4 h-4"></i>'; |
| removeBtn.addEventListener('click', () => removeFile(index)); |
| |
| fileInfo.appendChild(icon); |
| fileInfo.appendChild(name); |
| fileEl.appendChild(fileInfo); |
| fileEl.appendChild(removeBtn); |
| selectedFiles.appendChild(fileEl); |
| }); |
| |
| feather.replace(); |
| } |
| |
| function getFileIcon(filename) { |
| const ext = filename.split('.').pop().toLowerCase(); |
| switch(ext) { |
| case 'pdf': return 'file-text'; |
| case 'msg': return 'mail'; |
| case 'txt': return 'file'; |
| case 'xlsx': |
| case 'xls': return 'file-text'; |
| case 'docx': |
| case 'doc': return 'file-text'; |
| default: return 'file'; |
| } |
| } |
| |
| function removeFile(index) { |
| files.splice(index, 1); |
| updateFileList(); |
| extractBtn.disabled = files.length === 0; |
| } |
| |
| function addCustomKeyword() { |
| const keyword = customKeyword.value.trim(); |
| if (keyword && !customKeywords.includes(keyword)) { |
| customKeywords.push(keyword); |
| |
| const keywordEl = document.createElement('div'); |
| keywordEl.className = 'flex items-center bg-gray-100 px-3 py-1 rounded-full text-sm'; |
| |
| const text = document.createElement('span'); |
| text.className = 'mr-2'; |
| text.textContent = keyword; |
| |
| const removeBtn = document.createElement('button'); |
| removeBtn.className = 'text-gray-500 hover:text-red-500'; |
| removeBtn.innerHTML = '<i data-feather="x" class="w-3 h-3"></i>'; |
| removeBtn.addEventListener('click', () => { |
| customKeywords = customKeywords.filter(k => k !== keyword); |
| keywordsList.removeChild(keywordEl); |
| }); |
| |
| keywordEl.appendChild(text); |
| keywordEl.appendChild(removeBtn); |
| keywordsList.appendChild(keywordEl); |
| |
| customKeyword.value = ''; |
| feather.replace(); |
| } |
| } |
| async function extractInformation() { |
| emptyState.classList.add('hidden'); |
| resultsContainer.classList.remove('hidden'); |
| extractBtn.disabled = true; |
| extractBtn.innerHTML = '<i data-feather="loader" class="w-5 h-5 mr-2 animate-spin"></i> Processing...'; |
| feather.replace(); |
| |
| try { |
| |
| const formData = new FormData(); |
| files.forEach(file => formData.append('documents', file)); |
| |
| |
| const checkboxes = document.querySelectorAll('input[type="checkbox"]:checked'); |
| const entitiesToExtract = Array.from(checkboxes).map(cb => cb.nextElementSibling.textContent); |
| formData.append('entities', JSON.stringify(entitiesToExtract)); |
| |
| |
| if (customKeywords.length > 0) { |
| formData.append('keywords', JSON.stringify(customKeywords)); |
| } |
| |
| |
| const response = await fetch('/api/extract', { |
| method: 'POST', |
| body: formData |
| }); |
| |
| if (!response.ok) { |
| throw new Error(`Error: ${response.status}`); |
| } |
| |
| extractionResults = await response.json(); |
| currentDocIndex = 0; |
| updateDocumentView(); |
| |
| } catch (error) { |
| console.error('Extraction failed:', error); |
| alert('Extraction failed. Please try again.'); |
| } finally { |
| extractBtn.disabled = false; |
| extractBtn.innerHTML = '<i data-feather="search" class="w-5 h-5 mr-2"></i> Extract Information'; |
| feather.replace(); |
| } |
| } |
| function generateDocumentPreview(content) { |
| |
| if (typeof content === 'string') { |
| return content; |
| } |
| |
| return `Preview not available for this document. Extracted entities are shown below.`; |
| } |
| function updateDocumentView() { |
| if (extractionResults.length === 0) return; |
| |
| const currentDoc = extractionResults[currentDocIndex]; |
| |
| |
| documentPreview.textContent = generateDocumentPreview(currentDoc.preview || currentDoc.content); |
| |
| |
| docCounter.textContent = `${currentDocIndex + 1}/${extractionResults.length}`; |
| |
| |
| prevDocBtn.disabled = currentDocIndex === 0; |
| nextDocBtn.disabled = currentDocIndex === extractionResults.length - 1; |
| |
| |
| updateEntitiesDisplay(currentDoc.entities || {}); |
| } |
| function updateDocumentView() { |
| if (extractionResults.length === 0) return; |
| |
| const currentDoc = extractionResults[currentDocIndex]; |
| |
| |
| documentPreview.textContent = currentDoc.preview; |
| |
| |
| docCounter.textContent = `${currentDocIndex + 1}/${extractionResults.length}`; |
| |
| |
| prevDocBtn.disabled = currentDocIndex === 0; |
| nextDocBtn.disabled = currentDocIndex === extractionResults.length - 1; |
| |
| |
| updateEntitiesDisplay(currentDoc.entities); |
| } |
| |
| function updateEntitiesDisplay(entities) { |
| |
| const peopleSection = document.getElementById('peopleSection'); |
| const peopleList = document.getElementById('peopleList'); |
| const peopleCount = document.getElementById('peopleCount'); |
| |
| if (entities.people.length > 0) { |
| peopleSection.classList.remove('hidden'); |
| peopleList.innerHTML = ''; |
| entities.people.forEach(person => { |
| peopleList.appendChild(createEntityCard('user', person.name, person.context)); |
| }); |
| peopleCount.textContent = entities.people.length; |
| } else { |
| peopleSection.classList.add('hidden'); |
| } |
| |
| |
| const companiesSection = document.getElementById('companiesSection'); |
| const companiesList = document.getElementById('companiesList'); |
| const companiesCount = document.getElementById('companiesCount'); |
| |
| if (entities.companies.length > 0) { |
| companiesSection.classList.remove('hidden'); |
| companiesList.innerHTML = ''; |
| entities.companies.forEach(company => { |
| companiesList.appendChild(createEntityCard('briefcase', company.name, company.context)); |
| }); |
| companiesCount.textContent = entities.companies.length; |
| } else { |
| companiesSection.classList.add('hidden'); |
| } |
| |
| |
| const contactSection = document.getElementById('contactSection'); |
| const contactList = document.getElementById('contactList'); |
| const contactCount = document.getElementById('contactCount'); |
| |
| const contactItems = [...entities.emails, ...entities.phones]; |
| if (contactItems.length > 0) { |
| contactSection.classList.remove('hidden'); |
| contactList.innerHTML = ''; |
| entities.emails.forEach(email => { |
| contactList.appendChild(createEntityCard('mail', email.value, email.context)); |
| }); |
| entities.phones.forEach(phone => { |
| contactList.appendChild(createEntityCard('phone', phone.value, phone.context)); |
| }); |
| contactCount.textContent = contactItems.length; |
| } else { |
| contactSection.classList.add('hidden'); |
| } |
| |
| |
| const keywordsSection = document.getElementById('keywordsSection'); |
| const keywordsResults = document.getElementById('keywordsResults'); |
| const keywordsCount = document.getElementById('keywordsCount'); |
| |
| if (entities.custom.length > 0) { |
| keywordsSection.classList.remove('hidden'); |
| keywordsResults.innerHTML = ''; |
| entities.custom.forEach(item => { |
| keywordsResults.appendChild(createEntityCard('tag', item.keyword, item.context)); |
| }); |
| keywordsCount.textContent = entities.custom.length; |
| } else { |
| keywordsSection.classList.add('hidden'); |
| } |
| } |
| |
| function createEntityCard(icon, title, description) { |
| const card = document.createElement('div'); |
| card.className = 'entity-card bg-white border border-gray-200 rounded-lg p-4 shadow-sm'; |
| |
| const iconEl = document.createElement('i'); |
| iconEl.setAttribute('data-feather', icon); |
| iconEl.className = 'w-5 h-5 text-primary-500 mb-2'; |
| |
| const titleEl = document.createElement('h5'); |
| titleEl.className = 'font-medium text-gray-800 mb-1 truncate'; |
| titleEl.textContent = title; |
| |
| const descEl = document.createElement('p'); |
| descEl.className = 'text-sm text-gray-600 line-clamp-2'; |
| descEl.textContent = description; |
| |
| card.appendChild(iconEl); |
| card.appendChild(titleEl); |
| card.appendChild(descEl); |
| |
| feather.replace(); |
| return card; |
| } |
| |
| function navigateDocuments(direction) { |
| currentDocIndex += direction; |
| if (currentDocIndex < 0) currentDocIndex = 0; |
| if (currentDocIndex >= extractionResults.length) currentDocIndex = extractionResults.length - 1; |
| updateDocumentView(); |
| } |
| |
| function clearAll() { |
| files = []; |
| customKeywords = []; |
| extractionResults = []; |
| currentDocIndex = 0; |
| |
| fileInput.value = ''; |
| selectedFiles.innerHTML = ''; |
| fileList.classList.add('hidden'); |
| extractBtn.disabled = true; |
| keywordsList.innerHTML = ''; |
| documentPreview.textContent = ''; |
| emptyState.classList.remove('hidden'); |
| resultsContainer.classList.add('hidden'); |
| } |
| async function exportResults() { |
| try { |
| const response = await fetch('/api/export', { |
| method: 'POST', |
| headers: { |
| 'Content-Type': 'application/json', |
| }, |
| body: JSON.stringify({ |
| results: extractionResults, |
| format: 'csv' |
| }) |
| }); |
| |
| if (!response.ok) { |
| throw new Error(`Export failed: ${response.status}`); |
| } |
| |
| const blob = await response.blob(); |
| const url = window.URL.createObjectURL(blob); |
| const a = document.createElement('a'); |
| a.href = url; |
| a.download = `extracted_data_${new Date().toISOString().slice(0,10)}.csv`; |
| document.body.appendChild(a); |
| a.click(); |
| window.URL.revokeObjectURL(url); |
| a.remove(); |
| |
| } catch (error) { |
| console.error('Export failed:', error); |
| alert('Export failed. Please try again.'); |
| } |
| } |
| }); |
| </script> |
| </body> |
| </html> |
|
|