STTR commited on
Commit
a29fadd
·
1 Parent(s): 2ceddcf

Switch to Flask with custom dark theme UI

Browse files
Dockerfile ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM python:3.10-slim
2
+
3
+ WORKDIR /app
4
+
5
+ # Install system dependencies
6
+ RUN apt-get update && apt-get install -y \
7
+ git \
8
+ && rm -rf /var/lib/apt/lists/*
9
+
10
+ # Copy requirements
11
+ COPY requirements.txt .
12
+ RUN pip install --no-cache-dir -r requirements.txt
13
+
14
+ # Copy app files
15
+ COPY . .
16
+
17
+ # Expose port
18
+ EXPOSE 7860
19
+
20
+ # Run Flask app
21
+ CMD ["python", "app.py"]
README.md CHANGED
@@ -3,80 +3,24 @@ title: Instant Translat - AI Voice Translation
3
  emoji: 🌍
4
  colorFrom: purple
5
  colorTo: blue
6
- sdk: gradio
7
- sdk_version: "4.44.0"
8
- app_file: app.py
9
  pinned: true
10
  license: mit
11
- hardware: t4-small
12
  ---
13
 
14
  # 🌍 Instant Translat - AI Voice Translation
15
 
16
- **Real-time voice translation with AI - 200+ languages including Moroccan Darija**
17
 
18
- ## Features
 
 
 
 
 
19
 
20
- - 🎤 **Speech-to-Text** - SeamlessM4T v2 Large (101 languages)
21
- - 🌍 **Translation** - NLLB-200 (200 languages + Moroccan Darija)
22
- - 🔊 **Text-to-Speech** - Fish Audio S1 (Natural voice)
23
- - 🎭 **Voice Cloning** - Hear translation in your own voice!
24
- - 🧠 **Smart Mode** - Auto language detection
25
-
26
- ## 🌍 Supported Languages
27
-
28
- - 🇲🇦 **Moroccan Arabic (Darija)** - الدارجة المغربية
29
- - 🇸🇦 Arabic (MSA)
30
- - 🇫🇷 French
31
- - 🇬🇧 English
32
- - 🇪🇸 Spanish
33
- - 🇩🇪 German
34
- - 🇮🇹 Italian
35
- - 🇵🇹 Portuguese
36
- - 🇨🇳 Chinese
37
- - 🇯🇵 Japanese
38
- - 🇰🇷 Korean
39
- - 🇷🇺 Russian
40
- - And 190+ more languages!
41
-
42
- ## 🎯 How to Use
43
-
44
- 1. **Select Languages**: Choose your source and target languages
45
- 2. **Record**: Click the microphone button and speak clearly
46
- 3. **Translate**: Click "Translate" button
47
- 4. **Listen**: Hear the translation with natural voice
48
- 5. **Voice Clone**: Enable to hear translation in your own voice!
49
-
50
- ## 🔧 Technology
51
-
52
- - **STT**: Meta's SeamlessM4T v2 Large
53
- - **Translation**: Meta's NLLB-200
54
- - **TTS**: Fish Audio S1
55
- - **Voice Cloning**: Fish Audio API
56
- - **Framework**: Gradio + PyTorch
57
-
58
- ## 🔒 Privacy & Security
59
-
60
- - ✅ No data stored
61
- - ✅ Real-time processing
62
- - ✅ Secure API calls
63
- - ✅ Open source
64
-
65
- ## 📱 Use Cases
66
-
67
- - 🗣️ Real-time conversations
68
- - 📚 Language learning
69
- - 🌐 Travel assistance
70
- - 💼 Business meetings
71
- - 🎓 Education
72
-
73
- ## 🚀 Coming Soon
74
-
75
- - 💳 Premium features with Apple Pay & Google Pay
76
- - 📱 Mobile app (iOS & Android)
77
- - 🎯 More languages
78
- - 🔊 More voice options
79
-
80
- ---
81
-
82
- **Made with ❤️ using Meta AI models**
 
3
  emoji: 🌍
4
  colorFrom: purple
5
  colorTo: blue
6
+ sdk: docker
 
 
7
  pinned: true
8
  license: mit
 
9
  ---
10
 
11
  # 🌍 Instant Translat - AI Voice Translation
12
 
13
+ **Real-time voice translation with beautiful dark theme UI**
14
 
15
+ ## Features
16
+ - 🎤 Speech-to-Text (SeamlessM4T v2 Large)
17
+ - 🌍 Translation (NLLB-200 - 200 languages + Darija)
18
+ - 🔊 Text-to-Speech (Fish Audio S1)
19
+ - 🎭 Voice Cloning
20
+ - 🌙 Beautiful Dark Theme UI
21
 
22
+ ## Technology
23
+ - Flask backend
24
+ - Custom dark theme interface
25
+ - Meta AI models
26
+ - Fish Audio TTS
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
app.py CHANGED
@@ -1,4 +1,4 @@
1
- import gradio as gr
2
  from transformers import (
3
  AutoProcessor,
4
  SeamlessM4Tv2ForSpeechToText,
@@ -7,9 +7,13 @@ from transformers import (
7
  )
8
  import torch
9
  import numpy as np
 
 
10
  import requests
11
  import os
12
 
 
 
13
  # ============================================================
14
  # Device Setup
15
  # ============================================================
@@ -34,62 +38,42 @@ nllb_model = AutoModelForSeq2SeqLM.from_pretrained(NLLB_MODEL)
34
  nllb_model = nllb_model.to(device).eval()
35
  print("✅ NLLB-200 loaded!")
36
 
37
- # ============================================================
38
- # Language Codes
39
- # ============================================================
40
- NLLB_LANGS = {
41
- "🇲🇦 Moroccan Arabic (Darija)": "ary_Arab",
42
- "🇸🇦 Arabic": "arb_Arab",
43
- "🇫🇷 French": "fra_Latn",
44
- "🇬🇧 English": "eng_Latn",
45
- "🇪🇸 Spanish": "spa_Latn",
46
- "🇩🇪 German": "deu_Latn",
47
- "🇮🇹 Italian": "ita_Latn",
48
- "🇵🇹 Portuguese": "por_Latn",
49
- "🇨🇳 Chinese": "zho_Hans",
50
- "🇯🇵 Japanese": "jpn_Jpan",
51
- "🇰🇷 Korean": "kor_Hang",
52
- "🇷🇺 Russian": "rus_Cyrl",
53
- }
54
 
55
- STT_LANGS = {
56
- "🇲🇦 Moroccan Arabic (Darija)": "arb",
57
- "🇸🇦 Arabic": "arb",
58
- "🇫🇷 French": "fra",
59
- "🇬🇧 English": "eng",
60
- "🇪🇸 Spanish": "spa",
61
- "🇩🇪 German": "deu",
62
- "🇮🇹 Italian": "ita",
63
- "🇵🇹 Portuguese": "por",
64
- "🇨🇳 Chinese": "cmn",
65
- "🇯🇵 Japanese": "jpn",
66
- "🇰🇷 Korean": "kor",
67
- "🇷🇺 Russian": "rus",
68
  }
69
 
70
- FISH_AUDIO_API_KEY = os.environ.get('FISH_AUDIO_API_KEY', '')
71
-
72
- # ============================================================
73
- # Functions
74
- # ============================================================
75
 
76
- def translate_audio(audio, source_lang, target_lang, enable_voice_clone):
77
- """Complete translation pipeline"""
78
- if audio is None:
79
- return None, "❌ Please record audio first"
80
-
81
  try:
82
- # 1. STT
83
- if isinstance(audio, tuple):
84
- sample_rate, audio_data = audio
85
- audio_data = audio_data.astype(np.float32)
86
- if np.abs(audio_data).max() > 1.0:
87
- audio_data = audio_data / 32768.0
88
- else:
89
- return None, "❌ Invalid audio format"
90
-
91
- src_code = STT_LANGS.get(source_lang, "eng")
92
-
 
 
 
 
 
 
93
  inputs = stt_processor(
94
  audios=audio_data,
95
  sampling_rate=sample_rate,
@@ -105,9 +89,9 @@ def translate_audio(audio, source_lang, target_lang, enable_voice_clone):
105
 
106
  transcript = stt_processor.decode(output_tokens[0].tolist(), skip_special_tokens=True)
107
 
108
- # 2. Translation
109
- src_nllb = NLLB_LANGS.get(source_lang, "eng_Latn")
110
- tgt_nllb = NLLB_LANGS.get(target_lang, "fra_Latn")
111
 
112
  nllb_tokenizer.src_lang = src_nllb
113
  inputs = nllb_tokenizer(transcript, return_tensors="pt", padding=True, truncation=True, max_length=512).to(device)
@@ -124,242 +108,51 @@ def translate_audio(audio, source_lang, target_lang, enable_voice_clone):
124
 
125
  translation = nllb_tokenizer.decode(outputs[0], skip_special_tokens=True)
126
 
127
- # 3. TTS
128
- tts_audio = None
129
  if FISH_AUDIO_API_KEY:
130
- tts_audio = generate_tts(translation, enable_voice_clone, audio if enable_voice_clone else None)
131
-
132
- result_text = f"""
133
- <div style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); padding: 20px; border-radius: 12px; margin: 10px 0;">
134
- <h3 style="color: white; margin: 0 0 10px 0;">🎤 {source_lang}</h3>
135
- <p style="color: white; font-size: 1.1em; margin: 0;">{transcript}</p>
136
- </div>
137
-
138
- <div style="background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%); padding: 20px; border-radius: 12px; margin: 10px 0;">
139
- <h3 style="color: white; margin: 0 0 10px 0;">🌍 {target_lang}</h3>
140
- <p style="color: white; font-size: 1.1em; margin: 0;">{translation}</p>
141
- </div>
142
- """
143
-
144
- return tts_audio, result_text
145
 
146
  except Exception as e:
147
- return None, f"❌ Error: {str(e)}"
148
 
149
- def generate_tts(text, clone_voice=False, reference_audio=None):
150
  """Generate TTS using Fish Audio"""
151
  if not FISH_AUDIO_API_KEY:
152
  return None
153
 
154
  try:
155
  headers = {'Authorization': f'Bearer {FISH_AUDIO_API_KEY}'}
156
-
157
- if clone_voice and reference_audio:
158
- import tempfile
159
- import scipy.io.wavfile as wavfile
160
-
161
- with tempfile.NamedTemporaryFile(suffix='.wav', delete=False) as f:
162
- wavfile.write(f.name, reference_audio[0], reference_audio[1])
163
- audio_path = f.name
164
-
165
- with open(audio_path, 'rb') as f:
166
- files = {'reference_audio': ('ref.wav', f.read(), 'audio/wav')}
167
-
168
- data = {
169
- 'text': text,
170
- 'format': 'mp3',
171
- 'mp3_bitrate': '192',
172
- 'latency': 'balanced',
173
- 'normalize': 'true',
174
- }
175
-
176
- response = requests.post(
177
- 'https://api.fish.audio/v1/tts',
178
- headers=headers,
179
- files=files,
180
- data=data,
181
- timeout=120
182
- )
183
-
184
- os.remove(audio_path)
185
- else:
186
- payload = {
187
- 'text': text,
188
- 'format': 'mp3',
189
- 'mp3_bitrate': 192,
190
- }
191
-
192
- response = requests.post(
193
- 'https://api.fish.audio/v1/tts',
194
- headers=headers,
195
- json=payload,
196
- timeout=60
197
- )
198
 
199
  if response.status_code == 200:
200
- import tempfile
201
- with tempfile.NamedTemporaryFile(suffix='.mp3', delete=False) as f:
202
- f.write(response.content)
203
- return f.name
204
 
205
  return None
206
  except:
207
  return None
208
 
209
- # ============================================================
210
- # Custom CSS
211
- # ============================================================
212
-
213
- custom_css = """
214
- /* Modern Gradient Background */
215
- .gradio-container {
216
- background: linear-gradient(135deg, #667eea 0%, #764ba2 100%) !important;
217
- font-family: 'Inter', sans-serif;
218
- }
219
-
220
- /* Card Style */
221
- .contain {
222
- background: rgba(255, 255, 255, 0.95) !important;
223
- border-radius: 20px !important;
224
- box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3) !important;
225
- padding: 30px !important;
226
- backdrop-filter: blur(10px) !important;
227
- }
228
-
229
- /* Buttons */
230
- .primary {
231
- background: linear-gradient(135deg, #667eea 0%, #764ba2 100%) !important;
232
- border: none !important;
233
- border-radius: 12px !important;
234
- padding: 15px 30px !important;
235
- font-weight: 600 !important;
236
- font-size: 1.1em !important;
237
- transition: all 0.3s ease !important;
238
- }
239
-
240
- .primary:hover {
241
- transform: translateY(-2px) !important;
242
- box-shadow: 0 10px 25px rgba(102, 126, 234, 0.4) !important;
243
- }
244
-
245
- /* Input Fields */
246
- .input-audio, .dropdown {
247
- border-radius: 12px !important;
248
- border: 2px solid #e0e0e0 !important;
249
- }
250
-
251
- /* Headers */
252
- h1, h2, h3 {
253
- background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
254
- -webkit-background-clip: text;
255
- -webkit-text-fill-color: transparent;
256
- font-weight: 700;
257
- }
258
-
259
- /* Markdown Content */
260
- .markdown-text {
261
- line-height: 1.8;
262
- }
263
- """
264
-
265
- # ============================================================
266
- # Gradio Interface with Custom Theme
267
- # ============================================================
268
-
269
- theme = gr.themes.Soft(
270
- primary_hue="purple",
271
- secondary_hue="pink",
272
- neutral_hue="slate",
273
- font=gr.themes.GoogleFont("Inter"),
274
- ).set(
275
- button_primary_background_fill="*primary_500",
276
- button_primary_background_fill_hover="*primary_600",
277
- button_primary_text_color="white",
278
- )
279
-
280
- with gr.Blocks(theme=theme, css=custom_css, title="Instant Translat") as demo:
281
- gr.Markdown("""
282
- # 🌍 Instant Translat
283
- ### AI-Powered Voice Translation in 200+ Languages
284
-
285
- Translate your voice instantly with cutting-edge AI. Supports Moroccan Darija and 200+ languages!
286
- """)
287
-
288
- with gr.Row():
289
- with gr.Column(scale=1):
290
- gr.Markdown("### 🎤 Input")
291
-
292
- audio_input = gr.Audio(
293
- label="Record Your Voice",
294
- type="numpy",
295
- sources=["microphone"],
296
- elem_classes="input-audio"
297
- )
298
-
299
- with gr.Row():
300
- source_lang = gr.Dropdown(
301
- choices=list(NLLB_LANGS.keys()),
302
- value="🇲🇦 Moroccan Arabic (Darija)",
303
- label="🗣️ From",
304
- elem_classes="dropdown"
305
- )
306
-
307
- target_lang = gr.Dropdown(
308
- choices=list(NLLB_LANGS.keys()),
309
- value="🇬🇧 English",
310
- label="🎯 To",
311
- elem_classes="dropdown"
312
- )
313
-
314
- voice_clone = gr.Checkbox(
315
- label="🎭 Clone My Voice",
316
- value=True,
317
- info="Hear translation in your own voice"
318
- )
319
-
320
- translate_btn = gr.Button(
321
- "🌍 Translate Now",
322
- variant="primary",
323
- size="lg",
324
- elem_classes="primary"
325
- )
326
-
327
- with gr.Column(scale=1):
328
- gr.Markdown("### 🔊 Output")
329
-
330
- audio_output = gr.Audio(
331
- label="Translation Audio",
332
- type="filepath"
333
- )
334
-
335
- text_output = gr.HTML(label="Translation Text")
336
-
337
- translate_btn.click(
338
- translate_audio,
339
- inputs=[audio_input, source_lang, target_lang, voice_clone],
340
- outputs=[audio_output, text_output]
341
- )
342
-
343
- gr.Markdown("""
344
- ---
345
-
346
- ## ✨ Features
347
-
348
- - 🎤 **Speech Recognition** - Powered by Meta's SeamlessM4T v2 Large
349
- - 🌍 **Translation** - 200+ languages with NLLB-200
350
- - 🔊 **Natural Voice** - Fish Audio S1 TTS
351
- - 🎭 **Voice Cloning** - Hear translation in your voice
352
-
353
- ## 🌍 Popular Languages
354
-
355
- 🇲🇦 Moroccan Darija • 🇸🇦 Arabic • 🇫🇷 French • 🇬🇧 English • 🇪🇸 Spanish • 🇩🇪 German • 🇮🇹 Italian • 🇵🇹 Portuguese • 🇨🇳 Chinese • 🇯🇵 Japanese • 🇰🇷 Korean • 🇷🇺 Russian
356
-
357
- ---
358
-
359
- <div style="text-align: center; padding: 20px;">
360
- <p style="color: #666;">Made with ❤️ using Meta AI • Powered by HuggingFace</p>
361
- </div>
362
- """)
363
-
364
- if __name__ == "__main__":
365
- demo.launch()
 
1
+ from flask import Flask, render_template, request, jsonify
2
  from transformers import (
3
  AutoProcessor,
4
  SeamlessM4Tv2ForSpeechToText,
 
7
  )
8
  import torch
9
  import numpy as np
10
+ import base64
11
+ import io
12
  import requests
13
  import os
14
 
15
+ app = Flask(__name__)
16
+
17
  # ============================================================
18
  # Device Setup
19
  # ============================================================
 
38
  nllb_model = nllb_model.to(device).eval()
39
  print("✅ NLLB-200 loaded!")
40
 
41
+ # Fish Audio API
42
+ FISH_AUDIO_API_KEY = os.environ.get('FISH_AUDIO_API_KEY', '')
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
43
 
44
+ # Language mapping
45
+ LANG_MAP = {
46
+ 'ar-SA': ('arb', 'ary_Arab'),
47
+ 'fr-FR': ('fra', 'fra_Latn'),
48
+ 'en-US': ('eng', 'eng_Latn'),
49
+ 'es-ES': ('spa', 'spa_Latn'),
50
+ 'de-DE': ('deu', 'deu_Latn'),
 
 
 
 
 
 
51
  }
52
 
53
+ @app.route('/')
54
+ def index():
55
+ return render_template('index.html')
 
 
56
 
57
+ @app.route('/process_audio', methods=['POST'])
58
+ def process_audio():
 
 
 
59
  try:
60
+ data = request.json
61
+ audio_b64 = data.get('audio')
62
+ source_lang = data.get('source_lang', 'ar-SA')
63
+ target_lang = data.get('target_lang', 'en-US')
64
+
65
+ # Decode audio
66
+ audio_bytes = base64.b64decode(audio_b64)
67
+
68
+ # Convert to numpy
69
+ import scipy.io.wavfile as wavfile
70
+ sample_rate, audio_data = wavfile.read(io.BytesIO(audio_bytes))
71
+ audio_data = audio_data.astype(np.float32)
72
+ if np.abs(audio_data).max() > 1.0:
73
+ audio_data = audio_data / 32768.0
74
+
75
+ # STT
76
+ src_code = LANG_MAP.get(source_lang, ('eng', 'eng_Latn'))[0]
77
  inputs = stt_processor(
78
  audios=audio_data,
79
  sampling_rate=sample_rate,
 
89
 
90
  transcript = stt_processor.decode(output_tokens[0].tolist(), skip_special_tokens=True)
91
 
92
+ # Translation
93
+ src_nllb = LANG_MAP.get(source_lang, ('eng', 'eng_Latn'))[1]
94
+ tgt_nllb = LANG_MAP.get(target_lang, ('eng', 'eng_Latn'))[1]
95
 
96
  nllb_tokenizer.src_lang = src_nllb
97
  inputs = nllb_tokenizer(transcript, return_tensors="pt", padding=True, truncation=True, max_length=512).to(device)
 
108
 
109
  translation = nllb_tokenizer.decode(outputs[0], skip_special_tokens=True)
110
 
111
+ # TTS
112
+ tts_audio_b64 = None
113
  if FISH_AUDIO_API_KEY:
114
+ tts_audio = generate_tts(translation)
115
+ if tts_audio:
116
+ tts_audio_b64 = base64.b64encode(tts_audio).decode('utf-8')
117
+
118
+ return jsonify({
119
+ 'success': True,
120
+ 'original': transcript,
121
+ 'translation': translation,
122
+ 'audio': tts_audio_b64,
123
+ 'source_lang': source_lang,
124
+ 'target_lang': target_lang
125
+ })
 
 
 
126
 
127
  except Exception as e:
128
+ return jsonify({'success': False, 'error': str(e)}), 500
129
 
130
+ def generate_tts(text):
131
  """Generate TTS using Fish Audio"""
132
  if not FISH_AUDIO_API_KEY:
133
  return None
134
 
135
  try:
136
  headers = {'Authorization': f'Bearer {FISH_AUDIO_API_KEY}'}
137
+ payload = {
138
+ 'text': text,
139
+ 'format': 'mp3',
140
+ 'mp3_bitrate': 192,
141
+ }
142
+
143
+ response = requests.post(
144
+ 'https://api.fish.audio/v1/tts',
145
+ headers=headers,
146
+ json=payload,
147
+ timeout=60
148
+ )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
149
 
150
  if response.status_code == 200:
151
+ return response.content
 
 
 
152
 
153
  return None
154
  except:
155
  return None
156
 
157
+ if __name__ == '__main__':
158
+ app.run(host='0.0.0.0', port=7860)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
requirements.txt CHANGED
@@ -1,9 +1,7 @@
1
- transformers>=4.40.0
 
2
  torch>=2.0.0
3
- torchaudio
4
- sentencepiece
5
- protobuf
6
- gradio>=4.0.0
7
- numpy
8
  scipy
9
- accelerate
 
 
 
1
+ Flask==3.0.0
2
+ transformers>=4.30.0
3
  torch>=2.0.0
 
 
 
 
 
4
  scipy
5
+ numpy
6
+ requests
7
+ Werkzeug==3.0.0
static/enhanced.css ADDED
@@ -0,0 +1,620 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /* 🌑 BABEL x GROK - Absolute Dark Theme */
2
+ :root {
3
+ --bg-app: #000000;
4
+ --bg-sidebar: #000000;
5
+ --bg-main: #000000;
6
+ --bg-surface: #121212;
7
+ --bg-input: #1a1a1a;
8
+ --bg-input-hover: #222222;
9
+
10
+ --text-primary: #ffffff;
11
+ --text-secondary: #a3a3a3;
12
+ --text-tertiary: #525252;
13
+
14
+ --border-subtle: #27272a;
15
+ --border-active: #3f3f46;
16
+
17
+ --accent: #ffffff;
18
+ --accent-hover: #e5e5e5;
19
+
20
+ --recording: #ef4444;
21
+ --sidebar-width: 260px;
22
+ --input-max-width: 720px;
23
+ }
24
+
25
+ * {
26
+ box-sizing: border-box;
27
+ margin: 0;
28
+ padding: 0;
29
+ -webkit-tap-highlight-color: transparent;
30
+ }
31
+
32
+ body {
33
+ background-color: var(--bg-app);
34
+ color: var(--text-primary);
35
+ font-family: 'Inter', system-ui, -apple-system, sans-serif;
36
+ height: 100vh;
37
+ overflow: hidden;
38
+ }
39
+
40
+ /* 🏗️ LAYOUT GRID */
41
+ .app-layout {
42
+ display: flex;
43
+ height: 100vh;
44
+ width: 100vw;
45
+ }
46
+
47
+ /* ▌ SIDEBAR */
48
+ .grok-sidebar {
49
+ width: var(--sidebar-width);
50
+ background-color: var(--bg-sidebar);
51
+ border-right: 1px solid var(--border-subtle);
52
+ display: flex;
53
+ flex-direction: column;
54
+ padding: 16px;
55
+ flex-shrink: 0;
56
+ transition: transform 0.3s ease;
57
+ z-index: 50;
58
+ }
59
+
60
+ .sidebar-top {
61
+ display: flex;
62
+ justify-content: space-between;
63
+ align-items: center;
64
+ margin-bottom: 24px;
65
+ padding: 0 8px;
66
+ }
67
+
68
+ .sidebar-logo {
69
+ font-size: 1.5rem;
70
+ font-weight: 700;
71
+ color: var(--text-primary);
72
+ }
73
+
74
+ .new-chat {
75
+ background: transparent;
76
+ border: none;
77
+ color: var(--text-secondary);
78
+ font-size: 1.2rem;
79
+ cursor: pointer;
80
+ padding: 8px;
81
+ border-radius: 8px;
82
+ transition: all 0.2s;
83
+ }
84
+
85
+ .new-chat:hover {
86
+ background: var(--bg-surface);
87
+ color: var(--text-primary);
88
+ }
89
+
90
+ .sidebar-menu {
91
+ display: flex;
92
+ flex-direction: column;
93
+ gap: 4px;
94
+ flex: 1;
95
+ }
96
+
97
+ .menu-item {
98
+ display: flex;
99
+ align-items: center;
100
+ gap: 12px;
101
+ padding: 10px 12px;
102
+ border-radius: 8px;
103
+ color: var(--text-secondary);
104
+ cursor: pointer;
105
+ transition: all 0.2s;
106
+ font-size: 0.95rem;
107
+ font-weight: 500;
108
+ }
109
+
110
+ .menu-item:hover,
111
+ .menu-item.active {
112
+ background-color: var(--bg-surface);
113
+ color: var(--text-primary);
114
+ }
115
+
116
+ .menu-item i {
117
+ width: 20px;
118
+ text-align: center;
119
+ }
120
+
121
+ .sidebar-footer {
122
+ padding-top: 16px;
123
+ border-top: 1px solid var(--border-subtle);
124
+ }
125
+
126
+ .user-profile {
127
+ display: flex;
128
+ align-items: center;
129
+ gap: 10px;
130
+ padding: 8px;
131
+ border-radius: 8px;
132
+ cursor: pointer;
133
+ }
134
+
135
+ .user-profile:hover {
136
+ background-color: var(--bg-surface);
137
+ }
138
+
139
+ .avatar {
140
+ width: 32px;
141
+ height: 32px;
142
+ background: #333;
143
+ border-radius: 50%;
144
+ display: flex;
145
+ align-items: center;
146
+ justify-content: center;
147
+ font-size: 0.9rem;
148
+ font-weight: 600;
149
+ }
150
+
151
+ .info {
152
+ display: flex;
153
+ flex-direction: column;
154
+ }
155
+
156
+ .info .name {
157
+ font-size: 0.9rem;
158
+ font-weight: 600;
159
+ }
160
+
161
+ .info .status {
162
+ font-size: 0.75rem;
163
+ color: var(--text-secondary);
164
+ }
165
+
166
+ /* ▌ MAIN CONTENT */
167
+ .grok-main {
168
+ flex: 1;
169
+ display: flex;
170
+ flex-direction: column;
171
+ position: relative;
172
+ background-color: var(--bg-main);
173
+ }
174
+
175
+ .main-header {
176
+ height: 60px;
177
+ display: flex;
178
+ align-items: center;
179
+ justify-content: center;
180
+ border-bottom: 1px solid transparent;
181
+ /* Hidden unless scrolled */
182
+ }
183
+
184
+ .model-badge {
185
+ color: var(--text-secondary);
186
+ font-size: 0.9rem;
187
+ display: flex;
188
+ align-items: center;
189
+ gap: 6px;
190
+ opacity: 0.7;
191
+ }
192
+
193
+ /* CHAT STAGE */
194
+ .chat-stage {
195
+ flex: 1;
196
+ overflow-y: auto;
197
+ padding: 20px;
198
+ padding-bottom: 180px;
199
+ /* Space for input bar */
200
+ display: flex;
201
+ flex-direction: column;
202
+ align-items: center;
203
+ }
204
+
205
+ .chat-container {
206
+ width: 100%;
207
+ max-width: var(--input-max-width);
208
+ display: flex;
209
+ flex-direction: column;
210
+ gap: 20px;
211
+ }
212
+
213
+ /* EMPTY STATE */
214
+ .empty-state {
215
+ display: flex;
216
+ flex-direction: column;
217
+ align-items: center;
218
+ justify-content: center;
219
+ margin-top: 10vh;
220
+ animation: fadeIn 0.5s ease;
221
+ }
222
+
223
+ .grok-logo-hero {
224
+ font-size: 4rem;
225
+ font-weight: 700;
226
+ color: var(--text-primary);
227
+ margin-bottom: 16px;
228
+ letter-spacing: -0.04em;
229
+ }
230
+
231
+ .grok-hero-text {
232
+ color: var(--text-secondary);
233
+ font-size: 1.2rem;
234
+ }
235
+
236
+ /* 🟢 INPUT AREA (The "Grok" Bar) */
237
+ .grok-input-wrapper {
238
+ position: absolute;
239
+ bottom: 0;
240
+ left: 0;
241
+ right: 0;
242
+ padding: 24px;
243
+ padding-bottom: 40px;
244
+ display: flex;
245
+ flex-direction: column;
246
+ align-items: center;
247
+ gap: 16px;
248
+ background: linear-gradient(to top, var(--bg-main) 80%, transparent);
249
+ }
250
+
251
+ .grok-input-bar {
252
+ width: 100%;
253
+ max-width: var(--input-max-width);
254
+ background-color: var(--bg-input);
255
+ border-radius: 28px;
256
+ /* Pill shape */
257
+ padding: 12px 16px;
258
+ display: flex;
259
+ align-items: center;
260
+ gap: 16px;
261
+ box-shadow: 0 4px 20px rgba(0, 0, 0, 0.4);
262
+ border: 1px solid var(--border-subtle);
263
+ transition: all 0.2s;
264
+ min-height: 64px;
265
+ }
266
+
267
+ .grok-input-bar:focus-within {
268
+ border-color: var(--border-active);
269
+ background-color: var(--bg-input-hover);
270
+ }
271
+
272
+ .input-icon {
273
+ font-size: 1.2rem;
274
+ color: var(--text-secondary);
275
+ cursor: pointer;
276
+ width: 40px;
277
+ height: 40px;
278
+ display: flex;
279
+ align-items: center;
280
+ justify-content: center;
281
+ border-radius: 50%;
282
+ transition: all 0.2s;
283
+ }
284
+
285
+ .input-icon:hover {
286
+ background: rgba(255, 255, 255, 0.05);
287
+ color: var(--text-primary);
288
+ }
289
+
290
+ .input-status-area {
291
+ flex: 1;
292
+ display: flex;
293
+ flex-direction: column;
294
+ justify-content: center;
295
+ }
296
+
297
+ .status-text {
298
+ color: var(--text-secondary);
299
+ font-size: 1.1rem;
300
+ font-weight: 400;
301
+ }
302
+
303
+ /* ORB ACTION BUTTON */
304
+ .input-icon.action {
305
+ background-color: transparent;
306
+ color: var(--text-primary);
307
+ width: 48px;
308
+ height: 48px;
309
+ }
310
+
311
+ .input-icon.action:hover {
312
+ background-color: rgba(255, 255, 255, 0.1);
313
+ }
314
+
315
+ .input-icon.action.recording {
316
+ color: var(--recording);
317
+ animation: pulse-red 1.5s infinite;
318
+ }
319
+
320
+ /* PRO CHIPS (Below Input) */
321
+ .grok-chips {
322
+ display: flex;
323
+ align-items: center;
324
+ gap: 10px;
325
+ flex-wrap: wrap;
326
+ justify-content: center;
327
+ }
328
+
329
+ .chip-select-wrapper {
330
+ position: relative;
331
+ background: var(--bg-surface);
332
+ border-radius: 8px;
333
+ padding: 0 12px;
334
+ height: 36px;
335
+ display: flex;
336
+ align-items: center;
337
+ border: 1px solid var(--border-subtle);
338
+ min-width: 100px;
339
+ }
340
+
341
+ .chip-select-wrapper i {
342
+ font-size: 0.7rem;
343
+ color: var(--text-secondary);
344
+ pointer-events: none;
345
+ margin-left: 8px;
346
+ }
347
+
348
+ .chip-select {
349
+ appearance: none;
350
+ background: transparent;
351
+ border: none;
352
+ color: var(--text-primary);
353
+ font-size: 0.9rem;
354
+ font-family: inherit;
355
+ width: 100%;
356
+ cursor: pointer;
357
+ }
358
+
359
+ .chip-select option {
360
+ background: #121212;
361
+ color: white;
362
+ }
363
+
364
+ .chip-icon {
365
+ width: 36px;
366
+ height: 36px;
367
+ border-radius: 50%;
368
+ border: 1px solid var(--border-subtle);
369
+ background: var(--bg-surface);
370
+ color: var(--text-secondary);
371
+ display: flex;
372
+ align-items: center;
373
+ justify-content: center;
374
+ cursor: pointer;
375
+ font-size: 0.9rem;
376
+ }
377
+
378
+ .chip-pill {
379
+ height: 36px;
380
+ padding: 0 16px;
381
+ border-radius: 18px;
382
+ background: transparent;
383
+ border: 1px solid var(--border-subtle);
384
+ color: var(--text-secondary);
385
+ font-size: 0.9rem;
386
+ font-weight: 500;
387
+ cursor: pointer;
388
+ display: flex;
389
+ align-items: center;
390
+ gap: 8px;
391
+ transition: all 0.2s;
392
+ }
393
+
394
+ .chip-pill:hover {
395
+ border-color: var(--text-secondary);
396
+ color: var(--text-primary);
397
+ }
398
+
399
+ .chip-pill.active {
400
+ background: var(--text-primary);
401
+ color: var(--bg-app);
402
+ border-color: var(--text-primary);
403
+ }
404
+
405
+ .chip-divider {
406
+ width: 1px;
407
+ height: 20px;
408
+ background: var(--border-subtle);
409
+ margin: 0 8px;
410
+ }
411
+
412
+ /* MESSAGES */
413
+ .message-row {
414
+ display: flex;
415
+ width: 100%;
416
+ margin-bottom: 24px;
417
+ }
418
+
419
+ .user-row {
420
+ justify-content: flex-end;
421
+ }
422
+
423
+ .ai-row {
424
+ justify-content: flex-start;
425
+ }
426
+
427
+ .bubble {
428
+ max-width: 80%;
429
+ padding: 12px 18px;
430
+ border-radius: 18px;
431
+ font-size: 1rem;
432
+ line-height: 1.5;
433
+ }
434
+
435
+ .user-bubble {
436
+ background: var(--bg-surface);
437
+ color: var(--text-primary);
438
+ border: 1px solid var(--border-subtle);
439
+ }
440
+
441
+ .ai-bubble {
442
+ background: transparent;
443
+ color: var(--text-primary);
444
+ padding-left: 0;
445
+ }
446
+
447
+ /* BARS ANIMATION for Orb */
448
+ .bars {
449
+ display: flex;
450
+ align-items: center;
451
+ justify-content: center;
452
+ gap: 3px;
453
+ height: 20px;
454
+ }
455
+
456
+ .bar {
457
+ width: 3px;
458
+ height: 100%;
459
+ background: currentColor;
460
+ border-radius: 2px;
461
+ }
462
+
463
+ @keyframes pulse-red {
464
+ 0% {
465
+ transform: scale(1);
466
+ opacity: 1;
467
+ }
468
+
469
+ 50% {
470
+ transform: scale(1.1);
471
+ opacity: 0.8;
472
+ }
473
+
474
+ 100% {
475
+ transform: scale(1);
476
+ opacity: 1;
477
+ }
478
+ }
479
+
480
+ /* 📱 MOBILE RESPONSIVE */
481
+ @media (max-width: 768px) {
482
+ .grok-sidebar {
483
+ position: fixed;
484
+ left: -100%;
485
+ /* Hide */
486
+ height: 100%;
487
+ background: rgba(0, 0, 0, 0.95);
488
+ backdrop-filter: blur(10px);
489
+ }
490
+
491
+ .grok-sidebar.open {
492
+ left: 0;
493
+ }
494
+
495
+ .main-header {
496
+ justify-content: flex-start;
497
+ padding-left: 20px;
498
+ }
499
+
500
+ .grok-input-bar {
501
+ border-radius: 24px;
502
+ }
503
+
504
+ .chip-pill span {
505
+ display: none;
506
+ /* Icon only on mobile */
507
+ }
508
+ }
509
+ /* MODAL SETTINGS (Grok Dark) */
510
+ .modal {
511
+ position: fixed;
512
+ inset: 0;
513
+ background: rgba(0,0,0,0.8);
514
+ display: none; /* JS toggles display: flex */
515
+ align-items: center;
516
+ justify-content: center;
517
+ z-index: 1000;
518
+ }
519
+
520
+ .modal-content {
521
+ background: #121212;
522
+ border: 1px solid #333;
523
+ border-radius: 16px;
524
+ padding: 24px;
525
+ width: 90%;
526
+ max-width: 500px;
527
+ color: white;
528
+ position: relative;
529
+ box-shadow: 0 20px 50px rgba(0,0,0,0.5);
530
+ }
531
+
532
+ .modal h2 { margin-bottom: 20px; font-size: 1.5rem; }
533
+
534
+ .form-group { margin-bottom: 16px; }
535
+
536
+ .form-group label {
537
+ display: block;
538
+ margin-bottom: 8px;
539
+ color: #a3a3a3;
540
+ font-size: 0.9rem;
541
+ }
542
+
543
+ .form-control {
544
+ width: 100%;
545
+ background: #1a1a1a;
546
+ border: 1px solid #333;
547
+ padding: 12px;
548
+ border-radius: 8px;
549
+ color: white;
550
+ font-size: 1rem;
551
+ }
552
+
553
+ .form-control:focus {
554
+ border-color: white;
555
+ outline: none;
556
+ }
557
+
558
+ .save-btn {
559
+ width: 100%;
560
+ background: white;
561
+ color: black;
562
+ font-weight: 700;
563
+ padding: 12px;
564
+ border-radius: 8px;
565
+ border: none;
566
+ cursor: pointer;
567
+ margin-top: 16px;
568
+ }
569
+
570
+ .close-modal-btn {
571
+ position: absolute;
572
+ top: 16px;
573
+ right: 16px;
574
+ background: transparent;
575
+ border: none;
576
+ color: #666;
577
+ font-size: 1.2rem;
578
+ cursor: pointer;
579
+ }
580
+ .close-modal-btn:hover { color: white; }
581
+
582
+
583
+
584
+ /* MICROPHONE ORB BUTTON */
585
+ #record-btn {
586
+ width: 56px;
587
+ height: 56px;
588
+ border-radius: 50%;
589
+ background: linear-gradient(135deg, #1a1a1a, #2d2d2d);
590
+ border: 2px solid #444;
591
+ cursor: pointer;
592
+ transition: all 0.3s ease;
593
+ }
594
+
595
+ #record-btn i {
596
+ font-size: 1.4rem;
597
+ color: #ffffff;
598
+ }
599
+
600
+ #record-btn:hover {
601
+ background: linear-gradient(135deg, #2d2d2d, #3d3d3d);
602
+ border-color: #666;
603
+ transform: scale(1.05);
604
+ }
605
+
606
+ #record-btn.recording {
607
+ background: linear-gradient(135deg, #dc2626, #ef4444) !important;
608
+ border-color: #f87171 !important;
609
+ animation: pulse-record 1.5s infinite;
610
+ }
611
+
612
+ #record-btn.recording i {
613
+ color: white;
614
+ }
615
+
616
+ @keyframes pulse-record {
617
+ 0%, 100% { transform: scale(1); box-shadow: 0 0 0 0 rgba(239, 68, 68, 0.4); }
618
+ 50% { transform: scale(1.05); box-shadow: 0 0 20px 5px rgba(239, 68, 68, 0.3); }
619
+ }
620
+
static/enhanced.js ADDED
@@ -0,0 +1,217 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // ENHANCED UI - Darija Cards + Audio Player Manager
2
+
3
+ // Parse Darija text into structured cards
4
+ function parseDarijaToCards(text) {
5
+ // Check if text contains Darija labels
6
+ const hasArabicLabel = text.includes('🇲🇦') || text.includes('الدارجة');
7
+ const hasArabiziLabel = text.includes('🔤') || text.includes('Darija (Arabizi)');
8
+
9
+ if (!hasArabicLabel && !hasArabiziLabel) {
10
+ // Regular text, return as-is
11
+ return `<div class="regular-text">${text.replace(/\n/g, '<br>')}</div>`;
12
+ }
13
+
14
+ // Split by the labels
15
+ const parts = text.split(/(?=🇲🇦)|(?=🔤)/);
16
+
17
+ let html = '<div class="darija-container">';
18
+
19
+ for (const part of parts) {
20
+ const trimmed = part.trim();
21
+ if (!trimmed) continue;
22
+
23
+ if (trimmed.startsWith('🇲🇦') || trimmed.includes('الدارجة')) {
24
+ // Arabic card
25
+ const content = trimmed
26
+ .replace(/🇲🇦.*?\n/, '')
27
+ .replace(/الدارجة.*?\n/, '')
28
+ .trim();
29
+
30
+ html += `
31
+ <div class="darija-card arabic">
32
+ <div class="darija-card-header">
33
+ <span class="emoji">🇲🇦</span>
34
+ <span>الدارجة (Arabic)</span>
35
+ </div>
36
+ <div class="darija-card-content">${content}</div>
37
+ </div>
38
+ `;
39
+ } else if (trimmed.startsWith('🔤') || trimmed.includes('Darija (Arabizi)')) {
40
+ // Arabizi card
41
+ const content = trimmed
42
+ .replace(/🔤.*?\n/, '')
43
+ .replace(/Darija.*?\n/, '')
44
+ .trim();
45
+
46
+ html += `
47
+ <div class="darija-card arabizi">
48
+ <div class="darija-card-header">
49
+ <span class="emoji">🔤</span>
50
+ <span>Darija (Arabizi)</span>
51
+ </div>
52
+ <div class="darija-card-content">${content}</div>
53
+ </div>
54
+ `;
55
+ }
56
+ }
57
+
58
+ html += '</div>';
59
+ return html;
60
+ }
61
+
62
+ // Audio Player Manager
63
+ class AudioPlayer {
64
+ constructor() {
65
+ // Create a DEDICATED audio element for enhanced player
66
+ this.audio = new Audio();
67
+ this.audio.id = 'enhanced-audio-element';
68
+
69
+ this.container = null;
70
+ this.playBtn = null;
71
+ this.waveform = null;
72
+ this.timeDisplay = null;
73
+ this.isPlaying = false;
74
+ this.currentAudioSrc = null;
75
+
76
+ this.createPlayer();
77
+ this.setupEventListeners();
78
+ }
79
+
80
+ createPlayer() {
81
+ // Create player HTML
82
+ const playerHTML = `
83
+ <div class="audio-player-container" id="enhanced-audio-player" style="display: none;">
84
+ <div class="audio-player">
85
+ <div class="player-controls">
86
+ <button class="play-btn" id="player-play-btn">
87
+ <i class="fa-solid fa-play"></i>
88
+ </button>
89
+ <div class="waveform" id="player-waveform">
90
+ <div class="wave-bar"></div>
91
+ <div class="wave-bar"></div>
92
+ <div class="wave-bar"></div>
93
+ <div class="wave-bar"></div>
94
+ <div class="wave-bar"></div>
95
+ </div>
96
+ </div>
97
+ <div class="player-actions">
98
+ <button class="action-btn" id="player-download-btn">
99
+ <i class="fa-solid fa-download"></i>
100
+ <span>Download</span>
101
+ </button>
102
+ <button class="action-btn" id="player-close-btn">
103
+ <i class="fa-solid fa-xmark"></i>
104
+ <span>Close</span>
105
+ </button>
106
+ </div>
107
+ <div class="time-display" id="player-time-display">0:00 / 0:00</div>
108
+ </div>
109
+ </div>
110
+ `;
111
+
112
+ // Insert into DOM
113
+ document.body.insertAdjacentHTML('beforeend', playerHTML);
114
+
115
+ // Get references
116
+ this.container = document.getElementById('enhanced-audio-player');
117
+ this.playBtn = document.getElementById('player-play-btn');
118
+ this.waveform = document.getElementById('player-waveform');
119
+ this.timeDisplay = document.getElementById('player-time-display');
120
+
121
+ document.getElementById('player-download-btn').onclick = () => this.download();
122
+ document.getElementById('player-close-btn').onclick = () => this.hide();
123
+ }
124
+
125
+ setupEventListeners() {
126
+ this.playBtn.onclick = () => this.togglePlay();
127
+
128
+ this.audio.addEventListener('play', () => {
129
+ this.isPlaying = true;
130
+ this.playBtn.innerHTML = '<i class="fa-solid fa-pause"></i>';
131
+ this.waveform.classList.add('playing');
132
+ });
133
+
134
+ this.audio.addEventListener('pause', () => {
135
+ this.isPlaying = false;
136
+ this.playBtn.innerHTML = '<i class="fa-solid fa-play"></i>';
137
+ this.waveform.classList.remove('playing');
138
+ });
139
+
140
+ this.audio.addEventListener('ended', () => {
141
+ this.isPlaying = false;
142
+ this.playBtn.innerHTML = '<i class="fa-solid fa-play"></i>';
143
+ this.waveform.classList.remove('playing');
144
+ });
145
+
146
+ this.audio.addEventListener('timeupdate', () => {
147
+ this.updateTimeDisplay();
148
+ });
149
+ }
150
+
151
+ show(audioSrc) {
152
+ if (audioSrc) {
153
+ this.currentAudioSrc = audioSrc;
154
+ this.audio.src = audioSrc;
155
+ }
156
+
157
+ this.container.style.display = 'block';
158
+ setTimeout(() => {
159
+ this.container.classList.add('visible');
160
+ }, 10);
161
+
162
+ // Auto-play
163
+ this.audio.play().catch(err => {
164
+ console.log('Auto-play blocked:', err);
165
+ });
166
+ }
167
+
168
+ hide() {
169
+ this.container.classList.remove('visible');
170
+ setTimeout(() => {
171
+ this.container.style.display = 'none';
172
+ }, 300);
173
+ this.audio.pause();
174
+ }
175
+
176
+ togglePlay() {
177
+ if (this.isPlaying) {
178
+ this.audio.pause();
179
+ } else {
180
+ this.audio.play();
181
+ }
182
+ }
183
+
184
+ updateTimeDisplay() {
185
+ const current = this.formatTime(this.audio.currentTime);
186
+ const duration = this.formatTime(this.audio.duration || 0);
187
+ this.timeDisplay.textContent = `${current} / ${duration}`;
188
+ }
189
+
190
+ formatTime(seconds) {
191
+ if (isNaN(seconds)) return '0:00';
192
+ const mins = Math.floor(seconds / 60);
193
+ const secs = Math.floor(seconds % 60);
194
+ return `${mins}:${secs.toString().padStart(2, '0')}`;
195
+ }
196
+
197
+ download() {
198
+ if (!this.currentAudioSrc) return;
199
+
200
+ const a = document.createElement('a');
201
+ a.href = this.currentAudioSrc;
202
+ a.download = `translation_${Date.now()}.mp3`;
203
+ document.body.appendChild(a);
204
+ a.click();
205
+ document.body.removeChild(a);
206
+ }
207
+ }
208
+
209
+ // Initialize on load
210
+ window.enhancedAudioPlayer = null;
211
+
212
+ document.addEventListener('DOMContentLoaded', () => {
213
+ window.enhancedAudioPlayer = new AudioPlayer();
214
+ });
215
+
216
+ // Export functions
217
+ window.parseDarijaToCards = parseDarijaToCards;
static/favicon.ico ADDED
static/manifest.json ADDED
@@ -0,0 +1,17 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "Babel - Traducteur Vocal Instantané",
3
+ "short_name": "Babel",
4
+ "description": "Traduction vocale instantanée avec IA. Parlez, et le monde vous comprend.",
5
+ "start_url": "/",
6
+ "display": "standalone",
7
+ "background_color": "#0a0a0b",
8
+ "theme_color": "#0a0a0b",
9
+ "orientation": "portrait",
10
+ "icons": [
11
+ {
12
+ "src": "/static/favicon.ico",
13
+ "sizes": "64x64",
14
+ "type": "image/x-icon"
15
+ }
16
+ ]
17
+ }
static/script.js ADDED
@@ -0,0 +1,1982 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // Babel - Main Script (Cleaned & Optimized)
2
+
3
+ let mediaRecorder;
4
+ let audioChunks = [];
5
+ let isRecording = false;
6
+ let audioContext;
7
+ let analyser;
8
+ let micSource;
9
+ let animationId;
10
+ let recognition;
11
+ let streamTimeout;
12
+ let globalStream = null; // 🎤 PERSISTENT MIC STREAM
13
+ let isRestarting = false; // Prevent double restart logic
14
+ let isProcessingAudio = false; // Prevent duplicate audio processing
15
+ let detectedLanguage = null; // Store detected language
16
+ let isTTSPlaying = false; // Track TTS playback state to prevent mic feedback
17
+ let textProcessingTriggered = false; // Track if text was already sent (prevents double-processing)
18
+ let silenceDetectionActive = true; // Control silence detection loop
19
+ let currentRecognitionLang = 'fr-FR'; // Track current recognition language for duplex mode
20
+
21
+ // Global mode variable
22
+ window.continuousMode = false;
23
+ window.lastBotAudio = null; // 🌍 Exposed for Replay Button
24
+
25
+ // 🆔 Cycle tracking to prevent ghost handler duplicates
26
+ let currentCycleId = 0;
27
+
28
+ // 🔊 SILENCE DETECTION THRESHOLDS (More sensitive to prevent hallucination)
29
+ // 🔊 VOLUME THRESHOLD Config moved to top of file
30
+ const VOLUME_THRESHOLD = 8; // Noise Gate: Very sensitive (was 10)
31
+ const SILENCE_LIMIT_MS = 5000; // 5.0s silence - VERY Generous pause time
32
+ const SILENCE_THRESHOLD = 8; // Legacy support
33
+ const MIN_RECORDING_TIME = 500; // Minimum 0.5 second recording
34
+ const MIN_SPEECH_VOLUME = 5; // Minimum average volume to consider as speech
35
+ const TYPING_SPEED_MS = 25;
36
+
37
+
38
+
39
+ // CHAT UI HELPERS
40
+ let recentMessages = new Set();
41
+
42
+ // 🛡️ WHISPER HALLUCINATION FILTER - Common false outputs when silence/noise
43
+ const HALLUCINATION_PHRASES = [
44
+ 'thanks for watching',
45
+ 'thank you for watching',
46
+ 'subscribe',
47
+ 'like and subscribe',
48
+ 'see you next time',
49
+ 'bye bye',
50
+ 'goodbye',
51
+ 'merci d\'avoir regardé',
52
+ 'merci de votre attention',
53
+ 'à bientôt',
54
+ 'sous-titres',
55
+ 'sous-titrage',
56
+ 'subtitles by',
57
+ 'transcribed by',
58
+ 'music',
59
+ 'applause',
60
+ '[music]',
61
+ '[applause]',
62
+ '...',
63
+ 'you',
64
+ 'the',
65
+ 'i',
66
+ 'a'
67
+ ];
68
+
69
+ function isHallucination(text) {
70
+ if (!text) return true;
71
+ const cleaned = text.toLowerCase().trim();
72
+
73
+ // Too short = likely noise
74
+ if (cleaned.length < 3) return true;
75
+
76
+ // Check against known hallucinations
77
+ for (const phrase of HALLUCINATION_PHRASES) {
78
+ if (cleaned === phrase || cleaned.startsWith(phrase + '.') || cleaned.startsWith(phrase + '!')) {
79
+ console.log(`🚫 HALLUCINATION BLOCKED: "${text}"`);
80
+ return true;
81
+ }
82
+ }
83
+
84
+ // Single repeated character or word
85
+ if (/^(.)\1*$/.test(cleaned) || /^(\w+\s*)\1+$/.test(cleaned)) {
86
+ console.log(`🚫 REPEATED PATTERN BLOCKED: "${text}"`);
87
+ return true;
88
+ }
89
+
90
+ return false;
91
+ }
92
+
93
+ function createChatMessage(role, text, audioSrc = null, info = null, lang = null) {
94
+ const chatHistory = document.getElementById('chat-history');
95
+ if (!chatHistory) return;
96
+
97
+ // 🛡️ Block hallucinations at message level too
98
+ if (isHallucination(text)) {
99
+ console.log(`🚫 createChatMessage: Hallucination blocked: "${text}"`);
100
+ return;
101
+ }
102
+
103
+ // 🛡️ HOLY WAR VISUAL SHIELD (DEDUPLICATION)
104
+ // Prevent duplicate messages within 5 seconds
105
+ const normalizedText = text.trim().toLowerCase().substring(0, 100);
106
+ const messageHash = `${role}-${normalizedText}`;
107
+ if (recentMessages.has(messageHash)) {
108
+ console.log(`🛡️ VISUAL SHIELD: Blocked duplicate message: "${text.substring(0, 30)}..."`);
109
+ return;
110
+ }
111
+ recentMessages.add(messageHash);
112
+ setTimeout(() => recentMessages.delete(messageHash), 5000); // 5 seconds Blocking Period
113
+
114
+ const msgDiv = document.createElement('div');
115
+ msgDiv.className = `message ${role}-message`;
116
+ msgDiv.style.opacity = '0'; // For animation
117
+ msgDiv.style.cssText = `
118
+ background: ${role === 'user' ? 'rgba(30, 30, 35, 0.8)' : 'rgba(45, 45, 52, 0.8)'};
119
+ border-radius: 16px;
120
+ padding: 20px;
121
+ margin-bottom: 16px;
122
+ border: 1px solid ${role === 'user' ? 'rgba(60, 60, 70, 0.5)' : 'rgba(80, 80, 90, 0.5)'};
123
+ `;
124
+
125
+ // Language Badge (Always show)
126
+ const langBadge = document.createElement('div');
127
+ langBadge.className = 'lang-badge';
128
+ langBadge.style.cssText = `
129
+ display: inline-block;
130
+ background: ${role === 'user' ? 'rgba(60, 60, 70, 0.6)' : 'rgba(80, 80, 90, 0.6)'};
131
+ color: ${role === 'user' ? '#a0a0a8' : '#c0c0c8'};
132
+ padding: 6px 12px;
133
+ border-radius: 8px;
134
+ font-size: 0.75rem;
135
+ font-weight: 600;
136
+ text-transform: uppercase;
137
+ letter-spacing: 0.05em;
138
+ margin-bottom: 12px;
139
+ `;
140
+
141
+ // Determine language display
142
+ let langDisplay = lang || (role === 'user' ? 'Input' : 'Translation');
143
+ langBadge.innerText = `Language: ${langDisplay}`;
144
+ msgDiv.appendChild(langBadge);
145
+
146
+ // Text Content - Large and Clear
147
+ const textDiv = document.createElement('div');
148
+ textDiv.className = 'message-content';
149
+ textDiv.style.cssText = `
150
+ font-size: 1.25rem;
151
+ line-height: 1.7;
152
+ color: #ffffff;
153
+ font-weight: 400;
154
+ margin-top: 8px;
155
+ `;
156
+ textDiv.innerText = text;
157
+ msgDiv.appendChild(textDiv);
158
+
159
+ // Audio Player Integration (Only for Bot)
160
+ // 🛡️ FALLBACK: If audioSrc is missing, use Browser TTS!
161
+ if (role === 'bot') {
162
+ if (!audioSrc) {
163
+ console.warn("⚠️ No Audio from Server (API Limit/Error). Using Browser TTS Fallback.");
164
+ // Browser TTS Fallback
165
+ const utterance = new SpeechSynthesisUtterance(text);
166
+ // Try to set language (default to detected target or whatever)
167
+ // utterance.lang = 'en-US'; // Ideally passed in info
168
+ window.speechSynthesis.speak(utterance);
169
+ } else {
170
+ // Standard Server Audio
171
+ const audioContainer = document.createElement('div');
172
+ audioContainer.className = 'audio-container';
173
+ audioContainer.style.marginTop = '12px';
174
+ audioContainer.style.background = 'rgba(0,0,0,0.1)';
175
+ audioContainer.style.borderRadius = '8px';
176
+ audioContainer.style.padding = '8px';
177
+ audioContainer.style.display = 'flex';
178
+ audioContainer.style.alignItems = 'center';
179
+ audioContainer.style.gap = '10px';
180
+
181
+ const playBtn = document.createElement('button');
182
+ playBtn.innerHTML = '<i class="fa-solid fa-play"></i>';
183
+ playBtn.className = 'icon-btn'; // Re-use existing class
184
+ playBtn.style.width = '32px';
185
+ playBtn.style.height = '32px';
186
+ playBtn.style.background = '#fff';
187
+ playBtn.style.color = '#333';
188
+
189
+ // Waveform Visual (Fake/Static for aesthetics)
190
+ const waveDiv = document.createElement('div');
191
+ waveDiv.style.flex = '1';
192
+ waveDiv.style.height = '4px';
193
+ waveDiv.style.background = 'rgba(255,255,255,0.3)';
194
+ waveDiv.style.borderRadius = '2px';
195
+ waveDiv.style.position = 'relative';
196
+
197
+ const progressDiv = document.createElement('div');
198
+ progressDiv.style.width = '0%';
199
+ progressDiv.style.height = '100%';
200
+ progressDiv.style.background = '#fff';
201
+ progressDiv.style.borderRadius = '2px';
202
+ progressDiv.style.transition = 'width 0.1s linear';
203
+ waveDiv.appendChild(progressDiv);
204
+
205
+ // Audio Logic
206
+ const audio = new Audio(audioSrc);
207
+ audio.preload = 'auto'; // Force immediate buffer
208
+
209
+ // 🌍 Update Global Replay Reference
210
+ window.lastBotAudio = audio;
211
+
212
+ playBtn.onclick = () => {
213
+ if (audio.paused) {
214
+ audio.play();
215
+ playBtn.innerHTML = '<i class="fa-solid fa-pause"></i>';
216
+ } else {
217
+ audio.pause();
218
+ playBtn.innerHTML = '<i class="fa-solid fa-play"></i>';
219
+ }
220
+ };
221
+
222
+ // 🔇 CRITICAL: Pause speech recognition when TTS starts (prevent feedback loop)
223
+ audio.onplay = () => {
224
+ isTTSPlaying = true;
225
+ console.log('🔊 TTS Started - Pausing speech recognition to prevent feedback');
226
+
227
+ // Pause browser speech recognition if active
228
+ if (recognition) {
229
+ try {
230
+ recognition.stop();
231
+ console.log('⏸️ Paused speech recognition during TTS');
232
+ } catch (e) { }
233
+ }
234
+
235
+ // 🎯 DUPLEX MODE: MediaRecorder keeps running (don't pause it)
236
+ // We only pause speech recognition to avoid feedback
237
+ console.log('🎙️ MediaRecorder continues running during TTS');
238
+ };
239
+
240
+ audio.onended = () => {
241
+ playBtn.innerHTML = '<i class="fa-solid fa-play"></i>';
242
+ progressDiv.style.width = '0%';
243
+
244
+ // ▶️ CRITICAL: Resume after TTS
245
+ isTTSPlaying = false;
246
+ console.log('✅ TTS ended - Ready for next conversation');
247
+
248
+ // Update status for continuous mode
249
+ if (window.continuousMode) {
250
+ statusText.innerText = '💤 Prêt pour la suite...';
251
+ statusText.style.color = '#4a9b87';
252
+ console.log('🔄 Continuous mode active - system will listen automatically');
253
+ }
254
+ };
255
+
256
+ // 🚨 Error handling to prevent crashes
257
+ audio.onerror = (e) => {
258
+ console.error('❌ TTS playback error:', e);
259
+ isTTSPlaying = false;
260
+ playBtn.innerHTML = '<i class="fa-solid fa-play"></i>';
261
+
262
+ if (window.continuousMode) {
263
+ statusText.innerText = '⚠️ Erreur TTS - Prêt';
264
+ statusText.style.color = '#ff6b6b';
265
+ }
266
+ };
267
+
268
+ audio.ontimeupdate = () => {
269
+ const percent = (audio.currentTime / audio.duration) * 100;
270
+ progressDiv.style.width = `${percent}%`;
271
+ };
272
+
273
+ // 🚀 AUTO-PLAY + PRE-CHECK
274
+ // Ensure audio is playable immediately
275
+ audio.oncanplay = () => {
276
+ // Ready to start
277
+ };
278
+
279
+ audio.oncanplaythrough = () => {
280
+ // Fully ready
281
+ };
282
+
283
+ audioContainer.appendChild(playBtn);
284
+ audioContainer.appendChild(waveDiv);
285
+ msgDiv.appendChild(audioContainer);
286
+
287
+ // Latency Badge - REMOVED (cleaner UI)
288
+ // Users don't need to see engine names
289
+
290
+ // Immediate Trigger - Will auto-pause mic via onplay handler
291
+ const playPromise = audio.play();
292
+ if (playPromise !== undefined) {
293
+ playPromise.then(_ => {
294
+ playBtn.innerHTML = '<i class="fa-solid fa-pause"></i>';
295
+ }).catch(error => {
296
+ console.log("Auto-play blocked by browser policy:", error);
297
+ if (isTTSPlaying) {
298
+ // If blocked, we must ensure we don't get stuck in "TTS Playing" state
299
+ console.warn("⚠️ Autoplay blocked. Resetting state.");
300
+ isTTSPlaying = false;
301
+ playBtn.innerHTML = '<i class="fa-solid fa-play"></i>';
302
+ }
303
+ });
304
+ }
305
+ } // End of else (Server Audio)
306
+ } // End of if (Bot Role)
307
+
308
+ chatHistory.appendChild(msgDiv);
309
+
310
+ // AUTO-SCROLL: Scroll both containers to show latest message
311
+ const scrollToBottom = () => {
312
+ // Scroll chat history (if it becomes scrollable)
313
+ chatHistory.scrollTo({
314
+ top: chatHistory.scrollHeight,
315
+ behavior: 'smooth'
316
+ });
317
+
318
+ // 🚀 SUGAR: Scroll the Window (Main Stage is not scrollable)
319
+ window.scrollTo({
320
+ top: document.body.scrollHeight,
321
+ behavior: 'smooth'
322
+ });
323
+ };
324
+
325
+ // Immediate scroll
326
+ scrollToBottom();
327
+
328
+ // Scroll again after animation completes
329
+ setTimeout(scrollToBottom, 300);
330
+ setTimeout(scrollToBottom, 600);
331
+
332
+ // Fade In Animation
333
+ setTimeout(() => {
334
+ msgDiv.style.transition = 'opacity 0.3s ease, transform 0.3s ease';
335
+ msgDiv.style.transform = 'translateY(10px)';
336
+ requestAnimationFrame(() => {
337
+ msgDiv.style.opacity = '1';
338
+ msgDiv.style.transform = 'translateY(0)';
339
+ });
340
+ }, 50);
341
+ }
342
+
343
+ // DOM Elements - declared but not initialized yet
344
+ let recordBtn, statusText, settingsBtn, settingsModal, audioPlayer;
345
+ let originalTextField, translatedTextField, quickLangSelector, sourceLangSelector, aiModelSelector;
346
+
347
+ // 🔊 AUDIO UNLOCKER: Play silence on click to enable Autoplay
348
+ function unlockAudioContext() {
349
+ try {
350
+ const ctx = new (window.AudioContext || window.webkitAudioContext)();
351
+ const osc = ctx.createOscillator();
352
+ const gain = ctx.createGain();
353
+ gain.gain.value = 0.001;
354
+ osc.connect(gain);
355
+ gain.connect(ctx.destination);
356
+ osc.start(0);
357
+ setTimeout(() => { osc.stop(); ctx.close(); }, 100);
358
+ console.log("🔓 Audio Autoplay Unlocked");
359
+ } catch (e) {
360
+ console.log("Audio unlock not needed");
361
+ }
362
+ }
363
+
364
+ // ============================================================
365
+ // 🎯 INITIALIZE EVERYTHING WHEN DOM IS READY
366
+ // ============================================================
367
+ function initializeApp() {
368
+ console.log('🎯 initializeApp() called');
369
+
370
+ // 🚀 FULL AUTO CONFIGURATION (User Request)
371
+ if (!localStorage.getItem('googleKey')) {
372
+ console.log('💎 FULL AUTO: Injecting Google API Key...');
373
+ localStorage.setItem('googleKey', 'AIzaSyDB9wiqXsy1dG9OLU9r4Tar8oDdeVy4NOQ');
374
+ }
375
+
376
+ // Get DOM Elements
377
+ recordBtn = document.getElementById('record-btn');
378
+ statusText = document.getElementById('status-placeholder');
379
+ settingsBtn = document.getElementById('settings-trigger');
380
+ settingsModal = document.getElementById('settings-modal');
381
+ audioPlayer = document.getElementById('audio-player');
382
+ originalTextField = document.getElementById('original-text');
383
+ translatedTextField = document.getElementById('translated-text');
384
+ quickLangSelector = document.getElementById('target-lang-quick'); // 🔧 FIXED: Use correct ID
385
+ sourceLangSelector = document.getElementById('source-lang-selector');
386
+ aiModelSelector = document.getElementById('ai-model');
387
+
388
+ console.log('📦 DOM Elements loaded:');
389
+ console.log(' - recordBtn:', recordBtn ? '✅ FOUND' : '❌ NOT FOUND');
390
+ console.log(' - statusText:', statusText ? '✅ FOUND' : '❌ NOT FOUND');
391
+
392
+ if (!recordBtn) {
393
+ console.error('❌❌❌ CRITICAL: record-btn NOT FOUND IN DOM! ❌❌❌');
394
+ return;
395
+ }
396
+
397
+ // 🎙️ BUTTON CLICK HANDLER
398
+ console.log('🔧 Attaching click handler...');
399
+
400
+ recordBtn.onclick = async function (e) {
401
+ console.log('🔘🔘🔘 BUTTON CLICKED! 🔘🔘🔘');
402
+ e.preventDefault();
403
+ e.stopPropagation();
404
+
405
+ // Unlock audio
406
+ unlockAudioContext();
407
+
408
+ if (!window.continuousMode) {
409
+ // START
410
+ console.log('▶️ Starting continuous mode...');
411
+ window.continuousMode = true;
412
+ this.classList.add('active');
413
+
414
+ if (statusText) {
415
+ statusText.innerText = 'Écoute en continu...';
416
+ statusText.style.color = '#4a9b87';
417
+ }
418
+
419
+ try {
420
+ await listenContinuously();
421
+ } catch (error) {
422
+ console.error('❌ Error:', error);
423
+ window.continuousMode = false;
424
+ this.classList.remove('active');
425
+ if (statusText) {
426
+ statusText.innerText = 'Erreur: ' + error.message;
427
+ statusText.style.color = '#ff6b6b';
428
+ }
429
+ }
430
+ } else {
431
+ // STOP
432
+ console.log('⏹️ Stopping continuous mode...');
433
+ window.continuousMode = false;
434
+ this.classList.remove('active');
435
+ this.classList.remove('active-speech'); // ✅ FIXED: CSS class name
436
+ this.classList.remove('processing'); // ✅ FIXED: Remove processing state too
437
+
438
+ // Stop all components
439
+ try {
440
+ if (mediaRecorder && mediaRecorder.state !== 'inactive') {
441
+ mediaRecorder.stop();
442
+ }
443
+ if (recognition) {
444
+ recognition.stop();
445
+ recognition = null;
446
+ }
447
+ if (audioContext && audioContext.state !== 'closed') {
448
+ audioContext.close();
449
+ }
450
+ } catch (e) {
451
+ console.warn('Cleanup warning:', e);
452
+ }
453
+
454
+ // 🧹 CRITICAL: Full memory cleanup to keep system fast
455
+ console.log('🧹 Cleaning up memory and cache...');
456
+
457
+ audioContext = null;
458
+ analyser = null;
459
+ micSource = null;
460
+ mediaRecorder = null;
461
+ audioChunks = [];
462
+ isRecording = false;
463
+ isProcessingAudio = false;
464
+ speechDetected = false;
465
+ textProcessingTriggered = false;
466
+
467
+ // Clear audio buffers
468
+ if (animationId) {
469
+ cancelAnimationFrame(animationId);
470
+ animationId = null;
471
+ }
472
+
473
+ // ⚡ Clear backend conversation cache for fresh start
474
+ fetch('/clear_cache', { method: 'POST' })
475
+ .then(res => res.json())
476
+ .then(data => console.log(`✅ Backend cache cleared: ${data.cleared} entries`))
477
+ .catch(e => console.warn('Cache clear failed:', e));
478
+
479
+ if (statusText) {
480
+ statusText.innerText = 'Arrêté';
481
+ statusText.style.color = '#888';
482
+ }
483
+ console.log('✅ Stopped');
484
+
485
+ // Kill global stream on full stop
486
+ if (globalStream) {
487
+ try {
488
+ globalStream.getTracks().forEach(track => track.stop());
489
+ } catch (e) { }
490
+ globalStream = null;
491
+ }
492
+ }
493
+ };
494
+
495
+ // 📱 MOBILE TOUCH SUPPORT (CRITICAL FIX)
496
+ // Desktop: onclick works
497
+ // Mobile: Need touchstart/touchend
498
+ let touchHandled = false;
499
+
500
+ recordBtn.addEventListener('touchstart', (e) => {
501
+ e.preventDefault(); // Prevent mouse event simulation
502
+ touchHandled = true;
503
+ recordBtn.onclick(e); // Trigger the same logic
504
+ }, { passive: false });
505
+
506
+ recordBtn.addEventListener('touchend', (e) => {
507
+ e.preventDefault();
508
+ }, { passive: false });
509
+
510
+ // Fallback for desktop
511
+ recordBtn.addEventListener('click', (e) => {
512
+ if (touchHandled) {
513
+ touchHandled = false;
514
+ return; // Already handled by touch
515
+ }
516
+ // Desktop logic continues normally
517
+ });
518
+
519
+ // Disable context menu
520
+ recordBtn.oncontextmenu = (e) => e.preventDefault();
521
+
522
+ // ============================================================
523
+ // 🌍 LANGUAGE QUICK SELECTORS - Event Handlers
524
+ // ============================================================
525
+
526
+ const sourceLangQuick = document.getElementById('source-lang-quick');
527
+ const targetLangQuick = document.getElementById('target-lang-quick');
528
+ const swapLangsBtn = document.getElementById('swap-langs');
529
+
530
+ // 🎯 SOURCE LANGUAGE CHANGE
531
+ if (sourceLangQuick) {
532
+ sourceLangQuick.addEventListener('change', function () {
533
+ const newLang = this.value;
534
+ console.log(`🔄 Source language changed to: ${newLang}`);
535
+
536
+ // Save to localStorage for persistence
537
+ localStorage.setItem('sourceLangQuick', newLang);
538
+
539
+ // Update status to show change
540
+ if (statusText) {
541
+ statusText.innerText = `📍 Source: ${this.options[this.selectedIndex].text}`;
542
+ statusText.style.color = '#4a9b87';
543
+ setTimeout(() => {
544
+ statusText.innerText = 'Prêt';
545
+ statusText.style.color = '#888';
546
+ }, 2000);
547
+ }
548
+
549
+ // Restart recognition with new language if currently recording
550
+ if (window.continuousMode && recognition) {
551
+ console.log('🔄 Restarting recognition with new source language...');
552
+ try { recognition.stop(); } catch (e) { }
553
+ // It will auto-restart with new language via onend handler
554
+ }
555
+ });
556
+
557
+ // Restore saved value
558
+ const savedSource = localStorage.getItem('sourceLangQuick');
559
+ if (savedSource) {
560
+ sourceLangQuick.value = savedSource;
561
+ }
562
+ }
563
+
564
+ // 🎯 TARGET LANGUAGE CHANGE
565
+ if (targetLangQuick) {
566
+ targetLangQuick.addEventListener('change', function () {
567
+ const newLang = this.value;
568
+ console.log(`🎯 Target language changed to: ${newLang}`);
569
+
570
+ // Save to localStorage for persistence
571
+ localStorage.setItem('targetLangQuick', newLang);
572
+
573
+ // Also update the main selector if it exists
574
+ if (quickLangSelector) {
575
+ quickLangSelector.value = newLang;
576
+ }
577
+
578
+ // Update status to show change
579
+ if (statusText) {
580
+ statusText.innerText = `🎯 Cible: ${this.options[this.selectedIndex].text}`;
581
+ statusText.style.color = '#4a9b87';
582
+ setTimeout(() => {
583
+ statusText.innerText = 'Prêt';
584
+ statusText.style.color = '#888';
585
+ }, 2000);
586
+ }
587
+ });
588
+
589
+ // Restore saved value
590
+ const savedTarget = localStorage.getItem('targetLangQuick');
591
+ if (savedTarget) {
592
+ targetLangQuick.value = savedTarget;
593
+ }
594
+ }
595
+
596
+ // 🔄 SWAP LANGUAGES BUTTON
597
+ if (swapLangsBtn) {
598
+ swapLangsBtn.addEventListener('click', function () {
599
+ console.log('🔄 Swapping languages...');
600
+
601
+ const sourceSelect = document.getElementById('source-lang-quick');
602
+ const targetSelect = document.getElementById('target-lang-quick');
603
+
604
+ if (!sourceSelect || !targetSelect) {
605
+ console.warn('Language selectors not found');
606
+ return;
607
+ }
608
+
609
+ // Get current values
610
+ const currentSource = sourceSelect.value;
611
+ const currentTarget = targetSelect.value;
612
+
613
+ // Map target names to source codes
614
+ const targetToSourceMap = {
615
+ 'French': 'fr-FR',
616
+ 'English': 'en-US',
617
+ 'Arabic': 'ar-SA',
618
+ 'Moroccan Darija': 'ar-SA',
619
+ 'Spanish': 'es-ES',
620
+ 'German': 'de-DE'
621
+ };
622
+
623
+ // Map source codes to target names
624
+ const sourceToTargetMap = {
625
+ 'fr-FR': 'French',
626
+ 'en-US': 'English',
627
+ 'ar-SA': 'Arabic',
628
+ 'es-ES': 'Spanish',
629
+ 'de-DE': 'German',
630
+ 'auto': 'French' // Default when swapping from auto
631
+ };
632
+
633
+ // Calculate new values
634
+ const newSourceCode = targetToSourceMap[currentTarget] || 'auto';
635
+ const newTargetName = sourceToTargetMap[currentSource] || 'French';
636
+
637
+ // Apply swap
638
+ sourceSelect.value = newSourceCode;
639
+ targetSelect.value = newTargetName;
640
+
641
+ // Save to localStorage
642
+ localStorage.setItem('sourceLangQuick', newSourceCode);
643
+ localStorage.setItem('targetLangQuick', newTargetName);
644
+
645
+ // Visual feedback
646
+ this.style.transform = 'rotate(180deg)';
647
+ setTimeout(() => {
648
+ this.style.transform = 'rotate(0deg)';
649
+ }, 300);
650
+
651
+ // Update status
652
+ if (statusText) {
653
+ statusText.innerText = `🔄 ${sourceSelect.options[sourceSelect.selectedIndex].text} ↔ ${targetSelect.options[targetSelect.selectedIndex].text}`;
654
+ statusText.style.color = '#60a5fa';
655
+ setTimeout(() => {
656
+ statusText.innerText = 'Prêt';
657
+ statusText.style.color = '#888';
658
+ }, 2500);
659
+ }
660
+
661
+ console.log(`✅ Swapped: ${currentSource} → ${newTargetName}, ${currentTarget} → ${newSourceCode}`);
662
+
663
+ // Restart recognition if active
664
+ if (window.continuousMode && recognition) {
665
+ try { recognition.stop(); } catch (e) { }
666
+ }
667
+ });
668
+ }
669
+
670
+ console.log('�� Language quick selectors initialized');
671
+ console.log('✅✅✅ BUTTON HANDLER ATTACHED! ✅✅✅');
672
+ }
673
+
674
+ // Run initialization when DOM is ready
675
+ if (document.readyState === 'loading') {
676
+ console.log('📄 DOM not ready, waiting for DOMContentLoaded...');
677
+ document.addEventListener('DOMContentLoaded', initializeApp);
678
+ } else {
679
+ console.log('📄 DOM already ready, initializing now...');
680
+ initializeApp();
681
+ }
682
+
683
+ // --- CONTINUOUS CONVERSATION MODE ---
684
+
685
+ async function listenContinuously() {
686
+ if (!window.continuousMode) {
687
+ console.log("❌ listenContinuously called but window.continuousMode is false");
688
+ return;
689
+ }
690
+
691
+ // 🛡️ RECURSION GUARD: If we are already recording, don't start another loop!
692
+ if (isRecording) {
693
+ console.log("⚠️ Already recording, skipping duplicate start request");
694
+ return;
695
+ }
696
+
697
+ console.log("🎙️ Starting NEW listening cycle...");
698
+
699
+ try {
700
+ // 🔥 CRITICAL: Increment cycle ID to invalidate old handlers
701
+ currentCycleId++;
702
+ const thisCycleId = currentCycleId;
703
+ console.log(`🆔 Cycle ID: ${thisCycleId}`);
704
+
705
+ // 🔥 CRITICAL: Clean up old MediaRecorder to prevent ghost handlers
706
+ if (mediaRecorder && mediaRecorder.state !== 'inactive') {
707
+ try {
708
+ console.log("🧹 Cleaning up old mediaRecorder");
709
+ // Don't call stop() - it will trigger the old onstop handler!
710
+ // Just abandon it and create a new one
711
+ mediaRecorder.ondataavailable = null;
712
+ mediaRecorder.onstop = null;
713
+ mediaRecorder = null;
714
+ } catch (e) { console.warn("Cleanup warning:", e); }
715
+ }
716
+
717
+ isRecording = true;
718
+ audioChunks = [];
719
+ let speechDetected = false; // Reset speech detection
720
+ textProcessingTriggered = false; // Reset flag
721
+ silenceDetectionActive = true; // Enable silence detection
722
+
723
+ let stream;
724
+
725
+ // 🚀 REUSE STREAM IF AVAILABLE
726
+ if (globalStream && globalStream.active) {
727
+ console.log("♻️ Reusing existing microphone stream");
728
+ stream = globalStream;
729
+ } else {
730
+ console.log("🎤 Requesting NEW microphone access...");
731
+ stream = await navigator.mediaDevices.getUserMedia({ audio: true });
732
+ globalStream = stream;
733
+ console.log("✅ Microphone access granted");
734
+ }
735
+
736
+ // 🎯 REAL-TIME TRANSCRIPTION: Start immediately
737
+ startRealTimeTranscription();
738
+
739
+ // Setup audio analysis for silence detection
740
+ // Reuse context if active to reduce click/pop
741
+ if (!audioContext || audioContext.state === 'closed') {
742
+ audioContext = new (window.AudioContext || window.webkitAudioContext)();
743
+ }
744
+
745
+ analyser = audioContext.createAnalyser();
746
+ micSource = audioContext.createMediaStreamSource(stream);
747
+ micSource.connect(analyser);
748
+ analyser.fftSize = 256;
749
+ const bufferLength = analyser.frequencyBinCount;
750
+ const dataArray = new Uint8Array(bufferLength);
751
+
752
+ let silenceStart = Date.now();
753
+ // speechDetected already declared above
754
+
755
+ mediaRecorder = new MediaRecorder(stream);
756
+
757
+ mediaRecorder.ondataavailable = e => {
758
+ // 🛡️ Only process if this is still the current cycle
759
+ if (thisCycleId === currentCycleId) {
760
+ audioChunks.push(e.data);
761
+ } else {
762
+ console.warn(`⚠️ Ignoring data from old cycle ${thisCycleId} (current: ${currentCycleId})`);
763
+ }
764
+ };
765
+
766
+ mediaRecorder.onstop = async () => {
767
+ // 🛡️ CRITICAL: Ignore events from old cycles
768
+ if (thisCycleId !== currentCycleId) {
769
+ console.warn(`⚠️ Ignoring onstop from old cycle ${thisCycleId} (current: ${currentCycleId})`);
770
+ return;
771
+ }
772
+
773
+ console.log(`🛑 Chunk finalized (Cycle ${thisCycleId}).`);
774
+
775
+ // DON'T stop transcription here if we want continuous, but we do need to reset it
776
+ // for the new chunk context. Actually, let's keep it simply "Running".
777
+
778
+ // Process audio if we have valid speech
779
+ if (audioChunks.length > 0 && speechDetected) {
780
+ const blob = new Blob(audioChunks, { type: 'audio/wav' });
781
+
782
+ if (blob.size > 2000) {
783
+ // 🚀 INSTANT RESTART: Don't wait for processing!
784
+ // Trigger processing in background
785
+ statusText.innerText = 'Traitement...';
786
+ statusText.style.color = '#4a9b87';
787
+
788
+ // We DO NOT await here. We fire and forget (mostly),
789
+ // or let it handle the UI updates asynchronously.
790
+ // ⚡ OPTIMIZATION: Pass 'true' to bypass redundant silence check (we already checked it!)
791
+ processAudio(blob, true).catch(e => console.error("Processing error:", e));
792
+
793
+ }
794
+ }
795
+
796
+ // 🔄 INSTANT RESTART (Synchronized)
797
+ // Immediately restart listening *unless* completely stopped
798
+ if (window.continuousMode) {
799
+ // Reset flags for next turn - NOW it is safe to reset
800
+ speechDetected = false;
801
+
802
+ // 🚀 AGGRESSIVE IMMEDIATE RESTART
803
+ // Use 0ms delay to unblock the event loop but start ASAP
804
+ setTimeout(() => {
805
+ if (window.continuousMode) {
806
+ console.log("🔄 Instant Restart Triggered (Parallel)");
807
+ listenContinuously();
808
+ }
809
+ }, 0);
810
+ }
811
+
812
+ // NOTE: We do NOT close audioContext here anymore, to keep it 'warm'.
813
+ // Only disconnect analyser to save CPU if needed, but keeping it open is faster.
814
+ try {
815
+ if (micSource) micSource.disconnect();
816
+ if (analyser) analyser.disconnect();
817
+ if (animationId) cancelAnimationFrame(animationId);
818
+ } catch (e) { }
819
+ };
820
+
821
+ mediaRecorder.start();
822
+
823
+ if (animationId) cancelAnimationFrame(animationId);
824
+
825
+ // 🎯 Continuous monitoring with better noise filtering
826
+ let consecutiveSpeechFrames = 0;
827
+ let consecutiveSilenceFrames = 0;
828
+ const SPEECH_FRAMES_THRESHOLD = 3; // React faster to speech (3 frames = ~50ms)
829
+ const SILENCE_FRAMES_THRESHOLD = 1200; // ~20.0s silence (User request: "Keep button working / Don't cut")
830
+
831
+ function monitorAudio() {
832
+ if (!window.continuousMode || !isRecording) {
833
+ console.log("🛑 Audio monitoring stopped");
834
+ return;
835
+ }
836
+
837
+ // 🔇 Skip monitoring while TTS is playing
838
+ if (isTTSPlaying) {
839
+ requestAnimationFrame(monitorAudio);
840
+ return;
841
+ }
842
+
843
+ analyser.getByteFrequencyData(dataArray);
844
+ let sum = 0;
845
+ for (let i = 0; i < bufferLength; i++) sum += dataArray[i];
846
+ const average = sum / bufferLength;
847
+
848
+ // 🎯 Better noise filtering
849
+ // Use local VOLUME_THRESHOLD if defined, else generic 10
850
+ if (average > 4) { // Hardcoded decent threshold for consistency
851
+ consecutiveSpeechFrames++;
852
+ consecutiveSilenceFrames = 0;
853
+
854
+ // Only mark as speech after consecutive loud frames (avoid false starts)
855
+ if (consecutiveSpeechFrames >= SPEECH_FRAMES_THRESHOLD && !speechDetected) {
856
+ speechDetected = true;
857
+ silenceStart = Date.now();
858
+ console.log("🗣️ Speech confirmed (filtered noise)");
859
+ statusText.innerText = '🎤 Enregistrement...';
860
+ statusText.style.color = '#ff4444';
861
+ recordBtn.classList.add('active-speech'); // ✅ FIXED: Match CSS class
862
+ }
863
+ } else {
864
+ consecutiveSpeechFrames = 0;
865
+ consecutiveSilenceFrames++;
866
+
867
+ if (!speechDetected) {
868
+ // Still waiting for speech
869
+ if (!statusText.innerText.includes('Traitement')) {
870
+ statusText.innerText = '💤 En attente de parole...';
871
+ statusText.style.color = '#888';
872
+ }
873
+ } else {
874
+ // Speech detected before, now checking for end
875
+ if (consecutiveSilenceFrames >= SILENCE_FRAMES_THRESHOLD) {
876
+ console.log('🤫 Silence confirmed - ending speech');
877
+ consecutiveSpeechFrames = 0;
878
+ consecutiveSilenceFrames = 0;
879
+ // speechDetected = false; // ❌ DON'T RESET HERE! Needed for onstop check.
880
+
881
+ isRecording = false;
882
+ recordBtn.classList.remove('active-speech'); // ✅ FIXED: Match CSS class
883
+
884
+ // Stop recorder to process
885
+ if (mediaRecorder && mediaRecorder.state === 'recording') {
886
+ mediaRecorder.stop();
887
+ }
888
+ return;
889
+ }
890
+ }
891
+ }
892
+
893
+ animationId = requestAnimationFrame(monitorAudio);
894
+ }
895
+
896
+ monitorAudio();
897
+
898
+ } catch (err) {
899
+ console.error('Erreur listenContinuously:', err);
900
+ window.continuousMode = false;
901
+ recordBtn.classList.remove('active');
902
+ }
903
+ }
904
+
905
+ // --- REAL-TIME TRANSCRIPTION (Consolidated & Cleaned) ---
906
+ // This function handles browser-based speech recognition for instant feedback
907
+ // ⚠️ NOTE: Browser SpeechRecognition does NOT support Darija well - we use it only for visual feedback
908
+
909
+ let arabicModeActive = false; // Track if we're in Arabic/Darija mode
910
+
911
+ function startRealTimeTranscription() {
912
+ const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition;
913
+
914
+ // 1. Check Browser Support
915
+ if (!SpeechRecognition) {
916
+ console.warn("⚠️ Browser Speech Recognition not supported.");
917
+ return;
918
+ }
919
+
920
+ // 2. Prevent Multiple Instances
921
+ if (recognition) {
922
+ try { recognition.stop(); } catch (e) { }
923
+ recognition = null;
924
+ }
925
+
926
+ // 🧹 Reset global capture
927
+ window.currentTranscript = "";
928
+
929
+ try {
930
+ // 3. Get language from QUICK SELECTORS (visible in UI)
931
+ const sourceLangQuick = document.getElementById('source-lang-quick');
932
+ const targetLangQuick = document.getElementById('target-lang-quick');
933
+
934
+ const targetLang = targetLangQuick?.value || quickLangSelector?.value || 'French';
935
+ let sourceLang = sourceLangQuick?.value || localStorage.getItem('sourceLangQuick') || 'auto';
936
+
937
+ console.log(`🎯 Quick Selectors: Source=${sourceLang}, Target=${targetLang}`);
938
+
939
+ // 🇲🇦 SMART MODE: Only activate if source is 'auto'
940
+ if (sourceLang === 'auto') {
941
+ // Smart detection based on target language
942
+ if (targetLang === 'French') {
943
+ sourceLang = 'ar-SA'; // Likely speaking Arabic/Darija
944
+ arabicModeActive = true;
945
+ console.log('🇲🇦 AUTO MODE: Target=French → Assuming Arabic/Darija');
946
+ } else if (targetLang === 'Moroccan Darija' || targetLang === 'Arabic') {
947
+ sourceLang = 'fr-FR'; // Likely speaking French
948
+ arabicModeActive = false;
949
+ console.log('🇫🇷 AUTO MODE: Target=Arabic → Assuming French');
950
+ } else if (targetLang === 'English') {
951
+ // Default to Arabic for Moroccan users, but check cache
952
+ sourceLang = detectedLanguage === 'French' ? 'fr-FR' : 'ar-SA';
953
+ arabicModeActive = sourceLang === 'ar-SA';
954
+ console.log(`🌍 AUTO MODE: Target=English → Assuming ${sourceLang}`);
955
+ } else {
956
+ sourceLang = 'fr-FR'; // Default fallback
957
+ arabicModeActive = false;
958
+ }
959
+ } else {
960
+ // MANUAL MODE: User selected specific source language
961
+ arabicModeActive = sourceLang === 'ar-SA';
962
+ console.log(`📌 MANUAL MODE: Source=${sourceLang} (Arabic mode: ${arabicModeActive})`);
963
+ }
964
+
965
+ // 4. Configure Recognition with selected language
966
+ recognition = new SpeechRecognition();
967
+ recognition.continuous = true;
968
+ recognition.interimResults = true;
969
+ recognition.lang = sourceLang;
970
+ currentRecognitionLang = sourceLang;
971
+
972
+ console.log(`🎤 Browser Recognition: ${sourceLang} (Arabic mode: ${arabicModeActive})`);
973
+
974
+ // 5. Event Handlers
975
+ recognition.onstart = () => {
976
+ console.log("✅ Real-time transcription active");
977
+ if (navigator.vibrate) navigator.vibrate(50); // Haptic feedback
978
+ };
979
+
980
+ recognition.onerror = (event) => {
981
+ console.warn("❌ Recognition error:", event.error);
982
+ if (event.error === 'not-allowed') {
983
+ statusText.innerText = "⚠️ Accès micro refusé";
984
+ statusText.style.color = "yellow";
985
+ } else if (event.error !== 'aborted') {
986
+ // Restart recognition on non-critical errors (in continuous mode)
987
+ if (window.continuousMode && isRecording) {
988
+ console.log("🔄 Restarting recognition after error...");
989
+ setTimeout(() => {
990
+ if (window.continuousMode && isRecording) {
991
+ try { recognition.start(); } catch (e) { }
992
+ }
993
+ }, 500);
994
+ }
995
+ }
996
+ };
997
+
998
+ recognition.onend = () => {
999
+ console.log("🔄 Recognition ended");
1000
+
1001
+ // Auto-restart in continuous mode (unless TTS is playing)
1002
+ if (window.continuousMode && isRecording) {
1003
+ if (isTTSPlaying) {
1004
+ console.log("⏸️ TTS is playing - recognition will restart when TTS ends");
1005
+ // Don't restart now - the audio.onended handler will do it
1006
+ } else {
1007
+ console.log("🔄 Auto-restarting recognition for continuous mode...");
1008
+ setTimeout(() => {
1009
+ if (window.continuousMode && isRecording && !isTTSPlaying) {
1010
+ try {
1011
+ recognition.start();
1012
+ console.log("✅ Recognition restarted successfully");
1013
+ } catch (e) {
1014
+ console.warn("Could not restart recognition:", e);
1015
+ }
1016
+ }
1017
+ }, 300);
1018
+ }
1019
+ }
1020
+ };
1021
+
1022
+ recognition.onresult = (event) => {
1023
+ let interimTranscript = '';
1024
+ let finalTranscript = '';
1025
+
1026
+ for (let i = event.resultIndex; i < event.results.length; ++i) {
1027
+ const transcript = event.results[i][0].transcript;
1028
+ if (event.results[i].isFinal) {
1029
+ finalTranscript += transcript;
1030
+ } else {
1031
+ interimTranscript += transcript;
1032
+ }
1033
+ }
1034
+
1035
+ // ✨ AFFICHAGE INSTANTANÉ - LYRICS STYLE
1036
+ const fullText = finalTranscript || interimTranscript;
1037
+
1038
+ // 🚀 STEAL THE MICROPHONE: Store global transcript for processAudio to pick up
1039
+ if (finalTranscript.trim().length > 0) {
1040
+ window.currentTranscript = finalTranscript;
1041
+ } else if (interimTranscript.trim().length > 0) {
1042
+ window.currentTranscript = interimTranscript;
1043
+ }
1044
+
1045
+ if (fullText.trim().length > 0) {
1046
+ // 🇲🇦 ARABIC MODE: Browser recognition is unreliable for Arabic/Darija
1047
+ // Show the text but with a note that final transcription will be better
1048
+ if (arabicModeActive) {
1049
+ // For Arabic, only show if it looks like actual Arabic text
1050
+ const hasArabicChars = /[\u0600-\u06FF]/.test(fullText);
1051
+ if (hasArabicChars && originalTextField) {
1052
+ originalTextField.innerText = fullText;
1053
+ originalTextField.hidden = false;
1054
+ originalTextField.style.opacity = '1';
1055
+ originalTextField.style.direction = 'rtl';
1056
+ originalTextField.style.textAlign = 'right';
1057
+ originalTextField.style.fontSize = '1.3rem';
1058
+ originalTextField.style.fontWeight = '500';
1059
+ } else {
1060
+ // Browser gave garbage (Latin chars for Arabic speech) - show waiting status
1061
+ if (originalTextField) {
1062
+ originalTextField.innerText = '🎤 جاري الاستماع...'; // "Listening..." in Arabic
1063
+ originalTextField.style.direction = 'rtl';
1064
+ originalTextField.style.textAlign = 'right';
1065
+ originalTextField.style.opacity = '0.7';
1066
+ }
1067
+ }
1068
+ } else {
1069
+ // French/English mode - show normally
1070
+ if (originalTextField) {
1071
+ originalTextField.innerText = fullText;
1072
+ originalTextField.hidden = false;
1073
+ originalTextField.style.opacity = '1';
1074
+ originalTextField.style.direction = 'ltr';
1075
+ originalTextField.style.textAlign = 'left';
1076
+ originalTextField.style.fontSize = '1.2rem';
1077
+ originalTextField.style.fontWeight = '500';
1078
+ originalTextField.style.lineHeight = '1.6';
1079
+ originalTextField.style.fontStyle = 'normal';
1080
+ originalTextField.style.animation = 'fadeIn 0.3s ease';
1081
+ }
1082
+ }
1083
+
1084
+ // Scroll to bottom
1085
+ const chatHistory = document.getElementById('chat-history');
1086
+ if (chatHistory) chatHistory.scrollTop = chatHistory.scrollHeight;
1087
+
1088
+ // ✨ Recognition is ONLY for visual display
1089
+ if (finalTranscript.trim().length > 2) {
1090
+ console.log("✅ Sentence transcribed (visual feedback only)");
1091
+ }
1092
+ }
1093
+ };
1094
+
1095
+ // 6. Start
1096
+ recognition.start();
1097
+
1098
+ } catch (e) {
1099
+ console.error("❌ Fatal Error starting recognition:", e);
1100
+ }
1101
+ }
1102
+
1103
+ // 🚀 INTELLIGENT MODE: Send Text directly (Primary trigger via isFinal)
1104
+ async function sendTextForProcessing(text) {
1105
+ if (isProcessingAudio) {
1106
+ console.log("⚠️ Already processing, skipping duplicate...");
1107
+ return;
1108
+ }
1109
+ isProcessingAudio = true;
1110
+
1111
+ const targetLang = quickLangSelector?.value || 'French';
1112
+
1113
+ // 🌍 Language detection is now handled by the backend with Gemini
1114
+ console.log(`📤 Sending text for processing: "${text}"`);
1115
+ statusText.innerText = 'Traduction en cours...';
1116
+ statusText.style.color = '#4a9b87';
1117
+
1118
+ const payload = {
1119
+ text_input: text, // Sending TEXT, not AUDIO
1120
+ source_language: 'auto', // Let backend (Gemini) detect language
1121
+ target_language: targetLang, // Use quick-lang-selector
1122
+ model: localStorage.getItem('selectedModel') || 'Gemini', // Updated default
1123
+ tts_engine: localStorage.getItem('ttsEngine') || 'openai', // Updated default
1124
+ stt_engine: localStorage.getItem('sttEngine') || 'seamless-m4t', // NEW: SeamlessM4T default
1125
+ ai_correction: localStorage.getItem('aiCorrectionEnabled') !== 'false', // NEW: AI Correction enabled by default
1126
+ voice_cloning: false,
1127
+ use_grammar_correction: localStorage.getItem('grammarCorrectionEnabled') !== 'false',
1128
+ voice_gender_preference: localStorage.getItem('voiceGenderPreference') || 'auto'
1129
+ };
1130
+
1131
+ try {
1132
+ const response = await fetch('/process_audio', {
1133
+ method: 'POST',
1134
+ headers: { 'Content-Type': 'application/json' },
1135
+ body: JSON.stringify(payload)
1136
+ });
1137
+
1138
+ const data = await response.json();
1139
+
1140
+ if (data.error) {
1141
+ console.error("❌ Processing error:", data.error);
1142
+ statusText.innerText = 'Erreur';
1143
+ } else {
1144
+ // Handle Success - Update translated text display only
1145
+ // Chat messages are created by the main axios handler to avoid duplicates
1146
+ if (translatedTextField) {
1147
+ translatedTextField.innerText = data.translated_text;
1148
+ translatedTextField.style.opacity = '1';
1149
+ }
1150
+
1151
+ // 🧠 SMART MODE LATCHING: Update recognition language for the NEXT turn
1152
+ if (data.source_language_full) {
1153
+ const newLang = data.source_language_full;
1154
+ console.log(`🧠 SMART MODE: Latching onto detected language: ${newLang}`);
1155
+
1156
+ // Update the global state so startRealTimeTranscription uses it
1157
+ // We map standard names to BCP-47 codes for SpeechRecognition
1158
+ const langToCode = {
1159
+ 'French': 'fr-FR',
1160
+ 'English': 'en-US',
1161
+ 'Arabic': 'ar-SA', // Default to SA for generic, or MA if available
1162
+ 'Moroccan Darija': 'ar-MA', // Chrome might treat this as ar-SA or similar
1163
+ 'Spanish': 'es-ES',
1164
+ 'German': 'de-DE',
1165
+ 'Italian': 'it-IT',
1166
+ 'Portuguese': 'pt-PT',
1167
+ 'Russian': 'ru-RU',
1168
+ 'Japanese': 'ja-JP',
1169
+ 'Korean': 'ko-KR',
1170
+ 'Chinese': 'zh-CN',
1171
+ 'Hindi': 'hi-IN'
1172
+ };
1173
+
1174
+ const code = langToCode[newLang];
1175
+ if (code) {
1176
+ currentRecognitionLang = code;
1177
+ detectedLanguage = newLang; // Update global detected
1178
+
1179
+ // If we are in AUTO mode, this is critical
1180
+ if (document.getElementById('source-lang-selector').value === 'auto') {
1181
+ console.log(`🔄 UPDATING RECOGNITION to ${code} for next turn`);
1182
+
1183
+ // Restart recognition if it's running, to apply new language
1184
+ if (recognition) {
1185
+ try { recognition.stop(); } catch (e) { }
1186
+ // It will auto-restart via the 'end' event or our continuous loop
1187
+ }
1188
+ }
1189
+ }
1190
+ }
1191
+
1192
+ console.log("✅ Text processing complete - TTS will play automatically");
1193
+
1194
+ // Update status for continuous mode
1195
+ if (window.continuousMode) {
1196
+ statusText.innerText = '🔊 Lecture TTS...';
1197
+ statusText.style.color = '#4a9b87';
1198
+ console.log('🎙️ Continuous mode active - will resume listening after TTS');
1199
+ } else {
1200
+ statusText.innerText = 'Prêt';
1201
+ }
1202
+ }
1203
+ } catch (e) {
1204
+ console.error("❌ Text processing error:", e);
1205
+ statusText.innerText = 'Erreur réseau';
1206
+ } finally {
1207
+ isProcessingAudio = false;
1208
+ }
1209
+ }
1210
+
1211
+ // --- RECORDER LOGIC ---
1212
+
1213
+ // Silence Detection Config - Moved to top of file
1214
+ // CONSTANTS ARE GLOBAL NOW
1215
+ async function startSmartRecording() {
1216
+ try {
1217
+ console.log('🎤 STARTING RECORDING...');
1218
+ isRecording = true;
1219
+ recordBtn.classList.add('active');
1220
+ statusText.innerText = 'Écoute...';
1221
+ statusText.style.color = 'white';
1222
+
1223
+ document.dispatchEvent(new Event('reset-ui'));
1224
+ originalTextField.innerText = '...';
1225
+ translatedTextField.innerText = '...';
1226
+
1227
+ // 🎤 EXPERT MICROPHONE CONFIGURATION
1228
+ const stream = await navigator.mediaDevices.getUserMedia({
1229
+ audio: {
1230
+ echoCancellation: true, // 🛡️ Prevent Speaker Feedack
1231
+ noiseSuppression: true, // 🔇 Remove Background Noise
1232
+ autoGainControl: true, // 🎚️ Normalize Volume
1233
+ channelCount: 1,
1234
+ sampleRate: 48000
1235
+ }
1236
+ });
1237
+
1238
+ // 1. Setup Audio Analysis (Silence Detection)
1239
+ audioContext = new (window.AudioContext || window.webkitAudioContext)();
1240
+
1241
+ // ⚡ EXPERT: Force Active Context (Wake up the Audio Engine)
1242
+ if (audioContext.state === 'suspended') {
1243
+ await audioContext.resume();
1244
+ console.log('⚡ AudioContext Force-Resumed');
1245
+ }
1246
+ analyser = audioContext.createAnalyser();
1247
+ micSource = audioContext.createMediaStreamSource(stream);
1248
+ micSource.connect(analyser);
1249
+ analyser.fftSize = 256;
1250
+ const bufferLength = analyser.frequencyBinCount;
1251
+ const dataArray = new Uint8Array(bufferLength);
1252
+
1253
+ let silenceStart = Date.now();
1254
+
1255
+ // Flag to track if human speech was actually detected
1256
+ let smartSpeechDetected = false;
1257
+
1258
+ function detectSilence() {
1259
+ if (!isRecording) return;
1260
+
1261
+ analyser.getByteFrequencyData(dataArray);
1262
+
1263
+ // Calculate average volume
1264
+ let sum = 0;
1265
+ for (let i = 0; i < bufferLength; i++) sum += dataArray[i];
1266
+ const average = sum / bufferLength;
1267
+
1268
+ // Visual feedback
1269
+ const scale = 1 + (average / 100);
1270
+ recordBtn.style.transform = `scale(${Math.min(scale, 1.2)})`;
1271
+
1272
+ // UI Feedback for waiting status
1273
+ if (average < VOLUME_THRESHOLD && !smartSpeechDetected) {
1274
+ statusText.innerText = '💤 En attente de parole...';
1275
+ statusText.style.color = 'rgba(255,255,255,0.7)';
1276
+ }
1277
+
1278
+ if (average < VOLUME_THRESHOLD) {
1279
+ // It is silent
1280
+ if (Date.now() - silenceStart > SILENCE_LIMIT_MS) {
1281
+ // Silence limit reached! Stop!
1282
+ console.log("🤫 Silence limit reached.");
1283
+ stopSmartRecording();
1284
+ return;
1285
+ }
1286
+ } else {
1287
+ // Sound detected!
1288
+ silenceStart = Date.now();
1289
+ if (!smartSpeechDetected) {
1290
+ smartSpeechDetected = true; // ✅ Valid speech detected
1291
+ console.log("🗣️ Speech detected!");
1292
+ statusText.innerText = '🎤 Je vous écoute...';
1293
+ statusText.style.color = '#fff';
1294
+ recordBtn.classList.add('active-speech');
1295
+ }
1296
+ }
1297
+
1298
+ animationId = requestAnimationFrame(detectSilence);
1299
+ }
1300
+
1301
+ detectSilence(); // Start monitoring
1302
+
1303
+ // 2. Start Speech Recognition (Instant Mode)
1304
+ try { startRealTimeTranscription(); } catch (e) { }
1305
+
1306
+ // 3. Start MediaRecorder
1307
+ mediaRecorder = new MediaRecorder(stream);
1308
+ audioChunks = [];
1309
+ mediaRecorder.ondataavailable = e => audioChunks.push(e.data);
1310
+ mediaRecorder.onstop = async () => {
1311
+ console.log("🛑 Recorder stopped. Processing audio...");
1312
+
1313
+ // Clean up
1314
+ if (recognition) { try { recognition.stop(); } catch (e) { } }
1315
+ if (animationId) cancelAnimationFrame(animationId);
1316
+ if (micSource) micSource.disconnect();
1317
+ if (audioContext) audioContext.close();
1318
+
1319
+ if (audioChunks.length > 0) {
1320
+ const blob = new Blob(audioChunks, { type: 'audio/wav' });
1321
+ console.log(`📦 Audio Data: ${blob.size} bytes`);
1322
+
1323
+ // FORCE PROCESSING - UI Feedback
1324
+ statusText.innerText = 'Traitement...';
1325
+ statusText.style.color = '#4a9b87';
1326
+
1327
+ try {
1328
+ await processAudio(blob);
1329
+ } catch (e) {
1330
+ console.error("Error in processAudio", e);
1331
+ statusText.innerText = 'Erreur';
1332
+ }
1333
+ } else {
1334
+ console.error("❌ Audio was empty!");
1335
+ statusText.innerText = 'Audio Vide';
1336
+ }
1337
+
1338
+ // 🔄 AUTO-RESTART LOOP (Crucial for Continuous Conversation)
1339
+ if (window.continuousMode) {
1340
+ // 🛑 CRITICAL FIX: DO NOT RESTART IF TTS IS PLAYING!
1341
+ if (isTTSPlaying && window.lastBotAudio) {
1342
+ console.log("⏸️ TTS Playing - Waiting for audio to finish before restarting...");
1343
+
1344
+ // Chain the restart to the onended event
1345
+ const originalEnded = window.lastBotAudio.onended;
1346
+ window.lastBotAudio.onended = () => {
1347
+ if (originalEnded) originalEnded();
1348
+ console.log("✅ TTS Finished - Restarting conversation loop");
1349
+ // Small delay to ensure clean state
1350
+ setTimeout(() => {
1351
+ if (window.continuousMode) listenContinuously();
1352
+ }, 100);
1353
+ };
1354
+ return;
1355
+ } else {
1356
+ // No audio playing, restart immediately
1357
+ console.log("🔄 Auto-restarting conversation loop (No TTS active)...");
1358
+ setTimeout(() => {
1359
+ if (window.continuousMode) listenContinuously();
1360
+ }, 100);
1361
+ }
1362
+ } else {
1363
+ statusText.innerText = 'Prêt';
1364
+ }
1365
+ };
1366
+ mediaRecorder.start();
1367
+ console.log("🎤 Recording started (with Auto-Stop)...");
1368
+
1369
+ } catch (err) {
1370
+ console.error(err);
1371
+ statusText.innerText = "Erreur Micro";
1372
+ isRecording = false;
1373
+ recordBtn.classList.remove('active');
1374
+ }
1375
+ }
1376
+
1377
+ function stopSmartRecording() {
1378
+ if (mediaRecorder && mediaRecorder.state !== 'inactive') mediaRecorder.stop();
1379
+ if (recognition) { try { recognition.stop(); } catch (e) { } }
1380
+ isRecording = false;
1381
+ recordBtn.classList.remove('active');
1382
+ statusText.innerText = 'Réflexion...';
1383
+ }
1384
+
1385
+ // [Function setupRealTimeTranscription removed - Consolidated into startRealTimeTranscription]
1386
+
1387
+ function debouncedStreamTranslation(text) {
1388
+ if (streamTimeout) clearTimeout(streamTimeout);
1389
+ streamTimeout = setTimeout(() => performStreamTranslation(text), 200);
1390
+ }
1391
+
1392
+ async function performStreamTranslation(text) {
1393
+ try {
1394
+ const res = await axios.post('/stream_text', {
1395
+ text: text,
1396
+ target_lang: quickLangSelector?.value || 'English'
1397
+ });
1398
+ if (res.data.translation) {
1399
+ translatedTextField.innerText = res.data.translation;
1400
+
1401
+ // ✨ SHOW TRANSLATION CARD - Make it visible!
1402
+ if (res.data.translation.trim().length > 0) {
1403
+ translatedTextField.style.opacity = '1';
1404
+ console.log('🌍 Real-time translation:', res.data.translation);
1405
+ }
1406
+ }
1407
+ } catch (e) { console.error("Stream Error", e); }
1408
+ }
1409
+
1410
+ // Helper function to analyze audio energy and detect silence
1411
+ function analyzeAudioEnergy(blob) {
1412
+ return new Promise((resolve) => {
1413
+ const reader = new FileReader();
1414
+ reader.readAsArrayBuffer(blob);
1415
+ reader.onloadend = async () => {
1416
+ try {
1417
+ const audioContext = new (window.AudioContext || window.webkitAudioContext)();
1418
+ const audioBuffer = await audioContext.decodeAudioData(reader.result);
1419
+
1420
+ // Get audio samples
1421
+ const channelData = audioBuffer.getChannelData(0);
1422
+
1423
+ // Calculate RMS (Root Mean Square) energy
1424
+ let sum = 0;
1425
+ for (let i = 0; i < channelData.length; i++) {
1426
+ sum += channelData[i] * channelData[i];
1427
+ }
1428
+ const rms = Math.sqrt(sum / channelData.length);
1429
+
1430
+ // Calculate peak amplitude
1431
+ let peak = 0;
1432
+ for (let i = 0; i < channelData.length; i++) {
1433
+ const abs = Math.abs(channelData[i]);
1434
+ if (abs > peak) peak = abs;
1435
+ }
1436
+
1437
+ // Duration in seconds
1438
+ const duration = audioBuffer.duration;
1439
+
1440
+ console.log(`🔊 Audio Analysis: RMS=${rms.toFixed(4)}, Peak=${peak.toFixed(4)}, Duration=${duration.toFixed(2)}s`);
1441
+
1442
+ // 🛡️ WAR MODE SENSITIVITY: Pick up even whispers (0.002)
1443
+ // Was 0.01 - Lowered to prevent cutting out during "Chaos"
1444
+ resolve({ rms, peak, duration, isSilent: rms < 0.002 && peak < 0.01 });
1445
+ } catch (e) {
1446
+ console.error('⚠️ Audio analysis failed:', e);
1447
+ resolve({ rms: 0, peak: 0, duration: 0, isSilent: true });
1448
+ }
1449
+ };
1450
+ });
1451
+ }
1452
+
1453
+ async function processAudio(blob, bypassSilenceCheck = false) {
1454
+ // 🚀 PREVENT DUPLICATE PROCESSING
1455
+ if (isProcessingAudio) {
1456
+ console.log('⚠️ Audio already being processed, skipping...');
1457
+ return;
1458
+ }
1459
+
1460
+ isProcessingAudio = true;
1461
+ recordBtn.classList.add('processing'); // 🔵 Visual Feedback: Blue Spinner
1462
+ recordBtn.classList.remove('active'); // Stop Red pulse
1463
+ recordBtn.classList.remove('active-speech'); // Stop Green pulse
1464
+
1465
+ // 🔊 CRITICAL: SILENCE DETECTION - Prevent hallucination
1466
+ // ⚡ OPTIMIZATION: If we already confirmed speech in monitorAudio, skip this heavy decoding!
1467
+ if (!bypassSilenceCheck) {
1468
+ const audioAnalysis = await analyzeAudioEnergy(blob);
1469
+
1470
+ // Reject if audio is too quiet (silence/background noise)
1471
+ if (audioAnalysis.isSilent) {
1472
+ console.warn('🔇 SILENCE DETECTED (Threshold check failed) - Skipping processing');
1473
+ console.log(`📊 Analysis: RMS=${audioAnalysis.rms}, Peak=${audioAnalysis.peak}`);
1474
+ statusText.innerText = 'Trop silencieux';
1475
+ isProcessingAudio = false;
1476
+
1477
+ // Reset UI
1478
+ // ⚡ WAR MODE: Restart INSTANTLY (100ms) insead of 1s
1479
+ setTimeout(() => {
1480
+ statusText.innerText = 'Prêt';
1481
+ // Force restart if in continuous mode!
1482
+ if (window.continuousMode) listenContinuously();
1483
+ }, 100);
1484
+ return;
1485
+ }
1486
+
1487
+ // Reject if audio is too short (likely just a click)
1488
+ if (audioAnalysis.duration < 0.5) {
1489
+ console.log(`⏱️ Audio too short (${audioAnalysis.duration.toFixed(2)}s) - Skipping`);
1490
+ statusText.innerText = 'Audio trop court';
1491
+ isProcessingAudio = false;
1492
+
1493
+ setTimeout(() => {
1494
+ statusText.innerText = 'Prêt';
1495
+ if (window.continuousMode) listenContinuously();
1496
+ }, 800);
1497
+ return;
1498
+ }
1499
+ } else {
1500
+ console.log("⚡ SPEED: Bypassing secondary silence check (Speech already confirmed)");
1501
+ }
1502
+
1503
+ console.log('✅ Audio validation passed - Processing...');
1504
+
1505
+ const startTime = Date.now();
1506
+ const reader = new FileReader();
1507
+ reader.readAsDataURL(blob);
1508
+ reader.onloadend = async () => {
1509
+ const base64 = reader.result.split(',')[1];
1510
+ try {
1511
+ // 🚀 STEAL THE MICROPHONE (Client-Side STT Injection)
1512
+ // Use the global variable captured by Web Speech API directly!
1513
+ // This is cleaner than reading DOM.
1514
+ let textInput = (window.currentTranscript || originalTextField.innerText || "").trim();
1515
+
1516
+ // Clean up placeholders
1517
+ textInput = textInput.replace('...', '').replace('🎤', '').trim();
1518
+
1519
+ // Filter out placeholder indicators
1520
+ if (textInput.includes('Écoute') || textInput.length < 2) {
1521
+ textInput = ''; // Empty = backend will use Whisper/Gemini transcription
1522
+ console.log('🎯 Using backend STT only (no client text available)');
1523
+ } else {
1524
+ console.log(`🎤 Client-Side STT Injected: "${textInput}" (Skipping Server STT)`);
1525
+ }
1526
+
1527
+ // Get languages from quick selectors
1528
+ const targetLangQuick = document.getElementById('target-lang-quick');
1529
+ const sourceLangQuick = document.getElementById('source-lang-quick');
1530
+ const selectedTarget = targetLangQuick?.value || quickLangSelector?.value || 'French';
1531
+ const selectedSource = sourceLangQuick?.value || 'auto';
1532
+
1533
+ const settings = {
1534
+ audio: base64,
1535
+ text_input: textInput, // Only send real transcribed text, not placeholders
1536
+ target_language: selectedTarget,
1537
+ source_language: selectedSource === 'auto' ? 'auto' : selectedSource, // Pass manual selection to backend
1538
+ stt_engine: localStorage.getItem('sttEngine') || 'openai-whisper', // ⚡ WHISPER (Requested by User)
1539
+ model: localStorage.getItem('aiModel') || 'gpt-4o-mini', // ✅ CHATGPT (Requested by User)
1540
+ tts_engine: localStorage.getItem('ttsEngine') || 'seamless', // 🔊 SEAMLESS TTS (Kaggle GPU - FREE!)
1541
+ openai_api_key: localStorage.getItem('openaiKey'),
1542
+ google_api_key: localStorage.getItem('googleKey'), // ✅ For Gemini STT
1543
+ openai_voice: localStorage.getItem('openaiVoice') || 'nova',
1544
+ elevenlabs_key: localStorage.getItem('elevenlabsKey'), // Fixed: elevenlabs_key not elevenlabs_api_key
1545
+ use_grammar_correction: localStorage.getItem('grammarCorrectionEnabled') !== 'false', // Default: enabled
1546
+ voice_gender_preference: localStorage.getItem('voiceGenderPreference') || 'auto' // 🎙️ Voice gender: auto/male/female
1547
+ };
1548
+
1549
+ console.log(`📝 Grammar Correction: ${settings.use_grammar_correction ? 'ENABLED (GPT)' : 'DISABLED (Direct Translation)'}`);
1550
+ console.log(`🎙️ Voice Gender Preference: ${settings.voice_gender_preference.toUpperCase()}`);
1551
+
1552
+
1553
+ // VOICE CLONING LOGIC - Check if enabled via toggle
1554
+ // 🚀 SPEED FIRST: Default is DISABLED for instant translations
1555
+ const voiceCloneEnabled = localStorage.getItem('voiceCloneEnabled') === 'true'; // Default: DISABLED
1556
+ const ttsEngine = settings.tts_engine;
1557
+
1558
+ console.log(`🎭 Voice Cloning Status: ${voiceCloneEnabled ? 'ENABLED' : 'DISABLED'}`);
1559
+
1560
+ // Send voice cloning data if enabled
1561
+ if (voiceCloneEnabled) {
1562
+ console.log('🎤 Voice Cloning ENABLED → Sending audio sample to server');
1563
+ settings.voice_audio = `data:audio/wav;base64,${base64}`;
1564
+ settings.voice_cloning = true;
1565
+ } else {
1566
+ console.log('🔇 Voice Cloning DISABLED → Using gender-matched fallback voices');
1567
+ settings.voice_cloning = false;
1568
+ }
1569
+
1570
+ const res = await axios.post('/process_audio', settings);
1571
+
1572
+ if (res.data.translated_text) {
1573
+ const translation = res.data.translated_text;
1574
+ const userText = settings.text_input;
1575
+
1576
+ console.log('✅ Response received:', {
1577
+ original: userText?.substring(0, 50),
1578
+ translation: translation?.substring(0, 50),
1579
+ hasAudio: !!res.data.tts_audio
1580
+ });
1581
+
1582
+ // 🔊 FORCE DISPLAY RESULT (Fallback)
1583
+ const resultDisplay = document.getElementById('result-display');
1584
+ const originalDisplay = document.getElementById('original-display');
1585
+ const translationDisplay = document.getElementById('translation-display');
1586
+ const pronunciationDisplay = document.getElementById('pronunciation-display');
1587
+ const greeting = document.getElementById('greeting');
1588
+
1589
+ if (resultDisplay && translationDisplay) {
1590
+ if (greeting) greeting.style.display = 'none';
1591
+ resultDisplay.style.display = 'block';
1592
+ if (originalDisplay) originalDisplay.innerText = userText || 'Audio input';
1593
+
1594
+ // Pronunciation
1595
+ if (pronunciationDisplay) {
1596
+ const pronunciation = res.data.pronunciation;
1597
+ if (pronunciation && pronunciation !== translation) {
1598
+ pronunciationDisplay.innerText = pronunciation;
1599
+ pronunciationDisplay.style.display = 'block';
1600
+ } else {
1601
+ pronunciationDisplay.style.display = 'none';
1602
+ }
1603
+ }
1604
+
1605
+ translationDisplay.innerText = translation;
1606
+ console.log('📺 Result displayed on screen');
1607
+ }
1608
+
1609
+ // 🔊 FORCE PLAY AUDIO
1610
+ if (res.data.tts_audio) {
1611
+ const audioSrc = `data:audio/mp3;base64,${res.data.tts_audio}`;
1612
+ const audio = new Audio(audioSrc);
1613
+ audio.play().then(() => {
1614
+ console.log('🔊 Audio playing!');
1615
+ }).catch(err => {
1616
+ console.log('❌ Auto-play blocked:', err);
1617
+ // Show play button
1618
+ if (translationDisplay) {
1619
+ translationDisplay.innerHTML += ' <button onclick="this.previousSibling.click()" style="background:#4CAF50;color:white;border:none;padding:5px 10px;border-radius:5px;cursor:pointer;">▶️ Play</button>';
1620
+ }
1621
+ });
1622
+ window.lastAudio = audio;
1623
+ }
1624
+
1625
+ // 🛡️ HALLUCINATION CHECK - Block fake Whisper outputs
1626
+ // userText already defined above
1627
+ if (isHallucination(userText) || isHallucination(translation)) {
1628
+ console.log(`🚫 HALLUCINATION DETECTED - Skipping message creation`);
1629
+ console.log(` User: "${userText}" | Translation: "${translation}"`);
1630
+ statusText.innerText = 'Prêt';
1631
+ isProcessingAudio = false;
1632
+ recordBtn.classList.remove('processing');
1633
+ return;
1634
+ }
1635
+
1636
+ // AUTOMATIC LANGUAGE DETECTION - Update UI
1637
+ if (res.data.source_language_full && sourceLangSelector) {
1638
+ const detectedLang = res.data.source_language_full;
1639
+
1640
+ // Store detected language for potential future auto-selection
1641
+ detectedLanguage = detectedLang;
1642
+ console.log(`🌍 Language auto-detected: ${detectedLang}`);
1643
+
1644
+ // Update real-time transcription language for next recording
1645
+ if (recognition) {
1646
+ const langMap = {
1647
+ 'English': 'en-US', 'French': 'fr-FR', 'Spanish': 'es-ES',
1648
+ 'German': 'de-DE', 'Italian': 'it-IT', 'Portuguese': 'pt-PT',
1649
+ 'Russian': 'ru-RU', 'Japanese': 'ja-JP', 'Korean': 'ko-KR',
1650
+ 'Chinese': 'zh-CN', 'Arabic': 'ar-SA', 'Hindi': 'hi-IN',
1651
+ 'Dutch': 'nl-NL', 'Polish': 'pl-PL', 'Turkish': 'tr-TR',
1652
+ 'Indonesian': 'id-ID', 'Malay': 'ms-MY', 'Thai': 'th-TH',
1653
+ 'Vietnamese': 'vi-VN', 'Bengali': 'bn-IN', 'Urdu': 'ur-PK',
1654
+ 'Swahili': 'sw-KE', 'Hebrew': 'he-IL', 'Persian': 'fa-IR',
1655
+ 'Ukrainian': 'uk-UA', 'Swedish': 'sv-SE', 'Greek': 'el-GR',
1656
+ 'Czech': 'cs-CZ', 'Romanian': 'ro-RO', 'Hungarian': 'hu-HU',
1657
+ 'Danish': 'da-DK', 'Finnish': 'fi-FI', 'Norwegian': 'no-NO',
1658
+ 'Slovak': 'sk-SK', 'Filipino': 'fil-PH', 'Amharic': 'am-ET'
1659
+ };
1660
+
1661
+ const speechLang = langMap[detectedLang] || navigator.language || 'en-US';
1662
+ console.log(`🎤 Speech recognition updated to: ${speechLang}`);
1663
+ }
1664
+ }
1665
+
1666
+ // Hide greeting
1667
+ const greetingEl = document.getElementById('greeting');
1668
+ if (greetingEl) greetingEl.style.display = 'none';
1669
+
1670
+ // 2. Add User Message to Chat (with source language)
1671
+ // userText already defined above for hallucination check
1672
+ const sourceLang = res.data.source_language_full || 'Auto';
1673
+ const targetLang = res.data.target_language || 'Translation';
1674
+ createChatMessage('user', userText, null, null, sourceLang);
1675
+
1676
+ // 3. Create NEW audio for this message
1677
+ let messageAudioSrc = null;
1678
+ if (res.data.tts_audio) {
1679
+ messageAudioSrc = `data:audio/mp3;base64,${res.data.tts_audio}`;
1680
+ // Play on global player
1681
+ audioPlayer.src = messageAudioSrc;
1682
+ audioPlayer.play().catch(err => {
1683
+ console.log('Auto-play blocked:', err);
1684
+ });
1685
+ }
1686
+
1687
+ // 4. Add Bot Message with its OWN Audio Player (with target language)
1688
+ const info = {
1689
+ latency: ((Date.now() - startTime) / 1000).toFixed(2),
1690
+ stt: res.data.stt_engine,
1691
+ translation: res.data.translation_engine,
1692
+ tts: res.data.tts_engine
1693
+ };
1694
+ createChatMessage('bot', translation, messageAudioSrc, info, targetLang);
1695
+
1696
+ // 🎙️ IMPORTANT: Update status based on mode
1697
+ if (window.continuousMode) {
1698
+ // Keep button active in continuous mode
1699
+ statusText.innerText = 'Écoute en continu...';
1700
+ console.log('✅ TTS généré - En attente de la prochaine phrase');
1701
+ } else {
1702
+ // Normal mode: reset button
1703
+ isRecording = false;
1704
+ recordBtn.classList.remove('active');
1705
+ recordBtn.disabled = false;
1706
+ statusText.innerText = 'Prêt';
1707
+ console.log('✅ TTS généré - Bouton prêt');
1708
+ }
1709
+ }
1710
+ } catch (e) {
1711
+ console.error("Erreur de traitement:", e);
1712
+ statusText.innerText = "Erreur de connexion";
1713
+
1714
+ // Re-enable button even on error
1715
+ isRecording = false;
1716
+ recordBtn.classList.remove('active');
1717
+ recordBtn.disabled = false;
1718
+ }
1719
+ finally {
1720
+ // Ensure button is always ready
1721
+ recordBtn.disabled = false;
1722
+ recordBtn.classList.remove('processing'); // 🔵 Stop Blue Spinner
1723
+ isProcessingAudio = false; // 🚀 Reset processing flag
1724
+
1725
+ // If NOT continuous mode, ensure text says Ready
1726
+ if (!window.continuousMode) {
1727
+ statusText.innerText = 'Prêt';
1728
+ }
1729
+ }
1730
+ };
1731
+ }
1732
+
1733
+ // Logic de sauvegarde/chargement des paramètres
1734
+ window.loadModalSettings = () => {
1735
+ document.getElementById('stt-engine').value = localStorage.getItem('sttEngine') || 'openai-whisper';
1736
+ document.getElementById('openai-key').value = localStorage.getItem('openaiKey') || '';
1737
+ if (localStorage.getItem('sourceLang')) document.getElementById('source-lang-selector').value = localStorage.getItem('sourceLang');
1738
+
1739
+ // 🎯 Set default target language for bidirectional translation
1740
+ const savedTargetLang = localStorage.getItem('targetLang');
1741
+ if (savedTargetLang && quickLangSelector) {
1742
+ quickLangSelector.value = savedTargetLang;
1743
+ } else if (quickLangSelector) {
1744
+ // Default to French for Arabic ↔ French bidirectional translation
1745
+ quickLangSelector.value = 'French';
1746
+ console.log('🌍 Default target language set to French for bidirectional translation');
1747
+ }
1748
+ };
1749
+
1750
+ window.saveModalSettings = () => {
1751
+ localStorage.setItem('sttEngine', document.getElementById('stt-engine').value);
1752
+ localStorage.setItem('openaiKey', document.getElementById('openai-key').value);
1753
+ localStorage.setItem('targetLang', quickLangSelector.value);
1754
+ localStorage.setItem('sourceLang', document.getElementById('source-lang-selector').value);
1755
+ };
1756
+
1757
+ // Removed: replay-trigger button (deleted from HTML)
1758
+ // Functionality removed as button no longer exists
1759
+
1760
+ // ===================================
1761
+ // 🛠️ BUTTON TOGGLE LOGIC (FIXED)
1762
+ // ===================================
1763
+
1764
+ function setupToggle(id, storageKey, defaultValue, onToggle) {
1765
+ const btn = document.getElementById(id);
1766
+ if (!btn) return;
1767
+
1768
+ // Load initial state
1769
+ const saved = localStorage.getItem(storageKey);
1770
+ const isActive = saved === null ? defaultValue : saved === 'true';
1771
+
1772
+ if (isActive) btn.classList.add('active');
1773
+ else btn.classList.remove('active');
1774
+
1775
+ btn.addEventListener('click', (e) => {
1776
+ e.stopPropagation(); // Prevent bubbling
1777
+ const currentlyActive = btn.classList.contains('active');
1778
+ const newState = !currentlyActive;
1779
+
1780
+ // Toggle visual
1781
+ if (newState) btn.classList.add('active');
1782
+ else btn.classList.remove('active');
1783
+
1784
+ // Save state
1785
+ localStorage.setItem(storageKey, newState);
1786
+
1787
+ // Optional callback
1788
+ if (onToggle) onToggle(newState);
1789
+
1790
+ console.log(`🔘 Toggle ${id}: ${newState ? 'ON' : 'OFF'}`);
1791
+ });
1792
+ }
1793
+
1794
+ function setupCycle(id, storageKey, values, onCycle) {
1795
+ const btn = document.getElementById(id);
1796
+ if (!btn) return;
1797
+
1798
+ // Load initial state
1799
+ let currentVal = localStorage.getItem(storageKey) || values[0];
1800
+ if (!values.includes(currentVal)) currentVal = values[0]; // Fallback
1801
+
1802
+ const updateVisual = (val) => {
1803
+ // Remove all active classes first if needed, or just set generic active
1804
+ // For gender, we might want different icons?
1805
+ // For now, simple active state if not default
1806
+ if (val !== values[0]) btn.classList.add('active');
1807
+ else btn.classList.remove('active');
1808
+
1809
+ // Tooltip feedback
1810
+ btn.title = `Mode: ${val.toUpperCase()}`;
1811
+ };
1812
+
1813
+ updateVisual(currentVal);
1814
+
1815
+ btn.addEventListener('click', (e) => {
1816
+ e.stopPropagation();
1817
+ const currentIndex = values.indexOf(currentVal);
1818
+ const nextIndex = (currentIndex + 1) % values.length;
1819
+ currentVal = values[nextIndex];
1820
+
1821
+ localStorage.setItem(storageKey, currentVal);
1822
+ updateVisual(currentVal);
1823
+
1824
+ if (onCycle) onCycle(currentVal);
1825
+ console.log(`🔄 Cycle ${id}: ${currentVal}`);
1826
+
1827
+ // Visual text feedback (Toast)
1828
+ statusText.innerText = `Mode: ${currentVal.toUpperCase()}`;
1829
+ setTimeout(() => statusText.innerText = 'Prêt', 1500);
1830
+ });
1831
+ }
1832
+
1833
+ // 🎯 Initialize Toggles when DOM is ready
1834
+ document.addEventListener('DOMContentLoaded', () => {
1835
+ // 1. Magic/Grammar Toggle
1836
+ setupToggle('grammar-toggle', 'grammarCorrectionEnabled', true, (state) => {
1837
+ statusText.innerText = state ? '✨ Correction: ON' : '📝 Correction: OFF';
1838
+ setTimeout(() => statusText.innerText = 'Prêt', 1500);
1839
+ });
1840
+
1841
+ // 2. Voice Gender Toggle (Auto -> Male -> Female)
1842
+ setupCycle('voice-gender-toggle', 'voiceGenderPreference', ['auto', 'male', 'female']);
1843
+
1844
+ // 3. Smart Mode Toggle (Brain)
1845
+ setupToggle('smart-mode-toggle', 'smartModeEnabled', true, (state) => {
1846
+ statusText.innerText = state ? '🧠 Mode Smart: ON' : '🧠 Mode Smart: OFF';
1847
+ setTimeout(() => statusText.innerText = 'Prêt', 1500);
1848
+ });
1849
+ });
1850
+
1851
+ // ⚙️ SETTINGS MODAL LOGIC (Fixing the "Broken Button")
1852
+ document.addEventListener('DOMContentLoaded', () => {
1853
+ const settingsBtn = document.getElementById('settings-trigger'); // Found ID
1854
+ const closeSettingsBtn = document.getElementById('close-settings');
1855
+ const settingsModal = document.getElementById('settings-modal');
1856
+
1857
+ // Open
1858
+ if (settingsBtn && settingsModal) {
1859
+ settingsBtn.addEventListener('click', () => {
1860
+ settingsModal.style.display = 'flex'; // Or remove hidden class
1861
+ // settingsModal.classList.remove('hidden'); // If using classes
1862
+ console.log('⚙️ Settings Opened');
1863
+ });
1864
+ } else {
1865
+ console.error('❌ Settings Trigger or Modal NOT FOUND');
1866
+ }
1867
+
1868
+ // Close Button
1869
+ if (closeSettingsBtn && settingsModal) {
1870
+ closeSettingsBtn.addEventListener('click', () => {
1871
+ settingsModal.style.display = 'none';
1872
+ });
1873
+ }
1874
+
1875
+ // Close on Outside Click
1876
+ window.addEventListener('click', (e) => {
1877
+ if (e.target === settingsModal) {
1878
+ settingsModal.style.display = 'none';
1879
+ }
1880
+ });
1881
+
1882
+ // 💾 SAVE & LOAD LOGIC
1883
+ const saveBtn = document.getElementById('save-settings');
1884
+ const aiSelector = document.getElementById('ai-model-selector');
1885
+ const ttsSelector = document.getElementById('tts-selector');
1886
+
1887
+ // Load Initial Values
1888
+ if (aiSelector) aiSelector.value = localStorage.getItem('aiModel') || 'gpt-4o-mini';
1889
+ if (ttsSelector) ttsSelector.value = localStorage.getItem('ttsEngine') || 'openai';
1890
+
1891
+ // Save Handler
1892
+ if (saveBtn) {
1893
+ saveBtn.addEventListener('click', () => {
1894
+ if (aiSelector) {
1895
+ localStorage.setItem('aiModel', aiSelector.value);
1896
+ console.log(`🧠 AI Model set to: ${aiSelector.value}`);
1897
+ }
1898
+ if (ttsSelector) {
1899
+ localStorage.setItem('ttsEngine', ttsSelector.value);
1900
+ console.log(`🗣️ TTS Engine set to: ${ttsSelector.value}`);
1901
+ }
1902
+
1903
+ // Close Modal
1904
+ if (settingsModal) settingsModal.style.display = 'none';
1905
+
1906
+ // Feedback
1907
+ if (statusText) {
1908
+ statusText.innerText = '✅ Sauvegardé!';
1909
+ setTimeout(() => statusText.innerText = 'Prêt', 2000);
1910
+ }
1911
+ });
1912
+ }
1913
+ });
1914
+
1915
+
1916
+ // 🎯 Initialize language settings on page load
1917
+ document.addEventListener('DOMContentLoaded', () => {
1918
+ // Set default target language for bidirectional translation
1919
+ if (quickLangSelector) {
1920
+ const savedTargetLang = localStorage.getItem('targetLang');
1921
+ if (savedTargetLang) {
1922
+ quickLangSelector.value = savedTargetLang;
1923
+ console.log(`🌍 Loaded saved target language: ${savedTargetLang}`);
1924
+ } else {
1925
+ quickLangSelector.value = 'French';
1926
+ console.log('🌍 Default target language set to French for Arabic ↔ French bidirectional translation');
1927
+ }
1928
+ }
1929
+
1930
+ // Set source to auto for automatic detection
1931
+ const sourceLangSelector = document.getElementById('source-lang-selector');
1932
+ if (sourceLangSelector) {
1933
+ sourceLangSelector.value = 'auto';
1934
+ console.log('🎯 Source language set to AUTO for automatic detection');
1935
+
1936
+ // 🔄 HARD SYNC: If user changes Source, clear "Smart History" to prevent confusion
1937
+ sourceLangSelector.addEventListener('change', function () {
1938
+ console.log('🔄 Source Language Changed -> Clearing Smart History...');
1939
+ localStorage.setItem('sourceLang', this.value);
1940
+ fetch('/clear_cache', { method: 'POST' });
1941
+ });
1942
+ }
1943
+
1944
+ // 🔄 HARD SYNC: If user changes Target, clear "Smart History" to prevent confusion
1945
+ if (quickLangSelector) {
1946
+ quickLangSelector.addEventListener('change', function () {
1947
+ console.log('🔄 Target Language Changed -> Clearing Smart History...');
1948
+ localStorage.setItem('targetLang', this.value);
1949
+ fetch('/clear_cache', { method: 'POST' });
1950
+ });
1951
+ }
1952
+
1953
+ // ===================================
1954
+ // 🎭 VOICE CLONING TOGGLE
1955
+ // ===================================
1956
+ let voiceCloneEnabled = localStorage.getItem('voiceCloneEnabled') !== 'false'; // Default: ON
1957
+
1958
+ const voiceCloneToggle = document.getElementById('voice-clone-toggle');
1959
+ if (voiceCloneToggle) {
1960
+ // Set initial state
1961
+ if (voiceCloneEnabled) {
1962
+ voiceCloneToggle.classList.add('active');
1963
+ } else {
1964
+ voiceCloneToggle.classList.remove('active');
1965
+ }
1966
+
1967
+ // Toggle on click
1968
+ voiceCloneToggle.addEventListener('click', function () {
1969
+ voiceCloneEnabled = !voiceCloneEnabled;
1970
+ localStorage.setItem('voiceCloneEnabled', voiceCloneEnabled);
1971
+
1972
+ if (voiceCloneEnabled) {
1973
+ this.classList.add('active');
1974
+ console.log('🎭 Voice Cloning: ON');
1975
+ } else {
1976
+ this.classList.remove('active');
1977
+ console.log('🎭 Voice Cloning: OFF');
1978
+ }
1979
+ });
1980
+ }
1981
+
1982
+ });
templates/admin.html ADDED
@@ -0,0 +1,357 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+
4
+ <head>
5
+ <meta charset="UTF-8">
6
+ <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
7
+ <title>Admin Control Panel | Instant Translator</title>
8
+ <link href="https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;500;600;700&display=swap" rel="stylesheet">
9
+ <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
10
+ <style>
11
+ :root {
12
+ --primary: #6366f1;
13
+ --surface: #09090b;
14
+ --surface-light: #18181b;
15
+ --text: #e4e4e7;
16
+ --danger: #ef4444;
17
+ --success: #22c55e;
18
+ }
19
+
20
+ body {
21
+ font-family: 'Outfit', sans-serif;
22
+ background-color: #000;
23
+ color: var(--text);
24
+ margin: 0;
25
+ padding: 20px;
26
+ -webkit-font-smoothing: antialiased;
27
+ }
28
+
29
+ .admin-container {
30
+ max-width: 800px;
31
+ margin: 0 auto;
32
+ }
33
+
34
+ .header {
35
+ display: flex;
36
+ justify-content: space-between;
37
+ align-items: center;
38
+ margin-bottom: 30px;
39
+ padding-bottom: 20px;
40
+ border-bottom: 1px solid #27272a;
41
+ }
42
+
43
+ h1 {
44
+ margin: 0;
45
+ font-size: 24px;
46
+ background: linear-gradient(135deg, #fff 0%, #a5b4fc 100%);
47
+ -webkit-background-clip: text;
48
+ -webkit-text-fill-color: transparent;
49
+ }
50
+
51
+ .card {
52
+ background: var(--surface-light);
53
+ border-radius: 16px;
54
+ padding: 24px;
55
+ margin-bottom: 20px;
56
+ border: 1px solid #27272a;
57
+ }
58
+
59
+ .card h2 {
60
+ margin-top: 0;
61
+ font-size: 18px;
62
+ color: #a1a1aa;
63
+ margin-bottom: 20px;
64
+ }
65
+
66
+ .stat-grid {
67
+ display: grid;
68
+ grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
69
+ gap: 15px;
70
+ }
71
+
72
+ .stat-box {
73
+ background: #27272a;
74
+ padding: 15px;
75
+ border-radius: 12px;
76
+ text-align: center;
77
+ }
78
+
79
+ .stat-value {
80
+ font-size: 24px;
81
+ font-weight: 700;
82
+ color: #fff;
83
+ }
84
+
85
+ .stat-label {
86
+ font-size: 12px;
87
+ color: #a1a1aa;
88
+ margin-top: 5px;
89
+ }
90
+
91
+ .form-group {
92
+ margin-bottom: 15px;
93
+ }
94
+
95
+ .form-group label {
96
+ display: block;
97
+ margin-bottom: 8px;
98
+ font-size: 14px;
99
+ color: #a1a1aa;
100
+ }
101
+
102
+ .form-group input {
103
+ width: 100%;
104
+ padding: 12px;
105
+ background: #000;
106
+ border: 1px solid #27272a;
107
+ border-radius: 8px;
108
+ color: #fff;
109
+ font-family: inherit;
110
+ box-sizing: border-box;
111
+ }
112
+
113
+ .form-group input:focus {
114
+ outline: none;
115
+ border-color: var(--primary);
116
+ }
117
+
118
+ .btn {
119
+ background: var(--primary);
120
+ color: white;
121
+ border: none;
122
+ padding: 12px 24px;
123
+ border-radius: 8px;
124
+ font-weight: 600;
125
+ cursor: pointer;
126
+ width: 100%;
127
+ font-size: 16px;
128
+ transition: opacity 0.2s;
129
+ }
130
+
131
+ .btn:hover {
132
+ opacity: 0.9;
133
+ }
134
+
135
+ .btn-danger {
136
+ background: var(--danger);
137
+ }
138
+
139
+ .login-screen {
140
+ position: fixed;
141
+ inset: 0;
142
+ background: #000;
143
+ z-index: 100;
144
+ display: flex;
145
+ align-items: center;
146
+ justify-content: center;
147
+ }
148
+
149
+ .login-box {
150
+ width: 100%;
151
+ max-width: 400px;
152
+ padding: 30px;
153
+ }
154
+
155
+ .status-badge {
156
+ display: inline-block;
157
+ padding: 4px 12px;
158
+ border-radius: 20px;
159
+ font-size: 12px;
160
+ font-weight: 600;
161
+ background: #27272a;
162
+ }
163
+
164
+ .status-badge.online {
165
+ background: rgba(34, 197, 94, 0.2);
166
+ color: var(--success);
167
+ }
168
+ </style>
169
+ </head>
170
+
171
+ <body>
172
+
173
+ <!-- LOGIN SCREEN -->
174
+ <div id="login-screen" class="login-screen">
175
+ <div class="card login-box">
176
+ <h2 style="text-align: center; color: white;">Admin Access</h2>
177
+ <div class="form-group">
178
+ <label>Admin Password</label>
179
+ <input type="password" id="admin-pass" placeholder="Enter PIN">
180
+ </div>
181
+ <button class="btn" onclick="login()">Unlock Panel</button>
182
+ </div>
183
+ </div>
184
+
185
+ <!-- DASHBOARD -->
186
+ <div class="admin-container">
187
+ <div class="header">
188
+ <div>
189
+ <h1>Admin Control Panel</h1>
190
+ <div style="margin-top: 5px; color: #a1a1aa; font-size: 13px;">
191
+ <span class="status-badge online"><i class="fa-solid fa-circle" style="font-size: 8px;"></i> System
192
+ Online</span>
193
+ v2.5 Mobile Ready
194
+ </div>
195
+ </div>
196
+ <button class="btn" style="width: auto; background: #27272a;" onclick="logout()"><i
197
+ class="fa-solid fa-power-off"></i></button>
198
+ </div>
199
+
200
+ <!-- STATS -->
201
+ <div class="card">
202
+ <h2>📊 Live Statistics</h2>
203
+ <div class="stat-grid">
204
+ <div class="stat-box">
205
+ <div class="stat-value" id="stat-requests">0</div>
206
+ <div class="stat-label">Total Translations</div>
207
+ </div>
208
+ <div class="stat-box">
209
+ <div class="stat-value" id="stat-errors">0</div>
210
+ <div class="stat-label">API Errors</div>
211
+ </div>
212
+ <div class="stat-box">
213
+ <div class="stat-value" id="stat-cost">$0.00</div>
214
+ <div class="stat-label">Est. Cost</div>
215
+ </div>
216
+ </div>
217
+ </div>
218
+
219
+ <!-- MASTER KEYS -->
220
+ <div class="card">
221
+ <h2>🔑 Master API Configuration</h2>
222
+ <p style="font-size: 13px; color: #71717a; margin-bottom: 20px;">
223
+ These keys are stored securely on the server. App users will use these keys via the API without ever
224
+ seeing them.
225
+ <b>Required for App Store compliance.</b>
226
+ </p>
227
+
228
+ <div class="form-group">
229
+ <label>OpenAI API Key (Universal)</label>
230
+ <input type="password" id="key-openai" placeholder="sk-...">
231
+ </div>
232
+
233
+ <div class="form-group">
234
+ <label>ElevenLabs API Key</label>
235
+ <input type="password" id="key-eleven" placeholder="xi-...">
236
+ </div>
237
+
238
+ <div class="form-group">
239
+ <label>Google Cloud API Key</label>
240
+ <input type="password" id="key-google" placeholder="AIza...">
241
+ </div>
242
+
243
+ <!-- TTS ENGINE SELECTOR -->
244
+ <div class="form-group" style="border-top: 1px solid #27272a; padding-top: 20px; margin-top: 20px;">
245
+ <label style="margin-bottom: 12px;">🎤 TTS Engine Selection</label>
246
+ <div
247
+ style="display: flex; gap: 10px; align-items: center; background: #27272a; padding: 12px; border-radius: 8px;">
248
+ <div style="flex: 1; text-align: center; padding: 10px; border-radius: 6px; cursor: pointer; transition: 0.3s; background: #6366f1; color: white;"
249
+ id="tts-openai" onclick="selectTTS('openai')">
250
+ <div style="font-weight: 600;">OpenAI TTS</div>
251
+ <div style="font-size: 11px; opacity: 0.8; margin-top: 4px;">$0.015/1K · HD Quality</div>
252
+ </div>
253
+ <div style="flex: 1; text-align: center; padding: 10px; border-radius: 6px; cursor: pointer; transition: 0.3s; background: transparent; border: 1px solid #3f3f46; color: #a1a1aa;"
254
+ id="tts-elevenlabs" onclick="selectTTS('elevenlabs')">
255
+ <div style="font-weight: 600;">ElevenLabs</div>
256
+ <div style="font-size: 11px; opacity: 0.8; margin-top: 4px">$0.30/1K · Ultra Premium</div>
257
+ </div>
258
+ </div>
259
+ <div style="margin-top: 10px; font-size: 12px; color: #71717a; text-align: center;" id="tts-status">
260
+ Current: <span style="color: #6366f1; font-weight: 600;">OpenAI TTS</span>
261
+ </div>
262
+ </div>
263
+
264
+ <button class="btn" onclick="saveKeys()">Save Master Configuration</button>
265
+ </div>
266
+
267
+ <!-- SYSTEM CONTROL -->
268
+ <div class="card">
269
+ <h2>🛡️ System Controls</h2>
270
+ <div class="form-group">
271
+ <label style="display: flex; align-items: center; justify-content: space-between;">
272
+ Example Mode (Free for users)
273
+ <input type="checkbox" style="width: auto;" checked>
274
+ </label>
275
+ </div>
276
+ <button class="btn btn-danger" onclick="clearLogs()">Clear Server Logs</button>
277
+ </div>
278
+ </div>
279
+
280
+ <script>
281
+ // Simple client-side gate (Real auth is server-side)
282
+ function login() {
283
+ const pass = document.getElementById('admin-pass').value;
284
+ if (pass === 'admin123') { // Placeholder logic
285
+ document.getElementById('login-screen').style.display = 'none';
286
+ loadStats();
287
+ } else {
288
+ alert('Access Denied');
289
+ }
290
+ }
291
+
292
+ function logout() {
293
+ location.reload();
294
+ }
295
+
296
+ function loadStats() {
297
+ // Simulator
298
+ document.getElementById('stat-requests').innerText = Math.floor(Math.random() * 500) + 120;
299
+ document.getElementById('stat-errors').innerText = '0';
300
+
301
+ // Load saved TTS engine
302
+ const savedEngine = localStorage.getItem('ttsEngine') || 'openai';
303
+ selectTTS(savedEngine, false);
304
+ }
305
+
306
+ function selectTTS(engine, save = true) {
307
+ const openaiBtn = document.getElementById('tts-openai');
308
+ const elevenlabsBtn = document.getElementById('tts-elevenlabs');
309
+ const status = document.getElementById('tts-status');
310
+
311
+ if (engine === 'openai') {
312
+ // OpenAI selected
313
+ openaiBtn.style.background = '#6366f1';
314
+ openaiBtn.style.color = 'white';
315
+ openaiBtn.style.border = 'none';
316
+
317
+ elevenlabsBtn.style.background = 'transparent';
318
+ elevenlabsBtn.style.color = '#a1a1aa';
319
+ elevenlabsBtn.style.border = '1px solid #3f3f46';
320
+
321
+ status.innerHTML = 'Current: <span style="color: #6366f1; font-weight: 600;">OpenAI TTS</span>';
322
+ } else {
323
+ // ElevenLabs selected
324
+ elevenlabsBtn.style.background = '#6366f1';
325
+ elevenlabsBtn.style.color = 'white';
326
+ elevenlabsBtn.style.border = 'none';
327
+
328
+ openaiBtn.style.background = 'transparent';
329
+ openaiBtn.style.color = '#a1a1aa';
330
+ openaiBtn.style.border = '1px solid #3f3f46';
331
+
332
+ status.innerHTML = 'Current: <span style="color: #6366f1; font-weight: 600;">ElevenLabs</span>';
333
+ }
334
+
335
+ // Save to localStorage
336
+ if (save) {
337
+ localStorage.setItem('ttsEngine', engine);
338
+ console.log('TTS Engine switched to:', engine);
339
+ }
340
+ }
341
+
342
+ function saveKeys() {
343
+ const btn = document.querySelector('button[onclick="saveKeys()"]');
344
+ btn.innerText = 'Saving...';
345
+ setTimeout(() => {
346
+ btn.innerText = '✅ Saved Securely';
347
+ btn.style.background = '#22c55e';
348
+ setTimeout(() => {
349
+ btn.innerText = 'Save Master Configuration';
350
+ btn.style.background = '#6366f1';
351
+ }, 2000);
352
+ }, 800);
353
+ }
354
+ </script>
355
+ </body>
356
+
357
+ </html>
templates/index.html ADDED
@@ -0,0 +1,914 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="fr">
3
+
4
+ <head>
5
+ <meta charset="UTF-8">
6
+ <meta name="viewport"
7
+ content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover">
8
+
9
+ <!-- ✨ WORLD-CHANGING MOBILE EXPERIENCE (PWA) -->
10
+ <meta name="theme-color" content="#0a0a0b">
11
+ <meta name="apple-mobile-web-app-capable" content="yes">
12
+ <meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
13
+ <meta name="application-name" content="Babel">
14
+ <meta name="apple-mobile-web-app-title" content="Babel">
15
+
16
+ <!-- DISABLE BROWSER CACHING for instant loading -->
17
+ <meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate">
18
+ <meta http-equiv="Pragma" content="no-cache">
19
+ <meta http-equiv="Expires" content="0">
20
+
21
+ <title>Babel</title>
22
+ <link
23
+ href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=Noto+Sans+Arabic:wght@400;500;600&family=JetBrains+Mono:wght@400;500&display=swap"
24
+ rel="stylesheet">
25
+ <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
26
+ <link rel="stylesheet" href="/static/enhanced.css">
27
+ <link rel="manifest" href="/static/manifest.json">
28
+ <style>
29
+ /* CSS moved to static/enhanced.css */
30
+ </style>
31
+ </head>
32
+
33
+ <body>
34
+ <div class="app-layout">
35
+ <!-- –✨ GROK SIDEBAR -->
36
+ <aside class="grok-sidebar">
37
+ <div class="sidebar-top">
38
+ <div class="sidebar-logo">
39
+ <i class="fa-solid fa-cube"></i>
40
+ </div>
41
+ <button class="new-chat" onclick="location.reload()" title="New Session">
42
+ <i class="fa-solid fa-plus"></i>
43
+ </button>
44
+ </div>
45
+
46
+ <nav class="sidebar-menu">
47
+ <div class="menu-item active">
48
+ <i class="fa-solid fa-comment-dots"></i>
49
+ <span>Chat</span>
50
+ </div>
51
+ <div class="menu-item" id="sidebar-voice-mode">
52
+ <i class="fa-solid fa-microphone-lines"></i>
53
+ <span>Voice</span>
54
+ </div>
55
+ <!-- Settings Trigger Moved Here -->
56
+ <div class="menu-item" id="settings-trigger">
57
+ <i class="fa-solid fa-sliders"></i>
58
+ <span>Settings</span>
59
+ </div>
60
+ </nav>
61
+
62
+ <div class="sidebar-footer">
63
+ <div class="user-profile">
64
+ <div class="avatar">U</div>
65
+ <div class="info">
66
+ <span class="name">User</span>
67
+ <span class="status">Pro</span>
68
+ </div>
69
+ </div>
70
+ </div>
71
+ </aside>
72
+
73
+ <!-- –✨ MAIN CANVAS -->
74
+ <main class="grok-main">
75
+ <!-- Header for Mobile/Context -->
76
+ <header class="main-header">
77
+ <span class="model-badge">Babel v2.0</span>
78
+ </header>
79
+
80
+ <!-- CHAT STAGE -->
81
+ <div id="chat-stage" class="chat-stage">
82
+ <!-- Empty State (Centered Logo) -->
83
+ <div class="empty-state" id="greeting">
84
+ <div class="grok-logo-hero">Babel</div>
85
+ <p class="grok-hero-text">Intelligence Vocale Instantanee</p>
86
+ </div>
87
+
88
+ <!-- RESULT DISPLAY (Fallback if chat doesn't show) -->
89
+ <div id="result-display" style="display: none; padding: 20px; text-align: center;">
90
+ <div id="original-display" style="font-size: 1.1em; color: #aaa; margin-bottom: 10px;"></div>
91
+ <div id="translation-display" style="font-size: 1.5em; color: #fff; font-weight: bold;"></div>
92
+ </div>
93
+
94
+ <div id="chat-history" class="chat-history"></div>
95
+ </div>
96
+
97
+ <!-- GROK INPUT BAR AREA -->
98
+ <div class="grok-input-wrapper">
99
+
100
+ <!-- The Main Input Pill -->
101
+ <div class="grok-input-bar">
102
+ <div class="input-icon left">
103
+ <i class="fa-solid fa-paperclip"></i>
104
+ </div>
105
+
106
+ <!-- Status Text acting as Placeholder -->
107
+ <div class="input-status-area">
108
+ <span id="status-placeholder" class="status-text">Prêt à traduire...</span>
109
+ <span id="latency-badge"
110
+ style="display:none; font-size: 0.7em; color: #666; margin-left: 10px;">0ms</span>
111
+ </div>
112
+
113
+ <!-- THE ORB (Record Button) -->
114
+ <div class="input-icon right action" id="record-btn">
115
+ <i class="fa-solid fa-microphone"></i>
116
+ </div>
117
+ </div>
118
+
119
+ <!-- SUGGESTION CHIPS (Controls) -->
120
+ <div class="grok-chips">
121
+
122
+ <!-- Source Language -->
123
+ <div class="chip-select-wrapper">
124
+ <select id="source-lang-quick" class="chip-select">
125
+ <option value="auto">🎯 Auto</option>
126
+ <option value="ar-SA">🇲🇦 Darija</option>
127
+ <option value="fr-FR">🇫🇷 Français</option>
128
+ <option value="en-US">🇬🇧 English</option>
129
+ <option value="es-ES">🇪🇸 Español</option>
130
+ </select>
131
+ <i class="fa-solid fa-chevron-down"></i>
132
+ </div>
133
+
134
+ <!-- Swap -->
135
+ <button id="swap-langs" class="chip-icon" title="Swap">
136
+ <i class="fa-solid fa-arrow-right-arrow-left"></i>
137
+ </button>
138
+
139
+ <!-- Target Language (SeamlessM4T TTS supported) -->
140
+ <div class="chip-select-wrapper">
141
+ <select id="target-lang-quick" class="chip-select">
142
+ <option value="en-US" selected>🇬🇧 English</option>
143
+ <option value="fr-FR">🇫🇷 Français</option>
144
+ <option value="ar-SA">🇲🇦 Darija</option>
145
+ <option value="es-ES">🇪🇸 Español</option>
146
+ <option value="de-DE">🇩🇪 Deutsch</option>
147
+ <option value="it-IT">🇮🇹 Italiano</option>
148
+ <option value="pt-PT">🇵🇹 Português</option>
149
+ <option value="zh-CN">🇨🇳 中文</option>
150
+ <option value="ja-JP">🇯🇵 日本語</option>
151
+ <option value="ko-KR">🇰🇷 한국어</option>
152
+ <option value="ru-RU">🇷🇺 Русский</option>
153
+ <option value="tr-TR">🇹🇷 Türkçe</option>
154
+ </select>
155
+ <i class="fa-solid fa-chevron-down"></i>
156
+ </div>
157
+
158
+ <div class="chip-divider"></div>
159
+
160
+ <!-- Features -->
161
+ <button id="smart-mode-toggle" class="chip-pill active"
162
+ title="Auto-detect language and smart target">
163
+ <i class="fa-solid fa-brain"></i> Smart
164
+ </button>
165
+
166
+ <!-- Voice Cloning Toggle -->
167
+ <button id="voice-clone-toggle" class="chip-pill active" title="Clone voice from input audio">
168
+ <i class="fa-solid fa-user-secret"></i> Clone Voice
169
+ </button>
170
+ </div>
171
+ </div>
172
+ </main>
173
+
174
+ <!-- AUDIO PLAYER (Hidden - for TTS playback) -->
175
+ <audio id="audio-player" style="display: none;"></audio>
176
+ </div>
177
+
178
+ <!-- SETTINGS MODAL (Preserved ID) -->
179
+ <div class="modal" id="settings-modal">
180
+ <button id="close-settings" class="close-modal-btn">
181
+ <i class="fa-solid fa-xmark"></i>
182
+ </button>
183
+ <div class="modal-content">
184
+ <h2>Parametres</h2>
185
+ <div class="form-group">
186
+ <label>Votre Langue</label>
187
+ <select id="source-lang-selector" class="form-control" style="height: auto; max-height: 200px;">
188
+ <option value="auto">Detection Automatique</option>
189
+ <optgroup label="Langues Principales">
190
+ <option value="fr">Français</option>
191
+ <option value="ar">Darija / Arabe</option>
192
+ <option value="en">Anglais</option>
193
+ <option value="es">Espagnol</option>
194
+ <option value="de">Allemand</option>
195
+ <option value="it">Italien</option>
196
+ <option value="zh">Chinois</option>
197
+ <option value="ja">Japonais</option>
198
+ <option value="ru">Russe</option>
199
+ </optgroup>
200
+ <optgroup label="✨ Toutes les Langues">
201
+ <option value="af">Afrikaans</option>
202
+ <option value="sq">Albanian (Albanais)</option>
203
+ <option value="am">Amharic (Amharique)</option>
204
+ <option value="hy">Armenian (Arménien)</option>
205
+ <option value="az">Azerbaijani (Azéri)</option>
206
+ <option value="eu">Basque</option>
207
+ <option value="be">Belarusian (Biélorusse)</option>
208
+ <option value="bn">Bengali</option>
209
+ <option value="bs">Bosnian (Bosnien)</option>
210
+ <option value="bg">Bulgarian (Bulgare)</option>
211
+ <option value="ca">Catalan</option>
212
+ <option value="ceb">Cebuano</option>
213
+ <option value="co">Corsican (Corse)</option>
214
+ <option value="hr">Croatian (Croate)</option>
215
+ <option value="cs">Czech (Tchèque)</option>
216
+ <option value="da">Danish (Danois)</option>
217
+ <option value="nl">Dutch (Néerlandais)</option>
218
+ <option value="et">Estonian (Estonien)</option>
219
+ <option value="fi">Finnish (Finnois)</option>
220
+ <option value="gl">Galician (Galicien)</option>
221
+ <option value="ka">Georgian (Géorgien)</option>
222
+ <option value="el">Greek (Grec)</option>
223
+ <option value="gu">Gujarati</option>
224
+ <option value="ht">Haitian Creole (Créole Haïtien)</option>
225
+ <option value="ha">Hausa</option>
226
+ <option value="haw">Hawaiian (Hawaïen)</option>
227
+ <option value="he">Hebrew (Hébreu)</option>
228
+ <option value="hi">Hindi</option>
229
+ <option value="hmn">Hmong</option>
230
+ <option value="hu">Hungarian (Hongrois)</option>
231
+ <option value="is">Icelandic (Islandais)</option>
232
+ <option value="ig">Igbo</option>
233
+ <option value="id">Indonesian (Indonésien)</option>
234
+ <option value="ga">Irish (Irlandais)</option>
235
+ <option value="jw">Javanese (Javanais)</option>
236
+ <option value="kn">Kannada</option>
237
+ <option value="kk">Kazakh</option>
238
+ <option value="km">Khmer</option>
239
+ <option value="ko">Korean (Coréen)</option>
240
+ <option value="ku">Kurdish (Kurde)</option>
241
+ <option value="ky">Kyrgyz (Kirghize)</option>
242
+ <option value="lo">Lao</option>
243
+ <option value="la">Latin</option>
244
+ <option value="lv">Latvian (Letton)</option>
245
+ <option value="lt">Lithuanian (Lituanien)</option>
246
+ <option value="lb">Luxembourgish (Luxembourgeois)</option>
247
+ <option value="mk">Macedonian (Macédonien)</option>
248
+ <option value="mg">Malagasy (Malgache)</option>
249
+ <option value="ms">Malay (Malais)</option>
250
+ <option value="ml">Malayalam</option>
251
+ <option value="mt">Maltese (Maltais)</option>
252
+ <option value="mi">Maori</option>
253
+ <option value="mr">Marathi</option>
254
+ <option value="mn">Mongolian (Mongol)</option>
255
+ <option value="my">Myanmar (Birman)</option>
256
+ <option value="ne">Nepali (Népalais)</option>
257
+ <option value="no">Norwegian (Norvégien)</option>
258
+ <option value="ny">Nyanja (Chichewa)</option>
259
+ <option value="ps">Pashto (Pachto)</option>
260
+ <option value="fa">Persian (Persan/Farsi)</option>
261
+ <option value="pl">Polish (Polonais)</option>
262
+ <option value="pa">Punjabi</option>
263
+ <option value="ro">Romanian (Roumain)</option>
264
+ <option value="sm">Samoan</option>
265
+ <option value="gd">Scottish Gaelic</option>
266
+ <option value="sr">Serbian (Serbe)</option>
267
+ <option value="sn">Shona</option>
268
+ <option value="sd">Sindhi</option>
269
+ <option value="si">Sinhala (Cingalais)</option>
270
+ <option value="sk">Slovak (Slovaque)</option>
271
+ <option value="sl">Slovenian (Slovêne)</option>
272
+ <option value="so">Somali</option>
273
+ <option value="su">Sundanese (Soundanais)</option>
274
+ <option value="sw">Swahili</option>
275
+ <option value="sv">Swedish (Suédois)</option>
276
+ <option value="tl">Tagalog (Tagalog/Filipino)</option>
277
+ <option value="tg">Tajik (Tadjik)</option>
278
+ <option value="ta">Tamil</option>
279
+ <option value="te">Telugu</option>
280
+ <option value="th">Thai (Thaï)</option>
281
+ <option value="tr">Turkish (Turc)</option>
282
+ <option value="uk">Ukrainian (Ukrainien)</option>
283
+ <option value="ur">Urdu</option>
284
+ <option value="uz">Uzbek (Ouzbek)</option>
285
+ <option value="vi">Vietnamese (Vietnamien)</option>
286
+ <option value="cy">Welsh (Gallois)</option>
287
+ <option value="xh">Xhosa</option>
288
+ <option value="yi">Yiddish</option>
289
+ <option value="yo">Yoruba</option>
290
+ <option value="zu">Zulu</option>
291
+ <div class="form-group">
292
+ <label>Langue Cible</label>
293
+ <!-- ✨ UNIVERSAL LANGUAGE SELECTOR -->
294
+ <select id="target-lang-quick" class="lang-quick-select">
295
+ <option value="French">‡«‡· French</option>
296
+ <option value="Arabic">‡¸‡¦ Arabic</option>
297
+ <option value="English">‡¬‡§ English</option>
298
+ <option value="Darija">‡²‡¦ Darija</option>
299
+ <option value="Spanish">‡ª‡¸ Spanish</option>
300
+
301
+ <optgroup label="Ϭ African & Middle Eastern">
302
+ <option value="Amharic">‡ª‡¹ Amharic</option>
303
+ <option value="Arabic">‡¸‡¦ Arabic (Standard)</option>
304
+ <option value="Darija">‡²‡¦ Arabic (Moroccan Darija)</option>
305
+ <option value="Berber">™“ Amazigh (Berber)</option>
306
+ <option value="Egyptian">‡ª‡¬ Arabic (Egyptian)</option>
307
+ <option value="Hausa">‡³‡¬ Hausa</option>
308
+ <option value="Hebrew">‡®‡± Hebrew</option>
309
+ <option value="Igbo">‡³‡¬ Igbo</option>
310
+ <option value="Persian">‡®‡· Persian (Farsi)</option>
311
+ <option value="Somali">‡¸‡´ Somali</option>
312
+ <option value="Swahili">‡°‡ª Swahili</option>
313
+ <option value="Turkish">‡¹‡· Turkish</option>
314
+ <option value="Yoruba">‡³‡¬ Yoruba</option>
315
+ <option value="Zulu">‡¿‡¦ Zulu</option>
316
+ </optgroup>
317
+
318
+ <optgroup label="✨ Asian & Pacific">
319
+ <option value="Bengali">‡§‡© Bengali</option>
320
+ <option value="Chinese">‡¨‡³ Chinese (Mandarin)</option>
321
+ <option value="Cantonese">‡­‡° Chinese (Cantonese)</option>
322
+ <option value="Filipino">‡µ‡­ Filipino (Tagalog)</option>
323
+ <option value="Gujarati">‡®‡³ Gujarati</option>
324
+ <option value="Hindi">‡®‡³ Hindi</option>
325
+ <option value="Indonesian">‡®‡© Indonesian</option>
326
+ <option value="Japanese">‡¯‡µ Japanese</option>
327
+ <option value="Javanese">‡®‡© Javanese</option>
328
+ <option value="Kannada">‡®‡³ Kannada</option>
329
+ <option value="Khmer">‡°‡­ Khmer</option>
330
+ <option value="Korean">‡°‡· Korean</option>
331
+ <option value="Lao">‡±‡¦ Lao</option>
332
+ <option value="Malay">‡²‡¾ Malay</option>
333
+ <option value="Malayalam">‡®‡³ Malayalam</option>
334
+ <option value="Marathi">‡®‡³ Marathi</option>
335
+ <option value="Myanmar">‡²‡² Myanmar (Burmese)</option>
336
+ <option value="Nepali">‡³‡µ Nepali</option>
337
+ <option value="Punjabi">‡®‡³ Punjabi</option>
338
+ <option value="Sinhala">‡±‡° Sinhala</option>
339
+ <option value="Tamil">‡®‡³ Tamil</option>
340
+ <option value="Telugu">‡®‡³ Telugu</option>
341
+ <option value="Thai">‡¹‡­ Thai</option>
342
+ <option value="Urdu">‡µ‡° Urdu</option>
343
+ <option value="Vietnamese">‡»‡³ Vietnamese</option>
344
+ </optgroup>
345
+
346
+ <optgroup label="✨ European">
347
+ <option value="Albanian">‡¦‡± Albanian</option>
348
+ <option value="Armenian">‡¦‡² Armenian</option>
349
+ <option value="Azerbaijani">‡¦‡¿ Azerbaijani</option>
350
+ <option value="Bosnian">‡§‡¦ Bosnian</option>
351
+ <option value="Bulgarian">‡§‡¬ Bulgarian</option>
352
+ <option value="Catalan">‡ª‡¸ Catalan</option>
353
+ <option value="Croatian">‡­‡· Croatian</option>
354
+ <option value="Czech">‡¨‡¿ Czech</option>
355
+ <option value="Danish">‡©‡° Danish</option>
356
+ <option value="Dutch">‡³‡± Dutch</option>
357
+ <option value="Estonian">‡ª‡ª Estonian</option>
358
+ <option value="Finnish">‡«‡® Finnish</option>
359
+ <option value="French">‡«‡· French</option>
360
+ <option value="Georgian">‡¬‡ª Georgian</option>
361
+ <option value="German">‡©‡ª German</option>
362
+ <option value="Greek">‡¬‡· Greek</option>
363
+ <option value="Hungarian">‡­‡º Hungarian</option>
364
+ <option value="Icelandic">‡®‡¸ Icelandic</option>
365
+ <option value="Irish">‡®‡ª Irish</option>
366
+ <option value="Italian">‡®‡¹ Italian</option>
367
+ <option value="Latvian">‡±‡» Latvian</option>
368
+ <option value="Lithuanian">‡±‡¹ Lithuanian</option>
369
+ <option value="Macedonian">‡²‡° Macedonian</option>
370
+ <option value="Maltese">‡²‡¹ Maltese</option>
371
+ <option value="Norwegian">‡³‡´ Norwegian</option>
372
+ <option value="Polish">‡µ‡± Polish</option>
373
+ <option value="Portuguese">‡µ‡¹ Portuguese</option>
374
+ <option value="Romanian">‡·‡´ Romanian</option>
375
+ <option value="Russian">‡·‡º Russian</option>
376
+ <option value="Serbian">‡·‡¸ Serbian</option>
377
+ <option value="Slovak">‡¸‡° Slovak</option>
378
+ <option value="Slovenian">‡¸‡® Slovenian</option>
379
+ <option value="Spanish">‡ª‡¸ Spanish</option>
380
+ <option value="Swedish">‡¸‡ª Swedish</option>
381
+ <option value="Ukrainian">‡º‡¦ Ukrainian</option>
382
+ <option value="Welsh">´ó §ó ¢ó ·ó ¬ó ³ó ¿ Welsh</option>
383
+ </optgroup>
384
+ </select>
385
+ </div>
386
+
387
+ <!-- §  AI MODEL SELECTOR (New) -->
388
+ <div class="form-group" style="margin-top: 16px;">
389
+ <label>Mode d'Intelligence (Cerveau)</label>
390
+ <select id="ai-model-selector" class="form-control">
391
+ <option value="gpt-4o-mini">Ÿ¢ OpenAI GPT-4o (Stable & Illimité)</option>
392
+ <option value="gemini-flash-latest">š¡ Google Gemini Flash (Experimental)</option>
393
+ <option value="gemini-pro">’Ž Google Gemini Pro (Balanced)</option>
394
+ </select>
395
+ </div>
396
+
397
+
398
+ <audio id="audio-player"></audio>
399
+
400
+ <script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
401
+ <script src="/static/enhanced.js"></script>
402
+ <script src="/static/script.js"></script>
403
+
404
+
405
+ <script>
406
+ // Console log helper
407
+ function log(m) { console.log(m); }
408
+
409
+ // Settings Modal
410
+ const modal = document.getElementById('settings-modal');
411
+ document.getElementById('settings-trigger').onclick = () => modal.classList.add('open');
412
+ document.getElementById('close-settings').onclick = () => modal.classList.remove('open');
413
+ document.getElementById('save-settings').onclick = () => {
414
+ if (window.saveModalSettings) window.saveModalSettings();
415
+ modal.classList.remove('open');
416
+ };
417
+
418
+ // UI SYNC - Chat messages are handled by script.js createChatMessage()
419
+ // This observer is only for real-time status updates
420
+ const greeting = document.getElementById('greeting');
421
+ const status = document.getElementById('status-placeholder');
422
+
423
+ const observer = new MutationObserver((mutations) => {
424
+ mutations.forEach(m => {
425
+ if (m.target.id === 'original-text') {
426
+ const txt = m.target.innerText;
427
+ if (txt && txt !== '...') {
428
+ status.innerText = txt.substring(0, 50) + (txt.length > 50 ? '...' : '');
429
+ greeting.style.display = 'none';
430
+ }
431
+ }
432
+ });
433
+ });
434
+
435
+ observer.observe(document.getElementById('original-text'), { childList: true, characterData: true, subtree: true });
436
+
437
+ // SMART CONVERSATION MODE TOGGLE
438
+ const smartModeToggle = document.getElementById('smart-mode-toggle');
439
+ const statusPlaceholder = document.getElementById('status-placeholder');
440
+
441
+ const savedSmartMode = localStorage.getItem('smartModeEnabled');
442
+ if (savedSmartMode === 'false') {
443
+ smartModeToggle.classList.remove('active');
444
+ } else {
445
+ smartModeToggle.classList.add('active');
446
+ localStorage.setItem('smartModeEnabled', 'true');
447
+ }
448
+
449
+ smartModeToggle.onclick = () => {
450
+ smartModeToggle.classList.toggle('active');
451
+ const isActive = smartModeToggle.classList.contains('active');
452
+ localStorage.setItem('smartModeEnabled', isActive);
453
+
454
+ // Update status
455
+ if (isActive) {
456
+ console.log('Smart Conversation Mode ENABLED');
457
+ statusPlaceholder.innerText = 'Mode intelligent';
458
+ setTimeout(() => {
459
+ if (statusPlaceholder.innerText === 'Mode intelligent') {
460
+ statusPlaceholder.innerText = 'Pret';
461
+ }
462
+ }, 2000);
463
+ } else {
464
+ console.log('Smart Mode DISABLED - Using fixed target');
465
+ statusPlaceholder.innerText = 'Cible fixe';
466
+ setTimeout(() => {
467
+ if (statusPlaceholder.innerText === 'Cible fixe') {
468
+ statusPlaceholder.innerText = 'Pret';
469
+ }
470
+ }, 2000);
471
+ }
472
+ };
473
+
474
+ document.addEventListener('reset-ui', () => {
475
+ document.getElementById('status-placeholder').innerText = 'Pret';
476
+ });
477
+
478
+ // VOICE CLONING TOGGLE
479
+ let voiceCloneEnabled = localStorage.getItem('voiceCloneEnabled') === 'true';
480
+ const cloneToggle = document.getElementById('clone-toggle');
481
+
482
+ if (cloneToggle) {
483
+ function updateCloneToggle() {
484
+ if (voiceCloneEnabled) {
485
+ cloneToggle.classList.add('active');
486
+ cloneToggle.title = 'Clonage de Voix: Active';
487
+ } else {
488
+ cloneToggle.classList.remove('active');
489
+ cloneToggle.title = 'Clonage de Voix: Desactive';
490
+ }
491
+ }
492
+
493
+ cloneToggle.addEventListener('click', () => {
494
+ voiceCloneEnabled = !voiceCloneEnabled;
495
+ localStorage.setItem('voiceCloneEnabled', voiceCloneEnabled);
496
+ updateCloneToggle();
497
+ });
498
+
499
+ updateCloneToggle();
500
+ }
501
+
502
+ // GRAMMAR CORRECTION TOGGLE
503
+ let grammarCorrectionEnabled = localStorage.getItem('grammarCorrectionEnabled') !== 'false';
504
+ const grammarToggle = document.getElementById('grammar-toggle');
505
+
506
+ if (grammarToggle) {
507
+ function updateGrammarToggle() {
508
+ if (grammarCorrectionEnabled) {
509
+ grammarToggle.classList.add('active');
510
+ grammarToggle.title = 'Correction Intelligente: Active';
511
+ } else {
512
+ grammarToggle.classList.remove('active');
513
+ grammarToggle.title = 'Correction Intelligente: Desactive';
514
+ }
515
+ }
516
+
517
+ grammarToggle.addEventListener('click', () => {
518
+ grammarCorrectionEnabled = !grammarCorrectionEnabled;
519
+ localStorage.setItem('grammarCorrectionEnabled', grammarCorrectionEnabled);
520
+ updateGrammarToggle();
521
+ });
522
+
523
+ updateGrammarToggle();
524
+ }
525
+
526
+ // AI CORRECTION TOGGLE (Claude/GPT Reasoning)
527
+ // Read from localStorage - default is FALSE (disabled by default)
528
+ const savedAiCorrection = localStorage.getItem('aiCorrectionEnabled');
529
+ let aiCorrectionEnabled = savedAiCorrection === 'true'; // Only true if explicitly set to 'true'
530
+ console.log('📦 AI Correction from localStorage:', savedAiCorrection, '→', aiCorrectionEnabled);
531
+
532
+ const aiCorrectionToggle = document.getElementById('ai-correction-toggle');
533
+ const aiCorrectionHidden = document.getElementById('ai-correction');
534
+ const aiCorrectionCheckbox = document.getElementById('ai-correction-checkbox');
535
+
536
+ function updateAiCorrectionState() {
537
+ console.log('🔄 AI Correction State Update:', aiCorrectionEnabled);
538
+
539
+ // Update hidden input
540
+ if (aiCorrectionHidden) aiCorrectionHidden.value = aiCorrectionEnabled ? 'true' : 'false';
541
+
542
+ // Update checkbox in settings
543
+ if (aiCorrectionCheckbox) aiCorrectionCheckbox.checked = aiCorrectionEnabled;
544
+
545
+ // Update toggle button visual state
546
+ if (aiCorrectionToggle) {
547
+ if (aiCorrectionEnabled) {
548
+ aiCorrectionToggle.classList.add('active');
549
+ aiCorrectionToggle.style.background = 'linear-gradient(135deg, #667eea, #764ba2)';
550
+ aiCorrectionToggle.style.color = '#fff';
551
+ aiCorrectionToggle.title = '🧠 AI Correction: ACTIVÉ (Claude/GPT)';
552
+ aiCorrectionToggle.innerHTML = '<i class="fa-solid fa-sparkles"></i> AI ✓';
553
+ } else {
554
+ aiCorrectionToggle.classList.remove('active');
555
+ aiCorrectionToggle.style.background = 'rgba(255,255,255,0.1)';
556
+ aiCorrectionToggle.style.color = 'rgba(255,255,255,0.5)';
557
+ aiCorrectionToggle.title = '❌ AI Correction: DÉSACTIVÉ';
558
+ aiCorrectionToggle.innerHTML = '<i class="fa-solid fa-sparkles"></i> AI ✗';
559
+ }
560
+ }
561
+ }
562
+
563
+ if (aiCorrectionToggle) {
564
+ aiCorrectionToggle.addEventListener('click', (e) => {
565
+ e.preventDefault();
566
+ e.stopPropagation();
567
+
568
+ // Toggle the state
569
+ aiCorrectionEnabled = !aiCorrectionEnabled;
570
+
571
+ // Save to localStorage as string
572
+ localStorage.setItem('aiCorrectionEnabled', aiCorrectionEnabled.toString());
573
+
574
+ // Update UI
575
+ updateAiCorrectionState();
576
+
577
+ // Log for debugging
578
+ console.log('🧠 AI Correction TOGGLED to:', aiCorrectionEnabled ? 'ENABLED ✓' : 'DISABLED ✗');
579
+
580
+ // Visual feedback
581
+ aiCorrectionToggle.style.transform = 'scale(0.95)';
582
+ setTimeout(() => aiCorrectionToggle.style.transform = '', 150);
583
+ });
584
+ }
585
+
586
+ if (aiCorrectionCheckbox) {
587
+ aiCorrectionCheckbox.addEventListener('change', () => {
588
+ aiCorrectionEnabled = aiCorrectionCheckbox.checked;
589
+ localStorage.setItem('aiCorrectionEnabled', aiCorrectionEnabled.toString());
590
+ updateAiCorrectionState();
591
+ console.log('🧠 AI Correction (checkbox) set to:', aiCorrectionEnabled);
592
+ });
593
+ }
594
+
595
+ // Initialize state on page load
596
+ updateAiCorrectionState();
597
+ console.log('✅ AI Correction Toggle initialized:', aiCorrectionEnabled ? 'ON' : 'OFF');
598
+
599
+ // 🔄 SWAP LANGUAGES BUTTON
600
+ const swapBtn = document.getElementById('swap-langs');
601
+ // Using getElementById directly to avoid redeclaration
602
+ const srcLang = document.getElementById('source-lang-quick');
603
+ const tgtLang = document.getElementById('target-lang-quick');
604
+
605
+ if (swapBtn && srcLang && tgtLang) {
606
+ swapBtn.addEventListener('click', (e) => {
607
+ e.preventDefault();
608
+
609
+ // Get current values
610
+ const currentSource = srcLang.value;
611
+ const currentTarget = tgtLang.value;
612
+
613
+ // Swap them
614
+ if (currentSource !== 'auto') {
615
+ // Find matching option in target
616
+ const targetOptions = Array.from(tgtLang.options).map(o => o.value);
617
+ const sourceOptions = Array.from(srcLang.options).map(o => o.value);
618
+
619
+ if (targetOptions.includes(currentSource) && sourceOptions.includes(currentTarget)) {
620
+ srcLang.value = currentTarget;
621
+ tgtLang.value = currentSource;
622
+ } else {
623
+ // Fallback: just swap to auto and the target
624
+ srcLang.value = 'auto';
625
+ tgtLang.value = currentSource === 'auto' ? 'fr-FR' : currentSource;
626
+ }
627
+ } else {
628
+ // Source is auto - swap target with a default
629
+ const oldTarget = currentTarget;
630
+ tgtLang.value = oldTarget === 'fr-FR' ? 'ar-SA' : 'fr-FR';
631
+ }
632
+
633
+ // Save to localStorage
634
+ localStorage.setItem('sourceLangQuick', srcLang.value);
635
+ localStorage.setItem('targetLangQuick', tgtLang.value);
636
+
637
+ // Visual feedback
638
+ swapBtn.style.transform = 'rotate(180deg)';
639
+ setTimeout(() => swapBtn.style.transform = '', 300);
640
+
641
+ console.log(`🔄 Languages swapped: ${srcLang.value} → ${tgtLang.value}`);
642
+ });
643
+ }
644
+
645
+ // 🧠 SMART AUTO-SWAP: When language is detected, auto-swap target
646
+ // This is handled in processAudio response
647
+
648
+ // STT ENGINE SELECTOR
649
+ const sttSelector = document.getElementById('stt-selector-settings');
650
+ const sttEngineHidden = document.getElementById('stt-engine');
651
+ const savedSttEngine = localStorage.getItem('sttEngine') || 'seamless-m4t';
652
+
653
+ if (sttSelector) {
654
+ sttSelector.value = savedSttEngine;
655
+ if (sttEngineHidden) sttEngineHidden.value = savedSttEngine;
656
+
657
+ sttSelector.addEventListener('change', function () {
658
+ localStorage.setItem('sttEngine', this.value);
659
+ if (sttEngineHidden) sttEngineHidden.value = this.value;
660
+ console.log('🎤 STT Engine set to:', this.value);
661
+ });
662
+ }
663
+
664
+ // 🔊 TTS ENGINE SELECTOR
665
+ const ttsSelector = document.getElementById('tts-selector');
666
+ const ttsEngineHidden = document.getElementById('tts-engine');
667
+ const savedTtsEngine = localStorage.getItem('ttsEngine') || 'seamless';
668
+
669
+ if (ttsSelector) {
670
+ ttsSelector.value = savedTtsEngine;
671
+ if (ttsEngineHidden) ttsEngineHidden.value = savedTtsEngine;
672
+
673
+ ttsSelector.addEventListener('change', function () {
674
+ localStorage.setItem('ttsEngine', this.value);
675
+ if (ttsEngineHidden) ttsEngineHidden.value = this.value;
676
+ console.log('🔊 TTS Engine set to:', this.value);
677
+
678
+ // Visual feedback
679
+ this.style.backgroundColor = this.value === 'seamless' ? '#4CAF50' : '#2196F3';
680
+ setTimeout(() => this.style.backgroundColor = '', 500);
681
+ });
682
+ }
683
+
684
+ // 🌍 TRANSLATION ENGINE SELECTOR (NLLB vs MarianMT)
685
+ const translationEngineSelector = document.getElementById('translation-engine-selector');
686
+ const translationEngineHidden = document.getElementById('translation-engine');
687
+ const nllbModelSizeSelector = document.getElementById('nllb-model-size-selector');
688
+ const nllbModelSizeHidden = document.getElementById('nllb-model-size');
689
+ const nllbModelSizeGroup = document.getElementById('nllb-model-size-group');
690
+
691
+ const savedTranslationEngine = localStorage.getItem('translationEngine') || 'nllb';
692
+ const savedNllbModelSize = localStorage.getItem('nllbModelSize') || 'fast';
693
+
694
+ function updateNllbModelSizeVisibility() {
695
+ if (nllbModelSizeGroup) {
696
+ nllbModelSizeGroup.style.display =
697
+ translationEngineSelector && translationEngineSelector.value === 'nllb' ? 'block' : 'none';
698
+ }
699
+ }
700
+
701
+ if (translationEngineSelector) {
702
+ translationEngineSelector.value = savedTranslationEngine;
703
+ if (translationEngineHidden) translationEngineHidden.value = savedTranslationEngine;
704
+ updateNllbModelSizeVisibility();
705
+
706
+ translationEngineSelector.addEventListener('change', function () {
707
+ localStorage.setItem('translationEngine', this.value);
708
+ if (translationEngineHidden) translationEngineHidden.value = this.value;
709
+ console.log('🌍 Translation Engine set to:', this.value);
710
+ updateNllbModelSizeVisibility();
711
+ });
712
+ }
713
+
714
+ if (nllbModelSizeSelector) {
715
+ nllbModelSizeSelector.value = savedNllbModelSize;
716
+ if (nllbModelSizeHidden) nllbModelSizeHidden.value = savedNllbModelSize;
717
+
718
+ nllbModelSizeSelector.addEventListener('change', function () {
719
+ localStorage.setItem('nllbModelSize', this.value);
720
+ if (nllbModelSizeHidden) nllbModelSizeHidden.value = this.value;
721
+ console.log('🧠 NLLB Model Size set to:', this.value);
722
+ });
723
+ }
724
+
725
+
726
+ // SMART CONVERSATION MODE TOGGLE
727
+ const smartModeToggle = document.getElementById('smart-mode-toggle');
728
+ // ... (existing code for smart mode)
729
+
730
+ // REPLAY BUTTON LOGIC
731
+ const replayBtn = document.getElementById('replay-trigger');
732
+ if (replayBtn) {
733
+ replayBtn.addEventListener('click', () => {
734
+ if (window.lastBotAudio) {
735
+ console.log('”„ Replaying last audio...');
736
+ // Add visual effect
737
+ const icon = replayBtn.querySelector('i');
738
+ icon.style.transition = 'transform 0.5s ease';
739
+ icon.style.transform = 'rotate(-360deg)';
740
+
741
+ window.lastBotAudio.currentTime = 0;
742
+ window.lastBotAudio.play().catch(e => console.error("Replay failed:", e));
743
+
744
+ setTimeout(() => {
745
+ icon.style.transition = 'none';
746
+ icon.style.transform = 'rotate(0deg)';
747
+ }, 500);
748
+ } else {
749
+ // No audio to replay - shake animation
750
+ console.log('š ï¸ No audio to replay');
751
+ replayBtn.style.animation = 'shake 0.4s cubic-bezier(.36,.07,.19,.97) both';
752
+ setTimeout(() => replayBtn.style.animation = '', 400);
753
+ }
754
+ });
755
+ }
756
+
757
+ // Add Shake Animation Keyframes
758
+ const styleSheet = document.createElement("style");
759
+ styleSheet.innerText = `
760
+ @keyframes shake {
761
+ 10%, 90% { transform: translate3d(-1px, 0, 0); }
762
+ 20%, 80% { transform: translate3d(2px, 0, 0); }
763
+ 30%, 50%, 70% { transform: translate3d(-4px, 0, 0); }
764
+ 40%, 60% { transform: translate3d(4px, 0, 0); }
765
+ }
766
+ `;
767
+ document.head.appendChild(styleSheet);
768
+
769
+ // VOICE GENDER SELECTOR (3 states: auto, male, female)
770
+ let voiceGenderPreference = localStorage.getItem('voiceGenderPreference') || 'auto';
771
+ const voiceGenderToggle = document.getElementById('voice-gender-toggle');
772
+
773
+ if (voiceGenderToggle) {
774
+ const icons = {
775
+ 'auto': 'fa-user-gear',
776
+ 'male': 'fa-person',
777
+ 'female': 'fa-person-dress'
778
+ };
779
+
780
+ const titles = {
781
+ 'auto': 'Voix: Auto',
782
+ 'male': 'Voix: Masculine',
783
+ 'female': 'Voix: Feminine'
784
+ };
785
+
786
+ function updateVoiceGenderToggle() {
787
+ voiceGenderToggle.classList.remove('auto', 'male', 'female');
788
+ voiceGenderToggle.classList.add(voiceGenderPreference);
789
+
790
+ const iconElement = voiceGenderToggle.querySelector('i');
791
+ iconElement.className = `fa-solid ${icons[voiceGenderPreference]}`;
792
+ voiceGenderToggle.title = titles[voiceGenderPreference];
793
+ }
794
+
795
+ voiceGenderToggle.addEventListener('click', () => {
796
+ if (voiceGenderPreference === 'auto') {
797
+ voiceGenderPreference = 'male';
798
+ } else if (voiceGenderPreference === 'male') {
799
+ voiceGenderPreference = 'female';
800
+ } else {
801
+ voiceGenderPreference = 'auto';
802
+ }
803
+ localStorage.setItem('voiceGenderPreference', voiceGenderPreference);
804
+ updateVoiceGenderToggle();
805
+ });
806
+
807
+ updateVoiceGenderToggle();
808
+ }
809
+
810
+ // QUICK LANGUAGE SELECTORS
811
+ const sourceLangQuick = document.getElementById('source-lang-quick');
812
+ const targetLangQuick = document.getElementById('target-lang-quick');
813
+ const swapLangsBtn = document.getElementById('swap-langs');
814
+ const quickLangSelector = document.getElementById('quick-lang-selector');
815
+
816
+ // Load saved preferences
817
+ const savedSourceLang = localStorage.getItem('sourceLangQuick') || 'auto';
818
+ const savedTargetLang = localStorage.getItem('targetLangQuick') || 'French';
819
+
820
+ if (sourceLangQuick) sourceLangQuick.value = savedSourceLang;
821
+ if (targetLangQuick) targetLangQuick.value = savedTargetLang;
822
+ if (quickLangSelector) quickLangSelector.value = savedTargetLang;
823
+
824
+ // Sync source language with script.js
825
+ if (sourceLangQuick) {
826
+ sourceLangQuick.addEventListener('change', function () {
827
+ localStorage.setItem('sourceLangQuick', this.value);
828
+ console.log('ޤ Source language set to:', this.value);
829
+
830
+ // Clear cache for new language
831
+ fetch('/clear_cache', { method: 'POST' });
832
+
833
+ // Update status
834
+ const langName = this.options[this.selectedIndex].text;
835
+ statusPlaceholder.innerText = langName;
836
+ setTimeout(() => {
837
+ if (statusPlaceholder.innerText === langName) {
838
+ statusPlaceholder.innerText = 'Pret';
839
+ }
840
+ }, 1500);
841
+ });
842
+ }
843
+
844
+ // Sync target language with existing selector
845
+ if (targetLangQuick) {
846
+ targetLangQuick.addEventListener('change', function () {
847
+ localStorage.setItem('targetLangQuick', this.value);
848
+ localStorage.setItem('targetLang', this.value);
849
+
850
+ // Sync with modal selector
851
+ if (quickLangSelector) quickLangSelector.value = this.value;
852
+
853
+ console.log('✨ Target language set to:', this.value);
854
+
855
+ // Clear cache for new language
856
+ fetch('/clear_cache', { method: 'POST' });
857
+ });
858
+ }
859
+
860
+ // SWAP LANGUAGES BUTTON
861
+ if (swapLangsBtn && sourceLangQuick && targetLangQuick) {
862
+ swapLangsBtn.addEventListener('click', function () {
863
+ // Map between source codes and target names
864
+ const sourceToTarget = {
865
+ 'ar-SA': 'Arabic',
866
+ 'fr-FR': 'French',
867
+ 'en-US': 'English',
868
+ 'es-ES': 'Spanish',
869
+ 'de-DE': 'German',
870
+ 'auto': 'auto'
871
+ };
872
+
873
+ const targetToSource = {
874
+ 'Arabic': 'ar-SA',
875
+ 'Moroccan Darija': 'ar-SA',
876
+ 'French': 'fr-FR',
877
+ 'English': 'en-US',
878
+ 'Spanish': 'es-ES',
879
+ 'German': 'de-DE'
880
+ };
881
+
882
+ const currentSource = sourceLangQuick.value;
883
+ const currentTarget = targetLangQuick.value;
884
+
885
+ // Swap
886
+ const newSource = targetToSource[currentTarget] || 'auto';
887
+ const newTarget = sourceToTarget[currentSource] || 'French';
888
+
889
+ sourceLangQuick.value = newSource;
890
+ targetLangQuick.value = newTarget;
891
+
892
+ // Save
893
+ localStorage.setItem('sourceLangQuick', newSource);
894
+ localStorage.setItem('targetLangQuick', newTarget);
895
+ localStorage.setItem('targetLang', newTarget);
896
+
897
+ if (quickLangSelector) quickLangSelector.value = newTarget;
898
+
899
+ // Visual feedback
900
+ this.style.transform = 'rotate(180deg)';
901
+ setTimeout(() => {
902
+ this.style.transform = 'rotate(0deg)';
903
+ }, 300);
904
+
905
+ console.log('”„ Languages swapped:', newSource, '†”', newTarget);
906
+
907
+ // Clear cache
908
+ fetch('/clear_cache', { method: 'POST' });
909
+ });
910
+ }
911
+ </script>
912
+ </body>
913
+
914
+ </html>
templates/index_new.html ADDED
@@ -0,0 +1,727 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="fr">
3
+
4
+ <head>
5
+ <meta charset="UTF-8">
6
+ <meta name="viewport"
7
+ content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover">
8
+
9
+ <!-- 🌍 PWA -->
10
+ <meta name="theme-color" content="#22c55e">
11
+ <meta name="apple-mobile-web-app-capable" content="yes">
12
+ <meta name="apple-mobile-web-app-status-bar-style" content="default">
13
+ <meta name="application-name" content="Babel">
14
+ <meta name="apple-mobile-web-app-title" content="Babel">
15
+ <link rel="manifest" href="/static/manifest.json">
16
+
17
+ <title>Babel - Traducteur Vocal</title>
18
+ <link href="https://fonts.googleapis.com/css2?family=Nunito:wght@400;600;700;800;900&display=swap" rel="stylesheet">
19
+ <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
20
+
21
+ <style>
22
+ :root {
23
+ /* 🌿 FRESH GREEN PALETTE (Cloned from reference) */
24
+ --bg-gradient: linear-gradient(180deg, #4ade80 0%, #22c55e 50%, #16a34a 100%);
25
+ --card-bg: #ffffff;
26
+ --card-shadow: 0 8px 32px rgba(0, 0, 0, 0.12);
27
+ --text-dark: #1a1a2e;
28
+ --text-medium: #4a5568;
29
+ --text-light: #718096;
30
+
31
+ /* Action Colors */
32
+ --accent-green: #22c55e;
33
+ --accent-yellow: #fbbf24;
34
+ --accent-purple: #a855f7;
35
+ --accent-teal: #14b8a6;
36
+
37
+ /* Bottom Bar */
38
+ --bar-bg: #1a1a2e;
39
+ --bar-active: #fbbf24;
40
+ }
41
+
42
+ * {
43
+ margin: 0;
44
+ padding: 0;
45
+ box-sizing: border-box;
46
+ -webkit-tap-highlight-color: transparent;
47
+ }
48
+
49
+ body {
50
+ font-family: 'Nunito', -apple-system, BlinkMacSystemFont, sans-serif;
51
+ background: var(--bg-gradient);
52
+ min-height: 100vh;
53
+ min-height: 100dvh;
54
+ color: var(--text-dark);
55
+ overflow-x: hidden;
56
+ padding-bottom: 100px;
57
+ }
58
+
59
+ /* 🔝 TOP HEADER */
60
+ .top-header {
61
+ padding: 16px 20px;
62
+ padding-top: max(16px, env(safe-area-inset-top));
63
+ display: flex;
64
+ justify-content: space-between;
65
+ align-items: center;
66
+ }
67
+
68
+ .greeting {
69
+ color: white;
70
+ font-size: 1.1rem;
71
+ font-weight: 700;
72
+ }
73
+
74
+ .settings-icon {
75
+ width: 44px;
76
+ height: 44px;
77
+ background: rgba(255, 255, 255, 0.2);
78
+ border-radius: 12px;
79
+ display: flex;
80
+ align-items: center;
81
+ justify-content: center;
82
+ color: white;
83
+ font-size: 1.2rem;
84
+ cursor: pointer;
85
+ transition: all 0.2s;
86
+ }
87
+
88
+ .settings-icon:hover {
89
+ background: rgba(255, 255, 255, 0.3);
90
+ transform: scale(1.05);
91
+ }
92
+
93
+ /* 📦 MAIN CARD */
94
+ .main-card {
95
+ background: var(--card-bg);
96
+ margin: 0 16px;
97
+ border-radius: 24px;
98
+ padding: 24px;
99
+ box-shadow: var(--card-shadow);
100
+ }
101
+
102
+ .card-label {
103
+ font-size: 0.85rem;
104
+ color: var(--text-light);
105
+ font-weight: 600;
106
+ margin-bottom: 8px;
107
+ }
108
+
109
+ .main-value {
110
+ font-size: 2.5rem;
111
+ font-weight: 900;
112
+ color: var(--text-dark);
113
+ margin-bottom: 16px;
114
+ }
115
+
116
+ .main-value span {
117
+ font-size: 1.5rem;
118
+ color: var(--text-medium);
119
+ }
120
+
121
+ /* Language Display */
122
+ .language-display {
123
+ background: #f1f5f9;
124
+ border-radius: 12px;
125
+ padding: 12px 16px;
126
+ font-family: 'JetBrains Mono', monospace;
127
+ font-size: 0.9rem;
128
+ color: var(--text-medium);
129
+ margin-bottom: 20px;
130
+ text-align: center;
131
+ overflow: hidden;
132
+ text-overflow: ellipsis;
133
+ white-space: nowrap;
134
+ }
135
+
136
+ /* 🔘 ACTION BUTTONS ROW */
137
+ .action-row {
138
+ display: flex;
139
+ justify-content: center;
140
+ gap: 16px;
141
+ margin-top: 8px;
142
+ }
143
+
144
+ .action-btn {
145
+ display: flex;
146
+ flex-direction: column;
147
+ align-items: center;
148
+ gap: 8px;
149
+ cursor: pointer;
150
+ transition: all 0.2s;
151
+ }
152
+
153
+ .action-btn:hover {
154
+ transform: translateY(-2px);
155
+ }
156
+
157
+ .action-icon {
158
+ width: 56px;
159
+ height: 56px;
160
+ border-radius: 16px;
161
+ display: flex;
162
+ align-items: center;
163
+ justify-content: center;
164
+ font-size: 1.4rem;
165
+ color: white;
166
+ }
167
+
168
+ .action-icon.green {
169
+ background: var(--accent-green);
170
+ }
171
+
172
+ .action-icon.yellow {
173
+ background: var(--accent-yellow);
174
+ color: #1a1a2e;
175
+ }
176
+
177
+ .action-icon.purple {
178
+ background: var(--accent-purple);
179
+ }
180
+
181
+ .action-icon.teal {
182
+ background: var(--accent-teal);
183
+ }
184
+
185
+ .action-label {
186
+ font-size: 0.8rem;
187
+ font-weight: 700;
188
+ color: var(--text-dark);
189
+ }
190
+
191
+ /* 📊 PROGRESS SECTION */
192
+ .progress-card {
193
+ background: var(--bar-bg);
194
+ margin: 16px;
195
+ border-radius: 20px;
196
+ padding: 16px 20px;
197
+ display: flex;
198
+ align-items: center;
199
+ gap: 16px;
200
+ }
201
+
202
+ .progress-icon {
203
+ width: 44px;
204
+ height: 44px;
205
+ background: var(--accent-yellow);
206
+ border-radius: 12px;
207
+ display: flex;
208
+ align-items: center;
209
+ justify-content: center;
210
+ font-size: 1.2rem;
211
+ }
212
+
213
+ .progress-info {
214
+ flex: 1;
215
+ }
216
+
217
+ .progress-title {
218
+ color: white;
219
+ font-weight: 800;
220
+ font-size: 1.1rem;
221
+ }
222
+
223
+ .progress-sub {
224
+ color: rgba(255, 255, 255, 0.6);
225
+ font-size: 0.85rem;
226
+ }
227
+
228
+ /* 📜 HISTORY CARD */
229
+ .history-card {
230
+ background: var(--card-bg);
231
+ margin: 16px;
232
+ border-radius: 24px;
233
+ padding: 20px;
234
+ box-shadow: var(--card-shadow);
235
+ }
236
+
237
+ .history-header {
238
+ display: flex;
239
+ justify-content: space-between;
240
+ align-items: center;
241
+ margin-bottom: 16px;
242
+ }
243
+
244
+ .history-title {
245
+ font-size: 1.1rem;
246
+ font-weight: 800;
247
+ color: var(--text-dark);
248
+ }
249
+
250
+ .show-all {
251
+ font-size: 0.85rem;
252
+ color: var(--text-medium);
253
+ cursor: pointer;
254
+ }
255
+
256
+ .history-item {
257
+ display: flex;
258
+ align-items: center;
259
+ gap: 12px;
260
+ padding: 12px 0;
261
+ border-bottom: 1px solid #f1f5f9;
262
+ }
263
+
264
+ .history-item:last-child {
265
+ border-bottom: none;
266
+ }
267
+
268
+ .history-icon {
269
+ width: 40px;
270
+ height: 40px;
271
+ border-radius: 12px;
272
+ display: flex;
273
+ align-items: center;
274
+ justify-content: center;
275
+ font-size: 1rem;
276
+ }
277
+
278
+ .history-icon.received {
279
+ background: #dcfce7;
280
+ color: var(--accent-green);
281
+ }
282
+
283
+ .history-icon.sent {
284
+ background: #fef3c7;
285
+ color: var(--accent-yellow);
286
+ }
287
+
288
+ .history-details {
289
+ flex: 1;
290
+ }
291
+
292
+ .history-type {
293
+ font-weight: 700;
294
+ font-size: 0.95rem;
295
+ color: var(--text-dark);
296
+ }
297
+
298
+ .history-amount {
299
+ font-size: 0.85rem;
300
+ color: var(--text-medium);
301
+ }
302
+
303
+ .history-time {
304
+ font-size: 0.8rem;
305
+ color: var(--text-light);
306
+ }
307
+
308
+ /* 📱 BOTTOM NAV BAR */
309
+ .bottom-bar {
310
+ position: fixed;
311
+ bottom: 0;
312
+ left: 0;
313
+ right: 0;
314
+ padding: 12px 24px;
315
+ padding-bottom: max(12px, env(safe-area-inset-bottom));
316
+ display: flex;
317
+ justify-content: center;
318
+ }
319
+
320
+ .nav-pill {
321
+ background: var(--bar-bg);
322
+ border-radius: 100px;
323
+ padding: 12px 24px;
324
+ display: flex;
325
+ align-items: center;
326
+ gap: 8px;
327
+ box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
328
+ }
329
+
330
+ .nav-item {
331
+ width: 48px;
332
+ height: 48px;
333
+ border-radius: 50%;
334
+ display: flex;
335
+ align-items: center;
336
+ justify-content: center;
337
+ color: rgba(255, 255, 255, 0.5);
338
+ font-size: 1.2rem;
339
+ cursor: pointer;
340
+ transition: all 0.2s;
341
+ }
342
+
343
+ .nav-item:hover {
344
+ color: white;
345
+ }
346
+
347
+ .nav-item.active {
348
+ background: var(--accent-yellow);
349
+ color: var(--bar-bg);
350
+ }
351
+
352
+ /* 🎤 RECORD BUTTON (Special) */
353
+ .record-orb {
354
+ width: 64px;
355
+ height: 64px;
356
+ background: linear-gradient(135deg, #22c55e 0%, #16a34a 100%);
357
+ border-radius: 50%;
358
+ display: flex;
359
+ align-items: center;
360
+ justify-content: center;
361
+ color: white;
362
+ font-size: 1.5rem;
363
+ cursor: pointer;
364
+ transition: all 0.3s cubic-bezier(0.34, 1.56, 0.64, 1);
365
+ margin: -20px 8px 0 8px;
366
+ box-shadow: 0 8px 24px rgba(34, 197, 94, 0.4);
367
+ border: 4px solid var(--bar-bg);
368
+ }
369
+
370
+ .record-orb:hover {
371
+ transform: scale(1.1);
372
+ }
373
+
374
+ .record-orb:active {
375
+ transform: scale(0.95);
376
+ }
377
+
378
+ .record-orb.active {
379
+ background: linear-gradient(135deg, #ef4444 0%, #dc2626 100%);
380
+ box-shadow: 0 8px 24px rgba(239, 68, 68, 0.4);
381
+ animation: pulse-record 1.5s infinite;
382
+ }
383
+
384
+ @keyframes pulse-record {
385
+
386
+ 0%,
387
+ 100% {
388
+ box-shadow: 0 8px 24px rgba(239, 68, 68, 0.4);
389
+ }
390
+
391
+ 50% {
392
+ box-shadow: 0 8px 40px rgba(239, 68, 68, 0.6);
393
+ }
394
+ }
395
+
396
+ /* 📝 CHAT MESSAGES */
397
+ .chat-container {
398
+ padding: 0 16px;
399
+ display: flex;
400
+ flex-direction: column;
401
+ gap: 12px;
402
+ }
403
+
404
+ .chat-message {
405
+ max-width: 85%;
406
+ padding: 16px;
407
+ border-radius: 20px;
408
+ animation: slideIn 0.3s ease;
409
+ }
410
+
411
+ @keyframes slideIn {
412
+ from {
413
+ opacity: 0;
414
+ transform: translateY(10px);
415
+ }
416
+
417
+ to {
418
+ opacity: 1;
419
+ transform: translateY(0);
420
+ }
421
+ }
422
+
423
+ .chat-message.user {
424
+ background: var(--card-bg);
425
+ align-self: flex-start;
426
+ border-bottom-left-radius: 4px;
427
+ box-shadow: var(--card-shadow);
428
+ }
429
+
430
+ .chat-message.bot {
431
+ background: var(--bar-bg);
432
+ color: white;
433
+ align-self: flex-end;
434
+ border-bottom-right-radius: 4px;
435
+ }
436
+
437
+ .message-text {
438
+ font-size: 1rem;
439
+ line-height: 1.5;
440
+ }
441
+
442
+ .message-meta {
443
+ font-size: 0.75rem;
444
+ opacity: 0.6;
445
+ margin-top: 8px;
446
+ }
447
+
448
+ /* 🔘 LANGUAGE SELECTOR */
449
+ .lang-select {
450
+ background: #f1f5f9;
451
+ border: none;
452
+ border-radius: 12px;
453
+ padding: 12px 16px;
454
+ font-family: 'Nunito', sans-serif;
455
+ font-size: 0.9rem;
456
+ font-weight: 600;
457
+ color: var(--text-dark);
458
+ cursor: pointer;
459
+ width: 100%;
460
+ appearance: none;
461
+ }
462
+
463
+ /* Empty State */
464
+ .empty-state {
465
+ text-align: center;
466
+ padding: 40px 20px;
467
+ }
468
+
469
+ .empty-icon {
470
+ font-size: 4rem;
471
+ margin-bottom: 16px;
472
+ }
473
+
474
+ .empty-title {
475
+ font-size: 1.5rem;
476
+ font-weight: 900;
477
+ color: white;
478
+ margin-bottom: 8px;
479
+ }
480
+
481
+ .empty-sub {
482
+ color: rgba(255, 255, 255, 0.8);
483
+ font-size: 1rem;
484
+ }
485
+
486
+ /* MODAL */
487
+ .modal {
488
+ position: fixed;
489
+ inset: 0;
490
+ background: rgba(0, 0, 0, 0.5);
491
+ display: none;
492
+ align-items: flex-end;
493
+ justify-content: center;
494
+ z-index: 2000;
495
+ backdrop-filter: blur(4px);
496
+ }
497
+
498
+ .modal.visible {
499
+ display: flex;
500
+ }
501
+
502
+ .modal-content {
503
+ background: var(--card-bg);
504
+ width: 100%;
505
+ max-width: 500px;
506
+ border-radius: 24px 24px 0 0;
507
+ padding: 24px;
508
+ max-height: 80vh;
509
+ overflow-y: auto;
510
+ animation: slideUp 0.3s ease;
511
+ }
512
+
513
+ @keyframes slideUp {
514
+ from {
515
+ transform: translateY(100%);
516
+ }
517
+
518
+ to {
519
+ transform: translateY(0);
520
+ }
521
+ }
522
+
523
+ .modal-handle {
524
+ width: 40px;
525
+ height: 4px;
526
+ background: #e2e8f0;
527
+ border-radius: 2px;
528
+ margin: 0 auto 20px;
529
+ }
530
+
531
+ .modal-title {
532
+ font-size: 1.3rem;
533
+ font-weight: 800;
534
+ margin-bottom: 20px;
535
+ }
536
+
537
+ .form-group {
538
+ margin-bottom: 16px;
539
+ }
540
+
541
+ .form-group label {
542
+ display: block;
543
+ font-size: 0.85rem;
544
+ font-weight: 700;
545
+ color: var(--text-medium);
546
+ margin-bottom: 8px;
547
+ }
548
+
549
+ .save-btn {
550
+ width: 100%;
551
+ padding: 16px;
552
+ background: var(--accent-green);
553
+ color: white;
554
+ border: none;
555
+ border-radius: 16px;
556
+ font-family: 'Nunito', sans-serif;
557
+ font-size: 1rem;
558
+ font-weight: 800;
559
+ cursor: pointer;
560
+ margin-top: 16px;
561
+ transition: all 0.2s;
562
+ }
563
+
564
+ .save-btn:hover {
565
+ background: #16a34a;
566
+ transform: translateY(-2px);
567
+ }
568
+ </style>
569
+ </head>
570
+
571
+ <body>
572
+ <!-- TOP HEADER -->
573
+ <div class="top-header">
574
+ <div class="greeting">👋 Bonjour!</div>
575
+ <div class="settings-icon" id="settings-trigger">
576
+ <i class="fa-solid fa-gear"></i>
577
+ </div>
578
+ </div>
579
+
580
+ <!-- MAIN CARD -->
581
+ <div class="main-card">
582
+ <div class="card-label">Traduction Instantanée</div>
583
+ <div class="main-value">Babel <span>Matrix</span></div>
584
+
585
+ <div class="language-display" id="current-lang">
586
+ 🌍 Appuyez sur le micro pour parler...
587
+ </div>
588
+
589
+ <!-- Language Selector -->
590
+ <select class="lang-select" id="target-lang-quick">
591
+ <option value="French">🇫🇷 Français</option>
592
+ <option value="English">🇬🇧 English</option>
593
+ <option value="Moroccan Darija">🇲🇦 Darija</option>
594
+ <option value="Egyptian Arabic">🇪🇬 Masri</option>
595
+ <option value="Spanish">🇪🇸 Español</option>
596
+ <option value="German">🇩🇪 Deutsch</option>
597
+ <option value="Arabic">🇸🇦 العربية</option>
598
+ </select>
599
+
600
+ <!-- Action Buttons -->
601
+ <div class="action-row">
602
+ <div class="action-btn" id="listen-btn">
603
+ <div class="action-icon green">
604
+ <i class="fa-solid fa-play"></i>
605
+ </div>
606
+ <span class="action-label">Écouter</span>
607
+ </div>
608
+ <div class="action-btn" id="copy-btn">
609
+ <div class="action-icon yellow">
610
+ <i class="fa-solid fa-copy"></i>
611
+ </div>
612
+ <span class="action-label">Copier</span>
613
+ </div>
614
+ <div class="action-btn" id="share-btn">
615
+ <div class="action-icon purple">
616
+ <i class="fa-solid fa-share"></i>
617
+ </div>
618
+ <span class="action-label">Partager</span>
619
+ </div>
620
+ </div>
621
+ </div>
622
+
623
+ <!-- STATUS BAR -->
624
+ <div class="progress-card">
625
+ <div class="progress-icon">🎙️</div>
626
+ <div class="progress-info">
627
+ <div class="progress-title" id="status-text">Prêt</div>
628
+ <div class="progress-sub">Mode continu activé</div>
629
+ </div>
630
+ </div>
631
+
632
+ <!-- CHAT HISTORY -->
633
+ <div class="history-card">
634
+ <div class="history-header">
635
+ <span class="history-title">Historique</span>
636
+ <span class="show-all">Tout voir →</span>
637
+ </div>
638
+ <div id="chat-history">
639
+ <!-- Messages will appear here -->
640
+ <div class="empty-state" id="empty-hint" style="padding: 20px;">
641
+ <div style="font-size: 2rem;">💬</div>
642
+ <p style="color: var(--text-light); font-size: 0.9rem;">Vos traductions apparaîtront ici</p>
643
+ </div>
644
+ </div>
645
+ </div>
646
+
647
+ <!-- BOTTOM NAV BAR -->
648
+ <div class="bottom-bar">
649
+ <div class="nav-pill">
650
+ <div class="nav-item">
651
+ <i class="fa-solid fa-clock-rotate-left"></i>
652
+ </div>
653
+ <div class="nav-item active">
654
+ <i class="fa-solid fa-language"></i>
655
+ </div>
656
+
657
+ <!-- THE RECORD ORB -->
658
+ <div class="record-orb" id="record-btn">
659
+ <i class="fa-solid fa-microphone"></i>
660
+ </div>
661
+
662
+ <div class="nav-item">
663
+ <i class="fa-solid fa-brain"></i>
664
+ </div>
665
+ <div class="nav-item">
666
+ <i class="fa-solid fa-user"></i>
667
+ </div>
668
+ </div>
669
+ </div>
670
+
671
+ <!-- SETTINGS MODAL -->
672
+ <div class="modal" id="settings-modal">
673
+ <div class="modal-content">
674
+ <div class="modal-handle"></div>
675
+ <h2 class="modal-title">⚙️ Paramètres</h2>
676
+
677
+ <div class="form-group">
678
+ <label>Mode d'Intelligence</label>
679
+ <select class="lang-select" id="ai-model-selector">
680
+ <option value="gpt-4o-mini">🟢 OpenAI GPT-4o (Stable)</option>
681
+ <option value="gemini-flash-latest">⚡ Gemini Flash</option>
682
+ </select>
683
+ </div>
684
+
685
+ <div class="form-group">
686
+ <label>Moteur Vocal</label>
687
+ <select class="lang-select" id="tts-selector">
688
+ <option value="openai">🟢 OpenAI TTS</option>
689
+ <option value="gemini">💎 Gemini TTS</option>
690
+ </select>
691
+ </div>
692
+
693
+ <button class="save-btn" id="save-settings">Enregistrer</button>
694
+ </div>
695
+ </div>
696
+
697
+ <!-- SCRIPTS -->
698
+ <script src="/static/script.js"></script>
699
+ <script>
700
+ // Settings Modal Logic
701
+ document.getElementById('settings-trigger').addEventListener('click', () => {
702
+ document.getElementById('settings-modal').classList.add('visible');
703
+ });
704
+
705
+ document.getElementById('settings-modal').addEventListener('click', (e) => {
706
+ if (e.target === document.getElementById('settings-modal')) {
707
+ document.getElementById('settings-modal').classList.remove('visible');
708
+ }
709
+ });
710
+
711
+ document.getElementById('save-settings').addEventListener('click', () => {
712
+ const aiModel = document.getElementById('ai-model-selector').value;
713
+ const ttsEngine = document.getElementById('tts-selector').value;
714
+ localStorage.setItem('aiModel', aiModel);
715
+ localStorage.setItem('ttsEngine', ttsEngine);
716
+ document.getElementById('settings-modal').classList.remove('visible');
717
+ document.getElementById('status-text').innerText = '✅ Sauvegardé!';
718
+ setTimeout(() => document.getElementById('status-text').innerText = 'Prêt', 2000);
719
+ });
720
+
721
+ // Load saved values
722
+ document.getElementById('ai-model-selector').value = localStorage.getItem('aiModel') || 'gpt-4o-mini';
723
+ document.getElementById('tts-selector').value = localStorage.getItem('ttsEngine') || 'openai';
724
+ </script>
725
+ </body>
726
+
727
+ </html>