Spaces:
Running
Running
| <!-- 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> |