Spaces:
Runtime error
Runtime error
| # Anki_Validation_tab.py | |
| # Description: Gradio functions for the Anki Validation tab | |
| # | |
| # Imports | |
| import json | |
| import logging | |
| import os | |
| import tempfile | |
| from typing import Optional, Tuple, List, Dict | |
| # | |
| # External Imports | |
| import genanki | |
| import gradio as gr | |
| # | |
| # Local Imports | |
| from App_Function_Libraries.Chat.Chat_Functions import approximate_token_count, update_chat_content, save_chat_history, \ | |
| save_chat_history_to_db_wrapper | |
| from App_Function_Libraries.DB.DB_Manager import list_prompts | |
| from App_Function_Libraries.Gradio_UI.Chat_ui import update_dropdown_multiple, chat_wrapper, update_selected_parts, \ | |
| search_conversations, regenerate_last_message, load_conversation, debug_output | |
| from App_Function_Libraries.Third_Party.Anki import sanitize_html, generate_card_choices, \ | |
| export_cards, load_card_for_editing, handle_file_upload, \ | |
| validate_for_ui, update_card_with_validation, update_card_choices, enhanced_file_upload, \ | |
| handle_validation | |
| from App_Function_Libraries.Utils.Utils import default_api_endpoint, global_api_endpoints, format_api_name | |
| # | |
| ############################################################################################################ | |
| # | |
| # Functions: | |
| def create_anki_validation_tab(): | |
| with gr.TabItem("Anki Flashcard Validation", visible=True): | |
| gr.Markdown("# Anki Flashcard Validation and Editor") | |
| # State variables for internal tracking | |
| current_card_data = gr.State({}) | |
| preview_update_flag = gr.State(False) | |
| with gr.Row(): | |
| # Left Column: Input and Validation | |
| with gr.Column(scale=1): | |
| gr.Markdown("## Import or Create Flashcards") | |
| input_type = gr.Radio( | |
| choices=["JSON", "APKG"], | |
| label="Input Type", | |
| value="JSON" | |
| ) | |
| with gr.Group() as json_input_group: | |
| flashcard_input = gr.TextArea( | |
| label="Enter Flashcards (JSON format)", | |
| placeholder='''{ | |
| "cards": [ | |
| { | |
| "id": "CARD_001", | |
| "type": "basic", | |
| "front": "What is the capital of France?", | |
| "back": "Paris", | |
| "tags": ["geography", "europe"], | |
| "note": "Remember: City of Light" | |
| } | |
| ] | |
| }''', | |
| lines=10 | |
| ) | |
| import_json = gr.File( | |
| label="Or Import JSON File", | |
| file_types=[".json"] | |
| ) | |
| with gr.Group(visible=False) as apkg_input_group: | |
| import_apkg = gr.File( | |
| label="Import APKG File", | |
| file_types=[".apkg"] | |
| ) | |
| deck_info = gr.JSON( | |
| label="Deck Information", | |
| visible=False | |
| ) | |
| validate_button = gr.Button("Validate Flashcards") | |
| # Right Column: Validation Results and Editor | |
| with gr.Column(scale=1): | |
| gr.Markdown("## Validation Results") | |
| validation_status = gr.Markdown("") | |
| with gr.Accordion("Validation Rules", open=False): | |
| gr.Markdown(""" | |
| ### Required Fields: | |
| - Unique ID | |
| - Card Type (basic, cloze, reverse) | |
| - Front content | |
| - Back content | |
| - At least one tag | |
| ### Content Rules: | |
| - No empty fields | |
| - Front side should be a clear question/prompt | |
| - Back side should contain complete answer | |
| - Cloze deletions must have valid syntax | |
| - No duplicate IDs | |
| ### Image Rules: | |
| - Valid image tags | |
| - Supported formats (JPG, PNG, GIF) | |
| - Base64 encoded or valid URL | |
| ### APKG-specific Rules: | |
| - Valid SQLite database structure | |
| - Media files properly referenced | |
| - Note types match Anki standards | |
| - Card templates are well-formed | |
| """) | |
| with gr.Row(): | |
| # Card Editor | |
| gr.Markdown("## Card Editor") | |
| with gr.Row(): | |
| with gr.Column(scale=1): | |
| with gr.Accordion("Edit Individual Cards", open=True): | |
| card_selector = gr.Dropdown( | |
| label="Select Card to Edit", | |
| choices=[], | |
| interactive=True | |
| ) | |
| card_type = gr.Radio( | |
| choices=["basic", "cloze", "reverse"], | |
| label="Card Type", | |
| value="basic" | |
| ) | |
| # Front content with preview | |
| with gr.Group(): | |
| gr.Markdown("### Front Content") | |
| front_content = gr.TextArea( | |
| label="Content (HTML supported)", | |
| lines=3 | |
| ) | |
| front_preview = gr.HTML( | |
| label="Preview" | |
| ) | |
| # Back content with preview | |
| with gr.Group(): | |
| gr.Markdown("### Back Content") | |
| back_content = gr.TextArea( | |
| label="Content (HTML supported)", | |
| lines=3 | |
| ) | |
| back_preview = gr.HTML( | |
| label="Preview" | |
| ) | |
| tags_input = gr.TextArea( | |
| label="Tags (comma-separated)", | |
| lines=1 | |
| ) | |
| notes_input = gr.TextArea( | |
| label="Additional Notes", | |
| lines=2 | |
| ) | |
| with gr.Row(): | |
| update_card_button = gr.Button("Update Card") | |
| delete_card_button = gr.Button("Delete Card", variant="stop") | |
| with gr.Row(): | |
| with gr.Column(scale=1): | |
| # Export Options | |
| gr.Markdown("## Export Options") | |
| export_format = gr.Radio( | |
| choices=["Anki CSV", "JSON", "Plain Text"], | |
| label="Export Format", | |
| value="Anki CSV" | |
| ) | |
| export_button = gr.Button("Export Valid Cards") | |
| export_file = gr.File(label="Download Validated Cards") | |
| export_status = gr.Markdown("") | |
| with gr.Column(scale=1): | |
| gr.Markdown("## Export Instructions") | |
| gr.Markdown(""" | |
| ### Anki CSV Format: | |
| - Front, Back, Tags, Type, Note | |
| - Use for importing into Anki | |
| - Images preserved as HTML | |
| ### JSON Format: | |
| - JSON array of cards | |
| - Images as base64 or URLs | |
| - Use for custom processing | |
| ### Plain Text Format: | |
| - Question and Answer pairs | |
| - Images represented as [IMG] placeholder | |
| - Use for manual review | |
| """) | |
| def update_preview(content): | |
| """Update preview with sanitized content.""" | |
| if not content: | |
| return "" | |
| return sanitize_html(content) | |
| # Event handlers | |
| def validation_chain(content: str) -> Tuple[str, List[str]]: | |
| """Combined validation and card choice update.""" | |
| validation_message = validate_for_ui(content) | |
| card_choices = update_card_choices(content) | |
| return validation_message, card_choices | |
| def delete_card(card_selection, current_content): | |
| """Delete selected card and return updated content.""" | |
| if not card_selection or not current_content: | |
| return current_content, "No card selected", [] | |
| try: | |
| data = json.loads(current_content) | |
| selected_id = card_selection.split(" - ")[0] | |
| data['cards'] = [card for card in data['cards'] if card['id'] != selected_id] | |
| new_content = json.dumps(data, indent=2) | |
| return ( | |
| new_content, | |
| "Card deleted successfully!", | |
| generate_card_choices(new_content) | |
| ) | |
| except Exception as e: | |
| return current_content, f"Error deleting card: {str(e)}", [] | |
| def process_validation_result(is_valid, message): | |
| """Process validation result into a formatted markdown string.""" | |
| if is_valid: | |
| return f"✅ {message}" | |
| else: | |
| return f"❌ {message}" | |
| # Register event handlers | |
| input_type.change( | |
| fn=lambda t: ( | |
| gr.update(visible=t == "JSON"), | |
| gr.update(visible=t == "APKG"), | |
| gr.update(visible=t == "APKG") | |
| ), | |
| inputs=[input_type], | |
| outputs=[json_input_group, apkg_input_group, deck_info] | |
| ) | |
| # File upload handlers | |
| import_json.upload( | |
| fn=handle_file_upload, | |
| inputs=[import_json, input_type], | |
| outputs=[ | |
| flashcard_input, | |
| deck_info, | |
| validation_status, | |
| card_selector | |
| ] | |
| ) | |
| import_apkg.upload( | |
| fn=enhanced_file_upload, | |
| inputs=[import_apkg, input_type], | |
| outputs=[ | |
| flashcard_input, | |
| deck_info, | |
| validation_status, | |
| card_selector | |
| ] | |
| ) | |
| # Validation handler | |
| validate_button.click( | |
| fn=lambda content, input_format: ( | |
| handle_validation(content, input_format), | |
| generate_card_choices(content) if content else [] | |
| ), | |
| inputs=[flashcard_input, input_type], | |
| outputs=[validation_status, card_selector] | |
| ) | |
| # Card editing handlers | |
| # Card selector change event | |
| card_selector.change( | |
| fn=load_card_for_editing, | |
| inputs=[card_selector, flashcard_input], | |
| outputs=[ | |
| card_type, | |
| front_content, | |
| back_content, | |
| tags_input, | |
| notes_input, | |
| front_preview, | |
| back_preview | |
| ] | |
| ) | |
| # Live preview updates | |
| front_content.change( | |
| fn=update_preview, | |
| inputs=[front_content], | |
| outputs=[front_preview] | |
| ) | |
| back_content.change( | |
| fn=update_preview, | |
| inputs=[back_content], | |
| outputs=[back_preview] | |
| ) | |
| # Card update handler | |
| update_card_button.click( | |
| fn=update_card_with_validation, | |
| inputs=[ | |
| flashcard_input, | |
| card_selector, | |
| card_type, | |
| front_content, | |
| back_content, | |
| tags_input, | |
| notes_input | |
| ], | |
| outputs=[ | |
| flashcard_input, | |
| validation_status, | |
| card_selector | |
| ] | |
| ) | |
| # Delete card handler | |
| delete_card_button.click( | |
| fn=delete_card, | |
| inputs=[card_selector, flashcard_input], | |
| outputs=[flashcard_input, validation_status, card_selector] | |
| ) | |
| # Export handler | |
| export_button.click( | |
| fn=export_cards, | |
| inputs=[flashcard_input, export_format], | |
| outputs=[export_status, export_file] | |
| ) | |
| return ( | |
| flashcard_input, | |
| import_json, | |
| import_apkg, | |
| validate_button, | |
| validation_status, | |
| card_selector, | |
| card_type, | |
| front_content, | |
| back_content, | |
| front_preview, | |
| back_preview, | |
| tags_input, | |
| notes_input, | |
| update_card_button, | |
| delete_card_button, | |
| export_format, | |
| export_button, | |
| export_file, | |
| export_status, | |
| deck_info | |
| ) | |
| def create_anki_generator_tab(): | |
| with gr.TabItem("Anki Deck Generator", visible=True): | |
| try: | |
| default_value = None | |
| if default_api_endpoint: | |
| if default_api_endpoint in global_api_endpoints: | |
| default_value = format_api_name(default_api_endpoint) | |
| else: | |
| logging.warning(f"Default API endpoint '{default_api_endpoint}' not found in global_api_endpoints") | |
| except Exception as e: | |
| logging.error(f"Error setting default API endpoint: {str(e)}") | |
| default_value = None | |
| custom_css = """ | |
| .chatbot-container .message-wrap .message { | |
| font-size: 14px !important; | |
| } | |
| """ | |
| with gr.TabItem("LLM Chat & Anki Deck Creation", visible=True): | |
| gr.Markdown("# Chat with an LLM to help you come up with Questions/Answers for an Anki Deck") | |
| chat_history = gr.State([]) | |
| media_content = gr.State({}) | |
| selected_parts = gr.State([]) | |
| conversation_id = gr.State(None) | |
| initial_prompts, total_pages, current_page = list_prompts(page=1, per_page=10) | |
| with gr.Row(): | |
| with gr.Column(scale=1): | |
| search_query_input = gr.Textbox( | |
| label="Search Query", | |
| placeholder="Enter your search query here..." | |
| ) | |
| search_type_input = gr.Radio( | |
| choices=["Title", "Content", "Author", "Keyword"], | |
| value="Keyword", | |
| label="Search By" | |
| ) | |
| keyword_filter_input = gr.Textbox( | |
| label="Filter by Keywords (comma-separated)", | |
| placeholder="ml, ai, python, etc..." | |
| ) | |
| search_button = gr.Button("Search") | |
| items_output = gr.Dropdown(label="Select Item", choices=[], interactive=True) | |
| item_mapping = gr.State({}) | |
| with gr.Row(): | |
| use_content = gr.Checkbox(label="Use Content") | |
| use_summary = gr.Checkbox(label="Use Summary") | |
| use_prompt = gr.Checkbox(label="Use Prompt") | |
| save_conversation = gr.Checkbox(label="Save Conversation", value=False, visible=True) | |
| with gr.Row(): | |
| temperature = gr.Slider(label="Temperature", minimum=0.00, maximum=1.0, step=0.05, value=0.7) | |
| with gr.Row(): | |
| conversation_search = gr.Textbox(label="Search Conversations") | |
| with gr.Row(): | |
| search_conversations_btn = gr.Button("Search Conversations") | |
| with gr.Row(): | |
| previous_conversations = gr.Dropdown(label="Select Conversation", choices=[], interactive=True) | |
| with gr.Row(): | |
| load_conversations_btn = gr.Button("Load Selected Conversation") | |
| # Refactored API selection dropdown | |
| api_endpoint = gr.Dropdown( | |
| choices=["None"] + [format_api_name(api) for api in global_api_endpoints], | |
| value=default_value, | |
| label="API for Chat Interaction (Optional)" | |
| ) | |
| api_key = gr.Textbox(label="API Key (if required)", type="password") | |
| custom_prompt_checkbox = gr.Checkbox(label="Use a Custom Prompt", | |
| value=False, | |
| visible=True) | |
| preset_prompt_checkbox = gr.Checkbox(label="Use a Pre-set Prompt", | |
| value=False, | |
| visible=True) | |
| with gr.Row(visible=False) as preset_prompt_controls: | |
| prev_prompt_page = gr.Button("Previous") | |
| next_prompt_page = gr.Button("Next") | |
| current_prompt_page_text = gr.Text(f"Page {current_page} of {total_pages}") | |
| current_prompt_page_state = gr.State(value=1) | |
| preset_prompt = gr.Dropdown( | |
| label="Select Preset Prompt", | |
| choices=initial_prompts | |
| ) | |
| user_prompt = gr.Textbox(label="Custom Prompt", | |
| placeholder="Enter custom prompt here", | |
| lines=3, | |
| visible=False) | |
| system_prompt_input = gr.Textbox(label="System Prompt", | |
| value="You are a helpful AI assitant", | |
| lines=3, | |
| visible=False) | |
| with gr.Column(scale=2): | |
| chatbot = gr.Chatbot(height=800, elem_classes="chatbot-container") | |
| msg = gr.Textbox(label="Enter your message") | |
| submit = gr.Button("Submit") | |
| regenerate_button = gr.Button("Regenerate Last Message") | |
| token_count_display = gr.Number(label="Approximate Token Count", value=0, interactive=False) | |
| clear_chat_button = gr.Button("Clear Chat") | |
| chat_media_name = gr.Textbox(label="Custom Chat Name(optional)") | |
| save_chat_history_to_db = gr.Button("Save Chat History to DataBase") | |
| save_status = gr.Textbox(label="Save Status", interactive=False) | |
| save_chat_history_as_file = gr.Button("Save Chat History as File") | |
| download_file = gr.File(label="Download Chat History") | |
| search_button.click( | |
| fn=update_dropdown_multiple, | |
| inputs=[search_query_input, search_type_input, keyword_filter_input], | |
| outputs=[items_output, item_mapping] | |
| ) | |
| def update_prompt_visibility(custom_prompt_checked, preset_prompt_checked): | |
| user_prompt_visible = custom_prompt_checked | |
| system_prompt_visible = custom_prompt_checked | |
| preset_prompt_visible = preset_prompt_checked | |
| preset_prompt_controls_visible = preset_prompt_checked | |
| return ( | |
| gr.update(visible=user_prompt_visible, interactive=user_prompt_visible), | |
| gr.update(visible=system_prompt_visible, interactive=system_prompt_visible), | |
| gr.update(visible=preset_prompt_visible, interactive=preset_prompt_visible), | |
| gr.update(visible=preset_prompt_controls_visible) | |
| ) | |
| def update_prompt_page(direction, current_page_val): | |
| new_page = current_page_val + direction | |
| if new_page < 1: | |
| new_page = 1 | |
| prompts, total_pages, _ = list_prompts(page=new_page, per_page=20) | |
| if new_page > total_pages: | |
| new_page = total_pages | |
| prompts, total_pages, _ = list_prompts(page=new_page, per_page=20) | |
| return ( | |
| gr.update(choices=prompts), | |
| gr.update(value=f"Page {new_page} of {total_pages}"), | |
| new_page | |
| ) | |
| def clear_chat(): | |
| return [], None # Return empty list for chatbot and None for conversation_id | |
| custom_prompt_checkbox.change( | |
| update_prompt_visibility, | |
| inputs=[custom_prompt_checkbox, preset_prompt_checkbox], | |
| outputs=[user_prompt, system_prompt_input, preset_prompt, preset_prompt_controls] | |
| ) | |
| preset_prompt_checkbox.change( | |
| update_prompt_visibility, | |
| inputs=[custom_prompt_checkbox, preset_prompt_checkbox], | |
| outputs=[user_prompt, system_prompt_input, preset_prompt, preset_prompt_controls] | |
| ) | |
| prev_prompt_page.click( | |
| lambda x: update_prompt_page(-1, x), | |
| inputs=[current_prompt_page_state], | |
| outputs=[preset_prompt, current_prompt_page_text, current_prompt_page_state] | |
| ) | |
| next_prompt_page.click( | |
| lambda x: update_prompt_page(1, x), | |
| inputs=[current_prompt_page_state], | |
| outputs=[preset_prompt, current_prompt_page_text, current_prompt_page_state] | |
| ) | |
| submit.click( | |
| chat_wrapper, | |
| inputs=[msg, chatbot, media_content, selected_parts, api_endpoint, api_key, user_prompt, | |
| conversation_id, | |
| save_conversation, temperature, system_prompt_input], | |
| outputs=[msg, chatbot, conversation_id] | |
| ).then( # Clear the message box after submission | |
| lambda x: gr.update(value=""), | |
| inputs=[chatbot], | |
| outputs=[msg] | |
| ).then( # Clear the user prompt after the first message | |
| lambda: (gr.update(value=""), gr.update(value="")), | |
| outputs=[user_prompt, system_prompt_input] | |
| ).then( | |
| lambda history: approximate_token_count(history), | |
| inputs=[chatbot], | |
| outputs=[token_count_display] | |
| ) | |
| clear_chat_button.click( | |
| clear_chat, | |
| outputs=[chatbot, conversation_id] | |
| ) | |
| items_output.change( | |
| update_chat_content, | |
| inputs=[items_output, use_content, use_summary, use_prompt, item_mapping], | |
| outputs=[media_content, selected_parts] | |
| ) | |
| use_content.change(update_selected_parts, inputs=[use_content, use_summary, use_prompt], | |
| outputs=[selected_parts]) | |
| use_summary.change(update_selected_parts, inputs=[use_content, use_summary, use_prompt], | |
| outputs=[selected_parts]) | |
| use_prompt.change(update_selected_parts, inputs=[use_content, use_summary, use_prompt], | |
| outputs=[selected_parts]) | |
| items_output.change(debug_output, inputs=[media_content, selected_parts], outputs=[]) | |
| search_conversations_btn.click( | |
| search_conversations, | |
| inputs=[conversation_search], | |
| outputs=[previous_conversations] | |
| ) | |
| load_conversations_btn.click( | |
| clear_chat, | |
| outputs=[chatbot, chat_history] | |
| ).then( | |
| load_conversation, | |
| inputs=[previous_conversations], | |
| outputs=[chatbot, conversation_id] | |
| ) | |
| previous_conversations.change( | |
| load_conversation, | |
| inputs=[previous_conversations], | |
| outputs=[chat_history] | |
| ) | |
| save_chat_history_as_file.click( | |
| save_chat_history, | |
| inputs=[chatbot, conversation_id], | |
| outputs=[download_file] | |
| ) | |
| save_chat_history_to_db.click( | |
| save_chat_history_to_db_wrapper, | |
| inputs=[chatbot, conversation_id, media_content, chat_media_name], | |
| outputs=[conversation_id, gr.Textbox(label="Save Status")] | |
| ) | |
| regenerate_button.click( | |
| regenerate_last_message, | |
| inputs=[chatbot, media_content, selected_parts, api_endpoint, api_key, user_prompt, temperature, | |
| system_prompt_input], | |
| outputs=[chatbot, save_status] | |
| ).then( | |
| lambda history: approximate_token_count(history), | |
| inputs=[chatbot], | |
| outputs=[token_count_display] | |
| ) | |
| gr.Markdown("# Create Anki Deck") | |
| with gr.Row(): | |
| # Left Column: Deck Settings | |
| with gr.Column(scale=1): | |
| gr.Markdown("## Deck Settings") | |
| deck_name = gr.Textbox( | |
| label="Deck Name", | |
| placeholder="My Study Deck", | |
| value="My Study Deck" | |
| ) | |
| deck_description = gr.Textbox( | |
| label="Deck Description", | |
| placeholder="Description of your deck", | |
| lines=2 | |
| ) | |
| note_type = gr.Radio( | |
| choices=["Basic", "Basic (and reversed)", "Cloze"], | |
| label="Note Type", | |
| value="Basic" | |
| ) | |
| # Card Fields based on note type | |
| with gr.Group() as basic_fields: | |
| front_template = gr.Textbox( | |
| label="Front Template (HTML)", | |
| value="{{Front}}", | |
| lines=3 | |
| ) | |
| back_template = gr.Textbox( | |
| label="Back Template (HTML)", | |
| value="{{FrontSide}}<hr id='answer'>{{Back}}", | |
| lines=3 | |
| ) | |
| with gr.Group() as cloze_fields: | |
| cloze_template = gr.Textbox( | |
| label="Cloze Template (HTML)", | |
| value="{{cloze:Text}}", | |
| lines=3, | |
| visible=False | |
| ) | |
| css_styling = gr.Textbox( | |
| label="Card Styling (CSS)", | |
| value=".card {\n font-family: arial;\n font-size: 20px;\n text-align: center;\n color: black;\n background-color: white;\n}\n\n.cloze {\n font-weight: bold;\n color: blue;\n}", | |
| lines=5 | |
| ) | |
| # Right Column: Card Creation | |
| with gr.Column(scale=1): | |
| gr.Markdown("## Add Cards") | |
| with gr.Group() as basic_input: | |
| front_content = gr.TextArea( | |
| label="Front Content", | |
| placeholder="Question or prompt", | |
| lines=3 | |
| ) | |
| back_content = gr.TextArea( | |
| label="Back Content", | |
| placeholder="Answer", | |
| lines=3 | |
| ) | |
| with gr.Group() as cloze_input: | |
| cloze_content = gr.TextArea( | |
| label="Cloze Content", | |
| placeholder="Text with {{c1::cloze}} deletions", | |
| lines=3, | |
| visible=False | |
| ) | |
| tags_input = gr.TextArea( | |
| label="Tags (comma-separated)", | |
| placeholder="tag1, tag2, tag3", | |
| lines=1 | |
| ) | |
| add_card_btn = gr.Button("Add Card") | |
| cards_list = gr.JSON( | |
| label="Cards in Deck", | |
| value={"cards": []} | |
| ) | |
| clear_cards_btn = gr.Button("Clear All Cards", variant="stop") | |
| with gr.Row(): | |
| generate_deck_btn = gr.Button("Generate Deck", variant="primary") | |
| download_deck = gr.File(label="Download Deck") | |
| generation_status = gr.Markdown("") | |
| def update_note_type_fields(note_type: str): | |
| if note_type == "Cloze": | |
| return { | |
| basic_input: gr.update(visible=False), | |
| cloze_input: gr.update(visible=True), | |
| basic_fields: gr.update(visible=False), | |
| cloze_fields: gr.update(visible=True) | |
| } | |
| else: | |
| return { | |
| basic_input: gr.update(visible=True), | |
| cloze_input: gr.update(visible=False), | |
| basic_fields: gr.update(visible=True), | |
| cloze_fields: gr.update(visible=False) | |
| } | |
| def add_card(note_type: str, front: str, back: str, cloze: str, tags: str, current_cards: Dict[str, List]): | |
| if not current_cards: | |
| current_cards = {"cards": []} | |
| cards_data = current_cards["cards"] | |
| # Process tags | |
| card_tags = [tag.strip() for tag in tags.split(',') if tag.strip()] | |
| new_card = { | |
| "id": f"CARD_{len(cards_data) + 1}", | |
| "tags": card_tags | |
| } | |
| if note_type == "Cloze": | |
| if not cloze or "{{c" not in cloze: | |
| return current_cards, "❌ Invalid cloze format. Use {{c1::text}} syntax." | |
| new_card.update({ | |
| "type": "cloze", | |
| "content": cloze | |
| }) | |
| else: | |
| if not front or not back: | |
| return current_cards, "❌ Both front and back content are required." | |
| new_card.update({ | |
| "type": "basic", | |
| "front": front, | |
| "back": back, | |
| "is_reverse": note_type == "Basic (and reversed)" | |
| }) | |
| cards_data.append(new_card) | |
| return {"cards": cards_data}, "✅ Card added successfully!" | |
| def clear_cards() -> Tuple[Dict[str, List], str]: | |
| return {"cards": []}, "✅ All cards cleared!" | |
| def generate_anki_deck( | |
| deck_name: str, | |
| deck_description: str, | |
| note_type: str, | |
| front_template: str, | |
| back_template: str, | |
| cloze_template: str, | |
| css: str, | |
| cards_data: Dict[str, List] | |
| ) -> Tuple[Optional[str], str]: | |
| try: | |
| if not cards_data or not cards_data.get("cards"): | |
| return None, "❌ No cards to generate deck from!" | |
| # Create model based on note type | |
| if note_type == "Cloze": | |
| model = genanki.Model( | |
| 1483883320, # Random model ID | |
| 'Cloze Model', | |
| fields=[ | |
| {'name': 'Text'}, | |
| {'name': 'Back Extra'} | |
| ], | |
| templates=[{ | |
| 'name': 'Cloze Card', | |
| 'qfmt': cloze_template, | |
| 'afmt': cloze_template + '<br><hr id="extra">{{Back Extra}}' | |
| }], | |
| css=css, | |
| # FIXME CLOZE DOESNT EXIST | |
| model_type=1 | |
| ) | |
| else: | |
| templates = [{ | |
| 'name': 'Card 1', | |
| 'qfmt': front_template, | |
| 'afmt': back_template | |
| }] | |
| if note_type == "Basic (and reversed)": | |
| templates.append({ | |
| 'name': 'Card 2', | |
| 'qfmt': '{{Back}}', | |
| 'afmt': '{{FrontSide}}<hr id="answer">{{Front}}' | |
| }) | |
| model = genanki.Model( | |
| 1607392319, # Random model ID | |
| 'Basic Model', | |
| fields=[ | |
| {'name': 'Front'}, | |
| {'name': 'Back'} | |
| ], | |
| templates=templates, | |
| css=css | |
| ) | |
| # Create deck | |
| deck = genanki.Deck( | |
| 2059400110, # Random deck ID | |
| deck_name, | |
| description=deck_description | |
| ) | |
| # Add cards to deck | |
| for card in cards_data["cards"]: | |
| if card["type"] == "cloze": | |
| note = genanki.Note( | |
| model=model, | |
| fields=[card["content"], ""], | |
| tags=card["tags"] | |
| ) | |
| else: | |
| note = genanki.Note( | |
| model=model, | |
| fields=[card["front"], card["back"]], | |
| tags=card["tags"] | |
| ) | |
| deck.add_note(note) | |
| # Save deck to temporary file | |
| temp_dir = tempfile.mkdtemp() | |
| deck_path = os.path.join(temp_dir, f"{deck_name}.apkg") | |
| genanki.Package(deck).write_to_file(deck_path) | |
| return deck_path, "✅ Deck generated successfully!" | |
| except Exception as e: | |
| return None, f"❌ Error generating deck: {str(e)}" | |
| # Register event handlers | |
| note_type.change( | |
| fn=update_note_type_fields, | |
| inputs=[note_type], | |
| outputs=[basic_input, cloze_input, basic_fields, cloze_fields] | |
| ) | |
| add_card_btn.click( | |
| fn=add_card, | |
| inputs=[ | |
| note_type, | |
| front_content, | |
| back_content, | |
| cloze_content, | |
| tags_input, | |
| cards_list | |
| ], | |
| outputs=[cards_list, generation_status] | |
| ) | |
| clear_cards_btn.click( | |
| fn=clear_cards, | |
| inputs=[], | |
| outputs=[cards_list, generation_status] | |
| ) | |
| generate_deck_btn.click( | |
| fn=generate_anki_deck, | |
| inputs=[ | |
| deck_name, | |
| deck_description, | |
| note_type, | |
| front_template, | |
| back_template, | |
| cloze_template, | |
| css_styling, | |
| cards_list | |
| ], | |
| outputs=[download_deck, generation_status] | |
| ) | |
| return ( | |
| deck_name, | |
| deck_description, | |
| note_type, | |
| front_template, | |
| back_template, | |
| cloze_template, | |
| css_styling, | |
| front_content, | |
| back_content, | |
| cloze_content, | |
| tags_input, | |
| cards_list, | |
| add_card_btn, | |
| clear_cards_btn, | |
| generate_deck_btn, | |
| download_deck, | |
| generation_status | |
| ) | |
| # | |
| # End of Anki_Validation_tab.py | |
| ############################################################################################################ | |