import asyncio.base_events _orig_del = asyncio.base_events.BaseEventLoop.__del__ def _patched_del(self): try: _orig_del(self) except ValueError as e: if "Invalid file descriptor" not in str(e): raise asyncio.base_events.BaseEventLoop.__del__ = _patched_del import os import gradio as gr from extractors import extract_text_dispatcher from llm import generate_notes from chat import chat from rag import chunk_text, build_collection from flashcards import ( render_card, generate_flashcards_handler, flip_card, next_card, prev_card, ) from exam import ( render_question, generate_exam_handler, next_question, prev_question, record_answer, grade_exam, ) # --- Gradio Callback Handlers --- def process_file_uploads(files, history): """ Rebuilds study_context AND the RAG vector store from scratch based on whatever files are CURRENTLY in the upload widget. Runs on every change to the file list — additions AND deletions. """ if not files: # User cleared all files — context, vector store, and status all reset. return history, "", None, "No files currently loaded." file_list = files if isinstance(files, list) else [files] new_extracted_text = "" filenames = [] for file_obj in file_list: file_path = file_obj.name if hasattr(file_obj, "name") else file_obj filename = os.path.basename(file_path) filenames.append(filename) try: extracted = extract_text_dispatcher(file_path) new_extracted_text += f"\n--- Start of {filename} ---\n{extracted}\n--- End of {filename} ---\n" except Exception as e: new_extracted_text += f"\n--- Start of {filename} ---\n[Error processing file: {str(e)}]\n--- End of {filename} ---\n" system_msg = f"📁 Currently loaded: {', '.join(filenames)}" history = history + [{"role": "assistant", "content": system_msg}] upload_status_text = f"✅ **Currently loaded:** {', '.join(filenames)}" chunks = chunk_text(new_extracted_text) collection = build_collection(chunks) return history, new_extracted_text, collection, upload_status_text def put_message_in_chatbot(message, history): return "", history + [{"role": "user", "content": message}] # --- Interface Layout --- with gr.Blocks() as ui: # Shared state study_context = gr.State(value="") vector_store_state = gr.State(value=None) # Flashcard-specific state flashcards_state = gr.State(value=[]) current_index = gr.State(value=0) show_answer = gr.State(value=False) # Practice Exam-specific state exam_questions_state = gr.State(value=[]) exam_index = gr.State(value=0) user_answers_state = gr.State(value={}) exam_submitted_state = gr.State(value=False) gr.Markdown("## Study LLM") with gr.Tab("Upload Files"): with gr.Row(): upload = gr.File( file_types=[".pdf", ".pptx", ".docx", ".txt", ".mp4", ".avi", ".mov", ".png", ".jpg", ".jpeg"], file_count="multiple", label="Drag and drop your study materials here", ) with gr.Row(): upload_status = gr.Markdown("No files currently loaded.") with gr.Tab("Chat"): with gr.Row(): chatbot = gr.Chatbot(height=500) with gr.Row(): message = gr.Textbox(label="Chat with our AI Assistant:", placeholder="Ask something about your study notes...") with gr.Tab("Notes"): with gr.Column(): generate_notes_button = gr.Button("Generate Study Notes", variant="primary") notes_output = gr.Markdown(label="Generated Study Notes", latex_delimiters=[ {"left": "$$", "right": "$$", "display": True}, {"left": "$", "right": "$", "display": False}, {"left": "\\[", "right": "\\]", "display": True}, {"left": "\\(", "right": "\\)", "display": False}, ]) with gr.Tab("Flashcards"): with gr.Column(): generate_flashcards_button = gr.Button("Generate Flashcards", variant="primary") card_display = gr.Markdown(label="Flashcard", latex_delimiters=[ {"left": "$$", "right": "$$", "display": True}, {"left": "$", "right": "$", "display": False}, {"left": "\\[", "right": "\\]", "display": True}, {"left": "\\(", "right": "\\)", "display": False}, ]) with gr.Row(): prev_button = gr.Button("⬅ Previous") flip_button = gr.Button("Flip") next_button = gr.Button("Next ➡") with gr.Tab("Practice Exam"): with gr.Column(): generate_exam_button = gr.Button("Generate Practice Exam", variant="primary") question_display = gr.Markdown(label="Question", latex_delimiters=[ {"left": "$$", "right": "$$", "display": True}, {"left": "$", "right": "$", "display": False}, {"left": "\\[", "right": "\\]", "display": True}, {"left": "\\(", "right": "\\)", "display": False}, ]) answer_choices = gr.Radio(choices=[], label="Select an answer") with gr.Row(): prev_question_button = gr.Button("⬅ Previous Question") submit_exam_button = gr.Button("Submit Exam") next_question_button = gr.Button("Next Question ➡") results_display = gr.Markdown(label="Results") # --- Event Bindings --- # 1. Rebuild extracted text + vector store whenever the file list changes upload.change( fn=process_file_uploads, inputs=[upload, chatbot], outputs=[chatbot, study_context, vector_store_state, upload_status], ) # 2. Submit user chat message message.submit( put_message_in_chatbot, inputs=[message, chatbot], outputs=[message, chatbot], ).then( chat, inputs=[chatbot, study_context, vector_store_state], outputs=chatbot, ) # 3. Trigger note generation from stored context generate_notes_button.click( fn=lambda: "⏳ Generating notes...", outputs=[notes_output], ).then( fn=generate_notes, inputs=[study_context], outputs=[notes_output], ) # 4. Flashcards — generation + navigation. generate_flashcards_button.click( fn=lambda: "⏳ Generating flashcards...", outputs=[card_display], ).then( fn=generate_flashcards_handler, inputs=[study_context], outputs=[flashcards_state, current_index, show_answer, card_display], ) flip_button.click( fn=flip_card, inputs=[flashcards_state, current_index, show_answer], outputs=[show_answer, card_display], ) next_button.click( fn=next_card, inputs=[flashcards_state, current_index], outputs=[current_index, show_answer, card_display], ) prev_button.click( fn=prev_card, inputs=[flashcards_state, current_index], outputs=[current_index, show_answer, card_display], ) # 5. Practice Exam generate_exam_button.click( fn=generate_exam_handler, inputs=[study_context], outputs=[exam_questions_state, exam_index, user_answers_state, exam_submitted_state, question_display, answer_choices, results_display] ) next_question_button.click( fn=next_question, inputs=[exam_questions_state, exam_index, user_answers_state], outputs=[exam_index, question_display, answer_choices] ) prev_question_button.click( fn=prev_question, inputs=[exam_questions_state, exam_index, user_answers_state], outputs=[exam_index, question_display, answer_choices] ) submit_exam_button.click( fn=grade_exam, inputs=[exam_questions_state, user_answers_state], outputs=[results_display] ) answer_choices.change( fn=record_answer, inputs=[user_answers_state, exam_index, answer_choices], outputs=[user_answers_state] ) ui.launch(theme=gr.themes.Ocean())