sakshi-new-ui-ocr / index.html
sameernotes's picture
Update index.html
46e5f0d verified
<!DOCTYPE html>
<!-- Start with lang="en" and no 'dark' class initially -->
<html lang="en" class="">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Vision and Discern - AI Text Analysis</title>
<!-- Tailwind CSS via CDN -->
<script src="https://cdn.tailwindcss.com/3.4.1"></script>
<script>
tailwind.config = {
darkMode: 'class', // Enable class-based dark mode
theme: {
extend: {
colors: {
primary: '#4a90e2', // Adjusted primary to match old button color
secondary: '#f0f8ff', // Light blueish background
},
borderRadius: {
'none': '0px', 'sm': '4px', DEFAULT: '8px', 'md': '12px',
'lg': '16px', 'xl': '20px', '2xl': '24px', '3xl': '32px',
'full': '9999px',
'button': '4px'
}
}
}
}
</script>
<!-- Fonts -->
<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=Inter:wght@400;500;600;700&family=Noto+Sans+Devanagari:wght@400;700&display=swap" rel="stylesheet">
<!-- Icons -->
<link href="https://cdn.jsdelivr.net/npm/remixicon@4.2.0/fonts/remixicon.css" rel="stylesheet">
<!-- Custom Styles -->
<style>
body { font-family: 'Inter', sans-serif; }
.hindi-font { font-family: 'Noto Sans Devanagari', sans-serif; }
/* Custom Switch Styles - Keep As Is */
.custom-switch { position: relative; display: inline-block; width: 50px; height: 24px; }
.custom-switch-input { opacity: 0; width: 0; height: 0; }
.custom-switch-slider { position: absolute; cursor: pointer; top: 0; left: 0; right: 0; bottom: 0; background-color: #ccc; transition: .4s; border-radius: 34px; }
.custom-switch-slider:before { position: absolute; content: ""; height: 18px; width: 18px; left: 3px; bottom: 3px; background-color: white; transition: .4s; border-radius: 50%; }
.custom-switch-input:checked + .custom-switch-slider { background-color: #4a90e2; }
.custom-switch-input:checked + .custom-switch-slider:before { transform: translateX(26px); }
/* Dark mode styles ... (kept as is for brevity) */
html.dark body { background-color: #111827; color: #d1d5db; }
html.dark header, html.dark footer { background-color: #1f2937; }
html.dark .tab-container { border-bottom-color: #4b5563; }
html.dark .tab-button { color: #9ca3af; }
html.dark .tab-button.active { color: #60a5fa; border-bottom-color: #60a5fa; }
html.dark .card, html.dark .auth-card, html.dark .results { background-color: #1f2937; border-color: #374151; }
html.dark h1, html.dark h2, html.dark h3, html.dark p, html.dark span, html.dark li, html.dark label, html.dark small, html.dark .subtitle, html.dark .info-text, html.dark .credits p { color: #d1d5db; }
html.dark .text-gray-600 { color: #9ca3af; }
html.dark .text-gray-500 { color: #6b7280; }
html.dark .text-gray-700 { color: #9ca3af; }
html.dark input[type="text"], html.dark input[type="password"], html.dark input[type="email"], html.dark textarea { background-color: #374151; border-color: #4b5563; color: #d1d5db; }
html.dark input::placeholder, html.dark textarea::placeholder { color: #9ca3af; }
html.dark .button-secondary { background-color: #4b5563; color: #d1d5db; }
html.dark .button-secondary:hover { background-color: #374151; }
html.dark .error-message { background-color: #450a0a; color: #fecaca; border-color: #7f1d1d; }
html.dark .upload-area { border-color: #4b5563; color: #9ca3af; }
html.dark .upload-area.dragover { border-color: #60a5fa; background-color: #1e3a8a; }
html.dark #imagePreview, html.dark .sample-image, html.dark .result-image { border-color: #4b5563; }
html.dark .ocr-result { background-color: #374151; border-color: #4b5563; }
html.dark .result-content { background-color: #1f2937; border-color: #4b5563; }
html.dark .result-item { border-bottom-color: #4b5563; }
</style>
</head>
<body class="bg-gray-100 dark:bg-gray-900 min-h-screen flex flex-col text-gray-900 dark:text-gray-200">
<!-- Login Section -->
<div id="login-container" class="min-h-screen flex items-center justify-center p-4">
<!-- Login Card -->
<div id="login-card" class="auth-card bg-white dark:bg-gray-800 p-6 md:p-8 rounded-lg shadow-lg text-center w-full max-w-sm border dark:border-gray-700">
<h1 class="text-2xl font-bold mb-2 text-primary dark:text-blue-400">Vision and Discern</h1>
<h2 class="text-xl font-semibold mb-3">Welcome!</h2>
<p class="text-gray-600 dark:text-gray-400 mb-6 text-sm">Login to access the applications.</p>
<div class="error-message bg-red-100 border border-red-300 text-red-800 dark:bg-red-900 dark:bg-opacity-50 dark:border-red-600 dark:text-red-200 px-4 py-2 rounded-md text-sm mb-4 hidden" id="loginErrorMessage"></div>
<input type="text" id="username" placeholder="Username" class="w-full px-4 py-2 mb-4 border border-gray-300 rounded-button focus:outline-none focus:ring-2 focus:ring-primary/50 dark:bg-gray-700 dark:border-gray-600 dark:text-gray-200">
<input type="password" id="password" placeholder="Password" class="w-full px-4 py-2 mb-4 border border-gray-300 rounded-button focus:outline-none focus:ring-2 focus:ring-primary/50 dark:bg-gray-700 dark:border-gray-600 dark:text-gray-200">
<button id="loginButton" class="w-full bg-primary text-white px-4 py-2 rounded-button hover:bg-blue-700 transition-colors font-medium disabled:opacity-50">Login</button>
<div class="mt-4 text-sm text-gray-600 dark:text-gray-400">
Don't have an account? <a href="#" id="signup-link" class="text-primary dark:text-blue-400 hover:underline">Sign up</a>
</div>
</div>
<!-- Signup Card -->
<div class="auth-card bg-white dark:bg-gray-800 p-6 md:p-8 rounded-lg shadow-lg text-center w-full max-w-sm border dark:border-gray-700 hidden" id="signup-card">
<h1 class="text-2xl font-bold mb-2 text-primary dark:text-blue-400">Vision and Discern</h1>
<h2 class="text-xl font-semibold mb-3">Create Account</h2>
<p class="text-gray-600 dark:text-gray-400 mb-6 text-sm">Sign up to start using applications.</p>
<div class="error-message bg-red-100 border border-red-300 text-red-800 dark:bg-red-900 dark:bg-opacity-50 dark:border-red-600 dark:text-red-200 px-4 py-2 rounded-md text-sm mb-4 hidden" id="signupErrorMessage"></div>
<input type="text" id="signupUsername" placeholder="Username" class="w-full px-4 py-2 mb-4 border border-gray-300 rounded-button focus:outline-none focus:ring-2 focus:ring-primary/50 dark:bg-gray-700 dark:border-gray-600 dark:text-gray-200">
<input type="email" id="signupEmail" placeholder="Email" class="w-full px-4 py-2 mb-4 border border-gray-300 rounded-button focus:outline-none focus:ring-2 focus:ring-primary/50 dark:bg-gray-700 dark:border-gray-600 dark:text-gray-200">
<input type="password" id="signupPassword" placeholder="Password" class="w-full px-4 py-2 mb-4 border border-gray-300 rounded-button focus:outline-none focus:ring-2 focus:ring-primary/50 dark:bg-gray-700 dark:border-gray-600 dark:text-gray-200">
<button id="signupButton" class="w-full bg-primary text-white px-4 py-2 rounded-button hover:bg-blue-700 transition-colors font-medium disabled:opacity-50">Sign Up</button>
<div class="mt-4 text-sm text-gray-600 dark:text-gray-400">
Already have an account? <a href="#" id="login-link" class="text-primary dark:text-blue-400 hover:underline">Login</a>
</div>
</div>
</div>
<!-- Main Application Container -->
<div id="app-container" class="hidden flex-grow flex flex-col">
<!-- Header -->
<header class="bg-white dark:bg-gray-800 shadow-sm sticky top-0 z-50">
<div class="container mx-auto px-4 py-3 flex items-center justify-between">
<!-- Left Side: Logo & Nav -->
<div class="flex items-center">
<a href="#" class="text-xl font-bold text-primary dark:text-blue-400 mr-6">Vision & Discern</a>
<nav class="hidden md:flex space-x-6">
<a href="home.html" target="_blank" class="text-gray-600 dark:text-gray-300 hover:text-primary dark:hover:text-blue-400">Home</a>
<a href="features.html" target="_blank" class="text-gray-600 dark:text-gray-300 hover:text-primary dark:hover:text-blue-400">Key Features</a>
<a href="feedback.html" target="_blank" class="text-gray-600 dark:text-gray-300 hover:text-primary dark:hover:text-blue-400">Feedback</a>
<a href="contact.html" target="_blank" class="text-gray-600 dark:text-gray-300 hover:text-primary dark:hover:text-blue-400">Contact Us</a>
</nav>
</div>
<!-- Right Side: Switches & Logout -->
<div class="flex items-center space-x-4">
<!-- Language Switch -->
<div class="items-center space-x-2 hidden md:flex">
<span class="text-sm text-gray-600 dark:text-gray-400">EN</span>
<label class="custom-switch">
<input type="checkbox" id="languageToggle" class="custom-switch-input"> <!-- Re-enabled, but only changes lang attr -->
<span class="custom-switch-slider"></span>
</label>
<span class="text-sm text-gray-600 dark:text-gray-400 hindi-font">हिंदी</span>
</div>
<!-- Theme Switch -->
<div class="items-center space-x-2 hidden md:flex">
<span class="text-sm text-gray-600 dark:text-gray-400"><i class="ri-sun-line"></i></span>
<label class="custom-switch">
<input type="checkbox" id="themeToggle" class="custom-switch-input">
<span class="custom-switch-slider"></span>
</label>
<span class="text-sm text-gray-600 dark:text-gray-400"><i class="ri-moon-line"></i></span>
</div>
<!-- Logout Button -->
<button id="logoutButton" class="button-secondary bg-red-600 hover:bg-red-700 dark:bg-red-700 dark:hover:bg-red-800 text-white px-3 py-1.5 rounded-button text-sm inline-flex items-center">
<i class="ri-logout-box-r-line mr-1"></i>
Logout
</button>
<!-- Mobile Menu Button -->
<button class="md:hidden w-10 h-10 flex items-center justify-center" aria-label="Toggle Menu">
<i class="ri-menu-line text-gray-600 dark:text-gray-300 text-xl"></i>
</button>
</div>
</div>
</header>
<!-- Main Content Area -->
<main class="flex-grow container mx-auto px-4 py-8">
<!-- App Header Section -->
<section class="mb-8 text-center">
<h1 class="text-3xl md:text-4xl font-bold text-gray-900 dark:text-white mb-3">Comprehensive AI-Powered Text Analysis</h1>
<p class="subtitle text-lg text-gray-700 dark:text-gray-300 max-w-3xl mx-auto">
Leverage advanced AI to extract Hindi text from images, translate it, and predict gender from names.
</p>
</section>
<!-- Tab Navigation -->
<div class="tab-container flex justify-center border-b border-gray-200 dark:border-gray-700 mb-6">
<button class="tab-button flex items-center gap-2 px-6 py-3 text-gray-600 dark:text-gray-300 border-b-2 border-transparent hover:text-primary dark:hover:text-blue-400 transition-colors active" data-tab="ocr">
<i class="ri-image-line"></i> OCR
</button>
<button class="tab-button flex items-center gap-2 px-6 py-3 text-gray-600 dark:text-gray-300 border-b-2 border-transparent hover:text-primary dark:hover:text-blue-400 transition-colors" data-tab="translation">
<i class="ri-translate-2"></i> Translation
</button>
<button class="tab-button flex items-center gap-2 px-6 py-3 text-gray-600 dark:text-gray-300 border-b-2 border-transparent hover:text-primary dark:hover:text-blue-400 transition-colors" data-tab="gender">
<i class="ri-men-line"></i><i class="ri-women-line"></i> Gender
</button>
</div>
<!-- Tab Content Sections -->
<div>
<!-- OCR Tab Content -->
<div class="tab-content active" id="ocr">
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<!-- Input Card -->
<div class="card bg-white dark:bg-gray-800 p-6 rounded-lg shadow-md border border-gray-100 dark:border-gray-700">
<h2 class="card-title text-xl font-semibold text-gray-800 dark:text-white mb-4 border-b border-gray-200 dark:border-gray-600 pb-2 flex items-center">
<i class="ri-upload-cloud-2-line mr-2 text-primary dark:text-blue-400"></i> Upload Image
</h2>
<div id="uploadArea" class="upload-area border-2 border-dashed border-gray-300 dark:border-gray-600 rounded-lg p-6 text-center cursor-pointer hover:border-primary dark:hover:border-blue-400 mb-4 transition-colors">
<div class="upload-icon text-4xl text-gray-400 dark:text-gray-500 mb-2"><i class="ri-image-add-line"></i></div>
<p class="text-gray-700 dark:text-gray-300">Click to select or drag & drop</p>
<p class="info-text text-sm text-gray-500 dark:text-gray-400">PNG, JPG, JPEG</p>
</div>
<input type="file" id="fileInput" accept="image/png, image/jpeg, image/jpg" class="hidden"/>
<!-- Sample Images Section -->
<div class="sample-images-container mt-4 pt-4 border-t border-gray-200 dark:border-gray-600">
<h3 class="text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">Or try a sample image:</h3>
<div class="flex flex-wrap gap-2">
<img src="https://raw.githubusercontent.com/sameer-banchhor-git/data-sakshi/refs/heads/main/%E0%A4%B9%E0%A5%89%E0%A4%B8%E0%A5%8D%E0%A4%9F%E0%A4%B2.png" data-src="https://raw.githubusercontent.com/sameer-banchhor-git/data-sakshi/refs/heads/main/%E0%A4%B9%E0%A5%89%E0%A4%B8%E0%A5%8D%E0%A4%9F%E0%A4%B2.png" alt="Sample Hostel Image" class="sample-image h-16 w-auto border border-gray-300 dark:border-gray-600 rounded-sm cursor-pointer hover:scale-105 hover:shadow-md transition-transform" title="Load Hostel Sample">
<img src="https://raw.githubusercontent.com/sameer-banchhor-git/data-sakshi/refs/heads/main/%E0%A4%B8%E0%A5%8D%E0%A4%B5%E0%A4%BE%E0%A4%B8%E0%A5%8D%E0%A4%A5%E0%A5%8D%E0%A4%AF.png" data-src="https://raw.githubusercontent.com/sameer-banchhor-git/data-sakshi/refs/heads/main/%E0%A4%B8%E0%A5%8D%E0%A4%B5%E0%A4%BE%E0%A4%B8%E0%A5%8D%E0%A4%A5%E0%A5%8D%E0%A4%AF.png" alt="Sample Swasthya Image" class="sample-image h-16 w-auto border border-gray-300 dark:border-gray-600 rounded-sm cursor-pointer hover:scale-105 hover:shadow-md transition-transform" title="Load Swasthya Sample">
<img src="https://raw.githubusercontent.com/sameer-banchhor-git/data-sakshi/refs/heads/main/%E0%A4%B9%E0%A5%81%E0%A4%88.png" data-src="https://raw.githubusercontent.com/sameer-banchhor-git/data-sakshi/refs/heads/main/%E0%A4%B9%E0%A5%81%E0%A4%88.png" alt="Sample Hui Image" class="sample-image h-16 w-auto border border-gray-300 dark:border-gray-600 rounded-sm cursor-pointer hover:scale-105 hover:shadow-md transition-transform" title="Load Hui Sample">
</div>
</div>
<!-- Preview Area -->
<div class="mt-4 pt-4 border-t border-gray-200 dark:border-gray-600">
<h3 class="text-sm font-medium text-gray-700 dark:text-gray-300 mb-2 text-center">Preview</h3>
<div class="flex justify-center">
<img id="imagePreview" src="#" alt="Image Preview" class="hidden max-h-48 w-auto rounded border bg-gray-50 dark:bg-gray-700 dark:border-gray-600 p-1"/>
</div>
</div>
<!-- Action Buttons -->
<div class="mt-6 flex gap-3">
<button id="processButton" disabled class="flex-1 bg-green-600 text-white px-4 py-2 rounded-button font-medium hover:bg-green-700 transition-colors disabled:opacity-50 disabled:cursor-not-allowed inline-flex items-center justify-center gap-2">
<i class="ri-camera-lens-line"></i> Process
</button>
<button id="clearButton" class="button-secondary flex-1 bg-gray-300 text-gray-800 px-4 py-2 rounded-button hover:bg-gray-400 dark:bg-gray-600 dark:text-gray-200 dark:hover:bg-gray-500 transition-colors inline-flex items-center justify-center gap-2 hidden">
<i class="ri-delete-bin-line"></i> Clear
</button>
</div>
<div class="error-message bg-red-100 border border-red-300 text-red-800 dark:bg-red-900 dark:bg-opacity-50 dark:border-red-600 dark:text-red-200 px-4 py-2 rounded-md text-sm mt-4 hidden" id="ocrErrorMessage"></div>
</div>
<!-- Results Card -->
<div class="card bg-white dark:bg-gray-800 p-6 rounded-lg shadow-md border border-gray-100 dark:border-gray-700 flex flex-col items-center justify-center min-h-[400px]"> <!-- Added min-height -->
<h2 class="card-title text-xl font-semibold text-gray-800 dark:text-white mb-4 border-b border-gray-200 dark:border-gray-600 pb-2 w-full text-center flex items-center justify-center">
<i class="ri-file-text-line mr-2 text-primary dark:text-blue-400"></i> Results
</h2>
<!-- Loading Indicator -->
<div id="loadingSpinner" class="flex flex-col items-center justify-center my-4 text-primary dark:text-blue-400 hidden">
<svg class="animate-spin h-8 w-8 text-primary dark:text-blue-400 mb-2" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>
<p class="text-sm">Processing your image...</p>
</div>
<!-- Results Display Area -->
<div id="resultsSection" class="w-full space-y-6 hidden">
<div class="ocr-result bg-gray-50 dark:bg-gray-700 p-4 rounded-md border border-gray-200 dark:border-gray-600">
<div class="result-title text-md font-semibold text-gray-800 dark:text-white mb-2">OCR Text Output</div>
<div class="result-content bg-white dark:bg-gray-800 p-3 rounded text-lg font-medium min-h-[50px] border border-gray-200 dark:border-gray-600" id="ocrOutput"></div>
</div>
<div class="result-card border-t border-gray-200 dark:border-gray-600 pt-4">
<div class="result-title text-md font-semibold text-gray-800 dark:text-white mb-2">Word Detection</div>
<p class="text-sm text-gray-600 dark:text-gray-400 mb-2">Words Detected: <strong class="text-gray-800 dark:text-white"><span id="wordCount">0</span></strong></p>
<img id="wordDetectionImg" class="result-image w-full h-auto max-h-48 object-contain border border-gray-200 dark:border-gray-600 rounded bg-gray-100 dark:bg-gray-700 hidden" src="" alt="Word Detection">
</div>
<div class="result-card border-t border-gray-200 dark:border-gray-600 pt-4">
<div class="result-title text-md font-semibold text-gray-800 dark:text-white mb-2">Prediction</div>
<p class="text-sm text-gray-600 dark:text-gray-400 mb-2">Predicted Text: <strong class="text-gray-800 dark:text-white"><span id="predictionLabel">N/A</span></strong></p>
<img id="predictionImg" class="result-image w-full h-auto max-h-48 object-contain border border-gray-200 dark:border-gray-600 rounded bg-gray-100 dark:bg-gray-700 hidden" src="" alt="Prediction">
</div>
</div>
<p id="initialMessage" class="text-center text-gray-500 dark:text-gray-400 text-sm mt-4">Upload or select a sample image and click 'Process' to see the results.</p>
</div>
</div>
</div>
<!-- Translation Tab Content -->
<div class="tab-content hidden" id="translation">
<div class="card bg-white dark:bg-gray-800 p-6 rounded-lg shadow-md border border-gray-100 dark:border-gray-700 max-w-2xl mx-auto">
<h2 class="card-title text-xl font-semibold text-gray-800 dark:text-white mb-4 border-b border-gray-200 dark:border-gray-600 pb-2 flex items-center">
<i class="ri-translate-2 mr-2 text-primary dark:text-blue-400"></i>
Text Translation
</h2>
<div class="space-y-4">
<div>
<label for="text-to-translate" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Text to Translate:</label>
<textarea id="text-to-translate" placeholder="Enter text here or it will be populated from OCR results..." rows="4" class="w-full px-3 py-2 border border-gray-300 rounded-button focus:outline-none focus:ring-2 focus:ring-primary/50 dark:bg-gray-700 dark:border-gray-600 dark:text-gray-200"></textarea>
</div>
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
<div>
<label for="source-language" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Source Language (Optional):</label>
<!-- *** FIX: Changed placeholder to suggest language codes *** -->
<input type="text" id="source-language" placeholder="e.g., hi (auto-detect if empty)" class="w-full px-3 py-2 border border-gray-300 rounded-button focus:outline-none focus:ring-2 focus:ring-primary/50 dark:bg-gray-700 dark:border-gray-600 dark:text-gray-200">
</div>
<div>
<label for="target-language" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Target Language:</label>
<!-- *** FIX: Changed placeholder to suggest language codes *** -->
<input type="text" id="target-language" placeholder="e.g., en, hi" required class="w-full px-3 py-2 border border-gray-300 rounded-button focus:outline-none focus:ring-2 focus:ring-primary/50 dark:bg-gray-700 dark:border-gray-600 dark:text-gray-200">
</div>
</div>
<div>
<button id="translate-button" class="w-full sm:w-auto bg-primary text-white px-5 py-2 rounded-button hover:bg-blue-700 transition-colors font-medium inline-flex items-center justify-center gap-2 disabled:opacity-50">
<i class="ri-translate"></i> Translate Text
</button>
</div>
</div>
<!-- Loading Indicator -->
<div id="translation-loading" class="flex items-center justify-center my-4 text-primary dark:text-blue-400 hidden">
<svg class="animate-spin -ml-1 mr-3 h-5 w-5" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>
<span>Translating...</span>
</div>
<div class="error-message bg-red-100 border border-red-300 text-red-800 dark:bg-red-900 dark:bg-opacity-50 dark:border-red-600 dark:text-red-200 px-4 py-2 rounded-md text-sm mt-4 hidden" id="translationErrorMessage"></div>
</div>
<!-- Translation Results -->
<div id="translation-result" class="results card bg-white dark:bg-gray-800 p-6 rounded-lg shadow-md border border-gray-100 dark:border-gray-700 max-w-2xl mx-auto mt-6 hidden">
<h3 class="result-header text-lg font-semibold text-gray-800 dark:text-white mb-4 border-b border-gray-200 dark:border-gray-600 pb-2">Translation Result</h3>
<div class="space-y-3 text-sm">
<div class="result-item flex justify-between">
<span class="result-label font-medium text-gray-600 dark:text-gray-400">Source Language:</span>
<span id="detected-source-language" class="text-gray-800 dark:text-white font-medium"></span>
</div>
<div class="result-item flex justify-between">
<span class="result-label font-medium text-gray-600 dark:text-gray-400">Target Language:</span>
<span id="translation-target-language" class="text-gray-800 dark:text-white font-medium"></span>
</div>
<div class="result-item pt-3 mt-3 border-t border-gray-200 dark:border-gray-600">
<div class="result-label font-medium text-gray-600 dark:text-gray-400 mb-1">Translated Text:</div>
<div id="translated-text" class="result-content bg-gray-50 dark:bg-gray-700 p-3 rounded border border-gray-200 dark:border-gray-600 text-gray-800 dark:text-white"></div>
</div>
</div>
</div>
</div>
<!-- Gender Prediction Tab Content -->
<div class="tab-content hidden" id="gender">
<div class="card bg-white dark:bg-gray-800 p-6 rounded-lg shadow-md border border-gray-100 dark:border-gray-700 max-w-2xl mx-auto">
<h2 class="card-title text-xl font-semibold text-gray-800 dark:text-white mb-4 border-b border-gray-200 dark:border-gray-600 pb-2 flex items-center">
<i class="ri-user-search-line mr-2 text-primary dark:text-blue-400"></i>
Gender Prediction
</h2>
<div class="space-y-4">
<div>
<label for="names-input" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Enter names (comma-separated):</label>
<textarea id="names-input" placeholder="e.g., Sakshi, Sameer, Priya" rows="3" class="w-full px-3 py-2 border border-gray-300 rounded-button focus:outline-none focus:ring-2 focus:ring-primary/50 dark:bg-gray-700 dark:border-gray-600 dark:text-gray-200"></textarea>
</div>
<div>
<button id="predict-gender-button" class="w-full sm:w-auto bg-primary text-white px-5 py-2 rounded-button hover:bg-blue-700 transition-colors font-medium inline-flex items-center justify-center gap-2 disabled:opacity-50">
<i class="ri-user-shared-line"></i> Predict Gender
</button>
</div>
</div>
<!-- Loading Indicator -->
<div id="gender-loading" class="flex items-center justify-center my-4 text-primary dark:text-blue-400 hidden">
<svg class="animate-spin -ml-1 mr-3 h-5 w-5" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>
<span>Analyzing names...</span>
</div>
<div class="error-message bg-red-100 border border-red-300 text-red-800 dark:bg-red-900 dark:bg-opacity-50 dark:border-red-600 dark:text-red-200 px-4 py-2 rounded-md text-sm mt-4 hidden" id="genderErrorMessage"></div>
</div>
<!-- Gender Prediction Results -->
<div id="gender-results" class="results card bg-white dark:bg-gray-800 p-6 rounded-lg shadow-md border border-gray-100 dark:border-gray-700 max-w-2xl mx-auto mt-6 hidden">
<h3 class="result-header text-lg font-semibold text-gray-800 dark:text-white mb-4 border-b border-gray-200 dark:border-gray-600 pb-2">Prediction Results</h3>
<div class="space-y-4 text-sm">
<!-- JS will insert result items here -->
</div>
</div>
</div>
</div>
</main>
<!-- Footer -->
<footer class="bg-gray-800 text-gray-400 py-8 mt-12">
<div class="container mx-auto px-4 text-center">
<div class="mb-4">
<a href="#" class="text-xl font-bold text-white hover:text-blue-300">Vision & Discern</a>
</div>
<div class="credits text-sm mb-4">
<p>Powered by <strong>D SAKSHI</strong> (MCA Final Year BIT Durg, Chhattisgarh) | © SlimShadow Org. All Rights Reserved.</p>
</div>
<div class="flex justify-center space-x-4">
<a href="#" class="hover:text-white" title="GitHub (Placeholder)"><i class="ri-github-fill"></i></a>
<a href="#" class="hover:text-white" title="LinkedIn (Placeholder)"><i class="ri-linkedin-box-fill"></i></a>
<a href="#" class="hover:text-white" title="Twitter (Placeholder)"><i class="ri-twitter-fill"></i></a>
</div>
</div>
</footer>
</div> <!-- End #app-container -->
<script>
// --- API Base URLs ---
const OCR_API_BASE_URL = 'https://sameernotes-ocr.hf.space';
const TRANSLATION_API_BASE_URL = 'https://sameernotes-translation-prediction-space.hf.space';
const GENDER_API_BASE_URL = "https://sidvilas-gender-prediction-space.hf.space";
// Note: supportedLanguages list is not used for validation in this code, but useful for reference.
const supportedLanguages = ["Afrikaans", "Arabic", "Armenian", "Azerbaijani", "Belarusian", "Bosnian", "Bulgarian", "Catalan", "Chinese", "Croatian", "Czech", "Danish", "Dutch", "English", "Estonian", "Finnish", "French", "Galician", "German", "Greek", "Hebrew", "Hindi", "Hungarian", "Icelandic", "Indonesian", "Italian", "Japanese", "Kannada", "Kazakh", "Korean", "Latvian", "Lithuanian", "Macedonian", "Malay", "Marathi", "Maori", "Nepali", "Norwegian", "Persian", "Polish", "Portuguese", "Romanian", "Russian", "Serbian", "Slovak", "Slovenian", "Spanish", "Swahili", "Swedish", "Tagalog", "Tamil", "Thai", "Turkish", "Ukrainian", "Urdu", "Vietnamese", "Welsh"];
let accessToken = null;
let selectedImageSource = null;
// --- DOM Element References ---
const htmlElement = document.documentElement;
// Login/Signup
const loginContainer = document.getElementById('login-container');
const appContainer = document.getElementById('app-container');
const loginCard = document.getElementById('login-card');
const signupCard = document.getElementById('signup-card');
const loginLink = document.getElementById('login-link');
const signupLink = document.getElementById('signup-link');
const usernameInput = document.getElementById('username');
const passwordInput = document.getElementById('password');
const loginButton = document.getElementById('loginButton');
const loginErrorMessage = document.getElementById('loginErrorMessage');
const signupUsernameInput = document.getElementById('signupUsername');
const signupEmailInput = document.getElementById('signupEmail');
const signupPasswordInput = document.getElementById('signupPassword');
const signupButton = document.getElementById('signupButton');
const signupErrorMessage = document.getElementById('signupErrorMessage');
const logoutButton = document.getElementById('logoutButton');
// Theme/Lang
const themeToggle = document.getElementById('themeToggle');
const languageToggle = document.getElementById('languageToggle');
// OCR
const fileInput = document.getElementById('fileInput');
const uploadArea = document.getElementById('uploadArea');
const imagePreview = document.getElementById('imagePreview');
const processButton = document.getElementById('processButton');
const clearButton = document.getElementById('clearButton');
const sampleImages = document.querySelectorAll('.sample-image');
const loadingSpinner = document.getElementById('loadingSpinner');
const resultsSection = document.getElementById('resultsSection');
const initialMessage = document.getElementById('initialMessage');
const ocrErrorMessage = document.getElementById('ocrErrorMessage');
const ocrOutput = document.getElementById('ocrOutput');
const wordCount = document.getElementById('wordCount');
const wordDetectionImg = document.getElementById('wordDetectionImg');
const predictionLabel = document.getElementById('predictionLabel');
const predictionImg = document.getElementById('predictionImg');
// Translation
const textToTranslateInput = document.getElementById('text-to-translate');
const sourceLanguageInput = document.getElementById('source-language');
const targetLanguageInput = document.getElementById('target-language');
const translateButton = document.getElementById('translate-button');
const translationLoading = document.getElementById('translation-loading');
const translationResultDiv = document.getElementById('translation-result');
const detectedSourceLanguage = document.getElementById('detected-source-language');
const translationTargetLanguage = document.getElementById('translation-target-language');
const translatedText = document.getElementById('translated-text');
const translationErrorMessage = document.getElementById('translationErrorMessage');
// Gender
const namesInput = document.getElementById('names-input');
const predictGenderButton = document.getElementById('predict-gender-button');
const genderLoadingDiv = document.getElementById('gender-loading');
const genderResultsDiv = document.getElementById('gender-results');
const genderErrorMessageDiv = document.getElementById('genderErrorMessage');
// Tabs
const tabs = document.querySelectorAll('.tab-button');
const tabContents = document.querySelectorAll('.tab-content');
// --- THEME TOGGLE LOGIC ---
function applyTheme(isDark) {
if (isDark) {
htmlElement.classList.add('dark');
if (themeToggle) themeToggle.checked = true;
} else {
htmlElement.classList.remove('dark');
if (themeToggle) themeToggle.checked = false;
}
}
const prefersDark = localStorage.getItem('theme') === 'dark' ||
(localStorage.getItem('theme') === null && window.matchMedia('(prefers-color-scheme: dark)').matches);
applyTheme(prefersDark);
if (themeToggle) {
themeToggle.addEventListener('change', (event) => {
const isDark = event.target.checked;
applyTheme(isDark);
localStorage.setItem('theme', isDark ? 'dark' : 'light');
});
} else {
console.warn("Theme toggle button not found.");
}
// --- LANGUAGE TOGGLE LOGIC ---
// *** CLARIFICATION: This only changes the <html> lang attribute and saves preference. ***
// *** It DOES NOT change the visible UI text between English and Hindi. ***
// *** Implementing full UI translation requires a different approach (e.g., dictionaries). ***
function applyLanguage(lang) {
console.log("Applying language attribute:", lang);
htmlElement.setAttribute('lang', lang); // Set overall page lang attribute
// Update toggle state
if (languageToggle) languageToggle.checked = (lang === 'hi');
// Save preference
localStorage.setItem('language', lang);
// *** NOTE: No UI text change happens here. ***
if (lang === 'hi') {
console.log("Language attribute set to 'hi'. UI text remains unchanged by this function.");
} else {
console.log("Language attribute set to 'en'. UI text remains unchanged by this function.");
}
}
const savedLang = localStorage.getItem('language') || 'en';
applyLanguage(savedLang);
if (languageToggle) {
// languageToggle.disabled = false; // It wasn't disabled in the original HTML, so keep it enabled
languageToggle.addEventListener('change', (event) => {
const newLang = event.target.checked ? 'hi' : 'en';
applyLanguage(newLang);
});
} else {
console.warn("Language toggle button not found.");
}
// --- TAB MANAGEMENT ---
tabs.forEach(tab => {
tab.addEventListener('click', () => {
tabs.forEach(t => {
t.classList.remove('active', 'text-primary', 'dark:text-blue-400', 'border-primary', 'dark:border-blue-400', 'font-medium');
t.classList.add('text-gray-600', 'dark:text-gray-300', 'border-transparent');
});
tabContents.forEach(c => c.classList.add('hidden'));
tab.classList.add('active', 'text-primary', 'dark:text-blue-400', 'border-primary', 'dark:border-blue-400', 'font-medium');
tab.classList.remove('text-gray-600', 'dark:text-gray-300', 'border-transparent');
const activeTabContent = document.getElementById(tab.dataset.tab);
if (activeTabContent) {
activeTabContent.classList.remove('hidden');
}
});
});
// --- LOGIN/SIGNUP LOGIC ---
loginLink.addEventListener('click', (e) => { e.preventDefault(); hideElement(signupCard); showElement(loginCard); });
signupLink.addEventListener('click', (e) => { e.preventDefault(); hideElement(loginCard); showElement(signupCard); });
loginButton.addEventListener('click', async () => {
loginButton.disabled = true;
hideElement(loginErrorMessage);
const username = usernameInput.value;
const password = passwordInput.value;
if (!username || !password) { showLoginError("Please enter both username and password."); loginButton.disabled = false; return; }
const formData = new URLSearchParams();
formData.append('username', username);
formData.append('password', password);
try {
console.log("Attempting login to:", `${OCR_API_BASE_URL}/token`);
const response = await fetch(`${OCR_API_BASE_URL}/token`, {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: formData.toString()
});
console.log("Login response status:", response.status);
if (!response.ok) {
const errorData = await response.json().catch(() => ({ detail: `Login failed with status ${response.status}. Check credentials.` }));
showLoginError(errorData.detail || `Login failed with status ${response.status}. Check credentials.`);
return;
}
const data = await response.json();
accessToken = data.access_token;
console.log("Login successful. Token acquired.");
hideElement(loginContainer);
showElement(appContainer, 'flex flex-col');
} catch (error) {
showLoginError("Network error during login. Check console for details.");
console.error("Login fetch error:", error);
} finally {
loginButton.disabled = false;
}
});
signupButton.addEventListener('click', async () => {
signupButton.disabled = true;
hideElement(signupErrorMessage);
const username = signupUsernameInput.value;
const email = signupEmailInput.value;
const password = signupPasswordInput.value;
if (!username || !email || !password) { showSignupError("Please fill in all fields."); signupButton.disabled = false; return; }
if (!/\S+@\S+\.\S+/.test(email)) { showSignupError("Please enter a valid email address."); signupButton.disabled = false; return; }
try {
console.log("Attempting signup to:", `${OCR_API_BASE_URL}/signup`);
const response = await fetch(`${OCR_API_BASE_URL}/signup`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username, email, password })
});
console.log("Signup response status:", response.status);
if (!response.ok) {
let errorMsg = `Signup failed (Status: ${response.status}).`;
try { const errorData = await response.json(); errorMsg = `Signup failed: ${errorData.detail || 'Unknown error'}`; } catch (e) { }
showSignupError(errorMsg);
return;
}
alert('Signup successful! Please login.');
hideElement(signupCard);
showElement(loginCard);
signupUsernameInput.value = ''; signupEmailInput.value = ''; signupPasswordInput.value = '';
} catch (error) {
showSignupError("Network error during signup. Check console for details.");
console.error("Signup fetch error:", error);
} finally {
signupButton.disabled = false;
}
});
logoutButton.addEventListener('click', () => {
console.log("Logging out.");
accessToken = null;
selectedImageSource = null;
hideElement(appContainer);
showElement(loginContainer, 'flex');
usernameInput.value = ''; passwordInput.value = '';
hideElement(loginErrorMessage); hideElement(signupErrorMessage);
clearOCRResults();
clearTranslationResults();
clearGenderResults();
});
// --- OCR FUNCTIONALITY ---
uploadArea.addEventListener('click', () => fileInput.click());
uploadArea.addEventListener('dragover', (e) => { e.preventDefault(); uploadArea.classList.add('dragover', 'border-primary', 'dark:border-blue-400', 'bg-secondary'); });
uploadArea.addEventListener('dragleave', () => uploadArea.classList.remove('dragover', 'border-primary', 'dark:border-blue-400', 'bg-secondary'));
uploadArea.addEventListener('drop', (e) => {
e.preventDefault();
uploadArea.classList.remove('dragover', 'border-primary', 'dark:border-blue-400', 'bg-secondary');
if (e.dataTransfer.files.length) {
fileInput.files = e.dataTransfer.files;
handleFileSelect();
}
});
fileInput.addEventListener('change', handleFileSelect);
sampleImages.forEach(img => {
img.addEventListener('click', () => {
const imageUrl = img.dataset.src;
if (imageUrl) { loadSampleImage(imageUrl); }
else { showOCRError("Sample image URL is missing."); }
});
});
async function loadSampleImage(imageUrl) {
console.log("Loading sample image:", imageUrl);
clearOCRResults();
showLoading(loadingSpinner);
imagePreview.src = imageUrl;
showElement(imagePreview);
processButton.disabled = true;
showElement(clearButton, 'inline-flex');
hideOCRError();
try {
const response = await fetch(imageUrl);
if (!response.ok) throw new Error(`Failed to fetch sample image: ${response.statusText} (${response.status})`);
const blob = await response.blob();
const filename = decodeURIComponent(imageUrl.substring(imageUrl.lastIndexOf('/') + 1) || 'sample.png');
selectedImageSource = new File([blob], filename, { type: blob.type });
console.log("Sample image loaded as File object:", selectedImageSource);
processButton.disabled = false;
hideElement(initialMessage);
} catch (error) {
console.error('Error loading sample image:', error);
showOCRError(`Could not load sample image: ${error.message}`);
clearOCRResults(); // Ensure cleanup on error
} finally {
hideLoading(loadingSpinner);
}
}
function handleFileSelect() {
const file = fileInput.files[0];
if (file) {
console.log("Handling file select:", file.name, file.type);
if (!file.type.startsWith('image/')) {
showOCRError('Please select a valid image file (PNG, JPG, JPEG).');
fileInput.value = ''; return;
}
hideOCRError();
clearOCRResults(); // Clear previous before loading new
selectedImageSource = file;
const reader = new FileReader();
reader.onload = (e) => {
imagePreview.src = e.target.result;
showElement(imagePreview);
processButton.disabled = false;
showElement(clearButton, 'inline-flex');
hideElement(initialMessage);
console.log("File loaded into preview.");
};
reader.onerror = (e) => {
showOCRError('Error reading the selected file.');
console.error("FileReader error:", e);
};
reader.readAsDataURL(file);
}
}
processButton.addEventListener('click', processImage);
clearButton.addEventListener('click', clearOCRResults);
async function processImage() {
const imageSource = selectedImageSource;
if (!imageSource) { showOCRError('Please select or click a sample image first.'); return; }
if (!accessToken) { showOCRError('Not logged in. Please login again.'); logoutButton.click(); return; }
console.log("Processing image:", imageSource.name);
showLoading(loadingSpinner);
hideElement(resultsSection);
hideElement(initialMessage);
processButton.disabled = true;
clearButton.disabled = true; // Also disable clear during processing
hideOCRError();
try {
const formData = new FormData();
formData.append('file', imageSource, imageSource.name || 'image.png');
console.log("Sending OCR request to:", `${OCR_API_BASE_URL}/process/`);
const response = await fetch(`${OCR_API_BASE_URL}/process/`, {
method: 'POST',
headers: { 'Authorization': `Bearer ${accessToken}`, 'accept': 'application/json' },
body: formData
});
console.log("OCR response status:", response.status);
if (response.status === 401) {
showOCRError('Authentication failed. Please log out and log back in.');
logoutButton.click(); return;
}
if (!response.ok) {
let errorMsg = `Image processing failed (Status: ${response.status})`;
try { const errorData = await response.json(); errorMsg = `Image processing failed: ${errorData.detail || response.statusText}`; } catch (e) { }
throw new Error(errorMsg);
}
const data = await response.json();
console.log("OCR Response data:", data);
ocrOutput.textContent = data.sakshi_output || 'No text detected.';
// Populate translation input with OCR result
textToTranslateInput.value = data.sakshi_output || '';
wordCount.textContent = data.word_count ?? '0';
predictionLabel.textContent = data.prediction_label || 'N/A';
const fetchImage = async (url, imgElement) => {
if (!url) { imgElement.src = ''; hideElement(imgElement); return; }
// Check if URL is relative, if so prepend base URL
const absoluteUrl = url.startsWith('http') ? url : `${OCR_API_BASE_URL}${url}`;
console.log("Fetching result image from:", absoluteUrl);
try {
const imageResponse = await fetch(absoluteUrl, { headers: { 'Authorization': `Bearer ${accessToken}` } });
if (imageResponse.ok) {
const blob = await imageResponse.blob();
imgElement.src = URL.createObjectURL(blob);
showElement(imgElement);
const parentCard = imgElement.closest('.result-card');
if(parentCard) showElement(parentCard); // Ensure parent is visible
} else { console.error('Failed to fetch result image:', absoluteUrl, imageResponse.status); imgElement.src = ''; hideElement(imgElement); }
} catch (fetchError) { console.error('Error fetching result image:', absoluteUrl, fetchError); imgElement.src = ''; hideElement(imgElement); }
};
await Promise.all([
fetchImage(data.word_detection_url, wordDetectionImg),
fetchImage(data.prediction_image_url, predictionImg)
]);
showElement(resultsSection);
hideElement(initialMessage);
console.log("OCR processing complete and results shown.");
} catch (error) {
console.error('Error processing image:', error);
showOCRError(`Error: ${error.message}. Check console for details.`);
hideElement(resultsSection);
showElement(initialMessage);
initialMessage.textContent = 'An error occurred during processing. Please try again.';
} finally {
hideLoading(loadingSpinner);
// Re-enable buttons only if an image is still selected/previewed
if (selectedImageSource) {
processButton.disabled = false;
clearButton.disabled = false;
} else {
processButton.disabled = true; // Keep disabled if cleared
clearButton.disabled = true; // Should be hidden anyway
}
}
}
// --- TRANSLATION FUNCTIONALITY ---
translateButton.addEventListener('click', async () => {
const text = textToTranslateInput.value.trim();
const sourceLanguage = sourceLanguageInput.value.trim(); // User might enter 'hi' or leave empty
const targetLanguage = targetLanguageInput.value.trim(); // User enters 'Hindi' (as requested)
if (!accessToken) { showTranslationError('Not logged in. Please login again.'); logoutButton.click(); return; }
clearTranslationError();
hideElement(translationResultDiv);
if (!text) { showTranslationError('Please enter text to translate.'); return; }
if (!targetLanguage) { showTranslationError('Please enter the target language (e.g., Hindi).'); return; }
// No change here - allowing "Hindi" as per request
console.log(`Attempting translation: From '${sourceLanguage || 'auto'}' To '${targetLanguage}'`);
showLoading(translationLoading);
translateButton.disabled = true;
try {
// Note: Sending "Hindi" here. If the API strictly requires "hi",
// it might fail silently or throw an error response below.
// We are fixing the client-side JSON parsing error.
const requestBody = { text: text, target_language: targetLanguage };
if (sourceLanguage) {
requestBody.source_language = sourceLanguage;
}
console.log("Translation request body:", requestBody);
console.log("Sending translation request to:", `${TRANSLATION_API_BASE_URL}/translate`);
const response = await fetch(`${TRANSLATION_API_BASE_URL}/translate`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${accessToken}`,
'accept': 'application/json' // Client still indicates it accepts JSON
},
body: JSON.stringify(requestBody)
});
console.log("Translation response status:", response.status);
if (response.status === 401) {
showTranslationError('Authentication failed. Please log out and log back in.');
logoutButton.click();
throw new Error("Authentication failed"); // Stop further processing
}
// --- START FIX ---
// Check if response is OK before reading body
if (!response.ok) {
// Try reading error response as text first
let errorMsg = `Translation failed (Status: ${response.status})`;
try {
const errorText = await response.text();
// Attempt to parse as JSON ONLY if it looks like JSON
if (errorText && (errorText.trim().startsWith('{') || errorText.trim().startsWith('['))) {
const errorData = JSON.parse(errorText);
errorMsg = `Translation failed: ${errorData.detail || errorText}`;
} else {
// Otherwise, use the plain text error or statusText
errorMsg = `Translation failed: ${errorText || response.statusText}`;
}
} catch (e) {
// Fallback if reading text/parsing fails
errorMsg = `Translation failed (Status: ${response.status} ${response.statusText})`;
}
throw new Error(errorMsg);
}
// If response.ok is true, read the body as TEXT
// This is the core fix for the "Unexpected token" error
const translatedString = await response.text();
console.log("Raw translation response (as text):", translatedString);
// Display results using the raw string and user input values
// Since the API didn't return JSON, we use the inputs for source/target display
detectedSourceLanguage.textContent = sourceLanguage || "Auto-detected";
translationTargetLanguage.textContent = targetLanguage; // Display exactly what the user typed
translatedText.textContent = translatedString.trim() || "No translation returned."; // Use the text directly
// --- END FIX ---
showElement(translationResultDiv);
console.log("Translation successful (response treated as plain text).");
} catch (error) {
// Avoid double error display for auth failure
if (error.message !== "Authentication failed") {
showTranslationError(`Error: ${error.message}. Check console for details.`);
console.error('Error during translation fetch:', error);
}
} finally {
hideLoading(translationLoading);
translateButton.disabled = false;
}
});
// --- GENDER PREDICTION FUNCTIONALITY ---
predictGenderButton.addEventListener('click', async () => {
const namesString = namesInput.value.trim();
const names = namesString.split(',')
.map(name => name.trim())
.filter(name => name !== ""); // Filter out empty strings after trim/split
if (!accessToken) { showGenderError('Not logged in. Please login again.'); logoutButton.click(); return; }
if (names.length === 0) { showGenderError("Please enter at least one name."); return; }
clearGenderError();
hideElement(genderResultsDiv); // Hide previous results
showLoading(genderLoadingDiv);
predictGenderButton.disabled = true;
console.log("Predicting gender for names:", names);
try {
console.log("Sending gender prediction request to:", `${GENDER_API_BASE_URL}/predict`);
const response = await fetch(`${GENDER_API_BASE_URL}/predict`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${accessToken}`,
'accept': 'application/json'
},
// Ensure the body matches what the API expects: { "names": ["name1", "name2"] }
body: JSON.stringify({ names: names, threshold: 0.5 }) // Assuming API takes a list under 'names' key
});
console.log("Gender prediction response status:", response.status);
if (response.status === 401) {
showGenderError('Authentication failed. Please log out and log back in.');
logoutButton.click(); return;
}
if (response.status === 422) {
let errorMsg = `Prediction failed (Invalid Input - Status: ${response.status}).`;
try { const errorData = await response.json(); errorMsg = `Prediction failed: ${errorData.detail || 'Invalid input'}`; } catch (e) { }
throw new Error(errorMsg);
}
if (!response.ok) {
let errorMsg = `Gender prediction failed (Status: ${response.status})`;
try { const errorData = await response.json(); errorMsg = `Prediction failed: ${errorData.detail || response.statusText}`; } catch (e) { }
throw new Error(errorMsg);
}
const data = await response.json();
console.log("Gender prediction response data:", data);
// *** API RESPONSE CHECK ***: Verify the structure of 'data'.
// The previous code assumed data.predictions. Let's check if 'data' itself is the array
// or if it's nested like { "predictions": [...] } or { "results": [...] }
let predictionsArray = null;
if (Array.isArray(data)) {
predictionsArray = data; // API returns the array directly
} else if (data && Array.isArray(data.predictions)) {
predictionsArray = data.predictions; // API returns { "predictions": [...] }
} else if (data && Array.isArray(data.results)) {
predictionsArray = data.results; // API might return { "results": [...] }
}
// Add more checks if the API structure is different
if (predictionsArray) {
displayGenderResults(predictionsArray);
showElement(genderResultsDiv);
console.log("Gender prediction successful.");
} else {
console.error("Unexpected response format from gender API:", data);
showGenderError("Received an unexpected response format from the server.");
}
} catch (error) {
showGenderError(`Error: ${error.message}. Check console for details.`);
console.error("Gender prediction fetch error:", error);
} finally {
hideLoading(genderLoadingDiv);
predictGenderButton.disabled = false;
}
});
// --- DISPLAY RESULTS (GENDER) ---
function displayGenderResults(predictions) {
const resultsContainer = genderResultsDiv.querySelector('.space-y-4');
resultsContainer.innerHTML = ''; // Clear previous
if (!predictions || predictions.length === 0) {
resultsContainer.innerHTML = '<p class="text-gray-500 dark:text-gray-400">No predictions were returned.</p>';
return; // Exit early
}
predictions.forEach(prediction => {
// Adapt based on actual API response field names
const name = prediction.name || prediction.Name || 'N/A';
const gender = prediction.predicted_gender || prediction.Gender || 'Unknown';
// Check for probability fields - adjust names as needed (e.g., Male_Probability, Confidence)
const maleProbRaw = prediction.male_probability ?? prediction.Male_Probability ?? null;
const confidenceRaw = prediction.confidence ?? prediction.Confidence ?? null;
const maleProb = typeof maleProbRaw === 'number' ? (maleProbRaw * 100).toFixed(1) + '%' : 'N/A';
const confidence = typeof confidenceRaw === 'number' ? (confidenceRaw * 100).toFixed(1) + '%' : 'N/A';
const resultItemHTML = `
<div class="result-item border-b border-gray-200 dark:border-gray-600 pb-3 last:border-b-0 last:pb-0">
<div class="flex justify-between mb-1">
<span class="result-label font-medium text-gray-600 dark:text-gray-400">Name:</span>
<span class="text-gray-800 dark:text-white font-semibold">${name}</span>
</div>
<div class="flex justify-between mb-1">
<span class="result-label font-medium text-gray-600 dark:text-gray-400">Predicted Gender:</span>
<span class="text-gray-800 dark:text-white">${gender}</span>
</div>
<div class="flex justify-between mb-1">
<span class="result-label font-medium text-gray-600 dark:text-gray-400">Confidence:</span>
<span class="text-gray-800 dark:text-white">${confidence}</span>
</div>
<div class="flex justify-between">
<span class="result-label font-medium text-gray-600 dark:text-gray-400">Male Probability:</span>
<span class="text-gray-800 dark:text-white">${maleProb}</span>
</div>
</div>
`;
resultsContainer.innerHTML += resultItemHTML;
});
showElement(genderResultsDiv); // Ensure container is visible after adding content
}
// --- UTILITY FUNCTIONS ---
function showElement(element, displayType = 'block') {
if (element) {
element.classList.remove('hidden');
if (displayType !== 'block' && !element.classList.contains(displayType.split(' ')[0])) {
element.classList.remove('block', 'inline-block', 'flex', 'grid', 'inline-flex');
element.classList.add(...displayType.split(' '));
} else if (displayType === 'block' && !element.style.display) {
// Default to block if no specific type given and it's not already styled otherwise
// This might not be necessary if Tailwind 'block' is the default without 'hidden'
}
}
}
function hideElement(element) { if (element) element.classList.add('hidden'); }
function showLoginError(message) { loginErrorMessage.textContent = message; showElement(loginErrorMessage); }
function showSignupError(message) { signupErrorMessage.textContent = message; showElement(signupErrorMessage); }
function showOCRError(message) { ocrErrorMessage.textContent = message; showElement(ocrErrorMessage); }
function hideOCRError() { hideElement(ocrErrorMessage); }
function showTranslationError(message) { translationErrorMessage.textContent = message; showElement(translationErrorMessage); }
function clearTranslationError() { translationErrorMessage.textContent = ''; hideElement(translationErrorMessage); }
function showGenderError(message) { genderErrorMessageDiv.textContent = message; showElement(genderErrorMessageDiv); }
function clearGenderError() { genderErrorMessageDiv.textContent = ''; hideElement(genderErrorMessageDiv); }
function showLoading(loadingIndicator) { showElement(loadingIndicator, 'flex flex-col items-center justify-center'); }
function hideLoading(loadingIndicator) { hideElement(loadingIndicator); }
function clearOCRResults() {
console.log("Clearing OCR results.");
fileInput.value = '';
selectedImageSource = null;
hideElement(imagePreview); imagePreview.src = '';
hideElement(wordDetectionImg); wordDetectionImg.src = '';
hideElement(predictionImg); predictionImg.src = '';
ocrOutput.textContent = '';
textToTranslateInput.value = ''; // Also clear translation input linked to OCR
wordCount.textContent = '0';
predictionLabel.textContent = 'N/A';
hideElement(resultsSection);
showElement(initialMessage);
initialMessage.textContent = "Upload or select a sample image and click 'Process' to see the results.";
processButton.disabled = true;
hideElement(clearButton);
hideOCRError();
hideLoading(loadingSpinner);
}
function clearTranslationResults() {
console.log("Clearing translation results.");
// Keep textToTranslateInput if it might be manually entered, or clear it:
// textToTranslateInput.value = '';
sourceLanguageInput.value = ''; targetLanguageInput.value = '';
detectedSourceLanguage.textContent = ''; translationTargetLanguage.textContent = ''; translatedText.textContent = '';
hideElement(translationResultDiv);
clearTranslationError();
hideLoading(translationLoading);
}
function clearGenderResults() {
console.log("Clearing gender results.");
namesInput.value = '';
const resultsContainer = genderResultsDiv.querySelector('.space-y-4');
if (resultsContainer) resultsContainer.innerHTML = '';
hideElement(genderResultsDiv);
clearGenderError();
hideLoading(genderLoadingDiv);
}
// --- Initial state ---
showElement(loginContainer, 'flex');
hideElement(appContainer);
console.log("App initialized, showing login screen.");
</script>
</body>
</html>