import flet as ft import json import requests import yaml import os import csv import logging from typing import Dict, List, Any from result import Ok, Result from anki import AnkiError # Set up logging logging.basicConfig( level=logging.INFO, format="%(asctime)s - %(name)s - %(levelname)s - %(message)s" ) logger = logging.getLogger("flashcard-creator") class FlashcardCreator: def __init__(self): # Anki configuration from anki import AnkiConnect self.anki = AnkiConnect() # File picker self.file_picker = ft.FilePicker() self.is_processing = False # LiteLLM configuration self.llm_provider = "openai" self.llm_model = "gpt-3.5-turbo" self.base_url = "" self.api_key = "" self.load_settings() # Languages self.source_language = "English" self.target_language = "German" self.languages = [ "English", "German", "French", "Spanish", "Italian", "Chinese", "Japanese", "Russian", ] # Flashcards self.flashcards = {} def load_settings(self): settings_path = os.path.join( os.path.expanduser("~"), ".flashcard_creator", "settings.json" ) settings = {} if os.path.exists(settings_path): with open(settings_path, "r") as f: try: settings = json.load(f) print(settings) self.llm_provider = settings.get("provider", self.llm_provider) self.llm_model = settings.get("model", self.llm_model) self.base_url = settings.get("base_url", self.base_url) # Load provider-specific settings provider_settings = settings.get(self.llm_provider, {}) self.llm_model = provider_settings.get("model", self.llm_model) self.base_url = provider_settings.get("base_url", self.base_url) self.api_key = provider_settings.get("api_key", self.api_key) except: pass def build_ui(self, page: ft.Page): page.title = "Flashcard Creator" page.theme_mode = ft.ThemeMode.LIGHT # Language selection dropdowns self.source_dropdown = ft.Dropdown( label="Source Language", options=[ft.dropdown.Option(lang) for lang in self.languages], value=self.source_language, width=200, on_change=self.on_source_language_change, ) self.target_dropdown = ft.Dropdown( label="Target Language", options=[ft.dropdown.Option(lang) for lang in self.languages], value=self.target_language, width=200, on_change=self.on_target_language_change, ) # Text input for content to process self.input_text = ft.TextField( label="Input Text", multiline=True, min_lines=5, max_lines=15, width=600 ) # Process button self.process_button = ft.ElevatedButton( text="Process Text", on_click=self.on_process_text ) # Settings button self.settings_button = ft.ElevatedButton( text="Settings", on_click=self.open_settings ) # Stack name and description self.stack_name = ft.TextField(label="Stack Name", width=300) self.stack_description = ft.TextField( label="Stack Description", multiline=True, min_lines=2, max_lines=4, width=600, ) # Action buttons self.add_to_anki_button = ft.ElevatedButton( text="Add to Anki", on_click=self.add_to_anki ) self.export_yaml_button = ft.ElevatedButton( text="Export as YAML", on_click=self.export_as_yaml ) self.export_csv_button = ft.ElevatedButton( text="Export as CSV", on_click=self.export_as_csv ) # Flashcard data table - make it expand to fill available width self.flashcard_table = ft.DataTable( columns=[ ft.DataColumn(ft.Text("Front")), ft.DataColumn(ft.Text("Back")), ft.DataColumn(ft.Text("Actions")), ], rows=[], expand=True, # Make table expand to fill available width column_spacing=10, # Space between columns horizontal_lines=ft.border.BorderSide(1, ft.colors.OUTLINE), vertical_lines=ft.border.BorderSide(1, ft.colors.OUTLINE), width=page.width, # Make table as wide as the window ) # Initialize the flashcard table with the "add new card" row self.add_new_card_row() # Wrap the table in a Container with specific height to ensure visibility flashcard_table_container = ft.Container( content=ft.Row( controls=[ ft.Column( expand=True, controls=[self.flashcard_table], scroll=ft.ScrollMode.ALWAYS, ) ], expand=True, ), padding=10, expand=True, border=ft.border.all(1, ft.colors.OUTLINE), border_radius=5, ) # Add file picker to page overlay page.overlay.append(self.file_picker) # Create a scrollable column for the main content main_content = ft.Column( [ ft.Row( [self.source_dropdown, self.target_dropdown, self.settings_button] ), ft.Divider(), ft.Text("Input Text", size=16, weight=ft.FontWeight.BOLD), self.input_text, self.process_button, ft.Divider(), ft.Text("Flashcard Information", size=16, weight=ft.FontWeight.BOLD), self.stack_name, self.stack_description, ft.Divider(), ft.Text("Generated Flashcards", size=16, weight=ft.FontWeight.BOLD), flashcard_table_container, ft.Row( [ self.add_to_anki_button, self.export_yaml_button, self.export_csv_button, ] ), ], scroll=ft.ScrollMode.AUTO, expand=True, ) # Add the scrollable content to the page page.add(main_content) def on_source_language_change(self, e): self.source_language = e.control.value def on_target_language_change(self, e): self.target_language = e.control.value def open_settings(self, e): print("opening settings..") self.load_settings() # Create a settings dialog def close_dialog_save(e): self.llm_provider = provider_dropdown.value self.llm_model = model_input.value self.api_key = api_key_input.value self.base_url = base_url_input.value # Save settings to local storage settings_dir = os.path.join(os.path.expanduser("~"), ".flashcard_creator") os.makedirs(settings_dir, exist_ok=True) settings_path = os.path.join(settings_dir, "settings.json") settings = {} if os.path.exists(settings_path): try: with open(settings_path, "r") as f: settings = json.load(f) except: pass # Update provider-specific settings settings[self.llm_provider] = { "model": self.llm_model, "base_url": self.base_url, "api_key": self.api_key, } # Save the currently selected provider settings["provider"] = self.llm_provider with open(settings_path, "w") as f: json.dump(settings, f, indent=4) e.page.close(dialog) e.page.update() # Function to update displayed settings when provider changes def on_provider_change(e): selected_provider = provider_dropdown.value # Load settings from file settings_path = os.path.join( os.path.expanduser("~"), ".flashcard_creator", "settings.json" ) settings = {} if os.path.exists(settings_path): try: with open(settings_path, "r") as f: settings = json.load(f) except: pass # Get selected provider's settings or empty dict if not found provider_settings = settings.get(selected_provider, {}) # Update the input fields with the provider's settings model_input.value = provider_settings.get("model", "") base_url_input.value = provider_settings.get("base_url", "") api_key_input.value = provider_settings.get("api_key", "") # Update the dialog e.page.update() # Load settings if they exist settings_path = os.path.join( os.path.expanduser("~"), ".flashcard_creator", "settings.json" ) settings = {} if os.path.exists(settings_path): with open(settings_path, "r") as f: try: settings = json.load(f) print(settings) self.llm_provider = settings.get("provider", self.llm_provider) # Load provider-specific settings provider_settings = settings.get(self.llm_provider, {}) self.llm_model = provider_settings.get("model", self.llm_model) self.base_url = provider_settings.get("base_url", self.base_url) self.api_key = provider_settings.get("api_key", self.api_key) except e: logger.info(f"Settings couldn't be loaded: {e}") pass provider_dropdown = ft.Dropdown( label="LLM Provider", options=[ ft.dropdown.Option("openai"), ft.dropdown.Option("anthropic"), ft.dropdown.Option("vertexai"), ft.dropdown.Option("huggingface"), ft.dropdown.Option("deepseek"), ], value=self.llm_provider, on_change=on_provider_change, # Add the on_change handler ) model_input = ft.TextField(label="Model Name", value=self.llm_model) base_url_input = ft.TextField(label="Base URL (optional)", value=self.base_url) api_key_input = ft.TextField(label="API Key", value=self.api_key, password=True) dialog = ft.AlertDialog( title=ft.Text("Settings"), content=ft.Column( [provider_dropdown, model_input, base_url_input, api_key_input], width=400, height=400, ), actions=[ ft.TextButton("Cancel", on_click=lambda e: e.page.close(dialog)), ft.TextButton("Save", on_click=close_dialog_save), ], ) e.page.open(dialog) e.page.update() def on_process_text(self, e): # Prevent multiple simultaneous processing if self.is_processing: e.page.open( ft.SnackBar(content=ft.Text("Processing in progress, please wait...")) ) return input_text = self.input_text.value if not input_text: e.page.open( ft.SnackBar(content=ft.Text("Please enter some text to process")) ) return logger.info("Processing input text") self.is_processing = True self.process_button.disabled = True e.page.update() try: # Use LiteLLM to process the text and generate flashcards flashcards = self.generate_flashcards(input_text, e.page) # Update the data table with the generated flashcards self.update_flashcard_table(flashcards, e.page) finally: # Reset processing state and re-enable button self.is_processing = False self.process_button.disabled = False e.page.update() def generate_flashcards(self, text: str, page: ft.Page = None) -> Dict: """Use LiteLLM to generate flashcards from the input text.""" from litellm import completion # Show progress bar progress = None if page: progress = ft.ProgressBar(width=max(page.width - 40, 200)) page.add( ft.Column( [ ft.Text( f"Generating flashcards using {self.llm_provider}/{self.llm_model}", size=16, ), progress, ] ) ) page.update() logger.info( f"Generating flashcards: {self.source_language} -> {self.target_language}" ) logger.info(f"Using model: {self.llm_provider}/{self.llm_model}") prompt = f"""You are a language learning assistant creating flashcards to help students learn {self.target_language} from {self.source_language}. TASK: Create effective flashcards from the provided text. INSTRUCTIONS: 1. Extract important vocabulary, phrases, and concepts from the text 2. Create flashcards with {self.source_language} on the front and {self.target_language} on the back 3. For vocabulary words: - Include articles for nouns (e.g., "der/die/das" in German) - Include plural forms where appropriate - Include gender information for languages that have grammatical gender 4. For verbs, include the infinitive form and note if irregular 5. For idiomatic expressions, provide the closest equivalent in the target language 6. Choose content that represents different difficulty levels 7. Ensure translations are accurate and natural-sounding in the target language 8. Make sure to create flashcards for ALL relevant words in the input! If you are unsure, err on the side of too many flashcards. OUTPUT FORMAT: Return your response in valid YAML format with the following structure: ```yaml name: [Create a concise, descriptive name for this flashcard set] description: [Write a brief description of the content covered] cards: - "[source term 1]": "[target translation 1]" - "[source term 2]": "[target translation 2]" - "[source phrase]": "[target translation with grammatical notes if needed]" ``` For example, for Chinese -> German, based on some vocab text including the following words, your output could look like this: ```yaml name: Basic Chinese to German Vocabulary description: Essential vocabulary and phrases translated from Chinese to German cards: - "你好": "Hallo" - "谢谢": "Danke" - "再见": "Auf Wiedersehen" - "苹果": "der Apfel (pl. die Äpfel)" - "书": "das Buch (pl. die Bücher)" - "学生": "der/die Student/Studentin (pl. die Studenten/Studentinnen)" - "喝水": "Wasser trinken (verb, regular)" - "吃饭": "essen (verb, irregular: isst, aß, gegessen)" - "我很高兴认识你": "Es freut mich, dich kennenzulernen" - "慢走": "Komm gut nach Hause (idiom.)" ``` Here's the text to process: {text} """ try: # Set the API key for the selected provider # Load provider-specific settings settings_path = os.path.join( os.path.expanduser("~"), ".flashcard_creator", "settings.json" ) settings = {} if os.path.exists(settings_path): with open(settings_path, "r") as f: try: settings = json.load(f) except: pass provider_settings = settings.get(self.llm_provider, {}) api_key = provider_settings.get("api_key", "") os.environ[f"{self.llm_provider.upper()}_API_KEY"] = api_key logger.info("Sending request to LLM API...") # Use LiteLLM to generate flashcards response = completion( model=f"{self.llm_provider}/{self.llm_model}", api_base=self.base_url if self.base_url else None, messages=[{"role": "user", "content": prompt}], ) logger.info("Received response from LLM API") # Extract the YAML content from the response yaml_content = response.choices[0].message.content logger.info("Extracting YAML content from response") # Extract YAML content from code blocks if present if "```yaml" in yaml_content: # Extract content between yaml and the next yaml_content = ( yaml_content.split("```yaml", 1)[1].split("```")[0].strip() ) elif "```" in yaml_content: # Try to extract content between any yaml_blocks = yaml_content.split("```") if len(yaml_blocks) >= 3: # At least one complete code block yaml_content = yaml_blocks[1].strip() # Parse the YAML content logger.info("Parsing YAML content") flashcards = yaml.safe_load(yaml_content) logger.info( f"Generated {len(flashcards.get('cards', []))} flashcards successfully" ) return flashcards except Exception as e: logger.error(f"Error generating flashcards: {e}", exc_info=True) if page: page.open( ft.SnackBar( content=ft.Text(f"Error generating flashcards: {str(e)}") ) ) return {"name": "", "description": "", "cards": []} finally: # Remove progress bar if page and progress: page.controls.pop() page.update() def update_flashcard_table(self, flashcards: Dict, page: ft.Page): """Update the flashcard table with the generated flashcards.""" self.flashcards = flashcards # Set the stack name and description self.stack_name.value = flashcards.get("name", "") self.stack_description.value = flashcards.get("description", "") # Clear the existing rows self.flashcard_table.rows.clear() # Add a row for each flashcard for card in flashcards.get("cards", []): if isinstance(card, dict): for front, back in card.items(): self.add_row_to_table(front, back) else: # Handle case where card is a string with format "front: back" if ": " in card: front, back = card.split(": ", 1) self.add_row_to_table(front, back) # Add a row for adding new flashcards self.add_new_card_row() # Update the page page.update() def add_row_to_table(self, front, back): """Add a row to the flashcard table.""" # Calculate width to make text fields take approximately 40% of screen width each text_field_width = 400 # Adjust this value based on typical screen width front_field = ft.TextField( value=front, width=text_field_width, multiline=True, # Allow multiple lines for longer text min_lines=1, max_lines=3, ) back_field = ft.TextField( value=back, width=text_field_width, multiline=True, # Allow multiple lines for longer text min_lines=1, max_lines=3, ) delete_button = ft.IconButton( icon=ft.icons.DELETE, on_click=lambda e, f=front_field, b=back_field: self.delete_flashcard( e, f, b ), ) self.flashcard_table.rows.append( ft.DataRow( cells=[ ft.DataCell(front_field), ft.DataCell(back_field), ft.DataCell(delete_button), ] ) ) def add_new_card_row(self): """Add a row for adding new flashcards.""" # Calculate width to make text fields take approximately 40% of screen width each text_field_width = 400 # Adjust this value based on typical screen width new_front_field = ft.TextField( hint_text="New Front", width=text_field_width, multiline=True, min_lines=1, max_lines=3, ) new_back_field = ft.TextField( hint_text="New Back", width=text_field_width, multiline=True, min_lines=1, max_lines=3, ) add_button = ft.IconButton( icon=ft.icons.ADD, on_click=lambda e: self.add_flashcard(e, new_front_field, new_back_field), ) self.flashcard_table.rows.append( ft.DataRow( cells=[ ft.DataCell(new_front_field), ft.DataCell(new_back_field), ft.DataCell(add_button), ] ) ) def delete_flashcard(self, e, front_field, back_field): """Delete a flashcard from the table.""" for i, row in enumerate(self.flashcard_table.rows): if ( row.cells[0].content == front_field and row.cells[1].content == back_field ): self.flashcard_table.rows.pop(i) e.page.update() return def add_flashcard(self, e, front_field, back_field): """Add a new flashcard to the table.""" if not front_field.value or not back_field.value: e.page.open( ft.SnackBar(content=ft.Text("Please enter both front and back values")) ) e.page.update() return # remove add_new_card row if self.flashcard_table.rows: self.flashcard_table.rows.pop() # add card to table self.add_row_to_table(front_field.value, back_field.value) # re-add add_new_card_row to the end self.add_new_card_row() front_field.value = "" back_field.value = "" e.page.update() def add_to_anki(self, e): """Add the flashcards to Anki using AnkiConnect.""" # Collect the flashcards from the table cards = [] for row in self.flashcard_table.rows[:-1]: # Exclude the last row (add row) front = row.cells[0].content.value back = row.cells[1].content.value if front and back: cards.append({"front": front, "back": back}) if not cards: e.page.open(ft.SnackBar(content=ft.Text("No flashcards to add"))) return # Create a dialog for selecting the Anki deck decks = self.get_anki_decks() if not decks: e.page.open( ft.SnackBar( content=ft.Text("Failed to connect to Anki or no decks found") ) ) e.page.snack_bar.open = True e.page.update() return deck_dropdown = ft.Dropdown( label="Select Deck", options=[ft.dropdown.Option(deck) for deck in decks], width=300, ) def add_cards(e): selected_deck = deck_dropdown.value if not selected_deck: e.page.open(ft.SnackBar(content=ft.Text("Please select a deck"))) return # Close the deck selection dialog first e.page.close(dialog) e.page.update() # Update to ensure dialog is closed visually # Show a progress indicator while adding cards progress_ring = ft.ProgressRing(width=16, height=16, stroke_width=2) progress_snackbar = ft.SnackBar( content=ft.Row( [ progress_ring, ft.Text(f"Adding cards to {selected_deck}... Please wait."), ] ), open=True, duration=None, # Keep open until manually closed or replaced ) e.page.open(progress_snackbar) e.page.update() try: result = self.create_anki_cards(selected_deck, cards) # Close the progress snackbar e.page.close(progress_snackbar) e.page.update() if result.is_ok(): successful_count, failed_cards_list = result.ok_value total_attempted = len(cards) if not failed_cards_list: # All cards added successfully e.page.open( ft.SnackBar( content=ft.Text( f"Successfully added {successful_count}/{total_attempted} flashcards to {selected_deck}" ), open=True, ) ) else: # Some cards failed, show the failure dialog logger.warning( f"Failed to add {len(failed_cards_list)} cards. Opening retry dialog." ) self.show_failed_cards_dialog( e.page, selected_deck, failed_cards_list, successful_count, total_attempted, ) else: # Handle Err case from create_anki_cards e.page.open( ft.SnackBar( content=ft.Text( f"An error occurred while adding cards to Anki: {result.err_value}" ), open=True, ) ) except Exception as ex: # Catch any unexpected errors during the process # Close the progress snackbar in case of an error e.page.close(progress_snackbar) e.page.update() logger.error( f"Unexpected error during Anki card addition: {ex}", exc_info=True ) e.page.open( ft.SnackBar( content=ft.Text(f"An unexpected error occurred: {ex}"), open=True, ) ) dialog = ft.AlertDialog( title=ft.Text("Add to Anki"), content=ft.Column([deck_dropdown], width=400, height=100), actions=[ ft.TextButton( "Cancel", on_click=lambda e: setattr(dialog, "open", False) ), ft.TextButton("Add", on_click=add_cards), ], ) e.page.open(dialog) e.page.update() def get_anki_decks(self) -> List[str]: """Get the list of decks from Anki using AnkiConnect.""" result = self.anki.get_decks() if result.is_ok(): return result.ok_value else: logger.error(f"Error getting Anki decks: {result.err_value}") return [] def create_anki_cards( self, deck: str, cards: List[Dict[str, str]] ) -> Result[None, AnkiError]: """Create flashcards in Anki using Anki-connect.""" result = self.anki.create_cards( deck, cards, self.source_language, self.target_language ) if result.is_err(): logger.error(f"Error creating Anki cards: {result.err_value}") return result def export_as_yaml(self, e): """Export the flashcards as a YAML file.""" # Collect the flashcards from the table cards = [] for row in self.flashcard_table.rows[:-1]: # Exclude the last row (add row) front = row.cells[0].content.value back = row.cells[1].content.value if front and back: cards.append({front: back}) if not cards: e.page.open(ft.SnackBar(content=ft.Text("No flashcards to export"))) return # Create the YAML content yaml_content = { "name": self.stack_name.value, "description": self.stack_description.value, "cards": cards, } # Create a file picker def save_file(e: ft.FilePickerResultEvent): if e.path: try: with open(e.path, "w", encoding="utf-8") as f: yaml.dump(yaml_content, f, allow_unicode=True) e.page.open(ft.SnackBar(content=ft.Text(f"Saved to {e.path}"))) except Exception as ex: e.page.open( ft.SnackBar(content=ft.Text(f"Error saving file: {ex}")) ) self.file_picker.on_result = save_file e.page.update() self.file_picker.save_file( file_name=f"{self.stack_name.value or 'flashcards'}.yaml", file_type=ft.FilePickerFileType.CUSTOM, allowed_extensions=["yaml"], ) def export_as_csv(self, e): """Export the flashcards as a CSV file.""" # Collect the flashcards from the table cards = [] for row in self.flashcard_table.rows[:-1]: # Exclude the last row (add row) front = row.cells[0].content.value back = row.cells[1].content.value if front and back: cards.append([front, back]) if not cards: e.page.open(ft.SnackBar(content=ft.Text("No flashcards to export"))) return # Create a file picker def save_file(e: ft.FilePickerResultEvent): if e.path: try: with open(e.path, "w", encoding="utf-8", newline="") as f: writer = csv.writer(f) writer.writerow(["Front", "Back"]) writer.writerows(cards) e.page.open(ft.SnackBar(content=ft.Text(f"Saved to {e.path}"))) except Exception as ex: e.page.open( ft.SnackBar(content=ft.Text(f"Error saving file: {ex}")) ) self.file_picker.on_result = save_file e.page.update() self.file_picker.save_file( file_name=f"{self.stack_name.value or 'flashcards'}.csv", file_type=ft.FilePickerFileType.CUSTOM, allowed_extensions=["csv"], ) # --- Methods for Handling Failed Anki Card Additions --- def _build_failed_card_row( self, page: ft.Page, card_data: Dict[str, Any] ) -> ft.Row: """Builds a Flet Row for displaying a failed card with editable fields.""" front_field = ft.TextField( value=card_data["card"]["front"], width=300, multiline=True, min_lines=1, max_lines=3, ) back_field = ft.TextField( value=card_data["card"]["back"], width=300, multiline=True, min_lines=1, max_lines=3, ) error_text = ft.Text( f"Error: {card_data['error']}", color=ft.colors.ERROR, size=12 ) # Store references to the fields within the row for later access row = ft.Row( controls=[ ft.Column([front_field], width=310), # Column to constrain width ft.Column([back_field], width=310), # Column to constrain width ft.Column( [error_text], expand=True, alignment=ft.MainAxisAlignment.CENTER ), ], alignment=ft.MainAxisAlignment.START, ) # Attach fields and original data to the row object itself for easy retrieval row.front_field = front_field row.back_field = back_field row.card_data = card_data return row def show_failed_cards_dialog( self, page: ft.Page, deck: str, failed_cards: List[Dict[str, Any]], initial_success_count: int, total_attempted: int, ): """Displays a dialog with failed Anki cards, allowing editing and retrying.""" failed_card_rows = [ self._build_failed_card_row(page, card_data) for card_data in failed_cards ] # Use ListView for scrollability if many cards fail content_list = ft.ListView(controls=failed_card_rows, expand=True, spacing=10) dialog_title = ft.Text(f"Failed Cards for Deck: {deck}") status_text = ft.Text( f"{initial_success_count}/{total_attempted} added initially. {len(failed_cards)} failed." ) # --- Retry Logic --- def _retry_failed_cards(e): cards_to_retry = [] for ( row ) in content_list.controls: # Access controls directly from the ListView if hasattr(row, "front_field") and hasattr( row, "back_field" ): # Ensure it's a card row cards_to_retry.append( {"front": row.front_field.value, "back": row.back_field.value} ) if not cards_to_retry: page.open( ft.SnackBar( content=ft.Text("No cards remaining to retry."), open=True ) ) return # Disable button during retry retry_button.disabled = True page.update() # Show progress in dialog (optional, could use snackbar again) # status_text.value = "Retrying..." # page.update() try: retry_result = self.create_anki_cards(deck, cards_to_retry) if retry_result.is_ok(): retry_successful_count, still_failed_list = retry_result.ok_value new_total_successful = ( initial_success_count + retry_successful_count ) # Update the dialog content: keep only the ones that *still* failed new_failed_rows = [] still_failed_cards_data = { tuple(fc["card"].items()): fc for fc in still_failed_list } # For quick lookup updated_failed_cards_list = [] # Keep track of data for next retry for row in content_list.controls: if hasattr(row, "card_data"): original_card_tuple = tuple(row.card_data["card"].items()) if original_card_tuple in still_failed_cards_data: # Update error message if it changed (though unlikely for duplicates) row.card_data["error"] = still_failed_cards_data[ original_card_tuple ]["error"] # Rebuild or update the error text in the row if needed # For simplicity, we'll just keep the row new_failed_rows.append(row) updated_failed_cards_list.append(row.card_data) content_list.controls = ( new_failed_rows # Replace controls in ListView ) status_text.value = f"{new_total_successful}/{total_attempted} added. {len(new_failed_rows)} still failing." if not new_failed_rows: # All retried cards were successful! Close dialog. page.close(dialog) page.open( ft.SnackBar( content=ft.Text( f"All cards successfully added! ({new_total_successful}/{total_attempted})" ), open=True, ) ) else: # Some still failed, update dialog view page.open( ft.SnackBar( content=ft.Text( f"Successfully retried {retry_successful_count} cards. {len(new_failed_rows)} still failed." ), open=True, ) ) else: # Handle Err from retry attempt status_text.value = f"Retry Error: {retry_result.err_value}" page.open( ft.SnackBar( content=ft.Text( f"Error during retry: {retry_result.err_value}" ), open=True, ) ) except Exception as ex: logger.error( f"Unexpected error during Anki card retry: {ex}", exc_info=True ) status_text.value = f"Unexpected Retry Error: {ex}" page.open( ft.SnackBar( content=ft.Text( f"An unexpected error occurred during retry: {ex}" ), open=True, ) ) finally: # Re-enable button retry_button.disabled = ( False if content_list.controls else True ) # Disable if list is empty page.update() # Update UI (dialog content, status text, button state) # --- Dialog Buttons --- retry_button = ft.TextButton( "Retry Failed Cards", on_click=_retry_failed_cards, disabled=not failed_card_rows, ) close_button = ft.TextButton("Close", on_click=lambda e: page.close(dialog)) dialog = ft.AlertDialog( modal=True, title=dialog_title, content=ft.Container( # Use Container for better sizing control content=ft.Column([status_text, content_list], tight=True), width=max(page.width * 0.8, 700), # Make dialog wider height=max(page.height * 0.7, 400), # Make dialog taller padding=10, ), actions=[retry_button, close_button], actions_alignment=ft.MainAxisAlignment.END, ) page.open(dialog) page.update() def main(page: ft.Page): app = FlashcardCreator() app.build_ui(page) if __name__ == "__main__": ft.app(target=main)