feat: script basically done
This commit is contained in:
parent
a441e9bf8a
commit
28492688a5
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",
|
||||
]
|
||||
|
Loading…
Reference in New Issue
Block a user