EthicsGuard-Pro / index.html
Hma47's picture
Upload 4 files
945d5b8 verified
<!DOCTYPE html>
<html lang="en" dir="ltr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>EthicsGuard Pro - AI Education Ethics Assessment Platform</title>
<style>
:root {
--primary: #1e40af;
--secondary: #7c3aed;
--accent: #3b82f6;
--background: #f8fafc;
--surface: #ffffff;
--text: #1e293b;
--text-secondary: #64748b;
--border: #e2e8f0;
--success: #059669;
--warning: #ea580c;
--error: #dc2626;
--shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
--shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
}
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background: linear-gradient(135deg, #f1f5f9 0%, #e2e8f0 100%);
color: var(--text);
line-height: 1.6;
min-height: 100vh;
padding: 15px;
transition: all 0.3s ease;
}
/* RTL Support */
[dir="rtl"] {
text-align: right;
}
[dir="rtl"] .container {
direction: rtl;
}
[dir="rtl"] .api-key-section {
margin: 15px 15px 15px auto;
}
[dir="rtl"] .language-switcher {
right: auto;
left: 15px;
}
/* Language Selection Landing */
.language-landing {
display: block;
max-width: 600px;
margin: 50px auto;
background: var(--surface);
border-radius: 16px;
box-shadow: var(--shadow-lg);
padding: 40px;
text-align: center;
}
.language-landing h1 {
font-size: 2.5rem;
font-weight: 800;
background: linear-gradient(135deg, var(--primary) 0%, var(--secondary) 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
margin-bottom: 15px;
}
.language-landing p {
font-size: 1.1rem;
color: var(--text-secondary);
margin-bottom: 30px;
}
.language-selector {
margin-bottom: 25px;
}
.language-selector label {
display: block;
margin-bottom: 10px;
font-weight: 600;
color: var(--text);
font-size: 1.1rem;
}
.language-selector select {
width: 100%;
padding: 15px 20px;
border: 2px solid var(--border);
border-radius: 12px;
font-size: 1.1rem;
font-family: inherit;
background: var(--surface);
transition: all 0.2s ease;
margin-bottom: 20px;
}
.language-selector select:focus {
outline: none;
border-color: var(--primary);
box-shadow: 0 0 0 3px rgba(30, 64, 175, 0.1);
}
.api-key-landing {
margin-bottom: 25px;
}
.api-key-landing label {
display: block;
margin-bottom: 10px;
font-weight: 600;
color: var(--text);
font-size: 1.1rem;
}
.api-key-landing input {
width: 100%;
padding: 15px 20px;
border: 2px solid var(--border);
border-radius: 12px;
font-size: 1.1rem;
background: var(--surface);
transition: all 0.2s ease;
font-family: 'Courier New', monospace;
}
.api-key-landing input:focus {
outline: none;
border-color: var(--primary);
box-shadow: 0 0 0 3px rgba(30, 64, 175, 0.1);
}
.start-btn {
background: linear-gradient(135deg, var(--primary) 0%, var(--secondary) 100%);
color: white;
padding: 18px 40px;
border: none;
border-radius: 12px;
font-size: 1.2rem;
font-weight: 700;
cursor: pointer;
transition: all 0.3s ease;
width: 100%;
}
.start-btn:hover {
transform: translateY(-2px);
box-shadow: 0 12px 24px -8px rgba(30, 64, 175, 0.4);
}
.start-btn:disabled {
opacity: 0.7;
cursor: not-allowed;
transform: none;
}
/* Cache Management Section */
.cache-management {
background: rgba(30, 64, 175, 0.05);
border: 1px solid rgba(30, 64, 175, 0.1);
border-radius: 12px;
padding: 20px;
margin-bottom: 25px;
text-align: center;
}
.cache-management h3 {
color: var(--primary);
margin-bottom: 15px;
font-size: 1.1rem;
font-weight: 600;
}
.cache-status-display {
background: rgba(5, 150, 105, 0.1);
border: 1px solid rgba(5, 150, 105, 0.2);
color: var(--success);
padding: 12px 16px;
border-radius: 8px;
margin-bottom: 15px;
font-size: 0.9rem;
font-weight: 500;
}
.cache-status-display.no-cache {
background: rgba(100, 116, 139, 0.1);
border-color: rgba(100, 116, 139, 0.2);
color: var(--text-secondary);
}
.clear-cache-btn {
background: linear-gradient(135deg, var(--warning) 0%, #f59e0b 100%);
color: white;
padding: 12px 24px;
border: none;
border-radius: 8px;
font-size: 0.9rem;
font-weight: 600;
cursor: pointer;
transition: all 0.3s ease;
}
.clear-cache-btn:hover {
transform: translateY(-1px);
box-shadow: 0 6px 12px -4px rgba(234, 88, 12, 0.4);
}
.clear-cache-btn:disabled {
opacity: 0.5;
cursor: not-allowed;
transform: none;
}
/* Cache Status Indicator */
.cache-status {
background: rgba(5, 150, 105, 0.1);
border: 1px solid rgba(5, 150, 105, 0.2);
color: var(--success);
padding: 8px 12px;
border-radius: 6px;
margin-bottom: 15px;
font-size: 0.85rem;
text-align: center;
font-weight: 500;
display: none;
}
.cache-status.cached {
display: block;
}
.cache-status.translating {
background: rgba(30, 64, 175, 0.1);
border-color: rgba(30, 64, 175, 0.2);
color: var(--primary);
}
/* Translation Loading Overlay */
.translation-overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(30, 64, 175, 0.9);
display: none;
justify-content: center;
align-items: center;
z-index: 10000;
color: white;
text-align: center;
}
.translation-content {
background: rgba(255, 255, 255, 0.1);
padding: 40px;
border-radius: 16px;
backdrop-filter: blur(10px);
}
.translation-spinner {
width: 50px;
height: 50px;
border: 4px solid rgba(255, 255, 255, 0.3);
border-radius: 50%;
border-top-color: white;
animation: spin 1s ease-in-out infinite;
margin: 0 auto 20px;
}
/* Main Application (Hidden Initially) */
.main-app {
display: none;
}
.container {
max-width: 1200px;
margin: 0 auto;
background: var(--surface);
border-radius: 16px;
box-shadow: var(--shadow-lg);
overflow: hidden;
position: relative;
}
/* API Key Section - Static Position Top Left */
.api-key-section {
background: rgba(30, 64, 175, 0.05);
border: 1px solid rgba(30, 64, 175, 0.1);
border-radius: 8px;
padding: 12px;
margin: 15px;
max-width: 280px;
}
.api-key-section label {
font-size: 12px;
font-weight: 600;
color: var(--primary);
margin-bottom: 6px;
display: block;
}
.api-key-section input {
width: 100%;
padding: 8px 12px;
border: 1px solid var(--border);
border-radius: 6px;
font-size: 14px;
background: var(--surface);
transition: all 0.2s ease;
font-family: 'Courier New', monospace;
}
.api-key-section input:focus {
outline: none;
border-color: var(--primary);
box-shadow: 0 0 0 3px rgba(30, 64, 175, 0.1);
}
/* Language Switcher in Main App */
.language-switcher {
position: absolute;
top: 15px;
right: 15px;
display: flex;
gap: 10px;
align-items: center;
}
.language-switch-btn {
background: rgba(30, 64, 175, 0.05);
border: 1px solid rgba(30, 64, 175, 0.1);
border-radius: 8px;
padding: 8px 12px;
font-size: 12px;
cursor: pointer;
transition: all 0.2s ease;
color: var(--primary);
font-weight: 500;
}
.language-switch-btn:hover {
background: rgba(30, 64, 175, 0.1);
}
.mini-clear-cache {
background: rgba(234, 88, 12, 0.1);
border: 1px solid rgba(234, 88, 12, 0.2);
color: var(--warning);
padding: 6px 10px;
border-radius: 6px;
font-size: 11px;
cursor: pointer;
transition: all 0.2s ease;
font-weight: 500;
}
.mini-clear-cache:hover {
background: rgba(234, 88, 12, 0.2);
}
/* Main Content */
.main-content {
padding: 20px 30px 30px;
}
/* Header */
.header {
text-align: center;
margin-bottom: 25px;
padding-bottom: 20px;
border-bottom: 2px solid var(--border);
}
.header h1 {
font-size: 2.5rem;
font-weight: 800;
background: linear-gradient(135deg, var(--primary) 0%, var(--secondary) 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
margin-bottom: 8px;
}
.header .subtitle {
font-size: 1.1rem;
color: var(--text-secondary);
font-weight: 500;
}
.header .description {
font-size: 1rem;
color: var(--text-secondary);
margin-top: 12px;
max-width: 700px;
margin-left: auto;
margin-right: auto;
}
/* Progress Bar */
.progress-container {
background: rgba(30, 64, 175, 0.05);
border: 1px solid rgba(30, 64, 175, 0.1);
border-radius: 12px;
padding: 20px;
margin-bottom: 25px;
}
.progress-label {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 15px;
font-size: 0.9rem;
font-weight: 600;
color: var(--primary);
}
.progress-bar-track {
width: 100%;
height: 8px;
background: var(--border);
border-radius: 4px;
overflow: hidden;
}
.progress-bar-fill {
height: 100%;
background: linear-gradient(90deg, var(--primary), var(--secondary));
border-radius: 4px;
transition: width 0.3s ease;
width: 0%;
}
/* Section Containers */
.section {
background: var(--surface);
border-radius: 16px;
padding: 30px;
box-shadow: var(--shadow);
margin-bottom: 25px;
transition: all 0.3s ease;
}
.section:hover {
box-shadow: var(--shadow-lg);
}
.section-title {
font-size: 1.5rem;
font-weight: 700;
color: var(--primary);
margin-bottom: 20px;
text-align: center;
}
/* Generate Button */
.generate-button {
background: linear-gradient(135deg, var(--primary) 0%, var(--secondary) 100%);
color: white;
padding: 18px 40px;
border: none;
border-radius: 12px;
font-size: 1.2rem;
font-weight: 700;
cursor: pointer;
transition: all 0.3s ease;
display: block;
margin: 30px auto;
min-width: 320px;
position: relative;
overflow: hidden;
}
.generate-button:hover {
transform: translateY(-2px);
box-shadow: 0 12px 24px -8px rgba(30, 64, 175, 0.4);
}
.generate-button:disabled {
opacity: 0.7;
cursor: not-allowed;
transform: none;
}
/* Loading Spinner */
.loading-spinner {
display: none;
width: 20px;
height: 20px;
border: 2px solid rgba(255, 255, 255, 0.3);
border-radius: 50%;
border-top-color: white;
animation: spin 1s ease-in-out infinite;
margin-right: 8px;
}
[dir="rtl"] .loading-spinner {
margin-right: 0;
margin-left: 8px;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
/* Question Cards */
.questions-container {
display: flex;
flex-direction: column;
gap: 20px;
}
.question-card {
background: var(--surface);
border: 1px solid var(--border);
border-radius: 12px;
padding: 25px;
box-shadow: var(--shadow);
transition: all 0.3s ease;
}
.question-card:hover {
box-shadow: var(--shadow-lg);
border-color: var(--accent);
}
.question-number {
display: inline-flex;
align-items: center;
justify-content: center;
width: 32px;
height: 32px;
background: linear-gradient(135deg, var(--primary), var(--secondary));
color: white;
border-radius: 50%;
font-weight: 600;
font-size: 0.875rem;
margin-bottom: 15px;
}
.question-text {
font-size: 1.125rem;
font-weight: 500;
color: var(--text);
margin-bottom: 20px;
line-height: 1.5;
}
.options-container {
display: flex;
flex-direction: column;
gap: 12px;
}
.option-item {
position: relative;
}
.option-input {
position: absolute;
opacity: 0;
cursor: pointer;
}
.option-label {
display: flex;
align-items: center;
padding: 15px;
border: 2px solid var(--border);
border-radius: 8px;
cursor: pointer;
transition: all 0.2s ease;
background: var(--background);
font-weight: 500;
}
.option-label:hover {
border-color: var(--accent);
background: var(--surface);
box-shadow: var(--shadow);
}
.option-input:checked + .option-label {
border-color: var(--primary);
background: linear-gradient(135deg, rgba(30, 64, 175, 0.1), rgba(124, 58, 237, 0.1));
box-shadow: var(--shadow);
}
.option-indicator {
width: 20px;
height: 20px;
border: 2px solid var(--border);
border-radius: 50%;
margin-right: 15px;
transition: all 0.2s ease;
position: relative;
}
[dir="rtl"] .option-indicator {
margin-right: 0;
margin-left: 15px;
}
.option-input:checked + .option-label .option-indicator {
border-color: var(--primary);
background: var(--primary);
}
.option-input:checked + .option-label .option-indicator::after {
content: '';
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 8px;
height: 8px;
background: white;
border-radius: 50%;
}
.option-text {
flex: 1;
color: var(--text);
}
/* Action Buttons */
.action-buttons {
display: flex;
gap: 15px;
justify-content: center;
margin-top: 30px;
}
.btn {
display: inline-flex;
align-items: center;
gap: 8px;
padding: 15px 30px;
font-size: 1rem;
font-weight: 600;
border: none;
border-radius: 12px;
cursor: pointer;
transition: all 0.3s ease;
text-decoration: none;
}
.btn-primary {
color: white;
background: linear-gradient(135deg, var(--primary), var(--secondary));
box-shadow: var(--shadow);
}
.btn-primary:hover:not(:disabled) {
transform: translateY(-1px);
box-shadow: var(--shadow-lg);
}
.btn-secondary {
color: var(--primary);
background: var(--surface);
border: 2px solid var(--primary);
}
.btn-secondary:hover {
background: var(--primary);
color: white;
}
/* Results Section */
.results-container {
text-align: center;
}
.score-display {
background: linear-gradient(135deg, var(--success), #10b981);
color: white;
padding: 40px;
border-radius: 16px;
margin-bottom: 30px;
box-shadow: var(--shadow-lg);
}
.score-number {
font-size: 3rem;
font-weight: 700;
margin-bottom: 8px;
}
.score-label {
font-size: 1.25rem;
opacity: 0.9;
}
.feedback-container {
text-align: left;
background: var(--background);
border-radius: 12px;
padding: 25px;
margin-bottom: 25px;
}
.feedback-title {
font-size: 1.25rem;
font-weight: 600;
color: var(--text);
margin-bottom: 20px;
}
.feedback-item {
padding: 20px;
border-left: 4px solid var(--border);
margin-bottom: 20px;
background: var(--surface);
border-radius: 0 8px 8px 0;
}
[dir="rtl"] .feedback-item {
border-left: none;
border-right: 4px solid var(--border);
border-radius: 8px 0 0 8px;
}
.feedback-item.correct {
border-left-color: var(--success);
}
.feedback-item.incorrect {
border-left-color: var(--error);
}
[dir="rtl"] .feedback-item.correct {
border-right-color: var(--success);
}
[dir="rtl"] .feedback-item.incorrect {
border-right-color: var(--error);
}
.feedback-question {
font-weight: 600;
margin-bottom: 8px;
}
.feedback-answers {
font-size: 0.875rem;
color: var(--text-secondary);
margin-bottom: 8px;
}
.feedback-explanation {
color: var(--text);
line-height: 1.5;
}
/* Error Messages */
.error-message {
background: rgba(220, 38, 38, 0.1);
border: 1px solid rgba(220, 38, 38, 0.2);
color: var(--error);
padding: 12px 16px;
border-radius: 8px;
margin: 10px 0;
font-size: 0.9rem;
display: none;
}
/* Status Messages */
.status-message {
background: rgba(30, 64, 175, 0.1);
border: 1px solid rgba(30, 64, 175, 0.2);
color: var(--primary);
padding: 12px 16px;
border-radius: 8px;
margin: 10px 0;
font-size: 0.9rem;
text-align: center;
font-weight: 500;
display: none;
}
/* Hidden sections */
.hidden {
display: none !important;
}
/* Footer */
.footer {
text-align: center;
padding: 20px;
background: rgba(30, 64, 175, 0.05);
border-top: 1px solid var(--border);
color: var(--text-secondary);
font-size: 0.9rem;
}
/* Responsive Design */
@media (max-width: 768px) {
body {
padding: 10px;
}
.language-landing {
margin: 20px auto;
padding: 30px 20px;
}
.api-key-section {
margin: 10px;
max-width: none;
}
.language-switcher {
position: relative;
top: auto;
right: auto;
margin-bottom: 15px;
justify-content: center;
}
.main-content {
padding: 15px 20px 20px;
}
.header h1 {
font-size: 2rem;
}
.section {
padding: 20px 15px;
}
.action-buttons {
flex-direction: column;
align-items: center;
}
.btn {
width: 100%;
justify-content: center;
}
.generate-button {
width: 100%;
margin: 25px 0;
padding: 16px 32px;
font-size: 1.1rem;
}
.score-number {
font-size: 2.5rem;
}
}
@media (max-width: 480px) {
.language-landing h1 {
font-size: 2rem;
}
.header h1 {
font-size: 1.8rem;
}
}
/* Dark mode support */
@media (prefers-color-scheme: dark) {
:root {
--background: #0f172a;
--surface: #1e293b;
--text: #f1f5f9;
--text-secondary: #94a3b8;
--border: #334155;
}
body {
background: linear-gradient(135deg, #0f172a 0%, #1e293b 100%);
}
}
</style>
</head>
<body>
<!-- Translation Loading Overlay -->
<div class="translation-overlay" id="translationOverlay">
<div class="translation-content">
<div class="translation-spinner"></div>
<h2 id="translationTitle">Translating Interface...</h2>
<p id="translationMessage">Please wait while we translate the interface to your selected language.</p>
</div>
</div>
<!-- Language Selection Landing Page -->
<div class="language-landing" id="languageLanding">
<h1 data-translate="app_title">EthicsGuard Pro</h1>
<p data-translate="welcome_message">Welcome! Please select your preferred language and enter your API key to get started with AI ethics assessment.</p>
<!-- Cache Status Indicator -->
<div class="cache-status" id="cacheStatus">
💾 Translations cached - instant loading!
</div>
<!-- Cache Management Section -->
<div class="cache-management" id="cacheManagement">
<h3 data-translate="cache_management_title">🗂️ Translation Cache Management</h3>
<div class="cache-status-display" id="cacheStatusDisplay">
<span data-translate="cache_status_checking">Checking cache status...</span>
</div>
<button class="clear-cache-btn" id="clearCacheBtn" data-translate="clear_cache_button">
🗑️ Clear All Cached Translations
</button>
</div>
<div class="language-selector">
<label for="languageSelect" data-translate="select_language">🌐 Select Language</label>
<select id="languageSelect">
<option value="en">🇺🇸 English</option>
<option value="es">🇪🇸 Español (Spanish)</option>
<option value="fr">🇫🇷 Français (French)</option>
<option value="de">🇩🇪 Deutsch (German)</option>
<option value="zh">🇨🇳 中文 (Chinese)</option>
<option value="ja">🇯🇵 日本語 (Japanese)</option>
<option value="ko">🇰🇷 한국어 (Korean)</option>
<option value="pt">🇵🇹 Português (Portuguese)</option>
<option value="it">🇮🇹 Italiano (Italian)</option>
<option value="ar">🇸🇦 العربية (Arabic)</option>
<option value="ru">🇷🇺 Русский (Russian)</option>
<option value="hi">🇮🇳 हिन्दी (Hindi)</option>
<option value="bn">🇧🇩 বাংলা (Bengali)</option>
<option value="ur">🇵🇰 اردو (Urdu)</option>
<option value="tr">🇹🇷 Türkçe (Turkish)</option>
<option value="pl">🇵🇱 Polski (Polish)</option>
<option value="nl">🇳🇱 Nederlands (Dutch)</option>
<option value="sv">🇸🇪 Svenska (Swedish)</option>
<option value="da">🇩🇰 Dansk (Danish)</option>
<option value="no">🇳🇴 Norsk (Norwegian)</option>
<option value="fi">🇫🇮 Suomi (Finnish)</option>
<option value="is">🇮🇸 Íslenska (Icelandic)</option>
<option value="cs">🇨🇿 Čeština (Czech)</option>
<option value="sk">🇸🇰 Slovenčina (Slovak)</option>
<option value="hu">🇭🇺 Magyar (Hungarian)</option>
<option value="ro">🇷🇴 Română (Romanian)</option>
<option value="bg">🇧🇬 Български (Bulgarian)</option>
<option value="hr">🇭🇷 Hrvatski (Croatian)</option>
<option value="sr">🇷🇸 Српски (Serbian)</option>
<option value="sl">🇸🇮 Slovenščina (Slovenian)</option>
<option value="mk">🇲🇰 Македонски (Macedonian)</option>
<option value="sq">🇦🇱 Shqip (Albanian)</option>
<option value="lv">🇱🇻 Latviešu (Latvian)</option>
<option value="lt">🇱🇹 Lietuvių (Lithuanian)</option>
<option value="et">🇪🇪 Eesti (Estonian)</option>
<option value="mt">🇲🇹 Malti (Maltese)</option>
<option value="ga">🇮🇪 Gaeilge (Irish)</option>
<option value="cy">🏴󠁧󠁢󠁷󠁬󠁳󠁿 Cymraeg (Welsh)</option>
<option value="eu">🏴󠁥󠁳󠁰󠁶󠁿 Euskera (Basque)</option>
<option value="ca">🏴󠁥󠁳󠁣󠁴󠁿 Català (Catalan)</option>
<option value="gl">🏴󠁥󠁳󠁧󠁡󠁿 Galego (Galician)</option>
<option value="el">🇬🇷 Ελληνικά (Greek)</option>
<option value="he">🇮🇱 עברית (Hebrew)</option>
<option value="fa">🇮🇷 فارسی (Persian)</option>
<option value="ps">🇦🇫 پښتو (Pashto)</option>
<option value="ku">🏴󠁩󠁱󠁫󠁲󠁿 کوردی (Kurdish)</option>
<option value="az">🇦🇿 Azərbaycan (Azerbaijani)</option>
<option value="kk">🇰🇿 Қазақша (Kazakh)</option>
<option value="ky">🇰🇬 Кыргызча (Kyrgyz)</option>
<option value="uz">🇺🇿 O'zbek (Uzbek)</option>
<option value="tk">🇹🇲 Türkmen (Turkmen)</option>
<option value="tg">🇹🇯 Тоҷикӣ (Tajik)</option>
<option value="mn">🇲🇳 Монгол (Mongolian)</option>
<option value="ka">🇬🇪 ქართული (Georgian)</option>
<option value="hy">🇦🇲 Հայերեն (Armenian)</option>
<option value="th">🇹🇭 ไทย (Thai)</option>
<option value="vi">🇻🇳 Tiếng Việt (Vietnamese)</option>
<option value="lo">🇱🇦 ລາວ (Lao)</option>
<option value="km">🇰🇭 ខ្មែរ (Khmer)</option>
<option value="my">🇲🇲 မြန်မာ (Myanmar)</option>
<option value="si">🇱🇰 සිංහල (Sinhala)</option>
<option value="ta">🇱🇰 தமிழ் (Tamil)</option>
<option value="te">🇮🇳 తెలుగు (Telugu)</option>
<option value="kn">🇮🇳 ಕನ್ನಡ (Kannada)</option>
<option value="ml">🇮🇳 മലയാളം (Malayalam)</option>
<option value="gu">🇮🇳 ગુજરાતી (Gujarati)</option>
<option value="pa">🇮🇳 ਪੰਜਾਬੀ (Punjabi)</option>
<option value="or">🇮🇳 ଓଡ଼ିଆ (Odia)</option>
<option value="as">🇮🇳 অসমীয়া (Assamese)</option>
<option value="ne">🇳🇵 नेपाली (Nepali)</option>
<option value="dz">🇧🇹 རྫོང་ཁ (Dzongkha)</option>
<option value="bo">🏔️ བོད་ཡིག (Tibetan)</option>
<option value="id">🇮🇩 Bahasa Indonesia</option>
<option value="ms">🇲🇾 Bahasa Melayu (Malay)</option>
<option value="tl">🇵🇭 Filipino (Tagalog)</option>
<option value="ceb">🇵🇭 Cebuano</option>
<option value="haw">🏝️ ʻŌlelo Hawaiʻi (Hawaiian)</option>
<option value="mi">🇳🇿 Te Reo Māori (Maori)</option>
<option value="sm">🇼🇸 Gagana Samoa (Samoan)</option>
<option value="to">🇹🇴 Lea Fakatonga (Tongan)</option>
<option value="fj">🇫🇯 Na Vosa Vakaviti (Fijian)</option>
<option value="mg">🇲🇬 Malagasy</option>
<option value="sw">🇰🇪 Kiswahili (Swahili)</option>
<option value="zu">🇿🇦 isiZulu (Zulu)</option>
<option value="xh">🇿🇦 isiXhosa (Xhosa)</option>
<option value="af">🇿🇦 Afrikaans</option>
<option value="st">🇱🇸 Sesotho (Southern Sotho)</option>
<option value="tn">🇧🇼 Setswana (Tswana)</option>
<option value="ss">🇸🇿 siSwati (Swati)</option>
<option value="ve">🇿🇦 Tshivenḓa (Venda)</option>
<option value="ts">🇿🇦 Xitsonga (Tsonga)</option>
<option value="nr">🇿🇦 isiNdebele (Southern Ndebele)</option>
<option value="am">🇪🇹 አማርኛ (Amharic)</option>
<option value="ti">🇪🇷 ትግርኛ (Tigrinya)</option>
<option value="om">🇪🇹 Afaan Oromoo (Oromo)</option>
<option value="so">🇸🇴 Soomaali (Somali)</option>
<option value="ha">🇳🇬 Hausa</option>
<option value="yo">🇳🇬 Yorùbá (Yoruba)</option>
<option value="ig">🇳🇬 Igbo</option>
<option value="ff">🇸🇳 Fulfulde (Fulani)</option>
<option value="wo">🇸🇳 Wolof</option>
<option value="bm">🇲🇱 Bamanankan (Bambara)</option>
<option value="rn">🇧🇮 Kirundi (Rundi)</option>
<option value="rw">🇷🇼 Kinyarwanda (Rwanda)</option>
<option value="lg">🇺🇬 Luganda</option>
<option value="ny">🇲🇼 Chichewa (Nyanja)</option>
<option value="sn">🇿🇼 chiShona (Shona)</option>
<option value="nd">🇿🇼 isiNdebele (Northern Ndebele)</option>
</select>
</div>
<div class="api-key-landing">
<label for="apiKeyLanding" data-translate="api_key_label">🔑 OpenAI API Key</label>
<input type="password" id="apiKeyLanding" placeholder="Enter your OpenAI API key" data-translate-placeholder="api_key_placeholder">
</div>
<button class="start-btn" id="startBtn" data-translate="start_button">🚀 Start Ethics Assessment</button>
</div>
<!-- Main Application -->
<div class="main-app" id="mainApp">
<div class="container">
<!-- Language Switcher -->
<div class="language-switcher" id="languageSwitcher">
<div class="language-switch-btn" onclick="showLanguageLanding()">
<span data-translate="change_language">🌐 Change Language</span>
</div>
<div class="mini-clear-cache" onclick="clearAllTranslationCache()" title="Clear translation cache">
<span data-translate="clear_cache_mini">🗑️ Clear Cache</span>
</div>
</div>
<!-- API Key Section - Static Position Top Left -->
<div class="api-key-section">
<label for="apiKey" data-translate="api_key_short">🔑 API Key</label>
<input type="password" id="apiKey" data-translate-placeholder="api_key_placeholder" autocomplete="off">
</div>
<div class="main-content">
<!-- Header -->
<div class="header">
<h1 data-translate="app_title">EthicsGuard Pro</h1>
<p class="subtitle" data-translate="app_subtitle">AI Education Ethics Assessment Platform</p>
<p class="description" data-translate="app_description">Test your knowledge of ethical considerations when using AI in educational settings with personalized assessments and detailed feedback</p>
</div>
<!-- Progress Bar -->
<div class="progress-container">
<div class="progress-label">
<span data-translate="test_progress">Test Progress</span>
<span id="progressText">0%</span>
</div>
<div class="progress-bar-track">
<div class="progress-bar-fill" id="progressBar"></div>
</div>
</div>
<!-- Section: Test Setup -->
<section id="testSetup" class="section">
<h2 class="section-title" data-translate="generate_test_title">Generate Your Ethics Test</h2>
<p style="margin-bottom: 2rem; color: var(--text-secondary); text-align: center;" data-translate="generate_test_description">
Click the button below to generate a personalized 10-question multiple-choice test on AI in Education Ethics.
</p>
<button id="generateTestBtn" class="generate-button">
<span class="loading-spinner" id="loadingSpinner"></span>
<span class="button-text" data-translate="generate_test_button">🧠 Generate Test</span>
</button>
</section>
<!-- Section: Test Questions -->
<section id="testSection" class="section hidden">
<h2 class="section-title" data-translate="test_questions_title">Test Questions</h2>
<form id="testForm">
<div id="questionsContainer" class="questions-container"></div>
<div class="action-buttons">
<button type="submit" class="btn btn-primary" data-translate="submit_test_button">📝 Submit Test</button>
<button type="button" id="newTestBtn" class="btn btn-secondary" data-translate="new_test_button">🔄 New Test</button>
</div>
</form>
</section>
<!-- Section: Test Results -->
<section id="resultsSection" class="section hidden">
<h2 class="section-title" data-translate="test_results_title">Test Results</h2>
<div class="results-container">
<div class="score-display">
<div class="score-number" id="scoreNumber">0</div>
<div class="score-label" data-translate="score_label">out of 10 questions correct</div>
</div>
<div class="feedback-container">
<h3 class="feedback-title" data-translate="feedback_title">📋 Detailed Feedback</h3>
<div id="feedbackContent"></div>
</div>
<div class="action-buttons">
<button id="newTestBtn2" class="btn btn-primary" data-translate="generate_new_test_button">🔄 Generate New Test</button>
</div>
</div>
</section>
<!-- Error and Status Messages -->
<div class="error-message" id="errorMessage"></div>
<div class="status-message" id="statusMessage"></div>
</div>
<!-- Footer -->
<div class="footer">
Created by Shift Mind AI Labs
</div>
</div>
</div>
<script>
// RTL languages list
const rtlLanguages = ['ar', 'he', 'fa', 'ur', 'ps', 'ku'];
// Current language and API key
let currentLanguage = 'en';
let currentApiKey = '';
// Language names mapping
const languageNames = {
en: 'English', es: 'Spanish', fr: 'French', de: 'German', zh: 'Chinese',
ja: 'Japanese', ko: 'Korean', pt: 'Portuguese', it: 'Italian', ar: 'Arabic',
ru: 'Russian', hi: 'Hindi', bn: 'Bengali', ur: 'Urdu', tr: 'Turkish',
pl: 'Polish', nl: 'Dutch', sv: 'Swedish', da: 'Danish', no: 'Norwegian',
fi: 'Finnish', is: 'Icelandic', cs: 'Czech', sk: 'Slovak', hu: 'Hungarian',
ro: 'Romanian', bg: 'Bulgarian', hr: 'Croatian', sr: 'Serbian', sl: 'Slovenian',
mk: 'Macedonian', sq: 'Albanian', lv: 'Latvian', lt: 'Lithuanian', et: 'Estonian',
mt: 'Maltese', ga: 'Irish', cy: 'Welsh', eu: 'Basque', ca: 'Catalan',
gl: 'Galician', el: 'Greek', he: 'Hebrew', fa: 'Persian', ps: 'Pashto',
ku: 'Kurdish', az: 'Azerbaijani', kk: 'Kazakh', ky: 'Kyrgyz', uz: 'Uzbek',
tk: 'Turkmen', tg: 'Tajik', mn: 'Mongolian', ka: 'Georgian', hy: 'Armenian',
th: 'Thai', vi: 'Vietnamese', lo: 'Lao', km: 'Khmer', my: 'Myanmar',
si: 'Sinhala', ta: 'Tamil', te: 'Telugu', kn: 'Kannada', ml: 'Malayalam',
gu: 'Gujarati', pa: 'Punjabi', or: 'Odia', as: 'Assamese', ne: 'Nepali',
dz: 'Dzongkha', bo: 'Tibetan', id: 'Indonesian', ms: 'Malay', tl: 'Filipino',
ceb: 'Cebuano', haw: 'Hawaiian', mi: 'Maori', sm: 'Samoan', to: 'Tongan',
fj: 'Fijian', mg: 'Malagasy', sw: 'Swahili', zu: 'Zulu', xh: 'Xhosa',
af: 'Afrikaans', st: 'Southern Sotho', tn: 'Tswana', ss: 'Swati', ve: 'Venda',
ts: 'Tsonga', nr: 'Southern Ndebele', am: 'Amharic', ti: 'Tigrinya', om: 'Oromo',
so: 'Somali', ha: 'Hausa', yo: 'Yoruba', ig: 'Igbo', ff: 'Fulani',
wo: 'Wolof', bm: 'Bambara', rn: 'Rundi', rw: 'Rwanda', lg: 'Luganda',
ny: 'Chichewa', sn: 'Shona', nd: 'Northern Ndebele'
};
// Translation cache management
const CACHE_PREFIX = 'ethicsguard_translations_';
const CACHE_VERSION = '1.0';
// App Configuration
const AppConfig = {
API_BASE_URL: 'https://api.openai.com/v1/chat/completions',
MODEL: 'gpt-4o-mini',
MAX_TOKENS: 1500,
TEMPERATURE: 0.5
};
// App State Management
const AppState = {
testData: null,
isLoading: false,
currentSection: 'setup',
setLoading(loading) {
this.isLoading = loading;
UIController.updateLoadingState(loading);
}
};
// Check if translations are cached for a language
function isLanguageCached(language) {
const cacheKey = CACHE_PREFIX + language;
const cached = localStorage.getItem(cacheKey);
return cached !== null;
}
// Save translations to cache
function saveTranslationsToCache(language, translations) {
const cacheKey = CACHE_PREFIX + language;
const cacheData = {
version: CACHE_VERSION,
timestamp: Date.now(),
translations: translations
};
localStorage.setItem(cacheKey, JSON.stringify(cacheData));
console.log(`Translations cached for ${language}`);
}
// Load translations from cache
function loadTranslationsFromCache(language) {
const cacheKey = CACHE_PREFIX + language;
const cached = localStorage.getItem(cacheKey);
if (cached) {
try {
const cacheData = JSON.parse(cached);
if (cacheData.version === CACHE_VERSION) {
console.log(`Translations loaded from cache for ${language}`);
return cacheData.translations;
}
} catch (error) {
console.error('Error parsing cached translations:', error);
}
}
return null;
}
// Get all cached languages
function getCachedLanguages() {
const cachedLanguages = [];
for (let i = 0; i < localStorage.length; i++) {
const key = localStorage.key(i);
if (key && key.startsWith(CACHE_PREFIX)) {
const language = key.replace(CACHE_PREFIX, '');
cachedLanguages.push(language);
}
}
return cachedLanguages;
}
// Clear all translation cache
function clearAllTranslationCache() {
const cachedLanguages = getCachedLanguages();
if (cachedLanguages.length === 0) {
alert('No cached translations to clear.');
return;
}
const languageList = cachedLanguages.map(lang => languageNames[lang] || lang).join(', ');
const confirmMessage = `Are you sure you want to clear all cached translations?\n\nCached languages: ${languageList}\n\nThis will require re-downloading translations when switching languages.`;
if (confirm(confirmMessage)) {
// Clear all translation caches
cachedLanguages.forEach(language => {
const cacheKey = CACHE_PREFIX + language;
localStorage.removeItem(cacheKey);
});
// Update cache status
updateCacheStatus(currentLanguage);
updateCacheStatusDisplay();
alert(`Cache cleared successfully!\n\n${cachedLanguages.length} language(s) removed from cache.`);
// Ask if user wants to reload current language translations
if (currentLanguage !== 'en' && cachedLanguages.includes(currentLanguage)) {
if (confirm('Would you like to reload the current language translations?')) {
applyLanguage(currentLanguage);
}
}
}
}
// Update cache status indicator
function updateCacheStatus(language) {
const cacheStatus = document.getElementById('cacheStatus');
const isCached = isLanguageCached(language);
if (language === 'en') {
cacheStatus.classList.remove('cached', 'translating');
return;
}
if (isCached) {
cacheStatus.textContent = '💾 Translations cached - instant loading!';
cacheStatus.classList.add('cached');
cacheStatus.classList.remove('translating');
} else {
cacheStatus.textContent = '🔄 First time translation - will be cached for future use';
cacheStatus.classList.add('translating');
cacheStatus.classList.remove('cached');
}
}
// Update cache status display in management section
function updateCacheStatusDisplay() {
const cacheStatusDisplay = document.getElementById('cacheStatusDisplay');
const clearCacheBtn = document.getElementById('clearCacheBtn');
const cachedLanguages = getCachedLanguages();
if (cachedLanguages.length === 0) {
cacheStatusDisplay.textContent = '📭 No cached translations';
cacheStatusDisplay.className = 'cache-status-display no-cache';
clearCacheBtn.disabled = true;
} else {
const languageList = cachedLanguages.map(lang => languageNames[lang] || lang).join(', ');
cacheStatusDisplay.textContent = `💾 ${cachedLanguages.length} language(s) cached: ${languageList}`;
cacheStatusDisplay.className = 'cache-status-display';
clearCacheBtn.disabled = false;
}
}
// Initialize the application
function initializeApp() {
// Load saved language and API key
const savedLanguage = localStorage.getItem('ethicsguard_language') || 'en';
const savedApiKey = localStorage.getItem('ethicsguard_api_key') || '';
currentLanguage = savedLanguage;
currentApiKey = savedApiKey;
// Set language selector
document.getElementById('languageSelect').value = currentLanguage;
document.getElementById('apiKeyLanding').value = currentApiKey;
// Apply direction for current language
applyDirection(currentLanguage);
// Update cache status
updateCacheStatus(currentLanguage);
updateCacheStatusDisplay();
// Show appropriate screen
if (currentApiKey && currentLanguage) {
showMainApp();
} else {
showLanguageLanding();
}
}
// Apply language direction
function applyDirection(language) {
currentLanguage = language;
// Set document language and direction
document.documentElement.lang = language;
document.documentElement.dir = rtlLanguages.includes(language) ? 'rtl' : 'ltr';
// Save language preference
localStorage.setItem('ethicsguard_language', language);
// Update cache status
updateCacheStatus(language);
}
// API call function for translation
async function translateText(text, targetLanguage) {
if (!currentApiKey) {
throw new Error('API key is required for translation');
}
const languageName = languageNames[targetLanguage] || 'English';
const prompt = `Translate the following text to ${languageName}. Provide ONLY the exact translation without any explanations, additional information, or formatting:
"${text}"`;
const payload = {
model: "gpt-4o-mini",
messages: [{ role: "user", content: prompt }],
max_tokens: 500,
temperature: 0.1
};
const response = await fetch("https://api.openai.com/v1/chat/completions", {
method: "POST",
headers: {
"Content-Type": "application/json",
"Authorization": `Bearer ${currentApiKey}`
},
body: JSON.stringify(payload)
});
if (!response.ok) {
const errorData = await response.json();
throw new Error(errorData.error?.message || "Translation API request failed");
}
const data = await response.json();
return data.choices[0].message.content.trim();
}
// Apply cached translations to UI
function applyCachedTranslations(translations) {
// Apply text translations
Object.keys(translations.texts).forEach(originalText => {
const translation = translations.texts[originalText];
const elements = document.querySelectorAll(`[data-translate]`);
elements.forEach(element => {
const originalElementText = element.getAttribute('data-original-text') || element.textContent;
if (originalElementText === originalText) {
element.textContent = translation;
}
});
});
// Apply placeholder translations
Object.keys(translations.placeholders).forEach(originalPlaceholder => {
const translation = translations.placeholders[originalPlaceholder];
const elements = document.querySelectorAll(`[data-translate-placeholder]`);
elements.forEach(element => {
const originalElementPlaceholder = element.getAttribute('data-original-placeholder') || element.placeholder;
if (originalElementPlaceholder === originalPlaceholder) {
element.placeholder = translation;
}
});
});
}
// Translate all UI elements
async function translateInterface(targetLanguage) {
if (targetLanguage === 'en') {
return;
}
// Check if translations are cached
const cachedTranslations = loadTranslationsFromCache(targetLanguage);
if (cachedTranslations) {
console.log('Using cached translations for', targetLanguage);
applyCachedTranslations(cachedTranslations);
return;
}
// Need to translate via API
showTranslationOverlay();
try {
// Get all elements with data-translate attribute
const elements = document.querySelectorAll('[data-translate]');
const placeholderElements = document.querySelectorAll('[data-translate-placeholder]');
// Collect all texts to translate
const textsToTranslate = [];
const placeholdersToTranslate = [];
const elementMap = new Map();
elements.forEach(element => {
const originalText = element.getAttribute('data-original-text') || element.textContent;
if (!element.getAttribute('data-original-text')) {
element.setAttribute('data-original-text', originalText);
}
textsToTranslate.push(originalText);
elementMap.set(originalText, element);
});
placeholderElements.forEach(element => {
const originalPlaceholder = element.getAttribute('data-original-placeholder') || element.placeholder;
if (!element.getAttribute('data-original-placeholder')) {
element.setAttribute('data-original-placeholder', originalPlaceholder);
}
placeholdersToTranslate.push(originalPlaceholder);
elementMap.set(originalPlaceholder, element);
});
// Prepare cache structure
const translationsCache = {
texts: {},
placeholders: {}
};
// Translate texts in batches
const batchSize = 10;
const allTexts = [...textsToTranslate, ...placeholdersToTranslate];
for (let i = 0; i < allTexts.length; i += batchSize) {
const batch = allTexts.slice(i, i + batchSize);
// Update progress
updateTranslationProgress(i, allTexts.length);
// Translate batch
const translations = await Promise.all(
batch.map(text => translateText(text, targetLanguage))
);
// Apply translations and cache them
batch.forEach((originalText, index) => {
const element = elementMap.get(originalText);
const translation = translations[index];
if (element.hasAttribute('data-translate')) {
element.textContent = translation;
translationsCache.texts[originalText] = translation;
} else if (element.hasAttribute('data-translate-placeholder')) {
element.placeholder = translation;
translationsCache.placeholders[originalText] = translation;
}
});
}
// Save translations to cache
saveTranslationsToCache(targetLanguage, translationsCache);
// Update cache status
updateCacheStatus(targetLanguage);
updateCacheStatusDisplay();
} catch (error) {
console.error('Translation error:', error);
showError('Translation failed: ' + error.message);
} finally {
hideTranslationOverlay();
}
}
// Show translation overlay
function showTranslationOverlay() {
document.getElementById('translationOverlay').style.display = 'flex';
}
// Hide translation overlay
function hideTranslationOverlay() {
document.getElementById('translationOverlay').style.display = 'none';
}
// Update translation progress
function updateTranslationProgress(current, total) {
const percentage = Math.round((current / total) * 100);
document.getElementById('translationMessage').textContent =
`Translating interface... ${percentage}% complete (will be cached for future use)`;
}
// Apply language with API translation or cache
async function applyLanguage(language) {
applyDirection(language);
if (language !== 'en') {
await translateInterface(language);
}
}
// Show language landing page
function showLanguageLanding() {
document.getElementById('languageLanding').style.display = 'block';
document.getElementById('mainApp').style.display = 'none';
}
// Show main application
function showMainApp() {
document.getElementById('languageLanding').style.display = 'none';
document.getElementById('mainApp').style.display = 'block';
// Set API key in main app
document.getElementById('apiKey').value = currentApiKey;
}
// Show error message
function showError(message) {
const errorDiv = document.getElementById('errorMessage');
errorDiv.textContent = message;
errorDiv.style.display = 'block';
setTimeout(() => {
errorDiv.style.display = 'none';
}, 5000);
}
// API Service Module
const APIService = {
async callOpenAI(prompt, apiKey) {
const languageName = languageNames[currentLanguage] || 'English';
// Modify prompt to include language instruction
const languagePrompt = currentLanguage !== 'en'
? `\n\nIMPORTANT: Generate all content (questions, options, feedback) in ${languageName} language.`
: '';
const payload = {
model: AppConfig.MODEL,
messages: [{ role: 'system', content: prompt + languagePrompt }],
max_tokens: AppConfig.MAX_TOKENS,
temperature: AppConfig.TEMPERATURE
};
const response = await fetch(AppConfig.API_BASE_URL, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${apiKey}`
},
body: JSON.stringify(payload)
});
if (!response.ok) {
const errorData = await response.json();
throw new Error(errorData.error?.message || `HTTP ${response.status}: ${response.statusText}`);
}
const data = await response.json();
return data.choices[0].message.content;
}
};
// Utility Functions
const Utils = {
extractJSON(rawText, startMarker, endMarker) {
const startIndex = rawText.indexOf(startMarker);
const endIndex = rawText.indexOf(endMarker, startIndex);
if (startIndex !== -1 && endIndex !== -1) {
return rawText.substring(startIndex + startMarker.length, endIndex).trim();
}
return rawText;
},
updateProgress(percent) {
const progressBar = document.getElementById('progressBar');
const progressText = document.getElementById('progressText');
progressBar.style.width = percent + '%';
progressText.textContent = percent + '%';
}
};
// UI Controller Module
const UIController = {
elements: {},
init() {
this.elements = {
apiKeyInput: document.getElementById('apiKey'),
generateBtn: document.getElementById('generateTestBtn'),
loadingSpinner: document.getElementById('loadingSpinner'),
buttonText: document.querySelector('.button-text'),
testSetup: document.getElementById('testSetup'),
testSection: document.getElementById('testSection'),
resultsSection: document.getElementById('resultsSection'),
questionsContainer: document.getElementById('questionsContainer'),
testForm: document.getElementById('testForm'),
scoreNumber: document.getElementById('scoreNumber'),
feedbackContent: document.getElementById('feedbackContent'),
newTestBtn: document.getElementById('newTestBtn'),
newTestBtn2: document.getElementById('newTestBtn2')
};
this.setupEventListeners();
},
setupEventListeners() {
this.elements.generateBtn.addEventListener('click', () => {
App.generateTest();
});
this.elements.testForm.addEventListener('submit', (e) => {
e.preventDefault();
App.submitTest();
});
this.elements.newTestBtn.addEventListener('click', () => {
App.resetToSetup();
});
this.elements.newTestBtn2.addEventListener('click', () => {
App.resetToSetup();
});
},
updateLoadingState(isLoading) {
if (isLoading) {
this.elements.generateBtn.disabled = true;
this.elements.buttonText.style.opacity = '0';
this.elements.loadingSpinner.style.display = 'block';
} else {
this.elements.generateBtn.disabled = false;
this.elements.buttonText.style.opacity = '1';
this.elements.loadingSpinner.style.display = 'none';
}
},
showSection(sectionName) {
// Hide all sections
this.elements.testSetup.classList.add('hidden');
this.elements.testSection.classList.add('hidden');
this.elements.resultsSection.classList.add('hidden');
// Show target section
switch (sectionName) {
case 'setup':
this.elements.testSetup.classList.remove('hidden');
break;
case 'test':
this.elements.testSection.classList.remove('hidden');
break;
case 'results':
this.elements.resultsSection.classList.remove('hidden');
break;
}
AppState.currentSection = sectionName;
},
displayTest(testData) {
this.elements.questionsContainer.innerHTML = '';
testData.questions.forEach((question, index) => {
const questionNumber = index + 1;
const questionCard = this.createQuestionCard(question, questionNumber);
this.elements.questionsContainer.appendChild(questionCard);
});
this.showSection('test');
Utils.updateProgress(100);
},
createQuestionCard(question, questionNumber) {
const card = document.createElement('div');
card.className = 'question-card';
card.innerHTML = `
<div class="question-number">${questionNumber}</div>
<div class="question-text">${question.question}</div>
<div class="options-container">
${Object.entries(question.options).map(([key, value]) => `
<div class="option-item">
<input type="radio" id="q${questionNumber}_${key}" name="question${questionNumber}" value="${key}" class="option-input">
<label for="q${questionNumber}_${key}" class="option-label">
<div class="option-indicator"></div>
<div class="option-text"><strong>${key}.</strong> ${value}</div>
</label>
</div>
`).join('')}
</div>
`;
return card;
},
displayResults(gradingResult) {
this.elements.scoreNumber.textContent = gradingResult.score;
let feedbackHTML = '';
gradingResult.results.forEach(item => {
const isCorrect = item.userAnswer === item.correctAnswer;
feedbackHTML += `
<div class="feedback-item ${isCorrect ? 'correct' : 'incorrect'}">
<div class="feedback-question">Question ${item.questionNumber}</div>
<div class="feedback-answers">
Your answer: <strong>${item.userAnswer || 'Not answered'}</strong> |
Correct answer: <strong>${item.correctAnswer}</strong>
</div>
<div class="feedback-explanation">${item.feedback}</div>
</div>
`;
});
this.elements.feedbackContent.innerHTML = feedbackHTML;
this.showSection('results');
},
showError(message) {
const errorDiv = document.getElementById('errorMessage');
errorDiv.textContent = message;
errorDiv.style.display = 'block';
setTimeout(() => {
errorDiv.style.display = 'none';
}, 5000);
}
};
// Error Handler Module
const ErrorHandler = {
handleError(error) {
console.error('Application error:', error);
let userMessage = 'An unexpected error occurred. Please try again.';
if (error.message.includes('API key')) {
userMessage = 'Invalid API key. Please check your OpenAI API key and try again.';
} else if (error.message.includes('network') || error.message.includes('fetch')) {
userMessage = 'Network error. Please check your internet connection and try again.';
} else if (error.message.includes('rate limit')) {
userMessage = 'Rate limit exceeded. Please wait a moment before trying again.';
} else if (error.message.includes('quota')) {
userMessage = 'API quota exceeded. Please check your OpenAI account usage.';
}
UIController.showError(userMessage);
}
};
// Input Validator Module
const InputValidator = {
validateApiKey(apiKey) {
if (!apiKey || apiKey.length < 10) {
throw new Error('Please enter a valid OpenAI API key.');
}
if (!apiKey.startsWith('sk-')) {
throw new Error('OpenAI API key should start with "sk-".');
}
return true;
}
};
// Main App Module
const App = {
init() {
UIController.init();
Utils.updateProgress(0);
console.log('EthicsGuard Pro initialized');
},
async generateTest() {
if (AppState.isLoading) return;
try {
const apiKey = currentApiKey;
// Validate API key
InputValidator.validateApiKey(apiKey);
// Update state
AppState.setLoading(true);
Utils.updateProgress(10);
// Build prompt for test generation
const prompt = `
Generate a multiple-choice test on AI in Education Ethics. Provide exactly 10 questions.
For each question, provide 4 answer choices labeled A, B, C, and D.
Also provide an answer key that maps question numbers (1 to 10) to the correct option letter.
Output exactly valid JSON in the following format with no extra commentary:
<<<BEGIN TEST>>>
{
"questions": [
{
"question": "Question text here",
"options": {
"A": "Option A text",
"B": "Option B text",
"C": "Option C text",
"D": "Option D text"
}
},
... (10 questions total)
],
"answerKey": {
"1": "B",
"2": "D",
... (for all 10 questions)
}
}
<<<END TEST>>>
`;
Utils.updateProgress(30);
// Call API
const result = await APIService.callOpenAI(prompt, apiKey);
Utils.updateProgress(70);
// Parse result
const testJsonString = Utils.extractJSON(result, "<<<BEGIN TEST>>>", "<<<END TEST>>>");
AppState.testData = JSON.parse(testJsonString);
Utils.updateProgress(90);
// Display test
UIController.displayTest(AppState.testData);
} catch (error) {
ErrorHandler.handleError(error);
Utils.updateProgress(0);
} finally {
AppState.setLoading(false);
}
},
async submitTest() {
try {
// Collect user answers
const userAnswers = {};
AppState.testData.questions.forEach((q, index) => {
const questionNumber = index + 1;
const radios = document.getElementsByName(`question${questionNumber}`);
for (let radio of radios) {
if (radio.checked) {
userAnswers[questionNumber] = radio.value;
break;
}
}
if (!userAnswers[questionNumber]) {
userAnswers[questionNumber] = "";
}
});
// Build grading prompt
const prompt = `
You are an expert test grader. Given the following answer key and user answers for a 10-question multiple-choice test, calculate the score (number of correct answers) and provide a detailed feedback report.
Answer Key:
<<<BEGIN KEY>>>
${JSON.stringify(AppState.testData.answerKey, null, 2)}
<<<END KEY>>>
User Answers:
<<<BEGIN ANSWERS>>>
${JSON.stringify(userAnswers, null, 2)}
<<<END ANSWERS>>>
Output exactly valid JSON in the following format with no extra commentary:
{
"score": <number>,
"results": [
{
"questionNumber": <number>,
"userAnswer": "<letter>",
"correctAnswer": "<letter>",
"feedback": "<explanation>"
},
... (for each question)
]
}
`;
// Call API for grading
const result = await APIService.callOpenAI(prompt, currentApiKey);
const gradingResult = JSON.parse(result);
// Display results
UIController.displayResults(gradingResult);
} catch (error) {
ErrorHandler.handleError(error);
}
},
resetToSetup() {
AppState.testData = null;
UIController.elements.questionsContainer.innerHTML = "";
UIController.showSection('setup');
Utils.updateProgress(0);
}
};
// Start button click handler
document.getElementById('startBtn').addEventListener('click', async function() {
const selectedLanguage = document.getElementById('languageSelect').value;
const apiKey = document.getElementById('apiKeyLanding').value.trim();
if (!apiKey) {
alert('Please enter your OpenAI API key');
return;
}
currentLanguage = selectedLanguage;
currentApiKey = apiKey;
// Save API key
localStorage.setItem('ethicsguard_api_key', apiKey);
// Apply language with translation (cached or API)
await applyLanguage(selectedLanguage);
// Show main app
showMainApp();
});
// Language selector change handler
document.getElementById('languageSelect').addEventListener('change', async function() {
const selectedLanguage = this.value;
updateCacheStatus(selectedLanguage);
if (currentApiKey) {
await applyLanguage(selectedLanguage);
} else {
applyDirection(selectedLanguage);
}
});
// Clear cache button handler
document.getElementById('clearCacheBtn').addEventListener('click', clearAllTranslationCache);
// API key sync between landing and main app
document.getElementById('apiKeyLanding').addEventListener('input', function() {
currentApiKey = this.value;
localStorage.setItem('ethicsguard_api_key', this.value);
document.getElementById('apiKey').value = this.value;
});
document.getElementById('apiKey').addEventListener('input', function() {
currentApiKey = this.value;
localStorage.setItem('ethicsguard_api_key', this.value);
document.getElementById('apiKeyLanding').value = this.value;
});
// Initialize the application when page loads
document.addEventListener('DOMContentLoaded', function() {
initializeApp();
App.init();
});
</script>
</body>
</html>