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