Created
August 6, 2025 08:55
-
-
Save mstatt/cea118190fa371b8e37631fa9ef2b916 to your computer and use it in GitHub Desktop.
The single page html for implementing the FALCONLENS Image Forensic Solution
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <!DOCTYPE html> | |
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>FALCONLENS // Image Forensics v1.2</title> | |
| <style> | |
| @import url('https://fonts.googleapis.com/css2?family=Orbitron:wght@400;700&family=Roboto+Mono:wght@300;400&display=swap'); | |
| :root { | |
| --primary-color: #ff003c; | |
| --secondary-color: #00e5ff; | |
| --background-color: #0a0a0a; | |
| --text-color: #e0e0e0; | |
| --border-color: rgba(255, 0, 60, 0.4); | |
| --glow-color: rgba(255, 0, 60, 0.7); | |
| --container-bg: rgba(15, 15, 15, 0.85); | |
| } | |
| * { box-sizing: border-box; margin: 0; padding: 0; } | |
| body { | |
| background-color: var(--background-color); | |
| color: var(--text-color); | |
| font-family: 'Roboto Mono', monospace; | |
| display: flex; | |
| flex-direction: column; | |
| align-items: center; | |
| min-height: 100vh; | |
| padding: 2rem; | |
| background-image: | |
| linear-gradient(rgba(0, 229, 255, 0.04) 1px, transparent 1px), | |
| linear-gradient(90deg, rgba(0, 229, 255, 0.04) 1px, transparent 1px); | |
| background-size: 25px 25px; | |
| } | |
| .container { | |
| width: 100%; | |
| max-width: 900px; | |
| background: var(--container-bg); | |
| border: 1px solid var(--border-color); | |
| padding: 2rem; | |
| backdrop-filter: blur(5px); | |
| } | |
| header { | |
| text-align: center; | |
| margin-bottom: 2rem; | |
| padding: 1.5rem; | |
| border-bottom: 1px solid var(--border-color); | |
| } | |
| header h1 { | |
| font-family: 'Orbitron', sans-serif; | |
| color: var(--primary-color); | |
| font-size: 2.5rem; | |
| letter-spacing: 4px; | |
| text-shadow: 0 0 15px var(--glow-color); | |
| } | |
| header p { margin-top: 0.5rem; font-size: 0.9rem; max-width: 700px; margin-left: auto; margin-right: auto; } | |
| .mode-switcher { display: flex; border-bottom: 1px solid var(--border-color); margin-bottom: 2rem; } | |
| .tab-btn { | |
| flex-grow: 1; | |
| padding: 1rem; | |
| background: transparent; | |
| border: none; | |
| color: var(--text-color); | |
| font-family: 'Orbitron', sans-serif; | |
| font-size: 1.1rem; | |
| cursor: pointer; | |
| transition: all 0.3s ease; | |
| border-bottom: 3px solid transparent; | |
| } | |
| .tab-btn:hover { background: rgba(255, 0, 60, 0.1); } | |
| .tab-btn.active { color: var(--primary-color); border-bottom-color: var(--primary-color); text-shadow: 0 0 5px var(--glow-color); } | |
| .analysis-form { display: none; } | |
| .analysis-form.active { display: block; } | |
| .upload-grid { display: grid; grid-template-columns: 1fr; gap: 1.5rem; margin-bottom: 1.5rem; } | |
| .upload-grid.two-cols { grid-template-columns: 1fr 1fr; } | |
| .drop-zone { | |
| border: 2px dashed var(--border-color); | |
| padding: 1rem; | |
| text-align: center; | |
| cursor: pointer; | |
| transition: all 0.3s ease; | |
| position: relative; | |
| display: flex; | |
| flex-direction: column; | |
| justify-content: center; | |
| min-height: 150px; | |
| } | |
| .drop-zone:hover, .drop-zone.drag-over { | |
| border-color: var(--primary-color); | |
| background: rgba(255, 0, 60, 0.1); | |
| text-shadow: 0 0 5px var(--glow-color); | |
| } | |
| .drop-zone h3 { color: var(--secondary-color); margin-bottom: 0.5rem; } | |
| .drop-zone p { font-size: 0.8rem; } | |
| .drop-zone .file-name { color: var(--primary-color); font-style: italic; margin-top: 0.5rem; height: 1.2em; word-break: break-all; } | |
| .drop-zone input[type="file"] { display: none; } | |
| #submit-btn { | |
| display: block; width: 100%; | |
| background-color: transparent; | |
| color: var(--primary-color); | |
| border: 2px solid var(--primary-color); | |
| padding: 1rem 2.5rem; | |
| font-family: 'Orbitron', monospace; | |
| font-size: 1.2rem; | |
| font-weight: 700; | |
| cursor: pointer; | |
| text-transform: uppercase; | |
| letter-spacing: 2px; | |
| transition: all 0.3s ease; | |
| } | |
| #submit-btn:hover:not(:disabled) { background-color: var(--primary-color); color: var(--background-color); } | |
| #submit-btn:disabled { border-color: #555; color: #555; cursor: not-allowed; } | |
| #response-container { margin-top: 2rem; display: none; border-top: 1px solid var(--border-color); padding-top: 2rem; } | |
| #response-title { color: var(--secondary-color); font-family: 'Orbitron'; font-size: 1.5rem; margin-bottom: 1rem; } | |
| .result-card { | |
| background: rgba(20, 20, 20, 0.7); | |
| border: 1px solid #333; | |
| margin-bottom: 1.5rem; | |
| padding: 1.5rem; | |
| } | |
| .result-card h3 { color: var(--primary-color); border-bottom: 1px solid var(--border-color); padding-bottom: 0.5rem; margin-bottom: 1rem; } | |
| .result-item { display: flex; justify-content: space-between; align-items: center; margin-bottom: 0.8rem; flex-wrap: wrap; gap: 1rem; } | |
| .result-item .label { font-weight: bold; } | |
| .result-item .value { font-weight: bold; } | |
| .value.high { color: #ff4d4d; } .value.medium, .value.moderate { color: #ffc107; } .value.low, .value.very, .value.identical { color: #4caf50; } .value.different { color: #ffc107;} | |
| .confidence-bar-container { width: 100%; max-width: 200px; background: #333; height: 10px; border-radius: 5px; overflow: hidden; } | |
| .confidence-bar { height: 100%; background: var(--secondary-color); border-radius: 5px; transition: width 0.5s ease; } | |
| .result-details { border: 1px solid #222; margin-top: 1rem; } | |
| .result-details summary { cursor: pointer; padding: 0.8rem; background-color: #1a1a1a; font-weight: bold; } | |
| .result-details-content { padding: 1rem; background-color: #111; } | |
| .detail-item { display: grid; grid-template-columns: 200px 1fr; gap: 0.5rem; margin-bottom: 0.5rem; word-break: break-all; } | |
| .detail-item .key { color: var(--text-color); } | |
| .detail-item .value { color: #b0b0b0; } | |
| .detail-item img { max-width: 100%; border: 1px solid #444; margin-top: 0.5rem; } | |
| </style> | |
| </head> | |
| <body> | |
| <div class="container"> | |
| <header> | |
| <h1>FALCONLENS</h1> | |
| <p>ADVANCED IMAGE FORENSICS // FALCONS.AI</p> | |
| </header> | |
| <div class="mode-switcher"> | |
| <button class="tab-btn active" data-mode="single">Single Image Analysis</button> | |
| <button class="tab-btn" data-mode="comparison">Comparison Analysis</button> | |
| </div> | |
| <!-- Single Image Form --> | |
| <form id="single-form" class="analysis-form active"> | |
| <div class="upload-grid"> | |
| <label class="drop-zone" id="single-drop-zone"> | |
| <h3>SUSPECT IMAGE</h3> | |
| <p>DRAG & DROP OR CLICK TO UPLOAD</p> | |
| <p class="file-name" id="single-file-name"></p> | |
| <input type="file" id="single-file-input" accept="image/*"> | |
| </label> | |
| </div> | |
| </form> | |
| <!-- Comparison Form --> | |
| <form id="comparison-form" class="analysis-form"> | |
| <div class="upload-grid two-cols"> | |
| <label class="drop-zone" id="original-drop-zone"> | |
| <h3>ORIGINAL IMAGE</h3> | |
| <p>DRAG & DROP OR CLICK</p> | |
| <p class="file-name" id="original-file-name"></p> | |
| <input type="file" id="original-file-input" accept="image/*"> | |
| </label> | |
| <label class="drop-zone" id="suspect-drop-zone"> | |
| <h3>SUSPECT IMAGE</h3> | |
| <p>DRAG & DROP OR CLICK</p> | |
| <p class="file-name" id="suspect-file-name"></p> | |
| <input type="file" id="suspect-file-input" accept="image/*"> | |
| </label> | |
| </div> | |
| </form> | |
| <button id="submit-btn" disabled>SELECT IMAGE(S) TO BEGIN</button> | |
| <div id="response-container"> | |
| <h2 id="response-title"></h2> | |
| <div id="response-content"></div> | |
| </div> | |
| </div> | |
| <script> | |
| let currentMode = 'single'; | |
| const tabs = document.querySelectorAll('.tab-btn'); | |
| const forms = document.querySelectorAll('.analysis-form'); | |
| const submitBtn = document.getElementById('submit-btn'); | |
| const responseContainer = document.getElementById('response-container'); | |
| const responseTitle = document.getElementById('response-title'); | |
| const responseContent = document.getElementById('response-content'); | |
| const singleFileInput = document.getElementById('single-file-input'); | |
| const originalFileInput = document.getElementById('original-file-input'); | |
| const suspectFileInput = document.getElementById('suspect-file-input'); | |
| function setupDropZone(dropZoneId, inputId, nameDisplayId) { | |
| const dropZone = document.getElementById(dropZoneId); | |
| const fileInput = document.getElementById(inputId); | |
| const nameDisplay = document.getElementById(nameDisplayId); | |
| dropZone.addEventListener('click', () => fileInput.click()); | |
| fileInput.addEventListener('change', () => handleFile(fileInput.files[0], nameDisplay, fileInput)); | |
| ['dragenter', 'dragover', 'dragleave', 'drop'].forEach(e => dropZone.addEventListener(e, preventDefaults)); | |
| ['dragenter', 'dragover'].forEach(e => dropZone.addEventListener(e, () => dropZone.classList.add('drag-over'))); | |
| ['dragleave', 'drop'].forEach(e => dropZone.addEventListener(e, () => dropZone.classList.remove('drag-over'))); | |
| dropZone.addEventListener('drop', (e) => handleFile(e.dataTransfer.files[0], nameDisplay, fileInput)); | |
| } | |
| function preventDefaults(e) { | |
| e.preventDefault(); | |
| e.stopPropagation(); | |
| } | |
| function handleFile(file, nameDisplay, inputElement) { | |
| if (file && file.type.startsWith('image/')) { | |
| nameDisplay.textContent = `[ ${file.name} ]`; | |
| const dataTransfer = new DataTransfer(); | |
| dataTransfer.items.add(file); | |
| inputElement.files = dataTransfer.files; | |
| validateForm(); | |
| } | |
| } | |
| function validateForm() { | |
| let isValid = false; | |
| if (currentMode === 'single') { | |
| if (singleFileInput.files.length > 0) { | |
| isValid = true; | |
| submitBtn.textContent = 'ANALYZE SINGLE IMAGE'; | |
| } else { | |
| submitBtn.textContent = 'SELECT SUSPECT IMAGE'; | |
| } | |
| } | |
| else if (currentMode === 'comparison') { | |
| const hasOriginal = originalFileInput.files.length > 0; | |
| const hasSuspect = suspectFileInput.files.length > 0; | |
| if (hasOriginal && hasSuspect) { | |
| isValid = true; | |
| submitBtn.textContent = 'ANALYZE COMPARISON'; | |
| } else if (hasOriginal && !hasSuspect) { | |
| submitBtn.textContent = 'SELECT SUSPECT IMAGE'; | |
| } else if (!hasOriginal && hasSuspect) { | |
| submitBtn.textContent = 'SELECT ORIGINAL IMAGE'; | |
| } else { | |
| submitBtn.textContent = 'SELECT BOTH IMAGES'; | |
| } | |
| } | |
| submitBtn.disabled = !isValid; | |
| } | |
| tabs.forEach(tab => { | |
| tab.addEventListener('click', () => { | |
| currentMode = tab.dataset.mode; | |
| tabs.forEach(t => t.classList.remove('active')); | |
| tab.classList.add('active'); | |
| forms.forEach(f => f.classList.remove('active')); | |
| document.getElementById(`${currentMode}-form`).classList.add('active'); | |
| validateForm(); | |
| clearResponse(); | |
| }); | |
| }); | |
| setupDropZone('single-drop-zone', 'single-file-input', 'single-file-name'); | |
| setupDropZone('original-drop-zone', 'original-file-input', 'original-file-name'); | |
| setupDropZone('suspect-drop-zone', 'suspect-file-input', 'suspect-file-name'); | |
| // Handle form submission - REFACTORED FOR NEW API STRUCTURE | |
| submitBtn.addEventListener('click', async () => { | |
| // --- REFACTORED SECTION START --- | |
| // IMPORTANT: Replace with your actual RapidAPI key. | |
| // You can get a key from https://rapidapi.com/falcons-falcons-default/api/falconlens1 | |
| const API_KEY = ""; | |
| const API_HOST = "falconlens1.p.rapidapi.com"; | |
| let apiUrl = ''; | |
| const formData = new FormData(); | |
| if (currentMode === 'single') { | |
| if (singleFileInput.files.length === 0) return; | |
| formData.append('image', singleFileInput.files[0]); | |
| apiUrl = 'https://falconlens1.p.rapidapi.com/forensics/analyze-single'; | |
| } else { | |
| if (originalFileInput.files.length === 0 || suspectFileInput.files.length === 0) return; | |
| formData.append('original_image', originalFileInput.files[0]); | |
| formData.append('suspect_image', suspectFileInput.files[0]); | |
| apiUrl = 'https://falconlens1.p.rapidapi.com/forensics/analyze-comparison'; | |
| } | |
| const options = { | |
| method: 'POST', | |
| headers: { | |
| 'x-rapidapi-key': API_KEY, | |
| 'x-rapidapi-host': API_HOST | |
| // 'Content-Type': 'multipart/form-data' is set automatically by the browser when using FormData | |
| }, | |
| body: formData | |
| }; | |
| setLoadingState(true); | |
| try { | |
| // Check if the user has replaced the placeholder API key | |
| if (API_KEY === "YOUR_RAPIDAPI_KEY_HERE") { | |
| throw new Error("API Key not set. Please replace 'YOUR_RAPIDAPI_KEY_HERE' in the script."); | |
| } | |
| const response = await fetch(apiUrl, options); | |
| if (!response.ok) { | |
| // Try to parse the JSON error response from the API | |
| const errData = await response.json().catch(() => ({ detail: "Could not parse error response from server." })); | |
| throw new Error(errData.detail || `HTTP Error: ${response.status}`); | |
| } | |
| const data = await response.json(); | |
| displayResults(data.results); | |
| } catch (error) { | |
| displayError(error.message); | |
| } finally { | |
| setLoadingState(false); | |
| } | |
| // --- REFACTORED SECTION END --- | |
| }); | |
| function setLoadingState(isLoading) { | |
| if (isLoading) { | |
| submitBtn.disabled = true; | |
| submitBtn.textContent = "ANALYZING..."; | |
| responseContainer.style.display = 'block'; | |
| responseTitle.textContent = "SYSTEM // PROCESSING REQUEST"; | |
| responseContent.innerHTML = `<p>Transmitting data to FALCONLENS core... Please wait.</p>`; | |
| } else { | |
| validateForm(); | |
| } | |
| } | |
| function displayResults(results) { | |
| responseTitle.textContent = "SYSTEM // ANALYSIS COMPLETE"; | |
| responseContent.innerHTML = ''; | |
| if (results && results.length > 0) { | |
| results.forEach(result => { | |
| responseContent.innerHTML += createResultCard(result); | |
| }); | |
| } else { | |
| responseContent.innerHTML = `<p>Analysis complete, but no results were returned.</p>`; | |
| } | |
| } | |
| function displayError(message) { | |
| responseTitle.textContent = "SYSTEM // ERROR"; | |
| responseContent.innerHTML = `<p style="color:var(--primary-color);">Connection failed or an error occurred: <br/><strong>${message}</strong></p><p>Please check the browser console and ensure the FALCONLENS API server is running and accessible with a valid API Key.</p>`; | |
| } | |
| function clearResponse() { | |
| responseContainer.style.display = 'none'; | |
| responseTitle.textContent = ''; | |
| responseContent.innerHTML = ''; | |
| } | |
| function createResultCard(result) { | |
| const likelihoodClass = result.manipulation_likelihood.toLowerCase().replace(/\s+/g, '-'); | |
| const confidence = result.confidence_score * 100; | |
| let detailsHtml = ''; | |
| if (result.details) { | |
| for (const [key, value] of Object.entries(result.details)) { | |
| const cleanKey = key.replace(/_b64$/, '').replace(/_/g, ' '); | |
| if (key.endsWith('_b64') && value) { | |
| detailsHtml += `<div class="detail-item"> | |
| <span class="key" style="text-transform: capitalize;">${cleanKey}:</span> | |
| <span class="value"><img src="data:image/jpeg;base64,${value}" alt="${cleanKey}"></span> | |
| </div>`; | |
| } else if (typeof value === 'object' && value !== null) { | |
| detailsHtml += `<div class="detail-item"> | |
| <span class="key" style="text-transform: capitalize;">${cleanKey}:</span> | |
| <span class="value">${JSON.stringify(value)}</span> | |
| </div>`; | |
| } else { | |
| detailsHtml += `<div class="detail-item"> | |
| <span class="key" style="text-transform: capitalize;">${cleanKey}:</span> | |
| <span class="value">${value}</span> | |
| </div>`; | |
| } | |
| } | |
| } | |
| return ` | |
| <div class="result-card"> | |
| <h3>${result.test_name}</h3> | |
| <div class="result-item"> | |
| <span class="label">Manipulation Likelihood:</span> | |
| <span class="value ${likelihoodClass}">${result.manipulation_likelihood}</span> | |
| </div> | |
| <div class="result-item"> | |
| <span class="label">Confidence Score (${confidence.toFixed(1)}%):</span> | |
| <div class="confidence-bar-container"> | |
| <div class="confidence-bar" style="width: ${confidence}%;"></div> | |
| </div> | |
| </div> | |
| <details class="result-details"> | |
| <summary>VIEW TECHNICAL DETAILS</summary> | |
| <div class="result-details-content">${detailsHtml || '<p>No technical details available for this test.</p>'}</div> | |
| </details> | |
| </div> | |
| `; | |
| } | |
| // Initial validation check on page load | |
| validateForm(); | |
| </script> | |
| </body> | |
| </html> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment