# Standard library imports import os from pathlib import Path # Potentially for favicon_path from functools import partial # Moved to utils import gradio as gr import pandas as pd from ankigen_core.utils import ( get_logger, ResponseCache, ) # fetch_webpage_text is used by card_generator from ankigen_core.llm_interface import ( OpenAIClientManager, ) # structured_output_completion is internal to core modules from ankigen_core.card_generator import ( orchestrate_card_generation, AVAILABLE_MODELS, ) # GENERATION_MODES is internal to card_generator from ankigen_core.learning_path import analyze_learning_path from ankigen_core.exporters import ( export_csv, export_deck, ) # Anki models (BASIC_MODEL, CLOZE_MODEL) are internal to exporters from ankigen_core.ui_logic import update_mode_visibility, use_selected_subjects # --- Initialization --- logger = get_logger() response_cache = ResponseCache() # Initialize cache client_manager = OpenAIClientManager() # Initialize client manager js_storage = """ async () => { const loadDecks = () => { const decks = localStorage.getItem('ankigen_decks'); return decks ? JSON.parse(decks) : []; }; const saveDecks = (decks) => { localStorage.setItem('ankigen_decks', JSON.stringify(decks)); }; window.loadStoredDecks = loadDecks; window.saveStoredDecks = saveDecks; return loadDecks(); } """ custom_theme = gr.themes.Soft().set( body_background_fill="*background_fill_secondary", block_background_fill="*background_fill_primary", block_border_width="0", button_primary_background_fill="*primary_500", button_primary_text_color="white", ) # --- Example Data for Initialization --- example_data = pd.DataFrame( [ [ "1.1", "SQL Basics", "basic", "What is a SELECT statement used for?", "Retrieving data from one or more database tables.", "The SELECT statement is the most common command in SQL...", "```sql\nSELECT column1, column2 FROM my_table WHERE condition;\n```", ["Understanding of database tables"], ["Retrieve specific data"], ["❌ SELECT * is always efficient (Reality: Can be slow for large tables)"], "beginner", ], [ "2.1", "Python Fundamentals", "cloze", "The primary keyword to define a function in Python is {{c1::def}}.", "def", "Functions are defined using the `def` keyword...", r"""```python def greet(name): print(f"Hello, {name}!") ```""", ["Basic programming concepts"], ["Define reusable blocks of code"], ["❌ Forgetting the colon (:) after the definition"], "beginner", ], ], columns=[ "Index", "Topic", "Card_Type", "Question", "Answer", "Explanation", "Example", "Prerequisites", "Learning_Outcomes", "Common_Misconceptions", "Difficulty", ], ) # ------------------------------------- def create_ankigen_interface(): logger.info("Creating AnkiGen Gradio interface...") with gr.Blocks( theme=custom_theme, title="AnkiGen", css=""" #footer {display:none !important} .tall-dataframe {min-height: 500px !important} .contain {max-width: 100% !important; margin: auto;} .output-cards {border-radius: 8px; box-shadow: 0 4px 6px -1px rgba(0,0,0,0.1);} .hint-text {font-size: 0.9em; color: #666; margin-top: 4px;} .export-group > .gradio-group { margin-bottom: 0 !important; padding-bottom: 5px !important; } """, js=js_storage, ) as ankigen: with gr.Column(elem_classes="contain"): gr.Markdown("# 📚 AnkiGen - Advanced Anki Card Generator") gr.Markdown("#### Generate comprehensive Anki flashcards using AI.") with gr.Accordion("Configuration Settings", open=True): with gr.Row(): with gr.Column(scale=1): generation_mode = gr.Radio( choices=[ ("Single Subject", "subject"), ("Learning Path", "path"), ("From Text", "text"), ("From Web", "web"), ], value="subject", label="Generation Mode", info="Choose how you want to generate content", ) with gr.Group() as subject_mode: subject = gr.Textbox( label="Subject", placeholder="e.g., 'Basic SQL Concepts'", ) with gr.Group(visible=False) as path_mode: description = gr.Textbox( label="Learning Goal", placeholder="Paste a job description...", lines=5, ) analyze_button = gr.Button( "Analyze & Break Down", variant="secondary" ) with gr.Group(visible=False) as text_mode: source_text = gr.Textbox( label="Source Text", placeholder="Paste text here...", lines=15, ) with gr.Group(visible=False) as web_mode: url_input = gr.Textbox( label="Web Page URL", placeholder="Paste URL here..." ) api_key_input = gr.Textbox( label="OpenAI API Key", type="password", placeholder="Enter your OpenAI API key (sk-...)", value=os.getenv("OPENAI_API_KEY", ""), info="Your key is used solely for processing your requests.", elem_id="api-key-textbox", ) with gr.Column(scale=1): with gr.Accordion("Advanced Settings", open=False): model_choices_ui = [ (m["label"], m["value"]) for m in AVAILABLE_MODELS ] default_model_value = next( ( m["value"] for m in AVAILABLE_MODELS if "nano" in m["value"].lower() ), AVAILABLE_MODELS[0]["value"], ) model_choice = gr.Dropdown( choices=model_choices_ui, value=default_model_value, label="Model Selection", info="Select AI model for generation", ) _model_info = gr.Markdown( "**gpt-4.1**: Best quality | **gpt-4.1-nano**: Faster/Cheaper" ) topic_number = gr.Slider( label="Number of Topics", minimum=2, maximum=20, step=1, value=2, ) cards_per_topic = gr.Slider( label="Cards per Topic", minimum=2, maximum=30, step=1, value=3, ) preference_prompt = gr.Textbox( label="Learning Preferences", placeholder="e.g., 'Beginner focus'", lines=3, ) generate_cloze_checkbox = gr.Checkbox( label="Generate Cloze Cards (Experimental)", value=False ) generate_button = gr.Button("Generate Cards", variant="primary") with gr.Group(visible=False) as path_results: gr.Markdown("### Learning Path Analysis") subjects_list = gr.Dataframe( headers=["Subject", "Prerequisites", "Time Estimate"], label="Recommended Subjects", interactive=False, ) learning_order = gr.Markdown("### Recommended Learning Order") projects = gr.Markdown("### Suggested Projects") use_subjects = gr.Button("Use These Subjects â„šī¸", variant="primary") gr.Markdown( "*Click to copy subjects to main input*", elem_classes="hint-text" ) with gr.Group() as cards_output: gr.Markdown("### Generated Cards") with gr.Accordion("Output Format", open=False): gr.Markdown( "Cards: Index, Topic, Type, Q, A, Explanation, Example, Prerequisites, Outcomes, Misconceptions, Difficulty. Export: CSV, .apkg" ) with gr.Accordion("Example Card Format", open=False): gr.Code( label="Example Card", value='{"front": ..., "back": ..., "metadata": ...}', language="json", ) output = gr.Dataframe( value=example_data, headers=[ "Index", "Topic", "Card_Type", "Question", "Answer", "Explanation", "Example", "Prerequisites", "Learning_Outcomes", "Common_Misconceptions", "Difficulty", ], interactive=True, elem_classes="tall-dataframe", wrap=True, column_widths=[50, 100, 80, 200, 200, 250, 200, 150, 150, 150, 100], ) with gr.Group(elem_classes="export-group"): gr.Markdown("#### Export Generated Cards") with gr.Row(): export_csv_button = gr.Button( "Export to CSV", variant="secondary" ) export_anki_button = gr.Button( "Export to Anki Deck (.apkg)", variant="secondary" ) with gr.Row(): download_csv = gr.File(label="Download CSV", interactive=False) download_anki = gr.File( label="Download Anki Deck", interactive=False ) with gr.Row(): progress = gr.HTML(visible=False) total_cards = gr.Number( label="Total Cards Generated", value=0, visible=False ) # --- Event Handlers --- (Updated to use functions from ankigen_core) generation_mode.change( fn=update_mode_visibility, inputs=[generation_mode, subject, description, source_text, url_input], outputs=[ subject_mode, path_mode, text_mode, web_mode, path_results, cards_output, subject, description, source_text, url_input, output, subjects_list, learning_order, projects, progress, total_cards, ], ) analyze_button.click( fn=partial(analyze_learning_path, client_manager, response_cache), inputs=[ api_key_input, description, model_choice, ], outputs=[subjects_list, learning_order, projects], ) use_subjects.click( fn=use_selected_subjects, inputs=[subjects_list], outputs=[ generation_mode, subject_mode, path_mode, text_mode, web_mode, path_results, cards_output, subject, description, source_text, url_input, topic_number, preference_prompt, output, subjects_list, learning_order, projects, progress, total_cards, ], ) generate_button.click( fn=partial(orchestrate_card_generation, client_manager, response_cache), inputs=[ api_key_input, subject, generation_mode, source_text, url_input, model_choice, topic_number, cards_per_topic, preference_prompt, generate_cloze_checkbox, ], outputs=[output, progress, total_cards], show_progress="full", ) export_csv_button.click( fn=export_csv, inputs=[output], outputs=download_csv, show_progress="full", ) export_anki_button.click( fn=export_deck, inputs=[output, subject], outputs=download_anki, show_progress="full", ) logger.info("Gradio interface created.") return ankigen # --- Main Execution --- (Runs if script is executed directly) if __name__ == "__main__": try: ankigen_interface = create_ankigen_interface() logger.info("Launching AnkiGen Gradio interface...") # Ensure favicon.ico is in the same directory as app.py or provide correct path favicon_path = Path(__file__).parent / "favicon.ico" if favicon_path.exists(): ankigen_interface.launch(share=False, favicon_path=str(favicon_path)) else: logger.warning( f"Favicon not found at {favicon_path}, launching without it." ) ankigen_interface.launch(share=False) except Exception as e: logger.critical(f"Failed to launch Gradio interface: {e}", exc_info=True)