Spaces:
Sleeping
Sleeping
| import pymongo | |
| import certifi | |
| import PyPDF2 # PDF படிக்க | |
| import re # Suggestions பிரிக்க | |
| import os | |
| import uuid | |
| import time | |
| import json | |
| import base64 | |
| import io | |
| import warnings | |
| from PIL import Image | |
| from flask import Flask, request, jsonify, render_template_string, Response | |
| import google.generativeai as genai | |
| # 👇 புதிய வரிகள் (Hugging Face Library) | |
| from huggingface_hub import snapshot_download | |
| # 👇 இங்கே உங்க Dataset பெயரை சரியா போடுங்க (எ.கா: "username/dataset-name") | |
| dataset_id = "Shirpi/Education_library" | |
| # 👇 லோக்கல் ஃபோல்டர் பெயர் (இதை 'pdfs' என மாற்றுகிறோம்) | |
| data_folder = "books" | |
| # 👇 இதுதான் அந்த மேஜிக்! Dataset-ஐ டவுன்லோட் செய்யும் | |
| if not os.path.exists(data_folder): | |
| print(f"Downloading files from {dataset_id}...") | |
| try: | |
| snapshot_download( | |
| repo_id=dataset_id, | |
| repo_type="dataset", | |
| local_dir=data_folder, | |
| local_dir_use_symlinks=False | |
| ) | |
| print("✅ Download Completed!") | |
| except Exception as e: | |
| print(f"❌ Download Error: {e}") | |
| # --- FIX: IGNORE DEPRECATION WARNINGS --- | |
| warnings.filterwarnings("ignore") | |
| current_key_index = 0 | |
| app = Flask(__name__) | |
| # ========================================== | |
| # 👇 API KEYS SETUP 👇 | |
| # ========================================== | |
| keys_string = os.environ.get("API_KEYS", "") | |
| API_KEYS = [k.strip() for k in keys_string.replace(',', ' ').replace('\n', ' ').split() if k.strip()] | |
| # --- 💾 MONGODB DATABASE SETUP --- | |
| # (மேலே import pymongo, import certifi மறக்காம சேருங்க) | |
| # ரகசியப் பெட்டியில் (Secrets) இருந்து லின்க்கை எடு | |
| MONGO_URI = os.environ.get("MONGO_URI") | |
| # MongoDB Connection | |
| try: | |
| client = pymongo.MongoClient(MONGO_URI, tlsCAFile=certifi.where()) | |
| db = client["StudentAI_DB"] | |
| collection = db["chat_history"] | |
| print("✅ MongoDB Connected Successfully!") | |
| except Exception as e: | |
| print(f"❌ MongoDB Connection Error: {e}") | |
| def load_db(): | |
| try: | |
| data = collection.find_one({"_id": "global_store"}) | |
| if data: | |
| return data.get("data", {}) | |
| return {} | |
| except Exception as e: | |
| print(f"⚠️ DB Load Error: {e}") | |
| return {} | |
| def save_db(db_data): | |
| try: | |
| collection.update_one( | |
| {"_id": "global_store"}, | |
| {"$set": {"data": db_data}}, | |
| upsert=True | |
| ) | |
| except Exception as e: | |
| print(f"⚠️ DB Save Error: {e}") | |
| user_db = load_db() | |
| # 👇 REPLACED System Instruction (With Citation Rule) 👇 | |
| def get_system_instruction(medium="English"): | |
| base_instruction = """ | |
| ROLE: You are "Student's AI", a professional academic tutor. | |
| RULES: | |
| 1. **SOURCE:** Answer ONLY based on the provided 'Context Book'. | |
| 2. **FORMAT:** Use Markdown. Bold key terms. | |
| 3. **CITATION (CRITICAL):** - You MUST cite the page number exactly as it appears in the `[[PAGE X START]]` marker. | |
| - Format: `📖 **Source:** Page X` | |
| - **DO NOT GUESS the page number.** Look for the marker surrounding the text you used. | |
| - If you combine info from multiple pages, list them all. | |
| 4. **MATH:** Use LaTeX for formulas ($$ ... $$). | |
| 5. **SUGGESTIONS:** End with 2 follow-up questions: `<<SUGGEST: Q1 | Q2>>` | |
| # 👇 இதை RULES-ல் சேர்க்கவும் 👇 | |
| 6. **TABLES:** Use Markdown tables for comparisons or structured data. | |
| - Example: | |
| | Property | Value | | |
| |----------|-------| | |
| | Mass | 5kg | | |
| 7. **CHEMISTRY:** Use \ce{...} for formulas inside LaTeX. Example: $\ce{H2SO4}$. | |
| ⚠️ MANDATORY STRUCTURE (AUTO-QUIZ MODE): | |
| Every response must strictly follow this 3-part structure. Do not wait for the user to ask. | |
| - **Provide a clear answer based ONLY on the book.** | |
| - **End this part with the Page Citation.** | |
| - Generate Multiple Choice Questions (MCQ) based **STRICTLY** on Part 1. | |
| - **Count Rule:** Short answer = 2 Questions; Long answer = 5 Questions. | |
| - **STRICT CONSTRAINT:** **DO NOT REVEAL THE ANSWERS.** Just provide Options (A, B, C, D). Let the student think. | |
| - **SUGGESTIONS:** End with 2 follow-up questions: `<<SUGGEST: Q1 | Q2>>` | |
| """ | |
| if medium == "Tamil": | |
| base_instruction += """ | |
| 8. **LANGUAGE:** Tamil Medium selected. | |
| - Reply in **TAMIL SCRIPT (தமிழ்)**. | |
| - Cite the page number in English (e.g., `📖 **ஆதாரம்:** பக்கம் 12`). | |
| 9. **QUIZ MODE:** If user asks for "Quiz" or "Test", generate 5 Multiple Choice Questions (MCQ) based on the context. | |
| - Format: Question, Options (A,B,C,D). | |
| - Do NOT reveal answers immediately. Wait for user to reply. | |
| """ | |
| else: | |
| base_instruction += "\n6. **LANGUAGE:** English by default." | |
| return base_instruction | |
| # --- 🧬 MODEL & FILE HANDLING --- | |
| def get_working_model(key): | |
| try: | |
| genai.configure(api_key=key) | |
| models = list(genai.list_models()) | |
| chat_models = [m for m in models if 'generateContent' in m.supported_generation_methods] | |
| for m in chat_models: | |
| if "flash" in m.name.lower() and "1.5" in m.name: return m.name | |
| for m in chat_models: | |
| if "pro" in m.name.lower() and "1.5" in m.name: return m.name | |
| if chat_models: return chat_models[0].name | |
| except: return None | |
| return None | |
| # 👇 REPLACED get_book_text (Hybrid: Prints Real Page No if found, else PDF Index) 👇 | |
| def get_book_text(user_details): | |
| try: | |
| # 1. Path Construction | |
| base_path = "books/books" if os.path.exists("books/books") else "books" | |
| if user_details.get("type") == "school": | |
| std = user_details.get("standard", "").lower() | |
| sub = user_details.get("subject", "").lower() | |
| path = os.path.join(base_path, "school", std, f"{sub}.pdf") | |
| else: | |
| dept = user_details.get("dept", "").lower() | |
| sub = user_details.get("subject", "").lower() | |
| path = os.path.join(base_path, "college", dept, f"{sub}.pdf") | |
| print(f"🔍 Searching: {path}") | |
| if os.path.exists(path): | |
| text = "" | |
| with open(path, 'rb') as f: | |
| reader = PyPDF2.PdfReader(f) | |
| for i, page in enumerate(reader.pages[:50]): # Speed Limit | |
| content = page.extract_text() | |
| if content: | |
| lines = content.strip().split('\n') | |
| # Default: PDF வரிசை எண் (எ.கா: PDF-5) | |
| page_label = f"PDF-{i+1}" | |
| # 👇 SMART CHECK: கீழே ஒரிஜினல் நம்பர் இருக்கான்னு பார்க்கிறோம் 👇 | |
| if lines: | |
| last_line = lines[-1].strip() | |
| # கடைசி வரி நம்பராக இருந்தால் (எ.கா: "5" or "12") | |
| # அதுவும் 4 இலக்கத்திற்கு குறைவாக இருந்தால் (வருஷம் 2024 வராமல் இருக்க) | |
| if last_line.isdigit() and len(last_line) < 4: | |
| page_label = f"Page {last_line}" | |
| # Marker சேர்க்கிறோம் | |
| text += f"\n\n--- [[{page_label} START]] ---\n{content}\n--- [[{page_label} END]] ---\n" | |
| return text | |
| else: | |
| return None | |
| except Exception as e: | |
| print(f"❌ Error reading PDF: {e}") | |
| return None | |
| # 👇 REPLACED generate_with_retry FUNCTION 👇 | |
| def generate_with_retry(prompt, image_data=None, file_text=None, history_messages=[], system_instruction=None): | |
| global current_key_index | |
| if not API_KEYS: return "🚨 API Keys Missing." | |
| formatted_history = [] | |
| for m in history_messages[-6:]: | |
| role = "user" if m["role"] == "user" else "model" | |
| formatted_history.append({"role": role, "parts": [m["content"]]}) | |
| current_parts = [] | |
| if file_text: current_parts.append(f"analyzing file:\n{file_text}\n\n") | |
| current_parts.append(prompt) | |
| if image_data: | |
| img = process_image(image_data) | |
| if img: current_parts.append(img) | |
| for i in range(len(API_KEYS)): | |
| key = API_KEYS[current_key_index] | |
| model_name = get_working_model(key) | |
| if not model_name: | |
| current_key_index = (current_key_index + 1) % len(API_KEYS) | |
| continue | |
| try: | |
| genai.configure(api_key=key) | |
| # 👇 இங்கே தான் மாற்றம்: system_instruction வருகிறதா என பார்க்கிறோம் | |
| final_instruction = system_instruction if system_instruction else "You are a helpful tutor." | |
| model = genai.GenerativeModel(model_name=model_name, system_instruction=final_instruction) | |
| if image_data or file_text: | |
| response = model.generate_content(current_parts) | |
| else: | |
| chat = model.start_chat(history=formatted_history) | |
| response = chat.send_message(prompt) | |
| return response.text | |
| except Exception as e: | |
| current_key_index = (current_key_index + 1) % len(API_KEYS) | |
| time.sleep(1) | |
| return "⚠️ System Busy. Please try again." | |
| # --- UI TEMPLATE (UPDATED: PROFESSIONAL UI V2) --- | |
| HTML_TEMPLATE = """ | |
| <!DOCTYPE html> | |
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, interactive-widget=resizes-content"> | |
| <meta name="mobile-web-app-capable" content="yes"> | |
| <meta name="apple-mobile-web-app-capable" content="yes"> | |
| <meta name="theme-color" content="#09090b"> | |
| <link rel="manifest" href="/manifest.json"> | |
| <title>Student's AI</title> | |
| <link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css" rel="stylesheet"> | |
| <link href="https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet"> | |
| <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css"> | |
| <script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js"></script> | |
| <script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script> | |
| <script> | |
| window.MathJax = { | |
| loader: { load: ['[tex]/mhchem'] }, | |
| tex: { | |
| inlineMath: [['$', '$'], ['\\(', '\\)']], | |
| displayMath: [['$$', '$$'], ['\\[', '\\]']], | |
| packages: {'[+]': ['mhchem']} | |
| }, | |
| svg: { fontCache: 'global' } | |
| }; | |
| </script> | |
| <script src="https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-mml-chtml.js"></script> | |
| <script type="module"> | |
| import mermaid from 'https://cdn.jsdelivr.net/npm/mermaid@10/dist/mermaid.esm.min.mjs'; | |
| mermaid.initialize({ startOnLoad: false, theme: 'dark', securityLevel: 'loose' }); | |
| window.mermaid = mermaid; | |
| </script> | |
| <style> | |
| /* --- VARIABLES & THEMES --- */ | |
| :root { | |
| --bg: #09090b; | |
| --card: #18181b; | |
| --user-msg: #27272a; | |
| --text: #e4e4e7; | |
| --text-muted: #a1a1aa; | |
| --accent: #fff; | |
| --border: #27272a; | |
| --hover: #27272a; | |
| } | |
| /* LIGHT MODE OVERRIDES */ | |
| body.light-mode { | |
| --bg: #ffffff; | |
| --card: #f4f4f5; | |
| --user-msg: #e4e4e7; | |
| --text: #09090b; | |
| --text-muted: #52525b; | |
| --accent: #000; | |
| --border: #e4e4e7; | |
| --hover: #f4f4f5; | |
| } | |
| * { box-sizing: border-box; -webkit-tap-highlight-color: transparent; } | |
| body, html { | |
| margin: 0; padding: 0; height: 100dvh; width: 100%; max-width: 100%; | |
| background: var(--bg); color: var(--text); font-family: 'Outfit', sans-serif; | |
| overflow: hidden; font-size: 16px; | |
| transition: background 0.3s ease, color 0.3s ease; | |
| } | |
| /* --- APP STRUCTURE --- */ | |
| #app-container { | |
| display: flex; flex-direction: column; height: 100dvh; width: 100%; | |
| position: relative; overflow-x: hidden; padding-top: 60px; | |
| } | |
| /* --- HEADER --- */ | |
| header { | |
| height: 60px; padding: 0 15px; | |
| background: var(--bg); | |
| border-bottom: 1px solid var(--border); | |
| display: flex; align-items: center; justify-content: space-between; | |
| z-index: 50; padding-top: env(safe-area-inset-top); | |
| position: absolute; top: 0; left: 0; right: 0; | |
| transition: background 0.3s ease; | |
| } | |
| .menu-btn { width: 40px; height: 40px; border-radius: 50%; display: flex; align-items: center; justify-content: center; cursor: pointer; color: var(--text); transition: background 0.2s; } | |
| .menu-btn:active { background: var(--hover); } | |
| .app-title { font-size: 20px; font-weight: 700; color: var(--text); letter-spacing: -0.5px; } | |
| /* --- FONT FIX: Allows Icons to Show --- */ | |
| body, html, input, textarea, button, select { | |
| font-family: 'Outfit', sans-serif; | |
| } | |
| /* ஐகான்களைத் தொடாதே! */ | |
| i, .fas, .fab, .far { | |
| font-family: "Font Awesome 6 Free" !important; | |
| font-weight: 900; | |
| } | |
| /* --- SIDEBAR SMOOTH ANIMATION FIX --- */ | |
| /* 1. Container: பின்னணி ஃபேட் ஆவதற்கு (Fade Effect) */ | |
| #sidebar { | |
| position: fixed; top: 0; left: 0; width: 100%; height: 100%; | |
| z-index: 3000; display: flex; | |
| visibility: hidden; /* ஆரம்பத்தில் மறைந்திருக்கும் */ | |
| background-color: rgba(0,0,0,0); /* வெளிப்படையானது */ | |
| transition: visibility 0.4s, background-color 0.4s ease; | |
| transform: none !important; /* 👇 இதுதான் முக்கியம்! பழைய transform-ஐ நீக்குகிறது */ | |
| } | |
| /* 2. Open State: பின்னணி கருப்பாவதற்கு */ | |
| #sidebar.open { | |
| visibility: visible; | |
| background-color: rgba(0,0,0,0.5); | |
| } | |
| /* 3. Content: மெனு ஸ்லைடு ஆவதற்கு (Slide Effect) */ | |
| .sidebar-content { | |
| width: 280px; height: 100%; background: var(--bg); | |
| border-right: 1px solid var(--border); | |
| display: flex; flex-direction: column; padding: 20px; | |
| transform: translateX(-100%); /* இடது பக்கம் மறைந்திருக்கும் */ | |
| transition: transform 0.4s cubic-bezier(0.25, 0.8, 0.25, 1); /* Buttery Smooth Magic */ | |
| } | |
| /* 4. Content Open: வெளியே வருவதற்கு */ | |
| #sidebar.open .sidebar-content { | |
| transform: translateX(0); | |
| } | |
| /* --- SIDEBAR ICONS & FONT FIX --- */ | |
| .sidebar-content i { | |
| width: 20px; | |
| text-align: center; | |
| margin-right: 10px; | |
| font-size: 16px; | |
| } | |
| #hist-search { | |
| font-family: 'Outfit', sans-serif !important; | |
| } | |
| /* வலது பக்கம் இருக்கும் காலி இடம் (Gap) */ | |
| .sidebar-overlay-gap { flex: 1; cursor: pointer; } | |
| .sidebar-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px; padding: 5px 5px 5px 0;;} | |
| .user-info-text { font-size: 18px; font-weight: 700; color: var(--text); } | |
| /* 👇 PROFESSIONAL NEW CHAT BUTTON 👇 */ | |
| .new-chat-btn { | |
| width: 100%; | |
| padding: 14px 20px; | |
| background: var(--text); /* தீம்க்கு ஏத்த மாதிரி மாறும் */ | |
| color: var(--bg); /* எழுத்து கலர் மாறும் */ | |
| border: none; | |
| border-radius: 12px; /* அழகான வளைவுகள் */ | |
| font-size: 15px; | |
| font-weight: 600; | |
| letter-spacing: 0.5px; | |
| cursor: pointer; | |
| margin-bottom: 25px; | |
| /* 👇 Flexbox: ஐகானும் எழுத்தும் நேர்கோட்டில் வர */ | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| gap: 10px; /* ஐகானுக்கும் எழுத்துக்கும் இடைவெளி */ | |
| /* 👇 நிழல் மற்றும் அனிமேஷன் (Premium Feel) */ | |
| box-shadow: 0 4px 15px rgba(0,0,0,0.1); | |
| transition: all 0.3s cubic-bezier(0.25, 0.8, 0.25, 1); | |
| position: relative; | |
| overflow: hidden; | |
| } | |
| /* Hover Effect: மவுஸ் வைத்தால் மேலே எழும் */ | |
| .new-chat-btn:hover { | |
| transform: translateY(-2px); | |
| box-shadow: 0 8px 20px rgba(0,0,0,0.2); | |
| opacity: 0.95; | |
| } | |
| /* Click Effect: அமுக்கினால் உள்ளே போகும் */ | |
| .new-chat-btn:active { | |
| transform: scale(0.98); | |
| } | |
| .history-label { color: var(--text-muted); font-size: 12px; font-weight: 600; margin-bottom: 10px; text-transform: uppercase; letter-spacing: 1px; } | |
| #history-list { flex: 1; overflow-y: auto; padding-right: 5px; } | |
| .history-item { | |
| padding: 12px; margin-bottom: 8px; background: transparent; | |
| border-radius: 8px; cursor: pointer; color: var(--text); | |
| display: flex; justify-content: space-between; align-items: center; | |
| font-size: 14px; transition: background 0.2s; | |
| } | |
| .history-item:hover { background: var(--card); } | |
| /* 👇 Buttons-ஐ மறைத்து வைக்கிறோம் */ | |
| .history-actions { | |
| display: none; /* Default-ஆக தெரியாது */ | |
| gap: 15px; | |
| position: absolute; | |
| right: 10px; | |
| background: var(--card); | |
| padding: 5px 10px; | |
| border-radius: 8px; | |
| box-shadow: -5px 0 15px rgba(0,0,0,0.5); | |
| z-index: 10; | |
| } | |
| /* 👇 Long Press பண்ணும்போது இந்த கிளாஸ் வரும் */ | |
| .history-item.active-options .history-actions { | |
| display: flex; /* அப்போ மட்டும் தெரியும் */ | |
| } | |
| /* 👇 History Item டிசைன் */ | |
| .history-item { | |
| position: relative; /* இது முக்கியம் */ | |
| padding: 15px; | |
| margin-bottom: 8px; | |
| border-radius: 8px; | |
| cursor: pointer; | |
| color: var(--text); | |
| display: flex; | |
| align-items: center; | |
| font-size: 14px; | |
| transition: background 0.2s; | |
| user-select: none; /* Text select ஆகாம இருக்க */ | |
| -webkit-user-select: none; | |
| } | |
| .history-item:hover .history-actions { opacity: 1; } | |
| .hist-icon { color: var(--text-muted); font-size: 12px; padding: 4px; } | |
| .hist-icon:hover { color: var(--text); } | |
| .sidebar-footer { margin-top: auto; border-top: 1px solid var(--border); padding-top: 15px; } | |
| .footer-link { display: flex; align-items: center; gap: 10px; padding: 12px; color: var(--text); cursor: pointer; border-radius: 8px; font-weight: 500; } | |
| .footer-link:hover { background: var(--card); } | |
| /* --- CHAT AREA --- */ | |
| #chat-box { | |
| flex: 1; overflow-y: auto; | |
| padding: 20px 15px; | |
| padding-bottom: 40px; /* Reduced from 100px to 40px */ | |
| display: flex; flex-direction: column; gap: 20px; | |
| scroll-behavior: smooth; | |
| } | |
| .msg { width: 100%; display: flex; flex-direction: column; opacity: 0; animation: fadeIn 0.4s forwards; } | |
| @keyframes fadeIn { to { opacity: 1; } } | |
| .user-msg { align-items: flex-end; } | |
| /* --- FIXED: USER BUBBLE PROFESSIONAL STYLE --- */ | |
| /* AI Message Formatting */ | |
| .ai-content { | |
| width: 100%; | |
| max-width: 100%; | |
| font-size: 17px; | |
| line-height: 1.8; | |
| } | |
| .ai-content strong { color: var(--text); font-weight: 700; } | |
| /* Chat Actions (Icons below message) */ | |
| .msg-actions { | |
| display: flex; gap: 15px; margin-top: 8px; | |
| opacity: 1; /* 👇 FIX: Always visible (removed hover) */ | |
| font-size: 13px; padding: 0 5px; color: var(--text-muted); | |
| transition: color 0.2s; | |
| } | |
| .msg:hover .msg-actions { opacity: 1; } | |
| .action-icon { | |
| cursor: pointer; display: flex; align-items: center; gap: 5px; | |
| } | |
| .action-icon:hover { color: var(--text); } | |
| /* Code Blocks */ | |
| pre { background: #1e1e1e !important; border-radius: 12px; padding: 15px; overflow-x: auto; margin: 15px 0; border: 1px solid #333; } | |
| code { font-family: 'JetBrains Mono', monospace; font-size: 14px; } | |
| /* Input Area */ | |
| .input-wrapper { | |
| background: var(--bg); padding: 10px 15px; border-top: 1px solid var(--border); | |
| flex-shrink: 0; z-index: 60; padding-bottom: max(15px, env(safe-area-inset-bottom)); | |
| } | |
| textarea { | |
| flex: 1; background: transparent; border: none; color: var(--text); | |
| font-size: 16px; max-height: 120px; padding: 12px 0; resize: none; outline: none; | |
| font-family: 'Outfit', sans-serif; | |
| } | |
| #settings-overlay { | |
| position: fixed; top: 0; left: 0; width: 100%; height: 100%; | |
| background: var(--bg); z-index: 2000; | |
| display: flex; flex-direction: column; | |
| transform: translateX(100%); | |
| transition: transform 0.3s ease; | |
| /* 👇 இந்த இரண்டு வரிகள் தான் பிரச்சனையைத் தீர்க்கும் */ | |
| overflow-x: hidden !important; /* வலது பக்கம் ஸ்க்ரோல் ஆவதைத் தடுக்கும் */ | |
| overscroll-behavior: none; /* பக்கம் ரப்பர் மாதிரி இழுபடுவதைத் தடுக்கும் */ | |
| transform: translateX(100%); | |
| transition: transform 0.4s cubic-bezier(0.25, 0.8, 0.25, 1); | |
| } | |
| #settings-overlay.active { transform: translateX(0); } | |
| .settings-header { | |
| padding: 20px; padding-top: calc(20px + env(safe-area-inset-top)); | |
| display: flex; align-items: center; gap: 15px; | |
| } | |
| .back-btn { font-size: 20px; color: var(--text); cursor: pointer; padding: 5px; } | |
| .settings-search { | |
| width: 90%; max-width: 600px; margin: 0 auto 20px auto; | |
| background: var(--card); border: 1px solid var(--border); | |
| padding: 12px 20px; border-radius: 12px; color: var(--text); | |
| display: flex; align-items: center; gap: 10px; | |
| } | |
| .settings-search input { background: transparent; border: none; color: var(--text); width: 100%; outline: none; font-size: 16px; } | |
| .settings-content { | |
| width: 100%; max-width: 600px; margin: 0 auto; padding: 0 20px 40px 20px; | |
| } | |
| .section-title { color: var(--text-muted); font-size: 13px; font-weight: 600; margin: 20px 0 10px 0; text-transform: uppercase; } | |
| /* Profile Section */ | |
| .profile-card { | |
| background: var(--card); border-radius: 16px; padding: 20px; | |
| display: flex; flex-direction: column; align-items: center; margin-bottom: 30px; | |
| border: 1px solid var(--border); | |
| } | |
| .profile-pic-wrapper { | |
| position: relative; width: 100px; height: 100px; margin-bottom: 20px; | |
| } | |
| .profile-pic { | |
| width: 100%; height: 100%; border-radius: 50%; object-fit: cover; | |
| border: 2px solid var(--border); background: #111; | |
| } | |
| .edit-pic-btn { | |
| position: absolute; bottom: 0; right: 0; background: var(--text); color: var(--bg); | |
| width: 30px; height: 30px; border-radius: 50%; display: flex; align-items: center; justify-content: center; | |
| cursor: pointer; font-size: 14px; | |
| } | |
| .profile-details { width: 100%; } | |
| .detail-row { | |
| display: flex; justify-content: space-between; align-items: center; | |
| padding: 15px 0; border-bottom: 1px solid var(--border); | |
| font-size: 15px; | |
| } | |
| .detail-row:last-child { border-bottom: none; } | |
| .detail-label { color: var(--text-muted); } | |
| .detail-value { color: var(--text); font-weight: 600; text-align: right; max-width: 60%; } | |
| /* Editable Subject */ | |
| .editable-subject { border-bottom: 1px dashed var(--text-muted); cursor: pointer; } | |
| .subject-edit-input { | |
| background: var(--bg); | |
| color: var(--text); | |
| border: 1px solid var(--text); | |
| padding: 5px; | |
| border-radius: 5px; | |
| /* 👇 இங்கே மாற்றம் செய்யப்பட்டுள்ளது */ | |
| width: 60%; /* 100% ல இருந்து 60% ஆக குறைத்துள்ளேன் */ | |
| margin-left: 15px; /* இதுதான் அந்த இடைவெளியை (Gap) கொடுக்கும் */ | |
| text-align: right; | |
| } | |
| /* Themes */ | |
| .theme-list { background: var(--card); border-radius: 16px; border: 1px solid var(--border); overflow: hidden; } | |
| .theme-option { | |
| padding: 15px 20px; display: flex; align-items: center; gap: 15px; | |
| cursor: pointer; border-bottom: 1px solid var(--border); color: var(--text); | |
| } | |
| .theme-option:last-child { border-bottom: none; } | |
| .theme-icon { width: 24px; text-align: center; } | |
| .logout-row { | |
| margin-top: 10px; color: #ef4444; font-weight: 600; cursor: pointer; | |
| padding: 15px 0; text-align: center; | |
| } | |
| /* --- ONBOARDING OVERLAY (UNCHANGED LOGIC - FIXED CSS) --- */ | |
| #onboarding-overlay { | |
| position: fixed; top: 0; left: 0; width: 100vw; | |
| height: 100vh; height: 100lvh; | |
| z-index: 3000; | |
| display: flex; align-items: center; justify-content: center; | |
| transition: opacity 0.6s ease; opacity: 1; pointer-events: auto; | |
| background-color: #050505; overflow: hidden; | |
| } | |
| #onboarding-overlay.hidden { opacity: 0; pointer-events: none; } | |
| #onboarding-overlay::before, #onboarding-overlay::after { | |
| content: ""; position: absolute; width: 60vw; height: 60vw; | |
| border-radius: 50%; filter: blur(80px); z-index: -1; opacity: 0.6; | |
| } | |
| #onboarding-overlay::before { | |
| top: -20%; left: -20%; | |
| background: radial-gradient(circle at center, #d946ef, #7e22ce); | |
| animation: moveTopLeft 18s infinite alternate ease-in-out; | |
| } | |
| #onboarding-overlay::after { | |
| bottom: -20%; right: -20%; | |
| background: radial-gradient(circle at center, #2dd4bf, #0f766e); | |
| animation: moveBottomRight 15s infinite alternate ease-in-out; | |
| } | |
| @keyframes moveTopLeft { 0% { transform: translate(0, 0) scale(1); } 100% { transform: translate(20%, 20%) scale(1.2); } } | |
| @keyframes moveBottomRight { 0% { transform: translate(0, 0) scale(1); } 100% { transform: translate(-20%, -20%) scale(1.3); } } | |
| /* Wizard Styles */ | |
| .wizard-container { width: 90%; max-width: 450px; text-align: center; position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); z-index: 3001; } | |
| .step-content { display: none; animation: fadeIn 0.4s ease; } | |
| .step-content.active { display: block; } | |
| .intro-title { font-size: 32px; font-weight: 800; color: #fff; margin-bottom: 10px; } | |
| .intro-desc { color: #a1a1aa; font-size: 16px; margin-bottom: 40px; line-height: 1.5; } | |
| .btn-primary { background: #fff; color: #000; border: none; padding: 16px 40px; border-radius: 12px; font-size: 16px; font-weight: 700; cursor: pointer; width: 100%; transition: transform 0.2s; } | |
| .btn-primary:active { transform: scale(0.98); } | |
| .input-field { width: 100%; padding: 18px; border-radius: 12px; border: 1px solid #333; background: #111; color: #fff; text-align: center; outline: none; margin-bottom: 25px; font-size: 18px; font-family: 'Outfit', sans-serif; } | |
| .toggle-group { display: flex; gap: 10px; margin-bottom: 20px; } | |
| .toggle-btn { flex: 1; padding: 12px; border: 1px solid #333; border-radius: 10px; background: #111; color: #71717a; cursor: pointer; font-weight: 600; } | |
| .toggle-btn.selected { background: #fff; color: #000; border-color: #fff; } | |
| .dropdown-select { width: 100%; padding: 15px; margin-bottom: 15px; background: #111; border: 1px solid #333; border-radius: 12px; color: #fff; font-size: 16px; outline: none; appearance: none; } | |
| .hidden-opt { display: none; } | |
| .input-error { border: 2px solid #ef4444 !important; background: #2a0b0b !important; } | |
| .shake { animation: shake 0.4s cubic-bezier(.36,.07,.19,.97) both; } | |
| @keyframes shake { 10%, 90% { transform: translate3d(-1px, 0, 0); } 30%, 70% { transform: translate3d(-4px, 0, 0); } 50% { transform: translate3d(4px, 0, 0); } } | |
| /* --- FIX: FONT SIZE & FULL WIDTH --- */ | |
| /* 1. மெசேஜ் பாக்ஸ் செட்டிங்ஸ் */ | |
| .msg-bubble { | |
| padding: 12px 16px; | |
| /* 👇 எழுத்து அளவு பெரிதாக்கப்பட்டது */ | |
| font-size: 18px !important; | |
| line-height: 1.8 !important; | |
| /* 👇 வலது பக்கம் இடம் வீணாவதை தடுக்க (Full Width) */ | |
| max-width: 100% !important; | |
| width: fit-content; | |
| } | |
| /* 2. AI Text செட்டிங்ஸ் */ | |
| .ai-content { | |
| font-size: 18px !important; | |
| line-height: 1.8 !important; | |
| } | |
| /* --- PROFESSIONAL CHAT STYLES --- */ | |
| /* 1. Automatic Curve User Bubble */ | |
| /* --- USER MESSAGE BOX: CURVED SQUARE DESIGN --- */ | |
| .user-content { | |
| /* Square Shape with Curved Edges */ | |
| border-radius: 12px; /* சதுர வடிவம் மற்றும் வளைந்த விளிம்புகள் */ | |
| background: var(--user-msg); | |
| color: var(--text); | |
| padding: 12px 16px; | |
| /* Font Settings */ | |
| font-size: 17px; | |
| line-height: 1.6; | |
| /* 👇 Positioning & Auto-Sizing */ | |
| position: relative; | |
| width: fit-content; /* டெக்ஸ்ட் அளவிற்கு ஏற்ப மாறும் */ | |
| max-width: 85%; /* 85% மேல் போகாது */ | |
| min-width: 50px; /* மிகச் சிறிய வார்த்தைக்கும் வடிவம் மாறாது */ | |
| word-wrap: break-word; /* நீண்ட வார்த்தைகளை உடைக்கும் */ | |
| margin-left: auto; /* வலது பக்கம் ஒட்டி நிற்கும் */ | |
| box-shadow: 0 4px 10px rgba(0,0,0,0.2); /* அழகான நிழல் */ | |
| border: 1px solid rgba(255,255,255,0.05); /* மெல்லிய பார்டர் */ | |
| } | |
| /* 2. Stylish Input Bar (Glassmorphism) */ | |
| .input-container { | |
| background: rgba(20, 20, 20, 0.95); | |
| backdrop-filter: blur(10px); | |
| border: 1px solid #333; | |
| border-radius: 40px; | |
| padding: 8px 10px; | |
| display: flex; align-items: flex-end; gap: 12px; | |
| transition: all 0.3s ease; | |
| box-shadow: 0 10px 30px rgba(0,0,0,0.3); | |
| } | |
| .input-container:focus-within { border-color: #666; box-shadow: 0 10px 40px rgba(0,0,0,0.5); } | |
| /* 3. Stylish + Button (Rotating) */ | |
| .plus-btn { | |
| width: 44px; height: 44px; border-radius: 50%; | |
| background: #27272a; color: #aaa; | |
| display: flex; align-items: center; justify-content: center; | |
| cursor: pointer; flex-shrink: 0; font-size: 20px; | |
| transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1); | |
| border: 1px solid #333; | |
| } | |
| .plus-btn:hover { background: #fff; color: #000; transform: rotate(90deg); } | |
| /* 4. Stylish Send Button (Bouncing) */ | |
| .send-btn { | |
| width: 44px; height: 44px; border-radius: 50%; | |
| background: #fff; color: #000; | |
| display: flex; align-items: center; justify-content: center; | |
| cursor: pointer; flex-shrink: 0; font-size: 18px; | |
| transition: all 0.3s cubic-bezier(0.34, 1.56, 0.64, 1); | |
| margin-bottom: 0px; | |
| } | |
| .send-btn:hover { transform: scale(1.1); box-shadow: 0 0 15px rgba(255,255,255,0.4); } | |
| .send-btn:active { transform: scale(0.9); } | |
| /* ... ஏற்கனவே இருக்கும் டிசைன் கோடுகள் ... */ | |
| /* VOICE MODE STYLES */ | |
| .mic-btn { | |
| background: transparent; border: 1px solid var(--border); | |
| color: var(--text-muted); width: 40px; height: 40px; | |
| border-radius: 50%; display: flex; align-items: center; justify-content: center; | |
| cursor: pointer; transition: all 0.2s; margin-right: 8px; | |
| } | |
| .mic-btn:hover { background: var(--text); color: var(--bg); } | |
| /* 👇 UPDATED ANIMATION STYLE (With !important) */ | |
| .mic-btn.listening { | |
| background: #ef4444 !important; /* Force Red Background */ | |
| color: white !important; /* Force White Icon */ | |
| border-color: #ef4444 !important; | |
| animation: pulse 1.5s infinite; | |
| box-shadow: 0 0 0 0 rgba(239, 68, 68, 0.7); /* Extra Glow */ | |
| } | |
| @keyframes pulse { 0% { box-shadow: 0 0 0 0 rgba(239, 68, 68, 0.4); } 70% { box-shadow: 0 0 0 10px rgba(239, 68, 68, 0); } 100% { box-shadow: 0 0 0 0 rgba(239, 68, 68, 0); } } | |
| /* 👇 புதிய கோடை இங்கே மட்டும் பேஸ்ட் பண்ணுங்க */ | |
| * { | |
| -webkit-user-select: none; | |
| -ms-user-select: none; | |
| user-select: none; | |
| -webkit-touch-callout: none; | |
| } | |
| input, textarea, .msg-bubble, .ai-content, .user-content { | |
| -webkit-user-select: text; | |
| user-select: text; | |
| } | |
| #custom-modal { | |
| position: fixed; inset: 0; background: rgba(0,0,0,0.85); | |
| display: none; align-items: center; justify-content: center; z-index: 9999; | |
| } | |
| .modal-content { | |
| background: var(--card); border: 1px solid var(--border); | |
| padding: 25px; border-radius: 20px; width: 90%; max-width: 350px; text-align: center; | |
| } | |
| .modal-input { | |
| width: 100%; padding: 12px; border-radius: 10px; border: 1px solid var(--border); | |
| background: var(--bg); color: var(--text); margin: 15px 0; outline: none; | |
| } | |
| .modal-btns { display: flex; gap: 10px; margin-top: 10px; } | |
| .m-btn { flex: 1; padding: 12px; border-radius: 10px; border: none; font-weight: 600; cursor: pointer; } | |
| /* 👇 இதை அப்படியே Copy & Paste பண்ணுங்க (Styles பகுதி) */ | |
| .settings-option-btn { | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: center; | |
| width: 100%; | |
| padding: 15px 20px; | |
| margin-bottom: 12px; | |
| background: var(--card); | |
| border: 1px solid var(--border); | |
| border-radius: 16px; | |
| cursor: pointer; | |
| color: var(--text); | |
| font-weight: 500; | |
| transition: background 0.2s, transform 0.1s; | |
| min-height: 70px; /* உயரம் மாறாமல் இருக்க */ | |
| } | |
| .settings-option-btn:active { | |
| transform: scale(0.98); | |
| background: var(--hover); | |
| } | |
| /* 👇 இது புதுசு: ஐகானுக்கான கருப்பு பாக்ஸ் */ | |
| .setting-icon-box { | |
| width: 40px; | |
| height: 40px; | |
| background: var(--bg); | |
| border-radius: 10px; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| margin-right: 15px; | |
| border: 1px solid var(--border); | |
| flex-shrink: 0; | |
| } | |
| /* Sub-Pages (Hidden by default) */ | |
| .settings-sub-page { | |
| position: absolute; top: 0; left: 0; width: 100%; height: 100%; | |
| background: var(--bg); z-index: 2050; | |
| display: flex; flex-direction: column; | |
| transform: translateX(100%); transition: transform 0.3s ease; | |
| } | |
| .settings-sub-page.active { transform: translateX(0); } | |
| /* --- 1. CSS FIX FOR SMOOTH MENU --- */ | |
| /* Sub-header தனி */ | |
| .sub-header { | |
| padding: 20px; | |
| padding-top: calc(20px + env(safe-area-inset-top)); | |
| display: flex; align-items: center; gap: 15px; | |
| border-bottom: 1px solid var(--border); margin-bottom: 20px; | |
| } | |
| /* Active States (மெனு ஸ்மூத்தா வர இது முக்கியம்) */ | |
| #sidebar.open .sidebar-content, | |
| #settings-overlay.active, | |
| .settings-sub-page.active { | |
| transform: translateX(0); | |
| } | |
| /* Sidebar Animation */ | |
| .sidebar-content { | |
| transition: transform 0.4s cubic-bezier(0.25, 0.8, 0.25, 1); /* Buttery Smooth */ | |
| } | |
| /* Hide browser's default X button for search inputs */ | |
| input[type="search"]::-webkit-search-decoration, | |
| input[type="search"]::-webkit-search-cancel-button, | |
| input[type="search"]::-webkit-search-results-button, | |
| input[type="search"]::-webkit-search-results-decoration { | |
| -webkit-appearance: none; | |
| } | |
| /* 1. CONTAINER-ஐ நகர விடாமல் தடுத்தல் (Fixing Double Animation) */ | |
| #sidebar { | |
| transform: none !important; /* இது நகராது */ | |
| transition: visibility 0s linear 0.4s, background-color 0.4s ease !important; | |
| display: flex !important; | |
| visibility: hidden; | |
| background-color: rgba(0,0,0,0); | |
| } | |
| #sidebar.open { | |
| visibility: visible !important; | |
| background-color: rgba(0,0,0,0.6) !important; | |
| transition-delay: 0s !important; | |
| } | |
| /* 2. CONTENT-ஐ மட்டும் GPU-வில் நகர வைப்பது (Butter Smooth) */ | |
| .sidebar-content { | |
| transform: translate3d(-100%, 0, 0) !important; /* இடது பக்கம் மறைந்திருக்கும் */ | |
| width: 280px !important; | |
| transition: transform 0.35s cubic-bezier(0.2, 0.9, 0.2, 1) !important; | |
| will-change: transform; | |
| box-shadow: 5px 0 15px rgba(0,0,0,0.3); | |
| } | |
| /* 3. SETTINGS PAGES (வலது பக்கம் இருந்து வர) */ | |
| #settings-overlay, .settings-sub-page { | |
| transform: translate3d(100%, 0, 0) !important; /* வலது பக்கம் மறைந்திருக்கும் */ | |
| transition: transform 0.35s cubic-bezier(0.2, 0.9, 0.2, 1) !important; | |
| will-change: transform; | |
| } | |
| /* 4. ACTIVE STATES (இயக்கம்) */ | |
| #sidebar.open .sidebar-content, | |
| #settings-overlay.active, | |
| .settings-sub-page.active { | |
| transform: translate3d(0, 0, 0) !important; | |
| } | |
| /* 5. நாம் அழித்த .sub-header க்கான சரியான கோட் */ | |
| .sub-header { | |
| padding: 20px; | |
| padding-top: calc(20px + env(safe-area-inset-top)); | |
| display: flex; align-items: center; gap: 15px; | |
| border-bottom: 1px solid var(--border); margin-bottom: 20px; | |
| } | |
| /* AD BANNER STYLE */ | |
| .ad-banner-small { | |
| width: 100%; height: 60px; | |
| background: #202020; border: 1px dashed #444; | |
| display: flex; align-items: center; justify-content: center; | |
| color: #666; font-size: 11px; text-transform: uppercase; letter-spacing: 1px; | |
| border-radius: 8px; margin-top: 10px; flex-shrink: 0; /* சுருங்காது */ | |
| cursor: default; user-select: none; | |
| } | |
| /* Light Mode-க்கு */ | |
| body.light-mode .ad-banner-small { background: #e4e4e7; border-color: #ccc; color: #888; } | |
| /* Pro Mode வந்தால் மறைக்க */ | |
| .ad-banner-small.hidden { display: none !important; } | |
| /* SUGGESTION CHIPS */ | |
| .suggestion-container { display: flex; flex-wrap: wrap; gap: 10px; margin-top: 10px; } | |
| .suggestion-chip { | |
| background: transparent; border: 1px solid var(--border); | |
| color: var(--text-muted); padding: 8px 15px; border-radius: 20px; | |
| font-size: 13px; cursor: pointer; transition: all 0.2s; | |
| } | |
| .suggestion-chip:hover { background: var(--text); color: var(--bg); border-color: var(--text); } | |
| /* 👇 OVERLAP FIX: Force hide inactive steps 👇 */ | |
| .step-content { display: none !important; } | |
| .step-content.active { display: block !important; } | |
| /* 👇 Hide rogue buttons inside wizard container 👇 */ | |
| .wizard-container > button { display: none !important; } | |
| /* 👇 HIDE AD SPACES (Niotron Native Ads will float over this) 👇 */ | |
| .ad-banner-small { | |
| display: none !important; | |
| } | |
| /* 👇 LOGIN BUTTON STYLE FIX (Optional) 👇 */ | |
| .google-btn { | |
| background: #4285F4; color: white; | |
| display: flex; align-items: center; justify-content: center; | |
| gap: 10px; width: 100%; padding: 12px; | |
| border-radius: 12px; border: none; font-weight: 600; | |
| cursor: pointer; margin-top: 10px; | |
| } | |
| /* 👇 SCIENCE & MATH STYLING 👇 */ | |
| /* 1. TABLES (அட்டவணை அழகா தெரிய) */ | |
| table { | |
| width: 100%; | |
| border-collapse: collapse; | |
| margin: 20px 0; | |
| font-size: 15px; | |
| overflow: hidden; | |
| border-radius: 8px; | |
| border-style: hidden; /* Hide outer border */ | |
| box-shadow: 0 0 0 1px var(--border); /* Custom border */ | |
| display: block; /* மொபைலில் ஸ்க்ரோல் ஆக */ | |
| overflow-x: auto; | |
| white-space: nowrap; | |
| } | |
| th, td { | |
| padding: 12px 15px; | |
| border: 1px solid var(--border); | |
| text-align: left; | |
| } | |
| th { | |
| background-color: var(--card); | |
| color: var(--text); | |
| font-weight: 700; | |
| text-transform: uppercase; | |
| font-size: 13px; | |
| letter-spacing: 0.5px; | |
| } | |
| tr:nth-child(even) { background-color: rgba(255,255,255,0.03); } | |
| tr:hover { background-color: rgba(255,255,255,0.05); transition: 0.2s; } | |
| /* 2. MATH EQUATIONS (சமன்பாடுகள்) */ | |
| mjx-container { | |
| overflow-x: auto; | |
| overflow-y: hidden; | |
| max-width: 100%; | |
| padding: 10px 0; | |
| } | |
| /* Inline Math ($...$) கலர் */ | |
| .mjx-chtml { color: #a5b4fc !important; } | |
| /* 3. CHEMISTRY FORMULAS highlight */ | |
| code { | |
| color: #ef4444; /* Code வார்த்தைகள் தனி நிறத்தில் */ | |
| background: rgba(255,0,0,0.1); | |
| padding: 2px 5px; | |
| border-radius: 4px; | |
| } | |
| pre code { | |
| color: inherit; | |
| background: transparent; | |
| padding: 0; | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <div id="settings-overlay"> | |
| <div id="settings-main-view" style="height:100%; display:flex; flex-direction:column;"> | |
| <div class="settings-header" style="flex-shrink:0;"> | |
| <div class="back-btn" onclick="closeSettings()"><i class="fas fa-arrow-left"></i></div> | |
| <h2 style="margin:0; font-size:20px; color:var(--text);">Settings</h2> | |
| </div> | |
| <div class="settings-search" style="position:relative; flex-shrink:0;"> | |
| <i class="fas fa-search" style="position:absolute; left:15px; top:50%; transform:translateY(-50%); color:var(--text-muted);"></i> | |
| <input type="search" id="setting-search-input" name="setting_search_field" placeholder="Search settings..." autocomplete="off" spellcheck="false" | |
| style="width:100%; padding-left:30px; background:transparent; border:none; color:var(--text); outline:none;" | |
| oninput="filterSettings(this.value)" | |
| onkeydown="if(event.key==='Enter') this.blur()"> | |
| <i class="fas fa-times" id="clear-setting-search" onclick="clearSettingsSearch()" | |
| style="position:absolute; right:15px; top:50%; transform:translateY(-50%); cursor:pointer; display:none; color:var(--text-muted);"></i> | |
| </div> | |
| <div class="settings-content" style="flex:1; overflow-y:auto; padding:20px;"> | |
| <div class="settings-option-btn" onclick="openSubPage('subpage-profile')"> | |
| <div style="display:flex; align-items:center;"> | |
| <div class="setting-icon-box"> | |
| <i class="fas fa-user-graduate" style="color:var(--text); font-size:18px;"></i> | |
| </div> | |
| <div style="display:flex; flex-direction:column;"> | |
| <span style="font-size:16px; font-weight:600;">Student Details</span> | |
| <span style="font-size:12px; color:var(--text-muted);">Name, Standard, Subject</span> | |
| </div> | |
| </div> | |
| <i class="fas fa-chevron-right" style="color:var(--text-muted); font-size:14px;"></i> | |
| </div> | |
| <div class="settings-option-btn" onclick="openSubPage('subpage-themes')"> | |
| <div style="display:flex; align-items:center;"> | |
| <div class="setting-icon-box"> | |
| <i class="fas fa-palette" style="color:var(--text); font-size:18px;"></i> | |
| </div> | |
| <div style="display:flex; flex-direction:column;"> | |
| <span style="font-size:16px; font-weight:600;">Themes</span> | |
| <span style="font-size:12px; color:var(--text-muted);">Dark, Light, System</span> | |
| </div> | |
| </div> | |
| <i class="fas fa-chevron-right" style="color:var(--text-muted); font-size:14px;"></i> | |
| </div> | |
| <div class="settings-option-btn" onclick="openSubPage('subpage-chats')"> | |
| <div style="display:flex; align-items:center;"> | |
| <div class="setting-icon-box"> | |
| <i class="fas fa-comments" style="color:var(--text); font-size:18px;"></i> | |
| </div> | |
| <div style="display:flex; flex-direction:column;"> | |
| <span style="font-size:16px; font-weight:600;">Chats</span> | |
| <span style="font-size:12px; color:var(--text-muted);">History, Clear Data</span> | |
| </div> | |
| </div> | |
| <i class="fas fa-chevron-right" style="color:var(--text-muted); font-size:14px;"></i> | |
| </div> | |
| </div> | |
| <div style="padding:15px 20px; flex-shrink:0; border-top:1px solid var(--border); background:var(--bg);"> | |
| <div class="ad-banner-small" id="settings-ad">Settings Ad Space (320x60)</div> | |
| </div> | |
| </div> | |
| <div id="subpage-profile" class="settings-sub-page" style="height:100%; display:flex; flex-direction:column;"> | |
| <div class="sub-header" style="flex-shrink:0;"> | |
| <div class="back-btn" onclick="closeSubPage('subpage-profile')"><i class="fas fa-arrow-left"></i></div> | |
| <h2 style="margin:0; font-size:20px; color:var(--text);">Student Details</h2> | |
| </div> | |
| <div class="settings-content" style="flex:1; overflow-y:auto; padding:20px;"> | |
| <div class="profile-card"> | |
| <div class="profile-pic-wrapper"> | |
| <img id="settings-pic" src="https://ui-avatars.com/api/?name=User&background=random" class="profile-pic"> | |
| <label for="pic-upload" class="edit-pic-btn"><i class="fas fa-camera"></i></label> | |
| <input type="file" id="pic-upload" hidden accept="image/*" onchange="uploadProfilePic(this)"> | |
| </div> | |
| <div class="profile-details"> | |
| <div class="detail-row"> | |
| <span class="detail-label">Name</span> | |
| <span class="detail-value" id="profile-name">User</span> | |
| </div> | |
| <div class="detail-row"> | |
| <span class="detail-label">Education</span> | |
| <span class="detail-value" id="profile-edu">College</span> | |
| </div> | |
| <div class="detail-row"> | |
| <span class="detail-label">Standard/Dept</span> | |
| <span class="detail-value" id="profile-std">AI & DS</span> | |
| </div> | |
| <div class="detail-row"> | |
| <span class="detail-label">Subject</span> | |
| <span class="detail-value editable-subject" id="profile-sub" onclick="editSubject(this)">Maths</span> | |
| </div> | |
| <div class="detail-row"> | |
| <span class="detail-label">Medium</span> | |
| <span class="detail-value" id="profile-med">English</span> | |
| </div> | |
| <div class="logout-row" onclick="handleLogout()">Log Out</div> | |
| </div> | |
| </div> | |
| </div> | |
| <div style="padding:15px 20px; flex-shrink:0; border-top:1px solid var(--border); background:var(--bg);"> | |
| <div class="ad-banner-small" id="profile-ad">Profile Ad Space (320x60)</div> | |
| </div> | |
| </div> | |
| <div id="subpage-themes" class="settings-sub-page" style="height:100%; display:flex; flex-direction:column;"> | |
| <div class="sub-header" style="flex-shrink:0;"> | |
| <div class="back-btn" onclick="closeSubPage('subpage-themes')"><i class="fas fa-arrow-left"></i></div> | |
| <h2 style="margin:0; font-size:20px; color:var(--text);">Themes</h2> | |
| </div> | |
| <div class="settings-content" style="flex:1; overflow-y:auto; padding:20px;"> | |
| <div class="theme-list"> | |
| <div class="theme-option" onclick="setTheme('light')"> | |
| <div class="theme-icon"><i class="fas fa-sun"></i></div> | |
| <span>Light Mode</span> | |
| </div> | |
| <div class="theme-option" onclick="setTheme('dark')"> | |
| <div class="theme-icon"><i class="fas fa-moon"></i></div> | |
| <span>Dark Mode</span> | |
| </div> | |
| <div class="theme-option" onclick="setTheme('system')"> | |
| <div class="theme-icon"><i class="fas fa-desktop"></i></div> | |
| <span>System Default</span> | |
| </div> | |
| </div> | |
| </div> | |
| <div style="padding:15px 20px; flex-shrink:0; border-top:1px solid var(--border); background:var(--bg);"> | |
| <div class="ad-banner-small" id="theme-ad">Theme Ad Space (320x60)</div> | |
| </div> | |
| </div> | |
| <div id="subpage-chats" class="settings-sub-page" style="height:100%; display:flex; flex-direction:column;"> | |
| <div class="sub-header" style="flex-shrink:0;"> | |
| <div class="back-btn" onclick="closeSubPage('subpage-chats')"><i class="fas fa-arrow-left"></i></div> | |
| <h2 style="margin:0; font-size:20px; color:var(--text);">Chats Settings</h2> | |
| </div> | |
| <div class="settings-content" style="flex:1; overflow-y:auto; padding:20px;"> | |
| <div style="margin-bottom:20px; padding:0 5px;"> | |
| <div style="font-size:14px; color:var(--text-muted); margin-bottom:10px;">DATA MANAGEMENT</div> | |
| <div style="font-size:13px; color:#666;"> | |
| Control your chat history and data here. Deleting chats is permanent. | |
| </div> | |
| </div> | |
| <div style="background:rgba(239, 68, 68, 0.1); border:1px solid rgba(239, 68, 68, 0.3); border-radius:16px; padding:20px;"> | |
| <div style="display:flex; align-items:center; gap:15px; margin-bottom:15px;"> | |
| <div style="width:40px; height:40px; background:rgba(239, 68, 68, 0.2); border-radius:50%; display:flex; align-items:center; justify-content:center;"> | |
| <i class="fas fa-trash-alt" style="color:#ef4444; font-size:18px;"></i> | |
| </div> | |
| <div> | |
| <div style="font-size:16px; font-weight:600; color:var(--text);">Delete All History</div> | |
| <div style="font-size:12px; color:var(--text-muted);">Permanently remove all messages</div> | |
| </div> | |
| </div> | |
| <button onclick="confirmClearHistory()" | |
| style="width:100%; padding:14px; border-radius:12px; border:none; | |
| background:#ef4444; color:white; font-size:15px; font-weight:600; | |
| cursor:pointer; display:flex; align-items:center; justify-content:center; gap:8px; | |
| transition: background 0.2s;"> | |
| <i class="fas fa-exclamation-circle"></i> Delete All Chats | |
| </button> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <div id="onboarding-overlay"> | |
| <div class="wizard-container"> | |
| <div id="step-1" class="step-content active"> | |
| <div style="margin-bottom: 20px;"><i class="fas fa-graduation-cap" style="font-size: 60px; color: #fff;"></i></div> | |
| <h1 class="intro-title">Welcome to<br>Student's AI</h1> | |
| <p class="intro-desc">Your personal AI tutor designed to simplify learning, solve doubts, and help you excel.</p> | |
| <button class="btn-primary" onclick="nextStep(2)">Get Started</button> | |
| </div> | |
| <div id="step-2" class="step-content"> | |
| <h2 class="intro-title" style="font-size: 26px;">What's your name?</h2> | |
| <input type="text" id="name-input" class="input-field" placeholder="Enter your Name" autocomplete="off" onkeydown="if(event.key==='Enter') nextStep(3)"> | |
| <button class="btn-primary" onclick="nextStep(3)">Next</button> | |
| </div> | |
| <div id="step-3" class="step-content"> | |
| <h2 class="intro-title" style="font-size: 26px;">Student Details</h2> | |
| <div class="toggle-group"> | |
| <div class="toggle-btn selected" id="btn-school" onclick="toggleType('school')">School</div> | |
| <div class="toggle-btn" id="btn-college" onclick="toggleType('college')">College</div> | |
| </div> | |
| <div id="school-opts"> | |
| <select id="school-std" class="dropdown-select" onchange="checkStandard()"> | |
| <option value="" disabled selected>Select Standard</option> | |
| <option value="6th">6th</option><option value="7th">7th</option><option value="8th">8th</option> | |
| <option value="9th">9th</option><option value="10th">10th</option> | |
| <option value="11th">11th</option><option value="12th">12th</option> | |
| </select> | |
| <select id="school-group" class="dropdown-select" style="display:none;" onchange="updateGroupSubjects()"> | |
| <option value="" disabled selected>Select Group</option> | |
| <option value="Biology">Biology</option> | |
| </select> | |
| <select id="school-subject-select" class="dropdown-select" style="display:none;"> | |
| <option value="" disabled selected>Select Subject</option> | |
| </select> | |
| </div> | |
| <div id="college-opts" class="hidden-opt" style="display:none;"> | |
| <select id="college-dept" class="dropdown-select"> | |
| <option value="" disabled selected>Select Department</option> | |
| <option value="CSE">CSE</option><option value="AI & DS">AI & DS</option><option value="IT">IT</option> | |
| </select> | |
| <select id="college-year" class="dropdown-select" onchange="updateSemesters()"> | |
| <option value="" disabled selected>Select Year</option> | |
| <option value="1st Year">1st Year</option><option value="2nd Year">2nd Year</option> | |
| </select> | |
| <select id="college-sem" class="dropdown-select"><option value="" disabled selected>Select Semester</option></select> | |
| <input type="text" id="college-subject" class="input-field" placeholder="Enter Subject" autocomplete="off" onkeydown="if(event.key==='Enter') nextStep(4)"> | |
| </div> | |
| <button class="btn-primary" onclick="nextStep(4)">Next</button> | |
| </div> | |
| <div id="step-4" class="step-content"> | |
| <h2 class="intro-title" style="font-size: 26px;">Choose Medium</h2> | |
| <p class="intro-desc" style="margin-bottom:20px; color:#aaa;">Which language do you study in?</p> | |
| <div class="toggle-group"> | |
| <div class="toggle-btn selected" id="btn-english" onclick="toggleMedium('English')">English</div> | |
| <div class="toggle-btn" id="btn-tamil" onclick="toggleMedium('Tamil')">Tamil</div> | |
| </div> | |
| <button class="btn-primary" onclick="finishSetup()">Start Learning</button> | |
| </div> | |
| </div> | |
| </div> | |
| <div id="sidebar"> | |
| <div class="sidebar-content" style="position:relative;"> <div onclick="toggleSidebar()" | |
| style="position:absolute; top:15px; right:15px; cursor:pointer; padding:5px; z-index:10;"> | |
| <i class="fas fa-times" style="color:var(--text-muted); font-size:22px;"></i> | |
| </div> | |
| <div style="position:relative; margin-top:50px; margin-bottom:20px; flex-shrink:0;"> | |
| <i class="fas fa-search" style="position:absolute; left:12px; top:50%; transform:translateY(-50%); color:var(--text-muted); font-size:14px; pointer-events:none;"></i> | |
| <input type="search" id="hist-search" name="search_hist_field" placeholder="Search..." autocomplete="off" spellcheck="false" | |
| style="width:100%; height:45px; padding:0 35px 0 40px; border-radius:12px; border:1px solid var(--border); background:var(--card); color:var(--text); outline:none; font-size:15px;" | |
| oninput="filterHistory(this.value)" | |
| onkeydown="if(event.key==='Enter') this.blur()"> | |
| <i class="fas fa-times" id="clear-search" onclick="clearSearch()" | |
| style="position:absolute; right:12px; top:50%; transform:translateY(-50%); cursor:pointer; display:none; color:var(--text-muted);"></i> | |
| </div> | |
| <div style="margin-bottom:20px; padding-left:5px;"> | |
| <span class="user-info-text" id="display-name" style="font-size:22px; font-weight:700;">User</span> | |
| </div> | |
| <button class="new-chat-btn" onclick="newChat()"> | |
| <i class="fas fa-pen-to-square"></i> <span>New Chat</span> | |
| </button> | |
| <div class="history-label" style="flex-shrink:0;">Chat History</div> | |
| <div id="history-list" style="flex:1; overflow-y:auto; padding-right:5px; margin-bottom:10px;"></div> | |
| <div class="ad-banner-small" id="sidebar-ad">Menu Ad Space (320x60)</div> | |
| <div class="sidebar-footer"> | |
| <div class="footer-link" onclick="openSettings()"><i class="fas fa-cog"></i> Settings</div> | |
| <div class="footer-link"><i class="fas fa-question-circle"></i> Help</div> | |
| </div> | |
| </div> | |
| <div class="sidebar-overlay-gap" onclick="toggleSidebar()"></div> | |
| </div> | |
| <div id="app-container"> | |
| <header> | |
| <div class="menu-btn" onclick="toggleSidebar()"><i class="fas fa-bars"></i></div> | |
| <span class="app-title">Student's AI</span> | |
| <div style="width:40px;"></div> | |
| </header> | |
| <div id="chat-box"></div> | |
| <div class="input-wrapper"> | |
| <div id="preview-box" style="display:none; padding:10px 15px; background:var(--bg); border-top:1px solid var(--border);"> | |
| <div style="position:relative; display:inline-block;"> | |
| <img id="preview-img" style="width:60px; height:60px; border-radius:8px; object-fit:cover; border:1px solid #444;"> | |
| <div onclick="clearFile()" style="position:absolute; top:-8px; right:-8px; background:red; color:fff; border-radius:50%; width:20px; height:20px; display:flex; align-items:center; justify-content:center; cursor:pointer; font-size:12px;">×</div> | |
| </div> | |
| </div> | |
| <div id="attach-menu" style="display:none; position:absolute; bottom:75px; left:15px; background:#1a1a1a; border:1px solid #333; border-radius:16px; padding:8px; flex-direction:column; width:160px; z-index:100; box-shadow:0 10px 30px rgba(0,0,0,0.5);"> | |
| <label style="padding:12px; display:flex; align-items:center; gap:12px; color:#fff; cursor:pointer; font-size:14px;"> | |
| <i class="fas fa-camera" style="color:#00d2ff;"></i> Camera | |
| <input type="file" hidden accept="image/*" capture="environment" onchange="handleFile(this)"> | |
| </label> | |
| <label style="padding:12px; display:flex; align-items:center; gap:12px; color:#fff; cursor:pointer; font-size:14px;"> | |
| <i class="fas fa-image" style="color:#bf5af2;"></i> Gallery | |
| <input type="file" hidden accept="image/*" onchange="handleFile(this)"> | |
| </label> | |
| <label style="padding:12px; display:flex; align-items:center; gap:12px; color:#fff; cursor:pointer; font-size:14px;"> | |
| <i class="fas fa-file-pdf" style="color:#ff3b30;"></i> File | |
| <input type="file" hidden accept="application/pdf" onchange="handleFile(this)"> | |
| </label> | |
| </div> | |
| <div class="input-container"> | |
| <div class="plus-btn" onclick="toggleAttachMenu()"> | |
| <i class="fas fa-plus"></i> | |
| </div> | |
| <button class="mic-btn" id="mic-btn" onclick="toggleVoice()"> | |
| <i class="fas fa-microphone"></i> | |
| </button> | |
| <textarea id="msg-input" placeholder="Message..." rows="1" | |
| oninput="this.style.height='auto';this.style.height=this.scrollHeight+'px'"></textarea> | |
| <div class="send-btn" onclick="send()"> | |
| <i class="fas fa-arrow-up"></i> | |
| </div> | |
| </div> | |
| </div> | |
| <script> | |
| // 1. GLOBAL VARIABLES | |
| let currentUser = null; | |
| let userDetails = { type: 'school', medium: 'English' }; // Default Medium added | |
| let currentChatId = null; | |
| let isGenerating = false; | |
| let abortController = null; // 🛑 புதுசா சேருங்க | |
| /* 👇 NEW FUNCTION: Protect Math Formulas from Markdown 👇 */ | |
| function formatText(text) { | |
| // 1. $$...$$ (பெரிய ஃபார்முலா) மற்றும் $...$ (சிறிய ஃபார்முலா) கண்டுபிடிக்கிறது | |
| // 2. அதற்குள் இருக்கும் _ (underscore) குறியீட்டை பாதுகாக்கிறது | |
| return marked.parse(text.replace(/(\$\$[\s\S]*?\$\$|\$[^$]*?\$)/g, function(match) { | |
| return match.replace(/_/g, '\\_').replace(/\*/g, '\\*'); | |
| })); | |
| } | |
| /* 👇 UPDATED NEXT STEP (No Alert, Only Shake) 👇 */ | |
| function nextStep(targetStep) { | |
| // STEP 2 VALIDATION: Name Check | |
| if (targetStep === 3) { | |
| const name = document.getElementById('name-input').value.trim(); | |
| if (!name) return shakeElement('name-input'); | |
| currentUser = name; | |
| } | |
| // 👇👇👇 STEP 3 VALIDATION (Silent Check) 👇👇👇 | |
| if (targetStep === 4) { | |
| let isValid = true; | |
| if (userDetails.type === 'school') { | |
| // 1. Check Standard | |
| const std = document.getElementById('school-std').value; | |
| if (!std) { | |
| shakeElement('school-std'); | |
| isValid = false; | |
| } else { | |
| // 2. Check Subjects based on Standard | |
| if (['11th', '12th'].includes(std)) { | |
| // For 11th & 12th: Check Group AND Subject | |
| const grp = document.getElementById('school-group').value; | |
| const sub = document.getElementById('school-subject-select').value; | |
| if (!grp) { shakeElement('school-group'); isValid = false; } | |
| else if (!sub) { shakeElement('school-subject-select'); isValid = false; } | |
| } else { | |
| // For 6th to 10th: Check Subject only | |
| const sub = document.getElementById('school-subject-select').value; | |
| if (!sub) { shakeElement('school-subject-select'); isValid = false; } | |
| } | |
| } | |
| } else { | |
| // College Validation | |
| const dept = document.getElementById('college-dept').value; | |
| const year = document.getElementById('college-year').value; | |
| const sem = document.getElementById('college-sem').value; | |
| const sub = document.getElementById('college-subject').value.trim(); | |
| if (!dept) { shakeElement('college-dept'); isValid = false; } | |
| if (!year) { shakeElement('college-year'); isValid = false; } | |
| if (!sem) { shakeElement('college-sem'); isValid = false; } | |
| if (!sub) { shakeElement('college-subject'); isValid = false; } | |
| } | |
| // If anything is missing, JUST STOP (No Alert) | |
| if (!isValid) { | |
| return; // ⛔ சும்மா நின்றுவிடும், ஆனால் பாக்ஸ் ஷேக் ஆகும் | |
| } | |
| } | |
| // 👆👆👆 VALIDATION END 👆👆👆 | |
| // Proceed to Next Step | |
| if (targetStep > 1) history.pushState({ step: targetStep }, null, ""); | |
| document.querySelectorAll('.step-content').forEach(el => el.classList.remove('active')); | |
| document.getElementById('step-' + targetStep).classList.add('active'); | |
| } | |
| window.onpopstate = function(e) { | |
| document.querySelectorAll('.step-content').forEach(el => el.classList.remove('active')); | |
| const s = e.state && e.state.step ? e.state.step : 1; | |
| document.getElementById('step-' + s).classList.add('active'); | |
| }; | |
| function toggleType(type) { | |
| userDetails.type = type; | |
| document.getElementById('btn-school').classList.toggle('selected', type === 'school'); | |
| document.getElementById('btn-college').classList.toggle('selected', type === 'college'); | |
| document.getElementById('school-opts').style.display = type === 'school' ? 'block' : 'none'; | |
| document.getElementById('college-opts').style.display = type === 'college' ? 'block' : 'none'; | |
| } | |
| function toggleMedium(medium) { | |
| userDetails.medium = medium; | |
| document.getElementById('btn-english').classList.toggle('selected', medium === 'English'); | |
| document.getElementById('btn-tamil').classList.toggle('selected', medium === 'Tamil'); | |
| } | |
| function updateSemesters() { | |
| const year = document.getElementById('college-year').value; | |
| const semSelect = document.getElementById('college-sem'); | |
| semSelect.innerHTML = '<option value="" disabled selected>Select Semester</option>'; | |
| let options = year === '1st Year' ? ['Sem 1', 'Sem 2'] : ['Sem 3', 'Sem 4']; | |
| options.forEach(s => { | |
| let opt = document.createElement('option'); opt.value = s; opt.innerText = s; semSelect.appendChild(opt); | |
| }); | |
| } | |
| /* --- 🏫 SCHOOL LOGIC (Group & Dynamic Subjects) --- */ | |
| // Subject Lists | |
| const sub1to10 = ["Tamil", "English", "Maths", "Science", "Social Science"]; | |
| const subBioGroup = ["Tamil", "English", "Maths", "Physics", "Chemistry", "Botany", "Zoology"]; | |
| function checkStandard() { | |
| const std = document.getElementById('school-std').value; | |
| const groupSelect = document.getElementById('school-group'); | |
| const subjectSelect = document.getElementById('school-subject-select'); | |
| // Reset Selections | |
| subjectSelect.innerHTML = '<option value="" disabled selected>Select Subject</option>'; | |
| if (['11th', '12th'].includes(std)) { | |
| // 11th & 12th: Show Group, Hide Subject initially | |
| groupSelect.style.display = 'block'; | |
| subjectSelect.style.display = 'none'; // Group choose பண்ணதுக்கு அப்புறம் வரும் | |
| } else { | |
| // 1st to 10th: Hide Group, Show 1-10 Subjects | |
| groupSelect.style.display = 'none'; | |
| subjectSelect.style.display = 'block'; | |
| sub1to10.forEach(s => { | |
| let opt = document.createElement('option'); | |
| opt.value = s; opt.innerText = s; | |
| subjectSelect.appendChild(opt); | |
| }); | |
| } | |
| } | |
| function updateGroupSubjects() { | |
| const group = document.getElementById('school-group').value; | |
| const subjectSelect = document.getElementById('school-subject-select'); | |
| subjectSelect.innerHTML = '<option value="" disabled selected>Select Subject</option>'; | |
| subjectSelect.style.display = 'block'; | |
| let options = []; | |
| if (group === 'Biology') options = subBioGroup; | |
| // Future: else if (group === 'CS') options = subCSGroup; | |
| options.forEach(s => { | |
| let opt = document.createElement('option'); | |
| opt.value = s; opt.innerText = s; | |
| subjectSelect.appendChild(opt); | |
| }); | |
| } | |
| function finishSetup() { | |
| userDetails.name = currentUser; | |
| if (!userDetails.medium) userDetails.medium = 'English'; | |
| let valid = true; | |
| if(userDetails.type === 'school') { | |
| userDetails.standard = document.getElementById('school-std').value; | |
| const std = userDetails.standard; | |
| if (['11th', '12th'].includes(std)) { | |
| userDetails.group = document.getElementById('school-group').value; // Save Group | |
| userDetails.subject = document.getElementById('school-subject-select').value; | |
| if(!userDetails.group || !userDetails.subject) valid = false; | |
| } else { | |
| userDetails.group = null; // No group for 1-10 | |
| userDetails.subject = document.getElementById('school-subject-select').value; | |
| if(!userDetails.standard || !userDetails.subject) valid = false; | |
| } | |
| } else { | |
| userDetails.dept = document.getElementById('college-dept').value; | |
| userDetails.year = document.getElementById('college-year').value; | |
| userDetails.sem = document.getElementById('college-sem').value; | |
| userDetails.subject = document.getElementById('college-subject').value.trim(); | |
| if(!userDetails.dept || !userDetails.subject) valid = false; | |
| } | |
| if(!valid) { | |
| alert("Please fill all details"); | |
| return; | |
| } | |
| localStorage.setItem("student_ai_user", currentUser); | |
| localStorage.setItem("student_details", JSON.stringify(userDetails)); | |
| document.getElementById("onboarding-overlay").classList.add('hidden'); | |
| setTimeout(() => document.getElementById("onboarding-overlay").style.display = 'none', 600); | |
| showApp(); | |
| } | |
| function shakeElement(id) { | |
| const el = document.getElementById(id); | |
| el.classList.add('input-error', 'shake'); | |
| setTimeout(() => el.classList.remove('shake'), 500); | |
| el.addEventListener('input', () => el.classList.remove('input-error'), {once:true}); | |
| } | |
| // 👇 STOP BUTTON LOGIC | |
| function toggleBtn(state) { | |
| const btn = document.querySelector('.send-btn'); | |
| if (state === 'sending') { | |
| btn.innerHTML = '<i class="fas fa-stop"></i>'; // Square Icon | |
| btn.onclick = stopGeneration; | |
| } else { | |
| btn.innerHTML = '<i class="fas fa-arrow-up"></i>'; // Arrow Icon | |
| btn.onclick = send; | |
| } | |
| } | |
| // 👇 UPDATED STOP FUNCTION (Instant Visual Feedback) | |
| async function stopGeneration() { | |
| if (abortController) abortController.abort(); | |
| isGenerating = false; | |
| toggleBtn('idle'); | |
| // 👇 உடனே ஸ்கிரீன்ல "Stopped" னு காட்ட இந்த வரியை சேர்க்கிறோம் | |
| const lastAiMsg = document.querySelector('.ai-msg:last-child .msg-bubble'); | |
| if (lastAiMsg && !lastAiMsg.innerHTML.includes("Stopped")) { | |
| lastAiMsg.innerHTML += ' <span style="color:orange; font-weight:bold; font-size:14px;">... [Stopped]</span>'; | |
| } | |
| // Backend Sync | |
| if (currentChatId && window.typeProgress < 1) { | |
| await fetch('/truncate_response', { | |
| method: 'POST', | |
| headers: {'Content-Type': 'application/json'}, | |
| body: JSON.stringify({ | |
| username: currentUser, | |
| chat_id: currentChatId, | |
| ratio: window.typeProgress || 0.1 | |
| }) | |
| }); | |
| } | |
| } | |
| // 👇 மொத்த ஹிஸ்டரியையும் அழிக்க (With Confirmation) | |
| function confirmClearHistory() { | |
| showModal("Are you sure? This will delete ALL chat history permanently.", false, async (confirmed) => { | |
| if (confirmed) { | |
| try { | |
| const res = await fetch('/clear_all_history', { | |
| method: 'POST', | |
| headers: {'Content-Type': 'application/json'}, | |
| body: JSON.stringify({ username: currentUser }) | |
| }); | |
| const data = await res.json(); | |
| if (data.status === "success") { | |
| document.getElementById('chat-box').innerHTML = ""; | |
| document.getElementById('history-list').innerHTML = ""; | |
| currentChatId = null; | |
| renderWelcomeScreen(); | |
| closeSubPage('subpage-chats'); | |
| closeSettings(); | |
| if(document.getElementById('sidebar').classList.contains('open')) toggleSidebar(); | |
| } else { | |
| alert("Error deleting chats."); | |
| } | |
| } catch (e) { | |
| console.error(e); | |
| alert("Something went wrong."); | |
| } | |
| } | |
| }); | |
| } | |
| // 3. APP CORE & SETTINGS LOGIC | |
| function checkLogin() { | |
| const stored = localStorage.getItem("student_ai_user"); | |
| if (stored) { | |
| currentUser = stored; | |
| userDetails = JSON.parse(localStorage.getItem("student_details") || "{}"); | |
| document.getElementById("onboarding-overlay").style.display = 'none'; | |
| showApp(); | |
| } | |
| const theme = localStorage.getItem('app_theme') || 'system'; | |
| setTheme(theme); | |
| } | |
| /* 👇 UPDATED WELCOME SCREEN (Old Header + New Professional Cards) 👇 */ | |
| function renderWelcomeScreen() { | |
| currentChatId = null; | |
| const chatBox = document.getElementById('chat-box'); | |
| // User Details | |
| const name = userDetails.name || "Student"; | |
| const sub = (userDetails.subject || "").toUpperCase(); // Subject | |
| const subLower = (userDetails.subject || "").toLowerCase(); | |
| const isTamil = userDetails.medium === "Tamil"; | |
| // --- 1. OLD LOGIC: Subject-based Suggestions ( chips ) --- | |
| let starters = ["Unit 1 Summary", "Important Questions"]; // Default | |
| // Subject-க்கு ஏற்ற கேள்விகள் | |
| if(subLower.includes('math')) starters = ["Matrix Multiplication", "Calculus Formulas", "Trigonometry"]; | |
| else if(subLower.includes('phy')) starters = ["Newton's Laws", "Optics Derivations", "Thermodynamics"]; | |
| else if(subLower.includes('chem')) starters = ["Organic Chemistry", "Periodic Table", "Chemical Bonding"]; | |
| else if(subLower.includes('botany')) starters = ["Plant Anatomy", "Photosynthesis", "Plant Tissue"]; | |
| else if(subLower.includes('zoology')) starters = ["Human Physiology", "Genetics", "Digestion System"]; | |
| else if(subLower.includes('bio')) starters = ["Cell Structure", "DNA Replication"]; | |
| else if(subLower.includes('tamil')) starters = ["திருக்குறள்", "இலக்கணம்", "செய்யுள்"]; | |
| else if(subLower.includes('english')) starters = ["Grammar Rules", "Essay Writing", "Poem Summary"]; | |
| else if(subLower.includes('computer') || subLower.includes('cse')) starters = ["Python Basics", "Data Structures", "DBMS Concepts"]; | |
| // Chips HTML உருவாக்குதல் | |
| let chipsHtml = `<div class="suggestion-container" style="justify-content:center; margin-top:15px; margin-bottom:30px;">`; | |
| starters.forEach(s => { | |
| chipsHtml += `<div class="suggestion-chip" onclick="document.getElementById('msg-input').value='${s}';send()">${s}</div>`; | |
| }); | |
| chipsHtml += `</div>`; | |
| // --- 2. NEW LOGIC: Purpose Cards (Bilingual) --- | |
| let welcomeText, sectionTitle, cardsData; | |
| if (isTamil) { | |
| // 👉 தமிழ் மீடியம் | |
| welcomeText = `வணக்கம் ${name}, <b>${sub}</b> படிக்கத் தயாரா?`; | |
| sectionTitle = "நான் எப்படி உதவுவேன்?"; | |
| cardsData = [ | |
| { | |
| icon: "fas fa-book-open", color: "#60a5fa", | |
| title: "பாடப்புத்தகம் மட்டும்", | |
| desc: "வேறெங்கும் தேடமாட்டேன். உங்கள் புத்தகத்திலிருந்து (Textbook) மட்டுமே சரியான பதிலை தருவேன்." | |
| }, | |
| { | |
| icon: "fas fa-comments", color: "#a78bfa", | |
| title: "எளிய விளக்கம்", | |
| desc: "பதில் புரியவில்லையா? <b>'பேச்சு வழக்குல சொல்லு'</b> என்று கேளுங்கள், நண்பன் போல விளக்குவேன்!" | |
| }, | |
| { | |
| icon: "fas fa-hand-holding-heart", color: "#34d399", | |
| title: "தயக்கம் வேண்டாம்", | |
| desc: "வகுப்பில் சந்தேகம் கேட்க கூச்சமா? இங்கே எந்த கேள்வியையும் தைரியமாக கேட்கலாம்." | |
| } | |
| ]; | |
| } else { | |
| // 👉 English Medium | |
| welcomeText = `Hi ${name}, Ready to study <b>${sub}</b>?`; | |
| sectionTitle = "How I can help you:"; | |
| cardsData = [ | |
| { | |
| icon: "fas fa-book-open", color: "#60a5fa", | |
| title: "Textbook Based", | |
| desc: "I don't guess. I answer strictly from your uploaded book with page citations." | |
| }, | |
| { | |
| icon: "fas fa-comments", color: "#a78bfa", | |
| title: "Simple Explanations", | |
| desc: "Don't understand? Ask me to <b>'Explain simply'</b> like a friend!" | |
| }, | |
| { | |
| icon: "fas fa-hand-holding-heart", color: "#34d399", | |
| title: "Ask Fearlessly", | |
| desc: "Hesitant to ask doubts in class? Feel free to ask anything here, privately." | |
| } | |
| ]; | |
| } | |
| // Cards HTML உருவாக்குதல் | |
| let cardsHtml = `<div class="features-grid">`; | |
| cardsData.forEach(card => { | |
| cardsHtml += ` | |
| <div class="feature-card"> | |
| <div class="feature-icon"><i class="${card.icon}" style="color:${card.color};"></i></div> | |
| <div class="feature-title">${card.title}</div> | |
| <div class="feature-desc">${card.desc}</div> | |
| </div>`; | |
| }); | |
| cardsHtml += `</div>`; | |
| // --- 3. FINAL ASSEMBLY (Old Top + New Bottom) --- | |
| chatBox.innerHTML = ` | |
| <div class="msg ai-msg" style="align-items:center; width:100%;"> | |
| <div class="ai-content" style="text-align:center; max-width:100%;"> | |
| <h1 style="margin-bottom:5px; font-size:24px;">${welcomeText}</h1> | |
| ${chipsHtml} | |
| <div style="width:50px; height:2px; background:var(--border); margin:0 auto 20px auto;"></div> | |
| <p style="font-size:13px; color:var(--text-muted); text-transform:uppercase; letter-spacing:1px; font-weight:600; margin-bottom:10px;"> | |
| ${sectionTitle} | |
| </p> | |
| ${cardsHtml} | |
| </div> | |
| </div>`; | |
| } | |
| /* 👇 UPDATED SHOW APP (Instant Welcome - No Delay) 👇 */ | |
| async function showApp() { | |
| // 1. Display User Name (உடனே நடக்கும்) | |
| document.getElementById("display-name").innerText = currentUser; | |
| // 2. Update Sidebar Profile (உடனே நடக்கும்) | |
| updateProfileUI(); | |
| // 3. 🔥 FAST FIX: Welcome Screen-ஐ முதலில் காட்டு! (Don't wait for history) | |
| const chatBox = document.getElementById("chat-box"); | |
| if(chatBox.innerHTML.trim() === "") { | |
| renderWelcomeScreen(); // இது Local-ல் நடப்பதால் கண்ணிமைக்கும் நேரத்தில் வரும்! | |
| } | |
| // 4. Load History in Background (இது மெதுவா நடந்தாலும் பரவாயில்லை) | |
| await loadHistory(); | |
| } | |
| // 👆👆👆 மாற்றம் முடிந்தது 👆👆👆 | |
| function openSettings() { | |
| document.getElementById('settings-overlay').classList.add('active'); | |
| if(document.getElementById('sidebar').classList.contains('open')) toggleSidebar(); | |
| updateProfileUI(); | |
| } | |
| function closeSettings() { document.getElementById('settings-overlay').classList.remove('active'); } | |
| function updateProfileUI() { | |
| document.getElementById('profile-name').innerText = currentUser; | |
| document.getElementById('profile-edu').innerText = userDetails.type === 'school' ? 'School' : 'College'; | |
| document.getElementById('profile-std').innerText = userDetails.type === 'school' ? userDetails.standard : userDetails.dept; | |
| document.getElementById('profile-sub').innerText = userDetails.subject; | |
| document.getElementById('profile-med').innerText = userDetails.medium || 'English'; | |
| const pic = localStorage.getItem('profile_pic'); | |
| if(pic) { | |
| document.getElementById('settings-pic').src = pic; | |
| document.querySelector('.user-info-text').innerHTML = `<img src="${pic}" style="width:30px;height:30px;border-radius:50%;margin-right:10px;vertical-align:middle"> ${currentUser}`; | |
| } | |
| } | |
| function uploadProfilePic(input) { | |
| if (input.files && input.files[0]) { | |
| const reader = new FileReader(); | |
| reader.onload = function(e) { | |
| localStorage.setItem('profile_pic', e.target.result); | |
| updateProfileUI(); | |
| } | |
| reader.readAsDataURL(input.files[0]); | |
| } | |
| } | |
| /* 👇 UPDATED EDIT SUBJECT (No Alerts, Silent Update) 👇 */ | |
| function editSubject(el) { | |
| const currentSub = el.innerText; | |
| const std = userDetails.standard || ""; | |
| // 1. Dropdown Logic | |
| const select = document.createElement('select'); | |
| select.className = 'subject-edit-input'; | |
| select.style.width = "auto"; | |
| select.style.maxWidth = "150px"; | |
| let options = []; | |
| // 6-10th vs 11-12th Subjects | |
| const sub1to10 = ["Tamil", "English", "Maths", "Science", "Social Science"]; | |
| const subBioGroup = ["Tamil", "English", "Maths", "Physics", "Chemistry", "Botany", "Zoology"]; | |
| if (['11th', '12th'].includes(std)) { | |
| options = subBioGroup; | |
| } else if (userDetails.type === 'school') { | |
| options = sub1to10; | |
| } else { | |
| // College: Text Input (No Dropdown) | |
| const input = document.createElement('input'); | |
| input.value = currentSub; | |
| input.className = 'subject-edit-input'; | |
| el.replaceWith(input); | |
| input.focus(); | |
| const saveCollege = () => { | |
| const newVal = input.value.trim(); | |
| if (newVal && newVal !== currentSub) { | |
| userDetails.subject = newVal; | |
| localStorage.setItem("student_details", JSON.stringify(userDetails)); | |
| const span = document.createElement('span'); | |
| span.className = 'detail-value editable-subject'; | |
| span.id = 'profile-sub'; | |
| span.onclick = function() { editSubject(this) }; | |
| span.innerText = newVal; | |
| input.replaceWith(span); | |
| // 🔥 NO ALERT: Silent Update | |
| if(typeof renderWelcomeScreen === 'function') renderWelcomeScreen(); | |
| } else { | |
| revert(input, currentSub); | |
| } | |
| }; | |
| input.onblur = saveCollege; | |
| input.onkeydown = (e) => { if(e.key === 'Enter') saveCollege(); } | |
| return; | |
| } | |
| // Fill Dropdown Options | |
| options.forEach(s => { | |
| let opt = document.createElement('option'); | |
| opt.value = s; opt.innerText = s; | |
| if(s === currentSub) opt.selected = true; | |
| select.appendChild(opt); | |
| }); | |
| el.replaceWith(select); | |
| select.focus(); | |
| // UI-ஐ பழைய நிலைக்கு மாற்றும் உதவி ஃபங்ஷன் | |
| function revert(elem, val) { | |
| const span = document.createElement('span'); | |
| span.className = 'detail-value editable-subject'; | |
| span.id = 'profile-sub'; | |
| span.onclick = function() { editSubject(this) }; | |
| span.innerText = val; | |
| elem.replaceWith(span); | |
| } | |
| // Save Function | |
| const saveEdit = () => { | |
| const newVal = select.value; | |
| if (newVal && newVal !== currentSub) { | |
| userDetails.subject = newVal; | |
| localStorage.setItem("student_details", JSON.stringify(userDetails)); | |
| revert(select, newVal); | |
| // 🔥 முக்கிய மாற்றம்: Alert நீக்கப்பட்டது! Silent Update மட்டும் நடக்கும். | |
| if(typeof renderWelcomeScreen === 'function') renderWelcomeScreen(); | |
| } else { | |
| revert(select, currentSub); | |
| } | |
| }; | |
| // Select செய்தாலே Save ஆகிவிடும் | |
| select.onchange = saveEdit; | |
| select.onblur = saveEdit; | |
| } | |
| function setTheme(theme) { | |
| localStorage.setItem('app_theme', theme); | |
| document.body.classList.remove('light-mode'); | |
| if (theme === 'light' || (theme === 'system' && !window.matchMedia('(prefers-color-scheme: dark)').matches)) { | |
| document.body.classList.add('light-mode'); | |
| } | |
| } | |
| function handleLogout() { localStorage.clear(); location.reload(); } | |
| // 4. ATTACHMENT & UTILITY LOGIC | |
| function toggleAttachMenu() { | |
| const menu = document.getElementById('attach-menu'); | |
| menu.style.display = (menu.style.display === 'none' || menu.style.display === '') ? 'flex' : 'none'; | |
| } | |
| function handleFile(input) { | |
| if (input.files && input.files[0]) { | |
| const reader = new FileReader(); | |
| reader.onload = function(e) { | |
| window.currentFile = e.target.result; | |
| document.getElementById('preview-img').src = e.target.result; | |
| document.getElementById('preview-box').style.display = 'block'; | |
| document.getElementById('attach-menu').style.display = 'none'; | |
| }; | |
| reader.readAsDataURL(input.files[0]); | |
| } | |
| } | |
| function clearFile() { | |
| window.currentFile = null; | |
| document.getElementById('preview-box').style.display = 'none'; | |
| document.querySelectorAll('input[type="file"]').forEach(el => el.value = ""); | |
| } | |
| document.addEventListener('click', function(e) { | |
| const menu = document.getElementById('attach-menu'); | |
| const btn = document.querySelector('.plus-btn'); | |
| if (menu && menu.style.display === 'flex' && !menu.contains(e.target) && !btn.contains(e.target)) { | |
| menu.style.display = 'none'; | |
| } | |
| }); | |
| /* --- 1. SMOOTH TYPEWRITER LOGIC --- */ | |
| /* --- PROFESSIONAL MARKDOWN-AWARE TYPEWRITER --- */ | |
| // 👇 UPDATED TYPEWRITER (With Speaker Button 🔊) | |
| typeWriter = function(element, text, callback) { | |
| const chatBox = document.getElementById('chat-box'); | |
| let i = 0; | |
| window.typeProgress = 0; | |
| element.innerHTML = marked.parse(text); | |
| const finalHTML = element.innerHTML; | |
| element.innerHTML = ""; | |
| element.style.minHeight = "20px"; | |
| function type() { | |
| if (!isGenerating) return; | |
| if (finalHTML.length > 0) window.typeProgress = i / finalHTML.length; | |
| if (i < finalHTML.length) { | |
| // ... (Typing Logic same as before) ... | |
| if (finalHTML.charAt(i) === '<') { | |
| let tagEnd = finalHTML.indexOf('>', i); | |
| i = tagEnd + 1; | |
| } else { | |
| i += 3; | |
| } | |
| element.innerHTML = finalHTML.substring(0, i); | |
| chatBox.scrollTop = chatBox.scrollHeight; | |
| requestAnimationFrame(type); | |
| } else { | |
| element.innerHTML = finalHTML; | |
| window.typeProgress = 1; | |
| // ... (Colors & Copy Logic) ... | |
| element.querySelectorAll('pre code').forEach((block) => hljs.highlightElement(block)); | |
| // ... (Copy button logic) ... | |
| // 👇👇👇 முக்கியம்: இந்த பகுதி இருக்கானு பாருங்க 👇👇👇 | |
| // Clean text for speech | |
| const cleanTextForSpeech = text.replace(/[*#`]/g, ''); | |
| const safeSpeechText = cleanTextForSpeech.replace(/"/g, '"').replace(/'/g, "\\'"); | |
| const actionsHtml = ` | |
| <div class="msg-actions" style="margin-top:10px; display:flex; gap:10px;"> | |
| <div class="action-icon" onclick="speakText('${safeSpeechText}')"><i class="fas fa-volume-up"></i> Listen</div> | |
| <div class="action-icon" onclick="copyText(this, \`${text.replace(/`/g, '\\`').replace(/"/g, '"')}\`)"><i class="fas fa-copy"></i> Copy</div> | |
| <div class="action-icon" onclick="regenerateLast()"><i class="fas fa-sync-alt"></i> Regen</div> | |
| </div>`; | |
| element.insertAdjacentHTML('beforeend', actionsHtml); | |
| // 👆👆👆 இந்த கோட் இருந்தால் தான் பட்டன் வரும்! 👆👆👆 | |
| if (window.mermaid && text.includes("```mermaid")) mermaid.run({ nodes: [element] }); | |
| chatBox.scrollTop = chatBox.scrollHeight; | |
| if (callback) callback(); | |
| } | |
| } | |
| type(); | |
| }; | |
| function copyText(btn, text) { | |
| navigator.clipboard.writeText(text).then(() => { | |
| const originalIcon = btn.innerHTML; | |
| btn.innerHTML = '<i class="fas fa-check" style="color:#4ade80;"></i> Copied'; | |
| setTimeout(() => { btn.innerHTML = originalIcon; }, 2000); | |
| }); | |
| } | |
| function shareContent(text) { | |
| if (navigator.share) { | |
| navigator.share({ title: 'Student AI', text: text }).catch(console.error); | |
| } else { | |
| alert("Sharing not supported. Text copied."); | |
| } | |
| } | |
| // 5. CHAT HANDLING LOGIC | |
| /* --- TOGGLE SIDEBAR (FIX: HIDE CHAT INPUT) --- */ | |
| function toggleSidebar() { | |
| const sb = document.getElementById('sidebar'); | |
| const chatInput = document.querySelector('.input-wrapper'); // Chat Bar Element | |
| sb.classList.toggle('open'); | |
| if(sb.classList.contains('open')) { | |
| // மெனு திறக்கும்போது: | |
| // 1. ஹிஸ்டரி சேர்க்கிறோம் (Back button support) | |
| history.pushState({menu: 'open'}, null, ""); | |
| // 2. 👇 Chat Bar-ஐ மறைக்கிறோம் (Keyboard issue fix) | |
| if(chatInput) chatInput.style.display = 'none'; | |
| } else { | |
| // மெனு மூடும்போது Chat Bar மீண்டும் வரும் | |
| if(chatInput) chatInput.style.display = 'block'; | |
| } | |
| } | |
| function editMessage(text) { | |
| const inputEl = document.getElementById('msg-input'); | |
| inputEl.value = text; | |
| inputEl.focus(); | |
| inputEl.style.height = 'auto'; | |
| inputEl.style.height = inputEl.scrollHeight + 'px'; | |
| } | |
| async function regenerateLast() { | |
| const userMsgs = document.querySelectorAll('.user-content'); | |
| if (userMsgs.length === 0) return; | |
| const lastMsgText = userMsgs[userMsgs.length - 1].innerText; | |
| const aiMsgs = document.querySelectorAll('.ai-msg'); | |
| if (aiMsgs.length > 0) aiMsgs[aiMsgs.length - 1].remove(); | |
| document.getElementById('msg-input').value = lastMsgText; | |
| send(); | |
| } | |
| /* 👇 UPDATED ADD MSG (Buttons Fixed) 👇 */ | |
| function addMsg(role, text, img) { | |
| const box = document.getElementById('chat-box'); | |
| let contentHtml = ""; | |
| if (img) contentHtml += `<img src="${img}" class="chat-img">`; | |
| let cleanText = text; | |
| let suggestions = []; | |
| if (role === 'ai') { | |
| const match = text.match(/<<SUGGEST:(.*?)>>/); | |
| if (match) { | |
| cleanText = text.replace(match[0], ""); | |
| suggestions = match[1].split('|').map(s => s.trim()); | |
| } | |
| } | |
| if (role === 'ai') { | |
| contentHtml += `<div class="ai-content">${formatText(cleanText)}</div>`; | |
| } else { | |
| contentHtml += `<div class="user-content">${cleanText}</div>`; | |
| } | |
| const safeText = cleanText.replace(/`/g, '\\`').replace(/"/g, '"'); | |
| let actionsHtml = ""; | |
| if (role === 'user') { | |
| actionsHtml = ` | |
| <div class="msg-actions" style="justify-content: flex-end;"> | |
| <div class="action-icon" onclick="copyText(this, \`${safeText}\`)"><i class="fas fa-copy"></i> Copy</div> | |
| <div class="action-icon" onclick="editMessage(\`${safeText}\`)"><i class="fas fa-pen"></i> Edit</div> | |
| </div>`; | |
| } else { | |
| // 👇👇👇 இங்கே மாற்றம் (Listen நீக்கப்பட்டது & வரிசை சரிசெய்யப்பட்டது) 👇👇👇 | |
| actionsHtml = ` | |
| <div class="msg-actions" style="margin-top:10px; display:flex; gap:15px; align-items:center;"> | |
| <div class="action-icon" style="color:#a78bfa; font-weight:600; cursor:pointer;" onclick="startContextQuiz(this)"> | |
| <i class="fas fa-brain"></i> Test Yourself | |
| </div> | |
| <div class="action-icon" onclick="copyText(this, \`${safeText}\`)"><i class="fas fa-copy"></i> Copy</div> | |
| <div class="action-icon" onclick="regenerateLast()"><i class="fas fa-sync-alt"></i> Regen</div> | |
| <div class="action-icon" onclick="shareContent(\`${safeText}\`)"><i class="fas fa-share-alt"></i> Share</div> | |
| </div>`; | |
| // 👆👆👆 ---------------------------------------------------- 👆👆👆 | |
| } | |
| const msgDiv = document.createElement('div'); | |
| msgDiv.className = `msg ${role === 'user' ? 'user-msg' : 'ai-msg'}`; | |
| msgDiv.innerHTML = `<div class="msg-bubble" style="${role==='ai'?'background:transparent;padding:0;':''}">${contentHtml}</div>${actionsHtml}`; | |
| if (suggestions.length > 0) { | |
| const chipsDiv = document.createElement('div'); | |
| chipsDiv.className = 'suggestion-container'; | |
| suggestions.forEach(topic => { | |
| const chip = document.createElement('div'); | |
| chip.className = 'suggestion-chip'; | |
| chip.innerText = topic; | |
| chip.onclick = () => { document.getElementById('msg-input').value = topic; send(); }; | |
| chipsDiv.appendChild(chip); | |
| }); | |
| msgDiv.appendChild(chipsDiv); | |
| } | |
| box.appendChild(msgDiv); | |
| msgDiv.querySelectorAll('pre code').forEach((block) => hljs.highlightElement(block)); | |
| if (window.MathJax && role === 'ai') { | |
| MathJax.typesetPromise([msgDiv]).catch((err) => console.log(err)); | |
| } | |
| box.scrollTo(0, box.scrollHeight); | |
| } | |
| /* --- 🎙️ UPDATED VOICE FUNCTION (With Error Alerts) --- */ | |
| function toggleVoice() { | |
| // 1. Browser Support Check | |
| if (!('webkitSpeechRecognition' in window)) { | |
| alert("⚠️ Voice input not supported. Please use Google Chrome."); | |
| return; | |
| } | |
| const micBtn = document.getElementById('mic-btn'); | |
| const inputEl = document.getElementById('msg-input'); | |
| // 2. Stop if already listening | |
| if (window.recognition && window.isListening) { | |
| window.recognition.stop(); | |
| return; | |
| } | |
| // 3. Start New Recognition | |
| const recognition = new webkitSpeechRecognition(); | |
| recognition.lang = 'en-US'; // தமிழுக்கு 'ta-IN' போடலாம் | |
| recognition.interimResults = false; | |
| recognition.maxAlternatives = 1; | |
| recognition.onstart = () => { | |
| window.isListening = true; | |
| micBtn.classList.add('listening'); // Red Pulse Effect | |
| inputEl.placeholder = "Listening... Speak now..."; | |
| }; | |
| recognition.onend = () => { | |
| window.isListening = false; | |
| micBtn.classList.remove('listening'); | |
| inputEl.placeholder = "Message..."; | |
| }; | |
| recognition.onresult = (event) => { | |
| const speechResult = event.results[0][0].transcript; | |
| inputEl.value += (inputEl.value ? " " : "") + speechResult; | |
| // Auto resize textarea | |
| inputEl.style.height = 'auto'; | |
| inputEl.style.height = inputEl.scrollHeight + 'px'; | |
| }; | |
| // 👇👇👇 முக்கிய மாற்றம்: எர்ரர் வந்தால் அலர்ட் வரும் 👇👇👇 | |
| recognition.onerror = (event) => { | |
| console.error("Mic Error:", event.error); | |
| if (event.error === 'not-allowed') { | |
| alert("🚫 Mic Permission Denied! Please allow microphone access in Settings."); | |
| } else if (event.error === 'no-speech') { | |
| alert("🔇 No speech detected. Please speak louder."); | |
| } else { | |
| alert("⚠️ Mic Error: " + event.error); | |
| } | |
| micBtn.classList.remove('listening'); | |
| }; | |
| // 👆👆👆 மாற்றம் முடிந்தது 👆👆👆 | |
| window.recognition = recognition; | |
| recognition.start(); | |
| } | |
| // 1. பேசுறதுக்கான ஃபங்ஷன் (இதை typeWriter-க்கு மேலே தனியா போடுங்க) | |
| function speakText(txt) { | |
| window.speechSynthesis.cancel(); // பழைய பேச்சை நிறுத்து | |
| const utterance = new SpeechSynthesisUtterance(txt); | |
| utterance.lang = 'en-US'; // தமிழுக்கு 'ta-IN' | |
| utterance.rate = 1; | |
| window.speechSynthesis.speak(utterance); | |
| } | |
| /* --- UPDATED SEND FUNCTION (Fixes Listen Button Disappearing) --- */ | |
| async function send() { | |
| if (isGenerating) return; | |
| const inputEl = document.getElementById('msg-input'); | |
| const txt = inputEl.value.trim(); | |
| const fileData = window.currentFile; | |
| if (!txt && !fileData) return; | |
| // UI Reset | |
| inputEl.value = ""; | |
| inputEl.style.height = 'auto'; | |
| document.getElementById('preview-box').style.display = 'none'; | |
| window.currentFile = null; | |
| // User Message Add | |
| addMsg('user', txt, fileData); | |
| // Create AI Bubble | |
| const msgId = "ai-" + Date.now(); | |
| const chatBox = document.getElementById('chat-box'); | |
| chatBox.insertAdjacentHTML('beforeend', | |
| `<div id="${msgId}" class="msg ai-msg"><div class="msg-bubble" style="color:var(--text-muted);">Thinking...</div></div>` | |
| ); | |
| chatBox.scrollTo(0, chatBox.scrollHeight); | |
| // Start Generation | |
| isGenerating = true; | |
| toggleBtn('sending'); | |
| abortController = new AbortController(); | |
| try { | |
| if (!currentChatId) { | |
| const r = await fetch('/new_chat', {method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify({username:currentUser})}); | |
| const d = await r.json(); currentChatId = d.chat_id; loadHistory(); | |
| } | |
| const res = await fetch('/chat', { | |
| method: 'POST', | |
| headers: {'Content-Type': 'application/json'}, | |
| signal: abortController.signal, | |
| body: JSON.stringify({ | |
| message: txt, | |
| image: fileData, | |
| username: currentUser, | |
| chat_id: currentChatId, | |
| user_details: userDetails | |
| }) | |
| }); | |
| const data = await res.json(); | |
| loadHistory(); | |
| if (data.title) { | |
| // டைட்டில் வந்தா உடனே ஹிஸ்டரியை ரீஃப்ரெஷ் செய்! | |
| const chatItem = document.querySelector(`.history-item[onclick*="${currentChatId}"] span`); | |
| if (chatItem) chatItem.innerText = data.title; | |
| } | |
| loadHistory(); | |
| const aiDiv = document.getElementById(msgId); | |
| aiDiv.innerHTML = ""; | |
| const bubble = document.createElement('div'); | |
| bubble.className = "msg-bubble"; | |
| aiDiv.appendChild(bubble); | |
| // 👇👇👇 FIX START: Suggestions-ஐ முதலிலேயே பிரிக்கிறோம் 👇👇👇 | |
| let fullText = data.response; | |
| let cleanText = fullText; | |
| let suggestions = []; | |
| const match = fullText.match(/<<SUGGEST:(.*?)>>/); | |
| if (match) { | |
| cleanText = fullText.replace(match[0], ""); // Tag-ஐ நீக்குகிறோம் | |
| suggestions = match[1].split('|').map(s => s.trim()); | |
| } | |
| // 👇 Clean Text-ஐ மட்டும் டைப் செய்ய அனுப்புகிறோம் | |
| typeWriter(bubble, cleanText, () => { | |
| if(isGenerating) { | |
| isGenerating = false; | |
| toggleBtn('idle'); | |
| } | |
| // 👇 Chips-ஐ Bubble-க்கு வெளியே சேர்க்கிறோம் (Overwrite பண்ணாமல்!) | |
| if (suggestions.length > 0) { | |
| const chipsDiv = document.createElement('div'); | |
| chipsDiv.className = 'suggestion-container'; | |
| suggestions.forEach(topic => { | |
| const chip = document.createElement('div'); | |
| chip.className = 'suggestion-chip'; | |
| chip.innerText = topic; | |
| chip.onclick = () => { document.getElementById('msg-input').value = topic; send(); }; | |
| chipsDiv.appendChild(chip); | |
| }); | |
| if(aiDiv) aiDiv.appendChild(chipsDiv); | |
| } | |
| // Note: Action Buttons (Copy/Listen) இப்போது typeWriter-க்குள்ளேயே இருப்பதால், | |
| // இங்கே மீண்டும் சேர்க்க தேவையில்லை. (Double Buttons வராது). | |
| chatBox.scrollTop = chatBox.scrollHeight; | |
| }); | |
| // 👆👆👆 FIX END 👆👆👆 | |
| } catch (e) { | |
| if (e.name === 'AbortError') { | |
| document.getElementById(msgId).innerHTML = '<div class="msg-bubble" style="color:orange;">⏹️ Stopped.</div>'; | |
| } else { | |
| document.getElementById(msgId).innerHTML = '<div class="msg-bubble" style="color:red;">Error.</div>'; | |
| } | |
| isGenerating = false; | |
| toggleBtn('idle'); | |
| } | |
| } | |
| // 👇 LONG PRESS LOGIC (Desktop & Mobile) 👇 | |
| let pressTimer; | |
| function handleLongPress(element) { | |
| // மற்ற எல்லா மெனுவையும் மூடிவிடு | |
| document.querySelectorAll('.history-item').forEach(el => el.classList.remove('active-options')); | |
| // இதில் மட்டும் மெனுவை காட்டு | |
| element.classList.add('active-options'); | |
| } | |
| // History List-ஐ உருவாக்கும்போது இதை இணைக்கிறோம் | |
| async function loadHistory() { | |
| const res = await fetch('/get_history', {method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify({username:currentUser})}); | |
| const data = await res.json(); | |
| const list = document.getElementById('history-list'); list.innerHTML = ""; | |
| if(data.chats) { | |
| // Timestamp படி வரிசைப்படுத்துதல் | |
| const sortedChats = Object.entries(data.chats).sort(([,a], [,b]) => { | |
| return (b.timestamp || 0) - (a.timestamp || 0); | |
| }); | |
| sortedChats.forEach(([cid, chat]) => { | |
| // 👇 HTML உருவாக்கம் (Events உடன்) | |
| const item = document.createElement('div'); | |
| item.className = 'history-item'; | |
| item.innerHTML = ` | |
| <span style="flex:1; overflow:hidden; text-overflow:ellipsis; white-space:nowrap;">${chat.title}</span> | |
| <div class="history-actions"> | |
| <i class="fas fa-pen hist-icon" onclick="event.stopPropagation(); renameChat('${cid}')"></i> | |
| <i class="fas fa-trash hist-icon" style="color:#ef4444;" onclick="event.stopPropagation(); deleteChat('${cid}')"></i> | |
| </div>`; | |
| // 👇 Click Event (சாதாரணமாக தொட்டால் Chat லோட் ஆகும்) | |
| item.onclick = (e) => { | |
| if(!item.classList.contains('active-options')) { | |
| loadChat(cid); | |
| } | |
| }; | |
| // 👇 Touch Start (மொபைல் Long Press) | |
| item.ontouchstart = (e) => { | |
| pressTimer = setTimeout(() => handleLongPress(item), 600); // 600ms பிடித்தால் மெனு வரும் | |
| }; | |
| item.ontouchend = () => clearTimeout(pressTimer); | |
| item.ontouchmove = () => clearTimeout(pressTimer); // விரலை நகர்த்தினால் கேன்சல் | |
| // 👇 Mouse Down (கம்ப்யூட்டர் Long Click) | |
| item.onmousedown = () => { | |
| pressTimer = setTimeout(() => handleLongPress(item), 600); | |
| }; | |
| item.onmouseup = () => clearTimeout(pressTimer); | |
| item.onmouseleave = () => clearTimeout(pressTimer); | |
| list.appendChild(item); | |
| }); | |
| } | |
| } | |
| // 👇 திரை வேறு எங்காவது தொட்டால் மெனுவை மூடு | |
| document.addEventListener('click', function(e) { | |
| if (!e.target.closest('.history-item')) { | |
| document.querySelectorAll('.history-item').forEach(el => el.classList.remove('active-options')); | |
| } | |
| }); | |
| async function loadChat(cid) { | |
| currentChatId = cid; | |
| toggleSidebar(); | |
| const res = await fetch('/get_chat', { | |
| method:'POST', headers:{'Content-Type':'application/json'}, | |
| body:JSON.stringify({username:currentUser, chat_id:cid}) | |
| }); | |
| const d = await res.json(); | |
| document.getElementById('chat-box').innerHTML = ""; | |
| d.messages.forEach(m => addMsg(m.role === 'user' ? 'user' : 'ai', m.content)); | |
| } | |
| function showModal(title, isInput, callback) { | |
| const modal = document.getElementById('custom-modal') || createModalElement(); | |
| document.getElementById('m-title').innerText = title; | |
| const inp = document.getElementById('m-inp'); | |
| inp.style.display = isInput ? 'block' : 'none'; | |
| inp.value = ""; | |
| modal.style.display = 'flex'; | |
| window.modalCallback = (confirm) => { | |
| modal.style.display = 'none'; | |
| if(confirm) callback(isInput ? inp.value : true); | |
| }; | |
| } | |
| function createModalElement() { | |
| const div = document.createElement('div'); | |
| div.id = 'custom-modal'; | |
| div.innerHTML = `<div class="modal-content"><h3 id="m-title"></h3><input id="m-inp" class="modal-input"><div class="modal-btns"><button class="m-btn" style="background:#333;color:#fff" onclick="modalCallback(false)">Cancel</button><button class="m-btn" style="background:#fff;color:#000" onclick="modalCallback(true)">Confirm</button></div></div>`; | |
| document.body.appendChild(div); | |
| return div; | |
| } | |
| async function renameChat(cid) { | |
| showModal("Rename Chat", true, async (newTitle) => { | |
| if(newTitle) { | |
| await fetch('/rename_chat', {method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify({username:currentUser, chat_id:cid, title:newTitle})}); | |
| loadHistory(); | |
| } | |
| }); | |
| } | |
| async function deleteChat(cid) { | |
| showModal("Delete this chat?", false, async () => { | |
| await fetch('/delete_chat', {method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify({username:currentUser, chat_id:cid})}); | |
| loadHistory(); | |
| if(currentChatId === cid) document.getElementById('chat-box').innerHTML = ""; | |
| }); | |
| } | |
| function newChat() { | |
| renderWelcomeScreen(); // புது லாஜிக்கை கூப்பிடுகிறது | |
| toggleSidebar(); // இது Sidebar பட்டன் கிளிக் செய்யும்போது மட்டும் தேவை | |
| } | |
| // FIXED SEARCH & CLEAR LOGIC | |
| function filterHistory(query) { | |
| const items = document.querySelectorAll('.history-item'); | |
| const clearBtn = document.getElementById('clear-search'); | |
| clearBtn.style.display = query.length > 0 ? 'block' : 'none'; | |
| items.forEach(item => { | |
| const title = item.querySelector('span').innerText.toLowerCase(); | |
| item.style.display = title.includes(query.toLowerCase()) ? 'flex' : 'none'; | |
| }); | |
| } | |
| function clearSearch() { | |
| const input = document.getElementById('hist-search'); | |
| input.value = ""; | |
| filterHistory(""); | |
| input.focus(); | |
| } | |
| // --- SETTINGS NAVIGATION --- | |
| function openSubPage(pageId) { | |
| document.getElementById(pageId).classList.add('active'); | |
| } | |
| function closeSubPage(pageId) { | |
| document.getElementById(pageId).classList.remove('active'); | |
| } | |
| /* ========================================= | |
| 🚀 GLOBAL NAVIGATION & INPUT SUPPORT | |
| ========================================= */ | |
| // 1. MOBILE BACK BUTTON HANDLE (History Management) | |
| window.onpopstate = function(event) { | |
| // A. சப்-பேஜ் திறந்திருந்தால் (Student Details / Themes) | |
| const activeSubPage = document.querySelector('.settings-sub-page.active'); | |
| if (activeSubPage) { | |
| activeSubPage.classList.remove('active'); | |
| return; // இங்கே நிறுத்தவும், ஆப் வெளியே போகாது | |
| } | |
| // B. செட்டிங்ஸ் திறந்திருந்தால் | |
| const settings = document.getElementById('settings-overlay'); | |
| if (settings && settings.classList.contains('active')) { | |
| settings.classList.remove('active'); | |
| return; | |
| } | |
| // C. மெனு திறந்திருந்தால் | |
| const sidebar = document.getElementById('sidebar'); | |
| if (sidebar && sidebar.classList.contains('open')) { | |
| toggleSidebar(); // மெனுவை மூடும் | |
| return; | |
| } | |
| // D. ஆன்-போர்டிங் (Onboarding) ஸ்டெப்ஸ் பின்னோக்கி செல்ல | |
| if (event.state && event.state.step) { | |
| document.querySelectorAll('.step-content').forEach(el => el.classList.remove('active')); | |
| document.getElementById('step-' + event.state.step).classList.add('active'); | |
| } | |
| }; | |
| // 2. OPEN FUNCTIONS WITH HISTORY PUSH | |
| // (இதை பழைய function-க்கு பதில் மாற்றுங்கள்) | |
| function openSettings() { | |
| document.getElementById('settings-overlay').classList.add('active'); | |
| history.pushState({view: 'settings'}, null, ""); // ஹிஸ்டரி சேர்ப்பு | |
| // மெனு திறந்திருந்தால் மூடிவிடு | |
| const sb = document.getElementById('sidebar'); | |
| if(sb.classList.contains('open')) toggleSidebar(); | |
| } | |
| function openSubPage(pageId) { | |
| document.getElementById(pageId).classList.add('active'); | |
| history.pushState({view: 'subpage'}, null, ""); // சப்-பேஜ் ஹிஸ்டரி | |
| } | |
| function closeSettings() { | |
| // Back பட்டன் அழுத்தினால் தானாக மூடும், இருந்தாலும் Manual Close-க்கு: | |
| if(history.state && history.state.view === 'settings') history.back(); | |
| else document.getElementById('settings-overlay').classList.remove('active'); | |
| } | |
| function closeSubPage(pageId) { | |
| if(history.state && history.state.view === 'subpage') history.back(); | |
| else document.getElementById(pageId).classList.remove('active'); | |
| } | |
| /* --- UNIVERSAL ENTER KEY & KEYBOARD HIDE FIX --- */ | |
| document.addEventListener("DOMContentLoaded", function() { | |
| // 1. Chat Input: Send & Hide Keyboard | |
| const msgInput = document.getElementById('msg-input'); | |
| if(msgInput) { | |
| msgInput.addEventListener('keydown', function(e) { | |
| if (e.key === 'Enter' && !e.shiftKey) { | |
| e.preventDefault(); | |
| send(); | |
| this.blur(); // 👇 இதுதான் கீபோர்டை கீழே தள்ளும்! | |
| } | |
| }); | |
| } | |
| // 2. Onboarding Name | |
| const nameInput = document.getElementById('name-input'); | |
| if(nameInput) { | |
| nameInput.addEventListener('keydown', function(e) { | |
| if (e.key === 'Enter') { | |
| nextStep(3); | |
| this.blur(); | |
| } | |
| }); | |
| } | |
| // 3. Subject Inputs (UPDATED: Goes to Step 4) | |
| ['school-subject', 'college-subject'].forEach(id => { | |
| const el = document.getElementById(id); | |
| if(el) { | |
| el.addEventListener('keydown', function(e) { | |
| if (e.key === 'Enter') { | |
| nextStep(4); // ✅ இப்போ Enter தட்டினால் Medium Page போகும்! | |
| this.blur(); | |
| } | |
| }); | |
| } | |
| }); | |
| }); | |
| // --- SETTINGS SEARCH LOGIC --- | |
| function filterSettings(query) { | |
| const btns = document.querySelectorAll('.settings-option-btn'); | |
| const clearBtn = document.getElementById('clear-setting-search'); | |
| if(clearBtn) clearBtn.style.display = query.length > 0 ? 'block' : 'none'; | |
| btns.forEach(btn => { | |
| const text = btn.innerText.toLowerCase(); | |
| btn.style.display = text.includes(query.toLowerCase()) ? 'flex' : 'none'; | |
| }); | |
| } | |
| function clearSettingsSearch() { | |
| const inp = document.getElementById('setting-search-input'); | |
| inp.value = ""; | |
| filterSettings(""); | |
| inp.blur(); // கீபோர்டு மறைய | |
| } | |
| // 7. INITIALIZE APP | |
| checkLogin(); | |
| </script> | |
| <script> | |
| // 1. Add Highlight.js for Colors | |
| const link = document.createElement('link'); | |
| link.rel = 'stylesheet'; | |
| link.href = 'https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/atom-one-dark.min.css'; | |
| document.head.appendChild(link); | |
| /* 👇 CLEAN TYPEWRITER (Test Yourself பட்டன் நீக்கப்பட்டது) 👇 */ | |
| typeWriter = function(element, text, callback) { | |
| const chatBox = document.getElementById('chat-box'); | |
| let i = 0; | |
| window.typeProgress = 0; | |
| // Markdown & Math Formatting | |
| element.innerHTML = formatText(text); | |
| const finalHTML = element.innerHTML; | |
| element.innerHTML = ""; | |
| element.style.minHeight = "20px"; | |
| function type() { | |
| if (!isGenerating) return; | |
| if (finalHTML.length > 0) window.typeProgress = i / finalHTML.length; | |
| if (i < finalHTML.length) { | |
| if (finalHTML.charAt(i) === '<') { | |
| let tagEnd = finalHTML.indexOf('>', i); | |
| i = tagEnd + 1; | |
| } else { | |
| i += 3; // Typing Speed | |
| } | |
| element.innerHTML = finalHTML.substring(0, i); | |
| chatBox.scrollTop = chatBox.scrollHeight; | |
| requestAnimationFrame(type); | |
| } else { | |
| // Typing Finished | |
| element.innerHTML = finalHTML; | |
| window.typeProgress = 1; | |
| // Highlight Code Blocks | |
| element.querySelectorAll('pre code').forEach((block) => hljs.highlightElement(block)); | |
| element.querySelectorAll('pre').forEach(pre => { | |
| if (pre.querySelector('.code-copy-btn')) return; | |
| pre.style.position = 'relative'; | |
| const btn = document.createElement('button'); | |
| btn.className = 'code-copy-btn'; | |
| btn.innerHTML = '<i class="fas fa-copy"></i> Copy'; | |
| btn.style.cssText = "position:absolute; top:10px; right:10px; background:rgba(255,255,255,0.1); color:#a1a1aa; border:1px solid rgba(255,255,255,0.2); padding:5px 10px; border-radius:6px; cursor:pointer; font-size:12px; font-weight:600;"; | |
| btn.onclick = () => { | |
| navigator.clipboard.writeText(pre.querySelector('code').innerText).then(() => { | |
| btn.innerHTML = '<i class="fas fa-check"></i> Copied'; | |
| setTimeout(() => btn.innerHTML = '<i class="fas fa-copy"></i> Copy', 2000); | |
| }); | |
| }; | |
| pre.appendChild(btn); | |
| }); | |
| // 👇👇👇 இங்கே மாற்றப்பட்டுள்ளது! (Test Yourself பட்டன் காலி) 👇👇👇 | |
| // வெறும் Copy, Regen, Share மட்டும் தான் வரும். | |
| const actionsHtml = ` | |
| <div class="msg-actions" style="margin-top:10px; display:flex; gap:15px; align-items:center;"> | |
| <div class="action-icon" onclick="copyText(this, \`${text.replace(/`/g, '\\`').replace(/"/g, '"')}\`)"><i class="fas fa-copy"></i> Copy</div> | |
| <div class="action-icon" onclick="regenerateLast()"><i class="fas fa-sync-alt"></i> Regen</div> | |
| <div class="action-icon" onclick="shareContent(\`${text.replace(/`/g, '\\`').replace(/"/g, '"')}\`)"><i class="fas fa-share-alt"></i> Share</div> | |
| </div>`; | |
| element.insertAdjacentHTML('beforeend', actionsHtml); | |
| // 👆👆👆 மாற்றம் முடிந்தது 👆👆👆 | |
| if (window.MathJax) MathJax.typesetPromise([element]).catch((err) => console.log(err)); | |
| if (window.mermaid && text.includes("```mermaid")) mermaid.run({ nodes: [element] }); | |
| chatBox.scrollTop = chatBox.scrollHeight; | |
| if (callback) callback(); | |
| } | |
| } | |
| type(); | |
| }; | |
| </script> | |
| <style> | |
| /* பழைய பிழையான .sub-header ஐ சரிசெய்தல் */ | |
| .sub-header { | |
| padding: 20px; | |
| padding-top: calc(20px + env(safe-area-inset-top)); | |
| display: flex; align-items: center; gap: 15px; | |
| border-bottom: 1px solid var(--border); margin-bottom: 20px; | |
| } | |
| /* Container-ஐ நகர விடாமல் தடுத்தல் (Fixing Lag) */ | |
| #sidebar { | |
| transform: none !important; | |
| transition: visibility 0s linear 0.4s, background-color 0.4s ease !important; | |
| display: flex !important; | |
| visibility: hidden; | |
| background-color: rgba(0,0,0,0); | |
| } | |
| #sidebar.open { | |
| visibility: visible !important; | |
| background-color: rgba(0,0,0,0.6) !important; | |
| transition-delay: 0s !important; | |
| } | |
| /* CONTENT-ஐ மட்டும் GPU-வில் நகர வைப்பது (Butter Smooth) */ | |
| .sidebar-content { | |
| transform: translate3d(-100%, 0, 0) !important; /* இடது பக்கம் மறைந்திருக்கும் */ | |
| width: 280px !important; | |
| transition: transform 0.35s cubic-bezier(0.2, 0.9, 0.2, 1) !important; | |
| will-change: transform; | |
| box-shadow: 5px 0 15px rgba(0,0,0,0.3); | |
| } | |
| /* SETTINGS PAGES (வலது பக்கம் இருந்து வர) */ | |
| #settings-overlay, .settings-sub-page { | |
| transform: translate3d(100%, 0, 0) !important; | |
| transition: transform 0.35s cubic-bezier(0.2, 0.9, 0.2, 1) !important; | |
| will-change: transform; | |
| } | |
| /* ACTIVE STATES (இயக்கம்) */ | |
| #sidebar.open .sidebar-content, | |
| #settings-overlay.active, | |
| .settings-sub-page.active { | |
| transform: translate3d(0, 0, 0) !important; | |
| } | |
| </style> | |
| </body> | |
| </html> | |
| """ | |
| """Part 3: Backend Routes & Main Execution | |
| (இதை Part 2 முடிஞ்ச இடத்துல இருந்து அப்படியே தொடர்ந்து பேஸ்ட் பண்ணுங்க. முக்கியம்: இதை மிஸ் பண்ணிடாதீங்க)""" | |
| # --- BACKEND ROUTES (UNCHANGED) --- | |
| def home(): return render_template_string(HTML_TEMPLATE) | |
| def new_chat(): | |
| u = request.json.get("username") | |
| if u not in user_db: user_db[u] = {} | |
| nid = str(uuid.uuid4()) | |
| # 👇 "timestamp": time.time() என்பதை புதுசா சேர்த்திருக்கோம்! | |
| user_db[u][nid] = { | |
| "title": "New Chat", | |
| "messages": [], | |
| "timestamp": time.time() | |
| } | |
| save_db(user_db) | |
| return jsonify({"chat_id": nid}) | |
| def rename_chat(): | |
| d = request.json | |
| u, cid, t = d.get("username"), d.get("chat_id"), d.get("title") | |
| if u in user_db and cid in user_db[u]: | |
| user_db[u][cid]["title"] = t | |
| save_db(user_db) | |
| return jsonify({"status":"ok"}) | |
| def delete_chat(): | |
| d = request.json | |
| u, cid = d.get("username"), d.get("chat_id") | |
| if u in user_db and cid in user_db[u]: | |
| del user_db[u][cid] | |
| save_db(user_db) | |
| return jsonify({"status":"ok"}) | |
| def get_history(): | |
| u = request.json.get("username") | |
| return jsonify({"chats": user_db.get(u, {})}) | |
| def get_chat(): | |
| d = request.json | |
| return jsonify({"messages": user_db.get(d["username"], {}).get(d["chat_id"], {}).get("messages", [])}) | |
| # 👇 புதிய Smart Title Function (Language Aware) | |
| def get_chat_title(first_message, medium="English"): | |
| global current_key_index | |
| try: | |
| if not API_KEYS: return "New Chat" | |
| # 1. கீ மற்றும் மாடலை தேர்வு செய் | |
| key = API_KEYS[current_key_index] | |
| model_name = get_working_model(key) | |
| if not model_name: return "New Chat" | |
| # 2. மொழிக்கு ஏற்றவாறு Prompt-ஐ மாற்றுதல் | |
| lang_instruction = "in English" | |
| if medium == "Tamil": | |
| lang_instruction = "in Tamil (தமிழ்)" | |
| # 3. AI-யிடம் கேட்பது | |
| genai.configure(api_key=key) | |
| model = genai.GenerativeModel(model_name) | |
| prompt = f"Generate a very short title (max 4 words) {lang_instruction} for this message: {first_message}. Do not use quotes." | |
| response = model.generate_content(prompt) | |
| return response.text.strip().replace('"', '').replace("'", "") | |
| except Exception as e: | |
| print(f"Title Error: {e}") | |
| return "New Chat" | |
| def chat(): | |
| d = request.json | |
| u, cid, msg = d.get("username"), d.get("chat_id"), d.get("message") | |
| # 👇 Frontend-ல் இருந்து வரும் விபரங்கள் | |
| u_details = d.get("user_details", {}) | |
| medium = u_details.get("medium", "English") | |
| # 1. Load Book Content (RAG) | |
| book_text = get_book_text(u_details) | |
| # 2. Context Prompt (புக்கை AI-க்கு கொடுத்தல்) | |
| prompt = msg | |
| if book_text: | |
| prompt = f"Context Book Content:\n{book_text}\n\nUser Question: {msg}" | |
| else: | |
| prompt = f"Note: No textbook found for this subject. Answer generally.\n\nUser Question: {msg}" | |
| # DB-ல் பயனர் இருக்கிறாரா என பார்த்தல் | |
| if u not in user_db: user_db[u] = {} | |
| if cid not in user_db[u]: | |
| user_db[u][cid] = {"messages": [], "title": "New Chat"} | |
| # 👇👇👇 புது மாற்றம்: இதுதான் முதல் மெசேஜ்னா, தலைப்பு வை! 👇👇👇 | |
| current_msgs = user_db[u][cid].get("messages", []) | |
| chat_title = user_db[u][cid].get("title", "New Chat") | |
| # 👇👇👇 புது மாற்றம்: மீடியம் (medium) சேர்த்து அனுப்புகிறோம் 👇👇👇 | |
| if len(current_msgs) == 0: | |
| print("⚡ Generating Auto-Title...") | |
| # msg கூடவே medium-ையும் அனுப்புறோம் | |
| chat_title = get_chat_title(msg, medium) | |
| user_db[u][cid]["title"] = chat_title | |
| # 👆👆👆 ------------------------------------------------ 👆👆👆 | |
| # 👆👆👆 ------------------------------------------------ 👆👆👆 | |
| # 3. Instruction based on Medium | |
| sys_inst = get_system_instruction(medium) | |
| # 4. User Message-ஐ லிஸ்டில் சேர் | |
| user_db[u][cid]["messages"].append({"role": "user", "content": msg}) | |
| # 5. Call AI | |
| reply = generate_with_retry( | |
| prompt, | |
| system_instruction=sys_inst, | |
| history_messages=user_db[u][cid]["messages"][:-1] | |
| ) | |
| # 6. AI Message-ஐ லிஸ்டில் சேர் | |
| user_db[u][cid]["messages"].append({"role": "model", "content": reply}) | |
| user_db[u][cid]["timestamp"] = time.time() | |
| # 7. MongoDB-ல் சேமி | |
| save_db(user_db) | |
| # 8. Response அனுப்பு (Frontend-க்கு Title-ையும் சேர்த்து அனுப்புகிறோம்) | |
| return jsonify({"response": reply, "title": chat_title}) | |
| def manifest(): | |
| data = { | |
| "name": "Student's AI", | |
| "short_name": "Student's AI", | |
| "start_url": "/", | |
| "display": "standalone", | |
| "orientation": "portrait", | |
| "background_color": "#09090b", | |
| "theme_color": "#09090b", | |
| "icons": [ | |
| { | |
| "src": "https://huggingface.co/spaces/Shirpi/Student-s_AI/resolve/main/1000177401.png", | |
| "sizes": "192x192", | |
| "type": "image/png" | |
| }, | |
| { | |
| "src": "https://huggingface.co/spaces/Shirpi/Student-s_AI/resolve/main/1000177401.png", | |
| "sizes": "512x512", | |
| "type": "image/png" | |
| } | |
| ] | |
| } | |
| return Response(json.dumps(data), mimetype='application/json') | |
| # 👇 ADD THIS NEW ROUTE AT THE END (Before if __name__ == '__main__':) 👇 | |
| def truncate_response(): | |
| try: | |
| d = request.json | |
| u, cid, ratio = d.get("username"), d.get("chat_id"), d.get("ratio") | |
| if u in user_db and cid in user_db[u]: | |
| msgs = user_db[u][cid]["messages"] | |
| # கடைசி மெசேஜ் AI உடையதா இருந்தால் மட்டும் கட் செய்யவும் | |
| if msgs and msgs[-1]["role"] == "model": | |
| full_text = msgs[-1]["content"] | |
| # கட் பண்ண வேண்டிய இடத்தை கணக்கிடுதல் | |
| cut_idx = int(len(full_text) * float(ratio)) | |
| # பாதியில் நிறுத்தியதற்கான மார்க்கர் | |
| msgs[-1]["content"] = full_text[:cut_idx] + " ... [Stopped]" | |
| save_db(user_db) | |
| return jsonify({"status": "updated"}) | |
| except: return jsonify({"status": "error"}) | |
| # 👇 புதிய Route: மொத்த ஹிஸ்டரியையும் அழிக்க (Delete All) | |
| def clear_all_history(): | |
| try: | |
| u = request.json.get("username") | |
| # அந்த பயனரின் மொத்த dictionary-ையும் காலி செய்கிறோம் | |
| if u in user_db: | |
| user_db[u] = {} | |
| save_db(user_db) | |
| return jsonify({"status": "success"}) | |
| except Exception as e: | |
| return jsonify({"status": "error", "message": str(e)}) | |
| if __name__ == '__main__': | |
| app.run(host='0.0.0.0', port=7860) |