SolusOps commited on
Commit
fb70f81
Β·
verified Β·
1 Parent(s): e1bd0b4

feat: app.py

Browse files
Files changed (1) hide show
  1. app.py +242 -0
app.py ADDED
@@ -0,0 +1,242 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+ import gradio as gr
3
+ from config.settings import SETTINGS
4
+ from services.model_router import ModelRouter
5
+ from quiz.models import QuizSession
6
+ from quiz.engine import QuizEngine
7
+ from storage.local_db import DB
8
+ from storage.mastery import MasteryStore
9
+ from agents.orchestrator import LearningOrchestrator
10
+ from ui.theme import build_champ_theme, CHAMP_CSS
11
+
12
+ print("[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 generation
30
+ print("[StudyWithChampAI] Ready.")
31
+
32
+
33
+ def handle_process(file_obj, pasted_text: str, audio_obj, language: str):
34
+ global _QUEST_CACHE, _SOURCE_TEXT_CACHE
35
+ yield "Processing...", []
36
+ try:
37
+ quests = None
38
+ 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.", []; return
52
+
53
+ _QUEST_CACHE = quests
54
+ 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
+
62
+ def 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.questions
68
+ return session, f"Quest '{quest_name}' ready! Go to Battle Mode."
69
+
70
+
71
+ def 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), session
74
+ selected_idx = question.options.index(selected_option) if selected_option in question.options else -1
75
+ 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
+
92
+ def 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
+
98
+ def 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_grade
110
+ 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
+
116
+ def handle_revision_quest(session: QuizSession):
117
+ """Generate an adaptive revision quest for weak topics and add it to the cache."""
118
+ global _QUEST_CACHE
119
+ 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
+
131
+ def 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, idx
187
+ if idx >= len(session.questions):
188
+ return "_Quest complete! Go to Results tab._", "", gr.update(choices=[], visible=False), gr.update(visible=False), None, idx
189
+ 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 + 1
206
+ 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 demo
238
+
239
+
240
+ if __name__ == "__main__":
241
+ app = build_app()
242
+ app.launch(share=False, server_name="0.0.0.0", server_port=7860)