| <!DOCTYPE html> |
| <html lang="en"> |
|
|
| <head> |
| <meta charset="UTF-8"> |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> |
| <title>Proofly - Dashboard</title> |
| <meta name="description" |
| content="Advanced AI-powered fact-checking system that verifies claims using multiple sources and natural language inference."> |
| <link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}"> |
| <link rel="preconnect" href="https://fonts.googleapis.com"> |
| <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> |
| <link href="https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;500;600;700;800&display=swap" |
| rel="stylesheet"> |
| <script src="https://unpkg.com/@phosphor-icons/web"></script> |
| |
| <script>if (localStorage.getItem('proofly-theme') === 'dark') document.documentElement.setAttribute('data-theme', 'dark');</script> |
| <style> |
| |
| .ocr-upload-area { |
| display: flex; |
| align-items: center; |
| gap: 0.75rem; |
| padding: 0.6rem 1rem; |
| margin-bottom: 0.75rem; |
| background: rgba(37, 99, 235, 0.04); |
| border: 1.5px dashed rgba(37, 99, 235, 0.25); |
| border-radius: 12px; |
| transition: all 0.2s ease; |
| } |
| |
| .ocr-upload-area:hover { |
| background: rgba(37, 99, 235, 0.07); |
| border-color: rgba(37, 99, 235, 0.45); |
| } |
| |
| .ocr-label { |
| font-size: 0.82rem; |
| color: var(--text-muted); |
| flex: 1; |
| } |
| |
| .ocr-label strong { |
| color: var(--primary); |
| } |
| |
| .ocr-btn { |
| display: inline-flex; |
| align-items: center; |
| gap: 0.4rem; |
| background: var(--primary); |
| color: white; |
| border: none; |
| border-radius: 8px; |
| padding: 0.45rem 0.9rem; |
| font-size: 0.82rem; |
| font-weight: 600; |
| cursor: pointer; |
| font-family: inherit; |
| transition: background 0.2s; |
| white-space: nowrap; |
| } |
| |
| .ocr-btn:hover { |
| background: #1d4ed8; |
| } |
| |
| .ocr-btn i { |
| font-size: 1rem; |
| } |
| |
| .ocr-preview-wrap { |
| display: none; |
| align-items: center; |
| gap: 0.6rem; |
| flex: 1; |
| } |
| |
| .ocr-preview-wrap.visible { |
| display: flex; |
| } |
| |
| .ocr-thumb { |
| width: 38px; |
| height: 38px; |
| border-radius: 6px; |
| object-fit: cover; |
| border: 2px solid rgba(37, 99, 235, 0.3); |
| } |
| |
| .ocr-status-text { |
| font-size: 0.82rem; |
| color: var(--text-muted); |
| } |
| |
| .ocr-status-text.success { |
| color: #10b981; |
| font-weight: 600; |
| } |
| |
| .ocr-status-text.error { |
| color: #ef4444; |
| font-weight: 600; |
| } |
| |
| .ocr-spinner { |
| display: inline-block; |
| width: 14px; |
| height: 14px; |
| border: 2px solid rgba(37, 99, 235, 0.2); |
| border-top-color: var(--primary); |
| border-radius: 50%; |
| animation: spin 0.7s linear infinite; |
| vertical-align: middle; |
| margin-right: 4px; |
| } |
| |
| @keyframes spin { |
| to { |
| transform: rotate(360deg); |
| } |
| } |
| |
| .ocr-clear-btn { |
| background: none; |
| border: none; |
| cursor: pointer; |
| color: var(--text-light); |
| font-size: 1rem; |
| padding: 0.2rem; |
| line-height: 1; |
| transition: color 0.2s; |
| } |
| |
| .ocr-clear-btn:hover { |
| color: #ef4444; |
| } |
| </style> |
| </head> |
|
|
| <body> |
| <div class="app-container"> |
| |
| <aside class="sidebar"> |
| <div class="sidebar-top"> |
| <button class="icon-btn active-icon" title="New Check"><i class="ph ph-plus"></i></button> |
| <div class="spacer"></div> |
| <a href="/history" class="nav-btn" title="My History" style="text-decoration:none;"><i |
| class="ph ph-clock-counter-clockwise"></i></a> |
| {% if g.is_admin %} |
| <a href="/admin" class="nav-btn" title="God Mode" |
| style="text-decoration:none; color: var(--primary);"><i class="ph ph-shield-check"></i></a> |
| {% endif %} |
| </div> |
| <div class="sidebar-bottom"> |
| <button class="theme-toggle-btn" title="Toggle dark / light mode" onclick="toggleTheme()"> |
| <i class="ph ph-moon icon-moon"></i> |
| <i class="ph ph-sun icon-sun"></i> |
| </button> |
| <div class="profile-menu-container"> |
| <div class="profile-btn" onclick="toggleProfileMenu()" title="{{ g.username }}" |
| style="background: transparent; padding: 0; width: 44px; height: 44px;"> |
| <img src="{{ url_for('static', filename='default_profile.svg') }}" alt="Profile" |
| style="width: 100%; height: 100%; border-radius: 50%; object-fit: cover; border: 2px solid var(--border-color); background: var(--bg-input);"> |
| </div> |
| <div class="profile-dropdown" id="profileDropdown"> |
| <div class="dropdown-header"> |
| <span class="dropdown-username">{{ g.username }}</span> |
| </div> |
| <a href="{{ url_for('auth.logout') }}" class="dropdown-item danger"> |
| <i class="ph ph-sign-out"></i> Logout |
| </a> |
| </div> |
| </div> |
| </div> |
| </aside> |
|
|
| |
| <main class="main-content"> |
| |
| <header class="top-header"> |
| <div class="header-left"> |
| |
| </div> |
| <div class="header-center"> |
| <span class="daily-text">Intelligence Dashboard</span> |
| </div> |
| <div class="header-right" style="display:flex; align-items:center; gap:1rem;"> |
| </div> |
| </header> |
|
|
| |
| <section class="hero-section"> |
| <div class="hero-content"> |
| <h1 class="hero-title"> |
| <span class="greeting">Proofly,</span> <br><span class="main-text">Ready to Verify |
| Claims?</span> |
| </h1> |
|
|
| |
| <div class="action-cards"> |
| <div class="card" |
| onclick="document.getElementById('claimInput').value='The Earth revolves around the Sun';"> |
| <div class="card-icon" style="color: #2563eb;"><i class="ph-fill ph-globe"></i></div> |
| <p class="card-desc">"The Earth revolves around the Sun"</p> |
| <span class="card-label">Science Fact</span> |
| </div> |
| <div class="card" |
| onclick="document.getElementById('claimInput').value='Artificial Intelligence was invented in 2020';"> |
| <div class="card-icon" style="color: #EC4899;"><i class="ph-fill ph-cpu"></i></div> |
| <p class="card-desc">"Artificial Intelligence was invented in 2020"</p> |
| <span class="card-label">Tech History</span> |
| </div> |
| <div class="card" |
| onclick="document.getElementById('claimInput').value='Water boils at 100 degrees Celsius at sea level';"> |
| <div class="card-icon" style="color: #F59E0B;"><i class="ph-fill ph-drop"></i></div> |
| <p class="card-desc">"Water boils at 100 degrees Celsius at sea level"</p> |
| <span class="card-label">General Knowledge</span> |
| </div> |
| </div> |
| </div> |
| </section> |
|
|
| |
| <div class="prompt-container"> |
| <div class="prompt-header"> |
| <span class="pro-text"><i class="ph ph-info"></i> Enter any claim or statement to verify its |
| authenticity</span> |
| <span class="powered-text"><i class="ph ph-lightning"></i> Real-time multi-source analysis</span> |
| </div> |
|
|
| |
| <input type="file" id="ocrFileInput" accept="image/*,video/mp4,video/quicktime,video/webm" |
| style="display:none;"> |
|
|
| |
| <div class="ocr-preview-wrap" id="ocrPreviewWrap" |
| style="margin-bottom: 0.75rem; padding: 0.75rem; background: var(--bg-input); border-radius: var(--radius-sm); border: 1px solid var(--border-color); flex-wrap: nowrap; align-items: center;"> |
| <img id="ocrThumb" class="ocr-thumb" src="" alt="Preview"> |
| <div id="ocrActionBtns" style="display:flex; gap:0.5rem; flex:1;"> |
| <button type="button" class="ocr-btn" id="btnExtractText"><i class="ph ph-text-t"></i> Extract |
| Text</button> |
| <button type="button" class="ocr-btn" id="btnCheckDeepfake" style="background:#8b5cf6;"><i |
| class="ph ph-scan"></i> Check Authenticity</button> |
| </div> |
| <span class="ocr-status-text" id="ocrStatusText" style="flex:1; display:none;"></span> |
| <button type="button" class="ocr-clear-btn" id="ocrClearBtn" title="Clear image" |
| style="align-self: center;"> |
| <i class="ph ph-x-circle"></i> |
| </button> |
| </div> |
|
|
| <form id="claimForm" class="prompt-form"> |
| <div class="input-wrapper"> |
| <button type="button" class="btn-icon" id="ocrTriggerBtn" title="Upload Image for OCR"><i |
| class="ph ph-paperclip"></i></button> |
| <input type="text" id="claimInput" name="claim" class="prompt-input" |
| placeholder="Example : "The moon landing was faked""> |
| <button type="submit" id="submitBtn" class="btn-submit"><i |
| class="ph-fill ph-paper-plane-right"></i></button> |
| </div> |
| </form> |
|
|
| |
|
|
| |
| <div id="loadingState" class="status-overlay hidden"> |
| <div class="spinner-ring"></div> |
| <p>Analyzing claim and gathering evidence...</p> |
| </div> |
| <div id="errorState" class="status-overlay error-overlay hidden"> |
| <p class="error-text"></p> |
| <button type="button" class="retry-btn" onclick="resetForm()">Try Again</button> |
| </div> |
| </div> |
|
|
| |
| <div id="dfModal" class="status-overlay hidden" |
| style="position: fixed; z-index: 1000; background: rgba(0,0,0,0.6); backdrop-filter: blur(10px); display:flex; padding: 2rem;"> |
| <div class="df-modal-content" |
| style="background: var(--bg-card); max-width: 900px; width: 100%; max-height: 90vh; overflow-y: auto; border-radius: var(--radius-md); border: 1px solid var(--border-color); box-shadow: var(--app-shadow); position: relative; display: flex; flex-direction: column;"> |
| <button type="button" onclick="closeDfModal()" |
| style="position: absolute; top: 1.5rem; right: 1.5rem; background: transparent; border: none; color: var(--text-muted); font-size: 1.5rem; cursor: pointer; z-index: 10;"><i |
| class="ph ph-x"></i></button> |
| <div id="dfModalBody" style="padding: 2rem;"></div> |
| </div> |
| </div> |
| </main> |
| </div> |
|
|
| |
| <script> |
| function toggleTheme() { |
| const isDark = document.documentElement.getAttribute('data-theme') === 'dark'; |
| if (isDark) { |
| document.documentElement.removeAttribute('data-theme'); |
| localStorage.setItem('proofly-theme', 'light'); |
| } else { |
| document.documentElement.setAttribute('data-theme', 'dark'); |
| localStorage.setItem('proofly-theme', 'dark'); |
| } |
| } |
| |
| const form = document.getElementById('claimForm'); |
| const input = document.getElementById('claimInput'); |
| const loadingState = document.getElementById('loadingState'); |
| const errorState = document.getElementById('errorState'); |
| |
| // --- OCR Logic --- |
| const ocrFileInput = document.getElementById('ocrFileInput'); |
| const ocrTriggerBtn = document.getElementById('ocrTriggerBtn'); |
| const ocrPreviewWrap = document.getElementById('ocrPreviewWrap'); |
| const ocrThumb = document.getElementById('ocrThumb'); |
| const ocrStatusText = document.getElementById('ocrStatusText'); |
| const ocrClearBtn = document.getElementById('ocrClearBtn'); |
| const ocrActionBtns = document.getElementById('ocrActionBtns'); |
| const btnExtractText = document.getElementById('btnExtractText'); |
| const btnCheckDeepfake = document.getElementById('btnCheckDeepfake'); |
| const dfModal = document.getElementById('dfModal'); |
| const dfModalBody = document.getElementById('dfModalBody'); |
| |
| function closeDfModal() { |
| dfModal.classList.add('hidden'); |
| } |
| |
| ocrTriggerBtn.addEventListener('click', () => ocrFileInput.click()); |
| |
| ocrClearBtn.addEventListener('click', () => { |
| ocrFileInput.value = ''; |
| ocrPreviewWrap.classList.remove('visible'); |
| ocrThumb.src = ''; |
| ocrStatusText.textContent = ''; |
| ocrStatusText.style.display = 'none'; |
| ocrActionBtns.style.display = 'flex'; |
| }); |
| |
| ocrFileInput.addEventListener('change', () => { |
| const file = ocrFileInput.files[0]; |
| if (!file) return; |
| |
| // Enforce max 20MB limit immediately on the client side |
| const maxVideoSize = 20 * 1024 * 1024; |
| if (file.type.startsWith('video/') && file.size > maxVideoSize) { |
| alert("Video exceeds the 20MB maximum size limit. Please upload a smaller valid clip."); |
| ocrFileInput.value = ''; |
| return; |
| } |
| |
| // Show preview immediately, wait for user action |
| const objectUrl = URL.createObjectURL(file); |
| |
| // If it's a video, adjust the preview dynamically and hide the "Extract Text" button |
| if (file.type.startsWith('video/')) { |
| // Try create a generic video preview or icon |
| ocrThumb.src = objectUrl; |
| ocrThumb.outerHTML = `<video id="ocrThumb" class="ocr-thumb" src="${objectUrl}" muted autoplay playsinline loop></video>`; |
| // Need to re-grab reference if mutated |
| document.getElementById('btnExtractText').style.display = 'none'; |
| } else { |
| // Handle switching back to image from a previous video preview state |
| const thumbElem = document.getElementById('ocrThumb'); |
| if (thumbElem.tagName === 'VIDEO') { |
| thumbElem.outerHTML = `<img id="ocrThumb" class="ocr-thumb" src="${objectUrl}" alt="Preview">`; |
| } else { |
| thumbElem.src = objectUrl; |
| } |
| document.getElementById('btnExtractText').style.display = 'inline-flex'; |
| } |
| |
| ocrPreviewWrap.classList.add('visible'); |
| ocrStatusText.style.display = 'none'; |
| ocrActionBtns.style.display = 'flex'; |
| }); |
| |
| btnExtractText.addEventListener('click', async () => { |
| const file = ocrFileInput.files[0]; |
| if (!file) return; |
| |
| ocrActionBtns.style.display = 'none'; |
| ocrStatusText.style.display = 'block'; |
| ocrStatusText.className = 'ocr-status-text'; |
| ocrStatusText.innerHTML = '<span class="ocr-spinner"></span> Extracting text from image…'; |
| |
| try { |
| const formData = new FormData(); |
| formData.append('image', file); |
| |
| const response = await fetch('/ocr', { method: 'POST', body: formData }); |
| const data = await response.json(); |
| |
| if (data.success && data.text) { |
| input.value = data.text; |
| ocrStatusText.className = 'ocr-status-text success'; |
| ocrStatusText.innerHTML = '<i class="ph ph-check-circle"></i> Text extracted — claim auto-filled!'; |
| } else if (data.success && !data.text) { |
| ocrStatusText.className = 'ocr-status-text error'; |
| ocrStatusText.innerHTML = '<i class="ph ph-warning"></i> No text found in image.'; |
| } else { |
| ocrStatusText.className = 'ocr-status-text error'; |
| ocrStatusText.innerHTML = '<i class="ph ph-warning"></i> ' + (data.error || 'Could not process image.'); |
| } |
| } catch (err) { |
| ocrStatusText.className = 'ocr-status-text error'; |
| ocrStatusText.innerHTML = '<i class="ph ph-warning"></i> Network error during OCR.'; |
| } |
| }); |
| |
| btnCheckDeepfake.addEventListener('click', async () => { |
| const file = ocrFileInput.files[0]; |
| if (!file) return; |
| |
| const isVideo = file.type.startsWith('video/'); |
| const endpoint = isVideo ? '/api/verify_video' : '/api/verify_image'; |
| const fileKey = isVideo ? 'video' : 'image'; |
| |
| ocrActionBtns.style.display = 'none'; |
| ocrStatusText.style.display = 'block'; |
| ocrStatusText.className = 'ocr-status-text'; |
| ocrStatusText.innerHTML = '<span class="ocr-spinner"></span> ' + (isVideo ? 'Analyzing video frames...' : 'Running 5-Model Ensemble Deepfake Check...'); |
| |
| try { |
| const formData = new FormData(); |
| formData.append(fileKey, file); |
| |
| const response = await fetch(endpoint, { method: 'POST', body: formData }); |
| const data = await response.json(); |
| |
| if (data.success) { |
| ocrStatusText.className = 'ocr-status-text success'; |
| ocrStatusText.innerHTML = '<i class="ph ph-check-circle"></i> Authenticity check complete!'; |
| |
| // Render DF Modal |
| renderDfModal(data, isVideo); |
| dfModal.classList.remove('hidden'); |
| } else { |
| ocrStatusText.className = 'ocr-status-text error'; |
| ocrStatusText.innerHTML = '<i class="ph ph-warning"></i> ' + (data.error || 'Analysis failed.'); |
| } |
| } catch (err) { |
| ocrStatusText.className = 'ocr-status-text error'; |
| ocrStatusText.innerHTML = '<i class="ph ph-warning"></i> Network error during analysis.'; |
| } |
| }); |
| |
| function renderDfModal(data, isVideo = false) { |
| const isFake = data.label === 'FAKE'; |
| const isReal = data.label === 'REAL'; |
| const badgeColor = isFake ? '#ef4444' : (isReal ? '#10b981' : '#f59e0b'); |
| const wrapClass = isFake ? 'fake-wrap' : (isReal ? 'real-wrap' : 'uncertain-wrap'); |
| |
| const mediaText = isVideo ? (isFake ? "FAKE VIDEO" : "AUTHENTIC VIDEO") : (isFake ? "FAKE IMAGE" : "AUTHENTIC PHOTO"); |
| |
| let scoresHtml = ''; |
| const models = [ |
| { id: 'hf_primary', name: 'AI Detector (ViT)', weight: 35 }, |
| { id: 'hf_secondary', name: 'Deepfake Det (ViT)', weight: 25 }, |
| { id: 'clip', name: 'CLIP Semantics', weight: 20 }, |
| { id: 'frequency', name: 'Frequency / Noise', weight: 15 }, |
| { id: 'cnn', name: 'CNN EfficientNet', weight: 5 } |
| ]; |
| |
| models.forEach(m => { |
| const p = data.scores && data.scores[m.id] ? data.scores[m.id] : 0; |
| scoresHtml += ` |
| <div style="display:flex; align-items:center; gap: 1rem; margin-bottom: 0.75rem;"> |
| <div style="width: 140px; font-size: 0.85rem; font-weight:600; color:var(--text-main);">${m.name}</div> |
| <div style="flex:1; height: 8px; background: var(--border-color); border-radius: 4px; overflow:hidden;"> |
| <div style="height:100%; width: ${p * 100}%; background: ${p > 0.5 ? '#ef4444' : '#10b981'};"></div> |
| </div> |
| <div style="width: 50px; text-align:right; font-family: monospace; font-size:0.85rem;">${(p * 100).toFixed(1)}%</div> |
| </div> |
| `; |
| }); |
| |
| dfModalBody.innerHTML = ` |
| <div style="display:flex; gap: 2rem; flex-wrap: wrap;"> |
| <div style="flex:1; min-width:300px;"> |
| <div style="margin-bottom: 2rem;"> |
| <h2 style="font-size:1.8rem; margin-bottom:0.5rem; color:${badgeColor}; display:flex; align-items:center; gap:0.5rem;"> |
| ${isFake ? '<i class="ph-fill ph-warning-circle"></i> ' + mediaText : (isReal ? '<i class="ph-fill ph-check-circle"></i> ' + mediaText : '<i class="ph-fill ph-question"></i> UNCERTAIN')} |
| </h2> |
| <p style="color:var(--text-muted); font-size:1.1rem; margin-bottom: 1rem;"> |
| The ensemble is <strong>${isFake ? (data.fake_prob * 100).toFixed(1) + '% confident' : (data.real_prob * 100).toFixed(1) + '% confident'}</strong>. |
| </p> |
| <div style="border: 1px solid var(--border-color); border-radius: var(--radius-sm); padding: 1.5rem; margin-bottom: 1.5rem; background: var(--bg-input);"> |
| <h4 style="margin-bottom:1rem; font-size:0.9rem; text-transform:uppercase; color:var(--text-muted);">Ensemble Votes</h4> |
| ${scoresHtml} |
| </div> |
| <div style="font-size:0.95rem; color:var(--text-muted); white-space:pre-wrap; line-height:1.6; padding:1rem; background:rgba(37,99,235,0.05); border-radius:var(--radius-sm);"> |
| ${data.explanation} |
| </div> |
| </div> |
| </div> |
| ${data.gradcam_b64 ? ` |
| <div style="width:340px; border-radius: var(--radius-sm); overflow:hidden; border: 1px solid var(--border-color); background:#000; align-self:flex-start;"> |
| <h4 style="background:var(--bg-input); padding: 0.75rem 1rem; margin:0; font-size:0.85rem; border-bottom:1px solid var(--border-color); color:var(--text-main);">CNN GradCAM Heatmap</h4> |
| <img src="data:image/png;base64,${data.gradcam_b64}" style="width:100%; display:block; object-fit:contain; max-height:400px;" alt="GradCAM"> |
| <div style="padding:0.75rem 1rem; font-size:0.8rem; color:var(--text-muted); background:var(--bg-input);">Shows regions that triggered the CNN classifier most strongly. High activations often correspond to AI generation artifacts.</div> |
| </div>` : ''} |
| </div> |
| `; |
| } |
| |
| // --- Form Submit Logic --- |
| function resetForm() { |
| errorState.classList.add('hidden'); |
| loadingState.classList.add('hidden'); |
| form.querySelector('.input-wrapper').style.opacity = '1'; |
| } |
| |
| form.addEventListener('submit', async (e) => { |
| e.preventDefault(); |
| |
| const claim = input.value.trim(); |
| if (!claim) { |
| showError('Please enter a claim to verify'); |
| return; |
| } |
| |
| // Show loading state |
| form.querySelector('.input-wrapper').style.opacity = '0.5'; |
| errorState.classList.add('hidden'); |
| loadingState.classList.remove('hidden'); |
| |
| try { |
| const formData = new FormData(); |
| formData.append('claim', claim); |
| |
| const response = await fetch('/check', { |
| method: 'POST', |
| body: formData |
| }); |
| |
| const data = await response.json(); |
| |
| if (data.success) { |
| window.location.href = '/results'; |
| } else { |
| showError(data.error || 'Failed to verify claim'); |
| } |
| } catch (error) { |
| showError('Network error. Please try again.'); |
| } |
| }); |
| |
| function showError(message) { |
| loadingState.classList.add('hidden'); |
| errorState.classList.remove('hidden'); |
| errorState.querySelector('.error-text').textContent = message; |
| form.querySelector('.input-wrapper').style.opacity = '1'; |
| } |
| |
| function toggleProfileMenu() { |
| const menu = document.getElementById('profileDropdown'); |
| if (menu) menu.classList.toggle('open'); |
| } |
| |
| document.addEventListener('click', (e) => { |
| const container = document.querySelector('.profile-menu-container'); |
| const menu = document.getElementById('profileDropdown'); |
| if (container && menu && !container.contains(e.target)) { |
| menu.classList.remove('open'); |
| } |
| }); |
| |
| // --- Suggested Facts Logic (Cards) --- |
| function updateSuggestedFacts() { |
| fetch('/api/suggested_facts') |
| .then(res => res.json()) |
| .then(data => { |
| if (data.success && data.facts.length === 3) { |
| const cards = document.querySelectorAll('.action-cards .card'); |
| if (cards.length === 3) { |
| cards.forEach((card, i) => { |
| // Add smooth transition |
| card.style.transition = 'opacity 0.6s ease'; |
| card.style.opacity = '0'; |
| setTimeout(() => { |
| card.setAttribute('onclick', `document.getElementById('claimInput').value='${data.facts[i].replace(/'/g, "\\\\\\'")}';`); |
| const desc = card.querySelector('.card-desc'); |
| if (desc) desc.textContent = `"${data.facts[i]}"`; |
| card.style.opacity = '1'; |
| }, 600); // match transition time |
| }); |
| } |
| } |
| }) |
| .catch(err => console.error("Error fetching suggested facts:", err)); |
| } |
| |
| // Update every 5 minutes (300000ms) |
| setInterval(updateSuggestedFacts, 300000); |
| </script> |
| </body> |
|
|
| </html> |