Dyno1307 commited on
Commit
03b2ad1
·
verified ·
1 Parent(s): 2d62dcf

Upload 15 files

Browse files
.gitattributes CHANGED
@@ -35,3 +35,4 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
36
  data/processed/nepali.en filter=lfs diff=lfs merge=lfs -text
37
  data/processed/nepali.ne filter=lfs diff=lfs merge=lfs -text
 
 
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
36
  data/processed/nepali.en filter=lfs diff=lfs merge=lfs -text
37
  data/processed/nepali.ne filter=lfs diff=lfs merge=lfs -text
38
+ frontend/public/android-chrome-512x512.png filter=lfs diff=lfs merge=lfs -text
frontend/WhatsApp Image 2025-10-07 at 12.52.12.jpeg ADDED
frontend/backup/index.html ADDED
@@ -0,0 +1,23 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Saksi Translation</title>
7
+ <link rel="stylesheet" href="/frontend/styles.css">
8
+ <script src="/frontend/script.js" defer></script>
9
+ </head>
10
+ <body>
11
+ <div class="container">
12
+ <h1>Saksi Translation</h1>
13
+ <textarea id="text-to-translate" rows="5" placeholder="Enter text to translate..."></textarea>
14
+ <select id="source-language">
15
+ <option value="nepali">Nepali</option>
16
+ <option value="sinhala">Sinhala</option>
17
+ </select>
18
+ <button id="translate-button">Translate</button>
19
+ <h2>Translated Text:</h2>
20
+ <div id="output"></div>
21
+ </div>
22
+ </body>
23
+ </html>
frontend/backup/script.js ADDED
@@ -0,0 +1,42 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ document.addEventListener('DOMContentLoaded', () => {
2
+ const translateButton = document.getElementById('translate-button');
3
+ const textToTranslate = document.getElementById('text-to-translate');
4
+ const sourceLanguage = document.getElementById('source-language');
5
+ const outputDiv = document.getElementById('output');
6
+
7
+ translateButton.addEventListener('click', async () => {
8
+ const text = textToTranslate.value;
9
+ const lang = sourceLanguage.value;
10
+
11
+ outputDiv.innerText = "Translating...";
12
+
13
+ if (!text.trim()) {
14
+ outputDiv.innerText = "Please enter some text to translate.";
15
+ return;
16
+ }
17
+
18
+ try {
19
+ const response = await fetch('/translate', {
20
+ method: 'POST',
21
+ headers: {
22
+ 'Content-Type': 'application/json',
23
+ 'accept': 'application/json'
24
+ },
25
+ body: JSON.stringify({
26
+ text: text,
27
+ source_language: lang
28
+ })
29
+ });
30
+
31
+ if (!response.ok) {
32
+ const errorData = await response.json();
33
+ throw new Error(errorData.detail || 'An error occurred');
34
+ }
35
+
36
+ const data = await response.json();
37
+ outputDiv.innerText = data.translated_text;
38
+ } catch (error) {
39
+ outputDiv.innerText = `Error: ${error.message}`;
40
+ }
41
+ });
42
+ });
frontend/backup/styles.css ADDED
@@ -0,0 +1,54 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ body {
2
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
3
+ background-color: #f0f2f5;
4
+ display: flex;
5
+ justify-content: center;
6
+ align-items: center;
7
+ height: 100vh;
8
+ margin: 0;
9
+ }
10
+ .container {
11
+ background-color: #fff;
12
+ padding: 2rem;
13
+ border-radius: 8px;
14
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
15
+ width: 100%;
16
+ max-width: 500px;
17
+ }
18
+ h1 {
19
+ text-align: center;
20
+ color: #1c1e21;
21
+ }
22
+ textarea {
23
+ width: 100%;
24
+ padding: 0.5rem;
25
+ border: 1px solid #dddfe2;
26
+ border-radius: 6px;
27
+ margin-bottom: 1rem;
28
+ font-size: 1rem;
29
+ resize: vertical;
30
+ }
31
+ select, button {
32
+ width: 100%;
33
+ padding: 0.75rem;
34
+ border-radius: 6px;
35
+ border: 1px solid #dddfe2;
36
+ font-size: 1rem;
37
+ margin-bottom: 1rem;
38
+ }
39
+ button {
40
+ background-color: #1877f2;
41
+ color: #fff;
42
+ border: none;
43
+ cursor: pointer;
44
+ }
45
+ button:hover {
46
+ background-color: #166fe5;
47
+ }
48
+ #output {
49
+ margin-top: 1rem;
50
+ padding: 1rem;
51
+ background-color: #f0f2f5;
52
+ border-radius: 6px;
53
+ min-height: 50px;
54
+ }
frontend/index.html ADDED
@@ -0,0 +1,101 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en" data-theme="light">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Anuvaad AI</title>
7
+ <meta name="description" content="Anuvaad AI - Nepali and Sinhala to English Machine Translation">
8
+ <link rel="stylesheet" href="/frontend/styles.css">
9
+ <script src="/frontend/script.js" defer></script>
10
+ <!-- Preload brand logo for faster first paint -->
11
+ <link rel="preload" as="image" href="/frontend/public/android-chrome-192x192.png">
12
+ <link rel="icon" type="image/png" sizes="32x32" href="/frontend/public/favicon-32x32.png">
13
+ <link rel="icon" type="image/png" sizes="16x16" href="/frontend/public/favicon-16x16.png">
14
+ <link rel="apple-touch-icon" sizes="180x180" href="/frontend/public/apple-touch-icon.png">
15
+ <link rel="manifest" href="/frontend/public/site.webmanifest">
16
+ <!-- Optional larger icons -->
17
+ <link rel="icon" type="image/png" sizes="192x192" href="/frontend/public/android-chrome-192x192.png">
18
+ <link rel="icon" type="image/png" sizes="512x512" href="/frontend/public/android-chrome-512x512.png">
19
+ </head>
20
+ <body>
21
+ <header class="header">
22
+ <div class="brand">
23
+ <a href="/" class="brand-link" aria-label="Go to home">
24
+ <img src="/frontend/public/android-chrome-192x192.png" alt="Anuvaad AI logo" class="brand-logo" width="44" height="44" decoding="async" fetchpriority="high">
25
+ <span class="brand-text">Anuvaad.ai</span>
26
+ </a>
27
+ </div>
28
+ <div class="tagline">Nepali and Sinhala to English translation</div>
29
+ <div class="header-actions">
30
+ <!-- Theme selector in header -->
31
+ <label for="theme-select" class="sr-only">Theme</label>
32
+ <select id="theme-select" class="theme-select" aria-label="Theme">
33
+ <option value="gradient">Gradient (Default)</option>
34
+ <option value="light">Light</option>
35
+ <option value="dark">Dark</option>
36
+ <option value="ocean">Ocean</option>
37
+ <option value="sunset">Sunset</option>
38
+ <option value="forest">Forest</option>
39
+ <option value="rose">Rose</option>
40
+ <option value="slate">Slate</option>
41
+ </select>
42
+ <button id="theme-toggle" class="ghost" aria-label="Toggle theme">Dark mode</button>
43
+ <!-- Header status indicator -->
44
+ </div>
45
+ </header>
46
+ <main class="container">
47
+ <div class="grid">
48
+ <section class="panel">
49
+ <label for="text-to-translate" class="label">Enter text to translate</label>
50
+ <textarea id="text-to-translate" rows="10" placeholder="Type or paste text here. For batch translation, enter one sentence per line."></textarea>
51
+ <div class="controls">
52
+ <div class="control">
53
+ <label for="source-language" class="label">Source language</label>
54
+ <select id="source-language"></select>
55
+ <small id="lang-detect-hint" class="hint" aria-live="polite"></small>
56
+ </div>
57
+ <div class="control toggle">
58
+ <label class="checkbox-label">
59
+ <input type="checkbox" id="batch-toggle" />
60
+ Batch mode
61
+ </label>
62
+ <small class="hint">Translate multiple lines at once</small>
63
+ </div>
64
+ <!-- Borrowed words / names correction toggle -->
65
+ <div class="control toggle">
66
+ <label class="checkbox-label">
67
+ <input type="checkbox" id="borrowed-toggle" checked />
68
+ Fix borrowed words and names
69
+ </label>
70
+ <small class="hint">Transliterate and correct English-like names (e.g., Coco Beach)</small>
71
+ </div>
72
+ <!-- Dataset processing (no data display to users) -->
73
+ <div class="control">
74
+ <label for="process-data-button" class="label">Dataset processing</label>
75
+ <button id="process-data-button" class="ghost" aria-label="Process dataset">Process data</button>
76
+ <small id="process-data-status" class="hint" aria-live="polite"></small>
77
+ </div>
78
+ </div>
79
+ <div class="actions">
80
+ <button id="translate-button" class="primary">Translate</button>
81
+ <button id="clear-button" class="ghost" aria-label="Clear input">Clear</button>
82
+ </div>
83
+ </section>
84
+ <section class="panel">
85
+ <div class="panel-header">
86
+ <h2>Translated Text</h2>
87
+ <div class="panel-actions">
88
+ <button id="copy-button" class="ghost" aria-label="Copy output">Copy</button>
89
+ <button id="download-button" class="ghost" aria-label="Download output">Download</button>
90
+ <button id="share-button" class="ghost" aria-label="Share link">Share</button>
91
+ </div>
92
+ </div>
93
+ <div id="output" class="output" role="status" aria-live="polite"></div>
94
+ </section>
95
+ </div>
96
+ </main>
97
+ <footer class="footer">
98
+ <span>Powered by NLLB and FastAPI</span>
99
+ </footer>
100
+ </body>
101
+ </html>
frontend/public/android-chrome-192x192.png ADDED
frontend/public/android-chrome-512x512.png ADDED

Git LFS Details

  • SHA256: 46bd2b1389188ca0c79f00cdddf968941810274b68d3d9c2a14e5fb4ec2d5eb7
  • Pointer size: 131 Bytes
  • Size of remote file: 151 kB
frontend/public/apple-touch-icon.png ADDED
frontend/public/favicon-16x16.png ADDED
frontend/public/favicon-32x32.png ADDED
frontend/public/favicon.ico ADDED
frontend/public/site.webmanifest ADDED
@@ -0,0 +1 @@
 
 
1
+ {"name":"","short_name":"","icons":[{"src":"/android-chrome-192x192.png","sizes":"192x192","type":"image/png"},{"src":"/android-chrome-512x512.png","sizes":"512x512","type":"image/png"}],"theme_color":"#ffffff","background_color":"#ffffff","display":"standalone"}
frontend/script.js ADDED
@@ -0,0 +1,337 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ document.addEventListener('DOMContentLoaded', async () => {
2
+ const translateButton = document.getElementById('translate-button');
3
+ const clearButton = document.getElementById('clear-button');
4
+ const copyButton = document.getElementById('copy-button');
5
+ const downloadButton = document.getElementById('download-button');
6
+ const shareButton = document.getElementById('share-button');
7
+ const textToTranslate = document.getElementById('text-to-translate');
8
+ const sourceLanguage = document.getElementById('source-language');
9
+ const outputDiv = document.getElementById('output');
10
+ const batchToggle = document.getElementById('batch-toggle');
11
+ const langDetectHint = document.getElementById('lang-detect-hint');
12
+ const themeToggle = document.getElementById('theme-toggle');
13
+ const processDataButton = document.getElementById('process-data-button');
14
+ const processDataStatus = document.getElementById('process-data-status');
15
+
16
+ // Debounce timer for detection
17
+ let detectTimer = null;
18
+ const detectDelay = 250;
19
+
20
+ // Populate languages dynamically
21
+ try {
22
+ const langRes = await fetch('/languages');
23
+ const langData = await langRes.json();
24
+ const langs = (langData && langData.supported_languages) || ['nepali', 'sinhala'];
25
+ sourceLanguage.innerHTML = '';
26
+ langs.forEach(l => {
27
+ const opt = document.createElement('option');
28
+ opt.value = l;
29
+ opt.textContent = l.charAt(0).toUpperCase() + l.slice(1);
30
+ sourceLanguage.appendChild(opt);
31
+ });
32
+ } catch (e) {
33
+ // Fallback
34
+ sourceLanguage.innerHTML = '<option value="nepali">Nepali</option><option value="sinhala">Sinhala</option>';
35
+ }
36
+
37
+ // Theme toggle
38
+ // Ensure default gradient theme on first load unless user saved preference
39
+ (function() {
40
+ const savedTheme = localStorage.getItem('theme');
41
+ if (!savedTheme) {
42
+ document.documentElement.setAttribute('data-theme', 'gradient');
43
+ }
44
+ })();
45
+
46
+ themeToggle.addEventListener('click', () => {
47
+ const html = document.documentElement;
48
+ const isDark = html.getAttribute('data-theme') === 'dark';
49
+ html.setAttribute('data-theme', isDark ? 'light' : 'dark');
50
+ themeToggle.textContent = isDark ? 'Light mode' : 'Dark mode';
51
+ localStorage.setItem('anuvaad_theme', isDark ? 'light' : 'dark');
52
+ });
53
+ const savedTheme = localStorage.getItem('anuvaad_theme');
54
+ if (savedTheme) {
55
+ document.documentElement.setAttribute('data-theme', savedTheme);
56
+ themeToggle.textContent = savedTheme === 'dark' ? 'Dark mode' : 'Light mode';
57
+ }
58
+
59
+ function setLoading(isLoading) {
60
+ translateButton.disabled = isLoading;
61
+ translateButton.textContent = isLoading ? 'Translating…' : 'Translate';
62
+ outputDiv.setAttribute('aria-busy', String(isLoading));
63
+ }
64
+
65
+ // Basic language auto-detect by script characters (debounced)
66
+ textToTranslate.addEventListener('input', () => {
67
+ clearTimeout(detectTimer);
68
+ detectTimer = setTimeout(async () => {
69
+ const sample = (textToTranslate.value || '').slice(0, 200);
70
+ let detected = '';
71
+ // Backend-assisted detection for robustness
72
+ try {
73
+ const res = await fetch('/detect', {
74
+ method: 'POST',
75
+ headers: { 'Content-Type': 'application/json', 'accept': 'application/json' },
76
+ body: JSON.stringify({ text: sample })
77
+ });
78
+ if (res.ok) {
79
+ const data = await res.json();
80
+ detected = data.detected_language || '';
81
+ }
82
+ } catch (e) {
83
+ // ignore detection errors, fallback to script-based
84
+ }
85
+ if (!detected) {
86
+ const hasDevanagari = /[\u0900-\u097F]/.test(sample);
87
+ const hasSinhala = /[\u0D80-\u0DFF]/.test(sample);
88
+ if (hasDevanagari) detected = 'nepali';
89
+ else if (hasSinhala) detected = 'sinhala';
90
+ }
91
+ if (detected) {
92
+ sourceLanguage.value = detected;
93
+ langDetectHint.textContent = `Detected: ${detected}`;
94
+ } else {
95
+ langDetectHint.textContent = '';
96
+ }
97
+ }, detectDelay);
98
+ });
99
+
100
+ translateButton.addEventListener('click', async () => {
101
+ const text = (textToTranslate.value || '').trim();
102
+ const lang = sourceLanguage.value;
103
+ const isBatch = batchToggle && batchToggle.checked;
104
+ const borrowedFixEl = document.getElementById('borrowed-toggle');
105
+ const borrowedFix = borrowedFixEl ? borrowedFixEl.checked : true;
106
+
107
+ outputDiv.innerHTML = '';
108
+
109
+ if (!text) {
110
+ outputDiv.innerText = 'Please enter some text to translate.';
111
+ return;
112
+ }
113
+
114
+ setLoading(true);
115
+ try {
116
+ let response;
117
+ if (isBatch) {
118
+ const texts = text.split('\n').map(t => t.trim()).filter(Boolean);
119
+ if (texts.length === 0) {
120
+ outputDiv.innerText = 'Please provide at least one non-empty line for batch translation.';
121
+ setLoading(false);
122
+ return;
123
+ }
124
+ response = await fetch('/batch-translate', {
125
+ method: 'POST',
126
+ headers: {
127
+ 'Content-Type': 'application/json',
128
+ 'accept': 'application/json'
129
+ },
130
+ body: JSON.stringify({ texts, source_language: lang, borrowed_fix: borrowedFix })
131
+ });
132
+ } else {
133
+ response = await fetch('/translate', {
134
+ method: 'POST',
135
+ headers: {
136
+ 'Content-Type': 'application/json',
137
+ 'accept': 'application/json'
138
+ },
139
+ body: JSON.stringify({ text, source_language: lang, borrowed_fix: borrowedFix })
140
+ });
141
+ }
142
+
143
+ if (!response.ok) {
144
+ const errorData = await response.json().catch(() => ({}));
145
+ throw new Error(errorData.detail || 'An error occurred while translating.');
146
+ }
147
+
148
+ const data = await response.json();
149
+ if (isBatch) {
150
+ const results = data.translated_texts || [];
151
+ const table = document.createElement('table');
152
+ table.className = 'result-table';
153
+ const thead = document.createElement('thead');
154
+ thead.innerHTML = '<tr><th>#</th><th>Source</th><th>Translation</th></tr>';
155
+ table.appendChild(thead);
156
+ const tbody = document.createElement('tbody');
157
+ const sources = text.split('\n').map(t => t.trim()).filter(Boolean);
158
+ results.forEach((t, idx) => {
159
+ const tr = document.createElement('tr');
160
+ const tdIdx = document.createElement('td'); tdIdx.textContent = String(idx + 1);
161
+ const tdSrc = document.createElement('td'); tdSrc.textContent = sources[idx] || '';
162
+ const tdDst = document.createElement('td'); tdDst.textContent = t;
163
+ tr.appendChild(tdIdx); tr.appendChild(tdSrc); tr.appendChild(tdDst);
164
+ tbody.appendChild(tr);
165
+ });
166
+ table.appendChild(tbody);
167
+ outputDiv.appendChild(table);
168
+ downloadButton.dataset.csv = toCSV(sources, results);
169
+ } else {
170
+ outputDiv.innerText = data.translated_text || data.translation || '';
171
+ downloadButton.dataset.csv = toCSV([text], [outputDiv.innerText]);
172
+ }
173
+ } catch (error) {
174
+ outputDiv.innerText = `Error: ${error.message}`;
175
+ } finally {
176
+ setLoading(false);
177
+ }
178
+ });
179
+
180
+ // Allow Ctrl+Enter to trigger translation
181
+ textToTranslate.addEventListener('keydown', (e) => {
182
+ if ((e.ctrlKey || e.metaKey) && e.key === 'Enter') {
183
+ translateButton.click();
184
+ }
185
+ });
186
+
187
+ // Clear button
188
+ clearButton.addEventListener('click', () => {
189
+ textToTranslate.value = '';
190
+ outputDiv.innerHTML = '';
191
+ downloadButton.removeAttribute('data-csv');
192
+ });
193
+
194
+ // Hide dataset processing controls from users (keep in DOM, but not visible)
195
+ if (processDataButton) {
196
+ const datasetControl = processDataButton.closest('.control');
197
+ if (datasetControl) datasetControl.hidden = true;
198
+ const datasetLabel = document.querySelector('label[for="process-data-button"]');
199
+ if (datasetLabel) datasetLabel.hidden = true;
200
+ if (processDataStatus) processDataStatus.hidden = true;
201
+ }
202
+
203
+ // Hide borrowed words/names UI text while preserving functionality
204
+ const borrowedFixEl = document.getElementById('borrowed-toggle');
205
+ if (borrowedFixEl) {
206
+ const borrowedControl = borrowedFixEl.closest('.control');
207
+ // Keep the control present only during translation flow but hidden from display
208
+ if (borrowedControl) borrowedControl.hidden = true;
209
+ const borrowedLabel = borrowedFixEl.closest('label');
210
+ if (borrowedLabel) {
211
+ borrowedLabel.hidden = true;
212
+ // Remove any visible text nodes to avoid displaying borrowed words/names text
213
+ borrowedLabel.childNodes.forEach(node => {
214
+ if (node.nodeType === Node.TEXT_NODE) {
215
+ node.textContent = '';
216
+ }
217
+ });
218
+ }
219
+ const borrowedHint = borrowedControl ? borrowedControl.querySelector('small.hint') : null;
220
+ if (borrowedHint) {
221
+ borrowedHint.hidden = true;
222
+ borrowedHint.textContent = '';
223
+ }
224
+ // Remove the input element itself to ensure it never appears on screen
225
+ borrowedFixEl.remove();
226
+ }
227
+
228
+ // Helper to trigger dataset processing without user interaction
229
+ async function triggerProcessData() {
230
+ if (!processDataStatus) return;
231
+ try {
232
+ const res = await fetch('/process-data', { method: 'POST' });
233
+ if (!res.ok) {
234
+ const err = await res.json().catch(() => ({}));
235
+ throw new Error(err.detail || 'Failed to process dataset');
236
+ }
237
+ const data = await res.json();
238
+ // Update hidden status for diagnostics; users won't see it
239
+ processDataStatus.textContent = `Processed: ${data.processed_files} files, ${data.total_lines} lines`;
240
+ } catch (e) {
241
+ processDataStatus.textContent = `Error: ${e.message}`;
242
+ }
243
+ }
244
+
245
+ // Automatically process dataset on page load (runs once)
246
+ triggerProcessData();
247
+
248
+ // Dataset processing trigger (kept inside DOMContentLoaded for scope safety)
249
+ if (processDataButton) {
250
+ processDataButton.addEventListener('click', async () => {
251
+ // Even if clicked (hidden), keep behavior consistent
252
+ processDataStatus.textContent = 'Processing dataset…';
253
+ try {
254
+ const res = await fetch('/process-data', { method: 'POST' });
255
+ if (!res.ok) {
256
+ const err = await res.json().catch(() => ({}));
257
+ throw new Error(err.detail || 'Failed to process dataset');
258
+ }
259
+ const data = await res.json();
260
+ processDataStatus.textContent = `Processed: ${data.processed_files} files, ${data.total_lines} lines`;
261
+ } catch (e) {
262
+ processDataStatus.textContent = `Error: ${e.message}`;
263
+ }
264
+ });
265
+ }
266
+
267
+ // Copy button
268
+ copyButton.addEventListener('click', async () => {
269
+ const text = outputDiv.innerText || '';
270
+ if (!text) return;
271
+ try {
272
+ await navigator.clipboard.writeText(text);
273
+ } catch (e) {
274
+ // Fallback for older browsers
275
+ const ta = document.createElement('textarea');
276
+ ta.value = text;
277
+ document.body.appendChild(ta);
278
+ ta.select();
279
+ document.execCommand('copy');
280
+ document.body.removeChild(ta);
281
+ }
282
+ });
283
+
284
+ // Download CSV
285
+ downloadButton.addEventListener('click', () => {
286
+ const csv = downloadButton.dataset.csv;
287
+ if (!csv) return;
288
+ const blob = new Blob([csv], { type: 'text/csv;charset=utf-8;' });
289
+ const url = URL.createObjectURL(blob);
290
+ const a = document.createElement('a');
291
+ a.href = url;
292
+ a.download = 'translations.csv';
293
+ a.click();
294
+ URL.revokeObjectURL(url);
295
+ });
296
+
297
+ // Share result
298
+ shareButton.addEventListener('click', async () => {
299
+ const text = outputDiv.innerText || '';
300
+ if (!text) return;
301
+ try {
302
+ await navigator.share({ text });
303
+ } catch (e) {
304
+ // Ignore if not supported
305
+ }
306
+ });
307
+
308
+ function toCSV(sources, results) {
309
+ const rows = sources.map((s, i) => [s, results[i] || '']);
310
+ const csvRows = rows.map(r => r.map(v => '"' + String(v).replaceAll('"', '""') + '"').join(','));
311
+ return 'source,translation\n' + csvRows.join('\n');
312
+ }
313
+
314
+ // Theme select
315
+ const themeSelect = document.getElementById('theme-select');
316
+ if (themeSelect) {
317
+ const saved = localStorage.getItem('theme');
318
+ const initial = saved || 'gradient';
319
+ document.documentElement.setAttribute('data-theme', initial);
320
+ themeSelect.value = initial;
321
+ themeSelect.addEventListener('change', (e) => {
322
+ const v = e.target.value;
323
+ document.documentElement.setAttribute('data-theme', v);
324
+ localStorage.setItem('theme', v);
325
+ });
326
+ }
327
+
328
+ const themeToggleEl = document.getElementById('theme-toggle');
329
+ if (themeToggleEl) {
330
+ themeToggleEl.addEventListener('click', () => {
331
+ const html = document.documentElement;
332
+ const isDark = html.getAttribute('data-theme') === 'dark';
333
+ html.setAttribute('data-theme', isDark ? 'light' : 'dark');
334
+ localStorage.setItem('theme', isDark ? 'light' : 'dark');
335
+ });
336
+ }
337
+ });
frontend/site.webmanifest ADDED
@@ -0,0 +1 @@
 
 
1
+ {"name":"","short_name":"","icons":[{"src":"/android-chrome-192x192.png","sizes":"192x192","type":"image/png"},{"src":"/android-chrome-512x512.png","sizes":"512x512","type":"image/png"}],"theme_color":"#ffffff","background_color":"#ffffff","display":"standalone"}
frontend/styles.css ADDED
@@ -0,0 +1,512 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /* Base variables as fallback; specific themes override below */
2
+ :root {
3
+ --bg: #f8fafc;
4
+ --bg-soft: #f1f5f9;
5
+ --card: #ffffff;
6
+ --text: #0f172a;
7
+ --muted: #64748b;
8
+ --primary: #3b82f6;
9
+ --primary-hover: #2563eb;
10
+ --ghost: #e2e8f0;
11
+ --ghost-hover: #cbd5e1;
12
+ }
13
+
14
+ /* Default gradient theme */
15
+ html[data-theme="gradient"] {
16
+ --bg: #f8fafc; /* soft off-white */
17
+ --bg-soft: #f1f5f9; /* misty grey */
18
+ --card: #ffffff; /* pure card surface */
19
+ --text: #0f172a; /* rich charcoal */
20
+ --muted: #64748b; /* calm slate */
21
+ --primary: #3b82f6; /* vibrant blue */
22
+ --primary-hover: #2563eb;/* deeper blue */
23
+ --ghost: #e2e8f0; /* airy grey */
24
+ --ghost-hover: #cbd5e1; /* gentle hover */
25
+ }
26
+
27
+ /* Light theme (flat look) */
28
+ html[data-theme="light"] {
29
+ --bg: #ffffff;
30
+ --bg-soft: #ffffff;
31
+ --card: #ffffff;
32
+ --text: #0f172a;
33
+ --muted: #64748b;
34
+ --primary: #3b82f6;
35
+ --primary-hover: #2563eb;
36
+ --ghost: #eef2f7;
37
+ --ghost-hover: #e4e9f2;
38
+ }
39
+
40
+ /* Dark theme */
41
+ html[data-theme="dark"] {
42
+ --bg: #0f172a; /* midnight navy */
43
+ --bg-soft: #1e293b; /* subtle charcoal */
44
+ --card: #1e293b; /* sleek card */
45
+ --text: #f1f5f9; /* crisp white */
46
+ --muted: #94a3b8; /* muted silver */
47
+ --ghost: #334155; /* muted slate */
48
+ --ghost-hover: #475569; /* soft hover */
49
+ }
50
+
51
+ /* Ocean theme */
52
+ html[data-theme="ocean"] {
53
+ --bg: #e0f2fe; /* sky tint */
54
+ --bg-soft: #bae6fd; /* light ocean */
55
+ --card: #ffffff;
56
+ --text: #0f172a;
57
+ --muted: #0ea5e9;
58
+ --primary: #06b6d4; /* cyan */
59
+ --primary-hover: #0891b2;/* deep cyan */
60
+ --ghost: #e0f2fe;
61
+ --ghost-hover: #bae6fd;
62
+ }
63
+
64
+ /* Sunset theme */
65
+ html[data-theme="sunset"] {
66
+ --bg: #fff7ed; /* peach */
67
+ --bg-soft: #fde68a; /* amber */
68
+ --card: #ffffff;
69
+ --text: #0f172a;
70
+ --muted: #ea580c; /* warm orange */
71
+ --primary: #f97316; /* orange */
72
+ --primary-hover: #ea580c;/* deeper orange */
73
+ --ghost: #fff1e6;
74
+ --ghost-hover: #ffe4c7;
75
+ }
76
+
77
+ /* Forest theme */
78
+ html[data-theme="forest"] {
79
+ --bg: #dcfce7; /* mint */
80
+ --bg-soft: #a7f3d0; /* light green */
81
+ --card: #ffffff;
82
+ --text: #0f172a;
83
+ --muted: #16a34a; /* deep green */
84
+ --primary: #22c55e; /* green */
85
+ --primary-hover: #16a34a;/* deeper green */
86
+ --ghost: #e7ffe9;
87
+ --ghost-hover: #d1fadf;
88
+ }
89
+
90
+ /* Rose theme */
91
+ html[data-theme="rose"] {
92
+ --bg: #ffe4e6; /* blush */
93
+ --bg-soft: #fecdd3; /* soft rose */
94
+ --card: #ffffff;
95
+ --text: #0f172a;
96
+ --muted: #e11d48; /* rose */
97
+ --primary: #f43f5e; /* vibrant rose */
98
+ --primary-hover: #e11d48;/* deep rose */
99
+ --ghost: #fff1f2;
100
+ --ghost-hover: #ffe4e6;
101
+ }
102
+
103
+ /* Slate theme (neutral) */
104
+ html[data-theme="slate"] {
105
+ --bg: #f1f5f9; /* light slate */
106
+ --bg-soft: #e2e8f0; /* soft slate */
107
+ --card: #ffffff;
108
+ --text: #0f172a;
109
+ --muted: #64748b; /* slate */
110
+ --primary: #64748b; /* neutral accent */
111
+ --primary-hover: #475569;/* deeper slate */
112
+ --ghost: #eceff4;
113
+ --ghost-hover: #e1e6ee;
114
+ }
115
+
116
+ /* Header theme selector styling */
117
+ .theme-select {
118
+ padding: 0.5rem 0.75rem;
119
+ border-radius: 10px;
120
+ border: 1px solid var(--ghost-hover);
121
+ background: var(--ghost);
122
+ color: var(--text);
123
+ }
124
+
125
+ :root {
126
+ /* Fresh, vibrant palette */
127
+ --bg: #0b1020; /* deep navy */
128
+ --bg-soft: #10172a; /* softer navy */
129
+ --card: #ffffff; /* cards on light theme */
130
+ --text: #0e1a2b; /* dark text on light surfaces */
131
+ --muted: #64748b; /* slate */
132
+ --primary: #7c3aed; /* purple */
133
+ --primary-hover: #6d28d9;/* darker purple */
134
+ --ghost: #f1f5f9; /* light slate */
135
+ --ghost-hover: #e2e8f0; /* hover slate */
136
+ }
137
+
138
+ html[data-theme="dark"] {
139
+ --bg: #0b1020; /* deep navy */
140
+ --bg-soft: #10172a; /* softer navy */
141
+ --card: #0b1220; /* dark cards */
142
+ --text: #e2e8f0; /* light text */
143
+ --muted: #94a3b8; /* slate-muted */
144
+ --ghost: #0f172a; /* ghost dark */
145
+ --ghost-hover: #1f2937; /* ghost hover dark */
146
+ }
147
+
148
+ .header-actions {
149
+ margin-top: 0.5rem;
150
+ }
151
+
152
+ /* Utility: visually hidden (for sr-only labels) */
153
+ .sr-only {
154
+ position: absolute;
155
+ width: 1px;
156
+ height: 1px;
157
+ padding: 0;
158
+ margin: -1px;
159
+ overflow: hidden;
160
+ clip: rect(0, 0, 0, 0);
161
+ white-space: nowrap;
162
+ border: 0;
163
+ }
164
+
165
+ /* Adjust body gradient to respect theme */
166
+ body {
167
+ background: linear-gradient(135deg, var(--bg) 0%, var(--bg-soft) 100%);
168
+ }
169
+
170
+ .header {
171
+ width: 100%;
172
+ max-width: 1100px;
173
+ padding: 2rem 1rem 0.5rem 1rem;
174
+ margin: 0 auto;
175
+ color: #fff;
176
+ display: flex;
177
+ flex-wrap: wrap;
178
+ align-items: center;
179
+ justify-content: center;
180
+ text-align: center;
181
+ gap: 0.75rem 1rem; /* row/column gap for wrap */
182
+ }
183
+ /* Arrange brand, tagline, and actions for better UX */
184
+ .brand { order: 0; }
185
+ .tagline { order: 0; text-align: center; margin: 0.5rem auto; }
186
+ .header-actions { order: 0; margin: 0.5rem auto; }
187
+
188
+ @media (min-width: 768px) {
189
+ .header {
190
+ align-items: center;
191
+ justify-content: center;
192
+ text-align: center;
193
+ }
194
+ .header-actions {
195
+ margin: 0 auto;
196
+ order: 0;
197
+ justify-content: center;
198
+ align-items: center;
199
+ }
200
+ }
201
+ /* Prevent overflow of actions on small screens */
202
+ .header-actions {
203
+ display: flex;
204
+ flex-wrap: wrap;
205
+ gap: 0.5rem;
206
+ }
207
+
208
+ /* Improve status pill semantics and visibility */
209
+ .status {
210
+ padding: 0.35rem 0.6rem;
211
+ border-radius: 999px;
212
+ font-size: 0.85rem;
213
+ background-color: var(--ghost);
214
+ color: var(--text);
215
+ }
216
+ .brand {
217
+ display: flex;
218
+ align-items: center;
219
+ gap: 0.75rem;
220
+ font-size: 2.25rem; /* more prominent */
221
+ font-weight: 800;
222
+ }
223
+
224
+ .brand-link {
225
+ display: inline-flex;
226
+ align-items: center;
227
+ gap: 0.75rem;
228
+ text-decoration: none;
229
+ color: inherit;
230
+ }
231
+
232
+ .brand-text {
233
+ letter-spacing: 0.15px; /* slightly tighter for script fonts */
234
+ color: var(--text); /* adapt to theme for proper contrast */
235
+ text-shadow: none;
236
+ font-family: cursive; /* handwriting-style via generic cursive fallback */
237
+ }
238
+
239
+ html[data-theme="dark"] .brand-text {
240
+ text-shadow: 0 1px 2px rgba(0,0,0,0.25);
241
+ }
242
+
243
+ .brand-logo {
244
+ width: 44px;
245
+ height: 44px;
246
+ border-radius: 10px;
247
+ box-shadow: 0 6px 16px rgba(0,0,0,0.18);
248
+ }
249
+
250
+ @media (max-width: 640px) {
251
+ .brand {
252
+ font-size: 1.8rem;
253
+ }
254
+ .brand-logo {
255
+ width: 36px;
256
+ height: 36px;
257
+ }
258
+ }
259
+ .tagline {
260
+ flex: 1 1 100%; /* occupy a full row under the brand for perfect placement */
261
+ font-size: 1.05rem;
262
+ line-height: 1.5;
263
+ letter-spacing: 0.2px;
264
+ color: var(--muted); /* theme-aware secondary text for better visibility */
265
+ margin-top: 0.25rem;
266
+ text-shadow: none;
267
+ }
268
+
269
+ html[data-theme="dark"] .tagline {
270
+ color: var(--muted);
271
+ text-shadow: 0 1px 1.5px rgba(0,0,0,0.25); /* subtle lift on dark background */
272
+ }
273
+
274
+ .container {
275
+ background-color: var(--card);
276
+ padding: 1.5rem;
277
+ border-radius: 14px;
278
+ box-shadow: 0 10px 30px rgba(0, 0, 0, 0.15);
279
+ width: 100%;
280
+ max-width: 1100px;
281
+ margin: 1rem auto;
282
+ }
283
+
284
+ .grid {
285
+ display: grid;
286
+ grid-template-columns: 1fr 1fr;
287
+ gap: 1.25rem;
288
+ align-items: stretch; /* ensure panels have equal height for similar dimensions */
289
+ justify-items: stretch;
290
+ }
291
+
292
+ .grid > .panel {
293
+ height: 100%;
294
+ }
295
+
296
+ .panel {
297
+ background: #fff;
298
+ border: 1px solid #e5e7eb;
299
+ border-radius: 12px;
300
+ padding: 1rem;
301
+ display: flex; /* ensure inner elements are placed correctly */
302
+ flex-direction: column; /* stack content in order */
303
+ gap: 0.75rem; /* balanced spacing between children */
304
+ }
305
+
306
+ .panel > * { /* normalize child spacing */
307
+ margin: 0;
308
+ }
309
+
310
+ /* keep header layout consistent within the panel */
311
+ .panel-header {
312
+ display: flex;
313
+ align-items: center;
314
+ justify-content: space-between;
315
+ margin-bottom: 0.5rem;
316
+ }
317
+
318
+ .panel-actions {
319
+ display: flex;
320
+ gap: 0.5rem;
321
+ }
322
+
323
+ .label {
324
+ display: block;
325
+ font-size: 0.9rem;
326
+ color: var(--muted);
327
+ margin-bottom: 0.5rem;
328
+ }
329
+
330
+ /* Ensure consistent sizing and prevent overflow misalignment */
331
+ *, *::before, *::after {
332
+ box-sizing: border-box;
333
+ }
334
+
335
+ /* Prevent panels and form controls from exceeding their containers */
336
+ .panel, textarea, select {
337
+ max-width: 100%;
338
+ }
339
+
340
+ /* Ensure textarea aligns properly within its panel */
341
+ textarea {
342
+ display: block;
343
+ }
344
+
345
+ /* Hide any accidental overflow from internal content */
346
+ .panel {
347
+ overflow: hidden;
348
+ }
349
+
350
+ textarea {
351
+ width: 100%;
352
+ padding: 0.75rem 1rem;
353
+ border: 1px solid #e5e7eb;
354
+ border-radius: 10px; /* restore rounded corners */
355
+ margin-bottom: 1rem;
356
+ font-size: 1rem;
357
+ resize: vertical;
358
+ }
359
+
360
+ .controls {
361
+ display: block;
362
+ }
363
+
364
+ .controls .control { /* full-width block and natural spacing */
365
+ width: 100%;
366
+ margin-bottom: 1rem;
367
+ }
368
+ .control select {
369
+ width: 100%;
370
+ padding: 0.65rem 0.75rem;
371
+ border-radius: 10px;
372
+ border: 1px solid #e5e7eb;
373
+ font-size: 1rem;
374
+ background-color: #fff;
375
+ }
376
+
377
+ .toggle {
378
+ display: flex;
379
+ flex-direction: column;
380
+ align-items: flex-start;
381
+ }
382
+
383
+ .checkbox-label {
384
+ display: inline-flex;
385
+ align-items: center;
386
+ gap: 0.5rem;
387
+ user-select: none;
388
+ }
389
+
390
+ .hint {
391
+ color: var(--muted);
392
+ }
393
+
394
+ .actions {
395
+ display: flex;
396
+ gap: 0.5rem;
397
+ }
398
+
399
+ button.primary {
400
+ padding: 0.9rem 1rem;
401
+ border-radius: 10px;
402
+ border: none;
403
+ font-size: 1rem;
404
+ background-color: var(--primary);
405
+ color: #fff;
406
+ cursor: pointer;
407
+ }
408
+
409
+ button.primary:hover {
410
+ background-color: var(--primary-hover);
411
+ }
412
+
413
+ button.primary:disabled {
414
+ opacity: 0.7;
415
+ cursor: not-allowed;
416
+ }
417
+
418
+ button.ghost {
419
+ padding: 0.9rem 1rem;
420
+ border-radius: 10px;
421
+ border: 1px solid #e5e7eb;
422
+ font-size: 1rem;
423
+ background-color: var(--ghost);
424
+ color: var(--text);
425
+ cursor: pointer;
426
+ }
427
+
428
+ button.ghost:hover {
429
+ background-color: var(--ghost-hover);
430
+ }
431
+
432
+ .output {
433
+ padding: 1rem;
434
+ background-color: #f9fafb;
435
+ border-radius: 10px;
436
+ min-height: 120px;
437
+ border: 1px solid #e5e7eb;
438
+ }
439
+
440
+ .result-list {
441
+ margin: 0;
442
+ padding-left: 1.25rem;
443
+ }
444
+
445
+ .footer {
446
+ width: 100%;
447
+ max-width: 1100px;
448
+ text-align: right;
449
+ color: #cbd5e1;
450
+ padding: 0.5rem 1rem 1.5rem 1rem;
451
+ }
452
+
453
+ @media (max-width: 900px) {
454
+ .grid {
455
+ grid-template-columns: 1fr;
456
+ }
457
+ .actions {
458
+ flex-wrap: wrap;
459
+ }
460
+ }
461
+
462
+ /* Table styles for batch alignment */
463
+ .result-table {
464
+ width: 100%;
465
+ border-collapse: collapse;
466
+ background: #fff;
467
+ }
468
+ .result-table th, .result-table td {
469
+ border: 1px solid #e5e7eb;
470
+ padding: 0.5rem 0.6rem;
471
+ vertical-align: top;
472
+ }
473
+ .result-table th {
474
+ background: #f3f4f6;
475
+ text-align: left;
476
+ }
477
+ .result-table tr:nth-child(even) td {
478
+ background: #fafafa;
479
+ }
480
+
481
+ /* Smooth theme transitions */
482
+ html, body, .container, .panel, textarea, select, button, .output {
483
+ transition: background-color 0.2s ease, color 0.2s ease, border-color 0.2s ease, box-shadow 0.2s ease;
484
+ }
485
+
486
+ /* Elevated panel hover for subtle depth */
487
+ .panel:hover {
488
+ box-shadow: 0 6px 20px rgba(0, 0, 0, 0.12);
489
+ }
490
+
491
+ /* Improved focus visibility and accessibility */
492
+ textarea:focus-visible, select:focus-visible, button:focus-visible {
493
+ outline: 3px solid rgba(37, 99, 235, 0.35);
494
+ outline-offset: 2px;
495
+ border-color: var(--primary);
496
+ box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.15);
497
+ }
498
+
499
+ /* Harmonize output surface with theme variables */
500
+ .output {
501
+ background-color: var(--ghost);
502
+ color: var(--text);
503
+ }
504
+
505
+ /* Button hover and active subtle animations */
506
+ button.primary:hover, button.ghost:hover {
507
+ transform: translateY(-1px);
508
+ }
509
+ button.primary:active, button.ghost:active {
510
+ transform: translateY(0);
511
+ }
512
+