# 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}}