feat: script basically done
This commit is contained in:
		
							
								
								
									
										1
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							@ -8,3 +8,4 @@ wheels/
 | 
			
		||||
 | 
			
		||||
# Virtual environments
 | 
			
		||||
.venv
 | 
			
		||||
.aider*
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										25
									
								
								DesignRequirements.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										25
									
								
								DesignRequirements.md
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,25 @@
 | 
			
		||||
That python script should help users take a block of text / vocab list / whatever in any format from a book, website or such, input it, parse and understand it with an llm, create flashcards from source langauge (dropdown) to target language, and allows adding these with ankiconnect. 
 | 
			
		||||
 The script should have an UI for the following: 
 | 
			
		||||
 | 
			
		||||
- Have a dropdown for source and a dropdown for target language
 | 
			
		||||
- have a settings window for choosing the model and provider (litellm) and adding API keys. Stored locally, shared prefs or something? Or local folders?
 | 
			
		||||
- Have an input window for inputting text
 | 
			
		||||
- Create Anki flashcards from the text (by making the model output yaml in this format: 
 | 
			
		||||
 | 
			
		||||
```yaml
 | 
			
		||||
name: <stack name>
 | 
			
		||||
description: <short description of what the cards are about>
 | 
			
		||||
cards: 
 | 
			
		||||
  - front: back
 | 
			
		||||
  - front2: back2
 | 
			
		||||
  - the suspects: der/die verdächtige -n
 | 
			
		||||
  - ...
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
The UI should then show these flashcards in a datatable one by one, allow changing, modifying, and / or deleting / adding flashcards. Once that's done, there should be an "add to anki" button that adds the cards to a deck of the user's choice using a local instance of Anki running AnkiConnect. Also export as yaml, and csv
 | 
			
		||||
 | 
			
		||||
## Tooling 
 | 
			
		||||
I always use uv to install stuff, not pip
 | 
			
		||||
 | 
			
		||||
use `uv add <package>` to install instead of `pip install <package>`.
 | 
			
		||||
use `uv run <command>` to run commands, e.g. `uv run python hello.py`.
 | 
			
		||||
							
								
								
									
										728
									
								
								main.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										728
									
								
								main.py
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,728 @@
 | 
			
		||||
import flet as ft
 | 
			
		||||
import json
 | 
			
		||||
import requests
 | 
			
		||||
import yaml
 | 
			
		||||
import os
 | 
			
		||||
import csv
 | 
			
		||||
import logging
 | 
			
		||||
from typing import Dict, List, Any
 | 
			
		||||
 | 
			
		||||
# 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):
 | 
			
		||||
        # AnkiConnect configuration
 | 
			
		||||
        self.anki_connect_url = "http://127.0.0.1:8765"
 | 
			
		||||
 | 
			
		||||
        # File picker
 | 
			
		||||
        self.file_picker = ft.FilePicker()
 | 
			
		||||
 | 
			
		||||
        # 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)
 | 
			
		||||
                    self.api_key = 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
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        # Wrap the table in a Container with specific height to ensure visibility
 | 
			
		||||
        flashcard_table_container = ft.Container(
 | 
			
		||||
            content=ft.Column(
 | 
			
		||||
                expand=True,
 | 
			
		||||
                controls=[self.flashcard_table],
 | 
			
		||||
                scroll=ft.ScrollMode.ALWAYS,
 | 
			
		||||
            ),
 | 
			
		||||
            padding=10,
 | 
			
		||||
            expand=True,
 | 
			
		||||
            # height=400,
 | 
			
		||||
            border=ft.border.all(1, ft.colors.OUTLINE),
 | 
			
		||||
            border_radius=5,
 | 
			
		||||
            width=page.width,
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        # Add file picker to page overlay
 | 
			
		||||
        page.overlay.append(self.file_picker)
 | 
			
		||||
 | 
			
		||||
        # Update the page.add call to include the container instead of table directly
 | 
			
		||||
        page.add(
 | 
			
		||||
            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,  # Use the container instead of direct table
 | 
			
		||||
            ft.Row(
 | 
			
		||||
                [
 | 
			
		||||
                    self.add_to_anki_button,
 | 
			
		||||
                    self.export_yaml_button,
 | 
			
		||||
                    self.export_csv_button,
 | 
			
		||||
                ]
 | 
			
		||||
            ),
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    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)
 | 
			
		||||
 | 
			
		||||
            with open(os.path.join(settings_dir, "settings.json"), "w") as f:
 | 
			
		||||
                print(self.base_url)
 | 
			
		||||
                json.dump(
 | 
			
		||||
                    {
 | 
			
		||||
                        "provider": self.llm_provider,
 | 
			
		||||
                        "model": self.llm_model,
 | 
			
		||||
                        "base_url": self.base_url,
 | 
			
		||||
                        "api_key": self.api_key,
 | 
			
		||||
                    },
 | 
			
		||||
                    f,
 | 
			
		||||
                )
 | 
			
		||||
 | 
			
		||||
            e.close(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)
 | 
			
		||||
                    self.llm_model = settings.get("model", self.llm_model)
 | 
			
		||||
                    self.base_url = settings.get("base_url", self.base_url)
 | 
			
		||||
                    self.api_key = settings.get("api_key", self.api_key)
 | 
			
		||||
                except:
 | 
			
		||||
                    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"),
 | 
			
		||||
            ],
 | 
			
		||||
            value=self.llm_provider,
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        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.dialog = dialog
 | 
			
		||||
        # dialog.open = True
 | 
			
		||||
        e.page.update()
 | 
			
		||||
 | 
			
		||||
    def on_process_text(self, e):
 | 
			
		||||
        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")
 | 
			
		||||
 | 
			
		||||
        # 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)
 | 
			
		||||
 | 
			
		||||
    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=600)
 | 
			
		||||
            page.add(
 | 
			
		||||
                ft.Column([ft.Text("Generating flashcards...", 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
 | 
			
		||||
 | 
			
		||||
    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
 | 
			
		||||
            os.environ[f"{self.llm_provider.upper()}_API_KEY"] = self.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)
 | 
			
		||||
            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
 | 
			
		||||
 | 
			
		||||
        # Add a new row with the entered values
 | 
			
		||||
        self.add_row_to_table(front_field.value, back_field.value)
 | 
			
		||||
 | 
			
		||||
        # Clear the input fields
 | 
			
		||||
        front_field.value = ""
 | 
			
		||||
        back_field.value = ""
 | 
			
		||||
 | 
			
		||||
        # Update the page
 | 
			
		||||
        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
 | 
			
		||||
 | 
			
		||||
            success = self.create_anki_cards(selected_deck, cards)
 | 
			
		||||
            e.page.close(dialog)
 | 
			
		||||
            e.page.update()
 | 
			
		||||
 | 
			
		||||
            if success:
 | 
			
		||||
                e.page.open(
 | 
			
		||||
                    ft.SnackBar(
 | 
			
		||||
                        content=ft.Text(
 | 
			
		||||
                            f"Added {len(cards)} flashcards to {selected_deck}"
 | 
			
		||||
                        )
 | 
			
		||||
                    )
 | 
			
		||||
                )
 | 
			
		||||
            else:
 | 
			
		||||
                e.page.open(
 | 
			
		||||
                    ft.SnackBar(content=ft.Text("Failed to add flashcards to Anki"))
 | 
			
		||||
                )
 | 
			
		||||
 | 
			
		||||
        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."""
 | 
			
		||||
        try:
 | 
			
		||||
            response = requests.post(
 | 
			
		||||
                self.anki_connect_url, json={"action": "deckNames", "version": 6}
 | 
			
		||||
            )
 | 
			
		||||
            data = response.json()
 | 
			
		||||
            return data.get("result", [])
 | 
			
		||||
        except Exception as e:
 | 
			
		||||
            print(f"Error getting Anki decks: {e}")
 | 
			
		||||
            return []
 | 
			
		||||
 | 
			
		||||
    def create_anki_cards(self, deck: str, cards: List[Dict[str, str]]) -> bool:
 | 
			
		||||
        """Create flashcards in Anki using AnkiConnect."""
 | 
			
		||||
        try:
 | 
			
		||||
            # Prepare notes for addition
 | 
			
		||||
            notes = []
 | 
			
		||||
            for card in cards:
 | 
			
		||||
                notes.append(
 | 
			
		||||
                    {
 | 
			
		||||
                        "deckName": deck,
 | 
			
		||||
                        "modelName": "Basic",  # Using the basic model
 | 
			
		||||
                        "fields": {"Front": card["front"], "Back": card["back"]},
 | 
			
		||||
                        "options": {"allowDuplicate": False},
 | 
			
		||||
                        "tags": [f"{self.source_language}-{self.target_language}"],
 | 
			
		||||
                    }
 | 
			
		||||
                )
 | 
			
		||||
 | 
			
		||||
            # Add notes to Anki
 | 
			
		||||
            response = requests.post(
 | 
			
		||||
                self.anki_connect_url,
 | 
			
		||||
                json={"action": "addNotes", "version": 6, "params": {"notes": notes}},
 | 
			
		||||
            )
 | 
			
		||||
            data = response.json()
 | 
			
		||||
            return all(id is not None for id in data.get("result", []))
 | 
			
		||||
        except Exception as e:
 | 
			
		||||
            print(f"Error creating Anki cards: {e}")
 | 
			
		||||
            return False
 | 
			
		||||
 | 
			
		||||
    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"],
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def main(page: ft.Page):
 | 
			
		||||
    app = FlashcardCreator()
 | 
			
		||||
    app.build_ui(page)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
if __name__ == "__main__":
 | 
			
		||||
    ft.app(target=main)
 | 
			
		||||
@ -4,4 +4,8 @@ version = "0.1.0"
 | 
			
		||||
description = "Add your description here"
 | 
			
		||||
readme = "README.md"
 | 
			
		||||
requires-python = ">=3.12"
 | 
			
		||||
dependencies = []
 | 
			
		||||
dependencies = [
 | 
			
		||||
    "flet[all]>=0.27.6",
 | 
			
		||||
    "litellm>=1.63.12",
 | 
			
		||||
    "logging>=0.4.9.6",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
		Reference in New Issue
	
	Block a user