Amina-DS commited on
Commit
dc2d43f
Β·
verified Β·
1 Parent(s): 3b7ba2c

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +1234 -1234
app.py CHANGED
@@ -1,1234 +1,1234 @@
1
- import requests
2
- from bs4 import BeautifulSoup
3
- import os
4
- import re
5
- import json
6
- import tempfile
7
- from datetime import datetime
8
- from dotenv import load_dotenv
9
- from groq import Groq
10
- from openai import OpenAI
11
- import gradio as gr
12
-
13
- # -----------------------------------------------
14
- # LOAD API KEYS
15
- # -----------------------------------------------
16
- # load_dotenv() # Remove on Hugging Face
17
- BRIGHTDATA_API_KEY = os.getenv("BRIGHTDATA_API_KEY")
18
- BRIGHTDATA_USER = os.getenv("BRIGHTDATA_USER", "")
19
- BRIGHTDATA_PASS = os.getenv("BRIGHTDATA_PASS", "")
20
- BRIGHTDATA_HOST = os.getenv("BRIGHTDATA_HOST", "brd.superproxy.io")
21
- BRIGHTDATA_PORT = os.getenv("BRIGHTDATA_PORT", "22225")
22
- GROQ_API_KEY = os.getenv("GROQ_API_KEY")
23
- OPENAI_API_KEY = os.getenv("OPENAI_API_KEY")
24
- client = Groq(api_key=GROQ_API_KEY)
25
- openai_client = OpenAI(api_key=OPENAI_API_KEY) if OPENAI_API_KEY else None
26
-
27
- print("GROQ key exists:", bool(GROQ_API_KEY))
28
- print("OPENAI key exists:", bool(OPENAI_API_KEY))
29
-
30
- # -----------------------------------------------
31
- # PERSISTENT STORAGE β€” JSON Files
32
- # -----------------------------------------------
33
- HISTORY_FILE = "chat_history.json"
34
- PREFERENCES_FILE = "user_preferences.json"
35
-
36
-
37
- def load_chat_history():
38
- """Load chat history from JSON file."""
39
- try:
40
- if os.path.exists(HISTORY_FILE):
41
- with open(HISTORY_FILE, "r", encoding="utf-8") as f:
42
- data = json.load(f)
43
- return data if isinstance(data, list) else []
44
- except Exception as e:
45
- print(f"[Storage] Could not load history: {e}")
46
- return []
47
-
48
-
49
- def save_chat_history(history):
50
- """Save chat history to JSON file after every turn."""
51
- try:
52
- with open(HISTORY_FILE, "w", encoding="utf-8") as f:
53
- json.dump(history, f, indent=2, ensure_ascii=False)
54
- except Exception as e:
55
- print(f"[Storage] Could not save history: {e}")
56
-
57
-
58
- def load_preferences():
59
- """Load user preferences from JSON file."""
60
- try:
61
- if os.path.exists(PREFERENCES_FILE):
62
- with open(PREFERENCES_FILE, "r", encoding="utf-8") as f:
63
- return json.load(f)
64
- except Exception as e:
65
- print(f"[Storage] Could not load preferences: {e}")
66
- return {
67
- "tone": "friendly",
68
- "language": "English",
69
- "format": "concise",
70
- "custom_rules": ""
71
- }
72
-
73
-
74
- def save_preferences(prefs):
75
- """Save user preferences to JSON file."""
76
- try:
77
- with open(PREFERENCES_FILE, "w", encoding="utf-8") as f:
78
- json.dump(prefs, f, indent=2, ensure_ascii=False)
79
- return "βœ… Preferences saved!"
80
- except Exception as e:
81
- return f"❌ Could not save: {str(e)}"
82
-
83
-
84
- # Load at startup
85
- chat_history_store = load_chat_history()
86
- user_preferences = load_preferences()
87
- print(f"[Storage] Loaded {len(chat_history_store)} messages from history")
88
- print(f"[Storage] Preferences: {user_preferences}")
89
-
90
-
91
- # ================================================================
92
- # VERSION 1: GOODREADS SCRAPER
93
- # ================================================================
94
- def get_books():
95
- if not BRIGHTDATA_API_KEY:
96
- return [], "❌ BRIGHTDATA_API_KEY not found!"
97
-
98
- headers = {
99
- "Authorization": f"Bearer {BRIGHTDATA_API_KEY}",
100
- "Content-Type": "application/json"
101
- }
102
- data = {
103
- "zone": "web_unlocker1",
104
- "url": "https://www.goodreads.com/list/show/1.Best_Books_Ever",
105
- "format": "raw"
106
- }
107
-
108
- try:
109
- response = requests.post(
110
- "https://api.brightdata.com/request",
111
- json=data,
112
- headers=headers,
113
- timeout=30
114
- )
115
- if response.status_code != 200:
116
- return [], f"❌ Scraping failed! Status: {response.status_code}"
117
-
118
- soup = BeautifulSoup(response.text, "html.parser")
119
- books = soup.select("tr[itemtype='http://schema.org/Book']")
120
- if not books:
121
- return [], "⚠️ No books found."
122
-
123
- results = []
124
- for i, book in enumerate(books[:20], start=1):
125
- try:
126
- title = book.select_one("a.bookTitle span").text.strip()
127
- author = book.select_one("a.authorName span").text.strip()
128
- rating = book.select_one("span.minirating").text.strip()
129
- results.append({
130
- "rank": i,
131
- "title": title,
132
- "author": author,
133
- "rating": rating
134
- })
135
- except Exception:
136
- pass
137
- return results, f"βœ… Scraped {len(results)} books!"
138
- except Exception as e:
139
- return [], f"❌ Error: {str(e)}"
140
-
141
-
142
- def format_books_as_text(books):
143
- if not books:
144
- return "No book data available."
145
- return "\n".join([
146
- f"Rank #{b['rank']}: \"{b['title']}\" by {b['author']} β€” Rating: {b['rating']}"
147
- for b in books
148
- ])
149
-
150
-
151
- def show_books_table(books_data, scrape_status):
152
- if not books_data:
153
- return f"**Status:** {scrape_status}"
154
- md = f"**{scrape_status}**\n\n| Rank | Title | Author | Rating |\n|------|-------|--------|--------|\n"
155
- for b in books_data:
156
- md += f"| #{b['rank']} | {b['title']} | {b['author']} | {b['rating']} |\n"
157
- return md
158
-
159
-
160
- print("πŸ”„ Scraping Goodreads...")
161
- books_data, scrape_status = get_books()
162
- print(scrape_status)
163
-
164
-
165
- # ================================================================
166
- # VERSION 2: YOUTUBE TRANSCRIPT
167
- # ================================================================
168
- def get_youtube_transcript(video_id: str):
169
- """Fetch YouTube transcript β€” old aur new API dono handle karta hai."""
170
- video_id = video_id.strip()
171
-
172
- if "youtube.com/watch" in video_id:
173
- video_id = video_id.split("v=")[-1].split("&")[0]
174
- elif "youtu.be/" in video_id:
175
- video_id = video_id.split("youtu.be/")[-1].split("?")[0]
176
-
177
- if not video_id:
178
- return None, "❌ Please enter a valid YouTube Video ID or URL."
179
-
180
- try:
181
- from youtube_transcript_api import YouTubeTranscriptApi
182
-
183
- try:
184
- ytt = YouTubeTranscriptApi()
185
- fetched = ytt.fetch(video_id)
186
- full_text = " ".join([s.text for s in fetched])
187
- if full_text.strip():
188
- return full_text, f"βœ… Transcript fetched! ({len(full_text)} chars, ID: {video_id})"
189
- except Exception:
190
- pass
191
-
192
- try:
193
- fetched = YouTubeTranscriptApi.get_transcript(video_id)
194
- full_text = " ".join([s["text"] for s in fetched])
195
- if full_text.strip():
196
- return full_text, f"βœ… Transcript fetched! ({len(full_text)} chars, ID: {video_id})"
197
- except Exception:
198
- pass
199
-
200
- return None, "⚠️ Transcript empty or not available. Try another video."
201
-
202
- except Exception as e:
203
- return None, f"❌ Error: {str(e)}"
204
-
205
-
206
- current_transcript = {"text": None, "status": "No transcript loaded."}
207
-
208
-
209
- def load_transcript(video_id):
210
- global current_transcript
211
- text, status = get_youtube_transcript(video_id)
212
- current_transcript["text"] = text
213
- current_transcript["status"] = status
214
- if text:
215
- preview = text[:600] + "..." if len(text) > 600 else text
216
- return status, f"**πŸ“„ Preview:**\n\n_{preview}_"
217
- return status, "No preview available."
218
-
219
-
220
- # ================================================================
221
- # VERSION 4: VOICE HELPERS
222
- # ================================================================
223
- def transcribe_audio(audio_path):
224
- """Convert microphone audio to text using Groq Whisper."""
225
- if not audio_path:
226
- return "", "⚠️ No audio recorded."
227
-
228
- if not GROQ_API_KEY:
229
- return "", "❌ GROQ_API_KEY not set for transcription."
230
-
231
- try:
232
- with open(audio_path, "rb") as audio_file:
233
- transcript = client.audio.transcriptions.create(
234
- file=audio_file,
235
- model="whisper-large-v3-turbo",
236
- response_format="verbose_json"
237
- )
238
-
239
- text = getattr(transcript, "text", "") or ""
240
- if not text.strip():
241
- return "", "⚠️ Speech detected but transcript is empty."
242
-
243
- return text.strip(), "βœ… Voice transcribed successfully!"
244
- except Exception as e:
245
- return "", f"❌ Transcription error: {str(e)}"
246
-
247
-
248
- def text_to_speech(text):
249
- """Convert assistant reply to speech using OpenAI TTS."""
250
- if not text or not text.strip():
251
- return None, "⚠️ Empty text for audio output."
252
-
253
- if not OPENAI_API_KEY or not openai_client:
254
- return None, "⚠️ OPENAI_API_KEY not set. Text reply works, audio skipped."
255
-
256
- try:
257
- tmp_file = tempfile.NamedTemporaryFile(delete=False, suffix=".mp3")
258
- tmp_path = tmp_file.name
259
- tmp_file.close()
260
-
261
- speech_response = openai_client.audio.speech.create(
262
- model="tts-1",
263
- voice="alloy",
264
- input=text[:1000]
265
- )
266
-
267
- with open(tmp_path, "wb") as f:
268
- f.write(speech_response.content)
269
-
270
- return tmp_path, "βœ… Audio reply generated!"
271
- except Exception as e:
272
- print("TTS full error:", repr(e))
273
- return None, f"❌ TTS error: {str(e)}"
274
-
275
-
276
- def should_generate_image(user_text, reply_text):
277
- text = f"{user_text} {reply_text}".lower()
278
- triggers = [
279
- "draw", "image", "picture", "diagram", "visual", "visually",
280
- "show me", "illustrate", "architecture", "flowchart", "chart",
281
- "generate image", "generate an image", "show a", "show an",
282
- "solar system", "transformer", "mind map", "scene"
283
- ]
284
- return any(t in text for t in triggers)
285
-
286
-
287
- def build_image_prompt(user_text, reply_text):
288
- if not GROQ_API_KEY:
289
- return user_text[:500]
290
- try:
291
- resp = client.chat.completions.create(
292
- model="llama-3.1-8b-instant",
293
- messages=[
294
- {"role": "system",
295
- "content": "Create one short descriptive image prompt. Return only the prompt text."},
296
- {"role": "user",
297
- "content": f"User request: {user_text}\nAssistant reply: {reply_text[:1000]}\nCreate a concise image generation prompt."}
298
- ],
299
- temperature=0.4,
300
- max_tokens=120
301
- )
302
- return (resp.choices[0].message.content or user_text).strip()
303
- except Exception as e:
304
- print("Image prompt error:", repr(e))
305
- return user_text[:500]
306
-
307
-
308
- def maybe_generate_image(user_text, reply_text):
309
- if not should_generate_image(user_text, reply_text):
310
- return None, "ℹ️ No image needed for this reply."
311
- if not OPENAI_API_KEY:
312
- return None, "⚠️ OPENAI_API_KEY missing for image generation."
313
- try:
314
- image_prompt = build_image_prompt(user_text, reply_text)
315
- image_resp = openai_client.images.generate(
316
- model="dall-e-3",
317
- prompt=image_prompt,
318
- size="1024x1024",
319
- quality="standard",
320
- n=1
321
- )
322
- image_url = image_resp.data[0].url
323
- if not image_url:
324
- return None, "⚠️ Image was not generated."
325
- img_data = requests.get(image_url, timeout=60).content
326
- tmp_file = tempfile.NamedTemporaryFile(delete=False, suffix=".png")
327
- tmp_file.write(img_data)
328
- tmp_file.close()
329
- return tmp_file.name, "βœ… Image generated!"
330
- except Exception as e:
331
- print("Image generation error:", repr(e))
332
- return None, f"❌ Image generation error: {str(e)}"
333
-
334
-
335
- def process_voice_for_main_chat(audio_path, history):
336
- """Voice input for main chatbot."""
337
- transcript_text, voice_status = transcribe_audio(audio_path)
338
- if not transcript_text:
339
- return history, voice_status, None, "", None, ""
340
-
341
- reply = ask_main_ai(transcript_text, history or [])
342
- audio_reply, tts_status = text_to_speech(reply)
343
-
344
- updated_history = list(history) if history else []
345
- updated_history.append({"role": "user", "content": transcript_text})
346
- updated_history.append({"role": "assistant", "content": reply})
347
-
348
- image_path, image_status = maybe_generate_image(transcript_text, reply)
349
- combined_status = f"{voice_status}\n{tts_status}"
350
- return updated_history, combined_status, audio_reply, transcript_text, image_path, image_status
351
-
352
-
353
- def process_voice_for_books(audio_path, history):
354
- """Voice input for books chatbot."""
355
- transcript_text, voice_status = transcribe_audio(audio_path)
356
- if not transcript_text:
357
- return history, voice_status, None, "", None, ""
358
-
359
- reply = ask_books_ai(transcript_text, history or [])
360
- audio_reply, tts_status = text_to_speech(reply)
361
-
362
- updated_history = list(history) if history else []
363
- updated_history.append({"role": "user", "content": transcript_text})
364
- updated_history.append({"role": "assistant", "content": reply})
365
-
366
- image_path, image_status = maybe_generate_image(transcript_text, reply)
367
- combined_status = f"{voice_status}\n{tts_status}"
368
- return updated_history, combined_status, audio_reply, transcript_text, image_path, image_status
369
-
370
-
371
- def process_voice_for_youtube(audio_path, history):
372
- """Voice input for YouTube chatbot."""
373
- transcript_text, voice_status = transcribe_audio(audio_path)
374
- if not transcript_text:
375
- return history, voice_status, None, "", None, ""
376
-
377
- reply = ask_youtube_ai(transcript_text, history or [])
378
- audio_reply, tts_status = text_to_speech(reply)
379
-
380
- updated_history = list(history) if history else []
381
- updated_history.append({"role": "user", "content": transcript_text})
382
- updated_history.append({"role": "assistant", "content": reply})
383
-
384
- image_path, image_status = maybe_generate_image(transcript_text, reply)
385
- combined_status = f"{voice_status}\n{tts_status}"
386
- return updated_history, combined_status, audio_reply, transcript_text, image_path, image_status
387
-
388
-
389
- # ================================================================
390
- # VERSION 3: PREFERENCES + PERSISTENT CHAT
391
- # ================================================================
392
- def build_system_prompt(base_prompt, prefs):
393
- """Inject user preferences into system prompt dynamically."""
394
- pref_text = f"""
395
- USER PREFERENCES (follow these always):
396
- - Tone: {prefs.get('tone', 'friendly')}
397
- - Language: {prefs.get('language', 'English')}
398
- - Response Format: {prefs.get('format', 'concise')}
399
- """
400
- custom = prefs.get("custom_rules", "").strip()
401
- if custom:
402
- pref_text += f"- Custom Rules: {custom}\n"
403
- return base_prompt + pref_text
404
-
405
-
406
- def convert_history_for_display(history_store):
407
- """Convert stored history to Gradio chatbot format (list of dicts)."""
408
- return [{"role": item["role"], "content": item["content"]} for item in history_store]
409
-
410
-
411
- def ask_main_ai(message, history):
412
- """
413
- VERSION 3 Main Chatbot:
414
- - Multi-turn (session memory via history)
415
- - Persistent (saves to JSON after every turn)
416
- - Preferences injected into system prompt
417
- """
418
- global chat_history_store, user_preferences
419
-
420
- if not GROQ_API_KEY:
421
- return "❌ GROQ_API_KEY not set."
422
-
423
- base_prompt = """You are a smart, helpful AI assistant with memory of past conversations.
424
- You help users with general questions, book recommendations, research, and more.
425
- Always maintain context from previous messages in the conversation."""
426
-
427
- system = build_system_prompt(base_prompt, user_preferences)
428
- messages = [{"role": "system", "content": system}]
429
-
430
- for item in chat_history_store:
431
- messages.append({"role": item["role"], "content": item["content"]})
432
-
433
- for item in history:
434
- if isinstance(item, dict):
435
- if item not in chat_history_store:
436
- messages.append({"role": item["role"], "content": item["content"]})
437
- else:
438
- messages.append({"role": "user", "content": item[0]})
439
- messages.append({"role": "assistant", "content": item[1]})
440
-
441
- messages.append({"role": "user", "content": message})
442
-
443
- try:
444
- response = client.chat.completions.create(
445
- model="llama-3.1-8b-instant",
446
- messages=messages,
447
- temperature=0.6,
448
- max_tokens=1024
449
- )
450
- reply = response.choices[0].message.content
451
-
452
- chat_history_store.append({"role": "user", "content": message})
453
- chat_history_store.append({"role": "assistant", "content": reply})
454
- save_chat_history(chat_history_store)
455
-
456
- return reply
457
-
458
- except Exception as e:
459
- return f"❌ AI Error: {str(e)}"
460
-
461
-
462
- def ask_books_ai(message, history):
463
- """Goodreads Q&A β€” Version 1 (carried forward)."""
464
- if not GROQ_API_KEY:
465
- return "❌ GROQ_API_KEY not set."
466
- if not books_data:
467
- return f"⚠️ No book data. Status: {scrape_status}"
468
-
469
- base = """You are a smart and friendly book assistant named BookBot πŸ“š.
470
- Books data from Goodreads:
471
- {context}
472
- RULES: Only answer from this data. Be friendly and concise."""
473
-
474
- system = build_system_prompt(base.format(context=format_books_as_text(books_data)), user_preferences)
475
- messages = [{"role": "system", "content": system}]
476
- for item in history:
477
- if isinstance(item, dict):
478
- messages.append({"role": item["role"], "content": item["content"]})
479
- else:
480
- messages.append({"role": "user", "content": item[0]})
481
- messages.append({"role": "assistant", "content": item[1]})
482
- messages.append({"role": "user", "content": message})
483
-
484
- try:
485
- resp = client.chat.completions.create(
486
- model="llama-3.1-8b-instant",
487
- messages=messages,
488
- temperature=0.5,
489
- max_tokens=1024
490
- )
491
- return resp.choices[0].message.content
492
- except Exception as e:
493
- return f"❌ AI Error: {str(e)}"
494
-
495
-
496
- def ask_youtube_ai(message, history):
497
- """YouTube Q&A β€” Version 2 (carried forward)."""
498
- if not GROQ_API_KEY:
499
- return "❌ GROQ_API_KEY not set."
500
- transcript = current_transcript.get("text")
501
- if not transcript:
502
- return "⚠️ No transcript loaded. Enter a YouTube Video ID and click 'Load Transcript' first."
503
-
504
- base = "You are a helpful assistant answering ONLY from this transcript:\n{transcript}\nRULES: Only use transcript info. Be concise."
505
- system = build_system_prompt(base.format(transcript=transcript[:6000]), user_preferences)
506
- messages = [{"role": "system", "content": system}]
507
- for item in history:
508
- if isinstance(item, dict):
509
- messages.append({"role": item["role"], "content": item["content"]})
510
- else:
511
- messages.append({"role": "user", "content": item[0]})
512
- messages.append({"role": "assistant", "content": item[1]})
513
- messages.append({"role": "user", "content": message})
514
-
515
- try:
516
- resp = client.chat.completions.create(
517
- model="llama-3.1-8b-instant",
518
- messages=messages,
519
- temperature=0.4,
520
- max_tokens=1024
521
- )
522
- return resp.choices[0].message.content
523
- except Exception as e:
524
- return f"❌ AI Error: {str(e)}"
525
-
526
-
527
- def update_preferences(tone, language, fmt, custom_rules):
528
- """Save updated preferences and return status."""
529
- global user_preferences
530
- user_preferences = {
531
- "tone": tone,
532
- "language": language,
533
- "format": fmt,
534
- "custom_rules": custom_rules
535
- }
536
- status = save_preferences(user_preferences)
537
- return status
538
-
539
-
540
- def clear_history():
541
- """Clear all persistent chat history."""
542
- global chat_history_store
543
- chat_history_store = []
544
- save_chat_history([])
545
- return "πŸ—‘οΈ Chat history cleared!"
546
-
547
-
548
- # ================================================================
549
- # CUSTOM CSS β€” SAME UI + SMALL AUDIO SECTION SUPPORT
550
- # ================================================================
551
- CUSTOM_CSS = """
552
- /* ── Google Fonts ── */
553
- @import url('https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@300;400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap');
554
- :root {
555
- --bg-primary: #0a0e1a;
556
- --bg-secondary: #0f1629;
557
- --bg-card: #141d35;
558
- --bg-card-hover: #1a2540;
559
- --accent-cyan: #00d4ff;
560
- --accent-emerald: #00ff9d;
561
- --accent-amber: #ffb800;
562
- --accent-rose: #ff4f7b;
563
- --text-primary: #e8edf8;
564
- --text-secondary: #8899bb;
565
- --text-muted: #4a5a7a;
566
- --border: #1e2d50;
567
- --border-bright: #2a3f70;
568
- --glow-cyan: 0 0 20px rgba(0,212,255,0.15);
569
- --glow-emerald: 0 0 20px rgba(0,255,157,0.12);
570
- --radius-sm: 8px;
571
- --radius-md: 14px;
572
- --radius-lg: 20px;
573
- --font-main: 'Space Grotesk', sans-serif;
574
- --font-mono: 'JetBrains Mono', monospace;
575
- }
576
- *, *::before, *::after { box-sizing: border-box; }
577
- body, .gradio-container {
578
- background: var(--bg-primary) !important;
579
- font-family: var(--font-main) !important;
580
- color: var(--text-primary) !important;
581
- min-height: 100vh;
582
- }
583
- .gradio-container {
584
- background:
585
- radial-gradient(ellipse 80% 50% at 20% 10%, rgba(0,212,255,0.04) 0%, transparent 60%),
586
- radial-gradient(ellipse 60% 40% at 80% 90%, rgba(0,255,157,0.03) 0%, transparent 60%),
587
- var(--bg-primary) !important;
588
- max-width: 1200px !important;
589
- margin: 0 auto !important;
590
- padding: 0 16px !important;
591
- }
592
- .hero-header {
593
- text-align: center;
594
- padding: 40px 20px 28px;
595
- position: relative;
596
- }
597
- .hero-header::before {
598
- content: '';
599
- position: absolute;
600
- top: 0; left: 50%; transform: translateX(-50%);
601
- width: 300px; height: 1px;
602
- background: linear-gradient(90deg, transparent, var(--accent-cyan), transparent);
603
- }
604
- .hero-title {
605
- font-size: 2.4rem;
606
- font-weight: 700;
607
- letter-spacing: -0.5px;
608
- background: linear-gradient(135deg, var(--accent-cyan) 0%, var(--accent-emerald) 60%, var(--accent-amber) 100%);
609
- -webkit-background-clip: text;
610
- -webkit-text-fill-color: transparent;
611
- background-clip: text;
612
- margin: 0 0 10px;
613
- line-height: 1.1;
614
- }
615
- .hero-subtitle {
616
- color: var(--text-secondary);
617
- font-size: 0.95rem;
618
- font-weight: 400;
619
- letter-spacing: 0.3px;
620
- margin: 0;
621
- }
622
- .hero-subtitle span {
623
- color: var(--accent-cyan);
624
- font-weight: 500;
625
- }
626
- .hero-badges {
627
- display: flex;
628
- justify-content: center;
629
- gap: 10px;
630
- margin-top: 16px;
631
- flex-wrap: wrap;
632
- }
633
- .badge {
634
- background: var(--bg-card);
635
- border: 1px solid var(--border-bright);
636
- border-radius: 20px;
637
- padding: 4px 14px;
638
- font-size: 0.75rem;
639
- font-family: var(--font-mono);
640
- color: var(--text-secondary);
641
- letter-spacing: 0.5px;
642
- }
643
- .tab-nav {
644
- background: var(--bg-secondary) !important;
645
- border: 1px solid var(--border) !important;
646
- border-radius: var(--radius-lg) !important;
647
- padding: 6px !important;
648
- margin-bottom: 20px !important;
649
- display: flex;
650
- gap: 4px;
651
- }
652
- .tab-nav button {
653
- background: transparent !important;
654
- border: none !important;
655
- color: var(--text-muted) !important;
656
- font-family: var(--font-main) !important;
657
- font-size: 0.875rem !important;
658
- font-weight: 500 !important;
659
- padding: 10px 20px !important;
660
- border-radius: var(--radius-md) !important;
661
- cursor: pointer !important;
662
- transition: all 0.25s ease !important;
663
- letter-spacing: 0.2px !important;
664
- white-space: nowrap !important;
665
- }
666
- .tab-nav button:hover {
667
- background: var(--bg-card) !important;
668
- color: var(--text-primary) !important;
669
- }
670
- .tab-nav button.selected {
671
- background: linear-gradient(135deg, rgba(0,212,255,0.15), rgba(0,255,157,0.08)) !important;
672
- color: var(--accent-cyan) !important;
673
- border: 1px solid rgba(0,212,255,0.25) !important;
674
- box-shadow: var(--glow-cyan) !important;
675
- }
676
- .chatbot-wrap .chatbot,
677
- .chatbot-wrap > div {
678
- background: var(--bg-secondary) !important;
679
- border: 1px solid var(--border) !important;
680
- border-radius: var(--radius-md) !important;
681
- }
682
- .message.user {
683
- background: linear-gradient(135deg, rgba(0,212,255,0.12), rgba(0,212,255,0.06)) !important;
684
- border: 1px solid rgba(0,212,255,0.2) !important;
685
- border-radius: var(--radius-md) var(--radius-sm) var(--radius-md) var(--radius-md) !important;
686
- color: var(--text-primary) !important;
687
- font-family: var(--font-main) !important;
688
- }
689
- .message.bot {
690
- background: var(--bg-card) !important;
691
- border: 1px solid var(--border-bright) !important;
692
- border-radius: var(--radius-sm) var(--radius-md) var(--radius-md) var(--radius-md) !important;
693
- color: var(--text-primary) !important;
694
- font-family: var(--font-main) !important;
695
- }
696
- input[type="text"],
697
- textarea,
698
- .gr-textbox,
699
- .gr-textbox textarea,
700
- .gr-textbox input {
701
- background: var(--bg-secondary) !important;
702
- border: 1px solid var(--border-bright) !important;
703
- border-radius: var(--radius-sm) !important;
704
- color: var(--text-primary) !important;
705
- font-family: var(--font-main) !important;
706
- font-size: 0.9rem !important;
707
- padding: 12px 16px !important;
708
- transition: border-color 0.2s, box-shadow 0.2s !important;
709
- outline: none !important;
710
- }
711
- input[type="text"]:focus,
712
- textarea:focus,
713
- .gr-textbox textarea:focus,
714
- .gr-textbox input:focus {
715
- border-color: var(--accent-cyan) !important;
716
- box-shadow: 0 0 0 3px rgba(0,212,255,0.08) !important;
717
- }
718
- button.lg.primary, button[variant="primary"] {
719
- background: linear-gradient(135deg, var(--accent-cyan), #0099cc) !important;
720
- border: none !important;
721
- border-radius: var(--radius-sm) !important;
722
- color: #000 !important;
723
- font-family: var(--font-main) !important;
724
- font-weight: 600 !important;
725
- font-size: 0.875rem !important;
726
- letter-spacing: 0.3px !important;
727
- padding: 11px 22px !important;
728
- transition: all 0.2s ease !important;
729
- cursor: pointer !important;
730
- }
731
- button.lg.secondary, button[variant="secondary"] {
732
- background: var(--bg-card) !important;
733
- border: 1px solid var(--border-bright) !important;
734
- border-radius: var(--radius-sm) !important;
735
- color: var(--text-primary) !important;
736
- font-family: var(--font-main) !important;
737
- font-weight: 500 !important;
738
- font-size: 0.875rem !important;
739
- padding: 11px 22px !important;
740
- }
741
- button.lg.stop, button[variant="stop"] {
742
- background: rgba(255,79,123,0.1) !important;
743
- border: 1px solid rgba(255,79,123,0.3) !important;
744
- border-radius: var(--radius-sm) !important;
745
- color: var(--accent-rose) !important;
746
- font-family: var(--font-main) !important;
747
- font-weight: 500 !important;
748
- font-size: 0.875rem !important;
749
- padding: 11px 22px !important;
750
- }
751
- .gr-markdown, .prose {
752
- color: var(--text-primary) !important;
753
- font-family: var(--font-main) !important;
754
- }
755
- .status-box textarea {
756
- background: var(--bg-secondary) !important;
757
- border: 1px solid var(--border) !important;
758
- border-radius: var(--radius-sm) !important;
759
- color: var(--text-secondary) !important;
760
- font-family: var(--font-mono) !important;
761
- font-size: 0.82rem !important;
762
- }
763
- .transcript-preview {
764
- background: var(--bg-secondary);
765
- border: 1px solid var(--border);
766
- border-left: 3px solid var(--accent-amber);
767
- border-radius: var(--radius-sm);
768
- padding: 14px 18px;
769
- font-size: 0.875rem;
770
- line-height: 1.6;
771
- color: var(--text-secondary);
772
- max-height: 180px;
773
- overflow-y: auto;
774
- }
775
- .voice-box {
776
- background: rgba(0, 212, 255, 0.04);
777
- border: 1px solid rgba(0, 212, 255, 0.18);
778
- border-radius: 14px;
779
- padding: 14px;
780
- margin-top: 14px;
781
- }
782
- .footer-strip {
783
- text-align: center;
784
- padding: 18px 0 28px;
785
- color: var(--text-muted);
786
- font-size: 0.75rem;
787
- font-family: var(--font-mono);
788
- letter-spacing: 0.5px;
789
- border-top: 1px solid var(--border);
790
- margin-top: 24px;
791
- }
792
- """
793
-
794
- # ================================================================
795
- # GRADIO UI β€” SAME LAYOUT + VOICE BLOCKS ADDED
796
- # ================================================================
797
- initial_display = convert_history_for_display(chat_history_store)
798
- prefs = load_preferences()
799
-
800
- with gr.Blocks(title="⚑ NeuralChat β€” AI Assistant") as demo:
801
- gr.HTML("""
802
- <div class="hero-header">
803
- <p class="hero-title">⚑ NeuralChat</p>
804
- <p class="hero-subtitle">
805
- Powered by <span>Groq LLaMA</span> &nbsp;Β·&nbsp;
806
- <span>Persistent Memory</span> &nbsp;Β·&nbsp;
807
- <span>Goodreads + YouTube + Voice + Images</span>
808
- </p>
809
- <div class="hero-badges">
810
- <span class="badge">llama-3.3-70b</span>
811
- <span class="badge">llama-3.1-8b</span>
812
- <span class="badge">whisper-large-v3-turbo</span>
813
- <span class="badge">tts-1</span>
814
- <span class="badge">dall-e-3</span>
815
- <span class="badge">ver 5.0</span>
816
- </div>
817
- </div>
818
- """)
819
-
820
- with gr.Tabs(elem_classes=["tab-nav"]):
821
-
822
- # ============================================================
823
- # TAB 1: MAIN CHATBOT
824
- # ============================================================
825
- with gr.Tab("πŸ’¬ Chat"):
826
- gr.HTML("""
827
- <div style="margin-bottom:18px;">
828
- <div class="section-label">Main Assistant</div>
829
- <div class="section-title">Conversational AI with Memory</div>
830
- <div class="section-desc">
831
- Your assistant remembers past sessions and adapts to your preferences automatically.
832
- </div>
833
- </div>
834
- """)
835
-
836
- main_chatbot = gr.Chatbot(
837
- value=initial_display,
838
- height=460,
839
- placeholder="<div style='text-align:center;color:#4a5a7a;padding:40px;font-family:Space Grotesk,sans-serif;'>πŸ’¬ Start a conversation β€” I remember everything.</div>",
840
- elem_classes=["chatbot-wrap"],
841
- show_label=False
842
-
843
- )
844
-
845
- with gr.Row():
846
- main_msg = gr.Textbox(
847
- placeholder="Type a message…",
848
- scale=7,
849
- show_label=False,
850
- container=False,
851
- )
852
- main_send = gr.Button("Send ↑", variant="primary", scale=1)
853
-
854
-
855
- def submit_main_text(message, history):
856
- history = list(history) if history else []
857
- if not message.strip():
858
- return history, ""
859
- reply = ask_main_ai(message, history)
860
- history.append({"role": "user", "content": message})
861
- history.append({"role": "assistant", "content": reply})
862
- return history, ""
863
-
864
-
865
- main_send.click(
866
- fn=submit_main_text,
867
- inputs=[main_msg, main_chatbot],
868
- outputs=[main_chatbot, main_msg]
869
- )
870
- main_msg.submit(
871
- fn=submit_main_text,
872
- inputs=[main_msg, main_chatbot],
873
- outputs=[main_chatbot, main_msg]
874
- )
875
-
876
- with gr.Column(elem_classes=["voice-box"]):
877
- gr.Markdown("**🎀 Voice Input / πŸ”Š Audio Reply**")
878
- with gr.Row():
879
- main_audio_in = gr.Audio(sources=["microphone"], type="filepath", label="Record Voice")
880
- main_audio_out = gr.Audio(label="Assistant Voice Reply", autoplay=True)
881
- with gr.Row():
882
- main_voice_btn = gr.Button("πŸŽ™οΈ Ask by Voice", variant="secondary")
883
- main_voice_status = gr.Textbox(label="Voice Status", interactive=False, elem_classes=["status-box"])
884
- main_voice_text = gr.Textbox(label="Transcribed Text", interactive=False)
885
-
886
- main_voice_btn.click(
887
- fn=process_voice_for_main_chat,
888
- inputs=[main_audio_in, main_chatbot],
889
- outputs=[main_chatbot, main_voice_status, main_audio_out, main_voice_text]
890
- )
891
-
892
- gr.HTML("<div style='height:12px'></div>")
893
-
894
- with gr.Row():
895
- clear_btn = gr.Button("πŸ—‘οΈ Clear History", variant="stop", scale=1)
896
- clear_status = gr.Textbox(
897
- label="Status",
898
- interactive=False,
899
- scale=3,
900
- elem_classes=["status-box"],
901
- )
902
-
903
- clear_btn.click(fn=clear_history, outputs=clear_status)
904
-
905
- # ============================================================
906
- # TAB 2: GOODREADS
907
- # ============================================================
908
- with gr.Tab("πŸ“– Books"):
909
- gr.HTML("""
910
- <div style="margin-bottom:18px;">
911
- <div class="section-label">Goodreads Scraper</div>
912
- <div class="section-title">BookBot β€” Best Books Q&A</div>
913
- <div class="section-desc">
914
- Ask BookBot anything about the scraped Goodreads \"Best Books Ever\" list.
915
- </div>
916
- </div>
917
- """)
918
-
919
- books_chatbot = gr.Chatbot(
920
- height=380,
921
- placeholder="<div style='text-align:center;color:#4a5a7a;padding:40px;font-family:Space Grotesk,sans-serif;'>πŸ“š Ask me about the Goodreads Best Books list!</div>",
922
- show_label=False
923
-
924
- )
925
-
926
- with gr.Row():
927
- books_msg = gr.Textbox(
928
- placeholder="e.g. Who wrote the top book?",
929
- scale=7,
930
- show_label=False,
931
- container=False,
932
- )
933
- books_send = gr.Button("Ask ↑", variant="primary", scale=1)
934
-
935
-
936
- def submit_books_text(message, history):
937
- history = list(history) if history else []
938
- if not message.strip():
939
- return history, ""
940
- reply = ask_books_ai(message, history)
941
- history.append({"role": "user", "content": message})
942
- history.append({"role": "assistant", "content": reply})
943
- return history, ""
944
-
945
-
946
- books_send.click(
947
- fn=submit_books_text,
948
- inputs=[books_msg, books_chatbot],
949
- outputs=[books_chatbot, books_msg]
950
- )
951
- books_msg.submit(
952
- fn=submit_books_text,
953
- inputs=[books_msg, books_chatbot],
954
- outputs=[books_chatbot, books_msg]
955
- )
956
-
957
- with gr.Column(elem_classes=["voice-box"]):
958
- gr.Markdown("**🎀 Voice Input / πŸ”Š Audio Reply**")
959
- with gr.Row():
960
- books_audio_in = gr.Audio(sources=["microphone"], type="filepath", label="Record Voice")
961
- books_audio_out = gr.Audio(label="Assistant Voice Reply", autoplay=True)
962
- with gr.Row():
963
- books_voice_btn = gr.Button("πŸŽ™οΈ Ask by Voice", variant="secondary")
964
- books_voice_status = gr.Textbox(label="Voice Status", interactive=False,
965
- elem_classes=["status-box"])
966
- books_voice_text = gr.Textbox(label="Transcribed Text", interactive=False)
967
-
968
- books_voice_btn.click(
969
- fn=process_voice_for_books,
970
- inputs=[books_audio_in, books_chatbot],
971
- outputs=[books_chatbot, books_voice_status, books_audio_out, books_voice_text]
972
- )
973
-
974
- gr.HTML("<hr>")
975
- gr.HTML("""
976
- <div style="margin-bottom:14px;">
977
- <div class="section-label">Scraped Data</div>
978
- <div class="section-title">Goodreads Top 20</div>
979
- </div>
980
- """)
981
-
982
- books_display = gr.Markdown(value=show_books_table(books_data, scrape_status))
983
- refresh_btn = gr.Button("πŸ”„ Re-Scrape Goodreads", variant="secondary")
984
-
985
-
986
- def refresh_data():
987
- global books_data, scrape_status
988
- books_data, scrape_status = get_books()
989
- return show_books_table(books_data, scrape_status)
990
-
991
-
992
- refresh_btn.click(fn=refresh_data, outputs=books_display)
993
-
994
- # ============================================================
995
- # TAB 3: YOUTUBE
996
- # ============================================================
997
- with gr.Tab("🎬 YouTube"):
998
- gr.HTML("""
999
- <div style="margin-bottom:18px;">
1000
- <div class="section-label">YouTube Transcript</div>
1001
- <div class="section-title">Video Q&A Assistant</div>
1002
- <div class="section-desc">
1003
- Load any YouTube video's transcript, then ask questions about it.
1004
- </div>
1005
- </div>
1006
- """)
1007
-
1008
- gr.HTML("""
1009
- <div style="background:rgba(255,184,0,0.06);border:1px solid rgba(255,184,0,0.2);
1010
- border-left:3px solid #ffb800;border-radius:10px;
1011
- padding:12px 18px;margin-bottom:16px;
1012
- font-size:0.85rem;color:#c89a00;font-family:Space Grotesk,sans-serif;">
1013
- <strong>Step 1</strong> β€” Paste a YouTube Video ID or full URL and click <em>Load Transcript</em>
1014
- </div>
1015
- """)
1016
-
1017
- with gr.Row():
1018
- video_id_input = gr.Textbox(
1019
- placeholder="e.g. dQw4w9WgXcQ or https://youtu.be/…",
1020
- label="YouTube Video ID or URL",
1021
- scale=4,
1022
- )
1023
- load_btn = gr.Button("πŸ“₯ Load Transcript", variant="primary", scale=1)
1024
-
1025
- with gr.Row():
1026
- transcript_status = gr.Textbox(
1027
- label="Status",
1028
- interactive=False,
1029
- elem_classes=["status-box"],
1030
- )
1031
-
1032
- transcript_preview = gr.Markdown(
1033
- value="*Transcript preview will appear here…*",
1034
- elem_classes=["transcript-preview"],
1035
- )
1036
- load_btn.click(
1037
- fn=load_transcript,
1038
- inputs=[video_id_input],
1039
- outputs=[transcript_status, transcript_preview],
1040
- )
1041
-
1042
- gr.HTML("""
1043
- <div style="background:rgba(0,212,255,0.05);border:1px solid rgba(0,212,255,0.15);
1044
- border-left:3px solid #00d4ff;border-radius:10px;
1045
- padding:12px 18px;margin:16px 0;
1046
- font-size:0.85rem;color:#0099bb;font-family:Space Grotesk,sans-serif;">
1047
- <strong>Step 2</strong> β€” Ask anything about the loaded video below
1048
- </div>
1049
- """)
1050
-
1051
- youtube_chatbot = gr.Chatbot(
1052
- height=360,
1053
- placeholder="<div style='text-align:center;color:#4a5a7a;padding:40px;font-family:Space Grotesk,sans-serif;'>πŸ“₯ Load a transcript above, then ask anything!</div>",
1054
- show_label=False
1055
-
1056
- )
1057
-
1058
- with gr.Row():
1059
- youtube_msg = gr.Textbox(
1060
- placeholder="e.g. What is the main topic?",
1061
- scale=7,
1062
- show_label=False,
1063
- container=False,
1064
- )
1065
- youtube_send = gr.Button("Ask ↑", variant="primary", scale=1)
1066
-
1067
-
1068
- def submit_youtube_text(message, history):
1069
- history = list(history) if history else []
1070
- if not message.strip():
1071
- return history, ""
1072
- reply = ask_youtube_ai(message, history)
1073
- history.append({"role": "user", "content": message})
1074
- history.append({"role": "assistant", "content": reply})
1075
- return history, ""
1076
-
1077
-
1078
- youtube_send.click(
1079
- fn=submit_youtube_text,
1080
- inputs=[youtube_msg, youtube_chatbot],
1081
- outputs=[youtube_chatbot, youtube_msg]
1082
- )
1083
- youtube_msg.submit(
1084
- fn=submit_youtube_text,
1085
- inputs=[youtube_msg, youtube_chatbot],
1086
- outputs=[youtube_chatbot, youtube_msg]
1087
- )
1088
-
1089
- with gr.Column(elem_classes=["voice-box"]):
1090
- gr.Markdown("**🎀 Voice Input / πŸ”Š Audio Reply**")
1091
- with gr.Row():
1092
- youtube_audio_in = gr.Audio(sources=["microphone"], type="filepath", label="Record Voice")
1093
- youtube_audio_out = gr.Audio(label="Assistant Voice Reply", autoplay=True)
1094
- with gr.Row():
1095
- youtube_voice_btn = gr.Button("πŸŽ™οΈ Ask by Voice", variant="secondary")
1096
- youtube_voice_status = gr.Textbox(label="Voice Status", interactive=False,
1097
- elem_classes=["status-box"])
1098
- youtube_voice_text = gr.Textbox(label="Transcribed Text", interactive=False)
1099
-
1100
- youtube_voice_btn.click(
1101
- fn=process_voice_for_youtube,
1102
- inputs=[youtube_audio_in, youtube_chatbot],
1103
- outputs=[youtube_chatbot, youtube_voice_status, youtube_audio_out, youtube_voice_text]
1104
- )
1105
-
1106
- # ============================================================
1107
- # TAB 4: PREFERENCES
1108
- # ============================================================
1109
- with gr.Tab("βš™οΈ Preferences"):
1110
- gr.HTML("""
1111
- <div style="margin-bottom:20px;">
1112
- <div class="section-label">Personalization</div>
1113
- <div class="section-title">AI Behavior Settings</div>
1114
- <div class="section-desc">
1115
- These preferences are saved permanently and injected into every AI response across all tabs.
1116
- </div>
1117
- </div>
1118
- """)
1119
-
1120
- with gr.Row():
1121
- with gr.Column():
1122
- tone_input = gr.Dropdown(
1123
- choices=["friendly", "formal", "casual", "professional", "humorous"],
1124
- value=prefs.get("tone", "friendly"),
1125
- label="🎭 Tone",
1126
- info="How the AI speaks to you",
1127
- )
1128
- lang_input = gr.Dropdown(
1129
- choices=["English", "Urdu", "Roman Urdu", "Arabic", "French", "Spanish"],
1130
- value=prefs.get("language", "English"),
1131
- label="🌐 Response Language",
1132
- info="Language for all AI responses",
1133
- )
1134
-
1135
- with gr.Column():
1136
- fmt_input = gr.Dropdown(
1137
- choices=["concise", "detailed", "bullet points", "numbered list", "paragraph"],
1138
- value=prefs.get("format", "concise"),
1139
- label="πŸ“ Response Format",
1140
- info="How responses should be structured",
1141
- )
1142
- custom_input = gr.Textbox(
1143
- value=prefs.get("custom_rules", ""),
1144
- label="✏️ Custom Rules",
1145
- placeholder='e.g. "Always cite sources" or "Use emojis sparingly"',
1146
- lines=3,
1147
- info="Extra instructions for the AI to always follow",
1148
- )
1149
-
1150
- with gr.Row():
1151
- save_btn = gr.Button("πŸ’Ύ Save Preferences", variant="primary", scale=1)
1152
- pref_status = gr.Textbox(
1153
- label="Status",
1154
- interactive=False,
1155
- scale=2,
1156
- elem_classes=["status-box"],
1157
- )
1158
-
1159
- save_btn.click(
1160
- fn=update_preferences,
1161
- inputs=[tone_input, lang_input, fmt_input, custom_input],
1162
- outputs=pref_status,
1163
- )
1164
-
1165
-
1166
- def show_current_prefs():
1167
- p = load_preferences()
1168
- return f"""```
1169
- Tone β†’ {p.get('tone')}
1170
- Language β†’ {p.get('language')}
1171
- Format β†’ {p.get('format')}
1172
- Custom Rules β†’ {p.get('custom_rules') or 'None'}
1173
- ```"""
1174
-
1175
-
1176
- pref_preview = gr.Markdown(value=show_current_prefs())
1177
- save_btn.click(
1178
- fn=lambda t, l, f, c: show_current_prefs(),
1179
- inputs=[tone_input, lang_input, fmt_input, custom_input],
1180
- outputs=pref_preview,
1181
- )
1182
-
1183
- # ============================================================
1184
- # TAB 5: ABOUT
1185
- # ============================================================
1186
- with gr.Tab("ℹ️ About"):
1187
- gr.HTML(f"""
1188
- <div style="max-width:680px;margin:0 auto;padding:10px 0 24px;">
1189
- <div style="margin-bottom:24px;">
1190
- <div class="section-label">Project Info</div>
1191
- <div class="section-title">NeuralChat β€” Version 5</div>
1192
- <div class="section-desc">
1193
- A multi-tab AI assistant with web scraping, YouTube transcript Q&A,
1194
- persistent memory, user preference personalization, voice input, audio output, and on-demand image generation.
1195
- </div>
1196
- </div>
1197
- <div style="background:var(--bg-card,#141d35);border:1px solid #1e2d50;
1198
- border-radius:14px;overflow:hidden;margin-bottom:20px;">
1199
- <table style="width:100%;border-collapse:collapse;">
1200
- <thead>
1201
- <tr style="background:rgba(0,212,255,0.07);">
1202
- <th style="padding:11px 16px;text-align:left;font-size:0.72rem;letter-spacing:1px;text-transform:uppercase;color:#00d4ff;border-bottom:1px solid #1e2d50;">Layer</th>
1203
- <th style="padding:11px 16px;text-align:left;font-size:0.72rem;letter-spacing:1px;text-transform:uppercase;color:#00d4ff;border-bottom:1px solid #1e2d50;">Detail</th>
1204
- </tr>
1205
- </thead>
1206
- <tbody style="color:#e8edf8;font-size:0.875rem;">
1207
- <tr style="border-bottom:1px solid #1e2d50;"><td style="padding:10px 16px;color:#8899bb;">🌐 Scraping</td><td style="padding:10px 16px;">Bright Data Web Unlocker + BeautifulSoup</td></tr>
1208
- <tr style="border-bottom:1px solid #1e2d50;"><td style="padding:10px 16px;color:#8899bb;">🎬 YouTube</td><td style="padding:10px 16px;">youtube-transcript-api</td></tr>
1209
- <tr style="border-bottom:1px solid #1e2d50;"><td style="padding:10px 16px;color:#8899bb;">🎀 STT</td><td style="padding:10px 16px;">Groq Whisper Large v3 Turbo</td></tr>
1210
- <tr style="border-bottom:1px solid #1e2d50;"><td style="padding:10px 16px;color:#8899bb;">πŸ”Š TTS</td><td style="padding:10px 16px;">OpenAI tts-1 (alloy)</td></tr>
1211
- <tr style="border-bottom:1px solid #1e2d50;"><td style="padding:10px 16px;color:#8899bb;">πŸ€– AI (Main)</td><td style="padding:10px 16px;">Groq β€” llama-3.3-70b-versatile</td></tr>
1212
- <tr style="border-bottom:1px solid #1e2d50;"><td style="padding:10px 16px;color:#8899bb;">πŸ’Ύ Memory</td><td style="padding:10px 16px;">JSON persistent storage</td></tr>
1213
- <tr><td style="padding:10px 16px;color:#8899bb;">βš™οΈ Prefs</td><td style="padding:10px 16px;">JSON persistent storage</td></tr>
1214
- </tbody>
1215
- </table>
1216
- </div>
1217
- <div style="text-align:center;padding-top:10px;font-size:0.78rem;color:#4a5a7a;font-family:JetBrains Mono,monospace;letter-spacing:0.5px;border-top:1px solid #1e2d50;">
1218
- Assignment 01 β€” Ver 5 &nbsp;|&nbsp; Dept. of Data Science, University of Punjab<br>
1219
- Instructor: Dr. Muhammad Arif Butt
1220
- </div>
1221
- </div>
1222
- """)
1223
-
1224
- gr.HTML("""
1225
- <div class="footer-strip">
1226
- NeuralChat v5 &nbsp;Β·&nbsp; Groq + OpenAI + Gradio &nbsp;Β·&nbsp; Built for DSAI @ UOP
1227
- </div>
1228
- """)
1229
-
1230
- demo.launch(
1231
- share=False,
1232
- inbrowser=True,
1233
- css=CUSTOM_CSS,
1234
- )
 
1
+ import requests
2
+ from bs4 import BeautifulSoup
3
+ import os
4
+ import re
5
+ import json
6
+ import tempfile
7
+ from datetime import datetime
8
+ from dotenv import load_dotenv
9
+ from groq import Groq
10
+ from openai import OpenAI
11
+ import gradio as gr
12
+
13
+ # -----------------------------------------------
14
+ # LOAD API KEYS
15
+ # -----------------------------------------------
16
+ # load_dotenv() # Remove on Hugging Face
17
+ BRIGHTDATA_API_KEY = os.getenv("BRIGHTDATA_API_KEY")
18
+ BRIGHTDATA_USER = os.getenv("BRIGHTDATA_USER", "")
19
+ BRIGHTDATA_PASS = os.getenv("BRIGHTDATA_PASS", "")
20
+ BRIGHTDATA_HOST = os.getenv("BRIGHTDATA_HOST", "brd.superproxy.io")
21
+ BRIGHTDATA_PORT = os.getenv("BRIGHTDATA_PORT", "22225")
22
+ GROQ_API_KEY = os.getenv("GROQ_API_KEY")
23
+ OPENAI_API_KEY = os.getenv("OPENAI_API_KEY")
24
+ client = Groq(api_key=GROQ_API_KEY)
25
+ openai_client = OpenAI(api_key=OPENAI_API_KEY) if OPENAI_API_KEY else None
26
+
27
+ print("GROQ key exists:", bool(GROQ_API_KEY))
28
+ print("OPENAI key exists:", bool(OPENAI_API_KEY))
29
+
30
+ # -----------------------------------------------
31
+ # PERSISTENT STORAGE β€” JSON Files
32
+ # -----------------------------------------------
33
+ HISTORY_FILE = "chat_history.json"
34
+ PREFERENCES_FILE = "user_preferences.json"
35
+
36
+
37
+ def load_chat_history():
38
+ """Load chat history from JSON file."""
39
+ try:
40
+ if os.path.exists(HISTORY_FILE):
41
+ with open(HISTORY_FILE, "r", encoding="utf-8") as f:
42
+ data = json.load(f)
43
+ return data if isinstance(data, list) else []
44
+ except Exception as e:
45
+ print(f"[Storage] Could not load history: {e}")
46
+ return []
47
+
48
+
49
+ def save_chat_history(history):
50
+ """Save chat history to JSON file after every turn."""
51
+ try:
52
+ with open(HISTORY_FILE, "w", encoding="utf-8") as f:
53
+ json.dump(history, f, indent=2, ensure_ascii=False)
54
+ except Exception as e:
55
+ print(f"[Storage] Could not save history: {e}")
56
+
57
+
58
+ def load_preferences():
59
+ """Load user preferences from JSON file."""
60
+ try:
61
+ if os.path.exists(PREFERENCES_FILE):
62
+ with open(PREFERENCES_FILE, "r", encoding="utf-8") as f:
63
+ return json.load(f)
64
+ except Exception as e:
65
+ print(f"[Storage] Could not load preferences: {e}")
66
+ return {
67
+ "tone": "friendly",
68
+ "language": "English",
69
+ "format": "concise",
70
+ "custom_rules": ""
71
+ }
72
+
73
+
74
+ def save_preferences(prefs):
75
+ """Save user preferences to JSON file."""
76
+ try:
77
+ with open(PREFERENCES_FILE, "w", encoding="utf-8") as f:
78
+ json.dump(prefs, f, indent=2, ensure_ascii=False)
79
+ return "βœ… Preferences saved!"
80
+ except Exception as e:
81
+ return f"❌ Could not save: {str(e)}"
82
+
83
+
84
+ # Load at startup
85
+ chat_history_store = load_chat_history()
86
+ user_preferences = load_preferences()
87
+ print(f"[Storage] Loaded {len(chat_history_store)} messages from history")
88
+ print(f"[Storage] Preferences: {user_preferences}")
89
+
90
+
91
+ # ================================================================
92
+ # VERSION 1: GOODREADS SCRAPER
93
+ # ================================================================
94
+ def get_books():
95
+ if not BRIGHTDATA_API_KEY:
96
+ return [], "❌ BRIGHTDATA_API_KEY not found!"
97
+
98
+ headers = {
99
+ "Authorization": f"Bearer {BRIGHTDATA_API_KEY}",
100
+ "Content-Type": "application/json"
101
+ }
102
+ data = {
103
+ "zone": "web_unlocker2",
104
+ "url": "https://www.goodreads.com/list/show/1.Best_Books_Ever",
105
+ "format": "raw"
106
+ }
107
+
108
+ try:
109
+ response = requests.post(
110
+ "https://api.brightdata.com/request",
111
+ json=data,
112
+ headers=headers,
113
+ timeout=30
114
+ )
115
+ if response.status_code != 200:
116
+ return [], f"❌ Scraping failed! Status: {response.status_code}"
117
+
118
+ soup = BeautifulSoup(response.text, "html.parser")
119
+ books = soup.select("tr[itemtype='http://schema.org/Book']")
120
+ if not books:
121
+ return [], "⚠️ No books found."
122
+
123
+ results = []
124
+ for i, book in enumerate(books[:20], start=1):
125
+ try:
126
+ title = book.select_one("a.bookTitle span").text.strip()
127
+ author = book.select_one("a.authorName span").text.strip()
128
+ rating = book.select_one("span.minirating").text.strip()
129
+ results.append({
130
+ "rank": i,
131
+ "title": title,
132
+ "author": author,
133
+ "rating": rating
134
+ })
135
+ except Exception:
136
+ pass
137
+ return results, f"βœ… Scraped {len(results)} books!"
138
+ except Exception as e:
139
+ return [], f"❌ Error: {str(e)}"
140
+
141
+
142
+ def format_books_as_text(books):
143
+ if not books:
144
+ return "No book data available."
145
+ return "\n".join([
146
+ f"Rank #{b['rank']}: \"{b['title']}\" by {b['author']} β€” Rating: {b['rating']}"
147
+ for b in books
148
+ ])
149
+
150
+
151
+ def show_books_table(books_data, scrape_status):
152
+ if not books_data:
153
+ return f"**Status:** {scrape_status}"
154
+ md = f"**{scrape_status}**\n\n| Rank | Title | Author | Rating |\n|------|-------|--------|--------|\n"
155
+ for b in books_data:
156
+ md += f"| #{b['rank']} | {b['title']} | {b['author']} | {b['rating']} |\n"
157
+ return md
158
+
159
+
160
+ print("πŸ”„ Scraping Goodreads...")
161
+ books_data, scrape_status = get_books()
162
+ print(scrape_status)
163
+
164
+
165
+ # ================================================================
166
+ # VERSION 2: YOUTUBE TRANSCRIPT
167
+ # ================================================================
168
+ def get_youtube_transcript(video_id: str):
169
+ """Fetch YouTube transcript β€” old aur new API dono handle karta hai."""
170
+ video_id = video_id.strip()
171
+
172
+ if "youtube.com/watch" in video_id:
173
+ video_id = video_id.split("v=")[-1].split("&")[0]
174
+ elif "youtu.be/" in video_id:
175
+ video_id = video_id.split("youtu.be/")[-1].split("?")[0]
176
+
177
+ if not video_id:
178
+ return None, "❌ Please enter a valid YouTube Video ID or URL."
179
+
180
+ try:
181
+ from youtube_transcript_api import YouTubeTranscriptApi
182
+
183
+ try:
184
+ ytt = YouTubeTranscriptApi()
185
+ fetched = ytt.fetch(video_id)
186
+ full_text = " ".join([s.text for s in fetched])
187
+ if full_text.strip():
188
+ return full_text, f"βœ… Transcript fetched! ({len(full_text)} chars, ID: {video_id})"
189
+ except Exception:
190
+ pass
191
+
192
+ try:
193
+ fetched = YouTubeTranscriptApi.get_transcript(video_id)
194
+ full_text = " ".join([s["text"] for s in fetched])
195
+ if full_text.strip():
196
+ return full_text, f"βœ… Transcript fetched! ({len(full_text)} chars, ID: {video_id})"
197
+ except Exception:
198
+ pass
199
+
200
+ return None, "⚠️ Transcript empty or not available. Try another video."
201
+
202
+ except Exception as e:
203
+ return None, f"❌ Error: {str(e)}"
204
+
205
+
206
+ current_transcript = {"text": None, "status": "No transcript loaded."}
207
+
208
+
209
+ def load_transcript(video_id):
210
+ global current_transcript
211
+ text, status = get_youtube_transcript(video_id)
212
+ current_transcript["text"] = text
213
+ current_transcript["status"] = status
214
+ if text:
215
+ preview = text[:600] + "..." if len(text) > 600 else text
216
+ return status, f"**πŸ“„ Preview:**\n\n_{preview}_"
217
+ return status, "No preview available."
218
+
219
+
220
+ # ================================================================
221
+ # VERSION 4: VOICE HELPERS
222
+ # ================================================================
223
+ def transcribe_audio(audio_path):
224
+ """Convert microphone audio to text using Groq Whisper."""
225
+ if not audio_path:
226
+ return "", "⚠️ No audio recorded."
227
+
228
+ if not GROQ_API_KEY:
229
+ return "", "❌ GROQ_API_KEY not set for transcription."
230
+
231
+ try:
232
+ with open(audio_path, "rb") as audio_file:
233
+ transcript = client.audio.transcriptions.create(
234
+ file=audio_file,
235
+ model="whisper-large-v3-turbo",
236
+ response_format="verbose_json"
237
+ )
238
+
239
+ text = getattr(transcript, "text", "") or ""
240
+ if not text.strip():
241
+ return "", "⚠️ Speech detected but transcript is empty."
242
+
243
+ return text.strip(), "βœ… Voice transcribed successfully!"
244
+ except Exception as e:
245
+ return "", f"❌ Transcription error: {str(e)}"
246
+
247
+
248
+ def text_to_speech(text):
249
+ """Convert assistant reply to speech using OpenAI TTS."""
250
+ if not text or not text.strip():
251
+ return None, "⚠️ Empty text for audio output."
252
+
253
+ if not OPENAI_API_KEY or not openai_client:
254
+ return None, "⚠️ OPENAI_API_KEY not set. Text reply works, audio skipped."
255
+
256
+ try:
257
+ tmp_file = tempfile.NamedTemporaryFile(delete=False, suffix=".mp3")
258
+ tmp_path = tmp_file.name
259
+ tmp_file.close()
260
+
261
+ speech_response = openai_client.audio.speech.create(
262
+ model="tts-1",
263
+ voice="alloy",
264
+ input=text[:1000]
265
+ )
266
+
267
+ with open(tmp_path, "wb") as f:
268
+ f.write(speech_response.content)
269
+
270
+ return tmp_path, "βœ… Audio reply generated!"
271
+ except Exception as e:
272
+ print("TTS full error:", repr(e))
273
+ return None, f"❌ TTS error: {str(e)}"
274
+
275
+
276
+ def should_generate_image(user_text, reply_text):
277
+ text = f"{user_text} {reply_text}".lower()
278
+ triggers = [
279
+ "draw", "image", "picture", "diagram", "visual", "visually",
280
+ "show me", "illustrate", "architecture", "flowchart", "chart",
281
+ "generate image", "generate an image", "show a", "show an",
282
+ "solar system", "transformer", "mind map", "scene"
283
+ ]
284
+ return any(t in text for t in triggers)
285
+
286
+
287
+ def build_image_prompt(user_text, reply_text):
288
+ if not GROQ_API_KEY:
289
+ return user_text[:500]
290
+ try:
291
+ resp = client.chat.completions.create(
292
+ model="llama-3.1-8b-instant",
293
+ messages=[
294
+ {"role": "system",
295
+ "content": "Create one short descriptive image prompt. Return only the prompt text."},
296
+ {"role": "user",
297
+ "content": f"User request: {user_text}\nAssistant reply: {reply_text[:1000]}\nCreate a concise image generation prompt."}
298
+ ],
299
+ temperature=0.4,
300
+ max_tokens=120
301
+ )
302
+ return (resp.choices[0].message.content or user_text).strip()
303
+ except Exception as e:
304
+ print("Image prompt error:", repr(e))
305
+ return user_text[:500]
306
+
307
+
308
+ def maybe_generate_image(user_text, reply_text):
309
+ if not should_generate_image(user_text, reply_text):
310
+ return None, "ℹ️ No image needed for this reply."
311
+ if not OPENAI_API_KEY:
312
+ return None, "⚠️ OPENAI_API_KEY missing for image generation."
313
+ try:
314
+ image_prompt = build_image_prompt(user_text, reply_text)
315
+ image_resp = openai_client.images.generate(
316
+ model="dall-e-3",
317
+ prompt=image_prompt,
318
+ size="1024x1024",
319
+ quality="standard",
320
+ n=1
321
+ )
322
+ image_url = image_resp.data[0].url
323
+ if not image_url:
324
+ return None, "⚠️ Image was not generated."
325
+ img_data = requests.get(image_url, timeout=60).content
326
+ tmp_file = tempfile.NamedTemporaryFile(delete=False, suffix=".png")
327
+ tmp_file.write(img_data)
328
+ tmp_file.close()
329
+ return tmp_file.name, "βœ… Image generated!"
330
+ except Exception as e:
331
+ print("Image generation error:", repr(e))
332
+ return None, f"❌ Image generation error: {str(e)}"
333
+
334
+
335
+ def process_voice_for_main_chat(audio_path, history):
336
+ """Voice input for main chatbot."""
337
+ transcript_text, voice_status = transcribe_audio(audio_path)
338
+ if not transcript_text:
339
+ return history, voice_status, None, "", None, ""
340
+
341
+ reply = ask_main_ai(transcript_text, history or [])
342
+ audio_reply, tts_status = text_to_speech(reply)
343
+
344
+ updated_history = list(history) if history else []
345
+ updated_history.append({"role": "user", "content": transcript_text})
346
+ updated_history.append({"role": "assistant", "content": reply})
347
+
348
+ image_path, image_status = maybe_generate_image(transcript_text, reply)
349
+ combined_status = f"{voice_status}\n{tts_status}"
350
+ return updated_history, combined_status, audio_reply, transcript_text, image_path, image_status
351
+
352
+
353
+ def process_voice_for_books(audio_path, history):
354
+ """Voice input for books chatbot."""
355
+ transcript_text, voice_status = transcribe_audio(audio_path)
356
+ if not transcript_text:
357
+ return history, voice_status, None, "", None, ""
358
+
359
+ reply = ask_books_ai(transcript_text, history or [])
360
+ audio_reply, tts_status = text_to_speech(reply)
361
+
362
+ updated_history = list(history) if history else []
363
+ updated_history.append({"role": "user", "content": transcript_text})
364
+ updated_history.append({"role": "assistant", "content": reply})
365
+
366
+ image_path, image_status = maybe_generate_image(transcript_text, reply)
367
+ combined_status = f"{voice_status}\n{tts_status}"
368
+ return updated_history, combined_status, audio_reply, transcript_text, image_path, image_status
369
+
370
+
371
+ def process_voice_for_youtube(audio_path, history):
372
+ """Voice input for YouTube chatbot."""
373
+ transcript_text, voice_status = transcribe_audio(audio_path)
374
+ if not transcript_text:
375
+ return history, voice_status, None, "", None, ""
376
+
377
+ reply = ask_youtube_ai(transcript_text, history or [])
378
+ audio_reply, tts_status = text_to_speech(reply)
379
+
380
+ updated_history = list(history) if history else []
381
+ updated_history.append({"role": "user", "content": transcript_text})
382
+ updated_history.append({"role": "assistant", "content": reply})
383
+
384
+ image_path, image_status = maybe_generate_image(transcript_text, reply)
385
+ combined_status = f"{voice_status}\n{tts_status}"
386
+ return updated_history, combined_status, audio_reply, transcript_text, image_path, image_status
387
+
388
+
389
+ # ================================================================
390
+ # VERSION 3: PREFERENCES + PERSISTENT CHAT
391
+ # ================================================================
392
+ def build_system_prompt(base_prompt, prefs):
393
+ """Inject user preferences into system prompt dynamically."""
394
+ pref_text = f"""
395
+ USER PREFERENCES (follow these always):
396
+ - Tone: {prefs.get('tone', 'friendly')}
397
+ - Language: {prefs.get('language', 'English')}
398
+ - Response Format: {prefs.get('format', 'concise')}
399
+ """
400
+ custom = prefs.get("custom_rules", "").strip()
401
+ if custom:
402
+ pref_text += f"- Custom Rules: {custom}\n"
403
+ return base_prompt + pref_text
404
+
405
+
406
+ def convert_history_for_display(history_store):
407
+ """Convert stored history to Gradio chatbot format (list of dicts)."""
408
+ return [{"role": item["role"], "content": item["content"]} for item in history_store]
409
+
410
+
411
+ def ask_main_ai(message, history):
412
+ """
413
+ VERSION 3 Main Chatbot:
414
+ - Multi-turn (session memory via history)
415
+ - Persistent (saves to JSON after every turn)
416
+ - Preferences injected into system prompt
417
+ """
418
+ global chat_history_store, user_preferences
419
+
420
+ if not GROQ_API_KEY:
421
+ return "❌ GROQ_API_KEY not set."
422
+
423
+ base_prompt = """You are a smart, helpful AI assistant with memory of past conversations.
424
+ You help users with general questions, book recommendations, research, and more.
425
+ Always maintain context from previous messages in the conversation."""
426
+
427
+ system = build_system_prompt(base_prompt, user_preferences)
428
+ messages = [{"role": "system", "content": system}]
429
+
430
+ for item in chat_history_store:
431
+ messages.append({"role": item["role"], "content": item["content"]})
432
+
433
+ for item in history:
434
+ if isinstance(item, dict):
435
+ if item not in chat_history_store:
436
+ messages.append({"role": item["role"], "content": item["content"]})
437
+ else:
438
+ messages.append({"role": "user", "content": item[0]})
439
+ messages.append({"role": "assistant", "content": item[1]})
440
+
441
+ messages.append({"role": "user", "content": message})
442
+
443
+ try:
444
+ response = client.chat.completions.create(
445
+ model="llama-3.1-8b-instant",
446
+ messages=messages,
447
+ temperature=0.6,
448
+ max_tokens=1024
449
+ )
450
+ reply = response.choices[0].message.content
451
+
452
+ chat_history_store.append({"role": "user", "content": message})
453
+ chat_history_store.append({"role": "assistant", "content": reply})
454
+ save_chat_history(chat_history_store)
455
+
456
+ return reply
457
+
458
+ except Exception as e:
459
+ return f"❌ AI Error: {str(e)}"
460
+
461
+
462
+ def ask_books_ai(message, history):
463
+ """Goodreads Q&A β€” Version 1 (carried forward)."""
464
+ if not GROQ_API_KEY:
465
+ return "❌ GROQ_API_KEY not set."
466
+ if not books_data:
467
+ return f"⚠️ No book data. Status: {scrape_status}"
468
+
469
+ base = """You are a smart and friendly book assistant named BookBot πŸ“š.
470
+ Books data from Goodreads:
471
+ {context}
472
+ RULES: Only answer from this data. Be friendly and concise."""
473
+
474
+ system = build_system_prompt(base.format(context=format_books_as_text(books_data)), user_preferences)
475
+ messages = [{"role": "system", "content": system}]
476
+ for item in history:
477
+ if isinstance(item, dict):
478
+ messages.append({"role": item["role"], "content": item["content"]})
479
+ else:
480
+ messages.append({"role": "user", "content": item[0]})
481
+ messages.append({"role": "assistant", "content": item[1]})
482
+ messages.append({"role": "user", "content": message})
483
+
484
+ try:
485
+ resp = client.chat.completions.create(
486
+ model="llama-3.1-8b-instant",
487
+ messages=messages,
488
+ temperature=0.5,
489
+ max_tokens=1024
490
+ )
491
+ return resp.choices[0].message.content
492
+ except Exception as e:
493
+ return f"❌ AI Error: {str(e)}"
494
+
495
+
496
+ def ask_youtube_ai(message, history):
497
+ """YouTube Q&A β€” Version 2 (carried forward)."""
498
+ if not GROQ_API_KEY:
499
+ return "❌ GROQ_API_KEY not set."
500
+ transcript = current_transcript.get("text")
501
+ if not transcript:
502
+ return "⚠️ No transcript loaded. Enter a YouTube Video ID and click 'Load Transcript' first."
503
+
504
+ base = "You are a helpful assistant answering ONLY from this transcript:\n{transcript}\nRULES: Only use transcript info. Be concise."
505
+ system = build_system_prompt(base.format(transcript=transcript[:6000]), user_preferences)
506
+ messages = [{"role": "system", "content": system}]
507
+ for item in history:
508
+ if isinstance(item, dict):
509
+ messages.append({"role": item["role"], "content": item["content"]})
510
+ else:
511
+ messages.append({"role": "user", "content": item[0]})
512
+ messages.append({"role": "assistant", "content": item[1]})
513
+ messages.append({"role": "user", "content": message})
514
+
515
+ try:
516
+ resp = client.chat.completions.create(
517
+ model="llama-3.1-8b-instant",
518
+ messages=messages,
519
+ temperature=0.4,
520
+ max_tokens=1024
521
+ )
522
+ return resp.choices[0].message.content
523
+ except Exception as e:
524
+ return f"❌ AI Error: {str(e)}"
525
+
526
+
527
+ def update_preferences(tone, language, fmt, custom_rules):
528
+ """Save updated preferences and return status."""
529
+ global user_preferences
530
+ user_preferences = {
531
+ "tone": tone,
532
+ "language": language,
533
+ "format": fmt,
534
+ "custom_rules": custom_rules
535
+ }
536
+ status = save_preferences(user_preferences)
537
+ return status
538
+
539
+
540
+ def clear_history():
541
+ """Clear all persistent chat history."""
542
+ global chat_history_store
543
+ chat_history_store = []
544
+ save_chat_history([])
545
+ return "πŸ—‘οΈ Chat history cleared!"
546
+
547
+
548
+ # ================================================================
549
+ # CUSTOM CSS β€” SAME UI + SMALL AUDIO SECTION SUPPORT
550
+ # ================================================================
551
+ CUSTOM_CSS = """
552
+ /* ── Google Fonts ── */
553
+ @import url('https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@300;400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap');
554
+ :root {
555
+ --bg-primary: #0a0e1a;
556
+ --bg-secondary: #0f1629;
557
+ --bg-card: #141d35;
558
+ --bg-card-hover: #1a2540;
559
+ --accent-cyan: #00d4ff;
560
+ --accent-emerald: #00ff9d;
561
+ --accent-amber: #ffb800;
562
+ --accent-rose: #ff4f7b;
563
+ --text-primary: #e8edf8;
564
+ --text-secondary: #8899bb;
565
+ --text-muted: #4a5a7a;
566
+ --border: #1e2d50;
567
+ --border-bright: #2a3f70;
568
+ --glow-cyan: 0 0 20px rgba(0,212,255,0.15);
569
+ --glow-emerald: 0 0 20px rgba(0,255,157,0.12);
570
+ --radius-sm: 8px;
571
+ --radius-md: 14px;
572
+ --radius-lg: 20px;
573
+ --font-main: 'Space Grotesk', sans-serif;
574
+ --font-mono: 'JetBrains Mono', monospace;
575
+ }
576
+ *, *::before, *::after { box-sizing: border-box; }
577
+ body, .gradio-container {
578
+ background: var(--bg-primary) !important;
579
+ font-family: var(--font-main) !important;
580
+ color: var(--text-primary) !important;
581
+ min-height: 100vh;
582
+ }
583
+ .gradio-container {
584
+ background:
585
+ radial-gradient(ellipse 80% 50% at 20% 10%, rgba(0,212,255,0.04) 0%, transparent 60%),
586
+ radial-gradient(ellipse 60% 40% at 80% 90%, rgba(0,255,157,0.03) 0%, transparent 60%),
587
+ var(--bg-primary) !important;
588
+ max-width: 1200px !important;
589
+ margin: 0 auto !important;
590
+ padding: 0 16px !important;
591
+ }
592
+ .hero-header {
593
+ text-align: center;
594
+ padding: 40px 20px 28px;
595
+ position: relative;
596
+ }
597
+ .hero-header::before {
598
+ content: '';
599
+ position: absolute;
600
+ top: 0; left: 50%; transform: translateX(-50%);
601
+ width: 300px; height: 1px;
602
+ background: linear-gradient(90deg, transparent, var(--accent-cyan), transparent);
603
+ }
604
+ .hero-title {
605
+ font-size: 2.4rem;
606
+ font-weight: 700;
607
+ letter-spacing: -0.5px;
608
+ background: linear-gradient(135deg, var(--accent-cyan) 0%, var(--accent-emerald) 60%, var(--accent-amber) 100%);
609
+ -webkit-background-clip: text;
610
+ -webkit-text-fill-color: transparent;
611
+ background-clip: text;
612
+ margin: 0 0 10px;
613
+ line-height: 1.1;
614
+ }
615
+ .hero-subtitle {
616
+ color: var(--text-secondary);
617
+ font-size: 0.95rem;
618
+ font-weight: 400;
619
+ letter-spacing: 0.3px;
620
+ margin: 0;
621
+ }
622
+ .hero-subtitle span {
623
+ color: var(--accent-cyan);
624
+ font-weight: 500;
625
+ }
626
+ .hero-badges {
627
+ display: flex;
628
+ justify-content: center;
629
+ gap: 10px;
630
+ margin-top: 16px;
631
+ flex-wrap: wrap;
632
+ }
633
+ .badge {
634
+ background: var(--bg-card);
635
+ border: 1px solid var(--border-bright);
636
+ border-radius: 20px;
637
+ padding: 4px 14px;
638
+ font-size: 0.75rem;
639
+ font-family: var(--font-mono);
640
+ color: var(--text-secondary);
641
+ letter-spacing: 0.5px;
642
+ }
643
+ .tab-nav {
644
+ background: var(--bg-secondary) !important;
645
+ border: 1px solid var(--border) !important;
646
+ border-radius: var(--radius-lg) !important;
647
+ padding: 6px !important;
648
+ margin-bottom: 20px !important;
649
+ display: flex;
650
+ gap: 4px;
651
+ }
652
+ .tab-nav button {
653
+ background: transparent !important;
654
+ border: none !important;
655
+ color: var(--text-muted) !important;
656
+ font-family: var(--font-main) !important;
657
+ font-size: 0.875rem !important;
658
+ font-weight: 500 !important;
659
+ padding: 10px 20px !important;
660
+ border-radius: var(--radius-md) !important;
661
+ cursor: pointer !important;
662
+ transition: all 0.25s ease !important;
663
+ letter-spacing: 0.2px !important;
664
+ white-space: nowrap !important;
665
+ }
666
+ .tab-nav button:hover {
667
+ background: var(--bg-card) !important;
668
+ color: var(--text-primary) !important;
669
+ }
670
+ .tab-nav button.selected {
671
+ background: linear-gradient(135deg, rgba(0,212,255,0.15), rgba(0,255,157,0.08)) !important;
672
+ color: var(--accent-cyan) !important;
673
+ border: 1px solid rgba(0,212,255,0.25) !important;
674
+ box-shadow: var(--glow-cyan) !important;
675
+ }
676
+ .chatbot-wrap .chatbot,
677
+ .chatbot-wrap > div {
678
+ background: var(--bg-secondary) !important;
679
+ border: 1px solid var(--border) !important;
680
+ border-radius: var(--radius-md) !important;
681
+ }
682
+ .message.user {
683
+ background: linear-gradient(135deg, rgba(0,212,255,0.12), rgba(0,212,255,0.06)) !important;
684
+ border: 1px solid rgba(0,212,255,0.2) !important;
685
+ border-radius: var(--radius-md) var(--radius-sm) var(--radius-md) var(--radius-md) !important;
686
+ color: var(--text-primary) !important;
687
+ font-family: var(--font-main) !important;
688
+ }
689
+ .message.bot {
690
+ background: var(--bg-card) !important;
691
+ border: 1px solid var(--border-bright) !important;
692
+ border-radius: var(--radius-sm) var(--radius-md) var(--radius-md) var(--radius-md) !important;
693
+ color: var(--text-primary) !important;
694
+ font-family: var(--font-main) !important;
695
+ }
696
+ input[type="text"],
697
+ textarea,
698
+ .gr-textbox,
699
+ .gr-textbox textarea,
700
+ .gr-textbox input {
701
+ background: var(--bg-secondary) !important;
702
+ border: 1px solid var(--border-bright) !important;
703
+ border-radius: var(--radius-sm) !important;
704
+ color: var(--text-primary) !important;
705
+ font-family: var(--font-main) !important;
706
+ font-size: 0.9rem !important;
707
+ padding: 12px 16px !important;
708
+ transition: border-color 0.2s, box-shadow 0.2s !important;
709
+ outline: none !important;
710
+ }
711
+ input[type="text"]:focus,
712
+ textarea:focus,
713
+ .gr-textbox textarea:focus,
714
+ .gr-textbox input:focus {
715
+ border-color: var(--accent-cyan) !important;
716
+ box-shadow: 0 0 0 3px rgba(0,212,255,0.08) !important;
717
+ }
718
+ button.lg.primary, button[variant="primary"] {
719
+ background: linear-gradient(135deg, var(--accent-cyan), #0099cc) !important;
720
+ border: none !important;
721
+ border-radius: var(--radius-sm) !important;
722
+ color: #000 !important;
723
+ font-family: var(--font-main) !important;
724
+ font-weight: 600 !important;
725
+ font-size: 0.875rem !important;
726
+ letter-spacing: 0.3px !important;
727
+ padding: 11px 22px !important;
728
+ transition: all 0.2s ease !important;
729
+ cursor: pointer !important;
730
+ }
731
+ button.lg.secondary, button[variant="secondary"] {
732
+ background: var(--bg-card) !important;
733
+ border: 1px solid var(--border-bright) !important;
734
+ border-radius: var(--radius-sm) !important;
735
+ color: var(--text-primary) !important;
736
+ font-family: var(--font-main) !important;
737
+ font-weight: 500 !important;
738
+ font-size: 0.875rem !important;
739
+ padding: 11px 22px !important;
740
+ }
741
+ button.lg.stop, button[variant="stop"] {
742
+ background: rgba(255,79,123,0.1) !important;
743
+ border: 1px solid rgba(255,79,123,0.3) !important;
744
+ border-radius: var(--radius-sm) !important;
745
+ color: var(--accent-rose) !important;
746
+ font-family: var(--font-main) !important;
747
+ font-weight: 500 !important;
748
+ font-size: 0.875rem !important;
749
+ padding: 11px 22px !important;
750
+ }
751
+ .gr-markdown, .prose {
752
+ color: var(--text-primary) !important;
753
+ font-family: var(--font-main) !important;
754
+ }
755
+ .status-box textarea {
756
+ background: var(--bg-secondary) !important;
757
+ border: 1px solid var(--border) !important;
758
+ border-radius: var(--radius-sm) !important;
759
+ color: var(--text-secondary) !important;
760
+ font-family: var(--font-mono) !important;
761
+ font-size: 0.82rem !important;
762
+ }
763
+ .transcript-preview {
764
+ background: var(--bg-secondary);
765
+ border: 1px solid var(--border);
766
+ border-left: 3px solid var(--accent-amber);
767
+ border-radius: var(--radius-sm);
768
+ padding: 14px 18px;
769
+ font-size: 0.875rem;
770
+ line-height: 1.6;
771
+ color: var(--text-secondary);
772
+ max-height: 180px;
773
+ overflow-y: auto;
774
+ }
775
+ .voice-box {
776
+ background: rgba(0, 212, 255, 0.04);
777
+ border: 1px solid rgba(0, 212, 255, 0.18);
778
+ border-radius: 14px;
779
+ padding: 14px;
780
+ margin-top: 14px;
781
+ }
782
+ .footer-strip {
783
+ text-align: center;
784
+ padding: 18px 0 28px;
785
+ color: var(--text-muted);
786
+ font-size: 0.75rem;
787
+ font-family: var(--font-mono);
788
+ letter-spacing: 0.5px;
789
+ border-top: 1px solid var(--border);
790
+ margin-top: 24px;
791
+ }
792
+ """
793
+
794
+ # ================================================================
795
+ # GRADIO UI β€” SAME LAYOUT + VOICE BLOCKS ADDED
796
+ # ================================================================
797
+ initial_display = convert_history_for_display(chat_history_store)
798
+ prefs = load_preferences()
799
+
800
+ with gr.Blocks(title="⚑ NeuralChat β€” AI Assistant") as demo:
801
+ gr.HTML("""
802
+ <div class="hero-header">
803
+ <p class="hero-title">⚑ NeuralChat</p>
804
+ <p class="hero-subtitle">
805
+ Powered by <span>Groq LLaMA</span> &nbsp;Β·&nbsp;
806
+ <span>Persistent Memory</span> &nbsp;Β·&nbsp;
807
+ <span>Goodreads + YouTube + Voice + Images</span>
808
+ </p>
809
+ <div class="hero-badges">
810
+ <span class="badge">llama-3.3-70b</span>
811
+ <span class="badge">llama-3.1-8b</span>
812
+ <span class="badge">whisper-large-v3-turbo</span>
813
+ <span class="badge">tts-1</span>
814
+ <span class="badge">dall-e-3</span>
815
+ <span class="badge">ver 5.0</span>
816
+ </div>
817
+ </div>
818
+ """)
819
+
820
+ with gr.Tabs(elem_classes=["tab-nav"]):
821
+
822
+ # ============================================================
823
+ # TAB 1: MAIN CHATBOT
824
+ # ============================================================
825
+ with gr.Tab("πŸ’¬ Chat"):
826
+ gr.HTML("""
827
+ <div style="margin-bottom:18px;">
828
+ <div class="section-label">Main Assistant</div>
829
+ <div class="section-title">Conversational AI with Memory</div>
830
+ <div class="section-desc">
831
+ Your assistant remembers past sessions and adapts to your preferences automatically.
832
+ </div>
833
+ </div>
834
+ """)
835
+
836
+ main_chatbot = gr.Chatbot(
837
+ value=initial_display,
838
+ height=460,
839
+ placeholder="<div style='text-align:center;color:#4a5a7a;padding:40px;font-family:Space Grotesk,sans-serif;'>πŸ’¬ Start a conversation β€” I remember everything.</div>",
840
+ elem_classes=["chatbot-wrap"],
841
+ show_label=False
842
+
843
+ )
844
+
845
+ with gr.Row():
846
+ main_msg = gr.Textbox(
847
+ placeholder="Type a message…",
848
+ scale=7,
849
+ show_label=False,
850
+ container=False,
851
+ )
852
+ main_send = gr.Button("Send ↑", variant="primary", scale=1)
853
+
854
+
855
+ def submit_main_text(message, history):
856
+ history = list(history) if history else []
857
+ if not message.strip():
858
+ return history, ""
859
+ reply = ask_main_ai(message, history)
860
+ history.append({"role": "user", "content": message})
861
+ history.append({"role": "assistant", "content": reply})
862
+ return history, ""
863
+
864
+
865
+ main_send.click(
866
+ fn=submit_main_text,
867
+ inputs=[main_msg, main_chatbot],
868
+ outputs=[main_chatbot, main_msg]
869
+ )
870
+ main_msg.submit(
871
+ fn=submit_main_text,
872
+ inputs=[main_msg, main_chatbot],
873
+ outputs=[main_chatbot, main_msg]
874
+ )
875
+
876
+ with gr.Column(elem_classes=["voice-box"]):
877
+ gr.Markdown("**🎀 Voice Input / πŸ”Š Audio Reply**")
878
+ with gr.Row():
879
+ main_audio_in = gr.Audio(sources=["microphone"], type="filepath", label="Record Voice")
880
+ main_audio_out = gr.Audio(label="Assistant Voice Reply", autoplay=True)
881
+ with gr.Row():
882
+ main_voice_btn = gr.Button("πŸŽ™οΈ Ask by Voice", variant="secondary")
883
+ main_voice_status = gr.Textbox(label="Voice Status", interactive=False, elem_classes=["status-box"])
884
+ main_voice_text = gr.Textbox(label="Transcribed Text", interactive=False)
885
+
886
+ main_voice_btn.click(
887
+ fn=process_voice_for_main_chat,
888
+ inputs=[main_audio_in, main_chatbot],
889
+ outputs=[main_chatbot, main_voice_status, main_audio_out, main_voice_text]
890
+ )
891
+
892
+ gr.HTML("<div style='height:12px'></div>")
893
+
894
+ with gr.Row():
895
+ clear_btn = gr.Button("πŸ—‘οΈ Clear History", variant="stop", scale=1)
896
+ clear_status = gr.Textbox(
897
+ label="Status",
898
+ interactive=False,
899
+ scale=3,
900
+ elem_classes=["status-box"],
901
+ )
902
+
903
+ clear_btn.click(fn=clear_history, outputs=clear_status)
904
+
905
+ # ============================================================
906
+ # TAB 2: GOODREADS
907
+ # ============================================================
908
+ with gr.Tab("πŸ“– Books"):
909
+ gr.HTML("""
910
+ <div style="margin-bottom:18px;">
911
+ <div class="section-label">Goodreads Scraper</div>
912
+ <div class="section-title">BookBot β€” Best Books Q&A</div>
913
+ <div class="section-desc">
914
+ Ask BookBot anything about the scraped Goodreads \"Best Books Ever\" list.
915
+ </div>
916
+ </div>
917
+ """)
918
+
919
+ books_chatbot = gr.Chatbot(
920
+ height=380,
921
+ placeholder="<div style='text-align:center;color:#4a5a7a;padding:40px;font-family:Space Grotesk,sans-serif;'>πŸ“š Ask me about the Goodreads Best Books list!</div>",
922
+ show_label=False
923
+
924
+ )
925
+
926
+ with gr.Row():
927
+ books_msg = gr.Textbox(
928
+ placeholder="e.g. Who wrote the top book?",
929
+ scale=7,
930
+ show_label=False,
931
+ container=False,
932
+ )
933
+ books_send = gr.Button("Ask ↑", variant="primary", scale=1)
934
+
935
+
936
+ def submit_books_text(message, history):
937
+ history = list(history) if history else []
938
+ if not message.strip():
939
+ return history, ""
940
+ reply = ask_books_ai(message, history)
941
+ history.append({"role": "user", "content": message})
942
+ history.append({"role": "assistant", "content": reply})
943
+ return history, ""
944
+
945
+
946
+ books_send.click(
947
+ fn=submit_books_text,
948
+ inputs=[books_msg, books_chatbot],
949
+ outputs=[books_chatbot, books_msg]
950
+ )
951
+ books_msg.submit(
952
+ fn=submit_books_text,
953
+ inputs=[books_msg, books_chatbot],
954
+ outputs=[books_chatbot, books_msg]
955
+ )
956
+
957
+ with gr.Column(elem_classes=["voice-box"]):
958
+ gr.Markdown("**🎀 Voice Input / πŸ”Š Audio Reply**")
959
+ with gr.Row():
960
+ books_audio_in = gr.Audio(sources=["microphone"], type="filepath", label="Record Voice")
961
+ books_audio_out = gr.Audio(label="Assistant Voice Reply", autoplay=True)
962
+ with gr.Row():
963
+ books_voice_btn = gr.Button("πŸŽ™οΈ Ask by Voice", variant="secondary")
964
+ books_voice_status = gr.Textbox(label="Voice Status", interactive=False,
965
+ elem_classes=["status-box"])
966
+ books_voice_text = gr.Textbox(label="Transcribed Text", interactive=False)
967
+
968
+ books_voice_btn.click(
969
+ fn=process_voice_for_books,
970
+ inputs=[books_audio_in, books_chatbot],
971
+ outputs=[books_chatbot, books_voice_status, books_audio_out, books_voice_text]
972
+ )
973
+
974
+ gr.HTML("<hr>")
975
+ gr.HTML("""
976
+ <div style="margin-bottom:14px;">
977
+ <div class="section-label">Scraped Data</div>
978
+ <div class="section-title">Goodreads Top 20</div>
979
+ </div>
980
+ """)
981
+
982
+ books_display = gr.Markdown(value=show_books_table(books_data, scrape_status))
983
+ refresh_btn = gr.Button("πŸ”„ Re-Scrape Goodreads", variant="secondary")
984
+
985
+
986
+ def refresh_data():
987
+ global books_data, scrape_status
988
+ books_data, scrape_status = get_books()
989
+ return show_books_table(books_data, scrape_status)
990
+
991
+
992
+ refresh_btn.click(fn=refresh_data, outputs=books_display)
993
+
994
+ # ============================================================
995
+ # TAB 3: YOUTUBE
996
+ # ============================================================
997
+ with gr.Tab("🎬 YouTube"):
998
+ gr.HTML("""
999
+ <div style="margin-bottom:18px;">
1000
+ <div class="section-label">YouTube Transcript</div>
1001
+ <div class="section-title">Video Q&A Assistant</div>
1002
+ <div class="section-desc">
1003
+ Load any YouTube video's transcript, then ask questions about it.
1004
+ </div>
1005
+ </div>
1006
+ """)
1007
+
1008
+ gr.HTML("""
1009
+ <div style="background:rgba(255,184,0,0.06);border:1px solid rgba(255,184,0,0.2);
1010
+ border-left:3px solid #ffb800;border-radius:10px;
1011
+ padding:12px 18px;margin-bottom:16px;
1012
+ font-size:0.85rem;color:#c89a00;font-family:Space Grotesk,sans-serif;">
1013
+ <strong>Step 1</strong> β€” Paste a YouTube Video ID or full URL and click <em>Load Transcript</em>
1014
+ </div>
1015
+ """)
1016
+
1017
+ with gr.Row():
1018
+ video_id_input = gr.Textbox(
1019
+ placeholder="e.g. dQw4w9WgXcQ or https://youtu.be/…",
1020
+ label="YouTube Video ID or URL",
1021
+ scale=4,
1022
+ )
1023
+ load_btn = gr.Button("πŸ“₯ Load Transcript", variant="primary", scale=1)
1024
+
1025
+ with gr.Row():
1026
+ transcript_status = gr.Textbox(
1027
+ label="Status",
1028
+ interactive=False,
1029
+ elem_classes=["status-box"],
1030
+ )
1031
+
1032
+ transcript_preview = gr.Markdown(
1033
+ value="*Transcript preview will appear here…*",
1034
+ elem_classes=["transcript-preview"],
1035
+ )
1036
+ load_btn.click(
1037
+ fn=load_transcript,
1038
+ inputs=[video_id_input],
1039
+ outputs=[transcript_status, transcript_preview],
1040
+ )
1041
+
1042
+ gr.HTML("""
1043
+ <div style="background:rgba(0,212,255,0.05);border:1px solid rgba(0,212,255,0.15);
1044
+ border-left:3px solid #00d4ff;border-radius:10px;
1045
+ padding:12px 18px;margin:16px 0;
1046
+ font-size:0.85rem;color:#0099bb;font-family:Space Grotesk,sans-serif;">
1047
+ <strong>Step 2</strong> β€” Ask anything about the loaded video below
1048
+ </div>
1049
+ """)
1050
+
1051
+ youtube_chatbot = gr.Chatbot(
1052
+ height=360,
1053
+ placeholder="<div style='text-align:center;color:#4a5a7a;padding:40px;font-family:Space Grotesk,sans-serif;'>πŸ“₯ Load a transcript above, then ask anything!</div>",
1054
+ show_label=False
1055
+
1056
+ )
1057
+
1058
+ with gr.Row():
1059
+ youtube_msg = gr.Textbox(
1060
+ placeholder="e.g. What is the main topic?",
1061
+ scale=7,
1062
+ show_label=False,
1063
+ container=False,
1064
+ )
1065
+ youtube_send = gr.Button("Ask ↑", variant="primary", scale=1)
1066
+
1067
+
1068
+ def submit_youtube_text(message, history):
1069
+ history = list(history) if history else []
1070
+ if not message.strip():
1071
+ return history, ""
1072
+ reply = ask_youtube_ai(message, history)
1073
+ history.append({"role": "user", "content": message})
1074
+ history.append({"role": "assistant", "content": reply})
1075
+ return history, ""
1076
+
1077
+
1078
+ youtube_send.click(
1079
+ fn=submit_youtube_text,
1080
+ inputs=[youtube_msg, youtube_chatbot],
1081
+ outputs=[youtube_chatbot, youtube_msg]
1082
+ )
1083
+ youtube_msg.submit(
1084
+ fn=submit_youtube_text,
1085
+ inputs=[youtube_msg, youtube_chatbot],
1086
+ outputs=[youtube_chatbot, youtube_msg]
1087
+ )
1088
+
1089
+ with gr.Column(elem_classes=["voice-box"]):
1090
+ gr.Markdown("**🎀 Voice Input / πŸ”Š Audio Reply**")
1091
+ with gr.Row():
1092
+ youtube_audio_in = gr.Audio(sources=["microphone"], type="filepath", label="Record Voice")
1093
+ youtube_audio_out = gr.Audio(label="Assistant Voice Reply", autoplay=True)
1094
+ with gr.Row():
1095
+ youtube_voice_btn = gr.Button("πŸŽ™οΈ Ask by Voice", variant="secondary")
1096
+ youtube_voice_status = gr.Textbox(label="Voice Status", interactive=False,
1097
+ elem_classes=["status-box"])
1098
+ youtube_voice_text = gr.Textbox(label="Transcribed Text", interactive=False)
1099
+
1100
+ youtube_voice_btn.click(
1101
+ fn=process_voice_for_youtube,
1102
+ inputs=[youtube_audio_in, youtube_chatbot],
1103
+ outputs=[youtube_chatbot, youtube_voice_status, youtube_audio_out, youtube_voice_text]
1104
+ )
1105
+
1106
+ # ============================================================
1107
+ # TAB 4: PREFERENCES
1108
+ # ============================================================
1109
+ with gr.Tab("βš™οΈ Preferences"):
1110
+ gr.HTML("""
1111
+ <div style="margin-bottom:20px;">
1112
+ <div class="section-label">Personalization</div>
1113
+ <div class="section-title">AI Behavior Settings</div>
1114
+ <div class="section-desc">
1115
+ These preferences are saved permanently and injected into every AI response across all tabs.
1116
+ </div>
1117
+ </div>
1118
+ """)
1119
+
1120
+ with gr.Row():
1121
+ with gr.Column():
1122
+ tone_input = gr.Dropdown(
1123
+ choices=["friendly", "formal", "casual", "professional", "humorous"],
1124
+ value=prefs.get("tone", "friendly"),
1125
+ label="🎭 Tone",
1126
+ info="How the AI speaks to you",
1127
+ )
1128
+ lang_input = gr.Dropdown(
1129
+ choices=["English", "Urdu", "Roman Urdu", "Arabic", "French", "Spanish"],
1130
+ value=prefs.get("language", "English"),
1131
+ label="🌐 Response Language",
1132
+ info="Language for all AI responses",
1133
+ )
1134
+
1135
+ with gr.Column():
1136
+ fmt_input = gr.Dropdown(
1137
+ choices=["concise", "detailed", "bullet points", "numbered list", "paragraph"],
1138
+ value=prefs.get("format", "concise"),
1139
+ label="πŸ“ Response Format",
1140
+ info="How responses should be structured",
1141
+ )
1142
+ custom_input = gr.Textbox(
1143
+ value=prefs.get("custom_rules", ""),
1144
+ label="✏️ Custom Rules",
1145
+ placeholder='e.g. "Always cite sources" or "Use emojis sparingly"',
1146
+ lines=3,
1147
+ info="Extra instructions for the AI to always follow",
1148
+ )
1149
+
1150
+ with gr.Row():
1151
+ save_btn = gr.Button("πŸ’Ύ Save Preferences", variant="primary", scale=1)
1152
+ pref_status = gr.Textbox(
1153
+ label="Status",
1154
+ interactive=False,
1155
+ scale=2,
1156
+ elem_classes=["status-box"],
1157
+ )
1158
+
1159
+ save_btn.click(
1160
+ fn=update_preferences,
1161
+ inputs=[tone_input, lang_input, fmt_input, custom_input],
1162
+ outputs=pref_status,
1163
+ )
1164
+
1165
+
1166
+ def show_current_prefs():
1167
+ p = load_preferences()
1168
+ return f"""```
1169
+ Tone β†’ {p.get('tone')}
1170
+ Language β†’ {p.get('language')}
1171
+ Format β†’ {p.get('format')}
1172
+ Custom Rules β†’ {p.get('custom_rules') or 'None'}
1173
+ ```"""
1174
+
1175
+
1176
+ pref_preview = gr.Markdown(value=show_current_prefs())
1177
+ save_btn.click(
1178
+ fn=lambda t, l, f, c: show_current_prefs(),
1179
+ inputs=[tone_input, lang_input, fmt_input, custom_input],
1180
+ outputs=pref_preview,
1181
+ )
1182
+
1183
+ # ============================================================
1184
+ # TAB 5: ABOUT
1185
+ # ============================================================
1186
+ with gr.Tab("ℹ️ About"):
1187
+ gr.HTML(f"""
1188
+ <div style="max-width:680px;margin:0 auto;padding:10px 0 24px;">
1189
+ <div style="margin-bottom:24px;">
1190
+ <div class="section-label">Project Info</div>
1191
+ <div class="section-title">NeuralChat β€” Version 5</div>
1192
+ <div class="section-desc">
1193
+ A multi-tab AI assistant with web scraping, YouTube transcript Q&A,
1194
+ persistent memory, user preference personalization, voice input, audio output, and on-demand image generation.
1195
+ </div>
1196
+ </div>
1197
+ <div style="background:var(--bg-card,#141d35);border:1px solid #1e2d50;
1198
+ border-radius:14px;overflow:hidden;margin-bottom:20px;">
1199
+ <table style="width:100%;border-collapse:collapse;">
1200
+ <thead>
1201
+ <tr style="background:rgba(0,212,255,0.07);">
1202
+ <th style="padding:11px 16px;text-align:left;font-size:0.72rem;letter-spacing:1px;text-transform:uppercase;color:#00d4ff;border-bottom:1px solid #1e2d50;">Layer</th>
1203
+ <th style="padding:11px 16px;text-align:left;font-size:0.72rem;letter-spacing:1px;text-transform:uppercase;color:#00d4ff;border-bottom:1px solid #1e2d50;">Detail</th>
1204
+ </tr>
1205
+ </thead>
1206
+ <tbody style="color:#e8edf8;font-size:0.875rem;">
1207
+ <tr style="border-bottom:1px solid #1e2d50;"><td style="padding:10px 16px;color:#8899bb;">🌐 Scraping</td><td style="padding:10px 16px;">Bright Data Web Unlocker + BeautifulSoup</td></tr>
1208
+ <tr style="border-bottom:1px solid #1e2d50;"><td style="padding:10px 16px;color:#8899bb;">🎬 YouTube</td><td style="padding:10px 16px;">youtube-transcript-api</td></tr>
1209
+ <tr style="border-bottom:1px solid #1e2d50;"><td style="padding:10px 16px;color:#8899bb;">🎀 STT</td><td style="padding:10px 16px;">Groq Whisper Large v3 Turbo</td></tr>
1210
+ <tr style="border-bottom:1px solid #1e2d50;"><td style="padding:10px 16px;color:#8899bb;">πŸ”Š TTS</td><td style="padding:10px 16px;">OpenAI tts-1 (alloy)</td></tr>
1211
+ <tr style="border-bottom:1px solid #1e2d50;"><td style="padding:10px 16px;color:#8899bb;">πŸ€– AI (Main)</td><td style="padding:10px 16px;">Groq β€” llama-3.3-70b-versatile</td></tr>
1212
+ <tr style="border-bottom:1px solid #1e2d50;"><td style="padding:10px 16px;color:#8899bb;">πŸ’Ύ Memory</td><td style="padding:10px 16px;">JSON persistent storage</td></tr>
1213
+ <tr><td style="padding:10px 16px;color:#8899bb;">βš™οΈ Prefs</td><td style="padding:10px 16px;">JSON persistent storage</td></tr>
1214
+ </tbody>
1215
+ </table>
1216
+ </div>
1217
+ <div style="text-align:center;padding-top:10px;font-size:0.78rem;color:#4a5a7a;font-family:JetBrains Mono,monospace;letter-spacing:0.5px;border-top:1px solid #1e2d50;">
1218
+ Assignment 01 β€” Ver 5 &nbsp;|&nbsp; Dept. of Data Science, University of Punjab<br>
1219
+ Instructor: Dr. Muhammad Arif Butt
1220
+ </div>
1221
+ </div>
1222
+ """)
1223
+
1224
+ gr.HTML("""
1225
+ <div class="footer-strip">
1226
+ NeuralChat v5 &nbsp;Β·&nbsp; Groq + OpenAI + Gradio &nbsp;Β·&nbsp; Built for DSAI @ UOP
1227
+ </div>
1228
+ """)
1229
+
1230
+ demo.launch(
1231
+ share=False,
1232
+ inbrowser=True,
1233
+ css=CUSTOM_CSS,
1234
+ )