feat: more functionality and suchh
This commit is contained in:
parent
5d9139bafb
commit
a6f4cef792
@ -23,3 +23,20 @@ I always use uv to install stuff, not pip
|
|||||||
|
|
||||||
use `uv add <package>` to install instead of `pip install <package>`.
|
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`.
|
use `uv run <command>` to run commands, e.g. `uv run python hello.py`.
|
||||||
|
|
||||||
|
|
||||||
|
## Coding
|
||||||
|
|
||||||
|
For dialogs and snackbars, do the following:
|
||||||
|
|
||||||
|
```python
|
||||||
|
dialog = < create dialog >
|
||||||
|
|
||||||
|
# to show
|
||||||
|
page.open(dialog)
|
||||||
|
|
||||||
|
# to close
|
||||||
|
page.close(dialog)
|
||||||
|
```
|
||||||
|
|
||||||
|
Do NOT use `page.show_snack_bar or page.dialog = ...` as those don't exist in recent Flet versions anymore.
|
108
anki.py
108
anki.py
@ -1,32 +1,44 @@
|
|||||||
from result import Result, Ok, Err
|
from result import Result, Ok, Err
|
||||||
import requests
|
import requests
|
||||||
import logging
|
import logging
|
||||||
from typing import List, Dict
|
from typing import List, Dict, Tuple, Any
|
||||||
|
|
||||||
logger = logging.getLogger("flashcard-creator")
|
logger = logging.getLogger("flashcard-creator")
|
||||||
|
|
||||||
|
|
||||||
class AnkiError(Exception):
|
class AnkiError(Exception):
|
||||||
"""Base class for Anki-related errors"""
|
"""Base class for Anki-related errors"""
|
||||||
|
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
class ConnectionError(AnkiError):
|
class ConnectionError(AnkiError):
|
||||||
"""Error connecting to AnkiConnect"""
|
"""Error connecting to AnkiConnect"""
|
||||||
|
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
class DeckError(AnkiError):
|
class DeckError(AnkiError):
|
||||||
"""Error related to deck operations"""
|
"""Error related to deck operations"""
|
||||||
|
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
class CardError(AnkiError):
|
class CardError(AnkiError):
|
||||||
"""Error related to card operations"""
|
"""Error related to card operations"""
|
||||||
|
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
class AnkiConnect:
|
class AnkiConnect:
|
||||||
def __init__(self, url="http://127.0.0.1:8765"):
|
def __init__(self, url="http://127.0.0.1:8765"):
|
||||||
self.url = url
|
self.url = url
|
||||||
|
|
||||||
def get_decks(self) -> Result[List[str], AnkiError]:
|
def get_decks(self) -> Result[List[str], AnkiError]:
|
||||||
"""Get the list of decks from Anki using AnkiConnect."""
|
"""
|
||||||
|
Get the list of decks from Anki using AnkiConnect.
|
||||||
|
Returns Ok(list_of_deck_names) on success, Err(AnkiError) on failure.
|
||||||
|
"""
|
||||||
try:
|
try:
|
||||||
response = requests.post(
|
response = requests.post(
|
||||||
self.url, json={"action": "deckNames", "version": 6}
|
self.url, json={"action": "deckNames", "version": 6}
|
||||||
@ -40,37 +52,71 @@ class AnkiConnect:
|
|||||||
logger.error(f"Error getting Anki decks: {e}")
|
logger.error(f"Error getting Anki decks: {e}")
|
||||||
return Err(DeckError(f"Failed to get decks: {e}"))
|
return Err(DeckError(f"Failed to get decks: {e}"))
|
||||||
|
|
||||||
def create_cards(self, deck: str, cards: List[Dict[str, str]]) -> Result[bool, AnkiError]:
|
def create_cards(
|
||||||
"""Create flashcards in Anki using AnkiConnect."""
|
self,
|
||||||
try:
|
deck: str,
|
||||||
# Prepare notes for addition
|
cards: List[Dict[str, str]],
|
||||||
notes = []
|
source_language: str,
|
||||||
logger.info(f"Preparing to create {len(cards)} Anki cards in deck: {deck}")
|
target_language: str,
|
||||||
for card in cards:
|
) -> Result[Tuple[int, List[Dict[str, Any]]], AnkiError]:
|
||||||
notes.append(
|
"""Create flashcards in Anki using AnkiConnect, adding each note one by one.
|
||||||
{
|
|
||||||
"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
|
Returns:
|
||||||
logger.info("Sending request to AnkiConnect to add notes")
|
Ok((successful_count, failed_cards_list)): Where failed_cards_list contains
|
||||||
response = requests.post(
|
dictionaries like {'card': original_card_dict, 'error': error_message}.
|
||||||
self.url,
|
Err(ConnectionError): If connection to AnkiConnect fails.
|
||||||
json={"action": "addNotes", "version": 6, "params": {"notes": notes}},
|
Err(CardError): If a non-connection error occurs during the process.
|
||||||
)
|
"""
|
||||||
data = response.json()
|
try:
|
||||||
success = all(id is not None for id in data.get("result", []))
|
logger.info(f"Preparing to create {len(cards)} Anki cards in deck: {deck}")
|
||||||
if success:
|
|
||||||
logger.info("Successfully added all cards to Anki")
|
failed_cards = []
|
||||||
return Ok(True)
|
successful_count = 0
|
||||||
|
|
||||||
|
# Add each note individually
|
||||||
|
for card in cards:
|
||||||
|
note = {
|
||||||
|
"deckName": deck,
|
||||||
|
"modelName": "Basic", # Using the basic model
|
||||||
|
"fields": {"Front": card["front"], "Back": card["back"]},
|
||||||
|
"options": {"allowDuplicate": False},
|
||||||
|
"tags": [f"{source_language}-{target_language}"],
|
||||||
|
}
|
||||||
|
|
||||||
|
# Send request for a single note
|
||||||
|
response = requests.post(
|
||||||
|
self.url,
|
||||||
|
json={"action": "addNote", "version": 6, "params": {"note": note}},
|
||||||
|
)
|
||||||
|
data = response.json()
|
||||||
|
|
||||||
|
# Check if the note was added successfully
|
||||||
|
if data.get("error") is not None or data.get("result") is None:
|
||||||
|
error_msg = data.get("error", "Unknown error")
|
||||||
|
failed_cards.append({"card": card, "error": error_msg})
|
||||||
|
logger.warning(
|
||||||
|
f"Failed to add card: {card['front']} - Error: {error_msg}"
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
successful_count += 1
|
||||||
|
logger.debug(f"Successfully added card: {card['front']}")
|
||||||
|
|
||||||
|
# Report results
|
||||||
|
if failed_cards:
|
||||||
|
logger.warning(
|
||||||
|
f"Some cards failed to be added to Anki. Added {successful_count}/{len(cards)}"
|
||||||
|
)
|
||||||
|
elif successful_count > 0:
|
||||||
|
logger.info(f"Successfully added all {successful_count} cards to Anki")
|
||||||
else:
|
else:
|
||||||
logger.warning("Some cards failed to be added to Anki")
|
logger.info(
|
||||||
return Err(CardError("Some cards failed to be added"))
|
"No cards were added."
|
||||||
|
) # Should ideally not happen if input list wasn't empty
|
||||||
|
|
||||||
|
# Always return Ok with success count and failed list (which might be empty)
|
||||||
|
# The caller can decide if the presence of failed_cards constitutes an overall "error" state for the UI
|
||||||
|
return Ok((successful_count, failed_cards))
|
||||||
|
|
||||||
except requests.exceptions.RequestException as e:
|
except requests.exceptions.RequestException as e:
|
||||||
logger.error(f"Connection error creating Anki cards: {e}")
|
logger.error(f"Connection error creating Anki cards: {e}")
|
||||||
return Err(ConnectionError(f"Failed to connect to Anki: {e}"))
|
return Err(ConnectionError(f"Failed to connect to Anki: {e}"))
|
||||||
|
412
main.py
412
main.py
@ -6,7 +6,9 @@ import os
|
|||||||
import csv
|
import csv
|
||||||
import logging
|
import logging
|
||||||
from typing import Dict, List, Any
|
from typing import Dict, List, Any
|
||||||
from result import Ok
|
from result import Ok, Result
|
||||||
|
|
||||||
|
from anki import AnkiError
|
||||||
|
|
||||||
# Set up logging
|
# Set up logging
|
||||||
logging.basicConfig(
|
logging.basicConfig(
|
||||||
@ -19,11 +21,14 @@ class FlashcardCreator:
|
|||||||
def __init__(self):
|
def __init__(self):
|
||||||
# Anki configuration
|
# Anki configuration
|
||||||
from anki import AnkiConnect
|
from anki import AnkiConnect
|
||||||
|
|
||||||
self.anki = AnkiConnect()
|
self.anki = AnkiConnect()
|
||||||
|
|
||||||
# File picker
|
# File picker
|
||||||
self.file_picker = ft.FilePicker()
|
self.file_picker = ft.FilePicker()
|
||||||
|
|
||||||
|
self.is_processing = False
|
||||||
|
|
||||||
# LiteLLM configuration
|
# LiteLLM configuration
|
||||||
self.llm_provider = "openai"
|
self.llm_provider = "openai"
|
||||||
self.llm_model = "gpt-3.5-turbo"
|
self.llm_model = "gpt-3.5-turbo"
|
||||||
@ -144,19 +149,25 @@ class FlashcardCreator:
|
|||||||
width=page.width, # Make table as wide as the window
|
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
|
# Wrap the table in a Container with specific height to ensure visibility
|
||||||
flashcard_table_container = ft.Container(
|
flashcard_table_container = ft.Container(
|
||||||
content=ft.Column(
|
content=ft.Row(
|
||||||
|
controls=[
|
||||||
|
ft.Column(
|
||||||
|
expand=True,
|
||||||
|
controls=[self.flashcard_table],
|
||||||
|
scroll=ft.ScrollMode.ALWAYS,
|
||||||
|
)
|
||||||
|
],
|
||||||
expand=True,
|
expand=True,
|
||||||
controls=[self.flashcard_table],
|
|
||||||
scroll=ft.ScrollMode.ALWAYS,
|
|
||||||
),
|
),
|
||||||
padding=10,
|
padding=10,
|
||||||
expand=True,
|
expand=True,
|
||||||
# height=400,
|
|
||||||
border=ft.border.all(1, ft.colors.OUTLINE),
|
border=ft.border.all(1, ft.colors.OUTLINE),
|
||||||
border_radius=5,
|
border_radius=5,
|
||||||
width=page.width,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
# Add file picker to page overlay
|
# Add file picker to page overlay
|
||||||
@ -230,6 +241,8 @@ class FlashcardCreator:
|
|||||||
"base_url": self.base_url,
|
"base_url": self.base_url,
|
||||||
"api_key": self.api_key,
|
"api_key": self.api_key,
|
||||||
}
|
}
|
||||||
|
# Save the currently selected provider
|
||||||
|
settings["provider"] = self.llm_provider
|
||||||
|
|
||||||
with open(settings_path, "w") as f:
|
with open(settings_path, "w") as f:
|
||||||
json.dump(settings, f, indent=4)
|
json.dump(settings, f, indent=4)
|
||||||
@ -237,6 +250,33 @@ class FlashcardCreator:
|
|||||||
e.page.close(dialog)
|
e.page.close(dialog)
|
||||||
e.page.update()
|
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
|
# Load settings if they exist
|
||||||
settings_path = os.path.join(
|
settings_path = os.path.join(
|
||||||
os.path.expanduser("~"), ".flashcard_creator", "settings.json"
|
os.path.expanduser("~"), ".flashcard_creator", "settings.json"
|
||||||
@ -253,7 +293,8 @@ class FlashcardCreator:
|
|||||||
self.llm_model = provider_settings.get("model", self.llm_model)
|
self.llm_model = provider_settings.get("model", self.llm_model)
|
||||||
self.base_url = provider_settings.get("base_url", self.base_url)
|
self.base_url = provider_settings.get("base_url", self.base_url)
|
||||||
self.api_key = provider_settings.get("api_key", self.api_key)
|
self.api_key = provider_settings.get("api_key", self.api_key)
|
||||||
except:
|
except e:
|
||||||
|
logger.info(f"Settings couldn't be loaded: {e}")
|
||||||
pass
|
pass
|
||||||
|
|
||||||
provider_dropdown = ft.Dropdown(
|
provider_dropdown = ft.Dropdown(
|
||||||
@ -266,6 +307,7 @@ class FlashcardCreator:
|
|||||||
ft.dropdown.Option("deepseek"),
|
ft.dropdown.Option("deepseek"),
|
||||||
],
|
],
|
||||||
value=self.llm_provider,
|
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)
|
model_input = ft.TextField(label="Model Name", value=self.llm_model)
|
||||||
@ -287,11 +329,16 @@ class FlashcardCreator:
|
|||||||
)
|
)
|
||||||
|
|
||||||
e.page.open(dialog)
|
e.page.open(dialog)
|
||||||
# e.page.dialog = dialog
|
|
||||||
# dialog.open = True
|
|
||||||
e.page.update()
|
e.page.update()
|
||||||
|
|
||||||
def on_process_text(self, e):
|
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
|
input_text = self.input_text.value
|
||||||
if not input_text:
|
if not input_text:
|
||||||
e.page.open(
|
e.page.open(
|
||||||
@ -301,11 +348,21 @@ class FlashcardCreator:
|
|||||||
|
|
||||||
logger.info("Processing input text")
|
logger.info("Processing input text")
|
||||||
|
|
||||||
# Use LiteLLM to process the text and generate flashcards
|
self.is_processing = True
|
||||||
flashcards = self.generate_flashcards(input_text, e.page)
|
self.process_button.disabled = True
|
||||||
|
e.page.update()
|
||||||
|
|
||||||
# Update the data table with the generated flashcards
|
try:
|
||||||
self.update_flashcard_table(flashcards, e.page)
|
# 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:
|
def generate_flashcards(self, text: str, page: ft.Page = None) -> Dict:
|
||||||
"""Use LiteLLM to generate flashcards from the input text."""
|
"""Use LiteLLM to generate flashcards from the input text."""
|
||||||
@ -314,9 +371,17 @@ class FlashcardCreator:
|
|||||||
# Show progress bar
|
# Show progress bar
|
||||||
progress = None
|
progress = None
|
||||||
if page:
|
if page:
|
||||||
progress = ft.ProgressBar(width=600)
|
progress = ft.ProgressBar(width=max(page.width - 40, 200))
|
||||||
page.add(
|
page.add(
|
||||||
ft.Column([ft.Text("Generating flashcards...", size=16), progress])
|
ft.Column(
|
||||||
|
[
|
||||||
|
ft.Text(
|
||||||
|
f"Generating flashcards using {self.llm_provider}/{self.llm_model}",
|
||||||
|
size=16,
|
||||||
|
),
|
||||||
|
progress,
|
||||||
|
]
|
||||||
|
)
|
||||||
)
|
)
|
||||||
page.update()
|
page.update()
|
||||||
|
|
||||||
@ -432,6 +497,12 @@ class FlashcardCreator:
|
|||||||
return flashcards
|
return flashcards
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Error generating flashcards: {e}", exc_info=True)
|
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": []}
|
return {"name": "", "description": "", "cards": []}
|
||||||
finally:
|
finally:
|
||||||
# Remove progress bar
|
# Remove progress bar
|
||||||
@ -561,14 +632,19 @@ class FlashcardCreator:
|
|||||||
e.page.update()
|
e.page.update()
|
||||||
return
|
return
|
||||||
|
|
||||||
# Add a new row with the entered values
|
# 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)
|
self.add_row_to_table(front_field.value, back_field.value)
|
||||||
|
|
||||||
# Clear the input fields
|
# re-add add_new_card_row to the end
|
||||||
|
self.add_new_card_row()
|
||||||
|
|
||||||
front_field.value = ""
|
front_field.value = ""
|
||||||
back_field.value = ""
|
back_field.value = ""
|
||||||
|
|
||||||
# Update the page
|
|
||||||
e.page.update()
|
e.page.update()
|
||||||
|
|
||||||
def add_to_anki(self, e):
|
def add_to_anki(self, e):
|
||||||
@ -609,21 +685,80 @@ class FlashcardCreator:
|
|||||||
e.page.open(ft.SnackBar(content=ft.Text("Please select a deck")))
|
e.page.open(ft.SnackBar(content=ft.Text("Please select a deck")))
|
||||||
return
|
return
|
||||||
|
|
||||||
success = self.create_anki_cards(selected_deck, cards)
|
# Close the deck selection dialog first
|
||||||
e.page.close(dialog)
|
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()
|
e.page.update()
|
||||||
|
|
||||||
if success:
|
try:
|
||||||
e.page.open(
|
result = self.create_anki_cards(selected_deck, cards)
|
||||||
ft.SnackBar(
|
|
||||||
content=ft.Text(
|
# Close the progress snackbar
|
||||||
f"Added {len(cards)} flashcards to {selected_deck}"
|
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
|
||||||
)
|
)
|
||||||
else:
|
|
||||||
e.page.open(
|
e.page.open(
|
||||||
ft.SnackBar(content=ft.Text("Failed to add flashcards to Anki"))
|
ft.SnackBar(
|
||||||
|
content=ft.Text(f"An unexpected error occurred: {ex}"),
|
||||||
|
open=True,
|
||||||
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
dialog = ft.AlertDialog(
|
dialog = ft.AlertDialog(
|
||||||
@ -643,20 +778,22 @@ class FlashcardCreator:
|
|||||||
def get_anki_decks(self) -> List[str]:
|
def get_anki_decks(self) -> List[str]:
|
||||||
"""Get the list of decks from Anki using AnkiConnect."""
|
"""Get the list of decks from Anki using AnkiConnect."""
|
||||||
result = self.anki.get_decks()
|
result = self.anki.get_decks()
|
||||||
if isinstance(result, Ok):
|
if result.is_ok():
|
||||||
return result.value
|
return result.ok_value
|
||||||
else:
|
else:
|
||||||
logger.error(f"Error getting Anki decks: {result.value}")
|
logger.error(f"Error getting Anki decks: {result.err_value}")
|
||||||
return []
|
return []
|
||||||
|
|
||||||
def create_anki_cards(self, deck: str, cards: List[Dict[str, str]]) -> bool:
|
def create_anki_cards(
|
||||||
|
self, deck: str, cards: List[Dict[str, str]]
|
||||||
|
) -> Result[None, AnkiError]:
|
||||||
"""Create flashcards in Anki using Anki-connect."""
|
"""Create flashcards in Anki using Anki-connect."""
|
||||||
result = self.anki.create_cards(deck, cards)
|
result = self.anki.create_cards(
|
||||||
if isinstance(result, Ok):
|
deck, cards, self.source_language, self.target_language
|
||||||
return result.value
|
)
|
||||||
else:
|
if result.is_err():
|
||||||
logger.error(f"Error creating Anki cards: {result.value}")
|
logger.error(f"Error creating Anki cards: {result.err_value}")
|
||||||
return False
|
return result
|
||||||
|
|
||||||
def export_as_yaml(self, e):
|
def export_as_yaml(self, e):
|
||||||
"""Export the flashcards as a YAML file."""
|
"""Export the flashcards as a YAML file."""
|
||||||
@ -735,6 +872,209 @@ class FlashcardCreator:
|
|||||||
allowed_extensions=["csv"],
|
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):
|
def main(page: ft.Page):
|
||||||
app = FlashcardCreator()
|
app = FlashcardCreator()
|
||||||
|
@ -7,5 +7,5 @@ requires-python = ">=3.12"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"flet[all]>=0.27.6",
|
"flet[all]>=0.27.6",
|
||||||
"litellm>=1.63.12",
|
"litellm>=1.63.12",
|
||||||
"logging>=0.4.9.6",
|
"result>=0.17.0",
|
||||||
]
|
]
|
||||||
|
19
uv.lock
19
uv.lock
@ -574,12 +574,6 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/e4/01/6f2fb287b93144851bfd442b265a67933bbd677e6dcbfa9951063199cf94/litellm-1.63.12-py3-none-any.whl", hash = "sha256:ae72a9d7099100b4b1172aaa2954bf6d7b205d47ba76beec5cd53f62dd57913e", size = 6954945 },
|
{ url = "https://files.pythonhosted.org/packages/e4/01/6f2fb287b93144851bfd442b265a67933bbd677e6dcbfa9951063199cf94/litellm-1.63.12-py3-none-any.whl", hash = "sha256:ae72a9d7099100b4b1172aaa2954bf6d7b205d47ba76beec5cd53f62dd57913e", size = 6954945 },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "logging"
|
|
||||||
version = "0.4.9.6"
|
|
||||||
source = { registry = "https://pypi.org/simple" }
|
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/93/4b/979db9e44be09f71e85c9c8cfc42f258adfb7d93ce01deed2788b2948919/logging-0.4.9.6.tar.gz", hash = "sha256:26f6b50773f085042d301085bd1bf5d9f3735704db9f37c1ce6d8b85c38f2417", size = 96029 }
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "markdown-it-py"
|
name = "markdown-it-py"
|
||||||
version = "3.0.0"
|
version = "3.0.0"
|
||||||
@ -646,14 +640,14 @@ source = { virtual = "." }
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "flet", extra = ["all"] },
|
{ name = "flet", extra = ["all"] },
|
||||||
{ name = "litellm" },
|
{ name = "litellm" },
|
||||||
{ name = "logging" },
|
{ name = "result" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[package.metadata]
|
[package.metadata]
|
||||||
requires-dist = [
|
requires-dist = [
|
||||||
{ name = "flet", extras = ["all"], specifier = ">=0.27.6" },
|
{ name = "flet", extras = ["all"], specifier = ">=0.27.6" },
|
||||||
{ name = "litellm", specifier = ">=1.63.12" },
|
{ name = "litellm", specifier = ">=1.63.12" },
|
||||||
{ name = "logging", specifier = ">=0.4.9.6" },
|
{ name = "result", specifier = ">=0.17.0" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -1027,6 +1021,15 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/f9/9b/335f9764261e915ed497fcdeb11df5dfd6f7bf257d4a6a2a686d80da4d54/requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6", size = 64928 },
|
{ url = "https://files.pythonhosted.org/packages/f9/9b/335f9764261e915ed497fcdeb11df5dfd6f7bf257d4a6a2a686d80da4d54/requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6", size = 64928 },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "result"
|
||||||
|
version = "0.17.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/a3/47/2175be65744aa4d8419c27bd3a7a7d65af5bcad7a4dc6a812c00778754f0/result-0.17.0.tar.gz", hash = "sha256:b73da420c0cb1a3bf741dbd41ff96dedafaad6a1b3ef437a9e33e380bb0d91cf", size = 20180 }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e2/90/19110ce9374c3db619e2df0816f2c58e4ddc5cdad5f7284cd81d8b30b7cb/result-0.17.0-py3-none-any.whl", hash = "sha256:49fd668b4951ad15800b8ccefd98b6b94effc789607e19c65064b775570933e8", size = 11689 },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rich"
|
name = "rich"
|
||||||
version = "13.9.4"
|
version = "13.9.4"
|
||||||
|
Loading…
Reference in New Issue
Block a user