import gradio as gr import os import requests from huggingface_hub import InferenceClient HF_TOKEN = os.environ.get("HF_TOKEN", "") GEMINI_KEY = os.environ.get("GEMINI_API_KEY", "") PINECONE_KEY = os.environ.get("PINECONE_API_KEY", "") PINECONE_HOST = "https://feeled-ai-syllabus-5hnhu9u.svc.aped-4627-b74a.pinecone.io" GRADES = ["Grade 9", "Grade 10", "Grade 11", "Grade 12"] SUBJECTS = ["Commerce", "Economics", "Accountancy", "Business Mathematics", "Physics", "Chemistry", "Biology", "Mathematics", "History", "Geography", "Tamil", "English", "Computer Science"] LANGUAGES = ["Tanglish (Tamil + English)", "Tamil மட்டும்", "English Only"] MODES = ["📚 Chat Tutor", "✨ Story Mode", "🎯 Exam Mode"] STORY_STYLES = ["🌟 Deep", "🎉 Funny", "🚀 Adventure", "💙 Emotional"] SYSTEM_PROMPT = """You are FeelEd Lite, a warm and friendly AI tutor for TN Board students in Tamil Nadu, India. Language instruction: {language} CRITICAL RULES: 1. If Textbook Context is provided, use it as your PRIMARY source. Base your answer on it directly. 2. Quote key phrases from the context to make answers accurate. 3. If no context, use general TN Board knowledge clearly. 4. Always give COMPLETE structured answers — never cut short. 5. Be warm, encouraging, and student-friendly.""" def embed_query(text): if not GEMINI_KEY: return [] try: r = requests.post( f"https://generativelanguage.googleapis.com/v1beta/models/gemini-embedding-001:embedContent?key={GEMINI_KEY}", json={"content":{"parts":[{"text":text}]},"outputDimensionality":768}, timeout=10) if r.ok: return r.json().get("embedding",{}).get("values",[]) except: pass return [] def rag_search(query, grade="11", subject="", top_k=5): if not GEMINI_KEY or not PINECONE_KEY: return "" vec = embed_query(query) if not vec: return "" filt = {"grade":{"$eq":grade.replace("Grade ","")}} if subject: filt["subject"] = {"$eq":subject} try: r = requests.post(f"{PINECONE_HOST}/query", headers={"Api-Key":PINECONE_KEY,"Content-Type":"application/json"}, json={"vector":vec,"topK":top_k,"includeMetadata":True,"filter":filt}, timeout=10) if not r.ok: return "" return "\n\n".join([m["metadata"].get("text","").strip() for m in r.json().get("matches",[]) if m.get("score",0)>0.3 and m.get("metadata",{}).get("text","")]) except: return "" def get_lang(lang): if "Tamil மட்டும்" in lang: return "Answer ONLY in Tamil" if "English Only" in lang: return "Answer ONLY in English" return "Answer in Tamil + English mix (Tanglish)" # Tamil → English query mapping for better RAG retrieval TAMIL_TO_ENGLISH = { "தேவை விதி": "law of demand", "தேவை": "demand", "விநியோகம்": "supply", "விநியோக விதி": "law of supply", "இரட்டை பதிவு": "double entry", "இரட்டை பதிவு முறை": "double entry bookkeeping", "மொத்த உள்நாட்டு உற்பத்தி": "GDP gross domestic product", "தொழில் முனைவோர்": "entrepreneur entrepreneurship", "நுகர்வோர் உரிமைகள்": "consumer rights", "பணவீக்கம்": "inflation", "வங்கி": "bank banking", "வரி": "tax taxation", "இலாபம்": "profit", "நஷ்டம்": "loss", "சொத்து": "assets", "பொறுப்பு": "liability", "மூலதனம்": "capital", } def get_rag_query(message): """Convert Tamil query to English for better vector search""" msg_lower = message.lower() for tamil, english in TAMIL_TO_ENGLISH.items(): if tamil in message: return english # If mostly Tamil, use subject+grade as context tamil_chars = sum(1 for c in message if '஀' <= c <= '௿') if tamil_chars > len(message) * 0.3: return message # Let Gemini embed handle it return message def generate(message, mode, grade, subject, language, story_style): rag_query = get_rag_query(message) ctx = rag_search(rag_query, grade=grade, subject=subject) ctx_text = f"Textbook Context:\n{ctx}" if ctx else "(Use general TN Board knowledge)" system = SYSTEM_PROMPT.format(language=get_lang(language)) if mode == "📚 Chat Tutor": user = f"""{ctx_text} IMPORTANT: Use the textbook context above to answer accurately. Student Question: {message} Give a complete structured answer based on the context: 📖 **Definition:** [accurate definition from context] 🔑 **Key Points:** • [key point 1 from context] • [key point 2 from context] • [key point 3 from context] 💡 **Example:** [real-life Tamil student example] 📝 **Exam Tip:** [important exam tip] Write complete sentences. Do not cut short.""" elif mode == "✨ Story Mode": styles = {"🌟 Deep":"thoughtful and educational","🎉 Funny":"funny and playful","🚀 Adventure":"adventurous and exciting","💙 Emotional":"emotional and heartwarming"} user = f"""{ctx_text}\n\nWrite a {styles.get(story_style,'educational')} story (8-10 sentences) teaching: {message}\nTamil characters (Ramu, Priya, Anna) must EXPERIENCE the concept.\n\n🌟 **கதை / Story:**\n[story]\n\n📚 **பாடம் / Lesson:**\n[concept]""" else: user = f"""{ctx_text}\n\nTN Board exam questions for {grade} {subject}: {message}\n\n**கேள்வி 1 — 2 மதிப்பெண்கள்:** [question]\n**விடை:** [answer]\n\n**கேள்வி 2 — 5 மதிப்பெண்கள்:** [question]\n**விடை:** [structured answer]\n\n**கேள்வி 3 — 8 மதிப்பெண்கள்:** [question]\n**விடை:** [detailed answer]\n\n⭐ **Key Terms:** [4 terms]""" for model_id, provider in [ ("Qwen/Qwen2.5-72B-Instruct", "nebius"), ("Qwen/Qwen2.5-7B-Instruct", "novita"), ("meta-llama/Llama-3.1-8B-Instruct", "novita"), ("Qwen/Qwen2.5-32B-Instruct", "nebius"), ]: try: client = InferenceClient(model=model_id, token=HF_TOKEN, provider=provider) resp = client.chat_completion(messages=[{"role":"system","content":system},{"role":"user","content":user}], max_tokens=2048, temperature=0.3) return resp.choices[0].message.content.strip() except: continue return "மன்னிக்கவும், இப்போது busy. சற்று நேரம் கழித்து முயற்சிக்கவும்." def chat(message, history, mode, grade, subject, language, story_style): if not message.strip(): return history, "" history = history or [] loading = {"📚 Chat Tutor":["📖 பாடப்புத்தகத்தில் தேடுகிறோம்..."],"✨ Story Mode":["✨ கதை உருவாக்குகிறோம்..."],"🎯 Exam Mode":["📝 கேள்விகள் தயாராகிறது..."]} history.append({"role":"user","content":message}) history.append({"role":"assistant","content":loading.get(mode,["⏳ தயாராகிறது..."])[0]}) yield history, "" response = generate(message, mode, grade, subject, language, story_style) history[-1] = {"role":"assistant","content":response} yield history, "" CSS = """ @import url('https://fonts.googleapis.com/css2?family=Noto+Sans+Tamil:wght@400;500;600;700&family=Inter:wght@400;500;600;700;800&display=swap'); /* ── RESET & BASE ── */ *, *::before, *::after { box-sizing: border-box; font-family: 'Noto Sans Tamil', 'Inter', sans-serif !important; letter-spacing: 0 !important; word-spacing: 0 !important; } /* ONE CONSISTENT DARK THEME */ body, .gradio-container, .gr-panel, .gr-box, .contain, .wrap, .gap, .border-none { background: #111827 !important; color: #F9FAFB !important; } .gradio-container { max-width: 820px !important; margin: 0 auto !important; padding: 12px !important; } /* ── HERO ── */ #fl-hero { background: linear-gradient(135deg, #4338CA 0%, #6D28D9 50%, #7C3AED 100%); border-radius: 20px; padding: 24px 20px 20px; margin-bottom: 10px; text-align: center; box-shadow: 0 8px 32px rgba(109,40,217,0.4); position: relative; overflow: hidden; } #fl-hero::before { content:''; position:absolute; top:-50px; right:-50px; width:180px; height:180px; border-radius:50%; background:rgba(255,255,255,0.07); pointer-events:none; } #fl-hero h1 { font-size: 2rem !important; font-weight: 800 !important; color: white !important; -webkit-text-fill-color: white !important; margin: 0 0 6px 0 !important; letter-spacing: -0.3px !important; } #fl-hero .fl-tagline { color: rgba(255,255,255,0.95) !important; font-size: 1.25rem; font-weight: 600; margin: 2px 0 6px 0; letter-spacing: 0.5px !important; } #fl-hero .fl-sub { color: rgba(255,255,255,0.65) !important; font-size: 0.78rem; margin: 0 0 14px 0; } #fl-hero .fl-chips { display: flex; gap: 6px; justify-content: center; flex-wrap: wrap; } #fl-hero .fl-chip { background: rgba(255,255,255,0.15); border: 1px solid rgba(255,255,255,0.22); color: white; padding: 4px 12px; border-radius: 50px; font-size: 0.72rem; font-weight: 500; } /* ── STUDENT SIGNAL ── */ #fl-signal { background: rgba(124,58,237,0.1); border: none; border-left: 3px solid #7C3AED; border-radius: 8px; padding: 9px 16px; margin-bottom: 10px; font-size: 0.8rem; color: #A78BFA; text-align: center; } /* ── CONTROLS CARD ── */ #fl-controls { background: #1F2937 !important; border: 1px solid #374151 !important; border-radius: 14px !important; padding: 14px 16px !important; margin-bottom: 10px !important; } #fl-controls label { color: #A78BFA !important; font-size: 0.7rem !important; font-weight: 700 !important; text-transform: uppercase; letter-spacing: 0.6px !important; margin-bottom: 4px !important; } #fl-controls select { background: #111827 !important; border: 1px solid #374151 !important; border-radius: 8px !important; color: #F9FAFB !important; font-size: 0.88rem !important; padding: 9px 12px !important; width: 100% !important; cursor: pointer !important; transition: border-color 0.2s !important; -webkit-appearance: auto !important; } #fl-controls select:focus { border-color: #7C3AED !important; outline: none !important; box-shadow: 0 0 0 2px rgba(124,58,237,0.25) !important; } #fl-controls option { background: #1F2937 !important; color: #F9FAFB !important; } /* ── STORY PILLS CARD ── */ #fl-story-wrap { background: #1F2937 !important; border: 1px solid #78350F !important; border-radius: 12px !important; padding: 12px 16px !important; margin-bottom: 10px !important; } #fl-story-wrap label.svelte-1gfkn6j { color: #FCD34D !important; font-size: 0.7rem !important; font-weight: 700 !important; text-transform: uppercase; letter-spacing: 0.6px !important; } /* 2-column grid for story pills */ .fl-sr .gr-radio-group { display: grid !important; grid-template-columns: 1fr 1fr !important; gap: 8px !important; margin-top: 8px !important; } .fl-sr label { border-radius: 10px !important; padding: 9px 14px !important; font-size: 0.85rem !important; font-weight: 600 !important; cursor: pointer !important; transition: all 0.18s !important; border: 1.5px solid !important; text-align: center !important; letter-spacing: 0 !important; } .fl-sr label:nth-child(1){background:#1e1b4b!important;border-color:#4338ca!important;color:#a5b4fc!important;} .fl-sr label:nth-child(2){background:#1c1200!important;border-color:#b45309!important;color:#fcd34d!important;} .fl-sr label:nth-child(3){background:#1c0f00!important;border-color:#c2410c!important;color:#fb923c!important;} .fl-sr label:nth-child(4){background:#1c001a!important;border-color:#be185d!important;color:#f9a8d4!important;} .fl-sr label:has(input:checked):nth-child(1){background:#4338ca!important;color:white!important;border-color:#4338ca!important;box-shadow:0 3px 12px rgba(67,56,202,0.5)!important;} .fl-sr label:has(input:checked):nth-child(2){background:#b45309!important;color:white!important;border-color:#b45309!important;box-shadow:0 3px 12px rgba(180,83,9,0.5)!important;} .fl-sr label:has(input:checked):nth-child(3){background:#c2410c!important;color:white!important;border-color:#c2410c!important;box-shadow:0 3px 12px rgba(194,65,12,0.5)!important;} .fl-sr label:has(input:checked):nth-child(4){background:#be185d!important;color:white!important;border-color:#be185d!important;box-shadow:0 3px 12px rgba(190,24,93,0.5)!important;} /* ── CHAT AREA ── */ #fl-chat { background: #1F2937 !important; border: 1px solid #374151 !important; border-radius: 16px !important; overflow: hidden !important; margin-bottom: 10px !important; } .gr-chatbot, [data-testid="chatbot"] { background: #1F2937 !important; border: none !important; border-radius: 0 !important; } /* User bubble */ [data-testid="user"] > div { background: linear-gradient(135deg, #4338CA, #7C3AED) !important; color: white !important; border-radius: 18px 18px 4px 18px !important; padding: 12px 16px !important; max-width: 80% !important; margin-left: auto !important; box-shadow: 0 3px 12px rgba(109,40,217,0.35) !important; font-size: 0.93rem !important; line-height: 1.65 !important; } [data-testid="user"] > div * { color: white !important; } /* Bot bubble */ [data-testid="bot"] > div { background: #111827 !important; color: #F9FAFB !important; border: 1px solid #374151 !important; border-radius: 4px 18px 18px 18px !important; padding: 14px 18px !important; max-width: 90% !important; box-shadow: 0 2px 8px rgba(0,0,0,0.3) !important; font-size: 0.92rem !important; line-height: 1.8 !important; } [data-testid="bot"] > div * { color: #F9FAFB !important; } [data-testid="bot"] strong { color: #A78BFA !important; font-weight: 700 !important; } [data-testid="bot"] ul, [data-testid="bot"] ol { padding-left: 18px !important; margin: 6px 0 !important; } [data-testid="bot"] li { margin: 5px 0 !important; color: #D1D5DB !important; } [data-testid="bot"] p { margin-bottom: 8px !important; } /* ── INPUT AREA ── */ #fl-input { border-top: 1px solid #374151 !important; padding: 14px 14px 12px !important; background: #1F2937 !important; } #fl-input textarea { background: #111827 !important; border: 1.5px solid #4B5563 !important; border-radius: 12px !important; color: #F9FAFB !important; font-size: 0.95rem !important; padding: 12px 16px !important; line-height: 1.65 !important; resize: none !important; transition: border-color 0.2s, box-shadow 0.2s !important; } #fl-input textarea:focus { border-color: #7C3AED !important; box-shadow: 0 0 0 3px rgba(124,58,237,0.2) !important; outline: none !important; } #fl-input textarea::placeholder { color: #6B7280 !important; } #fl-send { background: linear-gradient(135deg, #4338CA, #7C3AED) !important; border: none !important; border-radius: 10px !important; color: white !important; font-weight: 700 !important; font-size: 0.93rem !important; height: 46px !important; box-shadow: 0 4px 16px rgba(109,40,217,0.4) !important; transition: all 0.2s !important; height: 40px !important; } #fl-send:hover { transform: translateY(-2px) !important; box-shadow: 0 6px 22px rgba(109,40,217,0.55) !important; } #fl-clear { background: transparent !important; border: 1px solid #374151 !important; border-radius: 10px !important; color: #6B7280 !important; height: 40px !important; font-size: 0.82rem !important; transition: all 0.18s !important; } #fl-clear:hover { border-color: #4B5563 !important; color: #9CA3AF !important; } /* ── EXAMPLES ── */ #fl-examples { margin-top: 4px; } .gr-examples .label { color: #6B7280 !important; font-size: 0.7rem !important; font-weight: 700 !important; text-transform: uppercase; letter-spacing: 0.5px !important; margin-bottom: 8px !important; } .gr-examples button { background: #1F2937 !important; border: 1px solid #374151 !important; border-radius: 50px !important; color: #A78BFA !important; font-size: 0.82rem !important; font-weight: 500 !important; padding: 7px 16px !important; margin: 3px !important; transition: all 0.18s !important; } .gr-examples button:hover { background: #2D3748 !important; border-color: #7C3AED !important; color: #C4B5FD !important; transform: translateY(-1px) !important; } /* ── FOOTER ── */ #fl-footer { text-align: center; color: #4B5563; font-size: 0.75rem; padding: 12px 0 4px; line-height: 1.9; } #fl-footer strong { color: #7C3AED; } #fl-footer a { color: #6D28D9; text-decoration: none; } /* ── MOBILE ── */ @media (max-width: 640px) { .gradio-container { padding: 8px !important; } #fl-hero { padding: 20px 14px 16px; border-radius: 16px; } #fl-hero h1 { font-size: 1.7rem !important; } #fl-hero .fl-tagline { font-size: 0.88rem; } #fl-hero .fl-sub { font-size: 0.74rem; } #fl-hero .fl-chip { font-size: 0.68rem; padding: 3px 10px; } #fl-controls { padding: 12px !important; } #fl-controls select { font-size: 0.85rem !important; padding: 8px 10px !important; } .fl-sr .gr-radio-group { grid-template-columns: 1fr 1fr !important; gap: 6px !important; } .fl-sr label { padding: 8px 10px !important; font-size: 0.82rem !important; } [data-testid="user"] > div { max-width: 88% !important; font-size: 0.9rem !important; } [data-testid="bot"] > div { max-width: 96% !important; font-size: 0.88rem !important; } #fl-input { padding: 10px 10px 8px !important; } #fl-input textarea { font-size: 0.92rem !important; } #fl-send { height: 48px !important; font-size: 0.9rem !important; } #fl-clear { height: 38px !important; font-size: 0.82rem !important; } .gr-examples button { font-size: 0.78rem !important; padding: 6px 12px !important; } } """ with gr.Blocks(css=CSS, title="FeelEd Lite — Tamil TN Board AI Tutor") as demo: gr.HTML("""

📚 FeelEd Lite

கற்க கசடற

Tamil Medium · Grades 9–12 · TN Samacheer AI Tutor

🏫 Grades 9–12 📖 21k+ Passages 🌐 Tamil + English 🔒 No Data Stored ⚡ Fast & Private
""") gr.HTML("""
👩‍🎓 Tested with Tamil-medium Grade 11 TN Board students preparing for exams. Built for students across the Global South.
""") with gr.Row(elem_id="fl-controls"): mode = gr.Dropdown(choices=MODES, value="📚 Chat Tutor", label="🎯 Mode", scale=1, filterable=False) grade = gr.Dropdown(choices=GRADES, value="Grade 11", label="🎓 Grade", scale=1, filterable=False) subject = gr.Dropdown(choices=SUBJECTS, value="Commerce", label="📘 Subject", scale=1, filterable=False) language = gr.Dropdown(choices=LANGUAGES, value="Tanglish (Tamil + English)", label="🌐 Language",scale=1, filterable=False) with gr.Row(elem_id="fl-story-wrap", visible=False) as story_row: story_style = gr.Radio( choices=STORY_STYLES, value="🌟 Deep", label="✨ Story Style", elem_classes=["fl-sr"], ) with gr.Column(elem_id="fl-chat"): chatbot = gr.Chatbot( type="messages", height=320, show_label=False, bubble_full_width=False, placeholder="""
📚

வணக்கம்! FeelEd Lite 🙏

TN Board கேள்விகளை Tamil, English அல்லது Tanglish-ல் கேளுங்கள்.

""", ) with gr.Column(elem_id="fl-input"): msg = gr.Textbox( placeholder="உங்கள் கேள்வியை இங்கே தட்டச்சு செய்யுங்கள்...", show_label=False, lines=2, ) with gr.Row(): send_btn = gr.Button("▶ அனுப்பு / Send", elem_id="fl-send", variant="primary", scale=4) clear_btn = gr.Button("🗑 Clear", elem_id="fl-clear", scale=1) with gr.Column(elem_id="fl-examples"): gr.Examples( examples=[ ["தேவை விதி என்றால் என்ன?"], ["இரட்டை பதிவு முறை விளக்கு"], ["GDP என்றால் என்ன?"], ["தொழில் முனைவோர் பண்புகள்"], ["Consumer rights in Tamil"], ["Newton's laws of motion"], ], inputs=msg, label="💡 Try these questions:", ) gr.HTML(""" """) mode.change(fn=lambda m: gr.update(visible=(m=="✨ Story Mode")), inputs=mode, outputs=story_row) send_btn.click(fn=chat, inputs=[msg,chatbot,mode,grade,subject,language,story_style], outputs=[chatbot,msg]) msg.submit(fn=chat, inputs=[msg,chatbot,mode,grade,subject,language,story_style], outputs=[chatbot,msg]) clear_btn.click(lambda: ([],""), outputs=[chatbot,msg]) if __name__ == "__main__": demo.launch()