SolusOps/Study-with-ChampAI
0
1from __future__ import annotations2import gradio as gr3from config.settings import SETTINGS4from services.model_router import ModelRouter5from quiz.models import QuizSession6from quiz.engine import QuizEngine7from storage.local_db import DB8from storage.mastery import MasteryStore9from agents.orchestrator import LearningOrchestrator10from ui.theme import build_champ_theme, CHAMP_CSS11 12print("[StudyWithChampAI] Initializing...")13_db = DB(SETTINGS.db_path)14_mastery_store = MasteryStore(_db)15_router = ModelRouter(16 ocr_model=SETTINGS.ocr_model_id,17 reasoning_model=SETTINGS.reasoning_model_id,18 multilingual_model=SETTINGS.multilingual_model_id,19 speech_model=SETTINGS.speech_model_id,20 hf_api_key=SETTINGS.hf_api_key,21 featherless_api_key=SETTINGS.featherless_api_key,22 max_tokens=SETTINGS.max_new_tokens,23 temperature=SETTINGS.temperature,24)25_orchestrator = LearningOrchestrator(26 _router, _db, _mastery_store, questions_per_quest=SETTINGS.questions_per_quest)27_engine = QuizEngine()28_QUEST_CACHE: list = []29_SOURCE_TEXT_CACHE: str = "" # retained for revision quest generation30print("[StudyWithChampAI] Ready.")31 32 33def handle_process(file_obj, pasted_text: str, audio_obj, language: str):34 global _QUEST_CACHE, _SOURCE_TEXT_CACHE35 yield "Processing...", []36 try:37 quests = None38 if audio_obj is not None:39 yield "Transcribing voice input with Whisper...", []40 with open(audio_obj, "rb") as f:41 audio_bytes = f.read()42 quests = _orchestrator.process_voice(audio_bytes, language)43 elif file_obj is not None:44 yield "Processing file with MiniCPM-V...", []45 quests = _orchestrator.process_file(file_obj.name, language)46 elif pasted_text and pasted_text.strip():47 _SOURCE_TEXT_CACHE = pasted_text.strip()48 yield "Extracting concepts with MiniCPM-V...", []49 quests = _orchestrator.process_text(pasted_text.strip(), language)50 else:51 yield "No input provided. Upload a file, paste text, or record audio.", []; return52 53 _QUEST_CACHE = quests54 count = sum(len(q.questions) for q in quests)55 yield (f"Generated {len(quests)} quests with {count} questions "56 f"({count - len(quests)} regular + {len(quests)} boss battles). Go to **Quest Map**!",57 [q.name for q in quests])58 except Exception as exc:59 yield f"Error: {exc}", []60 61 62def handle_select_quest(quest_name: str):63 quest = next((q for q in _QUEST_CACHE if q.name == quest_name), None)64 if quest is None:65 return None, "Quest not found."66 session = QuizSession(quest_name=quest.name)67 session.questions = quest.questions68 return session, f"Quest '{quest_name}' ready! Go to Battle Mode."69 70 71def handle_answer(selected_option: str, session: QuizSession, question):72 if session is None or question is None:73 return "No active session.", "", "", "", "", gr.update(visible=False), gr.update(visible=True), session74 selected_idx = question.options.index(selected_option) if selected_option in question.options else -175 result = _engine.submit_next(session, selected_idx)76 tutor_hint = ""77 if not result.is_correct:78 tutor_hint = _orchestrator.get_tutor_hint(question, selected_option)79 boss_label = "๐ BOSS BATTLE! " if result.was_boss else ""80 return (81 f"{boss_label}{'โ
CORRECT!' if result.is_correct else 'โ WRONG!'}",82 f"+{result.xp_delta} XP" if result.xp_delta > 0 else "",83 result.streak_label,84 question.explanation,85 tutor_hint,86 gr.update(visible=True),87 gr.update(visible=False),88 session,89 )90 91 92def handle_translate(hint_text: str, target_lang: str):93 if not hint_text:94 return "_No hint to translate._"95 return _orchestrator.translate_hint(hint_text, target_lang)96 97 98def handle_session_complete(session: QuizSession):99 if session is None:100 return "No session.", "", "", "", ""101 result = _orchestrator.complete_quest(session)102 all_mastery = _mastery_store.all_mastery()103 mastery_lines = "\n".join(104 f"- **{t}**: {'โ' * int(v * 10)}{'โ' * (10 - int(v * 10))} {int(v*100)}%"105 for t, v in all_mastery.items())106 weak = result.get("weak_topics", [])107 weak_lines = ("\n".join(f"- {t} โ revision needed" for t in weak)108 if weak else "_All topics strong!_ ๐")109 from quiz.scoring import compute_grade110 grade = compute_grade(session.score, len(session.questions))111 return (f"### {session.score}/{len(session.questions)} correct",112 f"### Grade: {grade}", f"### XP Earned: {session.xp_earned}",113 mastery_lines or "_No mastery data yet._", weak_lines)114 115 116def handle_revision_quest(session: QuizSession):117 """Generate an adaptive revision quest for weak topics and add it to the cache."""118 global _QUEST_CACHE119 weak = _mastery_store.weak_topics()120 if not weak:121 return "No weak topics found โ all strong!", [q.name for q in _QUEST_CACHE]122 try:123 revision = _orchestrator.generate_revision_quest(weak, source_text=_SOURCE_TEXT_CACHE)124 _QUEST_CACHE.append(revision)125 return (f"Revision quest unlocked: **{revision.name}**\nGo to Quest Map!",126 [q.name for q in _QUEST_CACHE])127 except Exception as exc:128 return f"Error generating revision quest: {exc}", [q.name for q in _QUEST_CACHE]129 130 131def build_app() -> gr.Blocks:132 with gr.Blocks(theme=build_champ_theme(), css=CHAMP_CSS, title="StudyWithChampAI") as demo:133 gr.Markdown("# StudyWithChampAI\n### Turn notes into quests. Turn studying into progression.")134 135 quest_names_state = gr.State([])136 session_state = gr.State(None)137 current_q_state = gr.State(None)138 q_idx_state = gr.State(0)139 140 with gr.Tab("Import Material"):141 gr.Markdown("### Upload PDF, image, voice, or paste text.\nMiniCPM-V reads and understands your material.")142 with gr.Row():143 file_input = gr.File(label="Upload File", file_types=[".pdf",".txt",".png",".jpg",".jpeg"])144 audio_input = gr.Audio(label="Record Voice", type="filepath", sources=["microphone"])145 text_input = gr.Textbox(label="Or paste notes here", lines=6, placeholder="Paste study material...")146 lang_dropdown = gr.Dropdown(choices=["English","Hindi","Bengali","Spanish","French"],147 value="English", label="Quiz Language")148 process_btn = gr.Button("Generate Quests โ๏ธ", variant="primary", size="lg")149 status_md = gr.Markdown("_Ready._")150 process_btn.click(fn=handle_process,151 inputs=[file_input, text_input, audio_input, lang_dropdown],152 outputs=[status_md, quest_names_state])153 154 with gr.Tab("Quest Map"):155 gr.Markdown("### Select a quest to begin.")156 quest_radio = gr.Radio(choices=[], label="Available Quests", interactive=True)157 start_btn = gr.Button("Enter Battle โ๏ธ", variant="primary")158 quest_status_md = gr.Markdown("")159 quest_names_state.change(fn=lambda n: gr.update(choices=n, visible=bool(n)),160 inputs=[quest_names_state], outputs=[quest_radio])161 start_btn.click(fn=handle_select_quest, inputs=[quest_radio],162 outputs=[session_state, quest_status_md])163 164 with gr.Tab("Battle Mode"):165 gr.Markdown("### Answer to earn XP! Boss battle always awaits at quest end.")166 question_md = gr.Markdown("_Select a quest first._")167 progress_md = gr.Markdown("")168 answer_radio = gr.Radio(choices=[], label="Your Answer", interactive=True)169 submit_btn = gr.Button("Submit Answer", variant="primary")170 171 with gr.Group(visible=False) as feedback_group:172 result_md = gr.Markdown("")173 xp_md = gr.Markdown("")174 streak_md = gr.Markdown("")175 explanation_md = gr.Markdown("")176 tutor_md = gr.Markdown("")177 with gr.Row():178 translate_lang = gr.Dropdown(choices=["Hindi","Bengali","Spanish","French"],179 value="Hindi", label="Translate hint to", scale=1)180 translate_btn = gr.Button("Translate ๐", scale=1)181 translated_md = gr.Markdown("")182 next_btn = gr.Button("Next Question โ")183 184 def load_question(session, idx):185 if session is None:186 return "_No active session._", "", gr.update(choices=[], visible=False), gr.update(visible=False), None, idx187 if idx >= len(session.questions):188 return "_Quest complete! Go to Results tab._", "", gr.update(choices=[], visible=False), gr.update(visible=False), None, idx189 q = session.questions[idx]190 header = "๐ **BOSS BATTLE** ๐\n\n" if q.is_boss else ""191 return (f"{header}**Q{idx+1}/{len(session.questions)}**\n\n{q.text}",192 f"Progress: {idx}/{len(session.questions)} | XP: {session.xp_earned}",193 gr.update(choices=q.options, visible=True, value=None),194 gr.update(visible=True), q, idx)195 196 session_state.change(fn=load_question, inputs=[session_state, q_idx_state],197 outputs=[question_md, progress_md, answer_radio, submit_btn, current_q_state, q_idx_state])198 submit_btn.click(fn=handle_answer, inputs=[answer_radio, session_state, current_q_state],199 outputs=[result_md, xp_md, streak_md, explanation_md, tutor_md,200 feedback_group, submit_btn, session_state])201 translate_btn.click(fn=handle_translate, inputs=[tutor_md, translate_lang],202 outputs=[translated_md])203 204 def advance_question(session, idx):205 new_idx = idx + 1206 if session is None or new_idx >= len(session.questions):207 return ("_Quest complete! Go to Results._", "",208 gr.update(choices=[], visible=False), gr.update(visible=False),209 gr.update(visible=False), None, new_idx)210 q = session.questions[new_idx]211 header = "๐ **BOSS BATTLE** ๐\n\n" if q.is_boss else ""212 return (f"{header}**Q{new_idx+1}/{len(session.questions)}**\n\n{q.text}",213 f"Progress: {new_idx}/{len(session.questions)} | XP: {session.xp_earned}",214 gr.update(choices=q.options, visible=True, value=None),215 gr.update(visible=True), gr.update(visible=False), q, new_idx)216 217 next_btn.click(fn=advance_question, inputs=[session_state, q_idx_state],218 outputs=[question_md, progress_md, answer_radio, submit_btn,219 feedback_group, current_q_state, q_idx_state])220 221 with gr.Tab("Results"):222 gr.Markdown("## Quest Complete! ๐")223 score_md = gr.Markdown("")224 grade_md = gr.Markdown("")225 xp_total_md = gr.Markdown("")226 mastery_md = gr.Markdown("### Mastery Map")227 weak_md = gr.Markdown("")228 with gr.Row():229 finish_btn = gr.Button("View Results", variant="primary")230 revision_btn = gr.Button("โ๏ธ Generate Revision Quest", variant="secondary")231 revision_status_md = gr.Markdown("")232 finish_btn.click(fn=handle_session_complete, inputs=[session_state],233 outputs=[score_md, grade_md, xp_total_md, mastery_md, weak_md])234 revision_btn.click(fn=handle_revision_quest, inputs=[session_state],235 outputs=[revision_status_md, quest_names_state])236 237 return demo238 239 240if __name__ == "__main__":241 app = build_app()242 app.launch(share=False, server_name="0.0.0.0", server_port=7860)243 